transfered relevant sections from sl4ng. added shell utilities. simplified walker interface. added libraries and iterable searches for result refinement. removed legacy code (preambles). added basic audio support via ffmpeg

This commit is contained in:
kendfss 2021-06-07 00:33:42 +01:00
parent c76c05e134
commit 2075020f25
25 changed files with 1839 additions and 2987 deletions

3
.gitignore vendored
View File

@ -127,3 +127,6 @@ dmypy.json
# Pyre type checker
.pyre/
filey/testdir
readme_test.py

190
README.md
View File

@ -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)

Binary file not shown.

View File

@ -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<len(self)-1:
self.index += 1
return self.content[self.index]
self.index = -1
raise StopIteration
def __getitem__(self, item:str):
"""
Return an object whose name is an exact match for the given item
"""
if any(re.search(f'^{item}$', i.name, re.I) for i in self.content):
return Address(os.path.join(self.path, item)).obj
raise ValueError(f'The folder "{self.name}" does not contain anything called "{item}"')
def __truediv__(self, other:str):
if isinstance(other, str):
return Address(os.path.join(self.path, other)).obj
raise TypeError(f"Other must be a string")
def __call__(self, keyword:str, sort:bool=False, case:bool=False, **kwargs) -> 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))
if __name__ == "__main__":
pass

709
filey/handles.py Normal file
View File

@ -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<len(self)-1:
self.index += 1
return self.content[self.index]
self.index = -1
raise StopIteration
def __getitem__(self, item:str|int) -> _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))

238
filey/persistence.py Normal file
View File

@ -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

View File

@ -1,7 +0,0 @@
import configparser
cfg = configparser.ConfigParser()
# cfg[]
if __name__=='__main__':
pass

View File

@ -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._ind<len(self)-1:
self._ind += 1
return self.content[self._ind]
self._ind = -1
raise StopIteration
def __getitem__(self,item):
if item in self.content:
return address(os.path.join(self.path,item)).obj
raise ValueError(f'The folder "{self.name}" does not contain anything called "{item}"')
move = rename
extension = ext
copy = clone
class file:
def __init__(self,path:str):
assert address(path).isfile, f'"{path}" is not a file'
self.path = trim(path)
@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(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(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]
@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 __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'))

View File

@ -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._ind<len(self)-1:
self._ind += 1
return self.content[self._ind]
self._ind = -1
raise StopIteration
def __getitem__(self,item):
if any(re.search(f'^{item}$',i.name,re.I) for i in self.content):
# for i in nopes((re.search(f'^{item}$',i.name,re.I) for i in self.content),True):
return address(os.path.join(self.path,item)).obj
raise ValueError(f'The folder "{self.name}" does not contain anything called "{item}"')
@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 branches(self):
return tuple(set(i.delevel() for i in self.gather()))
@property
def size(self):
return size(sum(file.size for file in self.leaves))
@property
def mime(self):
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 isroot(self):
return not self.depth
def enter(self):
os.chdir(self.path)
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:
yield from set(file(path) for path in gather(str(self),names,walk,ext))
# 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 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)
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)

View File

@ -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))

View File

@ -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<len(self)-1:
self._ind += 1
return self.content[self._ind]
self._ind = -1
raise StopIteration
def __getitem__(self,item):
if any(re.search(f'^{item}$',i.name,re.I) for i in self.content):
# for i in nopes((re.search(f'^{item}$',i.name,re.I) for i in self.content),True):
return address(os.path.join(self.path,item)).obj
raise ValueError(f'The folder "{self.name}" does not contain anything called "{item}"')
@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) -> 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)

View File

@ -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<len(self)-1:
self._ind += 1
return self.content[self._ind]
self._ind = -1
raise StopIteration
def __getitem__(self, item):
"""
Return an object whose name is an exact match for the given item
"""
if any(re.search(f'^{item}$', i.name, re.I) for i in self.content):
return Address(os.path.join(self.path, item)).obj
raise ValueError(f'The folder "{self.name}" does not contain anything called "{item}"')
def __truediv__(self, other):
if isinstance(other, str):
return Address(os.path.join(self.path, other)).obj
raise TypeError(f"Other must be a string")
def __call__(self, keyword:str, sort:bool=False, case:bool=False, **kwargs) -> 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))

View File

@ -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))

View File

@ -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))

347
filey/shell.py Normal file
View File

@ -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

44
filey/shortcuts.py Normal file
View File

@ -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)

View File

@ -1,4 +0,0 @@
from .strings import *
from .magnitudes import *
from .debug import *
from .media import *

View File

@ -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]

View File

@ -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))

View File

@ -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)))

View File

@ -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)))

251
filey/walking.py Normal file
View File

@ -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'))

33
readme.rst Normal file
View File

@ -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
<!-- c = Directory('c:/') -->
.. raw:: html
<!-- e = Directory('e:/') -->
.. raw:: html
<!-- f = Directory('f:/') -->
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)
ΓÇ£ΓÇ£ΓÇ¥

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
dill
filetype
pyperclip
send2trash
sl4ng

View File

@ -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',
)