'''
| Author: Ezio416
| Created: 2022-08-16
| Updated: 2024-01-31
- Functions for filesystem and path string manipulation
- OS-agnostic (Windows/Unix) - we always return Windows paths with forward slashes
- supports absolute and relative paths
- Does not yet support any paths that:
- are not strings
- contain multiple single or double-dots ( . | .. )
- contain single or double-dots in the beginning/middle
'''
from datetime import datetime as dt
from fnmatch import fnmatch, fnmatchcase
from functools import wraps
import os
from shutil import copy2, copytree, move as shmv, rmtree, unpack_archive
from time import time
from zipfile import BadZipFile, ZipFile
from py7zr import exceptions, SevenZipFile, unpack_7zarchive
from send2trash import send2trash
from .general import secmod, secmod_inverse, timestamp, unpack
[docs]class File():
'''
- class for keeping track of a file/folder and performing actions on it
Parameters
----------
path: str
- path to the file/folder we wish to track
'''
def __init__(self, path):
self.path = getpath(path)
def __repr__(self) -> str:
return f"py416.files.File('{self.path}')"
def __str__(self) -> str:
return self.path
@property
def atime(self) -> float:
'''
- the last time the file/folder was accessed
- wraps `os.path.getatime() <https://docs.python.org/3/library/os.path.html#os.path.getatime>`_
- given in `Unix time <https://www.unixtimestamp.com>`_ as a float
- i.e. 1663948504.5217497
'''
return os.path.getatime(self.path)
@property
def atimes(self) -> tuple:
'''
- the last time the file/folder was accessed
- given as a tuple[int], starting with year and ending with microseconds
- i.e. (2022, 09, 23, 10, 01, 43, 544875)
'''
return tuple(dt.fromtimestamp(self.atime).strftime('%Y,%m,%d,%H,%M,%S,%f').split(','))
@property
def children(self) -> tuple:
'''
- tuple of full paths for everything inside the folder
- if path is empty or not a folder, returns an empty tuple
- i.e. ('C:/thisfolder/file1.txt', 'C:/thisfolder/file2.csv') or ( )
'''
return listdir(self.path) if self.isdir else ()
@property
def ctime(self) -> float:
'''
- the time the file/folder was created
- wraps `os.path.getctime() <https://docs.python.org/3/library/os.path.html#os.path.getctime>`_
- given in `Unix time <https://www.unixtimestamp.com>`_ as a float
- i.e. 1663948504.5217497
'''
return os.path.getctime(self.path)
@property
def ctimes(self) -> tuple:
'''
- the time the file/folder was created
- given as a tuple[int], starting with year and ending with microseconds
- i.e. (2022, 09, 23, 10, 01, 43, 544875)
'''
return tuple(dt.fromtimestamp(self.ctime).strftime('%Y,%m,%d,%H,%M,%S,%f').split(','))
@property
def exists(self) -> bool:
'''
- whether the file/folder exists
- wraps `os.path.exists() <https://docs.python.org/3/library/os.path.html#os.path.exists>`_
'''
return os.path.exists(self.path)
@property
def isdir(self) -> bool:
'''
- whether the file/folder exists and is a folder
- wraps `os.path.isdir() <https://docs.python.org/3/library/os.path.html#os.path.isdir>`_
'''
return os.path.isdir(self.path)
@property
def isfile(self) -> bool:
'''
- whether the file/folder exists and is a file
- wraps `os.path.isfile() <https://docs.python.org/3/library/os.path.html#os.path.isfile>`_
'''
return os.path.isfile(self.path)
@property
def isroot(self) -> bool:
'''
- whether the folder is a root directory
- on Unix, this is always a single forward slash ( / )
- on Windows, these are drive letters ( C:/ ) or network locations ( //netloc )
'''
return self.path == str(self.parent)
@property
def mtime(self) -> float:
'''
- the last time the file/folder was modified
- wraps `os.path.getmtime() <https://docs.python.org/3/library/os.path.html#os.path.getmtime>`_
- given in `Unix time <https://www.unixtimestamp.com>`_ as a float
- i.e. 1663948504.5217497
'''
return os.path.getmtime(self.path)
@property
def mtimes(self) -> tuple:
'''
- the last time the file/folder was modified
- given as a tuple[int], starting with year and ending with microseconds
- i.e. (2022, 09, 23, 10, 01, 43, 544875)
'''
return tuple(dt.fromtimestamp(self.mtime).strftime('%Y,%m,%d,%H,%M,%S,%f').split(','))
@property
def name(self) -> str:
'''
- the basename of the file/folder
- i.e. 'file.txt' or 'foldername'
'''
return self.parts[-1]
@property
def parent(self) -> object:
'''
- the parent path of the file/folder
- creates a new File instance for the parent path
'''
return File(parent(self.path))
@property
def parts(self) -> tuple:
'''
- the parts of the file/folder's path
- given as a tuple[str]
- i.e. ('C:/', 'folder', 'file.txt')
'''
return splitpath(self.path)
@property
def root(self) -> str:
'''
- the root of the file/folder's path
- i.e. 'C:/' or '//netloc' or '/'
'''
return self.parts[0]
@property
def size(self) -> int:
'''
- the size of the file/folder in bytes
- wraps `os.path.getsize() <https://docs.python.org/3/library/os.path.html#os.path.getsize>`_
'''
return os.path.getsize(self.path)
@property
def stem(self) -> str:
'''
- the basename of the file/folder without the extension
- i.e. 'file' instead of 'file.txt'
'''
return self.name if self.isdir else self.name.split('.')[0]
@property
def suffix(self) -> str:
'''
- the extension of the file, or blank if a folder
- i.e. '.txt'
'''
return '.' + self.name.split('.')[-1] if not self.isdir else ''
[docs] def copy(self, dest: str, overwrite: bool = False) -> object:
'''
- copies file/folder without tracking the created copy
- in-place operation
Parameters
----------
dest: str
- folder to copy file/folder into
overwrite: bool
- whether to overwrite if the destination file/folder already exists
- default: False
'''
copy(self.path, dest, overwrite=overwrite)
return self
[docs] def delete(self, force: bool = False, trash: bool = False) -> object:
'''
- deletes file/folder, attempting to recursively delete empty subfolders
- in-place operation
Parameters
----------
force: bool
- whether to force deletion with `shutil.rmtree() <https://docs.python.org/3/library/shutil.html#shutil.rmtree>`_
- default: False
trash: bool
- whether to try moving item to trash/recycle bin (if enabled for that drive)
- uses `Send2Trash <https://github.com/arsenetar/send2trash>`_
- UNSAFE - deletes file if trash/recycle bin disabled
- default: False
'''
delete(self.path, force=force, trash=trash)
return self
[docs] def move(self, dest: str, overwrite: bool = False) -> object:
'''
- moves file/folder, maintaining tracking at the new location
- in-place operation
Parameters
----------
dest: str
- folder to move file/folder into
overwrite: bool
- whether to overwrite if the destination file/folder already exists
- default: False
'''
self.path = move(self.path, dest, overwrite=overwrite)
return self
[docs] def rename(self, name: str) -> object:
'''
- renames file/folder without moving it
- in-place operation
Parameters
----------
name: str
- new basename for the file/folder
'''
self.path = rename(self.path, name)
return self
[docs]def cd(path: str = '..') -> str:
'''
- changes the current working directory, creating it if nonexistent
- wraps `os.chdir() <https://docs.python.org/3/library/os.html#os.chdir>`_
Parameters
----------
path: str
- path to folder we want to go in
- default: parent (up a folder)
Returns
-------
str
- path to new current working directory
'''
makedirs(path := getpath(path))
os.chdir(path)
return path
[docs]def checkwindrive(drive: str) -> str:
'''
- checks if a given string is a Windows drive letter (i.e. 'C:', 'D:\\\\', 'E:/')
Parameters
----------
drive: str
- string to check
Returns
-------
str
- normalized root path ( 'C:/' ) if valid, else an empty string
'''
if (len_drive := len(drive := forslash(drive).upper())) not in (2, 3):
return ''
if drive[0] not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' or drive[1] != ':':
return ''
if len_drive == 3:
if drive[2] != '/':
return ''
return drive
return drive + '/'
[docs]def checkzip(path: str) -> bool:
'''
- checks if an archive file (.7z or .zip) exists and is valid
- deletes file if invalid or incomplete
- does nothing to files with the wrong extension
Parameters
----------
path: str
- path to the archive file
Returns
-------
True
- file exists and is valid
False
- file doesn't exist, because either:
- we deleted it
- it didn't exist before
'''
try:
if (path := getpath(path)).lower().endswith('.7z'):
try:
with SevenZipFile(path, 'r'):
pass
return True # 7z file is good
except exceptions.Bad7zFile:
delete(path)
return False
elif path.lower().endswith('.zip'):
try:
with ZipFile(path):
pass
return True # zip file is good
except BadZipFile:
delete(path)
return False
else:
raise NotImplementedError(f'unsupported archive format: {path.split(".")[-1]}')
except FileNotFoundError:
return False
[docs]def copymove(func):
'''
- wrapper for :func:`py416.files.copy` and :func:`py416.files.move`
- copy and move are nearly the same, so this handles some of what they share
'''
@wraps(func)
def _copymove(path: str, dest: str, overwrite: bool = False):
if not os.path.exists(path := getpath(path)):
raise FileNotFoundError(f'not found: {path}')
if path == (dest := getpath(dest)):
raise ValueError(f'you can\'t move something into itself: {path}')
if os.path.exists(dest) and not os.path.isdir(dest):
raise FileExistsError(f'you can\'t move something into a file: {dest}')
makedirs(dest)
return forslash(func(path, dest, bool(overwrite)))
return _copymove
[docs]@copymove
def copy(path: str, dest: str, overwrite: bool = False) -> str:
'''
- copies file/folder and all contents, creating destination if nonexistent
Parameters
----------
path: str
- path to desired file/folder
dest: str
- path to destination folder (where we're copying into)
overwrite: bool
- whether to overwrite an existing file/folder
- merges folders, but overwrites files if they exist
- default: False
Returns
-------
str
- path to copied file/folder
'''
new_path_exists = os.path.exists(new_path := f'{dest}/{os.path.basename(path)}')
if os.path.isfile(path): # copying file
if new_path_exists and not overwrite:
raise FileExistsError(f'destination file already exists: {new_path}')
return copy2(path, new_path)
if new_path_exists: # copying folder
if overwrite:
return copytree(path, new_path, dirs_exist_ok=True) # overwriting dest folder
raise FileExistsError(f'destination folder already exists: {new_path}')
return copytree(path, new_path) # dest folder doesn't exist, good
[docs]def delete(path: str, force: bool = False, trash: bool = False) -> None:
'''
- deletes file/folder
Parameters
----------
path: str
- path to file/folder
force: bool
- whether to try `shutil.rmtree() <https://docs.python.org/3/library/shutil.html#shutil.rmtree>`_ to delete a folder and its contents
- default: False
trash: bool
- whether to try moving item to trash/recycle bin (if enabled for that drive)
- uses `Send2Trash <https://github.com/arsenetar/send2trash>`_
- UNSAFE - deletes file if trash/recycle bin disabled
- default: False
'''
if not os.path.exists(path := getpath(path)):
return
if bool(trash):
if os.name == 'nt':
path = path.replace('/', '\\')
return send2trash(path)
if os.path.isdir(path):
if bool(force):
rmtree(path)
else:
rmdir(path)
else:
os.remove(path)
[docs]def forslash(path: str) -> str:
'''
- replaces backslashes in paths with forward slashes
- used to unify path formatting between OS types
Parameters
----------
path: str
- path string
Returns
-------
str
- path with forward slashes
'''
if type(path) is not str:
raise TypeError(f'input must be a string; invalid: {path}')
return path.replace('\\', '/')
[docs]def getcwd() -> str:
'''
- gets the current working directory
- wraps `os.getcwd() <https://docs.python.org/3/library/os.html#os.getcwd>`_
Returns
-------
str
- path to current working directory
'''
return forslash(os.getcwd())
[docs]def getpath(path: str) -> str:
'''
- gets the full path of something, resolving most relative paths
Parameters
----------
path: str
- absolute or relative path
- if relative, we assume it's in the current working directory
Returns
-------
str
- absolute path
'''
if (parts := list(splitpath(path))) == ['']: # special case
return ''
path = joinpath(parts)
if (root := parts[0]).startswith('//') or root == '/': # UNC or Unix root
return path
if (cwdrive := checkwindrive(root)): # Windows root
parts[0] = cwdrive
return joinpath(parts)
return f'{getcwd()}/{path}'
[docs]def joinpath(*parts) -> str:
'''
- joins parts of a path together in the order they were received
Parameters
----------
parts: iterable
- folder/file names to join together
- iterable may be single or nested lists or tuples
Returns
-------
str
- joined path
'''
parts = list(unpack([list(splitpath(part)) for part in unpack(parts)])) # split elements if partial paths
if parts == ['/', '/']: # special case
return '/'
while '' in parts:
parts.remove('') # special case, such as passing 'folder/..' to splitpath()
if not len(parts):
return '' # nothing was passed, or it was cancelled out with '..'
if parts == ['/']:
return '/' # Unix root, alone
if parts[0] == '/':
return '/' + '/'.join(parts[1:]) # Unix root, preceding
if parts[0].startswith('//'):
return '/'.join(parts) # UNC
if (cwdrive := checkwindrive(parts[0])):
if len(parts) == 1:
return cwdrive # Windows drive root, alone
return cwdrive + '/'.join(parts[1:]) # Windows drive root, preceding
return '/'.join(parts)
[docs]def listdir(path: str = '.', dirs: bool = True, files: bool = True, recursive: bool = False, search: str = '', case: bool = False, recency=0, return_dict: bool = False, sort_dict: str = 'path'):
'''
- lists files/folders within a folder
- allows for some filtering by filename and modify date
- wraps `os.listdir() <https://docs.python.org/3/library/os.html#os.listdir>`_
Parameters
----------
path: str
- folder to search in
- default: current working directory
dirs: bool
- whether to list folders
- default: True
files: bool
- whether to list files
- default: True
recursive: bool
- whether to list all files and folders recursively
- if False, this will only list files/folders in the specified folder
- default: False
search: str
- like a glob, searches filenames for a specified pattern
- this does not search any files, rather the list of files we already gathered
- accepts Unix wildcards ( * ? [...] [!...] )
- default: nothing
case: bool
- whether to exactly match capitalization of search term
- does nothing if `search` is not set
- default: False
recency: float | int | str
- how old a file may be, so this only shows the most recently created/modified files
- type: float | int
- number of seconds
- type: str
- must be formatted like the base output from :func:`py416.secmod`, i.e. "3d16h5m47s"
- can be missing parts, i.e. "3d47s"
- capitalization is ignored
- if multiple of the same type of value are passed in the string, i.e. "4h16h", only the first value is grabbed
- default: 0 (include everything)
return_dict: bool
- whether to return a dictionary instead of a tuple
- dict contains details on each file with paths as keys and dicts of their details as values
- structured like so:
- {path: {'size': int, 'mtime': int, 'mage': str}}
- size: int
- bytes
- mtime: int
- modify time
- Unix timestamp
- mage: str
- how long ago the modify time was
- return is from :func:`py416.secmod`, i.e. '46d19h08m07s'
- default: False
sort_dict: str
- what to sort a returned dictionary by
- accepted values: path, size, mtime, mage
- always sorts in ascending order (small -> large)
- sorting by mage (modify age) is reverse to sorting by mtime (modify time)
- default: path
Returns
-------
- dict[str: dict[str: int, str: int, str: str]]
- absolute path, size, modify time, modify age
- tuple[str]
- absolute paths
'''
if not os.path.exists(path := getpath(path)):
return ()
if type(search) is not str:
raise TypeError(f'input must be a string; invalid: {search}')
if (recency_type := type(recency)) not in (float, int, str):
raise TypeError(f'input must be a number or string; invalid: {recency}')
if (sort_dict := str(sort_dict).lower()) not in ('path', 'size', 'mtime', 'mage'):
raise ValueError(f'invalid sort option: {sort_dict}')
result = []
now = time()
for child in os.listdir(path):
child = joinpath(path, child)
if os.path.isdir(child):
if bool(dirs):
result.append(child)
if bool(recursive):
result += list(listdir(child, dirs=dirs, files=files, recursive=True))
elif bool(files):
result.append(child)
if search:
tmp = []
for fpath in result:
name = os.path.basename(fpath)
if bool(case):
if fnmatchcase(name, search):
tmp.append(fpath)
continue
if fnmatch(name, search):
tmp.append(fpath)
result = tmp
if recency:
tmp = []
recency = secmod_inverse(recency) if recency_type is str else float(recency)
for fpath in result:
if now - os.path.getmtime(fpath) < recency:
tmp.append(fpath)
result = tmp
if bool(return_dict):
mydict = {}
tmp = []
for fpath in result:
stat = os.stat(fpath)
tmp.append((fpath, stat.st_size, (mtime := stat.st_mtime), secmod(now - mtime)[0]))
if sort_dict == 'size':
tmp = sorted(tmp, key=lambda i: i[1])
elif sort_dict == 'mtime':
tmp = sorted(tmp, key=lambda i: i[2])
elif sort_dict == 'mage':
tmp = sorted(tmp, key=lambda i: i[2], reverse=True)
for file in tmp:
mydict[file[0]] = {'size': file[1], 'mtime': int(file[2]), 'mage': file[3]}
return mydict
return tuple(result)
[docs]def log(path: str, msg: str, ts: bool = True, ts_args: tuple = (1, 0, 1, 1, 1, 0), encoding: str = 'utf-8') -> None:
'''
- logs to file with current timestamp
- creates file and its parent folder if nonexistent
Parameters
----------
path: str
- path to desired log file
msg: str
- message to log
ts: bool
- whether to include timestamp
- default: True
ts_args: iterable
- arguments to pass to :func:`py416.timestamp`
- default return example: [2022-08-19 13:24:54 -06:00]
encoding: str
- character set
- default: utf-8
'''
if type(msg) is not str:
raise ValueError(f'input must be a string; invalid: {msg}')
if type(ts_args) not in (list, tuple):
raise ValueError(f'input must be a list/tuple; invalid: {ts_args}')
makedirs(parent(path := getpath(path)))
with open(path, 'a', encoding=encoding) as file:
file.write(f'{timestamp(*ts_args) + " " if bool(ts) else ""}{msg}\n')
[docs]def makedirs(*dirs, ignore_errors: bool = True) -> tuple:
'''
- creates directories if nonexistent
- wraps `os.makedirs() <https://docs.python.org/3/library/os.html#os.makedirs>`_
Parameters
----------
dirs: list | str | tuple
- type: str
- path to folder
- type: list | tuple
- nestings of lists and tuples with folder path strings as base elements
ignore_errors: bool
- whether to catch all Exceptions from :func:`os.makedirs()`
- useful to create as many of the requested folders as possible
- default: True
Returns
-------
tuple
- folders we failed to create
'''
if type(dirs) not in (list, str, tuple):
raise TypeError(f'input must be a string/list/tuple; invalid: {dirs}')
errored = []
for dir in unpack(dirs):
if type(dir) is not str:
errored.append(dir)
continue
if not os.path.exists(dir := getpath(dir)):
try:
os.makedirs(dir)
except Exception:
if not bool(ignore_errors):
raise
errored.append(dir)
return tuple(errored)
[docs]def makefile(path: str, msg: str = '', overwrite: bool = False, encoding: str = 'utf-8') -> str:
'''
- creates a new file
- wraps `open() <https://docs.python.org/3/library/functions.html#open>`_
Parameters
----------
path: str
- path to desired new file
msg: str
- text to put in the file
- default: nothing
overwrite: bool
- whether to overwrite a file if it already exists
- default: False
encoding: str
- character set
- default: utf-8
Returns
-------
str
- path to new file
'''
if type(msg) is not str:
raise TypeError(f'input must be a string; invalid: {msg}')
if os.path.exists(path := getpath(path)) and bool(overwrite):
delete(path, force=True)
if os.path.isfile(path):
raise FileExistsError(f'destination file already exists: {path}')
if os.path.isdir(path):
raise IsADirectoryError(f'destination already exists as a directory: {path}')
makedirs(parent(path))
with open(path, 'a', encoding=encoding) as file:
file.write(msg)
return path
[docs]@copymove
def move(path: str, dest: str, overwrite: bool = False) -> str:
'''
- moves file/folder and all contents, creating destination if nonexistent
Parameters
----------
path: str
- path to desired file/folder
dest: str
- path to destination folder (where we're moving into)
overwrite: bool
- whether to overwrite an existing file/folder
- merges folders, but overwrites files if they exist
- default: False
Returns
-------
str
- path to moved file/folder
'''
new_path_exists = os.path.exists(new_path := f'{dest}/{os.path.basename(path)}')
if os.path.isfile(path): # moving file
if new_path_exists:
if not overwrite:
raise FileExistsError(f'destination file already exists: {new_path}')
delete(new_path) # deleting dest file to overwrite it
return shmv(path, dest)
if new_path_exists: # moving folder
if overwrite:
result = copytree(path, new_path, dirs_exist_ok=True) # overwriting dest folder
delete(path, force=True) # deleting original (we're doing copy->delete manually)
return result
raise FileExistsError(f'destination folder already exists: {new_path}')
return shmv(path, dest) # dest folder doesn't exist, good
[docs]def parent(path: str = '.') -> str:
'''
- gets the folder containing something
Parameters
----------
path: str
- path to find the parent of
- default: current working directory
Returns
-------
str
- parent path
'''
if not (path := getpath(path)):
return ''
if any([path == '/', # Root
checkwindrive(path),
path.startswith('//') and len(splitpath(path)) == 1]):
return path
return getpath(f'{path}/..')
[docs]def readfile(path: str, encoding: str = 'utf-8') -> str:
'''
- reads text from a file
- wraps `open() <https://docs.python.org/3/library/functions.html#open>`_
Parameters
----------
path: str
- path to file
encoding: str
- character set
- default: utf-8
Returns
-------
str
- all text from the file
'''
if not os.path.exists(path := getpath(path)):
raise FileNotFoundError(f'not found: {path}')
with open(path, 'r', encoding=encoding) as file:
result = file.read()
return result
[docs]def rename(path: str, name: str) -> str:
'''
- renames file/folder without moving it
- wraps `os.rename() <https://docs.python.org/3/library/os.html#os.rename>`_
Parameters
----------
path: str
- path to file/folder to be renamed
name: str
- new basename for file/folder (not path)
Returns
-------
str
- new path to file/folder
'''
if not os.path.exists(path := getpath(path)):
raise FileNotFoundError(f'not found: {path}')
if type(name) is not str:
raise TypeError(f'input must be a string; invalid: {name}')
os.rename(path, (newpath := joinpath(parent(path), name)))
return newpath
[docs]def rmdir(path: str, delroot: bool = True) -> int:
'''
- recursively deletes empty directories
- wraps `os.rmdir() <https://docs.python.org/3/library/os.html#os.rmdir>`_
Parameters
----------
path: str
- folder path to delete within
delroot: bool
- whether to delete the folder specified as well
- default: True
Returns
-------
int
- number of deleted folders
'''
if not os.path.exists(path := getpath(path)):
raise FileNotFoundError(f'not found: {path}')
if not os.path.isdir(path):
raise ValueError(f'not a folder: {path}')
count = 0
for item in listdir(path):
if os.path.isdir(item):
count += rmdir(item)
if not len(listdir(path)) and bool(delroot):
if path == getcwd():
cd() # we're in the folder we're trying to delete, so go up
os.rmdir(path)
count += 1
return count
[docs]def splitpath(path: str) -> tuple:
'''
- splits a path string into its parts
Parameters
----------
path: str
- path
- can be absolute or relative
Returns
-------
tuple[str]
- folders/file
'''
if not path:
return '',
if (path := forslash(path)) in ('/', '/.', '/..'): # special cases
return '/',
win_net = False
if path == '.': # current directory
path = getcwd()
elif path == '..': # parent of current directory
path = forslash(os.path.dirname(getcwd()))
elif path == '.' * len(path): # >2 dots - current directory
path = getcwd()
elif (parts := path.split('/'))[-1] == '.': # folder/. is just folder
path = path[:-2]
elif parts[-1] == '..': # parent of path
if len(parts) == 2: # 'folder/..'
return '',
if path.startswith('//'): # UNC
path = path.lstrip('/')
win_net = True
path = os.path.dirname(os.path.dirname(path))
if win_net:
path = '//' + path
parts = path.split('/')
if not parts[0]: # Unix root
parts[0] = '/'
if (cwdrive := checkwindrive(parts[0])): # Windows drive root
parts[0] = cwdrive
if path.startswith('//'): # UNC
result = [f'//{parts[2]}']
return tuple(result + parts[3:]) if len(parts) > 2 else tuple(result)
if len(parts) == 2:
if parts[1] == '':
return parts[0],
return tuple(parts)
[docs]def unzip(path: str, remove: bool = False) -> None:
'''
- unzips archive files of type: (.7z, .gz, .rar, .tar, .zip)
Parameters
----------
path: str
- path to archive file
remove: bool
- whether to delete archive after unzipping
- default: False
'''
if not os.path.exists(path := getpath(path)):
raise FileNotFoundError(f'not found: {path}')
remove = bool(remove)
fparent = parent(path)
if path.endswith('.7z'):
unpack_7zarchive(path, fparent)
if remove:
delete(path)
elif path.endswith(('.gz', '.rar', '.tar', '.zip')):
unpack_archive(path, fparent)
if remove:
delete(path)
else:
raise NotImplementedError(f'unsupported archive format: {path.split(".")[-1]}')
[docs]def unzipdir(path: str = '.', ignore_errors: bool = True) -> int:
'''
- unzips all archive files in a folder until it is unable to continue
- recursive on archive files, but not folders
- deletes all archive files as it unzips
- supports archive files of type: (.7z, .gz, .rar, .tar, .zip)
Parameters
----------
path: str
- folder containing archive files
- default: current working directory
ignore_errors: bool
- whether to catch all Exceptions in unzipping
- useful to unzip everything possible in the directory
- default: True
Returns
-------
int
- number of unzipped archives
'''
if not os.path.exists(path := getpath(path)):
raise FileNotFoundError(f'not found: {path}')
unzipped = 0
while True:
unzipped_this_run = 0
for file in listdir(path, dirs=False):
if not file.endswith(('.7z', '.gz', '.rar', '.tar', '.zip')):
continue
try:
unzip(file, remove=True)
unzipped += 1
unzipped_this_run += 1
except Exception as e:
if not bool(ignore_errors):
raise Exception(f'unzipped {unzipped} archive file(s) before failing: {e}')
if not unzipped_this_run:
break
return unzipped