From 2d682a6115fba79f57536632c86b4ceeac66be6c Mon Sep 17 00:00:00 2001 From: kendfss Date: Sat, 20 Feb 2021 02:28:03 +0100 Subject: [PATCH] first commit --- LICENSE | 19 + README.md | 26 + filey/README.md | Bin 0 -> 24 bytes filey/__init__.py | 605 ++++++++++++++++++ filey/preamble/_parseconfigurationscunt.py | 7 + filey/preamble/filesystem.py | 380 +++++++++++ filey/preamble/filesystem2inheritance.py | 366 +++++++++++ filey/preamble/filesystem3pathlibbing.py | 54 ++ .../filesystem4-pathsarentinheritable.py | 344 ++++++++++ filey/preamble/filesystem4_2.py | 562 ++++++++++++++++ filey/preamble/magnitudes.py | 122 ++++ filey/preamble/utils.py | 24 + filey/testdir/testfile.ext | 0 filey/testdir/testfile_2.ext | 0 filey/testdir/testfile_3.ext | 0 filey/utils/__init__.py | 4 + filey/utils/debug.py | 99 +++ filey/utils/magnitudes.py | 100 +++ filey/utils/media.py | 191 ++++++ filey/utils/strings.py | 123 ++++ pyproject.toml | 6 + setup.py | 32 + 22 files changed, 3064 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 filey/README.md create mode 100644 filey/__init__.py create mode 100644 filey/preamble/_parseconfigurationscunt.py create mode 100644 filey/preamble/filesystem.py create mode 100644 filey/preamble/filesystem2inheritance.py create mode 100644 filey/preamble/filesystem3pathlibbing.py create mode 100644 filey/preamble/filesystem4-pathsarentinheritable.py create mode 100644 filey/preamble/filesystem4_2.py create mode 100644 filey/preamble/magnitudes.py create mode 100644 filey/preamble/utils.py create mode 100644 filey/testdir/testfile.ext create mode 100644 filey/testdir/testfile_2.ext create mode 100644 filey/testdir/testfile_3.ext create mode 100644 filey/utils/__init__.py create mode 100644 filey/utils/debug.py create mode 100644 filey/utils/magnitudes.py create mode 100644 filey/utils/media.py create mode 100644 filey/utils/strings.py create mode 100644 pyproject.toml create mode 100644 setup.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a318114 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 eli2and40 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..73c67b0 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# sl4ng + +This package serves as a workflow booster for emailing, iterating, and a veritable smorgasboard of cases scattered in between. + +You want persistant data but Pickle and Dill ask too many damned questions? + sl4ng.persistance.save + sl4ng.persistance.load + +You be writing generators so nice that you want to use them twice, thrice, or indefinitely? We gotchu! + sl4ng.types.regenerator + +You're on the brink of the creative breakthrough of the ages but you'll end it all in a fit of rage if you accidentally overwite your projects ever again? We gotchu! + sl4ng.files.paths.namespacer + +You want to look at an object's source code but inspect.getsource makes a mess of rendering things in your REPL, or perhaps you want to jump straight into its package-folder or source-file? + sl4ng.debug.getsource + sl4ng.debug.pop + +You want to want to see if your iterable is the codomain of a constant function? We gotchu + sl4ng.functional.eq + +You really like your dictionary, but it's got some duplicate values? We.. Got.. Chu! + sl4ng.iteration.deduplicate + +You've read this far and think "Damn, son, this package looks diggity fresh, but some of those functions are deeply nested"? We gotchu + Everything is imported to init \ No newline at end of file diff --git a/filey/README.md b/filey/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e3da38931c7385e31c3a673d5c3f2c56356814dc GIT binary patch literal 24 fcmezWPnki1!I>eEA&DWAA&()Ip@e~#fr|kEQ`Q9z literal 0 HcmV?d00001 diff --git a/filey/__init__.py b/filey/__init__.py new file mode 100644 index 0000000..0ba32e2 --- /dev/null +++ b/filey/__init__.py @@ -0,0 +1,605 @@ +""" +File management wrappers on os.path, shutil, and useful non-standard modules + +"Apparent drive" refers to the term before the first os.sep in an object's given path. + if the given path is relative + then the ADrive may be ".." or the name of the File/Directory + else + the drive is the name/letter of the disk partition on which the content at the address is stored. +"Apparent Directory" similarly refers first term before the last os.sep in an object's given path + if the given path is relative + then the ADir may be ".." or the name of the File/Directory + else + the drive is the name/letter of the disk partition on which the content at the address is stored. + + +TODO + Add relative path support + Classes for specific mimes + A version of @cached_property which checks the date modified before determining whether or not to recompute + Caches for new directories which include objects to add to them upon creation + Strict searching + Caches for pickling (backup simplification) + Remove Size from repr for Directories. Large ones take too long to initialize +""" +__all__ = 'Address Directory File'.split() + +from itertools import chain +from typing import Iterable, List, Dict +import os, re, sys, shutil + +import filetype as ft, audio_metadata as am +from send2trash import send2trash + +from .utils import * + +generator = type(i for i in range(0)) +function = type(ft.match) + +formats = { + 'pics': "bmp png jpg jpeg tiff".split(), + 'music': "mp3 m4a wav ogg wma flac aiff alac".split(), + 'videos': "mp4 wmv".split(), + 'docs': "doc docx pdf xlsx pptx ppt xls csv".split(), +} +formats['all'] = [*chain.from_iterable(formats.values())] + +class MemorySize(int): + """ + Why should you have to sacrifice utility for readability? + """ + def __repr__(self): + return nice_size(self) + +class Address: + """ + Base class for a non-descript path-string. + Relative paths are not currently supported + Methods return self unless otherwise stated + """ + def __init__(self, path:str): + self.path = path = normalize(path) + # if not os.path.exists(path): + # print(Warning(f'Warning: Path "{path}" does not exist')) + def __str__(self): + return self.path + def __hash__(self): + return hash(self.path) + def __repr__(self): + # return self.path + # tipo = "Directory File".split()[self.isfile] + return f"{tipo(self)}(name={self.name}, up={self.up.name})" + + def create(self, content:[str, bytes, bytearray]=None, raw:bool=False): + """ + Create an entry in the file-system. If the address is not vacant no action will be taken. + The raw handle only works for Files and enables writing bytes/bytearrays + """ + if self.exists: + return self + elif content: + os.makedirs(self.up.path, exist_ok=True) + if raw: + if isinstance(content, (bytes, bytearray)): + fobj = open(self.path, 'wb') + else: + fobj = open(self.path, 'w') + fobj.write(content) + fobj.close() + else: + if isinstance(content, str): + content = bytes(content, encoding='utf-8') + else: + content = bytes(content) + with open(self.path, 'wb') as fobj: + fobj.write(content) + else: + os.makedirs(self.up.path, exist_ok=True) + if likefile(self.path): + with open(self.path, 'x') as fobj: + pass + else: + os.makedirs(self.path) + return self + + @property + def exists(self): + return os.path.exists(self.path) + @property + def isdir(self): + if self.exists: + return os.path.isdir(self.path) + elif type(self) == type(Directory('.')): + return True + elif type(self) == type(File('a')): + return False + return not likefile(self.path) + @property + def isfile(self): + if self.exists: + return os.path.isfile(self.path) + elif type(self) == type(File('a')): + return True + elif type(self) == type(Directory('.')): + return False + return likefile(self.path) + + @property + def obj(self): + """ + Determine if self.path points to a file or folder and create the corresponding object + """ + if self.exists: + return File(self.path) if os.path.isfile(self.path) else Directory(self.path) + else: + return File(self.path) if likefile(self.path) else Directory(self.path) + @property + def up(self): + """ + Return the ADir + """ + return Address(delevel(self.path)).obj + @property + def name(self): + """ + Return the name of the referent + """ + return os.path.split(self.path)[1] + + @property + def ancestors(self): + """ + Return consecutive ADirs until the ADrive is reached + """ + level = [] + p = self.path[:] + while p != delevel(p): + p = delevel(p) + level.append(p) + return tuple(Address(i).obj for i in level)[::-1] + @property + def colleagues(self): + """ + Every member of the same Directory whose type is the same as the referent + """ + return (i for i in self.up if isinstance(i, type(self))) + @property + def neighbours(self): + """ + Everything in the same Directory + """ + return self.up.content + @property + def depth(self): + """ + Number of ancestors + """ + return len(self.ancestors) + @property + def top(self): + """ + The apparent drive. Will not be helpful if self.path is relative + """ + return self.ancestors[0] + @property + def stat(self): + """ + return os.stat(self.path) + """ + return os.stat(self.path) + + def delevel(self, steps:int=1, path:bool=False) -> str: + """ + Go up some number of levels in the File system + """ + return delevel(self.path, steps) if path else Directory(delevel(self.path, steps)) + @property + def ancestry(self): + """ + A fancy representation of the tree from the apparent drive up to the given path + """ + print(f'ancestry({self.name})') + ancs = list(self.ancestors[1:]) + # ancs = self.ancestors[1:] + ancs.append(self.path) + # print(ancs) + for i, anc in enumerate(ancs): + print('\t' + ('', '.'*i)[i>0] + i*' ' + [i for i in str(anc).split(os.sep) if i][-1] + '/') + return self + def touch(self): + """ + Implements the unix command 'touch', which updates the 'date modified' of the content at the path + """ + p = self.path + pathlib.Path(p).touch() + self = Address(p).obj + return self + def erase(self, recycle:bool=True): + """ + Send a File to the trash, or remove it without recycling. + """ + send2trash(self.path) if recycle else os.remove(self.path) + return self + def clone(self, folder:str=None, name:str=None, cwd:bool=False, sep:str='_', touch=False): + """ + Returns a clone of the referent at a given Directory-path + The given path will be created if it doesn't exist + Will copy in the File's original folder if no path is given + The cwd switch will always copy to the current working Directory + """ + copier = (shutil.copy2, shutil.copytree)[self.isdir] + if cwd: + new = os.path.join(os.getcwd(), name if name else self.name) + elif folder: + new = os.path.join(folder, name if name else self.name) + else: + new = self.path + new = nameSpacer(new, sep=sep) + os.makedirs(delevel(new), exist_ok=True) + copier(self.path, new) + out = Address(new).obj + return out.touch() if touch else out + def move(self, folder:str=None, name:str=None, dodge:bool=False, sep:str='_', recycle:bool=True): + """ + addy.move(folder) -> move to the given Directory () + addy.move(name) -> move to the given path (relative paths will follow from the objects existing path) + addy.move(folder, name) -> move to the given Directory + + :param dodge: + enables automatic evasion of file-system collisions + :param sep: + chooses the separator you use between the object's name and numerical affix in your directory's namespace + **inert if dodge is not True + :param recycle: + enables you to avoid the PermissionError raised by os.remove (if you have send2trash installed) + ** the PermissionError is due to the object being in use at the time of attempted deletion + """ + if folder and name: + os.makedirs(folder, exist_ok=True) + new = os.path.join(folder, name) + elif folder: + os.makedirs(folder, exist_ok=True) + new = os.path.join(folder, self.name) + elif name: + new = os.path.join(self.up.path, name) + else: + raise ValueError(f'The file couldn\'t be moved because {name=} and {folder=}. Set either one or both') + new = nameSpacer(new, sep=sep) if dodge else new + folder, name = os.path.split(new) + self.clone(folder=folder, name=name) + # os.remove(self.path) + self.erase() + self.path = new + return self + def rename(self, name:str): + """ + Change the name of the referent + """ + return self.move(name=name) + def expose(self): + """ + Reveal the referent in the system's file explorer (will open the containing Directory if the referent is a File) + """ + if self.isdir: + os.startfile(self.path) + else: + os.startfile(self.up.path) + return self + + +class File(Address): + """ + Create a new File object for context management and ordinary operations + """ + def __init__(self, path:str='NewFile'): + path = os.path.abspath(trim(path)) + super(File, self).__init__(path) + self._stream = None + def __enter__(self): + return self.open() + def __exit__(self, exc_type, exc_value, traceback): + self.close() + def __repr__(self): + # return self.path + # tipo = "Directory File".split()[self.isfile] + return f"{tipo(self)}(name={self.name}, up={self.up.name}, size={self.size})" + + @property + def size(self): + return MemorySize(os.stat(self.path).st_size) + @property + def mime(self): + return match.MIME if (match:=ft.guess(self.path)) else None + @property + def kind(self): + return self.mime.split('/')[0] if (match:=ft.guess(self.path)) else None + @property + def ext(self): + return os.path.splitext(self.name)[1] + @property + def title(self): + """ + return the File's name without the extension + """ + return os.path.splitext(self.name)[0] + def open(self, mode:str='r', scrape:bool=False): + """ + Return the File's byte or text stream. + Scrape splits the text at all whitespace and returns the content as a string + """ + if scrape: + with open(self.path, mode) as fobj: + return ' '.join(fobj.read().split()) + with open(self.path, mode) as fobj: + self._stream = fobj + return self._stream + def close(self): + if self._stream: + self._stream.close() + return self + @property + def run(self): + os.startfile(self.path) + return self + + +class Items: + """ + A wrapper on a directory's content which makes it easier to access by turning elements into attributes + """ + def __init__(self, path): + self.path = normalize(path) + def __getattr__(self, attr): + return Directory(self.path)[attr] + + +class Directory(Address): + """ + Directory('.') == Directory(os.getcwd()) + """ + def __init__(self, path:str='NewDirectory'): + if path=='.': + path = os.getcwd() + elif path == 'NewDirectory': + path = nameSpacer(path) + elif path == '~': + path = os.path.expanduser(path) + path = os.path.abspath(trim(path)) + self.path = normalize(path) + super(Directory, self).__init__(path) + self.index = -1 + def __repr__(self): + # return self.path + # tipo = "Directory File".split()[self.isfile] + return f"{tipo(self)}(name={self.name}, up={self.up.name})" + def __len__(self): + return len(os.listdir(self.path)) + def __bool__(self): + """ + Check if the Directory is empty or not + """ + return len(os.listdir(self.path)) > 0 + def __iter__(self): + return self + def __next__(self): + if self.index Iterable: + """ + See help(self.search) + """ + return self.search(keyword, sort, case, **kwargs) + + @property + def items(self): + """ + This extension allows you to call folder contents as if they were attributes. + Will not work if your file system does not use a python-viable naming convention + example: + >>> folder.items.subfolder + """ + return Items(self.path) + @property + def children(self): + """ + Return "os.listdir" but filtered for directories + """ + return (addy.obj for i in os.listdir(self.path) if (addy:=Address(os.path.join(self.path, i))).isdir) + @property + def files(self): + """ + Return "os.listdir" but filtered for Files + """ + return (addy.obj for i in os.listdir(self.path) if (addy:=Address(os.path.join(self.path, i))).isfile) + @property + def content(self): + """ + Return address-like objects from "os.listdir" + """ + return tuple(Address(os.path.join(self.path, i)).obj for i in os.listdir(self.path)) + @property + def leaves(self): + """ + Return All Files from all branches + """ + # return tuple(self.gather()) + return map(lambda x: Address(x).obj, self.gather()) + @property + def branches(self): + """ + Return Every Directory whose path contains "self.path" + """ + return tuple(set(File(i).delevel() for i in self.gather())) + @property + def size(self): + """ + Return the sum of sizes of all files in self and branches + """ + return MemorySize(sum(file.size for file in self.leaves)) + @property + def mimes(self): + """ + Return File mimes for all Files from all branches + """ + return tuple(set(file.mime for file in self.gather())) + @property + def kinds(self): + """ + Return File types for all Files from branches + """ + return tuple(set(m.split('/')[0] for m in self.mime)) + @property + def exts(self): + """ + Return extensions for all Files from all branches + """ + return tuple(set(f.ext for f in self.gather())) + @property + def isroot(self): + """ + Return check if the Directory is at the highest level of the File system + """ + return not self.depth + + def add(self, other:Address, copy:bool=False): + """ + Introduce new elements. Send an address-like object to self. + """ + if not self.exists: + raise OSError(f"Cannot add to Directory({self.name}) because it doesn't exist") + elif not other.exists: + if issubclass(type(other), Address): + tipo = 'Directory File'.split()[other.isfile] + raise OSError(f"{other.name.title()} could not be added to {self.name} because it doesn't exist") + new = os.path.join(self.path, os.path.split(other.path)[-1]) + other.clone(folder=self.up.path) if copy else other.rename(new) + return self + + def enter(self): + """ + Set referent as current working Directory + """ + os.chdir(self.path) + + def gather(self, titles:bool=False, walk:bool=True, ext:str='') -> generator: + """ + Generate an iterable of the files rooted in a given folder. The results will be strings, not File objects + It is possible to search for multiple File extensions if you separate each one with a space, comma, asterisk, or tilde. + Only use one symbol per gathering though. + + :param titles: if you only want to know the names of the files gathered, not their full paths + :param walk: if you want to recursively scan subdiretories + :param ext: if you want to filter for particular extensions + """ + folder = self.path + if walk: + if ext: + ext = ext.replace('.', '') + sep = [i for i in ',`* ' if i in ext] + pattern = '|'.join(f'\.{i}$' for i in ext.split(sep[0] if sep else None)) + pat = re.compile(pattern, re.I) + for root, folders, names in os.walk(folder): + for name in names: + if os.path.isfile(p:=os.path.join(root, name)) and pat.search(name) and name!='NTUSER.DAT': + yield name if titles else p + else: + for root, folders, names in os.walk(folder): + for name in names: + if os.path.exists(p:=os.path.join(root, name)): + yield name if titles else p + else: + if ext: + ext = ext.replace('.', '') + sep = [i for i in ',`* ' if i in ext] + pattern = '|'.join(f'\.{i}$' for i in ext.split(sep[0] if sep else None)) + pat = re.compile(pattern, re.I) + for name in os.listdir(folder): + if os.path.isfile(p:=os.path.join(folder, name)) and pat.search(name) and name!='NTUSER.DAT': + yield name if titles else p + else: + for name in os.listdir(folder): + if os.path.isfile(p:=os.path.join(folder, name)): + yield name if titles else p + + def search(self, keyword:str, sort:bool=False, case:bool=False, prescape:bool=False, strict:bool=True, **kwargs) -> Iterable: + """ + Return an iterator of Files whose path match the given keyword within a Directory. + The search is linear and the sorting is based on the number of matches. If sorted, a list will be returned. + Case pertains to case-sensitivity + Prescape informs the method that kewords do not need to be escaped + For kwargs see help(self.gather) + + :param keyword: terms you wish to match, separated by spaces + :param sort: if you would like to sort the results by number of matches + :param case: if you would like to make the search case sensitive + :param prescape: if you have already re.escape-d the terms you would like to search + :param strict: if you only want to see results which match every term in the keyword string + """ + casesensitivity = (re.I, 0)[case] + escaper = (re.escape, lambda x: x)[prescape] + if isinstance(keyword, str): + keyword = keyword.split() + if not isinstance(keyword, str): + keyword = '|'.join(map(escaper, keyword)) + if strict: + return filter( + lambda x: len([*re.finditer(keyword, x, casesensitivity)]) >= len(keyword.split('|')), + self.gather(**kwargs), + ) + elif sort: + return sorted( + filter( + lambda x: len([*re.finditer(keyword, x, casesensitivity)]) == len(keyword.split('|')), + self.gather(**kwargs), + ), + key=lambda x: len([*re.finditer(keyword, x, casesensitivity)]), + reverse=True + ) + else: + return filter( + lambda x: re.search(keyword, x, casesensitivity), + self.gather(**kwargs) + ) + + +# class Archive(Address): + # def __init__(self, path:str='NewFile'): + # path = os.path.abspath(trim(path)) + # super(File, self).__init__(path) + +if __name__ == '__main__': + show(locals().keys()) + + + fp = r'c:\users\kenneth\pyo_rec.wav' + dp = r'c:\users\kenneth\videos' + + d = Directory(dp) + f = File(fp) + tf = File('testfile.ext') + td = Directory('testdir') + + system = (d, f) + # show(d('slowthai', sort=True)) + # show(d('alix melanie', sort=True)) + # show(d('melanie alix', sort=True)) + + # print(formats['all']) + # for v in formats.values(): + # print(' '.join(v)) + + \ No newline at end of file diff --git a/filey/preamble/_parseconfigurationscunt.py b/filey/preamble/_parseconfigurationscunt.py new file mode 100644 index 0000000..bb1867b --- /dev/null +++ b/filey/preamble/_parseconfigurationscunt.py @@ -0,0 +1,7 @@ +import configparser + +cfg = configparser.ConfigParser() +# cfg[] + +if __name__=='__main__': + pass \ No newline at end of file diff --git a/filey/preamble/filesystem.py b/filey/preamble/filesystem.py new file mode 100644 index 0000000..1679c73 --- /dev/null +++ b/filey/preamble/filesystem.py @@ -0,0 +1,380 @@ +""" +Implementation of a rudimentary file system + +TODO: + grab the +""" + +import os, sys, shutil, pathlib +from dataclasses import dataclass + +import filetype as ft +from send2trash import send2trash + +from sl4ng import show, delevel, gather, nameSpacer + +def trim(path,edge=os.sep): + out = path[:] + while out.startswith(edge): + out = out[1:] + while out.endswith(edge): + out = out[:-1] + print(out) + return out + +class size(int): + def __repr__(self): + return f'{round(self*10**-3):,} kb' + +class _path(str): + + +@dataclass +class address: + """ + A systemic pointer to the location of the data associated with a file system object + """ + # def init(self,path:str): + # assert os.path.exists(self.path), f'"{path}" is not a valid address on this system' + # self.path = path + path:str + # if path: + # assert os.path.exists(self.path), f'"{path}" is not a valid address on this system' + + @property + def exists(self): + return os.path.exists(self.path) + @property + def isdir(self): + return os.path.isdir(self.path) + @property + def isfile(self): + return os.path.isfile(self.path) + @property + def obj(self): + if self.isdir: + return directory(self.path) + elif self.isfile: + return file(self.path) + else: + raise ValueError(f'"{self.path}" is not a valid address on this system') + if exists: + # del isfile,isdir + ( , ) + + +class directory: + def __init__(self,path:str): + assert address(path).isdir, f'"{path}" is not a directory' + self.path = trim(path) + self._ind = -1 + cntnt = (address(os.path.join(path,i)) for i in os.listdir(path)) + for i in cntnt: + name = os.path.split(i.path)[1] + if i.isdir: + setattr(self,name,i.path) + elif i.isfile: + setattr(self,os.path.splitext(name)[0],i.path) + + + @property + def children(self): + return tuple(addy.obj for i in os.listdir(self.path) if (addy:=address(os.path.join(str(self),i))).isdir) + @property + def files(self): + return tuple(addy.obj for i in os.listdir(self.path) if (addy:=address(os.path.join(self.path,i))).isfile) + @property + def content(self): + return tuple(address(os.path.join(self.path,i).obj for i in os.listdir(self.path))) + @property + def leaves(self): + return tuple(self.gather()) + @property + def leaves(self): + return tuple(set(i.container() for i in self.gather())) + @property + def name(self): + return os.path.split(self.path)[1] + @property + def ancestors(self): + level = [] + p = self.path[:] + while p != delevel(p): + p = delevel(p) + level.append(p) + return tuple(directory(i) for i in level)[::-1] + @property + def depth(self): + return len(self.ancestors) + @property + def root(self): + return self.ancestors[0]#.split(':')[0] + @property + def size(self): + # return size(sum(os.stat(file).st_size for file in self.gather())) + return size(sum(file.size for file in self.leaves)) + @property + def mime(self): + # return tuple(set(match.MIME if (match:=ft.guess(file)) else 'UNKNOWN' for file in self.gather())) + return tuple(set(file.mime for file in self.gather())) + @property + def kind(self): + return tuple(set(m.split('/')[0] for m in self.mime)) + @property + def ext(self): + return tuple(set(f.ext for f in self.gather())) + @property + def siblings(self): + return tuple(i for i in self.container() if isinstance(i,type(self))) + @property + def peers(self): + return self.container().content + @property + def stat(self): + """ + return os.stat(str(self)) + """ + return os.stat(str(self)) + + def touch(self): + p = str(self) + pathlib.Path(p).touch() + self = address(p).obj + return self + def delete(self,recycle=True): + send2trash(self.path) if recycle else os.remove(self.path) + del self + def rename(self,new,relative=False): + new = new if not relative else os.path.join(delevel(self.path),new) + os.makedirs(delevel(new),exist_ok=True) + os.rename(self.path,new) + self = address(new).obj + return self + def clone(self,new=None,relative=False,touch=False): + if new: + if relative: + new = nameSpacer(os.path.join(delevel(self.path),new)) + else: + new = nameSpacer(os.path.join(delevel(self.path),self.name)) + os.makedirs(delevel(new),exist_ok=True) + shutil.copy2(self.path,new) #if touch else shutil.copy2(self.path,new) + out = address(new).obj + return out.touch() if touch else out + def container(self,steps=1,path=False): + return delevel(self.path,steps) if path else directory(delevel(self.path,steps)) + def heritage(self): + print(f'\nheritage({self.name.title()})') + ancs = list(self.ancestors) + ancs.append(self.path) + for i,anc in enumerate(ancs): + print('\t'+('','.'*i)[i>0]+i*' '+[i for i in str(anc).split(os.sep) if i][-1]) + + def show(self, indentation:int=1, enum:bool=False, start:int=1, indentor:str='\t'): + assert indentation>0, f'"{indentation}" is not a viable indentation level' + print((indentation-1)*'\t'+self.name) + show(self.content,indentation,enum,start,indentor) + def gather(self,names:bool=False,walk:bool=True,ext:str=None,paths=False): + if paths: + yield from gather(str(self),names,walk,ext) + else: + for path in gather(str(self),names,walk,ext): + yield file(path) + def add(self,new): + if (obj:=address(new).obj): + return obj.move(self) + else: + raise ValueError(f'"{new}" does is neither a file or directory') + + def moveContent(self,other): + # assert address(trim(str(other))).isdir, f'"{other}" is not a viable directory here' + assert address((str(other))).isdir, f'"{other}" is not a viable directory here' + for f in self.gather(): + # rest = f.path[len(self.path)+1:] + ending = trim(f.path[len(self.path)+1:]) + new = os.path.join(trim(str(other)),ending) + # os.makedirs(delevel(new)) + print(self) + print(f) + print(ending) + print(new) + print() + + def isroot(self): + return not self.depth + def __bool__(self): + return len(os.listdir(self.path))>0 + def __str__(self): + return self.path + def __repr__(self): + return str(self) + def __iter__(self): + return self + def __len__(self): + return len(self.content) + def __next__(self): + if self._ind0]+i*' '+[i for i in str(anc).split(os.sep) if i][-1]) + + def __str__(self): + return self.path + def __repr__(self): + return str(self) + + move = rename + extension = ext + copy = clone + +if __name__ == '__main__': + dp = r'c:\users\kenneth\videos' + # print(directory(dp)) + # show(directory(dp).fils,) + fp = r'c:\users\kenneth\pyo_rec.wav' + # print(os.path.isfile(fp)) + # print(file(fp)) + fp = r'C:\Users\Kenneth\Music\Collection\slowthai\The Bottom _ North Nights\02 North Nights.mp3' + dp = delevel(fp,1) + + d = address(dp).obj + print(d) + # d.show(1) + f = address(fp).obj + print(f) + + system = (d,f) + print(system) + print([i.name for i in system]) + print([i.depth for i in system]) + print([i.ancestors for i in system]) + print([i.heritage() for i in system]) + print([i.root for i in system]) + print([i.size for i in system]) + [print(i.size) for i in system] + [print(i.kind) for i in system] + [print(i.ext) for i in system] + [print(i.stat) for i in system] + print(d.leaves) + + # d.moveContent(r'c:\users\kenneth\downloads\\') + # print(f.clone(touch=True)) + # print(f.clone()) + # print(f.move(nameSpacer(str(f)))) + # os.startfile(f.container().path) + # help(f) + # for i in d: + # ob = d[i] + # print(ob,ob.kind,ob.ext) + # if isinstance(ob,directory): + # for o in ob.children: + # o2 = ob[o] + # print('\t',o2,o2.kind,o2.ext) + + # for p in d.gather(): + # print(p.size,p.mime,p.ext,sys.getrefcount(p)) + # f = file(p) + # if 'matroska' in f.mime: + # print(f.container()) + # print(f.name) + # print(f.size) + + + # for i in d:print(i) + # print(os.stat(dp).st_size) + # print(os.stat(fp).st_size) + # print(dict(d)) + # d.heritage() + # os.rename('Few Nolder','randombumbashit') + # print(os.listdir('randombumbashit')) \ No newline at end of file diff --git a/filey/preamble/filesystem2inheritance.py b/filey/preamble/filesystem2inheritance.py new file mode 100644 index 0000000..7b974d2 --- /dev/null +++ b/filey/preamble/filesystem2inheritance.py @@ -0,0 +1,366 @@ +""" +Implementation of a rudimentary file system + +TODO: + grab the +""" + +import os, sys, shutil, pathlib, re +from dataclasses import dataclass + +import filetype as ft +from send2trash import send2trash + +from sl4ng import show, delevel, gather, nameSpacer, ffplay, commons, nopes +# from magnitudes import rep +from .magnitudes import represent# as rp +# from magnitudes import * + +forbiddenChars = r'\/:?*<>|"' + +formats = { + 'pics':['bmp','png','jpg','jpeg','tiff',], + 'music':['mp3','m4a','wav','ogg','wma','flac','aiff','alac',], + 'videos':['mp4','wmv',], + 'docs':['doc','docx','pdf','xlsx','pptx','ppt','xls','csv',], +} + +orders = { + 1:'deca', + 2:'hecto', + 3:'kilo', + 6:'mega', + 9:'giga', + 12:'tera', + 15:'peta', + 18:'exa', + 21:'zetta', + 24:'yotta' +} + +def trim(path,edge=os.sep): + out = path[:] + while out.startswith(edge): + out = out[1:] + while out.endswith(edge): + out = out[:-1] + return out + + +# @dataclass +# class size: + # val:int +class size(int): + # def __repr__(self): + # rep = round(self*10**-3) + # if len(st) + # return f'{round(self*10**-3):,} kb' + def __repr__(self): + return rp(self) + # def __str__(self): + # return str(self.val) + # def __add__(self,other): + # return self.val + size(other) + # def __truediv__(self,other): + # return self.val + size(other) + # def __sub__(self,other): + # return self.val + size(other) + # def __mul__(self,other): + # return self.val + size(other) + # pass + # def __str__(self): + # return str(int(self)) + # def __repr__(self,dim='bytes',long=False,lower=False,precision=2,sep='-'): + # orders = { + # 3:'kilo', + # 6:'mega', + # 9:'giga', + # 12:'tera', + # 15:'peta', + # 18:'exa', + # 21:'zetta', + # 24:'yotta', + # } + + # sredro = {v:k for k,v in orders.items()} + + # pretty = lambda number,unit='': f'{number:,} {unit}'.strip() + # setcase = lambda unit,lower=False: [unit.upper().strip(),unit.lower().strip()][lower] + # setlength = lambda mag,dim,long=False,sep='-': ('',sep)[long].join(((mag[0],dim[0]),(mag,dim))[long]) + + + # mags = tuple(sorted(orders.keys())) + # booly = lambda i: len(str(int(self))) < len(str(10**mags[i+1])) + # fits = tuple(nopes((booly(i) for i in range(len(mags)-1)),True)) + # fits = tuple(filter(booly,range(len(mags)-1))) + # mag = orders[mags[min(fits) if fits else len(mags)-1]] + # unit = setcase(setlength(mag,dim,long,['',sep][long]),lower) + # number = round(self*10**-sredro[mag],precision) + # return pretty(number,unit).lower() if lower else pretty(number,unit).upper() +@dataclass +class _pathLike: + path:str + + def __str__(self): + return self.path + def __repr__(self): + return str(self) + def __hash__(self): + return hash(str(self)) + + @property + def exists(self): + return os.path.exists(self.path) + @property + def up(self): + return address(delevel(str(self))).obj + @property + def name(self): + return os.path.split(self.path)[1] + @property + def title(self): + return os.path.splitext(self.name)[0] + @property + def ancestors(self): + level = [] + p = self.path[:] + while p != delevel(p): + p = delevel(p) + level.append(p) + return tuple(address(i).obj for i in level)[::-1] + @property + def siblings(self): + # return tuple(i for i in self.delevel() if isinstance(i,type(self))) + return tuple(i for i in self.up if isinstance(i,type(self))) + @property + def depth(self): + return len(self.ancestors) + @property + def root(self): + return self.ancestors[0]#.split(':')[0] + @property + def peers(self): + return self.delevel().content + @property + def stat(self): + """ + return os.stat(str(self)) + """ + return os.stat(str(self)) + + def delevel(self,steps=1,path=False): + return delevel(self.path,steps) if path else directory(delevel(self.path,steps)) + def heritage(self): + print(f'\nheritage({self.name.title()})') + ancs = list(self.ancestors) + ancs.append(self.path) + for i,anc in enumerate(ancs): + print('\t'+('','.'*i)[i>0]+i*' '+[i for i in str(anc).split(os.sep) if i][-1]) + + def touch(self): + p = str(self) + pathlib.Path(p).touch() + self = address(p).obj + return self + def delete(self,recycle=True): + send2trash(self.path) if recycle else os.remove(self.path) + del self + def rename(self,new,relative=False): + new = new if not relative else os.path.join(delevel(self.path),new) + os.makedirs(delevel(new),exist_ok=True) + os.rename(self.path,new) + self = address(new).obj + return self + def clone(self,new=None,relative=False,touch=False): + if new: + if relative: + new = nameSpacer(os.path.join(delevel(self.path),new)) + else: + new = nameSpacer(os.path.join(delevel(self.path),self.name)) + os.makedirs(delevel(new),exist_ok=True) + shutil.copy2(self.path,new) #if touch else shutil.copy2(self.path,new) + out = address(new).obj + return out.touch() if touch else out + + move = rename + copy = clone + + +@dataclass +class address(_pathLike): + """ + A systemic pointer to the location of the data associated with a file system object + """ + def __init__(self,path:str): + super(address,self).__init__(path) + # assert os.path.exists(path), f'"{path}" is not a valid address on this system' + assert self.exists, f'"{path}" is not a valid address on this system' + + + @property + def isdir(self): + return os.path.isdir(self.path) + @property + def isfile(self): + return os.path.isfile(self.path) + @property + def obj(self): + if self.isdir: + return directory(self.path) + elif self.isfile: + return file(self.path) + + def expose(self): + os.startfile(self.path) + return self + + +# class directory(_pathLike): +class directory(address): + def __init__(self,path:str): + path = os.path.abspath(trim(path)) + assert address(path)#.isdir, f'"{path}" is not a directory' + super(directory,self).__init__(path) + self._ind = -1 + # self. + def __bool__(self): + return len(os.listdir(self.path))>0 + def __len__(self): + return len(self.content) + def __iter__(self): + return self + def __next__(self): + if self._ind0, f'"{indentation}" is not a viable indentation level' + print((indentation-1)*'\t'+self.name) + show(self.content,indentation,enum,start,indentor) + + extension = ext + + +class file(_pathLike): + def __init__(self,path:str): + path = os.path.abspath(trim(path)) + assert address(path)#, f'"{path}" is not a file' + super(file,self).__init__(path) + self._stream = None + def __enter__(self): + self._stream = open(str(self),'b') + return self + def __exit__(self): + self._stream.close() + self._stream = None + + @property + def size(self): + return size(os.stat(str(self)).st_size) + @property + def mime(self): + return match.MIME if (match:=ft.guess(str(self))) else 'UNKNOWN' + @property + def kind(self): + return self.mime.split('/')[0] if (match:=ft.guess(str(self))) else 'UNKNOWN' + @property + def ext(self): + return os.path.splitext(self.name)[1] + + extension = ext + + +user = directory(commons['home']) +root = user.up.up + +if __name__ == '__main__': + fp = r'c:\users\kenneth\pyo_rec.wav' + dp = r'c:\users\kenneth\videos' + + # fp = r'C:\Users\Kenneth\Music\Collection\slowthai\The Bottom _ North Nights\02 North Nights.mp3'#[:-1] + # dp = delevel(fp,1)#[:-1] + + # d = address(dp).obj + # f = address(fp).obj + + d = directory(dp) + f = file(fp) + + system = (d,f) + [print(i) for i in system] + [print(i.size) for i in system] + print() + # [show(i.delevel()) for i in system] + # [show(i.delevel()) for i in system] + + print(forbiddenChars) + + # with f as fob: + # print(fob) \ No newline at end of file diff --git a/filey/preamble/filesystem3pathlibbing.py b/filey/preamble/filesystem3pathlibbing.py new file mode 100644 index 0000000..a1a9d96 --- /dev/null +++ b/filey/preamble/filesystem3pathlibbing.py @@ -0,0 +1,54 @@ +from pathlib import Path +from functools import reduce, cached_property +import os, re + +from sl4ng import show + + +def normalize(path,relative=False): + other = ''.join(i for i in '\/' if not i==os.sep) + if other in path: + terms = [] + for term in path.split(os.sep): + if other in term: + for part in term.split(other): + terms.append(part) + else: + terms.append(term) + path = os.path.join(*terms) + if relative: + path = '.'+os.sep+path + return path + +def hasdirs(path): + return bool(re.search(re.escape(os.sep),normalize(path))) + +def likeFile(path): + path = normalize(path) + return bool(re.search('readme$|.+\.(\S)+$',path.split(os.sep)[-1],re.I)) + + +class address(Path): + def __init__(self,string): + string = normalize(self.string) + if not os.path.exists(string): + print(Warning(f'Warning: Path "{string}" does not exist')) + + +if __name__ == '__main__': + mock = 'drive: users user place subplace file.extension.secondextension'.split() + # mock = 'drive: users user place subplace readme'.split() + testPaths = [ + ''.join(mock), + os.path.join(*mock), + os.path.join(*mock[:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[3:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[:-1]), + '/'.join(mock[:-1]), + mock[-1] + ] + # show(zip(testPaths,map(normalize,testPaths))) + # show(zip(testPaths,map(hasdirs,testPaths))) + # show(zip(testPaths,map(likeFile,testPaths))) + + demo = address(os.path.join(*mock)) \ No newline at end of file diff --git a/filey/preamble/filesystem4-pathsarentinheritable.py b/filey/preamble/filesystem4-pathsarentinheritable.py new file mode 100644 index 0000000..2b4e5e3 --- /dev/null +++ b/filey/preamble/filesystem4-pathsarentinheritable.py @@ -0,0 +1,344 @@ +from pathlib import Path +from functools import reduce, cached_property +import os, re, sys, shutil + + +import filetype as ft +from send2trash import send2trash +import audio_metadata as am + + +from sl4ng import show, delevel, gather, nameSpacer, ffplay, commons, nopes +# from magnitudes import represent# as rp + +def normalize(path, relative=False): + other = ''.join(i for i in '\/' if not i==os.sep) + if other in path: + terms = [] + for term in path.split(os.sep): + if other in term: + for part in term.split(other): + terms.append(part) + else: + terms.append(term) + path = os.path.join(*terms) + if relative: + path = '.'+os.sep+path + return path + +def hasdirs(path): + return bool(re.search(re.escape(os.sep),normalize(path))) + +def likeFile(path): + path = normalize(path) + return bool(re.search('readme$|.+\.(\S)+$',path.split(os.sep)[-1],re.I)) + + + +class address: + def __init__(self, path): + self.path = path = normalize(path) + if not os.path.exists(path): + print(Warning(f'Warning: Path "{path}" does not exist')) + def __str__(self): + return self.path + def __repr__(self): + return self.path + def __hash__(self): + return hash(self.path) + + def create(self, content=None, raw=False): + if self.exists: + return self + elif content: + if raw: + if isinstance(content,(bytes,bytearray)): + fobj = open(self.path,'wb') + else: + fobj = open(self.path,'w') + fobj.write(content) + fobj.close() + else: + if isinstance(content,str): + content = bytes(content,encoding='utf-8') + else: + content = bytes(content) + with open(self.path,'wb') as fobj: + fobj.write(content) + else: + if likeFile(self.path): + with open(self.path,'x') as fobj: + pass + else: + os.makedirs(self.path,exist_ok=True) + return self + + @property + def isdir(self): + return os.path.isdir(self.path) + @property + def isfile(self): + return os.path.isfile(self.path) + @property + def exists(self): + return os.path.exists(self.path) + @property + def obj(self): + if self.exists: + return file(self.path) if self.isfile else folder(self.path) + else: + return file(self.path) if likeFile(self.path) else directory(self.path) + @property + def up(self): + return address(delevel(self.path)).obj + @property + def name(self): + return os.path.split(self.path)[1] + + @property + def ancestors(self): + """ + + """ + level = [] + p = self.path[:] + while p != delevel(p): + p = delevel(p) + level.append(p) + return tuple(address(i).obj for i in level)[::-1] + @property + def colleagues(self): + """ + + """ + return tuple(i for i in self.up if isinstance(i, type(self))) + @property + def neighbours(self): + """ + + """ + return self.up.content + @property + def depth(self): + """ + + """ + return len(self.ancestors) + @property + def top(self): + """ + + """ + return self.ancestors[0]#.split(':')[0] + @property + def stat(self): + """ + return os.stat(self.path) + """ + return os.stat(self.path) + + def delevel(self, steps=1, path=False): + """ + + """ + return delevel(self.path,steps) if path else directory(delevel(self.path,steps)) + def heritage(self): + """ + + """ + print(f'\nheritage({self.name.title()})') + ancs = list(self.ancestors) + ancs.append(self.path) + for i,anc in enumerate(ancs): + print('\t'+('','.'*i)[i>0]+i*' '+[i for i in str(anc).split(os.sep) if i][-1]) + + def touch(self): + """ + + """ + p = self.path + pathlib.Path(p).touch() + self = address(p).obj + return self + def erase(self, recycle=True): + """ + + """ + send2trash(self.path) if recycle else os.remove(self.path) + return self + def rename(self, new, relative=False): + """ + + """ + new = new if not relative else os.path.join(delevel(self.path),new) + os.makedirs(delevel(new),exist_ok=True) + os.rename(self.path,new) + self = address(new).obj + return self + def clone(self,new=None, relative=False, touch=False): + """ + + """ + + if new: + if os.path.isdir(new): + new = nameSpacer(os.path.join(new, self.name)) + if relative: + new = nameSpacer(os.path.join(delevel(self.path), self.name)) + else: + new = nameSpacer(os.path.join(delevel(self.path), self.name)) + os.makedirs(delevel(new), exist_ok=True) + shutil.copy2(self.path, new) + out = address(new).obj + return out.touch() if touch else out + + def expose(self): + """ + + """ + os.startfile(self.path) + return self + +class file(address): + @property + def title(self): + return os.path.splitext(self.name)[0] + + def content(mode='rb'): + """ + + """ + with open(self.path, mode) as fobj: + return fobj + +class directory(address): + def __init__(self,path:str): + path = os.path.abspath(trim(path)) + assert address(path)#.isdir, f'"{path}" is not a directory' + super(directory,self).__init__(path) + self._ind = -1 + def __len__(self): + return len(self.content) + def __bool__(self): + """ + Check if the directory is empty or not + """ + return len(os.listdir(self.path))>0 + def __iter__(self): + return self + def __next__(self): + if self._ind bool: + """ + Return check if the directory is at the highest level of the file system + """ + return not self.depth + + def add(self, other, copy=False): + """ + Introduce new elements. Send an address-like object to self. + """ + new = os.path.join(self.path, os.path.split(other.path)[-1]) + other.rename(new) + return self + +if __name__ == '__main__': + mock = 'drive: users user place subplace file.extension.secondextension'.split() + # mock = 'drive: users user place subplace readme'.split() + testPaths = [ + ''.join(mock), + os.path.join(*mock), + os.path.join(*mock[:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[3:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[:-1]), + '/'.join(mock[:-1]), + mock[-1] + ] + # show(zip(testPaths,map(normalize,testPaths))) + # show(zip(testPaths,map(hasdirs,testPaths))) + # show(zip(testPaths,map(likeFile,testPaths))) + + # ddemo = address(os.path.join(*mock[:-1])).obj + # fdemo = address(os.path.join(*mock)).obj + # ddemo = directory(os.path.join(*mock[:-1])) + # fdemo = file(os.path.join(*mock)) + + fp = r'c:\users\kenneth\pyo_rec.wav' + dp = r'c:\users\kenneth\videos' + + d = directory(dp) + f = file(fp) + + system = (d,f) + \ No newline at end of file diff --git a/filey/preamble/filesystem4_2.py b/filey/preamble/filesystem4_2.py new file mode 100644 index 0000000..a297696 --- /dev/null +++ b/filey/preamble/filesystem4_2.py @@ -0,0 +1,562 @@ +""" +File management wrappers on os.path, shutil, and useful non-standard modules + +"Apparent drive" refers to the term before the first os.sep in an object's given path. + if the given path is relative + then the ADrive may be ".." or the name of the File/Directory + else + the drive is the name/letter of the disk partition on which the content at the address is stored. +"Apparent Directory" similarly refers first term before the last os.sep in an object's given path + if the given path is relative + then the ADir may be ".." or the name of the File/Directory + else + the drive is the name/letter of the disk partition on which the content at the address is stored. + + +TODO + Add relative path support + Classes for specific mimes +""" + +from pathlib import Path +from functools import reduce, cached_property +from itertools import chain +from typing import Iterable +import os, re, sys, shutil + + +import filetype as ft, audio_metadata as am +from send2trash import send2trash + + + +from sl4ng import show, delevel, gather, nameSpacer, ffplay, commons, nopes +# from magnitudes import represent# as rp + + +gen = type(i for i in range(0)) +func = type(ft.match) + +forbiddens = r'\/:?*<>|"' # at least on windows + +formats = { + 'pics': "bmp png jpg jpeg tiff".split(), + 'music': "mp3 m4a wav ogg wma flac aiff alac".split(), + 'videos': "mp4 wmv".split(), + 'docs': "doc docx pdf xlsx pptx ppt xls csv".split(), +} +formats['all'] = [*chain.from_iterable(formats.values())] + + + + +def trim(path:str, edge:str=os.sep) -> str: + out = path[:] + while out.startswith(edge): + out = out[1:] + while out.endswith(edge): + out = out[:-1] + return out + +def normalize(path:str, relative:bool=False) -> str: + other = ''.join(i for i in '\/' if not i==os.sep) + if other in path: + terms = [] + for term in path.split(os.sep): + if other in term: + for part in term.split(other): + terms.append(part) + else: + terms.append(term) + path = os.path.join(*terms) + if relative: + path = '.'+os.sep+path + return path + +def hasdirs(path:str) -> bool: + return bool(re.search(re.escape(os.sep), normalize(path))) + +def likeFile(path:str) -> bool: + path = normalize(path) + return bool(re.search('readme$|.+\.(\S)+$',path.split(os.sep)[-1],re.I)) + +def multisplit(splitters:Iterable[str], target:str) -> gen: + """ + Split a string by a the elements of a sequence + >>> list(multisplit(',`* ', 'wma,wmv mp3`vga*mp4 ,`* ogg')) + ['wma', 'wmv', 'mp3', 'vga', 'mp4', 'ogg'] + """ + splitters = iter(splitters) + result = target.split(next(splitters)) + for splitter in splitters: + result = [*chain.from_iterable(i.split(splitter) for i in result)] + yield from filter(None, result) + +class Address: + """ + Methods return self unless otherwise stated + """ + def __init__(self, path:str): + self.path = path = normalize(path) + # if not os.path.exists(path): + # print(Warning(f'Warning: Path "{path}" does not exist')) + def __str__(self): + return self.path + def __repr__(self): + return self.path + def __hash__(self): + return hash(self.path) + + def create(self, content:[str, bytes, bytearray]=None, raw:bool=False): + """ + Create an entry in the file-system. If the address is not vacant no action will be taken. + The raw handle only works for Files and enables writing bytes/bytearrays + """ + if self.exists: + return self + elif content: + if raw: + if isinstance(content,(bytes,bytearray)): + fobj = open(self.path,'wb') + else: + fobj = open(self.path,'w') + fobj.write(content) + fobj.close() + else: + if isinstance(content,str): + content = bytes(content,encoding='utf-8') + else: + content = bytes(content) + with open(self.path,'wb') as fobj: + fobj.write(content) + else: + if likeFile(self.path): + with open(self.path,'x') as fobj: + pass + else: + os.makedirs(self.path,exist_ok=True) + return self + + @property + def exists(self): + return os.path.exists(self.path) + @property + def isdir(self): + if self.exists: + return os.path.isdir(self.path) + return not likeFile(self.path) + @property + def isfile(self): + if self.exists: + return os.path.isdir(self.path) + return likeFile(self.path) + + @property + def obj(self): + """ + Determine if self.path points to a file or folder and create the corresponding object + """ + if self.exists: + return File(self.path) if os.path.isfile(self.path) else Directory(self.path) + else: + return File(self.path) if likeFile(self.path) else Directory(self.path) + @property + def up(self): + """ + Return the ADir + """ + return Address(delevel(self.path)).obj + @property + def name(self): + """ + Return the name of the referent + """ + return os.path.split(self.path)[1] + + @property + def ancestors(self): + """ + Return consecutive ADirs until the ADrive is reached + """ + level = [] + p = self.path[:] + while p != delevel(p): + p = delevel(p) + level.append(p) + return tuple(Address(i).obj for i in level)[::-1] + @property + def colleagues(self): + """ + Every member of the same Directory whose type is the same as the referent + """ + return tuple(i for i in self.up if isinstance(i, type(self))) + @property + def neighbours(self): + """ + Everything in the same Directory + """ + return self.up.content + @property + def depth(self): + """ + Number of ancestors + """ + return len(self.ancestors) + @property + def top(self): + """ + The apparent drive. Will not be helpful if self.path is relative + """ + return self.ancestors[0] + @property + def stat(self): + """ + return os.stat(self.path) + """ + return os.stat(self.path) + + def delevel(self, steps:int=1, path:bool=False) -> str: + """ + Go up some number of levels in the File system + """ + return delevel(self.path,steps) if path else Directory(delevel(self.path,steps)) + def heritage(self): + """ + A fancy representation of the tree from the apparent drive up to the given path + """ + print(f'\nheritage({self.name.title()})') + ancs = list(self.ancestors) + ancs.append(self.path) + for i,anc in enumerate(ancs): + print('\t' + ('','.'*i)[i>0] + i*' ' + [i for i in str(anc).split(os.sep) if i][-1]) + return self + def touch(self): + """ + Implements the unix command 'touch', which updates the 'date modified' of the content at the path + """ + p = self.path + pathlib.Path(p).touch() + self = Address(p).obj + return self + def erase(self, recycle:bool=True): + """ + Send a File to the trash, or remove it without recycling. + """ + send2trash(self.path) if recycle else os.remove(self.path) + return self + def copy(self, folder:str=None, cwd:bool=False, sep:str='_'): + """ + Returns a clone of the referent at a given Directory-path + The given path will be created if it doesn't exist + Will copy in the File's original folder if no path is given + The cwd switch will always copy to the current working Directory + """ + copier = (shutil.copy2, shutil.copytree)[self.isdir] + if folder: + new = os.path.join(folder, self.name) + elif cwd: + new = os.path.join(os.getcwd(), self.name) + else: + new = self.path + new = nameSpacer(new, sep=sep) + os.makedirs(delevel(new), exist_ok=True) + copier(self.path, new) + out = Address(new).obj + return out.touch() if touch else out + def move(self, folder:str=None, name:str=None, dodge:str=False, sep:str='_'): + """ + addy.move(folder) -> move to the given Directory () + addy.move(name) -> move to the given path (relative paths will follow from the objects existing path) + addy.move(folder, name) -> move to the given Directory + + The dodge handle enables automatic file-system collision evasion + """ + if folder and name: + new = os.path.join(folder, name) + elif folder: + new = os.path.join(folder, self.name) + elif name: + new = os.path.join(self.up.path, name) + else: + raise ValueError('The file couldn\'t be moved because "name" and "folder" are undefined') + new = nameSpacer(new, sep=sep) if dodge else new + os.rename(self.path, new) + self.path = new + return self + def rename(self, name:str): + """ + Change the name of the referent + """ + return self.move(name=name) + def expose(self): + """ + Reveal the referent in the system's file explorer (will open the containing Directory if the referent is a File) + """ + if self.isdir: + os.startFile(self.path) + else: + os.startFile(self.up.path) + return self + +class File(Address): + def __init__(self,path:str): + path = os.path.abspath(trim(path)) + super(File,self).__init__(path) + self._stream = None + def __enter__(self): + self._stream = open(str(self),'b') + return self._stream + def __exit__(self, exc_type, exc_value, traceback): + self._stream.close() + self._stream = None + + @property + def size(self): + return size(os.stat(self.path).st_size) + @property + def mime(self): + return match.MIME if (match:=ft.guess(str(self))) else None + @property + def kind(self): + return self.mime.split('/')[0] if (match:=ft.guess(self.path)) else None + @property + def ext(self): + return os.path.splitext(self.name)[1] + @property + def title(self): + """ + return the File's name without the extension + """ + return os.path.splitext(self.name)[0] + def open(mode:str='r', scrape:bool=False): + """ + Return the File's byte or text stream. + Scrape splits the text at all whitespace and returns the content as a string + """ + if scrape: + with open(self.path, mode) as fobj: + return ' '.join(fobj.read().split()) + with open(self.path, mode) as fobj: + return fobj + +class Directory(Address): + def __init__(self, path:str): + path = os.path.abspath(trim(path)) + self.path = normalize(path) + super(Directory, self).__init__(path) + self._ind = -1 + def __len__(self): + return len(self.content) + def __bool__(self): + """ + Check if the Directory is empty or not + """ + return len(os.listdir(self.path))>0 + def __iter__(self): + return self + def __next__(self): + if self._ind Iterable: + """ + See help(self.search) + """ + return self.search(keyword, sort, case, **kwargs) + @property + def children(self): + """ + Return "os.listdir" but filtered for directories + """ + return tuple(addy.obj for i in os.listdir(self.path) if (addy:=Address(os.path.join(str(self),i))).isdir) + @property + def files(self): + """ + Return "os.listdir" but filtered for Files + """ + return tuple(addy.obj for i in os.listdir(self.path) if (addy:=Address(os.path.join(self.path,i))).isfile) + @property + def content(self): + """ + Return address-like objects from "os.listdir" + """ + return tuple(Address(os.path.join(self.path,i)).obj for i in os.listdir(self.path)) + @property + def leaves(self): + """ + Return All Files from all branches + """ + return tuple(self.gather()) + @property + def branches(self): + """ + Return Every Directory whose path contains "self.path" + """ + return tuple(set(i.delevel() for i in self.gather())) + @property + def size(self): + """ + Return Prettified version of _size + """ + return sum(File.size for File in self.leaves) + @property + def _size(self): + """ + Return sum of File sizes for all leaves + """ + return sum(File.size for File in self.leaves) + @property + def mimes(self): + """ + Return File mimes for all Files from all branches + """ + return tuple(set(File.mime for File in self.gather())) + @property + def kinds(self): + """ + Return File types for all Files from branches + """ + return tuple(set(m.split('/')[0] for m in self.mime)) + @property + def exts(self): + """ + Return extensions for all Files from all branches + """ + return tuple(set(f.ext for f in self.gather())) + @property + def isroot(self): + """ + Return check if the Directory is at the highest level of the File system + """ + return not self.depth + + def add(self, other, copy=False): + """ + Introduce new elements. Send an address-like object to self. + """ + if not other.exists: + raise OSError(f"{other.name.title()} could not be added to {self.name.title()} because it doesn't exist") + new = os.path.join(self.path, os.path.split(other.path)[-1]) + other.rename(new) + return self + + def enter(self): + """ + Set referent as current working Directory + """ + os.chdir(self.path) + + def gather(self, titles:bool=False, walk:bool=True, ext:str='') -> gen: + """ + Generate an iterable of the Files rooted in a given folder + It is possible to search for multiple File extensions if you separate each one with a space, comma, asterisk, or tilde. + Only use one symbol per gathering though. + """ + folder = self.path + if walk: + if ext: + ext = ext.replace('.', '') + sep = [i for i in ',`* ' if i in ext] + pattern = '|'.join(f'\.{i}$' for i in ext.split(sep[0] if sep else None)) + pat = re.compile(pattern, re.I) + for root, folders, names in os.walk(folder): + for name in names: + if os.path.isfile(p:=os.path.join(root, name)) and pat.search(name) and name!='NTUSER.DAT': + yield name if titles else p + else: + for root, folders, names in os.walk(folder): + for name in names: + if os.path.exists(p:=os.path.join(root, name)): + yield name if titles else p + else: + if ext: + ext = ext.replace('.', '') + sep = [i for i in ',`* ' if i in ext] + pattern = '|'.join(f'\.{i}$' for i in ext.split(sep[0] if sep else None)) + pat = re.compile(pattern, re.I) + for name in os.listdir(folder): + if os.path.isfile(p:=os.path.join(folder, name)) and pat.search(name) and name!='NTUSER.DAT': + yield name if titles else p + else: + for name in os.listdir(folder): + if os.path.isfile(p:=os.path.join(folder, name)): + yield name if titles else p + + def search(self, keyword:str, sort:bool=False, case:bool=False, prescape:bool=False, **kwargs) -> Iterable: + """ + Return an iterator of Files whose path match the given keyword within a Directory. + The search is linear and the sorting is based on the number of matches. If sorted, a list will be returned. + Case pertains to case-sensitivity + Prescape informs the method that kewords do not need to be escaped + For kwargs see help(self.gather) + """ + casesensitivity = (re.I, 0)[case] + escaper = (re.escape, id)[prescape] + if isinstance(keyword, str): + keyword = keyword.split() + if not isinstance(keyword, str): + keyword = '|'.join(map(escaper, keyword)) + if sort: + return sorted( + filter( + # lambda x: re.search(keyword, x, casesensitivity), + lambda x: len([*re.finditer(keyword, x, casesensitivity)]) == len(keyword.split('|')), + self.gather(**kwargs), + ), + key=lambda x: len([*re.finditer(keyword, x, casesensitivity)]), + reverse=True + ) + else: + return filter( + # lambda x: re.finditer(keyword, x, casesensitivity), + lambda x: re.search(keyword, x, casesensitivity), + self.gather(**kwargs) + ) +if __name__ == '__main__': + mock = 'drive: users user place subplace File.extension.secondextension'.split() + # mock = 'drive: users user place subplace readme'.split() + testPaths = [ + ''.join(mock), + os.path.join(*mock), + os.path.join(*mock[:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[3:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[:-1]), + '/'.join(mock[:-1]), + mock[-1] + ] + show(zip(map(type,map(lambda x: Address(x).obj, testPaths)),testPaths)) + # show(zip(testPaths,map(normalize,testPaths))) + # show(zip(testPaths,map(hasdirs,testPaths))) + # show(zip(testPaths,map(likeFile,testPaths))) + + # ddemo = Address(os.path.join(*mock[:-1])).obj + # fdemo = Address(os.path.join(*mock)).obj + # ddemo = Directory(os.path.join(*mock[:-1])) + # fdemo = File(os.path.join(*mock)) + + fp = r'c:\users\kenneth\pyo_rec.wav' + dp = r'c:\users\kenneth\videos' + + d = Directory(dp) + f = File(fp) + + system = (d, f) + show(d('slowthai',sort=True)) + show(d('alix melanie', sort=True)) + + print(formats['all']) + for v in formats.values(): + print(' '.join(v)) \ No newline at end of file diff --git a/filey/preamble/magnitudes.py b/filey/preamble/magnitudes.py new file mode 100644 index 0000000..b310516 --- /dev/null +++ b/filey/preamble/magnitudes.py @@ -0,0 +1,122 @@ +""" +Compute the correct order of magnitude of an integer in a given dimension +""" +all = [ + 'represent', + 'force', +] + + +from dataclasses import dataclass + +# from m3ta import sample,show,nopes +from sl4ng import sample,nopes + + +orders = { + 0:'mono', + 1:'deca', + 2:'hecto', + 3:'kilo', + 6:'mega', + 9:'giga', + 12:'tera', + 15:'peta', + 18:'exa', + 21:'zetta', + 24:'yotta', +} + +sredro = {v:k for k,v in orders.items()} + +pretty = lambda number,unit='': f'{number:,} {unit}'.strip() +setcase = lambda unit,lower=False: [unit.upper().strip(),unit.lower().strip()][lower] +setlength = lambda mag,dim,long=False,sep='-': ('',sep)[long].join(((mag[0],dim[0]),(mag,dim))[long]) + +# def getmagnitude(value,never='mono deca hecto'.split()): + # mags = tuple(sorted(i for i in orders.keys() if not orders[i] in never)) + # booly = lambda i: len(str(int(value))) < len(str(10**mags[i+1])) + # fits = tuple(nopes((booly(i) for i in range(len(mags)-1)),True)) + # fit = mags[min(fits) if fits else len(mags)-1] + # return orders[fit] + +def magnitude(value,omissions='mono deca hecto'.split()): + mags = tuple(sorted(i for i in orders.keys() if not orders[i] in omissions)) + booly = lambda i: len(str(int(value))) < len(str(10**mags[i+1])) + fits = tuple(nopes((booly(i) for i in range(len(mags)-1)),True)) + fit = mags[min(fits) if fits else len(mags)-1] + return orders[fit] + +# def represent(self,dim='bytes',long=False,lower=False,precision=2,sep='-',omissions='mono deca hecto'.split()): + # mag = magnitude(self,omissions) + # unit = setcase(setlength(mag,dim,long,['',sep][long]),lower) + # number = round(self*10**-sredro[mag],precision) + # out = pretty(number,unit) + # return (out.upper(),out.lower())[lower] + + +def represent(self,dim='bytes',long=False,lower=False,precision=2,sep='-',omissions='mono deca hecto'.split()): + return force(self,mag=magnitude(self,omissions),dim=dim,long=long,lower=lower,precision=precision,sep=sep) + +def force(self,mag='kilo',dim='bytes',long=False,lower=False,precision=2,sep='-'): + # precision = sredro[mag] + unit = setcase(setlength(mag,dim,long,sep),lower) + val = round(self*10**-(sredro[mag]),precision) + return pretty(val,unit) + +def f2(self,dim='bytes',long=False,lower=False,precision=2,sep='-',omissions='mono deca hecto'.split()): + """ + This should behave well on int subclasses + """ + mag = magnitude(self,omissions) + precision = sredro[mag] if self<5 else precision + unit = setcase(setlength(mag,dim,long,sep),lower) + val = round(self*10**-(sredro[mag]),precision) + return pretty(val,unit) + + +def file_size(self,dim='bytes',mag='kilo',long=False,lower=False,precision=2,sep='-',never='mono deca hecto'.split(),force=False): + if force: + return force(self,mag=mag,dim=dim,long=long,lower=lower,precision=precision,sep=sep) + return rep(self,dim=dim,long=long,lower=lower,precision=precision,sep=sep,never=never) + + + + +if __name__ == '__main__': + band = lambda level,base=10,shift=0,root=1: tuple(root+i+shift for i in ((level+1)*base,level*base))[::-1] + format = lambda selection: int(''.join(str(i) for i in selection)) + + digits = range(*band(0)) + + l = 0 + s = 0 + b = 30 + r = 1 + x,y = band(l,b,s,r) + # print(x,y) + val = sample(digits,y) + sizes = [format(val[:i][::-1]) for i in range(x,y)] + # show(sizes) + # sizes = [44259260028,315436] + + for size in sizes[::-1]: + # print( + string = f"""{(len(pretty(max(sizes)))-len(pretty(size)))*' '+pretty(size)} + {size = } + {len(str(size)) = } + {force(size) = } + {force(size,mag=magnitude(size)) = } + {f2(size) = } + {represent(size) = } + {magnitude(size) = } + + + """.splitlines() + print(*((x.strip(),x)[i<1] for i,x in enumerate(string)),sep='\n\t',end='\n\n') + # ) + print(pretty(sizes[-1])) + print(band(l,s,b,r)) + print(x,y) + print(f'{format(digits):,}') + # print(all(rep(size)==rep2(size) for size in sizes)) \ No newline at end of file diff --git a/filey/preamble/utils.py b/filey/preamble/utils.py new file mode 100644 index 0000000..7bbc919 --- /dev/null +++ b/filey/preamble/utils.py @@ -0,0 +1,24 @@ +from sl4ng import gen, show, chords, flatten as chain +from itertools import chain, tee +from typing import Sequence + +def multisplit(splitters:Sequence[str], target:str) -> gen: + """ + Split a string by a sequence of arguments + >>> list(multisplit(',`* ', 'wma,wmv mp3`vga*mp4 ,`* ogg')) + ['wma', 'wmv', 'mp3', 'vga', 'mp4', 'ogg'] + """ + result = target.split(splitters[0]) + for splitter in splitters[1:]: + result = [*chain.from_iterable(i.split(splitter) for i in result)] + # yield from [i for i in result if i] + yield from filter(None,result) + + + +# multisplit('a b c'.split(), 'carrot cabbage macabre'.split(',')) +x, y = tee(multisplit('a b c'.split(), 'carrot cabbage macabre')) +x, y = tee(multisplit(',`* ', 'carrot cabbage macabre')) +x, y = tee(multisplit(',`* ', 'wma,wmv mp3`vga*mp4 ,`* ogg')) +show(x) +print(list(y)) \ No newline at end of file diff --git a/filey/testdir/testfile.ext b/filey/testdir/testfile.ext new file mode 100644 index 0000000..e69de29 diff --git a/filey/testdir/testfile_2.ext b/filey/testdir/testfile_2.ext new file mode 100644 index 0000000..e69de29 diff --git a/filey/testdir/testfile_3.ext b/filey/testdir/testfile_3.ext new file mode 100644 index 0000000..e69de29 diff --git a/filey/utils/__init__.py b/filey/utils/__init__.py new file mode 100644 index 0000000..6b760bc --- /dev/null +++ b/filey/utils/__init__.py @@ -0,0 +1,4 @@ +from .strings import * +from .magnitudes import * +from .debug import * +from .media import * \ No newline at end of file diff --git a/filey/utils/debug.py b/filey/utils/debug.py new file mode 100644 index 0000000..771867a --- /dev/null +++ b/filey/utils/debug.py @@ -0,0 +1,99 @@ +__all__ = 'show pop sample nopes generator tipo'.split() + + +from typing import Iterable, Any +import sys, os + + +generator = type(i for i in '') + +def show(array:Iterable[Any], indentation:int=0, enum:bool=False, first:int=1, indentor:str='\t', tail=True, head=False, file=sys.stdout, sep:str=None) -> None: + """ + Print each element of an array. This will consume a generator. + """ + if (wasstr:=isinstance(file,str)): file = open(file) + print('\n', file=file) if head else None + for i,j in enumerate(array,first): + print( + ( + f"{indentation*indentor}{j}", + f"{indentation*indentor}{i}\t{j}" + )[enum], + sep='', + file=file + ) + if sep: print(sep) + print('\n', file=file) if tail else None + if wasstr: file.close() + + +def pop(arg:Any=None, file=False, silent:bool=False) -> str: + """ + Open the folder containing a given module, or object's module + Open current working directory if no object is given + Open the module's file if file=True + Return the path which is opened + + This will raise an attribute error whenever there is no file to which the object/module is imputed + """ + import os + module = type(os) + if arg: + if isinstance(arg, module): + path = arg.__file__ + else: + mstr = arg.__module__ + if (top:=mstr.split('.')[0]) in globals().keys(): + m = eval(mstr) + else: + t = exec(f'import {top}') + m = eval(mstr) + path = m.__file__ + if not file: + path = os.path.dirname(path) + else: + path = os.getcwd() + if not silent: + os.startfile(path) + return path + + +def sample(iterable:Iterable, n:int) -> tuple: + """ + Obtains a random sample of any length from any iterable and returns it as a tuple + Dependencies: random.randint(a,b) + In: iterable, lengthOfDesiredSample + Out: iterable of length(n) + """ + import random as r + iterable = tuple(iterable) if type(iterable)==type({0,1}) else iterable + choiceIndices = tuple(r.randint(0,len(iterable)-1) for i in range(n)) + return tuple(iterable[i] for i in choiceIndices) + + +def nopes(iterable:Iterable[Any], yeps:bool=False) -> generator: + """ + if yeps==False + Return the indices of all false-like boolean values of an iterable + Return indices of all true-like boolean values of an iterable + dependencies: None + example + t = (0,1,0,0,1) + >>> tuple(nopes(t)) + (0,2,3) + >>> tuple(nopes(t,True)) + (1,4) + """ + for i, j in enumerate(iterable): + if (not j, j)[yeps]: + yield i + + +def tipo(inpt:Any=type(lambda:0)) -> str: + """ + Return the name of an object's type + Dependencies: None + In: object + Out: str + """ + return str(type(inpt)).split("'")[1].split('.')[-1] \ No newline at end of file diff --git a/filey/utils/magnitudes.py b/filey/utils/magnitudes.py new file mode 100644 index 0000000..c09da2b --- /dev/null +++ b/filey/utils/magnitudes.py @@ -0,0 +1,100 @@ +""" +Making numbers readable and dimensional one order of magnitude at a time +""" +__all__ = 'nice_size'.split() + +from dataclasses import dataclass + +from .debug import sample, show, nopes + + +orders = { + 0: 'mono', + 1: 'deca', + 2: 'hecto', + 3: 'kilo', + 6: 'mega', + 9: 'giga', + 12: 'tera', + 15: 'peta', + 18: 'exa', + 21: 'zetta', + 24: 'yotta', +} + +sredro = {v:k for k,v in orders.items()} + +def lasso(number:complex, unit:str='') -> str: + """ + Make a large number more readable by inserting commas before every third power of 10 and adding units + """ + return f'{number:,} {unit}'.strip() + +def set_case(unit:str, lower:bool=False) -> str: + """ + Set the case of a number's unit string + """ + return [unit.upper().strip(), unit.lower().strip()][lower] + +def set_length(mag:str, unit:str, long:bool=False, sep:str='-') -> str: + """ + Set the length of a number's unit string + """ + return ('', sep)[long].join(((mag[0], unit[0]), (mag, unit))[long]) + +def magnitude(value:complex, omissions:list='mono deca hecto'.split()): + """ + Determine the best order of magnitude for a given number + """ + mags = tuple(sorted(i for i in orders.keys() if not orders[i] in omissions)) + booly = lambda i: len(str(int(value))) < len(str(10**mags[i+1])) + fits = tuple(nopes((booly(i) for i in range(len(mags)-1)),True)) + fit = mags[min(fits) if fits else len(mags)-1] + return orders[fit] + +def nice_size(self:complex, unit:str='bytes', long:bool=False, lower:bool=False, precision:int=2, sep:str='-', omissions:list='mono deca hecto'.split()): + """ + This should behave well on int subclasses + """ + mag = magnitude(self, omissions) + precision = sredro[mag] if self<5 else precision + unit = set_case(set_length(mag, unit, long, sep), lower) + val = round(self*10**-(sredro[mag]), precision) + return lasso(val, unit) + + + + + +if __name__ == '__main__': + band = lambda level, base=10, shift=0, root=1: tuple(root+i+shift for i in ((level+1)*base, level*base))[::-1] + format = lambda selection: int(''.join(str(i) for i in selection)) + + digits = range(*band(0)) + + l = 0 + s = 0 + b = 30 + r = 1 + x,y = band(l, b, s, r) + # print(x,y) + val = sample(digits,y) + sizes = [format(val[:i][::-1]) for i in range(x, y)] + # show(sizes) + # sizes = [44259260028,315436] + + for size in sizes[::-1]: + string = f"""{(len(lasso(max(sizes)))-len(lasso(size)))*' '+lasso(size)} + {size = } + {len(str(size)) = } + {nice_size(size) = } + {magnitude(size) = } + + + """.splitlines() + print(*((x.strip(), x)[i<1] for i, x in enumerate(string)), sep='\n\t', end='\n\n') + print(lasso(sizes[-1])) + print(band(l,s,b,r)) + print(x,y) + print(f'{format(digits):,}') + # print(all(rep(size)==rep2(size) for size in sizes)) \ No newline at end of file diff --git a/filey/utils/media.py b/filey/utils/media.py new file mode 100644 index 0000000..8df4cd0 --- /dev/null +++ b/filey/utils/media.py @@ -0,0 +1,191 @@ +__all__ = 'commons ffplay delevel convert sortbysize'.split() + +from typing import Sequence, Iterable +import os, time, re, subprocess, sys + +import filetype as ft + +from .debug import show + + +defaultScriptDirectory = r'e:\projects\monties' +commons = { + """ + Hopefully such hacks as these are now obsolete + """ + 'music': os.path.join(os.path.expanduser('~'), 'music', 'collection'), + 'images': os.path.join(os.path.expanduser('~'), 'pictures'), + 'pictures': os.path.join(os.path.expanduser('~'), 'pictures'), + 'pics': os.path.join(os.path.expanduser('~'), 'pictures'), + 'videos': os.path.join(os.path.expanduser('~'), 'videos'), + 'ytdls': { + 'music': { + 'singles': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'music', 'singles'), + 'mixes': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'music', 'mixes'), + 'albums': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'music', 'album'), + }, + 'graff': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'graff'), + 'bullshit': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'bullshitters'), + 'bull': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'bullshitters'), + 'code': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'cscode'), + 'cs': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'cscode'), + 'maths': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'maths'), + 'math': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'maths'), + 'movies': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'movies'), + 'other': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'other'), + 'physics': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'physics'), + 'phys': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'physics'), + 'politics': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'politics'), + 'pol': os.path.join(os.path.expanduser('~'), 'videos', 'ytdls', 'politics'), + }, + 'documents': os.path.join(os.path.expanduser('~'), 'documents'), + 'docs': os.path.join(os.path.expanduser('~'), 'documents'), + 'downloads': os.path.join(os.path.expanduser('~'), 'downloads'), + 'desktop': os.path.join(os.path.expanduser('~'), 'desktop'), + 'books': os.path.join(os.path.expanduser('~'), 'documents', 'bookes'), + 'monties': os.path.join(defaultScriptDirectory, str(time.localtime()[0])), + 'scripts': defaultScriptDirectory, + 'demos': os.path.join(defaultScriptDirectory, 'demos'), + 'site': os.path.join(sys.exec_prefix, 'lib', 'site-packages'), + 'home': os.path.expanduser('~'), + 'user': os.path.expanduser('~'), + 'root': os.path.expanduser('~'), + '~': os.path.expanduser('~'), +} +os.makedirs(commons['monties'], exist_ok=True) + + +def ffplay(files:Sequence[str], hide:bool=True, fullscreen:bool=True, loop:bool=True, quiet:bool=True, randomize:bool=True, silent:bool=False) -> None: + """ + Play a collection of files using ffmpeg's ffplay cli + Dependencies: FFMPEG,subprocess,os + In: files,fullscreen=True,quiet=True + Out: None + + If entering files as a string, separate each path by an asterisk (*), othewise feel free to use any iterator + -loop {f"-loop {loop}" if loop else ""} + """ + namext = lambda file: os.path.split(file)[1] + nome = lambda file: os.path.splitext(namext(file))[0] + ext = lambda file: os.path.splitext(file)[1] + isvid = lambda file: ft.match(file) in ft.video_matchers + vidtitle = lambda vid: '-'.join(i.strip() for i in vid.split('-')[:-1]) + albumtrack = lambda file: bool(re.search(f'\d+\s.+{ext(file)}', file, re.I)) + attitle = lambda file: ' '.join(i.strip() for i in nome(file).split(' ')[1:]) + aov = lambda file: albumtrack(file) or isvid(file) + title = lambda file: ''.join(i for i in os.path.splitext(namext(file)[1])[0] if i not in '0123456789').strip() + windowtitle = lambda file: [namext(file),[attitle(file),vidtitle(file)][isvid(file)]][aov(file)] + play = lambda file: subprocess.run(f'ffplay {("", "-nodisp")[hide]} -window_title "{windowtitle(file)}" -autoexit {"-fs" if fullscreen else ""} {"-v error" if quiet else ""} "{file}"') + files = files.split('*') if isinstance(files,str) else files + if loop: + while (1 if loop==True else loop+1): + files = shuffle(files) if randomize else files + for i,f in enumerate(files, 1): + if os.path.isdir(f): + fls = [os.path.join(f, i) for i in gather(f, names=False)] + for j,file in enumerate(fls, 1): + name = os.path.split(file)[1] + print(f'{j} of {len(fls)}:\t{name}') if not silent else None + ffplay(file, hide, fullscreen, False, quiet, randomize, True) + else: + folder,name = os.path.split(f) + print(f'{i} of {len(files)}:\t{name}') if not silent else None + play(f) + loop -= 1 + else: + files = shuffle(files) if randomize else files + for i, f in enumerate(files, 1): + if os.path.isdir(f): + fls = [os.path.join(f, i) for i in gather(f, names=False)] + for j, file in enumerate(fls, 1): + name = os.path.split(file)[1] + print(f'{j} of {len(fls)}:\t{name}') if not silent else None + ffplay(file, hide, fullscreen, False, quiet, randomize, True) + else: + print(f'{i} of {len(files)}:\t{title(f)}') if not silent else None + play(f) + + +def delevel(path:str, steps:int=1) -> str: + """ + This will climb the given path tree by the given number of steps. + No matter how large the number of steps, it will stop as soon as it reaches the root. + Probably needs revision for paths on systems which hide the root drive. + example + >>> for i in range(4):print(delevel(r'c:/users/admin',i)) + c:/users/admin + c:/users + c:/ + c:/ + dependencies: os.sep + """ + while steps and (len(path.split(os.sep))-1): + path = os.sep.join((path.split(os.sep)[:-1])) + steps -= 1 + return path if not path.endswith(':') else path+os.sep + + +def convert(file:str, format:str='.wav', bitRate:int=450, delete:bool=False, options:str=''): + """ + Convert an audio file + Dependencies: m3ta.nameUpdater, send2trash, ffmpeg, subprocess, os + In: file,format='.wav',bitRate=450,delete=False,options='' + Outs: None + """ + trash = tryimport('send2trash','send2trash','remove','os') + os.chdir(os.path.split(file)[0]) + + _title = lambda file: file.split(os.sep)[-1].split('.')[0] + _new = lambda file,format: nameUpdater(_title(file)+format) + _name = lambda file: file.split(os.sep)[-1] + format = '.' + format if '.' != format[0] else format + + name = _title(file) + new = _new(file,format) + cmd = f'ffmpeg -y -i "{file}" -ab {bitRate*1000} "{new}"' if bitRate != 0 else f'ffmpeg {options} -y -i "{file}" "{new}"' + announcement = f"Converting:\n\t{file} --> {new}\n\t{cmd=}" + print(announcement) + subprocess.run(cmd) + print('Conversion is complete') + if delete: + trash(file) + print(f'Deletion is complete\n\t{new}\n\n\n') + return new + + +def sortbysize(files:Iterable[str]=None) -> list: + """ + Sort a collection of file paths by the size of the corresponding files (largest to smallest) + Dependencies: os + In: iterableCollection + Out: list + """ + files = [os.getcwd(), list(files)][bool(files)] + size = lambda file: os.stat(file).st_size + out = [] + while len(files)>0: + sizes = set(size(file) for file in files) + for file in files: + if size(file) == max(sizes): + out.append(file) + files.remove(file) + return out + + +if __name__ == '__main__': + mock = 'drive: users user place subplace file.extension.secondextension'.split() + # mock = 'drive: users user place subplace readme'.split() + testPaths = [ + ''.join(mock), + os.path.join(*mock), + os.path.join(*mock[:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[3:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[:-1]), + '/'.join(mock[:-1]), + mock[-1] + ] + show(testPaths) + # show(zip(map(type,map(lambda x: Address(x).obj, testPaths)),testPaths)) + # show(zip(testPaths,map(normalize,testPaths))) + # show(zip(testPaths,map(hasdirs,testPaths))) + # show(zip(testPaths,map(likeFile,testPaths))) \ No newline at end of file diff --git a/filey/utils/strings.py b/filey/utils/strings.py new file mode 100644 index 0000000..3977cb1 --- /dev/null +++ b/filey/utils/strings.py @@ -0,0 +1,123 @@ +__all__ = "trim normalize hasdirs likefile multisplit forbiddens nameSpacer".split() + +from typing import Iterable +import os, re + +from .debug import * + + +forbiddens = r'\/:?*<>|"' + +def trim(path:str, edge:str=os.sep) -> str: + """ + Remove trailing/leading separators (or edges) from a path string + """ + out = path[:] + while any(out.startswith(x) for x in (edge,' ')): + out = out[1:] + while any(out.endswith(x) for x in (edge,' ')): + out = out[:-1] + return out + + +def normalize(path:str, relative:bool=False, force:bool=False) -> str: + """ + Standardize a path for a current operating system. + Given an equivalently structured file/project system, this should make code reusable across platforms + If force is true, all forbidden characters will be replaced with an empty string + """ + other = ''.join(i for i in '\/' if not i==os.sep) + if force: + new = ''.join(i for i in path if not i in forbiddens) + else: + new = path[:] + if other in path: + terms = [] + for term in path.split(os.sep): + if other in term: + for part in term.split(other): + terms.append(part) + else: + terms.append(term) + new = os.path.join(*terms) + if relative: + new = '.'+os.sep+path + return new + + +def hasdirs(path:str) -> bool: + """ + check if a path string contains directories or not + """ + return bool(re.search(re.escape(os.sep), normalize(path))) + + +def likefile(path:str) -> bool: + """ + Check if a path string looks like a file or not + """ + path = normalize(path) + return bool(re.search('readme$|.+\.(\S)+$',path.split(os.sep)[-1], re.I)) + + +def multisplit(splitters:Iterable[str], target:str='abc') -> generator: + """ + Split a string by a the elements of a sequence + >>> list(multisplit(',`* ', 'wma,wmv mp3`vga*mp4 ,`* ogg')) + ['wma', 'wmv', 'mp3', 'vga', 'mp4', 'ogg'] + """ + splitters = iter(splitters) + result = target.split(next(splitters)) + for splitter in splitters: + result = [*chain.from_iterable(i.split(splitter) for i in result)] + yield from filter(None, result) + + +def nameSpacer(path:str, sep:str='_') -> str: + """ + Returns a unique version of a given string by appending an integer + Dependencies: os module + In: string + Out: string + """ + id = 2 + oldPath = path[:] + while os.path.exists(path): ##for general use + newPath = list(os.path.splitext(path)) + if sep in newPath[0]: + if newPath[0].split(sep)[-1].isnumeric(): + # print('case1a') + id = newPath[0].split(sep)[-1] + newPath[0] = newPath[0].replace(f'{sep}{id}', f'{sep}{str(int(id)+1)}') + path = ''.join(newPath) + else: + # print('case1b') + newPath[0] += f'{sep}{id}' + path = ''.join(newPath) + id += 1 + else: + # print('case2') + newPath[0] += f'{sep}{id}' + path = ''.join(newPath) + id += 1 + return path + +if __name__ == '__main__': + mock = 'drive: users user place subplace file.extension.secondextension'.split() + # mock = 'drive: users user place subplace readme'.split() + testPaths = [ + ''.join(mock), + os.path.join(*mock), + os.path.join(*mock[:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[3:4])+'/'+'/'.join(mock[4:]), + os.path.join(*mock[:-1]), + '/'.join(mock[:-1]), + mock[-1] + ] + tests = [(eval(f'{f}(r"{p}")'), f, p) for f in __all__ for p in testPaths] + + show(tests) + # show(zip(map(type, map(lambda x: Address(x).obj, testPaths)), testPaths)) + # show(zip(testPaths, map(normalize, testPaths))) + # show(zip(testPaths, map(hasdirs, testPaths))) + # show(zip(testPaths, map(likefile, testPaths))) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0ad39d0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..cf315f9 --- /dev/null +++ b/setup.py @@ -0,0 +1,32 @@ +from setuptools import setup, find_packages + +with open('README.md', 'r') as fob: + long_description = fob.read() + +setup( + name='filey', + version='0.0.1', + author='Kenneth Elisandro', + author_email='eli2and40@tilde.club', + url='https://tildegit.org/eli2and40/cabinet', + # packages=find_packages(), + packages=['filey'], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + long_description=long_description, + long_description_content_type='text/markdown', + keywords='utilities productivity', + license='MIT', + requires=[ + 'filetype', + 'audio_metadata', + 'send2trash', + 'sl4ng' + ], + # py_modules=['filey'], + python_requires='>=3.8', + +) \ No newline at end of file