pagonic 0.5.0__tar.gz
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.
- pagonic-0.5.0/LICENSE +21 -0
- pagonic-0.5.0/PKG-INFO +245 -0
- pagonic-0.5.0/Pagonic/__init__.py +62 -0
- pagonic-0.5.0/Pagonic/cli/__init__.py +16 -0
- pagonic-0.5.0/Pagonic/cli/commands/__init__.py +5 -0
- pagonic-0.5.0/Pagonic/cli/commands/benchmark.py +280 -0
- pagonic-0.5.0/Pagonic/cli/commands/config.py +216 -0
- pagonic-0.5.0/Pagonic/cli/main.py +752 -0
- pagonic-0.5.0/Pagonic/cli/policy.py +137 -0
- pagonic-0.5.0/Pagonic/cli/utils.py +144 -0
- pagonic-0.5.0/Pagonic/core/compression.py +165 -0
- pagonic-0.5.0/Pagonic/core/config_manager.py +287 -0
- pagonic-0.5.0/Pagonic/core/formats/__init__.py +21 -0
- pagonic-0.5.0/Pagonic/core/formats/adaptive_buffer.py +335 -0
- pagonic-0.5.0/Pagonic/core/formats/base.py +165 -0
- pagonic-0.5.0/Pagonic/core/formats/buffer_pool.py +355 -0
- pagonic-0.5.0/Pagonic/core/formats/compression_utils.py +552 -0
- pagonic-0.5.0/Pagonic/core/formats/constants.py +74 -0
- pagonic-0.5.0/Pagonic/core/formats/errors.py +262 -0
- pagonic-0.5.0/Pagonic/core/formats/handlers/__init__.py +99 -0
- pagonic-0.5.0/Pagonic/core/formats/handlers/zip_handler.py +301 -0
- pagonic-0.5.0/Pagonic/core/formats/hybrid_decompressor.py +219 -0
- pagonic-0.5.0/Pagonic/core/formats/inspection.py +629 -0
- pagonic-0.5.0/Pagonic/core/formats/minimal_zip_writer.py +467 -0
- pagonic-0.5.0/Pagonic/core/formats/optimized_decompressor.py +290 -0
- pagonic-0.5.0/Pagonic/core/formats/registry.py +790 -0
- pagonic-0.5.0/Pagonic/core/formats/results.py +62 -0
- pagonic-0.5.0/Pagonic/core/formats/security.py +276 -0
- pagonic-0.5.0/Pagonic/core/formats/simd_crc32.py +327 -0
- pagonic-0.5.0/Pagonic/core/formats/simd_memory.py +66 -0
- pagonic-0.5.0/Pagonic/core/formats/zip_parallel_orchestrator.py +129 -0
- pagonic-0.5.0/Pagonic/core/formats/zip_reader.py +479 -0
- pagonic-0.5.0/Pagonic/core/formats/zip_structs.py +698 -0
- pagonic-0.5.0/Pagonic/core/formats/zip_writer.py +304 -0
- pagonic-0.5.0/Pagonic/core/utils/__init__.py +13 -0
- pagonic-0.5.0/Pagonic/core/utils/path_utils.py +138 -0
- pagonic-0.5.0/Pagonic/gui/__init__.py +28 -0
- pagonic-0.5.0/Pagonic/gui/__main__.py +19 -0
- pagonic-0.5.0/Pagonic/gui/widgets/__init__.py +5 -0
- pagonic-0.5.0/Pagonic/gui/windows/__init__.py +12 -0
- pagonic-0.5.0/Pagonic/gui/windows/archive_window.py +402 -0
- pagonic-0.5.0/Pagonic/gui/windows/compress_dialog.py +400 -0
- pagonic-0.5.0/Pagonic/gui/windows/main_window.py +288 -0
- pagonic-0.5.0/Pagonic/gui/windows/settings_dialog.py +227 -0
- pagonic-0.5.0/Pagonic/gui/workers/__init__.py +10 -0
- pagonic-0.5.0/Pagonic/gui/workers/compression.py +118 -0
- pagonic-0.5.0/Pagonic/gui/workers/extraction.py +109 -0
- pagonic-0.5.0/README.md +212 -0
- pagonic-0.5.0/pagonic.egg-info/PKG-INFO +245 -0
- pagonic-0.5.0/pagonic.egg-info/SOURCES.txt +77 -0
- pagonic-0.5.0/pagonic.egg-info/dependency_links.txt +1 -0
- pagonic-0.5.0/pagonic.egg-info/entry_points.txt +3 -0
- pagonic-0.5.0/pagonic.egg-info/requires.txt +16 -0
- pagonic-0.5.0/pagonic.egg-info/top_level.txt +1 -0
- pagonic-0.5.0/pyproject.toml +69 -0
- pagonic-0.5.0/setup.cfg +4 -0
- pagonic-0.5.0/tests/test_api_contracts.py +85 -0
- pagonic-0.5.0/tests/test_callback_integration.py +266 -0
- pagonic-0.5.0/tests/test_ci_integration.py +107 -0
- pagonic-0.5.0/tests/test_cli.py +718 -0
- pagonic-0.5.0/tests/test_error_handling_comprehensive.py +291 -0
- pagonic-0.5.0/tests/test_examples.py +26 -0
- pagonic-0.5.0/tests/test_gui_entrypoint.py +27 -0
- pagonic-0.5.0/tests/test_hybrid_multiple_large_files.py +88 -0
- pagonic-0.5.0/tests/test_import_boundaries.py +116 -0
- pagonic-0.5.0/tests/test_inspection.py +408 -0
- pagonic-0.5.0/tests/test_inspection_schema.py +126 -0
- pagonic-0.5.0/tests/test_integration.py +317 -0
- pagonic-0.5.0/tests/test_package_audit.py +28 -0
- pagonic-0.5.0/tests/test_performance.py +79 -0
- pagonic-0.5.0/tests/test_policy.py +124 -0
- pagonic-0.5.0/tests/test_release_audit.py +35 -0
- pagonic-0.5.0/tests/test_sarif_evaluation.py +29 -0
- pagonic-0.5.0/tests/test_smart_compression.py +172 -0
- pagonic-0.5.0/tests/test_unicode_paths.py +161 -0
- pagonic-0.5.0/tests/test_zip64.py +135 -0
- pagonic-0.5.0/tests/test_zip_handler_compatibility.py +83 -0
- pagonic-0.5.0/tests/test_zip_reader_comprehensive.py +276 -0
- pagonic-0.5.0/tests/test_zip_writer_comprehensive.py +339 -0
pagonic-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pagonic contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pagonic-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pagonic
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: A security-aware Python ZIP inspection and safe extraction toolkit.
|
|
5
|
+
Author: Pagonic contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: zip,archive,inspection,safe-extraction,cli
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: System :: Archiving :: Compression
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: click>=8.1.7
|
|
20
|
+
Requires-Dist: rich>=13.7.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
23
|
+
Requires-Dist: coverage>=7.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=6.1.1; extra == "dev"
|
|
26
|
+
Requires-Dist: psutil>=5.9.0; extra == "dev"
|
|
27
|
+
Provides-Extra: gui
|
|
28
|
+
Requires-Dist: PyQt6>=6.6.0; extra == "gui"
|
|
29
|
+
Provides-Extra: performance
|
|
30
|
+
Requires-Dist: numpy>=1.24.0; extra == "performance"
|
|
31
|
+
Requires-Dist: psutil>=5.9.0; extra == "performance"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# Pagonic
|
|
35
|
+
|
|
36
|
+
Pagonic is an alpha Python ZIP toolkit focused on safe archive inspection,
|
|
37
|
+
secure extraction, and repeatable local benchmarking. Its main idea is simple:
|
|
38
|
+
|
|
39
|
+
> Inspect before you extract.
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<img src="assets/pagonic-demo.gif" alt="Pagonic inspecting a suspicious ZIP and refusing unsafe automation" width="960">
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
The intended workflow is visible in the demo: inspect the archive, verify it
|
|
46
|
+
against an explicit risk threshold, and let `safe-extract` refuse unsafe input
|
|
47
|
+
before it writes files.
|
|
48
|
+
|
|
49
|
+
* A core library for inspecting, writing, reading, and validating ZIP archives.
|
|
50
|
+
* A `pagonic` command-line interface for inspect, verify, safe extract, and ZIP utilities.
|
|
51
|
+
* An optional PyQt6 GUI launched with `pagonic-gui`.
|
|
52
|
+
|
|
53
|
+
This repository contains the `v0.5.0` alpha release. The import package remains
|
|
54
|
+
`Pagonic` for compatibility; the distribution name is `pagonic`. No PyPI or
|
|
55
|
+
TestPyPI package is published, so install from a local checkout or use the
|
|
56
|
+
artifacts attached to the [GitHub release](https://github.com/SetraTheXX/pagonic/releases/tag/v0.5.0).
|
|
57
|
+
|
|
58
|
+
## Project Story
|
|
59
|
+
|
|
60
|
+
Pagonic started more than a year ago as one of my earliest software-learning projects. Its first direction was much broader and more experimental: a ZIP/archive engine with compression, extraction, GUI ideas, benchmarking, and performance experiments.
|
|
61
|
+
|
|
62
|
+
After many iterations, I revised the project direction and narrowed the public scope into something clearer:
|
|
63
|
+
|
|
64
|
+
> Pagonic is not trying to be another desktop archive manager.
|
|
65
|
+
> It is becoming a security-aware ZIP inspection and safe extraction toolkit.
|
|
66
|
+
|
|
67
|
+
This `v0.5.0` release builds on the first cleaned-up public direction from
|
|
68
|
+
`v0.4.0`. It keeps the useful ZIP core, CLI, tests, and safety work while making
|
|
69
|
+
the inspection policy, regression corpus, automation examples, package surface,
|
|
70
|
+
and compatibility boundaries explicit.
|
|
71
|
+
|
|
72
|
+
Pagonic is still evolving, but its purpose is now clearer: inspect first, extract safely.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
No PyPI or TestPyPI package is published for this release, so install from a
|
|
77
|
+
local checkout after cloning the repository.
|
|
78
|
+
|
|
79
|
+
For CLI-only use from a local checkout:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
python -m pip install .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For local development:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python -m pip install -e .[dev,gui]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For CLI-only development with test dependencies:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m pip install -e .[dev]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Experimental performance helpers are optional and are not required by inspection
|
|
98
|
+
or safe extraction:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
python -m pip install -e .[performance]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The GUI is optional. If PyQt6 is not installed, `pagonic-gui` exits with a clear install message.
|
|
105
|
+
|
|
106
|
+
## CLI Quick Start
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pagonic --help
|
|
110
|
+
pagonic inspect suspicious.zip
|
|
111
|
+
pagonic inspect suspicious.zip --json
|
|
112
|
+
pagonic inspect suspicious.zip --markdown
|
|
113
|
+
pagonic verify release.zip
|
|
114
|
+
pagonic verify release.zip --max-risk medium
|
|
115
|
+
pagonic safe-extract upload.zip output/
|
|
116
|
+
pagonic safe-extract upload.zip output/ --dry-run
|
|
117
|
+
pagonic list archive.zip --tree
|
|
118
|
+
pagonic compress path/to/file.txt -o archive.zip
|
|
119
|
+
pagonic config list
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use `inspect` before extraction for untrusted ZIP files. `safe-extract` applies
|
|
123
|
+
the inspection gate before writing files, supports `--dry-run`, and refuses ZIP
|
|
124
|
+
entries that use unsupported compression methods.
|
|
125
|
+
|
|
126
|
+
## Python API Quick Start
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from Pagonic.core.formats.zip_writer import ZipWriter
|
|
130
|
+
from Pagonic.core.formats.zip_reader import ZipReader
|
|
131
|
+
|
|
132
|
+
writer = ZipWriter("archive.zip", compression_level=6)
|
|
133
|
+
writer.add_file("file.txt")
|
|
134
|
+
writer.finalize()
|
|
135
|
+
|
|
136
|
+
reader = ZipReader("archive.zip")
|
|
137
|
+
report = reader.inspect()
|
|
138
|
+
|
|
139
|
+
if report.risk_level in {"ok", "low"}:
|
|
140
|
+
reader.extract_all("output")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Project Layout
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
Pagonic/ Python package
|
|
147
|
+
tests/ pytest suite
|
|
148
|
+
docs/ public documentation
|
|
149
|
+
examples/ small runnable examples
|
|
150
|
+
pyproject.toml package metadata and tool config
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
- [Architecture](docs/architecture.md)
|
|
156
|
+
- [User Guide](docs/user-guide.md)
|
|
157
|
+
- [Inspection Policy Contract](docs/inspection-policy.md)
|
|
158
|
+
- [Inspection JSON Schema Contract](docs/inspection-schema.md)
|
|
159
|
+
- [CI Integration](docs/ci-integration.md)
|
|
160
|
+
- [Package Surface Audit](docs/package-audit.md)
|
|
161
|
+
- [0.5 Migration Notes](docs/migration-0.5.md)
|
|
162
|
+
- [0.5 Release Audit](docs/release-audit-0.5.md)
|
|
163
|
+
- [SARIF Evaluation](docs/sarif-evaluation.md)
|
|
164
|
+
- [ZipHandler Compatibility Policy](docs/zip-handler-compatibility.md)
|
|
165
|
+
- [Developer Guide](docs/developer-guide.md)
|
|
166
|
+
- [0.4 Migration Notes](docs/migration-0.4.md)
|
|
167
|
+
- [Roadmap](docs/roadmap.md)
|
|
168
|
+
- [Changelog](CHANGELOG.md)
|
|
169
|
+
- [Contributing](CONTRIBUTING.md)
|
|
170
|
+
- [Security Policy](SECURITY.md)
|
|
171
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
172
|
+
|
|
173
|
+
## Contributing
|
|
174
|
+
|
|
175
|
+
Focused contributions are welcome, especially improvements to inspection
|
|
176
|
+
determinism, security regression coverage, safe extraction policy, CI
|
|
177
|
+
integration, and documentation. Read [CONTRIBUTING.md](CONTRIBUTING.md), check
|
|
178
|
+
[the 0.5 roadmap](docs/roadmap.md), and use the issue templates before opening
|
|
179
|
+
a pull request. Do not include private plans, local paths, secrets, generated
|
|
180
|
+
archives, or benchmark output in public changes.
|
|
181
|
+
|
|
182
|
+
## Risk Signals
|
|
183
|
+
|
|
184
|
+
Inspection reports are deterministic and do not use runtime AI. Current risk
|
|
185
|
+
flags include:
|
|
186
|
+
|
|
187
|
+
| Flag | Severity | Meaning |
|
|
188
|
+
| -------------------------------- | ---------- | -------------------------------------------------------------------------------------- |
|
|
189
|
+
| `path_traversal` | `high` | Entry contains `..` path segments. |
|
|
190
|
+
| `absolute_path` | `high` | Entry uses a POSIX absolute path. |
|
|
191
|
+
| `windows_drive_path` | `high` | Entry looks like a Windows drive path. |
|
|
192
|
+
| `hidden_file` | `low` | Entry basename starts with `.`. |
|
|
193
|
+
| `empty_filename` | `medium` | Entry cannot be mapped to a useful safe path. |
|
|
194
|
+
| `too_many_files` | `high` | Archive exceeds the configured file-count limit. |
|
|
195
|
+
| `large_uncompressed_size` | `high` | Archive exceeds the configured uncompressed-size limit. |
|
|
196
|
+
| `high_compression_ratio` | `high` | Entry expands much more than its compressed size. |
|
|
197
|
+
| `unsupported_compression_method` | `medium` | Entry uses a ZIP method Pagonic does not currently support; `safe-extract` refuses it. |
|
|
198
|
+
| `crc_or_structure_error` | `critical` | ZIP structure or CRC validation failed. |
|
|
199
|
+
| `suspicious_extension` | `medium` | Entry has an executable or script-like extension. |
|
|
200
|
+
| `duplicate_filename` | `high` | The same archive filename appears more than once. |
|
|
201
|
+
| `normalized_path_collision` | `high` | Different names resolve to the same sanitized path. |
|
|
202
|
+
| `case_insensitive_collision` | `high` | Names collide on case-insensitive filesystems. |
|
|
203
|
+
| `unicode_normalization_collision`| `high` | Different Unicode spellings normalize to one path. |
|
|
204
|
+
| `symlink_entry` | `high` | ZIP metadata marks the entry as a symbolic link. |
|
|
205
|
+
| `encrypted_entry` | `high` | Entry contents cannot be validated by the current workflow. |
|
|
206
|
+
| `nested_archive` | `low` | Entry appears to contain another archive; it is not recursively inspected. |
|
|
207
|
+
| `long_filename` | `medium` | Entry name exceeds the configured review length. |
|
|
208
|
+
| `long_archive_comment` | `low` | Archive comment exceeds the configured review length. |
|
|
209
|
+
|
|
210
|
+
`pagonic inspect --json` emits a stable alpha report with archive totals,
|
|
211
|
+
overall `risk_level`, top-level `risk_flags`, `recommended_action`, and per-entry
|
|
212
|
+
metadata. Entries preserve archive order and risk flags use a deterministic
|
|
213
|
+
catalog order. See the [Inspection JSON Schema Contract](docs/inspection-schema.md)
|
|
214
|
+
for canonical fields, compatibility aliases, ordering guarantees, and clean,
|
|
215
|
+
risky, and invalid report examples. `pagonic inspect --markdown` renders the
|
|
216
|
+
same inspection as a saved human-readable report.
|
|
217
|
+
|
|
218
|
+
## Command Policy
|
|
219
|
+
|
|
220
|
+
Use `inspect` before extraction when the archive is untrusted. For automation,
|
|
221
|
+
`verify` returns exit code `0` only when the report is within `--max-risk` and
|
|
222
|
+
has no validation errors. `safe-extract` applies the same inspection gate before
|
|
223
|
+
writing files and supports `--dry-run`.
|
|
224
|
+
|
|
225
|
+
See the [inspection policy contract](docs/inspection-policy.md) for the exact
|
|
226
|
+
clean/risky/invalid decision table, defaults, unsupported-method rule, and exit
|
|
227
|
+
codes.
|
|
228
|
+
|
|
229
|
+
The older `extract` command remains as a compatibility command for trusted
|
|
230
|
+
archives. It uses secure path handling, but it is not an inspection policy gate;
|
|
231
|
+
use `safe-extract` for untrusted input. `list` and `info` are read-only display
|
|
232
|
+
commands and do not replace an inspection report.
|
|
233
|
+
|
|
234
|
+
## Status
|
|
235
|
+
|
|
236
|
+
The current public release is `0.5.0`: an alpha-stage, test-backed release with
|
|
237
|
+
security-aware ZIP inspection, explicit policy gates, a synthetic security
|
|
238
|
+
regression corpus, gated safe extraction, core ZIP behavior, CLI support,
|
|
239
|
+
optional GUI packaging, MIT license, and CI-ready tests.
|
|
240
|
+
|
|
241
|
+
Pagonic is not intended for production-critical automation yet and is not
|
|
242
|
+
positioned as a general multi-format desktop archive manager. The next work is
|
|
243
|
+
evidence-driven maintenance: collect real usage signals, expand the security
|
|
244
|
+
corpus when new rules are added, and revisit deferred integrations only when a
|
|
245
|
+
concrete consumer justifies them.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Pagonic security-aware ZIP inspection and extraction toolkit."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.5.0"
|
|
4
|
+
__author__ = "Pagonic contributors"
|
|
5
|
+
|
|
6
|
+
from .core.formats.base import FormatHandler
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FormatHandler",
|
|
10
|
+
"ZipHandler",
|
|
11
|
+
"ZipReader",
|
|
12
|
+
"ZipWriter",
|
|
13
|
+
"inspect_archive",
|
|
14
|
+
"ArchiveEntryReport",
|
|
15
|
+
"ArchiveInspectionReport",
|
|
16
|
+
"ArchiveRisk",
|
|
17
|
+
"ArchiveInfo",
|
|
18
|
+
"CompressionStats",
|
|
19
|
+
"ExtractionFailure",
|
|
20
|
+
"ExtractionResult",
|
|
21
|
+
"FileInfo",
|
|
22
|
+
"RiskDefinition",
|
|
23
|
+
"RISK_CATALOG",
|
|
24
|
+
"get_risk_definition",
|
|
25
|
+
"__version__",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_LAZY_EXPORTS = {
|
|
30
|
+
"ZipHandler": ("Pagonic.core.formats.handlers.zip_handler", "ZipHandler"),
|
|
31
|
+
"ZipReader": ("Pagonic.core.formats.zip_reader", "ZipReader"),
|
|
32
|
+
"ZipWriter": ("Pagonic.core.formats.zip_writer", "ZipWriter"),
|
|
33
|
+
"inspect_archive": ("Pagonic.core.formats.inspection", "inspect_archive"),
|
|
34
|
+
"ArchiveEntryReport": ("Pagonic.core.formats.inspection", "ArchiveEntryReport"),
|
|
35
|
+
"ArchiveInspectionReport": (
|
|
36
|
+
"Pagonic.core.formats.inspection",
|
|
37
|
+
"ArchiveInspectionReport",
|
|
38
|
+
),
|
|
39
|
+
"ArchiveRisk": ("Pagonic.core.formats.inspection", "ArchiveRisk"),
|
|
40
|
+
"ArchiveInfo": ("Pagonic.core.formats.results", "ArchiveInfo"),
|
|
41
|
+
"CompressionStats": ("Pagonic.core.formats.results", "CompressionStats"),
|
|
42
|
+
"ExtractionFailure": ("Pagonic.core.formats.results", "ExtractionFailure"),
|
|
43
|
+
"ExtractionResult": ("Pagonic.core.formats.results", "ExtractionResult"),
|
|
44
|
+
"FileInfo": ("Pagonic.core.formats.results", "FileInfo"),
|
|
45
|
+
"RiskDefinition": ("Pagonic.core.formats.inspection", "RiskDefinition"),
|
|
46
|
+
"RISK_CATALOG": ("Pagonic.core.formats.inspection", "RISK_CATALOG"),
|
|
47
|
+
"get_risk_definition": ("Pagonic.core.formats.inspection", "get_risk_definition"),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def __getattr__(name):
|
|
52
|
+
"""Load public ZIP APIs only when a caller requests them."""
|
|
53
|
+
target = _LAZY_EXPORTS.get(name)
|
|
54
|
+
if target is None:
|
|
55
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
56
|
+
|
|
57
|
+
import importlib
|
|
58
|
+
|
|
59
|
+
module_name, attribute_name = target
|
|
60
|
+
value = getattr(importlib.import_module(module_name), attribute_name)
|
|
61
|
+
globals()[name] = value
|
|
62
|
+
return value
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Command-line interface for Pagonic."""
|
|
2
|
+
|
|
3
|
+
from Pagonic import __version__
|
|
4
|
+
|
|
5
|
+
__all__ = ["cli"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def __getattr__(name):
|
|
9
|
+
"""Load the Click application only when the compatibility export is used."""
|
|
10
|
+
if name != "cli":
|
|
11
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
12
|
+
|
|
13
|
+
from .main import cli
|
|
14
|
+
|
|
15
|
+
globals()[name] = cli
|
|
16
|
+
return cli
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagonic CLI - Benchmark Command
|
|
3
|
+
================================
|
|
4
|
+
Local benchmark runner for Pagonic ZIP workflows.
|
|
5
|
+
|
|
6
|
+
Benchmark output is intended for local validation and regression tracking, not
|
|
7
|
+
as a universal performance claim.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
import tempfile
|
|
13
|
+
import random
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich import box
|
|
21
|
+
|
|
22
|
+
from Pagonic.cli.utils import format_size, format_time
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_test_data(size_mb: int) -> bytes:
|
|
28
|
+
"""Generate compressible test data."""
|
|
29
|
+
# Mix of random and repetitive data (realistic compression scenario)
|
|
30
|
+
chunk_size = 1024 * 1024 # 1 MB chunks
|
|
31
|
+
data = bytearray()
|
|
32
|
+
|
|
33
|
+
for i in range(size_mb):
|
|
34
|
+
if i % 3 == 0:
|
|
35
|
+
# Highly compressible: repeated text
|
|
36
|
+
text = "The quick brown fox jumps over the lazy dog. " * 23000
|
|
37
|
+
data.extend(text.encode('utf-8')[:chunk_size])
|
|
38
|
+
elif i % 3 == 1:
|
|
39
|
+
# Medium compressible: code-like
|
|
40
|
+
lines = []
|
|
41
|
+
for j in range(5000):
|
|
42
|
+
lines.append(f"def function_{j}(x, y): return x + y * {j}")
|
|
43
|
+
code = "\n".join(lines)
|
|
44
|
+
data.extend(code.encode('utf-8')[:chunk_size])
|
|
45
|
+
else:
|
|
46
|
+
# Low compressible: random data
|
|
47
|
+
random_bytes = bytes([random.randint(0, 255) for _ in range(chunk_size)])
|
|
48
|
+
data.extend(random_bytes)
|
|
49
|
+
|
|
50
|
+
return bytes(data)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_compression_benchmark(size_mb: int, level: int, iterations: int) -> dict:
|
|
54
|
+
"""Run compression benchmark."""
|
|
55
|
+
from Pagonic.core.formats.zip_writer import ZipWriter
|
|
56
|
+
|
|
57
|
+
results = {
|
|
58
|
+
'compress_times': [],
|
|
59
|
+
'compress_speeds': [],
|
|
60
|
+
'ratios': [],
|
|
61
|
+
'output_sizes': []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Generate test data once
|
|
65
|
+
console.print(f"[dim]Generating {size_mb} MB test data...[/]")
|
|
66
|
+
test_data = generate_test_data(size_mb)
|
|
67
|
+
original_size = len(test_data)
|
|
68
|
+
|
|
69
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
70
|
+
# Create test file
|
|
71
|
+
test_file = Path(tmpdir) / "benchmark_data.bin"
|
|
72
|
+
test_file.write_bytes(test_data)
|
|
73
|
+
|
|
74
|
+
for i in range(iterations):
|
|
75
|
+
output_zip = Path(tmpdir) / f"benchmark_{i}.zip"
|
|
76
|
+
|
|
77
|
+
# Compress
|
|
78
|
+
start = time.perf_counter()
|
|
79
|
+
writer = ZipWriter(str(output_zip), compression_level=level)
|
|
80
|
+
writer.add_file(str(test_file))
|
|
81
|
+
writer.finalize()
|
|
82
|
+
end = time.perf_counter()
|
|
83
|
+
|
|
84
|
+
compress_time = end - start
|
|
85
|
+
output_size = output_zip.stat().st_size
|
|
86
|
+
compress_speed = original_size / compress_time / 1024 / 1024 # MB/s
|
|
87
|
+
ratio = (1 - output_size / original_size) * 100
|
|
88
|
+
|
|
89
|
+
results['compress_times'].append(compress_time)
|
|
90
|
+
results['compress_speeds'].append(compress_speed)
|
|
91
|
+
results['output_sizes'].append(output_size)
|
|
92
|
+
results['ratios'].append(ratio)
|
|
93
|
+
|
|
94
|
+
# Cleanup
|
|
95
|
+
output_zip.unlink()
|
|
96
|
+
|
|
97
|
+
# Calculate averages
|
|
98
|
+
results['avg_compress_time'] = sum(results['compress_times']) / len(results['compress_times'])
|
|
99
|
+
results['avg_compress_speed'] = sum(results['compress_speeds']) / len(results['compress_speeds'])
|
|
100
|
+
results['avg_ratio'] = sum(results['ratios']) / len(results['ratios'])
|
|
101
|
+
results['avg_output_size'] = sum(results['output_sizes']) / len(results['output_sizes'])
|
|
102
|
+
results['original_size'] = original_size
|
|
103
|
+
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_decompression_benchmark(size_mb: int, level: int, iterations: int) -> dict:
|
|
108
|
+
"""Run decompression benchmark."""
|
|
109
|
+
from Pagonic.core.formats.zip_writer import ZipWriter
|
|
110
|
+
from Pagonic.core.formats.zip_reader import ZipReader
|
|
111
|
+
|
|
112
|
+
results = {
|
|
113
|
+
'decompress_times': [],
|
|
114
|
+
'decompress_speeds': []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Generate and compress test data
|
|
118
|
+
console.print(f"[dim]Preparing compressed archive...[/]")
|
|
119
|
+
test_data = generate_test_data(size_mb)
|
|
120
|
+
original_size = len(test_data)
|
|
121
|
+
|
|
122
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
123
|
+
# Create test file and archive
|
|
124
|
+
test_file = Path(tmpdir) / "benchmark_data.bin"
|
|
125
|
+
test_file.write_bytes(test_data)
|
|
126
|
+
|
|
127
|
+
archive_path = Path(tmpdir) / "benchmark.zip"
|
|
128
|
+
writer = ZipWriter(str(archive_path), compression_level=level)
|
|
129
|
+
writer.add_file(str(test_file))
|
|
130
|
+
writer.finalize()
|
|
131
|
+
|
|
132
|
+
# Remove original file
|
|
133
|
+
test_file.unlink()
|
|
134
|
+
|
|
135
|
+
for i in range(iterations):
|
|
136
|
+
output_dir = Path(tmpdir) / f"output_{i}"
|
|
137
|
+
output_dir.mkdir()
|
|
138
|
+
|
|
139
|
+
# Decompress
|
|
140
|
+
start = time.perf_counter()
|
|
141
|
+
reader = ZipReader(str(archive_path))
|
|
142
|
+
reader.extract_all(str(output_dir))
|
|
143
|
+
end = time.perf_counter()
|
|
144
|
+
|
|
145
|
+
decompress_time = end - start
|
|
146
|
+
decompress_speed = original_size / decompress_time / 1024 / 1024 # MB/s
|
|
147
|
+
|
|
148
|
+
results['decompress_times'].append(decompress_time)
|
|
149
|
+
results['decompress_speeds'].append(decompress_speed)
|
|
150
|
+
|
|
151
|
+
# Cleanup
|
|
152
|
+
for f in output_dir.iterdir():
|
|
153
|
+
f.unlink()
|
|
154
|
+
output_dir.rmdir()
|
|
155
|
+
|
|
156
|
+
# Calculate averages
|
|
157
|
+
results['avg_decompress_time'] = sum(results['decompress_times']) / len(results['decompress_times'])
|
|
158
|
+
results['avg_decompress_speed'] = sum(results['decompress_speeds']) / len(results['decompress_speeds'])
|
|
159
|
+
results['original_size'] = original_size
|
|
160
|
+
|
|
161
|
+
return results
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@click.command()
|
|
165
|
+
@click.option('--size', '-s', default=10, type=click.IntRange(1, 500),
|
|
166
|
+
help='Test data size in MB (1-500)')
|
|
167
|
+
@click.option('--level', '-l', default=6, type=click.IntRange(0, 9),
|
|
168
|
+
help='Compression level (0-9)')
|
|
169
|
+
@click.option('--iterations', '-i', default=3, type=click.IntRange(1, 10),
|
|
170
|
+
help='Number of benchmark iterations (1-10)')
|
|
171
|
+
@click.option('--compress-only', is_flag=True, help='Run only compression benchmark')
|
|
172
|
+
@click.option('--decompress-only', is_flag=True, help='Run only decompression benchmark')
|
|
173
|
+
def benchmark(size: int, level: int, iterations: int, compress_only: bool, decompress_only: bool):
|
|
174
|
+
"""
|
|
175
|
+
Run a local benchmark.
|
|
176
|
+
|
|
177
|
+
Measures compression and decompression on generated test data for local
|
|
178
|
+
validation and regression tracking. Results are workload-specific and are
|
|
179
|
+
not universal performance claims.
|
|
180
|
+
|
|
181
|
+
\b
|
|
182
|
+
Examples:
|
|
183
|
+
pagonic benchmark # Default: 10MB, level 6, 3 iterations
|
|
184
|
+
pagonic benchmark -s 50 -l 9 # 50MB with max compression
|
|
185
|
+
pagonic benchmark -s 100 -i 5 # 100MB with 5 iterations
|
|
186
|
+
pagonic benchmark --compress-only # Only test compression
|
|
187
|
+
"""
|
|
188
|
+
console.print()
|
|
189
|
+
console.print(Panel.fit(
|
|
190
|
+
f"[bold magenta] Pagonic Local Benchmark[/]",
|
|
191
|
+
subtitle=f"Size: {size}MB | Level: {level} | Iterations: {iterations}",
|
|
192
|
+
border_style="magenta",
|
|
193
|
+
box=box.ASCII,
|
|
194
|
+
))
|
|
195
|
+
console.print()
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Compression benchmark
|
|
199
|
+
if not decompress_only:
|
|
200
|
+
console.print("[bold cyan] Compression Benchmark[/]")
|
|
201
|
+
console.print("[dim]Running compression tests...[/]")
|
|
202
|
+
compress_results = run_compression_benchmark(size, level, iterations)
|
|
203
|
+
console.print("[green]Compression tests complete.[/]")
|
|
204
|
+
|
|
205
|
+
# Decompression benchmark
|
|
206
|
+
if not compress_only:
|
|
207
|
+
console.print()
|
|
208
|
+
console.print("[bold green] Decompression Benchmark[/]")
|
|
209
|
+
console.print("[dim]Running decompression tests...[/]")
|
|
210
|
+
decompress_results = run_decompression_benchmark(size, level, iterations)
|
|
211
|
+
console.print("[green]Decompression tests complete.[/]")
|
|
212
|
+
|
|
213
|
+
# Results table
|
|
214
|
+
console.print()
|
|
215
|
+
table = Table(title=" Benchmark Results", box=box.ASCII)
|
|
216
|
+
table.add_column("Metric", style="cyan", no_wrap=True)
|
|
217
|
+
table.add_column("Value", style="green")
|
|
218
|
+
table.add_column("Notes", style="dim")
|
|
219
|
+
|
|
220
|
+
table.add_row(" Test Size", format_size(size * 1024 * 1024), f"{size} MB")
|
|
221
|
+
table.add_row(" Compression Level", str(level), "0=store, 9=more compression")
|
|
222
|
+
table.add_row(" Iterations", str(iterations), "")
|
|
223
|
+
table.add_row("", "", "") # Separator
|
|
224
|
+
|
|
225
|
+
if not decompress_only:
|
|
226
|
+
table.add_row(
|
|
227
|
+
" Compress Speed",
|
|
228
|
+
f"[bold]{compress_results['avg_compress_speed']:.1f} MB/s[/]",
|
|
229
|
+
f"{max(compress_results['compress_speeds']) - min(compress_results['compress_speeds']):.1f}"
|
|
230
|
+
)
|
|
231
|
+
table.add_row(
|
|
232
|
+
" Compression Ratio",
|
|
233
|
+
f"{compress_results['avg_ratio']:.1f}%",
|
|
234
|
+
f"{format_size(compress_results['avg_output_size'])} output"
|
|
235
|
+
)
|
|
236
|
+
table.add_row(
|
|
237
|
+
" Compress Time",
|
|
238
|
+
format_time(compress_results['avg_compress_time']),
|
|
239
|
+
f"per {size}MB"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not compress_only:
|
|
243
|
+
if not decompress_only:
|
|
244
|
+
table.add_row("", "", "") # Separator
|
|
245
|
+
table.add_row(
|
|
246
|
+
" Decompress Speed",
|
|
247
|
+
f"[bold]{decompress_results['avg_decompress_speed']:.1f} MB/s[/]",
|
|
248
|
+
f"{max(decompress_results['decompress_speeds']) - min(decompress_results['decompress_speeds']):.1f}"
|
|
249
|
+
)
|
|
250
|
+
table.add_row(
|
|
251
|
+
" Decompress Time",
|
|
252
|
+
format_time(decompress_results['avg_decompress_time']),
|
|
253
|
+
f"per {size}MB"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
console.print(table)
|
|
257
|
+
|
|
258
|
+
# Summary panel
|
|
259
|
+
console.print()
|
|
260
|
+
console.print(Panel.fit(
|
|
261
|
+
f"[bold] Local Benchmark Notes[/]\n\n"
|
|
262
|
+
f"Compression measured [bold green]{compress_results['avg_compress_speed']:.0f} MB/s[/]; "
|
|
263
|
+
f"decompression measured [bold green]{decompress_results['avg_decompress_speed']:.0f} MB/s[/] "
|
|
264
|
+
f"for this local benchmark run.\n"
|
|
265
|
+
f"Use these numbers for local validation and regression tracking only.",
|
|
266
|
+
border_style="green",
|
|
267
|
+
box=box.ASCII,
|
|
268
|
+
) if not compress_only and not decompress_only else None)
|
|
269
|
+
|
|
270
|
+
console.print()
|
|
271
|
+
console.print("[bold green] Benchmark complete![/]")
|
|
272
|
+
console.print()
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
console.print(f"\n[red] Benchmark error:[/] {str(e)}")
|
|
276
|
+
raise SystemExit(1)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == '__main__':
|
|
280
|
+
benchmark()
|