PK!docci/__init__.pyPK!WLdocci/config.py""" Constants required for lib """ import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) TEST_DATA_DIR = os.path.join(BASE_DIR, "test_data") PK!~ll docci/file.py""" Utils for file manipulations like extracting file name from path """ import base64 import io import os from dataclasses import dataclass, field from typing import Optional, Dict, Union, Iterable, Tuple from urllib.parse import urlencode, quote DirectoryName = str Directory = Tuple[DirectoryName, Iterable['FileAttachment']] @dataclass class FileAttachment: """ Class for file abstraction """ name: str content: bytes = field(repr=False) def __post_init__(self) -> None: """Normalize name""" self.name = normalize_name(self.name) @property def name_without_extension(self) -> str: """ >>> FileAttachment("sample.py", b"").name_without_extension 'sample' """ return self.name.rsplit(".", 1)[0] @property def extension(self) -> str: """ >>> FileAttachment("sample.py", b"").extension 'py' """ return self.name.rsplit(".", 1)[-1] @property def content_stream(self) -> io.BytesIO: """Return file attachment content as bytes stream""" return io.BytesIO(self.content) @property def content_base64(self) -> bytes: """Convert content to base64 binary string""" return base64.b64encode(self.content) @property def content_disposition(self) -> Dict[str, str]: """ Convert file name to urlencoded Content-Disposition header >>> FileAttachment("sample.py", b"").content_disposition {'Content-Disposition': 'attachment; filename=sample.py'} >>> FileAttachment("98 - February 2019.zip", b"").content_disposition {'Content-Disposition': 'attachment; filename=98%20-%20February%202019.zip'} """ file_name = urlencode({"filename": self.name}, quote_via=quote) return {"Content-Disposition": f'attachment; {file_name}'} @property def mimetype(self) -> str: """ Guess mimetype by extension. """ if self.extension == "json": return "application/json" if self.extension == "zip": return "application/zip" return "application/octet-stream" def save(self, path: Optional[str] = None) -> None: """ Save file to disk """ path = path or self.name with open(path, "wb") as f: f.write(self.content) @classmethod def load(cls, path: str) -> 'FileAttachment': """ Load file from disk """ assert os.path.exists(path), f'No such file: "{path}"' with open(path, "rb") as f: return FileAttachment(extract_file_name(path), f.read()) @classmethod def load_from_base64(cls, base64_str: Union[str, bytes], name: str) -> 'FileAttachment': """ Load file from base64 string """ return FileAttachment(name, base64.b64decode(base64_str)) def extract_file_name(path: str) -> str: """ Extract file name from path, works to directories too >>> extract_file_name("tests/test_api.py") 'test_api.py' >>> extract_file_name("tests/test") 'test' """ return os.path.basename(path) def normalize_name(raw_name: str) -> str: """ Extract file name, remove restricted chars >>> normalize_name('op/"oppa".txt') 'oppa.txt' """ name = extract_file_name(raw_name) for restricted in r'\/:*?"<>|': name = name.replace(restricted, "") return name def list_dir_files(directory: str) -> Directory: """List directory files, return Directory""" paths = os.listdir(directory) full_paths = [os.path.join(directory, path) for path in paths] files = [FileAttachment.load(path) for path in full_paths if os.path.isfile(path)] return extract_file_name(directory), files PK!۳ docci/zip.py""" Utils for working with zip archives """ import io from typing import Union, Iterable, Sequence from zipfile import ZipFile from docci.file import FileAttachment, Directory RawZipFile = Union[str, bytes, io.BytesIO, ZipFile, FileAttachment] def raw_to_zip(raw_zip_file: RawZipFile) -> ZipFile: """ Convert path, bytes, stream, FileAttachment to ZipFile. """ if isinstance(raw_zip_file, ZipFile): return raw_zip_file if isinstance(raw_zip_file, bytes): return ZipFile(io.BytesIO(raw_zip_file)) if isinstance(raw_zip_file, FileAttachment): return ZipFile(raw_zip_file.content_stream) if isinstance(raw_zip_file, (str, io.BytesIO)): return ZipFile(raw_zip_file) def list_zip_files(raw_zip_file: RawZipFile) -> Sequence[FileAttachment]: """ List zip archive files """ def zip_file_generator(zip_file: ZipFile) -> Iterable[FileAttachment]: for filename in zip_file.namelist(): content = zip_file.read(filename) yield FileAttachment(filename, content) zip_file = raw_to_zip(raw_zip_file) return list(zip_file_generator(zip_file)) def zip_files(zip_name: str, files: Iterable[FileAttachment]) -> FileAttachment: """Zip files to archive with {zip_name}""" stream = io.BytesIO() with ZipFile(stream, mode="w") as zf: for file in files: zf.writestr(file.name, file.content) return FileAttachment(zip_name, stream.getvalue()) def zip_dirs(zip_name: str, dirs: Iterable[Directory]) -> FileAttachment: """Zip folders into single zip archive with {zip_name}""" stream = io.BytesIO() with ZipFile(stream, mode="w") as zf: for folder_name, folder_files in dirs: for file in folder_files: zf.writestr(f"{folder_name}/{file.name}", file.content) return FileAttachment(zip_name, stream.getvalue()) PK!HڽTUdocci-0.3.1.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H/t蜷{7gl|$3o /Va/jNͮ_~2 |\:X_w02`8[gRdwCH͍ H t,n*JBvt߆Mz&J)>W' +$*ٮg.'M#8@PK!HGcNdocci-0.3.1.dist-info/RECORDuKr0},1dj|, c%`FQj8}gZqw{MְKq?VoaPWs/%CItzuWV/ѿ.v@~ICd,JH/_eWo#Ql!`LJP&c}Jv']^#Ocu+Q,eXFe"סt