diff --git a/.gitignore b/.gitignore index 25ff4b0..8cb4b27 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +filey/testdir +readme_test.py diff --git a/README.md b/README.md index e69de29..0f7332f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,190 @@ +from typing import Dict, List, Callable +from subprocess import run, Popen +from pathlib import Path +from time import sleep +import re, os, platform, sys + +from pyperclip import copy, paste +from sl4ng import show, getsource, pop, unzip, main, join, ffplay, regenerator, kill, kbd, convert +from filey import Address, Directory, File +import sl4ng, filey + +def clear(): + Popen('clear') + sleep(.2) +def cls(): + clear() + show(map(repr, cd), head=False, tail=False) + +architecture = platform.architecture() + +escaped = tuple(i for i in kbd() if re.escape(i)!=i) + +this = __file__ +here = os.path.dirname(__file__) + +c = Directory('c:/') +e = Directory('e:/') +f = Directory('f:/') + +cd = Directory('.') +user = Directory('~') +documents = user/'documents' +root = c if c.exists else None +appdata = user/'appdata' + +downs = user / 'Downloads' +web = downs / 'tools/languages' # need library class + +dev = Directory(r"e:/") +sp = dev / 'shellpower' + +git = dev / 'gitting' +clones = git/'gitclone/clones' +ignore = clones / 'github/gitignore' +mdn = git / r'gitclone\clones\mdn\content\files\en-us' +brython = git / r'gitclone\clones\brython-dev\brython\www\doc\en' + +projects = dev / 'projects' +monties = projects / 'monties' + +site = dev / 'Languages\Python38-64\lib' +docs = monties / 'docs' + +prod = Directory(r'f:/') +flps = prod / 'programs/project files/fl' + +def killfl(): + kill('fl64') + kill('ilbridge') + +def chdir(path): + global cd + # import os + os.chdir(path) + cd = Directory(path) +# os.chdir = chdir + +# p = videos / "please refrain from touching the pelican\\told you so" +# play = lambda x, d=p: ffplay((d/x).path, hide=False, loop=False, fullscreen=False) +# rename = lambda x, y, d=p: (d/x).rename(y) +# find = lambda words, ext='': show + +class Scanner: + """ + Create an object which scans a text file for a given keyword + """ + def __init__(self, keywords:str, mode:str='r', strict:bool=True, prescaped:bool=False, casefold:bool=True, opener:Callable=open, lines:bool=True): + """ + params: + keywords + terms to search for + mode + 'r' or 'rb' + strict + True -> search for words + False -> clauses + prescaped + whether or not terms have already been regex escaped + casefold + true -> case insensitive + opener + must return an object with a "readlines" or "read" method (depends on lines) + lines + + """ + self.casefold = casefold + self.keywords = keywords + self.opener = opener + self.lines = lines + self.mode = mode + self.strict = strict + self.prescaped = prescaped + @property + def __keywords(self): + """ + handles any necessary escaping + """ + return re.escape(self.keywords) if not self.prescaped else self.keywords + @property + def __casefold(self): + """ + standardize the case-fold setting + """ + return re.I if self.casefold else 0 + @property + def pattern(self): + """ + compile the search pattern + """ + return re.compile( + ( + join(self.__keywords.split(), '|'), + self.__keywords + )[self.strict], + self.__casefold + ) + def __call__(self, path:str, lines:bool=None): + """ + Scan a file at a given path for a predefined word/clause + """ + if isinstance(lines, type(None)): + lines = self.lines + with self.opener(path, self.mode) as fob: + if lines: + return self.pattern.search(path) or any(map(self.pattern.search, fob.readlines())) + return self.pattern.search(path) or any(map(self.pattern.search, fob.read())) +def scan(keywords:str, mode='r', strict=True, prescaped=False, casefold=True): + casefold = (0, re.I)[casefold] + keywords = re.escape(keywords) if not prescaped else keywords + pattern = re.compile(keywords if strict and not prescaped else join(keywords.split(), '|'), casefold) + def wrap(path): + with open(path, mode) as fob: + return any(map(pattern.search, fob.readlines())) or pattern.search(path) + return wrap +# show(user('', ext='txt')) + +def philter(func, itr, start=1, index=False): + for i, e in enumerate(itr, start): + if any(j in e for j in 'venv'.split()): + continue + else: + try: + if func(e): + # yield i, e if index else e + yield (e, (i, e))[index] + except UnicodeDecodeError: + # print(f"UnicodeDecodeError @ {e}") + # raise + # break + continue +def docsearch(keywords:str, location:Directory=monties, ext:str='py'): + show(filter(scan(keywords), location('', ext=ext))) +exotics = { + "cdot": "·", +} +exords = {key: ord(val) for key, val in exotics.items()} +tfs = [ + r'C:\Users\Kenneth\childlist.txt', + r'C:\Users\Kenneth\file.txt', + # r'C:\Users\Kenneth\frozenpips.txt', + r'C:\Users\Kenneth\parentlist.txt', + # r'C:\Users\Kenneth\pipfreeze.txt', + # r'C:\Users\Kenneth\pipsweets.txt', +] + + +# tword = 'def clone' +# show(filter(scan(tword), monties('', ext='py'))) +# show(map(pop, filter(scan(tword), tfs))) +# show(filter(scan(tword), tfs)) + + +# show(downs('tensor', ext='pdf')) +# with open('') +# with (Directory('~') / 'pyo_rec.wav').open('r') as fob: + # help(fob) + # print(fob.read()) + # fob = fob.open( +# f = (Directory('~') / 'pyo_rec.wav') +# help(f.open) diff --git a/filey/README.md b/filey/README.md deleted file mode 100644 index e3da389..0000000 Binary files a/filey/README.md and /dev/null differ diff --git a/filey/__init__.py b/filey/__init__.py index 0ba32e2..b7b778a 100644 --- a/filey/__init__.py +++ b/filey/__init__.py @@ -1,605 +1,14 @@ -""" -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. +from .handles import * +from .shell import * +from .walking import * +from .persistence import * +from .shortcuts import * -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 +if __name__ == "__main__": + pass \ No newline at end of file diff --git a/filey/handles.py b/filey/handles.py new file mode 100644 index 0000000..2aa3747 --- /dev/null +++ b/filey/handles.py @@ -0,0 +1,709 @@ +""" +A collection of route objects to help you manage your filey Things and Places elegantly +Exports the following aliases: + File <-> Thing + Directory <-> Place + Path/Address <-> Thing +Thing 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 Thing/Place + else + the drive is the name/letter of the disk partition on which the content at the address is stored. +"Apparent Place" 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 Thing/Place + 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__ = 'forbiddens Thing Place Thing Path Address Directory Folder File Library'.split() + +from itertools import chain +from typing import Dict, Iterable, Iterator, List, Sequence, TypeAlias +from warnings import warn +import io, os, pathlib, re, sys, shutil + +from send2trash import send2trash +from sl4ng import unique, nice_size +from sl4ng import pop, show +import audio_metadata as am, filetype as ft + +from . import shell, walking + + +formats = { # incase mimes fail + 'pics': "bmp png jpg jpeg tiff svg psd".split(), + 'music': "mp3 m4a wav ogg wma flac aiff alac flp live".split(), + 'videos': "mp4 wmv".split(), + 'docs': "doc docx pdf xlsx pptx ppt xls csv".split(), +} +formats['all'] = [*chain.from_iterable(formats.values())] + + +forbiddens = r'\/:?*<>|"' + + +_Path:TypeAlias = "Thing" +_Pathstr:TypeAlias = "Thing|str" +_Place:TypeAlias = "Place" +_Placestr:TypeAlias = "Place|str" +_Placefile:TypeAlias = "Place|File" +_File:TypeAlias = "File" +_Filestr:TypeAlias = "File|str" +SYSTEM_PATH:TypeAlias = type(pathlib.Path(__file__)) + + +class MemorySize(int): + """ + Why should you have to sacrifice utility for readability? + """ + def __repr__(self): + return nice_size(self) + +class Library: + """ + Allows categorization for multiple searching and can also be used as a makeshift playlist + """ + def __init__(self, *paths): + self.paths = [] + for p in paths: + if os.path.exists(str(p)): + self.paths.append(str(p)) + self.index = -1 + def __iter__(self) -> Iterator[str]: + return self + def __next__(self) -> str: + if self.index < len(self.paths) - 1: + self.index += 1 + return self.paths[self.index] + self.index = -1 + raise StopIteration + def __call__(self, terms, **kwargs) -> Iterator[str]: + """ + Find files under directories in self.paths matching the given terms/criteria + Any files in self.path will also be yielded if they match + + Params + Args + terms:str + the terms sought after. + separate by spaces + an empty string simply walks the full tree + Kwargs + exts:str + any file extensions you wish to check for, separate by spaces + case:bool + toggle case sensitivity, assumes True if any terms are upper cased + negative:bool - kwarg + Any files/folders with names or extensions matching the terms and exts will be omitted. + dirs:int + 0 -> ignore all directories + 1 -> directories and files + 2 -> directories only + strict:int + 0 -> match any terms in any order + 1 -> match all terms in any order (interruptions allowed) + 2 -> match all terms in any order (no interruptions allowed) + 3 -> match all terms in given order (interruptions) + 4 -> match all terms in given order (no interruptions) + combinations of the following are not counted as interruptions: + [' ', '_', '-'] + 5 -> match termstring as though it was pre-formatted regex + names:bool + True -> only yield results whose names match + False -> yield results who match at any level + """ + for i in self: + if os.path.isfile(i): + yield i + elif os.path.isdir(i): + yield from walking.search(i, terms, **kwargs) + +class Thing: + """ + 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 = os.path.realpath(str(path)) + if not os.path.exists(path): + warn(f'Warning: Path "{path}" does not exist', Warning) + def __eq__(self, other:_Pathstr) -> bool: + if isinstance(other, (str, type(self))): + return self.sameas(str(other)) + raise NotImplementedError(f"Cannot compare {type(self).__name__} with {type(other).__name__}") + def __str__(self): + return self.path + def __hash__(self): + """ + Compute the hash of this Thing's path + """ + return hash(self.path) + def __repr__(self): + name = type(self).__name__.split('.')[-1] + size = f", size={self.size}" if self.isfile or isinstance(self, File) else '' + real = (f", real={self.exists}", '')[self.exists] + return f"{name}(name={self.name}, dir={self.up.name}{size}{real})" + + def create(self, content:str|bytes|bytearray=None, raw:bool=False, exist_ok:bool=True, mode:int=511) -> _Path: + """ + Create an entry in the file-system. If the address is not vacant no action will be taken. + The raw handle only works for Things and enables writing bytes/bytearrays + """ + if self.exists: + return self + elif content or isinstance(self, File): + os.makedirs(self.up.path, exist_ok=exist_ok) + 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) + self = File(self.path) + else: + os.makedirs(self.path, mode=mode, exist_ok=exist_ok) + self = Place(self.path) + return self + @property + def __short_repr(self) -> str: + """ + Return check if the Place is at the highest level of the Thing system + """ + return f"{type(self).__name__}({self.name})" + @property + def exists(self) -> bool: + return os.path.exists(self.path) + @property + def isdir(self) -> bool: + return os.path.isdir(self.path) + @property + def isfile(self) -> bool: + return os.path.isfile(self.path) + @property + def obj(self) -> _Path: + """ + Determine if self.path points to a file or folder and create the corresponding object + """ + if self.isfile: + return Thing(self.path) + elif self.isdir: + return Place(self.path) + else: + return Thing(self.path) + @property + def dir(self) -> _Place: + """ + Return the containing directory + """ + return Thing(os.path.dirname(self.path)).obj + up = dir + @property + def name(self) -> str: + """ + Return the name of the referent + """ + return os.path.split(self.path)[1] + + @property + def ancestors(self) -> tuple: + """ + Return consecutive ADirs until the ADrive is reached + """ + level = [] + p = self.path + while p != delevel(p): + p = delevel(p) + level.append(p) + return tuple(Thing(i).obj for i in level)[::-1] + @property + def colleagues(self) -> Iterator: + """ + Every member of the same Place whose type is the same as the referent + """ + return (i for i in self.up if isinstance(i, type(self))) + @property + def neighbours(self) -> tuple[_File, _Place]: + """ + Everything in the same Place + """ + return self.up.content + @property + def depth(self) -> int: + """ + Number of ancestors + """ + return len(self.ancestors) + @property + def top(self) -> str: + """ + The apparent drive. Will not be helpful if self.path is relative + """ + return self.ancestors[0] + @property + def stat(self) -> os.stat_result: + """ + return os.stat(self.path) + """ + return os.stat(self.path) + @property + def ancestry(self) -> str: + """ + 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.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 + @property + def isempty(self): + return shell.isempty(self.path, make=False) + + def start(self, command:str=None) -> _Path: + """ + Open the path using the system default or a command of your choice + """ + pred = isinstance(command, type(None)) + + arg = (f'{command} "{self.path}"', self.path)[pred] + fun = (os.system, os.startfile)[pred] + fun(arg) + # os.startfile(self.path) if isinstance(command, type(None)) else os.system(f'{self.path} ""') + return self + def expose(self): + """ + Reveal the referent in the system's file explorer (will open the containing Place if the referent is a Thing) + """ + os.startfile(self.up.path) + return self + + def delevel(self, steps:int=1, path:bool=False) -> str: + """ + Go up some number of levels in the Thing system + Params + steps + the number of steps to delevel + path + return (True -> string, False -> Thing-like object) + """ + return delevel(self.path, steps) if path else Place(delevel(self.path, steps)) + def touch(self) -> _Path: + """ + Implements the unix command 'touch', which updates the 'date modified' of the content at the path + """ + p = self.path + pathlib.Path(p).touch() + self = Thing(p).obj + return self + def erase(self, recycle:bool=True) -> _Path: + """ + Send a Thing to the trash, or remove it without recycling. + """ + shell.discard(self.path, recycle=recycle) + return self + def sameas(self, other:_Pathstr) -> bool: + """ + Check if this Thing's associated data content is equivalent to some other + """ + if self.exists and os.path.exists(str(other)): + return os.path.samefile(self.path, str(other)) + else: + raise OSError("One or both paths point to a non-existent entity") + def clone(self, folder:str=None, name:str=None, sep:str='_', touch=False) -> _Path: + """ + Returns a clone of the referent at a given Place-path + The given path will be created if it doesn't exist + Will copy in the Thing's original folder if no path is given + The cwd switch will always copy to the current working Place + """ + copier = (shutil.copy2, shutil.copytree)[self.isdir] + if not self.exists: + raise NotImplementedError(f"Copying is not implemented for inexistant files/directories") + elif folder: + new = os.path.join(folder, name if name else self.name) + else: + new = self.path + new = shell.namespacer(new, sep=sep) + os.makedirs(delevel(new), exist_ok=True) + copier(self.path, new) + out = Thing(new).obj + return out.touch() if touch else out + def move(self, folder:_Placestr, dodge:bool=False) -> _Path: + """ + This will clone and delete the underlying file/directory + addy.move(folder) -> move to the given Place () + 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 Place + + :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 not self.exists: + raise OSError(f"{ self.__short_repr} does not exist") + folder = str(folder) + if not os.path.exists(folder): + raise OSError(f"folder={other.__short_repr} does not exist") + self.path = shell.move(self.path, folder) + return self + def rename(self, name:str) -> _Path: + """ + Change the name of the referent + This will clone and delete the underlying file/directory + """ + path = os.path.join(self.dir.path, name) + os.rename(self.path, path) + self.path = path + return self +Address = Path = Thing + + +class File(Thing): + """ + Create a new File object for context management and ordinary operations + """ + def __init__(self, path:str='NewThing'): + path = os.path.abspath(shell.trim(path)) + if os.path.isdir(path): + raise ValueError("Given path corresponds to a directory") + super(type(self), self).__init__(path) + self.__stream = None + def __enter__(self): + return self.open() + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + @property + def size(self) -> int: + if self.exists: + return MemorySize(os.stat(self.path).st_size) + @property + def mime(self) -> str|type(None): + return match.MIME if (match := ft.guess(self.path)) else None + @property + def kind(self) -> str|type(None): + return self.mime.split('/')[0] if (match:=ft.guess(self.path)) else None + @property + def ext(self) -> str: + return os.path.splitext(self.name)[1] + @property + def title(self) -> str: + """ + return the File's name without the extension + """ + return os.path.splitext(self.name)[0] + def open(self, mode='r', lines=False, buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None) -> io.TextIOWrapper|io.BufferedReader: + """ + Return the File's byte or text stream. + Scrape splits the text at all whitespace and returns the content as a string + """ + fobj = open(self.path, mode, buffering, encoding, errors, newline, closefd, opener) + self.__stream = fobj + return self.__stream + def close(self) -> _File: + if self.__stream: + self.__stream.close() + self.stream = None + return self + def cat(self, text:bool=True, lines:bool=False, copy:bool=False) -> str|bytes|list[str|bytes]: + """ + Mimmicks the unix command. + + Params + lines + whether or not you would like to read the lines (instead of the uninterrupted stream) + For other args, please refer to help (io.open) + """ + return shell.cat(self.path, lines=lines, copy=copy) + + +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 = os.path.realpath(path) + def __getattr__(self, attr): + return Place(self.path)[attr] + + +class Place(Thing): + """ + Place('.') == Place(os.getcwd()) + """ + def __init__(self, path:str='NewPlace'): + if path=='.': + path = os.getcwd() + elif path == 'NewPlace': + path = shell.namespacer(path) + elif path == '~': + path = os.path.expanduser(path) + path = os.path.abspath(shell.trim(path)) + if os.path.isfile(path): + raise ValueError("Given path corresponds to a file") + self.path = os.path.realpath(path) + super(type(self), self).__init__(path) + self.index = -1 + def __len__(self): + return len(os.listdir(self.path)) + def __bool__(self): + """ + Check if the Place is empty or not + """ + return len(os.listdir(self.path)) > 0 + def __iter__(self) -> Iterator[_Placefile]: + return self + def __next__(self) -> Sequence[_Placefile]: + if self.index _Path: + """ + Return an object whose name is an exact match for the given item + Params + items + int -> calls os.listdir and returns the item'th element + str -> checks if the item is reachable through self.path and returns if possible + """ + if isinstance(item, int): + return os.listdir(self.path)[item] + elif isinstance(item, str): + if os.path.isabs(item): + if os.path.dirname(item) == self.path: + return Thing(item).obj + if os.path.exists(path := os.path.join(self.path, item)): + return Thing(path).obj + raise ValueError(f'The folder "{self.name}" does not contain anything called "{item}"') + def __truediv__(self, other:str): + if isinstance(other, str): + return Thing(os.path.join(self.path, other)).obj + raise TypeError(f"Other must be a string") + def __call__(self, terms, **kwargs) -> Iterator[str]: + """ + Find files under self.path matching the given terms/criteria + + Params + Args + terms:str + the terms sought after. an empty string simply walks + Kwargs + exts:str + any file extensions you wish to check for, separate by spaces + case:bool + toggle case sensitivity, assumes True if any terms are upper cased + negative:bool - kwarg + Any files/folders with names or extensions matching the terms and exts will be omitted. + dirs:int + 0 -> ignore all directories + 1 -> directories and files + 2 -> directories only + strict:int + 0 -> match any terms in any order + 1 -> match all terms in any order (interruptions allowed) + 2 -> match all terms in any order (no interruptions allowed) + 3 -> match all terms in given order (interruptions) + 4 -> match all terms in given order (no interruptions) + combinations of the following are not counted as interruptions: + [' ', '_', '-'] + regex:bool + if true, the term-string will be compiled immediately with no further processing. + don't forget to set case=True if need be! + names:bool + True -> only yield results whose names match + False -> yield results who match at any level + """ + # yield from Searcher(terms, ext='', folders=False, absolute=True, case=False, strict=True)(self.path) + # yield from walking.search(self.path, terms, exts=exts, folders=folders, absolute=absolute, case=case, strict=strict, regex=regex, names=names) + yield from walking.search(self.path, terms, **kwargs) + def gather(self, dirs:bool=False, absolute:bool=True) -> Iterator[str]: + """ + 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 + """ + yield from walking.walk(self.path, dirs=dirs, absolute=True) + @property + def items(self) -> Items: + """ + This extension allows you to call folder contents as if they were attributes. + Will not work on any files/folders whose names wouldn't fly as python variables/attributes + example: + >>> folder.items.subfolder + """ + return Items(self.path) + @property + def children(self) -> _Place: + """ + Return "os.listdir" but filtered for directories + """ + return (addy.obj for i in os.listdir(self.path) if (addy:=Thing(os.path.join(self.path, i))).isdir) + @property + def files(self) -> Iterator[File]: + """ + Return "os.listdir" but filtered for Files + """ + return (addy.obj for i in os.listdir(self.path) if (addy:=Thing(os.path.join(self.path, i))).isfile) + @property + def content(self) -> Iterator[Thing]: + """ + Return address-like objects from "os.listdir" + """ + return tuple(Thing(os.path.join(self.path, i)).obj for i in os.listdir(self.path)) + @property + def leaves(self) -> Iterator[File]: + """ + Return All Things from all branches + """ + yield from map(lambda x: Thing(x).obj, walking.files(self.path, absolute=True)) + @property + def branches(self) -> Iterator[_Place]: + """ + Return Every Place whose path contains "self.path" + """ + yield from map(lambda x: Thing(x).obj, walking.folders(self.path, absolute=True)) + @property + def size(self) -> MemorySize: + """ + 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) -> tuple[str]: + """ + Return Thing mimes for all Things from all branches + """ + return tuple(unique(filter(None, (File(path).mime for path in self.gather(dirs=False, absolute=True))))) + @property + def kinds(self) -> tuple[str]: + """ + Return Thing types for all Things from branches + """ + return tuple(m.split('/')[1] for m in self.mimes) + @property + def exts(self) -> tuple[str]: + """ + Return extensions for all Things from all branches + """ + return tuple(unique(filter(None, (File(path).ext for path in self.gather(dirs=False, absolute=True))))) + @property + def isroot(self) -> bool: + """ + Return check if the Place is at the highest level of the Thing system + """ + return not self.depth + + + def add(self, other:Address, copy:bool=False) -> _Place: + """ + Introduce new elements. Send an address-like object to self. + """ + if isinstance(other, (str, SYSTEM_PATH)): + other = Thing(str(other)).obj + if not self.exists: + raise OSError(f"{self.__short_repr} does not exist") + if not other.exists: + raise OSError(f"{other.__short_repr} does not 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) -> _Place: + """ + Set referent as current working Directory + """ + if self.exists: + os.chdir(self.path) + return self + raise OSError(f"{self.__short_repr} does not exist") +Directory = Folder = Place + +# class Archive(Thing): + # def __init__(self, path:str='NewThing'): + # path = os.path.abspath(shell.trim(path)) + # super(Thing, self).__init__(path) +# class Searcherable: + +class Audio(File): + def __init__(self, path:str): + path = os.path.abspath(shell.trim(path)) + if os.path.isdir(path) or not ft.audio_match(path): + raise ValueError("Given path corresponds to a directory") + super(type(self), self).__init__(path) + @property + def metadata(self): + return am.load(self.path) + @property + def tags(self): + self.metadata['tags'] + @property + def pictures(self): + self.metadata['pictures'] + @property + def streaminfo(self): + self.metadata['streaminfo'] + + @property + def artist(self): + return self.tags['artist'] + @property + def album(self): + return self.tags['album'] + @property + def title(self): + return self.tags['title'] + +if __name__ == '__main__': + # show(locals().keys()) + + + fp = r'c:\users\kenneth\pyo_rec.wav' + dp = r'c:\users\kenneth\videos' + + d = Place(dp) + f = Thing(fp) + tf = Thing('testfile.ext') + td = Place('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/persistence.py b/filey/persistence.py new file mode 100644 index 0000000..fba0e2e --- /dev/null +++ b/filey/persistence.py @@ -0,0 +1,238 @@ +from typing import Any, Tuple +import os, pickle, re + +import dill + +from sl4ng.iteration import deduplicate, dictate, flat +from sl4ng.debug import tryimport, tipo + +from .shell import namespacer + +def dillsave(obj:Any, filename:str, overwrite:bool=True) -> Any: + """ + Pickles the given object to a file with the given path/name. + If the object cannot be serialized with pickle, we shall use dill instead + Returns the object + """ + if overwrite: + with open(filename, 'wb') as fob: + dill.dump(obj, fob, protocol=dill.HIGHEST_PROTOCOL) + else: + with open(namespacer(filename), 'wb') as fob: + dill.dump(obj, fob, protocol=dill.HIGHEST_PROTOCOL) + +def save(obj:Any, filename:str, overwrite:bool=True) -> Any: + """ + Pickles the given object to a file with the given path/name. + If the object cannot be serialized with pickle, we shall use dill instead + Returns the object + """ + try: + if overwrite: + with open(filename, 'wb') as fob: + pickle.dump(obj, fob, protocol=pickle.HIGHEST_PROTOCOL) + else: + with open(namespacer(filename), 'wb') as fob: + pickle.dump(obj, fob, protocol=pickle.HIGHEST_PROTOCOL) + except pickle.PicklingError: + dillsave(obj, filename, overwrite) + except TypeError: + dillsave(obj, filename, overwrite) + return obj + +def load(filename:str) -> Any: + """ + Return unpickled data from a chosen file + If the file doesn't exist it will be created + """ + + if os.path.exists(filename): + with open(filename, 'rb') as fob: + var = pickle.load(fob) + return var + else: + x = open(filename) + x.close() + return + +def jar(file, duplicates:bool=False, flags:int=re.I): + """ + Consolidate your pickles and eat them, discard the clones by default though + """ + trash = tryimport('send2trash', 'send2trash', 'remove', 'os') + name, ext = os.path.splitext(file) + pattern = f'^{name}|{name}_\d+\.{ext}$' + p = re.compile(pattern) + folder = None if not os.sep in file else os.path.split(file)[0] + os.chdir(folder) if folder else None + matches = deduplicate({f: load(f) for f in os.listdir(folder) if p.search(f, flags)}) + results = list(matches.values())[:] + for i in matches: print(i) + [trash(file) for file in matches] + save(results, file) + + + +class LoggerLengthError(Exception): + """ + A logger has reached it's maximum length + """ +class LoggerMaxLengthError(Exception): + """ + Cannot positively update due to inevitable length error + """ +class LoggerKindError(Exception): + """ + Tried to add an element of the wrong type + """ +class LoggerArgumentError(Exception): + """ + Couldn't unite loggers because they have distinct arguments + """ +class LoggerAgreementError(Exception): + """ + Shouldn't compare loggers because they have distinct arguments + """ + + + + +class Logger: + def __init__(self, path:str='log.pkl', tight:bool=True, kind:[type, Tuple[type]]=None, max_len:int=None, dodge:bool=True): + """ + Keep track of things so that you can pick up where you left off at the last runtime. + All saves are made at the end of an update + + + params + path + path to the logger's pickle file + tight + True -> no element can be added twice + kind + guarantees that any elements added will be of the desired type. Leave as None if you don't care + max_len + if the Logger's length >= max_len, the first element will be removed + doge + if false and IF there already exists a logger with equal arguments but different content, it's content will be synchronized and it will be overwritten upon updating. + if true, a new name will be generated for the serial-file. + """ + kind = kind if isinstance(kind, (tuple, list)) else tuple(flat([kind])) if kind else None + if os.path.exists(path): + if isinstance((slf := load(path)), type(self)): + omittables = '_Logger__index _Logger__itr path content'.split() + # kws = dictate(slf.__dict__, omittables) + kwargs = dict(zip(('tight', 'kind', 'max_len'), (tight, kind, max_len))) + # if all(slf.__dict__.get(i)==kwargs.get(i) for i in kws): + kws = {key: slf.__dict__[key] for key in kwargs} + # print([j==kwargs.get(i) for i, j in kws.values()]) + # if all(j==kwargs.get(i) for i, j in kws.values()): + if all(j==kwargs.get(i) for i, j in kws.items()): + self.max_len = slf.max_len + self.kind = slf.kind + self.tight = slf.tight + self.path = slf.path + self.content = slf.content + else: + # diff = {key: False for key in slf.__dict__ if (val:=slf.__dict__[key])!=kwargs.get(key)} + # diff = {key: kws[key]==val for key, val in kwargs.items()} + diff = {key: (kws[key], val) for key, val in kwargs.items() if kws[key]!=val} + # raise LoggerArgumentError(f"There is already a {tipo(self)} at the given path whose attributes do not agree:\n\t{load(path).__dict__}\n\t{kwargs}") + raise LoggerArgumentError(f"There is already a {tipo(self)} at the given path whose attributes do not agree:\n\t{diff}") + else: + raise TypeError(f'There is already persistent data in a file at the given path but it is not an instance of the {tipo(self)!r} class') + else: + self.max_len = max_len + self.kind = kind + self.tight = tight + self.path = path + self.content = [] + if dodge: + self.path = namespacer(path) + self.__index = -1 + + def __repr__(self): + r = f"Logger(length={len(self)}, tight={self.tight}, max_len={self.max_len})" + r += f"[{', '.join(map(tipo, self.kind))}]" if self.kind else '' + return r + + def compare(self, other): + """ + Return essentialized versions of self's and other's __dict__ attributes + """ + if isinstance(other, type(self)): + omittables = '_Logger__index _Logger__itr path content'.split() + sd = dictate(self.__dict__, omittables) + od = dictate(other.__dict__, omittables) + return sd, od + raise TypeError(f"Other is not an instance of the {tipo(self)} class") + def agree(self, other): + """ + Check if they are suitable for concatenation + """ + sd, od = self.compare(other) + return sd == od + def __eq__(self, other): + """ + Not order sensitive + """ + if self.agree(other): + return sorted(self.content) == sorted(other.content) + raise LoggerAgreementError(f"self does not agree with other") + def __gt__(self, other): + if self.agree(other): + return all(i in self for i in other) and len(self) > len(other) + raise LoggerAgreementError(f"self does not agree with other") + def __lt__(self, other): + if self.agree(other): + return all(i in other for i in self) and len(self) < len(other) + raise LoggerAgreementError(f"self does not agree with other") + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + def __le__(self, other): + return self.__lt__(other) or self.__eq__(other) + + + def __add__(self, other): + if self.agree(other): + if isinstance(self.max_len, None): + [*map(self.update, other.content)] + elif len(other) <= self.max_len - len(self): + [*map(self.update, other.content)] + else: + raise LoggerMaxLengthError(f'Cannot update "self" with content from "other" because the outcome would have more than {self.max_lengh} elements') + def __len__(self): + return len(self.content) + def __iter__(self): + self.__itr = iter(self.content) + return self.__itr + def __next__(self): + if self.__index < len(self) - 1: + self.__index += 1 + return self.content[self.__index] + self.__index = -1 + raise StopIteration + def __in__(self, item): + return item in self.content + @property + def exists(self): + return os.path.exists(self.path) + def update(self, element, remove:bool=False): + """ + Safely add/remove an element to/from, serialize, and return, self. + """ + if remove: + self.content.remove(element) + else: + if not isinstance(self.kind, type(None)): + if not type(element) in self.kind: + raise LoggerKindError(f'Argument is not {self.kind}') + if self.tight: + if element in self: + return self + if not isinstance(self.max_len, type(None)): + if self.max_len == len(self): + raise LoggerLengthError("Logger has reached maximum capacity") + self.content.append(element) + save(self, self.path) + return self diff --git a/filey/preamble/_parseconfigurationscunt.py b/filey/preamble/_parseconfigurationscunt.py deleted file mode 100644 index bb1867b..0000000 --- a/filey/preamble/_parseconfigurationscunt.py +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 1679c73..0000000 --- a/filey/preamble/filesystem.py +++ /dev/null @@ -1,380 +0,0 @@ -""" -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 deleted file mode 100644 index 7b974d2..0000000 --- a/filey/preamble/filesystem2inheritance.py +++ /dev/null @@ -1,366 +0,0 @@ -""" -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 deleted file mode 100644 index a1a9d96..0000000 --- a/filey/preamble/filesystem3pathlibbing.py +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index 2b4e5e3..0000000 --- a/filey/preamble/filesystem4-pathsarentinheritable.py +++ /dev/null @@ -1,344 +0,0 @@ -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 deleted file mode 100644 index a297696..0000000 --- a/filey/preamble/filesystem4_2.py +++ /dev/null @@ -1,562 +0,0 @@ -""" -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 deleted file mode 100644 index b310516..0000000 --- a/filey/preamble/magnitudes.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -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 deleted file mode 100644 index 7bbc919..0000000 --- a/filey/preamble/utils.py +++ /dev/null @@ -1,24 +0,0 @@ -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/shell.py b/filey/shell.py new file mode 100644 index 0000000..b5cdd30 --- /dev/null +++ b/filey/shell.py @@ -0,0 +1,347 @@ +# __all__ = "discard unarchive create_enter audio_or_video namespacer isempty move send2trash ffplay trim move_file delevel convert cat".split() +__all__ = "mcd mv move_file rm ffplay convert cat".split() + +from itertools import filterfalse +from typing import Iterable, Iterator +from warnings import warn +import os, subprocess, sys, time + +from send2trash import send2trash +from sl4ng import shuffle, flat +import filetype as ft, audio_metadata as am, pyperclip as pc + + +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 + """ + path = os.path.normpath(path) + 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 namespacer(path:str, sep:str='_', start:int=2) -> str: + """ + Returns a unique version of a given string by appending an integer + + example: + tree: + /folder + /file.ext + /file_2.ext + + >>> namespacer('file', sep='-', start=2) + file-2.ext + >>> namespacer('file', sep='_', start=2) + file_3.ext + >>> namespacer('file', sep='_', start=0) + file_0.ext + """ + id = start + oldPath = path[:] + while os.path.exists(path): + 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 + +def trim(path:str, edge:str=os.sep) -> str: + """ + Remove trailing/leading separators (or edges) from a path string + """ + out = path[:] + if sys.platform == 'win32': + 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 cat(path:str, text:bool=True, lines:bool=False, copy:bool=False) -> str|bytes|list[str|bytes]: + """ + Extract the text or bytes, if the keyword is set to false, from a file + ::text:: + text or bytes? + ::lines:: + split text by line or return raw? + """ + if os.path.isfile(path): + mode = "rb r".split()[text] + with open(path, mode) as f: + content = f.readlines() if lines else f.read() + pc.copy(content) if copy else None + return content + +def create_enter(*args:[str, Iterable[str]], go_back:bool=False, recursive:bool=True, overwrite:bool=True, exist_ok:bool=True) -> str: + """ + recursively create and enter directories. + params: + go_back + if set to True, the process will return to the starting directory + recursive + if set to False, all directories will be created in the starting directory + overwrite + if a directory "dir_n" exists, "dir_n+1" will be created, unless set to False + exist_ok + passed to the os.makedirs call. If set to False, and overwrite is True, and collision occurs, an exception will be raised + eg + each of the following calls create the following tree: + dir-1 + dir0: starting directory + dir1 + dir2 + dir3 + dir4 + dir5 + + >>> mcd('dir1 dir2 .. dir3 .. .. dir4 .. .. dir5'.split()) + >>> mcd('dir1/dir2 ../dir3 ../../dir4 ../../dir5'.split()) + """ + home = os.getcwd() + for arg in flat(args): + arg = nameSpacer(arg) if arg!='..' and not overwrite else arg + os.makedirs(arg, exist_ok=exist_ok) + os.chdir(arg if recursive else home) + last_stop = home if go_back else os.getcwd() + os.chdir(last_stop) + return last_stop +mcd = create_enter + +def move(source:str, dest:str, make_dest:bool=False) -> str: + """ + Move source to dest + Return path to new version + + Params + source + path to original file/folder + dest + path to new containing directory. + This will assume that the directory is on the same disk as os.getcwd() + make_dest + create destination if it doesn't already exist + """ + dest = (os.path.realpath, str)[os.path.isabs(dest)](dest) + if not os.path.isdir(dest): + if not make_dest: + raise ValueError(f"Destination's path doesn't point to a directory") + os.makedirs(dest, exist_ok=True) + root, name = os.path.split(path) + new = os.path.join(dest, name) + os.rename(path, new) + return new +mv = move + +def move_file(file:str, dest:str, make_dest:bool=False, clone:bool=False) -> str: + """ + Move a file to a given directory + This uses iteration to copy the file byte by byte. + Use filey.operations.move unless you're having some permission issues + Params + source + path to original file/folder + dest + path to new containing directory. + This will assume that the directory is on the same disk as os.getcwd() + make_dest + create destination if it doesn't already exist + """ + if os.path.isdir(source): + raise ValueError(f"Source path points to a directory") + + dest = (os.path.realpath, str)[os.path.isabs(dest)](dest) + if not os.path.isdir(dest): + if not make_dest: + raise ValueError(f"Destination path doesn't point to a directory") + else: + os.makedirs(dest, exist_ok=True) + + root, name = os.path.split(path) + new = os.path.join(dest, name) + + with open(source, 'rb') as src: + with open(new, 'wb') as dst: + dst.write(src.read()) + try: + None if clone else os.remove(file) + except PermissionError: + warn("Could not remove file after copying due to PermissionError", Warning) + return new + +def discard(path:str, recycle:bool=True) -> None: + """ + Remove an address from the file-system. + Will fall back to recycle if recycle is False, but a permission error is raised, and vis-versa + Params + Path + address of the file/folder you wish to remove + recycle + send to (True -> recycle bin, False -> anihilate) + """ + fb = (os.remove, send2trash) + first, backup = fb if not recycle else fb[::-1] + try: + first(path) + except PermissionError: + backup(path) +rm = discard + +def audio_or_video(path:str) -> bool: + """ + Check if a file is audio or video + True if audio, False if video, else ValueError + """ + if ft.video_match(path): + return False + elif ft.audio_match(path): + return True + raise ValueError("Mime does not compute") + +def ffplay(files:Iterable[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 + Files can be passed as a single string of paths separated by asterisks + + 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: audio_or_video(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 convert(file:str, format:str='wav', bitRate:int=450, delete:bool=False, options:str='') -> str: + """ + Convert an audio file using FFMPEG. + Verbosity is minimized by default. + Params + file + path to target file + format + desired output format + bitRate + only applies to lossy formats like ogg and mp3, will be autocorrected by FFMPEG + delete + whether or not to keep the file upon completion + options + additional options to pass to ffmpeg + """ + os.chdir(os.path.split(file)[0]) + + _title = lambda file: file.split(os.sep)[-1].split('.')[0] + _new = lambda file, format: namespacer(_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: + send2trash(file) + print(f'Deletion is complete\n\t{new}\n\n\n') + return new + +def unarchive(path:str, app:str='rar') -> str: + """ + Extract an archive to a chosen destination, or one generated based on the name of the archive + App refers to the comandlet you wish to invoke via subprocess.run + + """ + path = os.path.realpath(path) + route, namext = os.path.split(path) + name, ext = os.path.splitext(namext) + dest = namespacer(os.path.join(route, name)) + + options = { + 'tar':'-x -f', + 'rar':'e -or -r', + 'winrar':'', + } + cmd = f'{app} {options[app]} "{src}" ' + + os.makedirs(dest, exist_ok=True) + os.chdir(dest) + + subprocess.run(cmd) + return dest + +def isempty(path:str, make:bool=False) -> bool: + """ + Check if a given file or folder is empty or not with the option to create it if it doesn't exit + """ + if os.path.isfile(path): + with open(path, 'rb') as f: + return not bool(len(tuple(i for i in f))) + elif os.path.isdir(path): + return not bool(len(os.listdir(file))) + elif make: + if os.path.splitext(path)[-1]: + x = open(path, 'x') + x.close() + else: + os.makedirs(path, exist_ok=True) + return True \ No newline at end of file diff --git a/filey/shortcuts.py b/filey/shortcuts.py new file mode 100644 index 0000000..ee27611 --- /dev/null +++ b/filey/shortcuts.py @@ -0,0 +1,44 @@ +import os, sys, time + +user_scripts = r'e:\projects\monties' +shortcuts = { + '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(user_scripts, str(time.localtime()[0])), + 'scripts': user_scripts, + 'demos': os.path.join(user_scripts, '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(shortcuts['monties'], exist_ok=True) \ No newline at end of file diff --git a/filey/utils/__init__.py b/filey/utils/__init__.py deleted file mode 100644 index 6b760bc..0000000 --- a/filey/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -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 deleted file mode 100644 index 771867a..0000000 --- a/filey/utils/debug.py +++ /dev/null @@ -1,99 +0,0 @@ -__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 deleted file mode 100644 index c09da2b..0000000 --- a/filey/utils/magnitudes.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -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 deleted file mode 100644 index 8df4cd0..0000000 --- a/filey/utils/media.py +++ /dev/null @@ -1,191 +0,0 @@ -__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 deleted file mode 100644 index 3977cb1..0000000 --- a/filey/utils/strings.py +++ /dev/null @@ -1,123 +0,0 @@ -__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/filey/walking.py b/filey/walking.py new file mode 100644 index 0000000..1b9bb17 --- /dev/null +++ b/filey/walking.py @@ -0,0 +1,251 @@ +# __all__ = "walk files folders".split() +__all__ = "search_iter files show search folders walk".split() + + +from typing import Iterator, Iterable, Any +from itertools import permutations, chain +import os, re +from sl4ng import pop, show, multisplit, join, mainame, eq + +def walk(root:str='.', dirs:bool=False, absolute:bool=True) -> Iterator[str]: + """ + Walk a directory's tree yielding paths to any files and/or folders along the way + This will always yield files. + If you only want directories, look for the "folders" function in filey.utils.walkers + + Caution: if you pass a relative pathname for top, don't change the + current working directory between resumptions of walk. walk never + changes the current directory, and assumes that the client doesn't + either. - taken from os.walk documentation + + Params + root: str|pathlike|Place + path to starting directory + folders + (True -> omit, False -> include) paths to directories + absolute + yield (True -> absolute paths, False -> names only) + """ + root = (str, os.path.realpath)[absolute](str(root)) + for name in os.listdir(root): + path = os.path.join(root, name) + if os.path.isdir(path): + if dirs: + yield (name, path)[absolute] + yield from walk(path, dirs=dirs, absolute=absolute) + else: + yield (name, path)[absolute] + +def parse_extensions(extensions:str) -> re.Pattern: + """ + Create a regex parser to check for file extensions. + Note: Separate extensions by one of + [',', '`', '*', ' '] + """ + sep = [i for i in ',`* ' if i in extensions] + pattern = '|'.join(f'\.{i}$' for i in extensions.split(sep[0] if sep else None)) + pat = re.compile(pattern, re.I) + return pat + +def files(root:str='.', exts:str='', negative:bool=False, absolute:bool=True) -> Iterator[str]: + """ + Search for files along a directory's tree. + Also (in/ex)-clude any whose extension satisfies the requirement + + Params + root: str|pathlike|Place + path to starting directory + exts + extensions of interest. + you can pass more than one extension at a time by separating them with one of the following: + [',', '`', '*', ' '] + negative + (True -> omit, False -> include) matching extensions + absolute + yield (True -> abspaths, False -> names only) + """ + root = (str, os.path.realpath)[absolute](str(root)) + pat = parse_extensions(exts) + predicate = lambda x: not bool(pat.search(x)) if negative else bool(pat.search(x)) + for name in os.listdir(root): + path = os.path.join(root, name) + if os.path.isdir(path): + yield from files(path, exts=exts, negative=negative, absolute=absolute) + elif predicate(path): + yield (name, path)[absolute] + +def folders(root:str='.', absolute:bool=True) -> Iterator[str]: + """ + Search for files along a directory's tree. + Also (in/ex)-clude any whose extension satisfies the requirement + + Params + root: str|pathlike|Place + path to starting directory + exts + extensions of interest. + you can pass more than one extension at a time by separating them with any of the following: + [',', '`', '*', ' '] + negative + (True -> omit, False -> include) matching extensions + absolute + yield (True -> abspaths, False -> names only) + """ + root = (str, os.path.realpath)[absolute](str(root)) + for name in os.listdir(root): + path = os.path.join(root, name) + if os.path.isdir(path): + yield (name, path)[absolute] + yield from folders(path, absolute=absolute) + +def __term_perms(terms:str, case:int, tight:bool) -> re.Pattern: + """ + compute the regex pattern for posible permutations of search terms + """ + sep = "[\\ _\\-]*" if tight else "(.)*" + if isinstance(terms, str): + terms = terms.split() + terms = map(re.escape, terms) + rack = (sep.join(perm) for perm in permutations(terms)) + return re.compile("|".join(rack), case) + + +def search_iter(iterable:str, terms:Iterable[str], exts:str='', case:bool=False, negative:bool=False, dirs:int=0, strict:int=1, regex:bool=False, names:bool=True) -> Iterator[str]: + """ + Find files matching the given terms within a directory's tree + Params + root + the directory in which the walking search commences + separate by spaces + terms + the terms sought after + separate by spaces + exts + any file extensions you wish to check for + case + toggle case sensitivity + negative + Ignored unless dirs==0. Any files matching the terms will be omitted. + dirs + 0 -> ignore all directories + 1 -> directories and files + 2 -> directories only + strict + 0 -> match any terms in any order + 1 -> match all terms in any order (interruptions allowed) + 2 -> match all terms in any order (no interruptions allowed) + 3 -> match all terms in given order (interruptions) + 4 -> match all terms in given order (no interruptions) + combinations of the following are not counted as interruptions: + [' ', '_', '-'] + 5 -> match string will be compiled as though it was preformatted regex + names + True -> only yield results whose names match + False -> yield results who match at any level + """ + tight = strict in (2, 4) + sep = "[\\ _\\-]*" if tight else "(.)*" + scope = (str, lambda x: os.path.split(x)[1])[names] + case = 0 if case else re.I + + expat = parse_extensions(exts) + tepat = { + 0: re.compile("|".join(map(re.escape, terms.split())), case), + 1: __term_perms(terms, case, 0), + 2: __term_perms(terms, case, 1), + 3: re.compile(sep.join(map(re.escape, terms)), case), + 4: re.compile(sep.join(map(re.escape, terms)), case), + 5: re.compile(terms, case) + }[strict] if not regex else re.compile(terms, case) + + predicate = ( + lambda i: tepat.search(i) and expat.search(i), + lambda i: not (tepat.search(i) or expat.search(i)), + )[negative] + + for i in iterable: + if predicate(scope(i)): + yield i + +def search(root:str, terms:Iterable[str], exts:str='', case:bool=False, negative:bool=False, dirs:int=0, strict:int=1, regex:bool=False, names:bool=True) -> Iterator[str]: + """ + Find files matching the given terms within a directory's tree + Uses linear search + Params + root + the directory in which the walking search commences + separate by spaces + terms + the terms sought after + exts + any file extensions you wish to check for + separate by spaces + case + toggle case sensitivity + negative + Any files/folders with names or extensions matching the terms and exts will be omitted. + dirs + 0 -> ignore all directories + 1 -> directories and files + 2 -> directories only + strict + 0 -> match any terms in any order + 1 -> match all terms in any order (interruptions allowed) + 2 -> match all terms in any order (no interruptions allowed) + 3 -> match all terms in given order (interruptions) + 4 -> match all terms in given order (no interruptions) + combinations of the following are not counted as interruptions: + [' ', '_', '-'] + 5 -> match string will be compiled as though it was preformatted regex + names + True -> only yield results whose names match + False -> yield results who match at any level + """ + func = { + 0: files, + 1: walk, + 2: folders, + }[dirs] + kwargs = { + 0: { "exts": exts, "negative": negative, "absolute": True, }, + 1: { "dirs": True, "absolute": True, }, + 2: { "absolute": True, }, + }[dirs] + + yield from search_iter( + (i for i in func(root, **kwargs)), + terms=terms, exts=exts, case=case, + negative=negative, dirs=dirs, + strict=strict, names=names + ) + + +if __name__ == "__main__": + folder = r'E:\Projects\Monties\2021\file management' + folder = 'C:\\Users\\Kenneth\\Downloads\\byextension' + folder = r"E:\Projects\Monties\2021\media\file_management\filey" + folder = "../../.." + folder = "c:/users/kenneth/pictures" + + # box = [*walk(folder, absolute=True)] + # print(all(map(os.path.exists, box))) + # print(__file__ in box) + # show(box, 0, 1) + + # box = [*walk(folder, dirs=False, absolute=True)] + # box2 = [*files(folder, exts='', absolute=True)] + # print(all(i in box2 for i in box) and all(i in box for i in box2)) + + # exts = 'jpg .jpeg pdf' + # show([*files(folder, exts=exts, negative=False, absolute=True)]) + + # box = [*walk(folder, dirs=False, absolute=True)] + # box2 = [*files(folder, exts=exts, negative=False, absolute=True)] + # box3 = [*files(folder, exts=exts, negative=True, absolute=True)] + # print(eq(map(sorted, (box2+box3, box)))) + + + # box4 = [*folders(folder, True)] + # show(box4, 0, 1) + # show(search(folder, '__init__')) + show(search(folder, '_', 'png')) \ No newline at end of file diff --git a/readme.rst b/readme.rst new file mode 100644 index 0000000..f13dd20 --- /dev/null +++ b/readme.rst @@ -0,0 +1,33 @@ +Flexible functions and handy handles for your favourite filey things. + +ΓÇ£ΓÇ£ΓÇ£python # central_nervous_system.py import os + +from filey import Directory, File, ffplay, convert + +this = **file** here = os.path.dirname(**file**) pwd = Directory(here) + +here == str(pwd) # True + +def cd(path): global pwd os.chdir(path) pwd.path = path + +.. raw:: html + + + +.. raw:: html + + + +.. raw:: html + + + +user = Directory(ΓÇÿ~ΓÇÖ) music = user / ΓÇ£musicΓÇ¥ album1 = +music[ΓÇ£collection/Alix Perez/1984ΓÇ¥] # raises an OSError if it cannot be +found album2 = music[ΓÇ£collection/dBridge/the gemini principleΓÇ¥] artist1 += music.items.collection[ΓÇ£venuq/escrowsΓÇ¥] playlist = Library(album1, +album2, artist1) for track in playlist(ΓÇÖΓÇÖ): # search library for any +matches of the empty string. Directories are also searchable via +**call** ffplay(track.path) + +ΓÇ£ΓÇ£ΓÇ¥ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5ba35e8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +dill +filetype +pyperclip +send2trash +sl4ng \ No newline at end of file diff --git a/setup.py b/setup.py index cf315f9..38bccd6 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,10 @@ with open('README.md', 'r') as fob: setup( name='filey', - version='0.0.1', - author='Kenneth Elisandro', - author_email='eli2and40@tilde.club', - url='https://tildegit.org/eli2and40/cabinet', - # packages=find_packages(), + version='0.0.2', + author='Kenneth Sabalo', + author_email='kennethsantanasablo@gmail.com', + url='https://tildegit.org/eli2and40/filey', packages=['filey'], classifiers=[ "Programming Language :: Python :: 3", @@ -18,15 +17,15 @@ setup( ], long_description=long_description, long_description_content_type='text/markdown', - keywords='utilities productivity', + keywords='utilities operating path file system', license='MIT', requires=[ - 'filetype', - 'audio_metadata', - 'send2trash', - 'sl4ng' + "audio_metadata", + "dill", + "filetype", + "send2trash", + "sl4ng", + "pypeclip", ], - # py_modules=['filey'], - python_requires='>=3.8', - + python_requires='>3.9', ) \ No newline at end of file