whiff-python 0.1.0__cp311-abi3-win_amd64.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.
whiff/scratch.py ADDED
@@ -0,0 +1,294 @@
1
+ """Opt-in local staging for path extraction.
2
+
3
+ ``stage="local"`` copies a regular file into a private directory before the
4
+ native call opens it, then rewrites the report so it still describes the
5
+ original path. Times are second precision. Fields this module does not read
6
+ (birth time, allocated size, sparse flag, owner names, ACLs, extended
7
+ attributes) are omitted and named in ``diagnostics.warnings``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import secrets
14
+ import shutil
15
+ import stat
16
+ import tempfile
17
+ from dataclasses import dataclass
18
+ from datetime import UTC, datetime
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from whiff.errors import (
23
+ AccessError,
24
+ InvalidOptionError,
25
+ NativeError,
26
+ NotFoundError,
27
+ UnsupportedInputError,
28
+ WhiffError,
29
+ )
30
+
31
+ _DEFAULT_MAX_BYTES = 1_073_741_824
32
+ _DIR_MODE = 0o700
33
+ _FILE_MODE = 0o600
34
+
35
+ _OMITTED = (
36
+ 'stage="local": filesystem fields taken from the original at second precision; '
37
+ "created, allocated_bytes, sparse, owner names, acl, and xattrs omitted"
38
+ )
39
+ _UNREAD = 'stage="local": original file could not be re-read; filesystem fields describe the copy'
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Staged:
44
+ original: str
45
+ staged: str
46
+
47
+
48
+ def scratch_dir() -> str:
49
+ return os.environ.get("WHIFF_SCRATCH_DIR") or str(Path(tempfile.gettempdir()) / "whiff-scratch")
50
+
51
+
52
+ def scratch_max_bytes() -> int:
53
+ raw = os.environ.get("WHIFF_SCRATCH_MAX_BYTES")
54
+ if raw is None:
55
+ return _configured_max
56
+ try:
57
+ value = int(raw)
58
+ except ValueError:
59
+ return _configured_max
60
+ return value if value > 0 else _configured_max
61
+
62
+
63
+ _configured_max = _DEFAULT_MAX_BYTES
64
+
65
+
66
+ def configure(*, directory: str | None = None, max_bytes: int | None = None) -> None:
67
+ global _configured_max
68
+ if directory is not None:
69
+ os.environ["WHIFF_SCRATCH_DIR"] = directory
70
+ if max_bytes is not None:
71
+ if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes < 1:
72
+ raise InvalidOptionError("scratch_max_bytes must be a positive integer")
73
+ _configured_max = max_bytes
74
+ os.environ["WHIFF_SCRATCH_MAX_BYTES"] = str(max_bytes)
75
+
76
+
77
+ def stage(path: str, mode: str) -> Staged | None:
78
+ """Copy ``path`` when ``mode`` is ``local``. Return None when staging is off."""
79
+ if mode == "never":
80
+ return None
81
+ if mode != "local":
82
+ raise InvalidOptionError(f"invalid stage: {mode!r}")
83
+ return _stage_local(path)
84
+
85
+
86
+ def cleanup(tag: Staged | None) -> None:
87
+ if tag is None:
88
+ return
89
+ shutil.rmtree(os.path.dirname(tag.staged), ignore_errors=True)
90
+
91
+
92
+ def restore_report(report: dict[str, Any], tag: Staged | None) -> dict[str, Any]:
93
+ if tag is None:
94
+ return report
95
+ try:
96
+ info = os.stat(tag.original)
97
+ except OSError:
98
+ return _warn(_put(report, ("source", "path"), tag.original), _UNREAD)
99
+ updated = _put(report, ("source", "path"), tag.original)
100
+ updated = _restore_temporal(updated, info)
101
+ updated = _restore_system(updated, info)
102
+ updated = _restore_security(updated, info)
103
+ return _warn(updated, _OMITTED)
104
+
105
+
106
+ def restore_error(err: WhiffError, tag: Staged | None) -> WhiffError:
107
+ if tag is None:
108
+ return err
109
+ message = err.message.replace(tag.staged, tag.original)
110
+ err.message = message
111
+ err.path = tag.original
112
+ err.args = (err._format(),)
113
+ return err
114
+
115
+
116
+ def _stage_local(path: str) -> Staged:
117
+ try:
118
+ info = os.stat(path)
119
+ except FileNotFoundError as err:
120
+ raise NotFoundError(f"stat: no such file: {err.strerror}", path=path) from err
121
+ except PermissionError as err:
122
+ raise AccessError(f"stat: permission denied: {err.strerror}", path=path) from err
123
+ except OSError as err:
124
+ raise NativeError(f"stat: {err.strerror or err}", path=path) from err
125
+ if not stat.S_ISREG(info.st_mode):
126
+ kind = "directory" if stat.S_ISDIR(info.st_mode) else "non-regular file"
127
+ raise UnsupportedInputError(
128
+ f"scratch staging requires a regular file, got {kind}", path=path
129
+ )
130
+ limit = scratch_max_bytes()
131
+ if info.st_size > limit:
132
+ raise InvalidOptionError(
133
+ f"file size {info.st_size} exceeds scratch_max_bytes {limit}",
134
+ path=path,
135
+ details={"size": info.st_size, "scratch_max_bytes": limit},
136
+ )
137
+ root = _ensure_root(scratch_dir())
138
+ job = _job_dir(root)
139
+ try:
140
+ _verify_root(root, job)
141
+ staged = os.path.join(job, os.path.basename(path) or "input")
142
+ _copy(path, staged, info)
143
+ except Exception:
144
+ shutil.rmtree(job, ignore_errors=True)
145
+ raise
146
+ return Staged(original=path, staged=staged)
147
+
148
+
149
+ def _ensure_root(root: str) -> str:
150
+ if os.path.islink(root):
151
+ raise NativeError("scratch_dir is a symlink, not a directory", path=root)
152
+ if os.path.exists(root):
153
+ if not os.path.isdir(root):
154
+ raise NativeError("scratch_dir is not a directory", path=root)
155
+ return root
156
+ os.makedirs(root, mode=_DIR_MODE, exist_ok=False)
157
+ os.chmod(root, _DIR_MODE)
158
+ return root
159
+
160
+
161
+ def _job_dir(root: str) -> str:
162
+ last_error: Exception | None = None
163
+ for _ in range(3):
164
+ path = os.path.join(root, "whiff-" + secrets.token_urlsafe(12))
165
+ try:
166
+ os.mkdir(path, _DIR_MODE)
167
+ except FileExistsError:
168
+ continue
169
+ except OSError as err:
170
+ last_error = err
171
+ break
172
+ os.chmod(path, _DIR_MODE)
173
+ return path
174
+ raise NativeError(
175
+ f"could not create a unique scratch job directory: {last_error}",
176
+ path=root,
177
+ )
178
+
179
+
180
+ def _verify_root(root: str, job: str) -> None:
181
+ if os.name != "posix":
182
+ return
183
+ root_stat = os.lstat(root)
184
+ job_stat = os.lstat(job)
185
+ if root_stat.st_uid != job_stat.st_uid:
186
+ raise AccessError("scratch_dir is owned by another user", path=root)
187
+ if (root_stat.st_mode & 0o022) != 0 and (root_stat.st_mode & 0o1000) == 0:
188
+ raise AccessError("scratch_dir is writable by group or other users", path=root)
189
+
190
+
191
+ def _copy(source: str, dest: str, info: os.stat_result) -> None:
192
+ shutil.copyfile(source, dest)
193
+ os.chmod(dest, _FILE_MODE)
194
+ os.utime(dest, (info.st_atime, info.st_mtime))
195
+
196
+
197
+ def _restore_temporal(report: dict[str, Any], info: os.stat_result) -> dict[str, Any]:
198
+ def update(section: dict[str, Any]) -> dict[str, Any]:
199
+ section = dict(section)
200
+ section["modified"] = _rfc3339(info.st_mtime)
201
+ section["accessed"] = _rfc3339(info.st_atime)
202
+ section.pop("created", None)
203
+ if os.name == "posix":
204
+ section["changed"] = _rfc3339(info.st_ctime)
205
+ else:
206
+ section.pop("changed", None)
207
+ return section
208
+
209
+ return _update(report, ("temporal", "filesystem"), update)
210
+
211
+
212
+ def _restore_system(report: dict[str, Any], info: os.stat_result) -> dict[str, Any]:
213
+ def update(section: dict[str, Any]) -> dict[str, Any]:
214
+ section = dict(section)
215
+ section["device"] = info.st_dev
216
+ section["inode"] = info.st_ino
217
+ section["links"] = info.st_nlink
218
+ section.pop("allocated_bytes", None)
219
+ section.pop("sparse", None)
220
+ return section
221
+
222
+ return _update(report, ("system",), update)
223
+
224
+
225
+ def _restore_security(report: dict[str, Any], info: os.stat_result) -> dict[str, Any]:
226
+ def update(section: dict[str, Any]) -> dict[str, Any]:
227
+ section = dict(section)
228
+ mode = info.st_mode & 0o7777
229
+ _put_if(section, "mode", "0" + format(mode, "o"))
230
+ _put_if(section, "permissions", _permissions(mode))
231
+ _put_if(section, "setuid", (mode & 0o4000) != 0)
232
+ _put_if(section, "setgid", (mode & 0o2000) != 0)
233
+ _put_if(section, "sticky", (mode & 0o1000) != 0)
234
+ if "owner" in section:
235
+ section["owner"] = {"uid": info.st_uid, "gid": info.st_gid}
236
+ section.pop("acl", None)
237
+ section.pop("xattrs", None)
238
+ return section
239
+
240
+ return _update(report, ("security",), update)
241
+
242
+
243
+ def _put_if(section: dict[str, Any], key: str, value: object) -> None:
244
+ if key in section:
245
+ section[key] = value
246
+
247
+
248
+ def _permissions(mode: int) -> str:
249
+ bits = (0o400, 0o200, 0o100, 0o040, 0o020, 0o010, 0o004, 0o002, 0o001)
250
+ chars = "rwxrwxrwx"
251
+ return "".join(char if mode & bit else "-" for bit, char in zip(bits, chars, strict=True))
252
+
253
+
254
+ def _rfc3339(timestamp: float) -> str:
255
+ return datetime.fromtimestamp(int(timestamp), UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
256
+
257
+
258
+ def _update(report: dict[str, Any], path: tuple[str, ...], fn) -> dict[str, Any]:
259
+ current = _get(report, path)
260
+ if not isinstance(current, dict):
261
+ return report
262
+ return _put(report, path, fn(current))
263
+
264
+
265
+ def _warn(report: dict[str, Any], warning: str) -> dict[str, Any]:
266
+ diagnostics = report.get("diagnostics")
267
+ diagnostics = dict(diagnostics) if isinstance(diagnostics, dict) else {}
268
+ warnings = list(diagnostics.get("warnings") or [])
269
+ warnings.append(warning)
270
+ diagnostics["warnings"] = warnings
271
+ updated = dict(report)
272
+ updated["diagnostics"] = diagnostics
273
+ return updated
274
+
275
+
276
+ def _get(report: dict[str, Any], path: tuple[str, ...]) -> object:
277
+ current: object = report
278
+ for key in path:
279
+ if not isinstance(current, dict) or key not in current:
280
+ return None
281
+ current = current[key]
282
+ return current
283
+
284
+
285
+ def _put(report: dict[str, Any], path: tuple[str, ...], value: object) -> dict[str, Any]:
286
+ if len(path) == 1:
287
+ updated = dict(report)
288
+ updated[path[0]] = value
289
+ return updated
290
+ child = report.get(path[0])
291
+ child = dict(child) if isinstance(child, dict) else {}
292
+ updated = dict(report)
293
+ updated[path[0]] = _put(child, path[1:], value)
294
+ return updated
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.4
2
+ Name: whiff-python
3
+ Version: 0.1.0
4
+ Classifier: License :: OSI Approved :: MIT License
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: Programming Language :: Python :: 3.11
7
+ Classifier: Programming Language :: Python :: 3.12
8
+ Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: Programming Language :: Rust
10
+ Classifier: Typing :: Typed
11
+ Requires-Dist: pytest>=8 ; extra == 'dev'
12
+ Requires-Dist: ruff>=0.8 ; extra == 'dev'
13
+ Provides-Extra: dev
14
+ License-File: LICENSE
15
+ Summary: Identify a file or bytes and return a whiff/1 report.
16
+ Keywords: metadata,file-format,detection,whiff
17
+ Author: Sin Anima
18
+ License-Expression: MIT
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
+ Project-URL: Upstream, https://github.com/baoulo/whiff
22
+
23
+ # whiff
24
+
25
+ Python bindings for [whiff](https://github.com/baoulo/whiff). Point it at a file or at bytes and it returns one [`whiff/1`](https://github.com/baoulo/whiff/blob/main/docs/schema/whiff-1.schema.json) report: format, hashes, security and rights signals, content, and format-specific metadata.
26
+
27
+ Identification and extraction run in the Rust crate (0.8.0, the `common` feature set). Python extractors and analyzers can add fields afterwards. They do not run inside native extraction, and they do not run for embedded children.
28
+
29
+ ```python
30
+ import whiff
31
+
32
+ report = whiff.extract("invoice.pdf")
33
+ report["identification"]["format"] # "pdf"
34
+
35
+ report = whiff.extract(data, name="invoice.pdf", hashes=["sha256"])
36
+ view = whiff.llm("contract.pdf", tokens=800)
37
+ ```
38
+
39
+ Empty optional fields are omitted. The vendored schema is `whiff.schema()`.
40
+
41
+ ## Install
42
+
43
+ Import the package as `whiff`. The PyPI project `whiff` already belongs to an unrelated library, so this distribution is named `whiff-python`. A GitHub release tagged with the `version` in `pyproject.toml` (an optional `v` prefix is fine) publishes the wheels. From a checkout, with Rust 1.89 or later:
44
+
45
+ ```sh
46
+ maturin develop
47
+ ```
48
+
49
+ That builds the `common` formats. A source build of every collection is:
50
+
51
+ ```sh
52
+ maturin develop --features full
53
+ ```
54
+
55
+ `geo`, `science`, `data`, `cad`, `forensics`, and `games` are the individual cargo features. Wheels are built with `common` only.
56
+
57
+ ## Extract
58
+
59
+ `extract` accepts a path, `bytes`, or a binary file object. A file object is read up to `max_read_bytes` and then treated as bytes. `name` (or `filename`, not both) drives extension detection for in-memory input.
60
+
61
+ Corrupt or partial files still return a report. Problems inside the file are listed in `diagnostics`. The call raises `WhiffError` when the path cannot be inspected or an option is invalid:
62
+
63
+ ```python
64
+ try:
65
+ report = whiff.extract(path)
66
+ except whiff.NotFoundError:
67
+ ...
68
+ except whiff.AccessError:
69
+ ...
70
+ except whiff.InvalidOptionError as err:
71
+ print(err.message)
72
+ ```
73
+
74
+ `NotFoundError` is also a `FileNotFoundError`. `AccessError` is also a `PermissionError`. IO messages include the full path.
75
+
76
+ `llm` returns upstream `Report::llm_view`. Python plugins do not run for it.
77
+
78
+ ```python
79
+ results = whiff.extract_many(
80
+ paths,
81
+ max_concurrency=4,
82
+ timeout=300_000,
83
+ profile="minimal",
84
+ hashes="none",
85
+ )
86
+ ```
87
+
88
+ Results keep input order. With `on_error="collect"` (the default) each item is `(path, report)` or `(path, error)`. With `on_error="halt"` the first error is raised and `err.details["partial"]` holds the pairs completed before it. `timeout` is per path, in milliseconds. A timeout does not stop native work that has already started.
89
+
90
+ An `Engine` holds native options and is safe to share across threads:
91
+
92
+ ```python
93
+ engine = whiff.Engine(profile="minimal", hashes="none")
94
+ engine.extract(path)
95
+ ```
96
+
97
+ Native calls take a process-wide slot (default 8). A caller that waits longer than `native_wait_ms` (default 60000) raises `OverloadedError`. Change the limits with `whiff.configure(native_max_concurrency=4, native_wait_ms=10_000)`.
98
+
99
+ `stage="local"` copies a regular file into a private scratch directory before taking a slot, for slow or network filesystems. The copy is removed afterwards. The report still describes the original path. Times are second precision, and fields Python does not read (birth time, allocated size, owner names, ACLs, extended attributes) are omitted, with a `diagnostics.warnings` entry. Files larger than `scratch_max_bytes` (default 1 GiB) are rejected.
100
+
101
+ ## Options
102
+
103
+ An option a function does not use is rejected.
104
+
105
+ | Option | Values | Default |
106
+ |---|---|---|
107
+ | `profile` | `minimal`, `standard`, `full`, `llm` | `standard` |
108
+ | `hashes` | `all`, `none`, or a list of `sha256` / `blake3` | `all` |
109
+ | `privacy` | `strict`, `off` | `strict` |
110
+ | `only` / `skip` | extractor ids or groups | none |
111
+ | `format` | format id to force | detect |
112
+ | `provenance` | record which extractor set each field | `False` |
113
+ | `limits` | dict of native limits; see below | profile limits |
114
+ | `stage` | `never`, `local` | `never` |
115
+ | `name` / `filename` / `mime` | strings (`bytes` and file objects) | none |
116
+ | `tokens` | 1..100000 | `800` (`llm` only) |
117
+ | `mode` | `fallback`, `augment`, `override` | `fallback` |
118
+ | `extractors` / `analyzers` | plugin classes or instances | none |
119
+ | `strict` | plugin failure raises | `False` |
120
+ | `plugin_timeout_ms` | 1..600000 | `5000` |
121
+ | `override_fields` | dotted paths, for example `["content.title"]` | `[]` |
122
+ | `max_concurrency` / `on_error` / `timeout` | `extract_many` | `4` / `collect` / `300000` |
123
+
124
+ `limits` keys: `max_read_bytes` and `max_decompressed_bytes` (1..4 GiB), `hash_max_bytes` (0..4 GiB), `max_parse_bytes` (0..256 MiB), `max_text_bytes` (0..64 MiB), `max_depth` (0..8), `max_children` (0..500), `file_timeout_ms` and `extractor_timeout_ms` (1..600000).
125
+
126
+ `privacy="strict"` reduces personal data in native structured fields to counts and scrubs plugin patches. Upstream redacts secrets either way. `privacy="off"` leaves plugin patches unchanged.
127
+
128
+ ## Plugins
129
+
130
+ ```python
131
+ class Trade(whiff.Extractor):
132
+ id = "myapp.trade"
133
+ version = 1
134
+
135
+ def supports(self, source, report) -> bool:
136
+ return source.extension == ".trade"
137
+
138
+ def extract(self, source, report) -> dict:
139
+ return {"format": {"myapp.trade": {"trade_id": "T-1001"}}}
140
+
141
+
142
+ report = whiff.extract("positions/2026-10-28.trade", extractors=[Trade])
143
+ ```
144
+
145
+ An analyzer subclasses `whiff.Analyzer` and implements `analyze(report, source)`. Analyzers run after extractors. Their patches always merge in `augment` mode.
146
+
147
+ | Mode | When an extractor runs | Existing native values |
148
+ |---|---|---|
149
+ | `fallback` | native extraction did not cover a readable file | kept, except `content.*` |
150
+ | `augment` | `supports` returns true | kept |
151
+ | `override` | `supports` returns true | replaced for paths in `override_fields` |
152
+
153
+ `whiff`, `integrity`, and `embedded` cannot be patched. `diagnostics` accepts only extra warnings. `security.risks` are appended with `source` set to `python:<id>`. `metadata` and `format` keys are namespaces. One plugin cannot replace a value another plugin wrote unless that path is declared.
154
+
155
+ A failed plugin is recorded in `diagnostics.extractors` with `status` `error`, and the call still returns the report. With `strict=True` the call raises. Plugins run on a worker thread. They are not a security boundary.
156
+
157
+ `whiff.merge` applies the same rules to a patch dict.
158
+
159
+ ## Development
160
+
161
+ ```sh
162
+ cargo test
163
+ cargo clippy --all-targets -- -D warnings
164
+ maturin develop
165
+ ruff check src tests
166
+ pytest
167
+ ```
168
+
169
+ Golden reports are compared with `probes/rust_extract`, which links the same `whiff` 0.8.0 crate. Design notes: [docs/adr/0001-baseline.md](docs/adr/0001-baseline.md).
170
+
171
+ ## Limitations
172
+
173
+ - `extract` of `bytes` copies the buffer once into native memory.
174
+ - There is no streaming reader. A file object is read up to the read cap.
175
+ - Python callbacks do not run for embedded children, and `llm` does not run them.
176
+ - `stage="local"` copies the whole file first.
177
+ - A native `file_timeout` can leave a Rust worker running after the call returns.
178
+ - IO error messages include the full path.
179
+
180
+ ## License
181
+
182
+ MIT. Upstream whiff is also MIT.
183
+
@@ -0,0 +1,20 @@
1
+ whiff/__init__.py,sha256=FlXzk5La2EAmFEpa63QUgrBw_GMXHCs6uFTweB2TDck,1412
2
+ whiff/_native.pyd,sha256=EEyljJWAfoBQWHUOX7_UZwSPFWRTdFgptOSaztH1QBk,27445760
3
+ whiff/_native.pyi,sha256=8DBhwUI3HBf3GCoPwjNNN3v_4VYbJlxzZlIwShGhBU0,470
4
+ whiff/engine.py,sha256=CLiDTHMhwy68kkg8IbIBl_JVr-ObPrEk4gpu3-IDOM4,14158
5
+ whiff/errors.py,sha256=ZSK50TZufBAhx3go5fd3BOC7pxYFqpoGXNwd25XL7gY,4996
6
+ whiff/input.py,sha256=88mnZgT4_7YkmSJ8YLCZh2SymeIG2ARD-u4aVi1krKY,1613
7
+ whiff/limiter.py,sha256=prTlHegotUxtarDugMecSn8Bs-BtsA9NEt-B5ALk57Q,4249
8
+ whiff/merge.py,sha256=1mutnMGCYM7qscy19yHWEC4eVYEW9yA7Aq97kN4yWwA,14397
9
+ whiff/options.py,sha256=DX0wYFaAbYq-UCDLW-bn_mA0Kvsji5pDOr5SeafeP5c,8313
10
+ whiff/plugins.py,sha256=eSu5CVEp8j5T7SeDwarvkWpMbtSTzaJMmQCxcQVHI5w,10677
11
+ whiff/privacy.py,sha256=Gk7YerZzfrfRHODR11NolpYsJbFcZOtDNd8K_m1iGFg,6094
12
+ whiff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ whiff/report.py,sha256=GzZOyuxWHKtutGMyjEqEzSO0J8qHP2fCBdXBR4pILeA,2901
14
+ whiff/schemas/whiff-1.schema.json,sha256=5_dfDz8-7zn7nfrdnwQlYkwtETZ2PqANQEq49qWa2iI,46452
15
+ whiff/scratch.py,sha256=aWN5TF-lrYuNV7M-tocJ88aO0mfPTCItRQN8ZmHvPyM,10023
16
+ whiff_python-0.1.0.dist-info/METADATA,sha256=VfhfVX-1we6EciBJ_QpjjWCHmG31Wwu_KCxY1dgsciY,8209
17
+ whiff_python-0.1.0.dist-info/WHEEL,sha256=DRguzO_JHkJTcrXDweuHyM66sVwM00caBYZ3J_zMn0w,96
18
+ whiff_python-0.1.0.dist-info/licenses/LICENSE,sha256=tFTWErWxMCMtJ-_hJwDjOrCZXO30u6hYoYR_5dzUMF8,1087
19
+ whiff_python-0.1.0.dist-info/sboms/whiff_native.cyclonedx.json,sha256=1tXqSRMzGGsWb8qZvsp3zDKXVSQufJGXI71cSVbd9Ys,180490
20
+ whiff_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.16.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp311-abi3-win_amd64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sin Anima
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.