l10n-gr 0.1.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.
@@ -0,0 +1,18 @@
1
+ node_modules/
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ dist/
6
+ .next/
7
+ .DS_Store
8
+ .idea/
9
+ .venv/
10
+ .output/
11
+ .nuxt/
12
+ __pycache__/
13
+ infra/site/.env
14
+ apps/site/dist
15
+
16
+ # Odoo Enterprise packages: proprietary, never in this repo (CLAUDE.md hard boundary 1 spirit)
17
+ odoo-deb/
18
+ *.deb
l10n_gr-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.5
2
+ Name: l10n-gr
3
+ Version: 0.1.0
4
+ Summary: l10n.gr command line: live development of your Odoo modules on a project's staging (l10n-gr dev)
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # l10n-gr
10
+
11
+ Command line for partners of [l10n.gr](https://l10n.gr). `l10n-gr dev` sends the modules of your local checkout to
12
+ your project's staging as you save, so you see the change in Odoo seconds later, without a push and a pipeline run.
13
+
14
+ ```
15
+ pip install l10n-gr
16
+ cd ~/code/acme-odoo # the repository your project's pipeline uses
17
+ l10n-gr dev --project acme
18
+ ```
19
+
20
+ Start a session first in console.l10n.gr: project → Staging → «Live development» → Start. The console shows a dev
21
+ token once; paste it when the CLI asks (or set `L10N_GR_TOKEN`). The token reaches only this project's staging.
22
+
23
+ Every save runs the pipeline's checks (static rules and the AI review). A blocking finding rejects the change and
24
+ staging keeps the last accepted state. Accepted changes install new modules and upgrade the changed ones. The staging
25
+ log follows in the same terminal.
26
+
27
+ `Ctrl+C`, «End» in the console, 8 hours without a change, or a deploy to staging end the session, and staging goes back
28
+ to the approved release. Code reaches production only through git and the pipeline.
29
+
30
+ In PyCharm, run it in the terminal or as a Run configuration (module `l10n_gr_cli.main`, parameters
31
+ `dev --project acme`, working directory your repository).
@@ -0,0 +1,23 @@
1
+ # l10n-gr
2
+
3
+ Command line for partners of [l10n.gr](https://l10n.gr). `l10n-gr dev` sends the modules of your local checkout to
4
+ your project's staging as you save, so you see the change in Odoo seconds later, without a push and a pipeline run.
5
+
6
+ ```
7
+ pip install l10n-gr
8
+ cd ~/code/acme-odoo # the repository your project's pipeline uses
9
+ l10n-gr dev --project acme
10
+ ```
11
+
12
+ Start a session first in console.l10n.gr: project → Staging → «Live development» → Start. The console shows a dev
13
+ token once; paste it when the CLI asks (or set `L10N_GR_TOKEN`). The token reaches only this project's staging.
14
+
15
+ Every save runs the pipeline's checks (static rules and the AI review). A blocking finding rejects the change and
16
+ staging keeps the last accepted state. Accepted changes install new modules and upgrade the changed ones. The staging
17
+ log follows in the same terminal.
18
+
19
+ `Ctrl+C`, «End» in the console, 8 hours without a change, or a deploy to staging end the session, and staging goes back
20
+ to the approved release. Code reaches production only through git and the pipeline.
21
+
22
+ In PyCharm, run it in the terminal or as a Run configuration (module `l10n_gr_cli.main`, parameters
23
+ `dev --project acme`, working directory your repository).
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "l10n-gr"
3
+ version = "0.1.0"
4
+ description = "l10n.gr command line: live development of your Odoo modules on a project's staging (l10n-gr dev)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ dependencies = [] # standard library only
9
+
10
+ [project.scripts]
11
+ l10n-gr = "l10n_gr_cli.main:main"
12
+
13
+ [dependency-groups]
14
+ dev = ["pytest>=8.4"]
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/l10n_gr_cli"]
@@ -0,0 +1 @@
1
+ """l10n.gr command line."""
@@ -0,0 +1,251 @@
1
+ """`l10n-gr dev`: live development on a project's staging (console spec §5.13, ADR-0010).
2
+
3
+ Watches the Odoo modules of the current repository (folders with a __manifest__.py at its root, as the pipeline builds
4
+ them), sends the whole tree on every save (paths and sha256, contents only when the console asks), prints each check's
5
+ result and follows the staging log. Standard library only."""
6
+ import argparse
7
+ import base64
8
+ import getpass
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import sys
13
+ import threading
14
+ import time
15
+ import urllib.error
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ CONSOLE = "https://console.l10n.gr"
20
+ SKIP_DIRS = {"__pycache__", "node_modules"}
21
+ # what the console accepts (livedev.SUFFIXES): other files stay local, so a stray build artefact never blocks a save
22
+ SUFFIXES = {".py", ".xml", ".csv", ".js", ".json", ".txt", ".cfg", ".yml", ".yaml", ".ini", ".md", ".rst", ".po", ".pot",
23
+ ".scss", ".css", ".less", ".html", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".woff", ".woff2",
24
+ ".ttf", ".otf", ".eot", ".pdf", ".xlsx", ".docx", ".odt", ".mp3", ".mp4", ".wav"}
25
+ MAX_FILE = 5_000_000
26
+ DONE = {"applied", "rejected", "failed", "superseded"}
27
+ TOKENS = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "l10n-gr" / "dev-tokens.json"
28
+
29
+
30
+ class ApiError(Exception):
31
+ def __init__(self, status: int, code: str):
32
+ super().__init__(code)
33
+ self.status, self.code = status, code
34
+
35
+
36
+ class Console:
37
+ def __init__(self, url: str, token: str):
38
+ self.url, self.token = url.rstrip("/"), token
39
+
40
+ def call(self, method: str, path: str, body: dict | None = None):
41
+ req = urllib.request.Request(f"{self.url}/api/dev{path}", method=method,
42
+ data=json.dumps(body).encode() if body is not None else None,
43
+ headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json",
44
+ "User-Agent": "l10n-gr-cli/0.1"})
45
+ try:
46
+ with urllib.request.urlopen(req, timeout=120) as r:
47
+ return json.loads(r.read() or b"null")
48
+ except urllib.error.HTTPError as e:
49
+ try:
50
+ code = json.loads(e.read())["error"]["code"]
51
+ except (ValueError, KeyError, TypeError):
52
+ code = f"http.{e.code}"
53
+ raise ApiError(e.code, code) from None
54
+
55
+
56
+ # --- the local tree -----------------------------------------------------------------------------------------------------
57
+ def modules(root: Path) -> list[Path]:
58
+ return sorted(d for d in root.iterdir() if d.is_dir() and not d.name.startswith(".") and (d / "__manifest__.py").is_file())
59
+
60
+
61
+ def snapshot(root: Path) -> dict[str, tuple[int, int]]:
62
+ """path -> (mtime_ns, size) of every file of every module."""
63
+ out = {}
64
+ for mod in modules(root):
65
+ for dirpath, dirnames, filenames in os.walk(mod):
66
+ dirnames[:] = [d for d in dirnames if not d.startswith(".") and d not in SKIP_DIRS]
67
+ for name in filenames:
68
+ if name.startswith(".") or Path(name).suffix.lower() not in SUFFIXES or name.endswith("~"):
69
+ continue
70
+ f = Path(dirpath) / name
71
+ try:
72
+ st = f.stat()
73
+ except OSError: # removed while scanning
74
+ continue
75
+ out[f.relative_to(root).as_posix()] = (st.st_mtime_ns, st.st_size)
76
+ return out
77
+
78
+
79
+ class Tree:
80
+ """The tree as the console sees it: sha256 per path, hashed again only when mtime or size changes."""
81
+
82
+ def __init__(self, root: Path):
83
+ self.root, self.cache = root, {}
84
+
85
+ def hashes(self, snap: dict) -> dict[str, str]:
86
+ out = {}
87
+ for path, stamp in snap.items():
88
+ if stamp[1] > MAX_FILE:
89
+ print(f" skipped {path}: larger than {MAX_FILE // 1_000_000} MB", file=sys.stderr)
90
+ continue
91
+ if self.cache.get(path, (None,))[0] != stamp:
92
+ try:
93
+ self.cache[path] = (stamp, hashlib.sha256((self.root / path).read_bytes()).hexdigest())
94
+ except OSError:
95
+ continue
96
+ out[path] = self.cache[path][1]
97
+ return out
98
+
99
+ def blobs(self, paths: list[str], tree: dict[str, str]) -> dict[str, str]:
100
+ return {tree[p]: base64.b64encode((self.root / p).read_bytes()).decode() for p in paths}
101
+
102
+
103
+ # --- output -------------------------------------------------------------------------------------------------------------
104
+ def say(msg: str) -> None:
105
+ print(f"{time.strftime('%H:%M:%S')} {msg}", flush=True)
106
+
107
+
108
+ def report(ch: dict) -> None:
109
+ status = ch["status"]
110
+ if status == "applied":
111
+ parts = [f"installed {', '.join(ch['installed'])}" if ch["installed"] else "",
112
+ f"upgraded {', '.join(ch['upgraded'])}" if ch["upgraded"] else ""]
113
+ say(f"✓ applied on staging{': ' + '; '.join(x for x in parts if x) if any(parts) else ''}")
114
+ elif status == "rejected":
115
+ say("✗ rejected: staging keeps the last accepted state")
116
+ elif status == "failed":
117
+ say("✗ failed on staging: the previous state is back")
118
+ for line in ch.get("output", "").splitlines()[-30:]:
119
+ print(f" {line}")
120
+ for f in ch.get("findings", []):
121
+ print(f" {f['severity'].upper():5} {f['file']}:{f['line']} [{f['rule']}] {f['message']}")
122
+
123
+
124
+ def follow_log(api: Console, stop: threading.Event) -> None:
125
+ after = ""
126
+ while not stop.wait(2):
127
+ try:
128
+ entries = api.call("GET", f"/log?after={after}")["entries"]
129
+ except (ApiError, OSError, ValueError):
130
+ continue
131
+ for e in entries:
132
+ after = e["ts"]
133
+ print(f" log {e['level']:7} {e['logger']}: {e['message']}", flush=True)
134
+ for line in (e.get("trace") or "").splitlines():
135
+ print(f" {line}")
136
+ if e.get("collapsed"):
137
+ print(f" [{e['collapsed']['lines']} lines of an l10n.gr traceback hidden] {e['collapsed']['error']}")
138
+
139
+
140
+ # --- dev ----------------------------------------------------------------------------------------------------------------
141
+ def _saved() -> dict:
142
+ try:
143
+ return json.loads(TOKENS.read_text())
144
+ except (OSError, ValueError):
145
+ return {}
146
+
147
+
148
+ def _save(project: str, token: str | None) -> None:
149
+ tokens = _saved()
150
+ if token:
151
+ tokens[project] = token
152
+ else:
153
+ tokens.pop(project, None)
154
+ TOKENS.parent.mkdir(parents=True, exist_ok=True)
155
+ TOKENS.write_text(json.dumps(tokens))
156
+ TOKENS.chmod(0o600)
157
+
158
+
159
+ ENDED = {"partner": "ended from the console", "cli": "ended", "idle": "ended after 8 hours without a change",
160
+ "deploy": "ended by a deploy to staging", "suite": "ended by an l10n.gr upgrade of staging",
161
+ "extraction": "ended: the change reads l10n.gr code"}
162
+
163
+
164
+ def _ended(code: str) -> str:
165
+ return ENDED.get(code.removeprefix("live.ended."), "ended") if code.startswith("live.ended.") else code
166
+
167
+
168
+ def sync(api: Console, tree: Tree, hashes: dict) -> dict | None:
169
+ r = api.call("POST", "/changes", {"tree": hashes})
170
+ if r["missing"]:
171
+ r = api.call("POST", "/changes", {"tree": hashes, "blobs": tree.blobs(r["missing"], hashes)})
172
+ return r.get("change")
173
+
174
+
175
+ def dev(args) -> int:
176
+ root = Path(args.path).resolve()
177
+ if not modules(root):
178
+ print(f"No Odoo modules in {root}: run l10n-gr dev at the root of your project's repository.", file=sys.stderr)
179
+ return 2
180
+ token = os.environ.get("L10N_GR_TOKEN") or _saved().get(args.project)
181
+ while True:
182
+ if not token:
183
+ print("Start a session in console.l10n.gr: project → Staging → Live development → Start.")
184
+ token = getpass.getpass("Dev token: ").strip()
185
+ api = Console(args.console, token)
186
+ try:
187
+ info = api.call("GET", "/session")
188
+ break
189
+ except ApiError as e:
190
+ _save(args.project, None)
191
+ if e.status == 401 or e.code.startswith("live.ended."):
192
+ print(f"This token is not valid ({_ended(e.code)}). Start a new session in the console.", file=sys.stderr)
193
+ token = None
194
+ if os.environ.get("L10N_GR_TOKEN"):
195
+ return 1
196
+ continue
197
+ print(f"The console refused: {e.code}", file=sys.stderr)
198
+ return 1
199
+ if info["project"] != args.project:
200
+ print(f"This token is for project {info['project']}, not {args.project}.", file=sys.stderr)
201
+ return 1
202
+ _save(args.project, token)
203
+ say(f"live session on {info['staging']} ({', '.join(p.name for p in modules(root))})")
204
+ stop = threading.Event()
205
+ threading.Thread(target=follow_log, args=(api, stop), daemon=True).start()
206
+ tree, last, pending = Tree(root), None, None
207
+ try:
208
+ while True:
209
+ snap = snapshot(root)
210
+ if snap != last:
211
+ time.sleep(0.4) # editors write in steps: send once the save settles
212
+ last = snapshot(root)
213
+ if change := sync(api, tree, tree.hashes(last)):
214
+ say(f"sent {len(change['files'])} changed file(s), checking…")
215
+ pending = change["id"]
216
+ if pending:
217
+ ch = api.call("GET", f"/changes/{pending}")
218
+ if ch["status"] in DONE:
219
+ if ch["status"] != "superseded":
220
+ report(ch)
221
+ pending = None
222
+ time.sleep(1)
223
+ except KeyboardInterrupt:
224
+ stop.set()
225
+ try:
226
+ api.call("POST", "/end")
227
+ say("session ended: staging goes back to the approved release")
228
+ except (ApiError, OSError):
229
+ pass
230
+ _save(args.project, None)
231
+ return 0
232
+ except ApiError as e:
233
+ stop.set()
234
+ _save(args.project, None)
235
+ say(f"session {_ended(e.code)}" if e.code.startswith("live.ended.") else f"the console refused: {e.code}")
236
+ return 1
237
+
238
+
239
+ def main(argv: list[str] | None = None) -> int:
240
+ ap = argparse.ArgumentParser(prog="l10n-gr", description="l10n.gr command line")
241
+ sub = ap.add_subparsers(dest="command", required=True)
242
+ d = sub.add_parser("dev", help="live development on the project's staging")
243
+ d.add_argument("--project", required=True, help="the project's name in the console, e.g. acme")
244
+ d.add_argument("--path", default=".", help="your repository (default: the current folder)")
245
+ d.add_argument("--console", default=os.environ.get("L10N_GR_CONSOLE", CONSOLE), help=argparse.SUPPRESS)
246
+ args = ap.parse_args(argv)
247
+ return dev(args)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ sys.exit(main())
@@ -0,0 +1,21 @@
1
+ """The tree the CLI sends: only module folders, no caches or editor files, hashes follow the contents."""
2
+ import hashlib
3
+
4
+ from l10n_gr_cli.main import Tree, modules, snapshot
5
+
6
+
7
+ def test_tree(tmp_path):
8
+ (tmp_path / "acme" / "models" / "__pycache__").mkdir(parents=True)
9
+ (tmp_path / "acme" / "__manifest__.py").write_text("{'name': 'Acme'}")
10
+ (tmp_path / "acme" / "models" / "move.py").write_text("x = 1\n")
11
+ (tmp_path / "acme" / "models" / "__pycache__" / "move.cpython-312.pyc").write_bytes(b"\0")
12
+ (tmp_path / "acme" / ".move.py.swp").write_text("")
13
+ (tmp_path / "docs").mkdir()
14
+ (tmp_path / "docs" / "notes.md").write_text("not a module")
15
+ assert [m.name for m in modules(tmp_path)] == ["acme"]
16
+ snap = snapshot(tmp_path)
17
+ assert sorted(snap) == ["acme/__manifest__.py", "acme/models/move.py"]
18
+ tree = Tree(tmp_path)
19
+ assert tree.hashes(snap)["acme/models/move.py"] == hashlib.sha256(b"x = 1\n").hexdigest()
20
+ (tmp_path / "acme" / "models" / "move.py").write_text("x = 22\n")
21
+ assert tree.hashes(snapshot(tmp_path))["acme/models/move.py"] == hashlib.sha256(b"x = 22\n").hexdigest()
l10n_gr-0.1.0/uv.lock ADDED
@@ -0,0 +1,174 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "colorama"
7
+ version = "0.4.6"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "exceptiongroup"
16
+ version = "1.3.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "iniconfig"
28
+ version = "2.3.1"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "l10n-gr"
37
+ version = "0.1.0"
38
+ source = { editable = "." }
39
+
40
+ [package.dev-dependencies]
41
+ dev = [
42
+ { name = "pytest" },
43
+ ]
44
+
45
+ [package.metadata]
46
+
47
+ [package.metadata.requires-dev]
48
+ dev = [{ name = "pytest", specifier = ">=8.4" }]
49
+
50
+ [[package]]
51
+ name = "packaging"
52
+ version = "26.3"
53
+ source = { registry = "https://pypi.org/simple" }
54
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
55
+ wheels = [
56
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
57
+ ]
58
+
59
+ [[package]]
60
+ name = "pluggy"
61
+ version = "1.7.0"
62
+ source = { registry = "https://pypi.org/simple" }
63
+ sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
64
+ wheels = [
65
+ { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
66
+ ]
67
+
68
+ [[package]]
69
+ name = "pygments"
70
+ version = "2.21.0"
71
+ source = { registry = "https://pypi.org/simple" }
72
+ sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "pytest"
79
+ version = "9.2.0"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ dependencies = [
82
+ { name = "colorama", marker = "sys_platform == 'win32'" },
83
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
84
+ { name = "iniconfig" },
85
+ { name = "packaging" },
86
+ { name = "pluggy" },
87
+ { name = "pygments" },
88
+ { name = "tomli", marker = "python_full_version < '3.11'" },
89
+ ]
90
+ sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "tomli"
97
+ version = "2.5.0"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" }
100
+ wheels = [
101
+ { url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" },
102
+ { url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" },
103
+ { url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" },
104
+ { url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" },
105
+ { url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" },
106
+ { url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" },
107
+ { url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" },
108
+ { url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" },
109
+ { url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" },
110
+ { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" },
111
+ { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" },
112
+ { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" },
113
+ { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" },
114
+ { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" },
115
+ { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" },
116
+ { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" },
117
+ { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" },
118
+ { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" },
119
+ { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" },
120
+ { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" },
121
+ { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" },
122
+ { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" },
123
+ { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" },
124
+ { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" },
125
+ { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" },
126
+ { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" },
127
+ { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" },
128
+ { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" },
129
+ { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" },
130
+ { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" },
131
+ { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" },
132
+ { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" },
133
+ { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" },
134
+ { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" },
135
+ { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" },
136
+ { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" },
137
+ { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" },
138
+ { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" },
139
+ { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" },
140
+ { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" },
141
+ { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" },
142
+ { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" },
143
+ { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" },
144
+ { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" },
145
+ { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" },
146
+ { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" },
147
+ { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" },
148
+ { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" },
149
+ { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" },
150
+ { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" },
151
+ { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" },
152
+ { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" },
153
+ { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" },
154
+ { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" },
155
+ { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" },
156
+ { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" },
157
+ { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" },
158
+ { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" },
159
+ { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" },
160
+ { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" },
161
+ { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" },
162
+ { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" },
163
+ { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" },
164
+ { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" },
165
+ ]
166
+
167
+ [[package]]
168
+ name = "typing-extensions"
169
+ version = "4.16.0"
170
+ source = { registry = "https://pypi.org/simple" }
171
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
172
+ wheels = [
173
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
174
+ ]