klygo 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
klygo/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ from . import archive
2
+ from . import datasets
3
+ from . import io
4
+ from . import models
5
+ from . import utils
6
+ from . import validators
7
+
8
+ __version__ = "1.0.1"
9
+ __author__ = "IchigoMazone"
10
+
11
+ __all__ = [
12
+ "archive",
13
+ "datasets",
14
+ "io",
15
+ "models",
16
+ "utils",
17
+ "validators",
18
+ ]
@@ -0,0 +1,20 @@
1
+ from .compress import compress
2
+ from .extract import extract, extract_file
3
+ from .list import list_files, search
4
+ from .info import get_info, test
5
+ from .modify import add, remove
6
+ from .transform import merge, split
7
+
8
+ __all__ = [
9
+ "compress",
10
+ "extract",
11
+ "extract_file",
12
+ "list_files",
13
+ "search",
14
+ "get_info",
15
+ "test",
16
+ "add",
17
+ "remove",
18
+ "merge",
19
+ "split",
20
+ ]
@@ -0,0 +1,7 @@
1
+ def _human_size(num_bytes: int) -> str:
2
+ """Return a human-readable file size string."""
3
+ for unit in ("B", "KB", "MB", "GB", "TB"):
4
+ if num_bytes < 1024:
5
+ return f"{num_bytes:.1f} {unit}"
6
+ num_bytes //= 1024
7
+ return f"{num_bytes:.1f} PB"
@@ -0,0 +1,119 @@
1
+ from zipfile import ZipFile, ZIP_DEFLATED
2
+ from pathlib import Path
3
+
4
+ from tqdm import tqdm
5
+
6
+ from klygo.validators.archive import Compress
7
+ from klygo.archive._utils import _human_size
8
+
9
+
10
+ def compress(
11
+ source: str | Path,
12
+ output: str | Path,
13
+ format: str = "zip",
14
+ overwrite: bool = False,
15
+ verbose: bool = True,
16
+ ) -> None:
17
+ """Compress a file or directory into a ZIP archive.
18
+
19
+ When ``source`` is a directory, all files are added recursively
20
+ while preserving the internal directory structure. Parent-level
21
+ directory name is kept as the root inside the archive.
22
+
23
+ Parameters
24
+ ----------
25
+ source : str or Path
26
+ Path to the file or directory to compress.
27
+ output : str or Path
28
+ Destination path for the archive (e.g. ``"data.zip"``).
29
+ Parent directories are created automatically if they do not exist.
30
+ format : str, optional
31
+ Archive format. Currently only ``"zip"`` is supported.
32
+ Default is ``"zip"``.
33
+ overwrite : bool, optional
34
+ If *True*, silently overwrite ``output`` when it already exists.
35
+ If *False* (default) and ``output`` exists, raises ``FileExistsError``.
36
+ verbose : bool, optional
37
+ If *True* (default), display a coloured progress bar and print
38
+ the final archive size on completion.
39
+
40
+ Returns
41
+ -------
42
+ None
43
+
44
+ Raises
45
+ ------
46
+ TypeError
47
+ If any argument has the wrong type.
48
+ FileNotFoundError
49
+ If ``source`` does not exist.
50
+ FileExistsError
51
+ If ``output`` already exists and ``overwrite`` is *False*.
52
+ ValueError
53
+ If ``format`` is not supported, or ``output`` has the wrong extension.
54
+
55
+ Examples
56
+ --------
57
+ Compress a single file:
58
+
59
+ >>> compress("report.csv", "report.zip")
60
+
61
+ Compress an entire directory, overwriting any existing archive:
62
+
63
+ >>> compress("my_dataset/", "dataset.zip", overwrite=True)
64
+
65
+ Compress silently (no progress bar):
66
+
67
+ >>> compress("images/", "images.zip", verbose=False)
68
+ """
69
+
70
+ params = Compress(
71
+ source=source,
72
+ output=output,
73
+ format=format,
74
+ overwrite=overwrite,
75
+ verbose=verbose,
76
+ )
77
+
78
+ source_path: Path = params.source
79
+ output_path: Path = params.output
80
+
81
+ # Collect all files to compress
82
+ if source_path.is_dir():
83
+ all_files: list[Path] = sorted(
84
+ f for f in source_path.rglob("*") if f.is_file()
85
+ )
86
+ else:
87
+ all_files = [source_path]
88
+
89
+ output_path.parent.mkdir(parents=True, exist_ok=True)
90
+
91
+ with ZipFile(output_path, mode="w", compression=ZIP_DEFLATED) as zf:
92
+ bar = (
93
+ tqdm(
94
+ total=len(all_files) or 1,
95
+ desc="Compressing",
96
+ unit="file",
97
+ colour="green",
98
+ bar_format="{l_bar}{bar:30}{r_bar}",
99
+ )
100
+ if verbose
101
+ else None
102
+ )
103
+ for file in all_files:
104
+ arcname = (
105
+ file.relative_to(source_path.parent)
106
+ if source_path.is_dir()
107
+ else file.name
108
+ )
109
+ zf.write(file, arcname=arcname)
110
+ if bar is not None:
111
+ bar.update(1)
112
+ if bar is not None:
113
+ if not all_files:
114
+ bar.update(1)
115
+ bar.close()
116
+
117
+ if verbose:
118
+ total_size = output_path.stat().st_size
119
+ print(f"Done. Archive size: {_human_size(total_size)}")
@@ -0,0 +1,183 @@
1
+ from zipfile import ZipFile
2
+ from pathlib import Path
3
+
4
+ from tqdm import tqdm
5
+
6
+ from klygo.validators.archive import Extract, ExtractFile
7
+
8
+
9
+ def extract(
10
+ source: str | Path,
11
+ output: str | Path = ".",
12
+ overwrite: bool = False,
13
+ verbose: bool = True,
14
+ ) -> None:
15
+ """Extract all contents of an archive to a directory.
16
+
17
+ The full directory structure stored inside the archive is
18
+ recreated under ``output``. The output directory is created
19
+ automatically if it does not exist.
20
+
21
+ Parameters
22
+ ----------
23
+ source : str or Path
24
+ Path to the archive file to extract (e.g. ``"data.zip"``).
25
+ output : str or Path, optional
26
+ Directory where the archive contents will be extracted.
27
+ Defaults to the current working directory (``"."``).
28
+ overwrite : bool, optional
29
+ If *True*, overwrite files that already exist in ``output``.
30
+ If *False* (default) and conflicting files exist, raises
31
+ ``FileExistsError`` listing the first offending names.
32
+ verbose : bool, optional
33
+ If *True* (default), display a coloured progress bar and print
34
+ the total number of extracted files on completion.
35
+
36
+ Returns
37
+ -------
38
+ None
39
+
40
+ Raises
41
+ ------
42
+ TypeError
43
+ If any argument has the wrong type.
44
+ FileNotFoundError
45
+ If ``source`` does not exist.
46
+ ValueError
47
+ If ``source`` is not a supported archive format.
48
+ FileExistsError
49
+ If files in the archive already exist at the destination
50
+ and ``overwrite`` is *False*.
51
+
52
+ Examples
53
+ --------
54
+ Extract to a specific folder:
55
+
56
+ >>> extract("data.zip", output="data/")
57
+
58
+ Extract and overwrite any existing files:
59
+
60
+ >>> extract("data.zip", output="data/", overwrite=True)
61
+
62
+ Extract silently:
63
+
64
+ >>> extract("data.zip", output="data/", verbose=False)
65
+ """
66
+
67
+ params = Extract(
68
+ source=source,
69
+ output=output,
70
+ overwrite=overwrite,
71
+ verbose=verbose,
72
+ )
73
+
74
+ source_path: Path = params.source
75
+ output_path: Path = params.output
76
+
77
+ output_path.mkdir(parents=True, exist_ok=True)
78
+
79
+ with ZipFile(source_path, mode="r") as zf:
80
+ members = zf.infolist()
81
+
82
+ if not overwrite:
83
+ existing = [
84
+ m for m in members
85
+ if (output_path / m.filename).exists()
86
+ ]
87
+ if existing:
88
+ names = ", ".join(m.filename for m in existing[:5])
89
+ suffix = f"… (+{len(existing) - 5} more)" if len(existing) > 5 else ""
90
+ raise FileExistsError(
91
+ f"Files already exist in output directory: {names}{suffix}. "
92
+ "Use overwrite=True to replace them."
93
+ )
94
+
95
+ iterator = (
96
+ tqdm(
97
+ members,
98
+ desc="Extracting",
99
+ unit="file",
100
+ colour="cyan",
101
+ bar_format="{l_bar}{bar:30}{r_bar}",
102
+ )
103
+ if verbose
104
+ else iter(members)
105
+ )
106
+
107
+ for member in iterator:
108
+ zf.extract(member, path=output_path)
109
+
110
+ if verbose:
111
+ print(f"Done. Extracted {len(members)} file(s) to '{output_path}'")
112
+
113
+
114
+ def extract_file(
115
+ source: str | Path,
116
+ filename: str,
117
+ output: str | Path = ".",
118
+ overwrite: bool = False,
119
+ ) -> None:
120
+ """Extract a single file from an archive without unpacking everything.
121
+
122
+ Use :func:`list_files` to discover the exact ``filename`` string
123
+ as it is stored inside the archive, then pass that value here.
124
+
125
+ Parameters
126
+ ----------
127
+ source : str or Path
128
+ Path to the archive file.
129
+ filename : str
130
+ Path of the target file *inside* the archive, exactly as returned
131
+ by :func:`list_files` (e.g. ``"images/frame_001.jpg"``).
132
+ output : str or Path, optional
133
+ Directory where the file will be written.
134
+ Defaults to the current working directory (``"."``).
135
+ overwrite : bool, optional
136
+ If *True*, overwrite the file if it already exists at the destination.
137
+ Default is *False*.
138
+
139
+ Returns
140
+ -------
141
+ None
142
+
143
+ Raises
144
+ ------
145
+ TypeError
146
+ If any argument has the wrong type.
147
+ FileNotFoundError
148
+ If ``source`` does not exist.
149
+ KeyError
150
+ If ``filename`` is not found inside the archive.
151
+ FileExistsError
152
+ If the destination file already exists and ``overwrite`` is *False*.
153
+
154
+ Examples
155
+ --------
156
+ >>> extract_file("data.zip", "images/frame_001.jpg", output="out/")
157
+ """
158
+
159
+ params = ExtractFile(
160
+ source=source,
161
+ filename=filename,
162
+ output=output,
163
+ overwrite=overwrite,
164
+ )
165
+
166
+ output_path: Path = params.output
167
+ output_path.mkdir(parents=True, exist_ok=True)
168
+
169
+ with ZipFile(params.source, mode="r") as zf:
170
+ names = zf.namelist()
171
+ if params.filename not in names:
172
+ raise KeyError(
173
+ f"'{params.filename}' not found in archive. "
174
+ f"Use list_files() to see available files."
175
+ )
176
+
177
+ target = output_path / Path(params.filename).name
178
+ if target.exists() and not params.overwrite:
179
+ raise FileExistsError(
180
+ f"file already exists: {target}. Use overwrite=True."
181
+ )
182
+
183
+ zf.extract(params.filename, path=output_path)
klygo/archive/info.py ADDED
@@ -0,0 +1,125 @@
1
+ from zipfile import ZipFile
2
+ from pathlib import Path
3
+
4
+ from klygo.validators.archive import GetInfo, Test
5
+
6
+
7
+ def get_info(
8
+ source: str | Path,
9
+ ) -> dict:
10
+ """Return a metadata summary of an archive.
11
+
12
+ Reads all member headers in the ZIP central directory — no
13
+ decompression is performed, so this is fast even for very large archives.
14
+
15
+ Parameters
16
+ ----------
17
+ source : str or Path
18
+ Path to the archive file.
19
+
20
+ Returns
21
+ -------
22
+ dict
23
+ A dictionary with the following keys:
24
+
25
+ * ``"path"`` (*str*) — absolute path to the archive.
26
+ * ``"format"`` (*str*) — archive format, e.g. ``"zip"``.
27
+ * ``"file_count"`` (*int*) — number of files inside the archive.
28
+ * ``"uncompressed_size"`` (*int*) — total size of all files before
29
+ compression, in bytes.
30
+ * ``"compressed_size"`` (*int*) — total size of all files after
31
+ compression, in bytes.
32
+ * ``"compress_ratio"`` (*float*) — space saved by compression,
33
+ expressed as a percentage (0–100). A value of ``0.0`` means
34
+ no compression was applied.
35
+ * ``"archive_size"`` (*int*) — actual size of the ``.zip`` file
36
+ on disk (includes ZIP metadata overhead).
37
+
38
+ Raises
39
+ ------
40
+ TypeError
41
+ If ``source`` is not a ``str`` or ``Path``.
42
+ FileNotFoundError
43
+ If ``source`` does not exist.
44
+ ValueError
45
+ If ``source`` is not a supported archive format.
46
+
47
+ Examples
48
+ --------
49
+ >>> info = get_info("data.zip")
50
+ >>> print(info["file_count"], info["compress_ratio"])
51
+ 723 0.64
52
+ """
53
+
54
+ params = GetInfo(source=source)
55
+
56
+ with ZipFile(params.source, mode="r") as zf:
57
+ members = zf.infolist()
58
+ total_uncompressed = sum(m.file_size for m in members)
59
+ total_compressed = sum(m.compress_size for m in members)
60
+
61
+ ratio = (
62
+ round((1 - total_compressed / total_uncompressed) * 100, 2)
63
+ if total_uncompressed > 0
64
+ else 0.0
65
+ )
66
+
67
+ return {
68
+ "path": str(params.source),
69
+ "format": params.source.suffix.lstrip("."),
70
+ "file_count": len(members),
71
+ "uncompressed_size": total_uncompressed,
72
+ "compressed_size": total_compressed,
73
+ "compress_ratio": ratio,
74
+ "archive_size": params.source.stat().st_size,
75
+ }
76
+
77
+
78
+ def test(
79
+ source: str | Path,
80
+ ) -> bool:
81
+ """Verify the integrity of an archive using CRC checksum validation.
82
+
83
+ Each file inside the archive is read and its CRC32 checksum is compared
84
+ against the value stored in the ZIP header. This catches bit-rot,
85
+ partial downloads, and corrupted archives.
86
+
87
+ Parameters
88
+ ----------
89
+ source : str or Path
90
+ Path to the archive file to test.
91
+
92
+ Returns
93
+ -------
94
+ bool
95
+ *True* if every file in the archive passes its CRC check.
96
+
97
+ Raises
98
+ ------
99
+ TypeError
100
+ If ``source`` is not a ``str`` or ``Path``.
101
+ FileNotFoundError
102
+ If ``source`` does not exist.
103
+ ValueError
104
+ If ``source`` is not a supported archive format, or if the
105
+ archive is corrupted. The error message includes the name of
106
+ the first file that failed the checksum.
107
+
108
+ Examples
109
+ --------
110
+ >>> ok = test("data.zip")
111
+ >>> print(ok)
112
+ True
113
+ """
114
+
115
+ params = Test(source=source)
116
+
117
+ with ZipFile(params.source, mode="r") as zf:
118
+ bad_file = zf.testzip()
119
+
120
+ if bad_file is not None:
121
+ raise ValueError(
122
+ f"archive is corrupted. First bad file: '{bad_file}'"
123
+ )
124
+
125
+ return True
klygo/archive/list.py ADDED
@@ -0,0 +1,108 @@
1
+ from zipfile import ZipFile
2
+ from pathlib import Path
3
+ import fnmatch
4
+
5
+ from klygo.validators.archive import ListFiles, Search
6
+
7
+
8
+ def list_files(
9
+ source: str | Path,
10
+ ) -> list[str]:
11
+ """Return a list of all file paths stored inside an archive.
12
+
13
+ The returned strings are the *arcnames* — the paths as they are
14
+ stored inside the ZIP (e.g. ``"images/frame_001.jpg"``). These
15
+ values can be passed directly to :func:`extract_file`, :func:`remove`,
16
+ and :func:`search`.
17
+
18
+ Parameters
19
+ ----------
20
+ source : str or Path
21
+ Path to the archive file (e.g. ``"data.zip"``).
22
+
23
+ Returns
24
+ -------
25
+ list[str]
26
+ Ordered list of file paths as stored inside the archive.
27
+
28
+ Raises
29
+ ------
30
+ TypeError
31
+ If ``source`` is not a ``str`` or ``Path``.
32
+ FileNotFoundError
33
+ If ``source`` does not exist.
34
+ ValueError
35
+ If ``source`` is not a supported archive format.
36
+
37
+ Examples
38
+ --------
39
+ >>> files = list_files("data.zip")
40
+ >>> print(files[:3])
41
+ ['images/frame_0.jpg', 'images/frame_1.jpg', 'data.yaml']
42
+ """
43
+
44
+ params = ListFiles(source=source)
45
+
46
+ with ZipFile(params.source, mode="r") as zf:
47
+ names = zf.namelist()
48
+
49
+ return names
50
+
51
+
52
+ def search(
53
+ source: str | Path,
54
+ pattern: str,
55
+ ) -> list[str]:
56
+ """Find files inside an archive whose paths match a glob pattern.
57
+
58
+ Matching is performed with :func:`fnmatch.fnmatch` against each
59
+ arcname stored in the archive. The search is case-sensitive on
60
+ case-sensitive filesystems.
61
+
62
+ Parameters
63
+ ----------
64
+ source : str or Path
65
+ Path to the archive file.
66
+ pattern : str
67
+ Glob pattern to match against file paths inside the archive.
68
+ Supports the standard wildcards:
69
+
70
+ * ``*`` — matches any sequence of characters (not ``/``).
71
+ * ``?`` — matches any single character.
72
+ * ``[seq]`` — matches any character in *seq*.
73
+
74
+ Example: ``"images/*.jpg"`` matches all JPEG files under the
75
+ ``images/`` folder.
76
+
77
+ Returns
78
+ -------
79
+ list[str]
80
+ Arcnames of the files that matched ``pattern``, in the order
81
+ they appear inside the archive. Returns an empty list if
82
+ nothing matches.
83
+
84
+ Raises
85
+ ------
86
+ TypeError
87
+ If ``source`` or ``pattern`` has the wrong type.
88
+ FileNotFoundError
89
+ If ``source`` does not exist.
90
+ ValueError
91
+ If ``source`` is not a supported archive format.
92
+
93
+ Examples
94
+ --------
95
+ >>> results = search("data.zip", "images/frame_1*.jpg")
96
+ >>> print(len(results))
97
+ 111
98
+
99
+ >>> search("data.zip", "*.yaml")
100
+ ['data.yaml']
101
+ """
102
+
103
+ params = Search(source=source, pattern=pattern)
104
+
105
+ with ZipFile(params.source, mode="r") as zf:
106
+ names = zf.namelist()
107
+
108
+ return [name for name in names if fnmatch.fnmatch(name, params.pattern)]