ruff-droids 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,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # ruff-droids temp artifacts
13
+ .factory_ruff_units.jsonl
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: ruff-droids
3
+ Version: 0.1.0
4
+ Summary: Orchestrator CLI tool for ruff fixes with Factory AI droids
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: pytest>=9.0.2
7
+ Description-Content-Type: text/markdown
8
+
9
+ # ruff-droids
10
+
11
+ CLI tool that runs `ruff --fix` on a codebase and dispatches remaining violations to Factory AI droids for resolution.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ uv tool install .
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ ruff-droids --path /your/project
23
+ ```
24
+
25
+ ### Options
26
+
27
+ | Flag | Default | Description |
28
+ |------|---------|-------------|
29
+ | `--path` | `.` | Target directory |
30
+ | `--factory-api-key` | `FACTORY_API_KEY` env | Factory API key |
31
+ | `--concurrency` | `4` | Parallel droid workers |
@@ -0,0 +1,23 @@
1
+ # ruff-droids
2
+
3
+ CLI tool that runs `ruff --fix` on a codebase and dispatches remaining violations to Factory AI droids for resolution.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ uv tool install .
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ ruff-droids --path /your/project
15
+ ```
16
+
17
+ ### Options
18
+
19
+ | Flag | Default | Description |
20
+ |------|---------|-------------|
21
+ | `--path` | `.` | Target directory |
22
+ | `--factory-api-key` | `FACTORY_API_KEY` env | Factory API key |
23
+ | `--concurrency` | `4` | Parallel droid workers |
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "ruff-droids"
3
+ version = "0.1.0"
4
+ description = "Orchestrator CLI tool for ruff fixes with Factory AI droids"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "pytest>=9.0.2",
9
+ ]
10
+
11
+ [project.scripts]
12
+ ruff-droids = "ruff_droids.cli:main"
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.ruff]
19
+ line-length = 130
20
+
21
+ [tool.ruff.lint]
22
+ select = ["ALL"]
23
+ ignore = [
24
+ "D203",
25
+ "D213",
26
+ "S101",
27
+ "T201"
28
+ ]
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "ruff>=0.15.0",
33
+ ]
@@ -0,0 +1 @@
1
+ """Initialize for ruff_droids."""
@@ -0,0 +1,34 @@
1
+ """CLI entry point for ruff-droids."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .orchestrator import run_lint_fix
9
+
10
+
11
+ def main() -> None:
12
+ """Parse arguments, set up Factory auth, and delegate to the orchestrator."""
13
+ parser = argparse.ArgumentParser(
14
+ prog="ruff-droids",
15
+ description="Run ruff auto-fixes and delegate remaining lint issues to Factory AI droids.",
16
+ )
17
+ parser.add_argument("--path", default=".", help="Target directory (default: .)")
18
+ parser.add_argument("--factory-api-key", help="Factory API key (fallback: FACTORY_API_KEY env var)")
19
+ parser.add_argument("--concurrency", type=int, default=4, help="Number of parallel droid-exec workers (default: 4)")
20
+ args = parser.parse_args()
21
+
22
+ target = str(Path(args.path).resolve())
23
+
24
+ # 1) Set Factory auth
25
+ api_key: str | None = args.factory_api_key or os.getenv("FACTORY_API_KEY")
26
+ if not api_key:
27
+ api_key = input("Enter your Factory API key: ").strip()
28
+ if not api_key:
29
+ print("Error: Factory API key is required.")
30
+ sys.exit(1)
31
+ os.environ["FACTORY_API_KEY"] = api_key
32
+
33
+ # 2) Delegate to orchestrator
34
+ sys.exit(run_lint_fix(target, concurrency=args.concurrency))
@@ -0,0 +1,203 @@
1
+ """Orchestrator: run ruff, build work units, dispatch to Factory droids."""
2
+
3
+ import ast
4
+ import json
5
+ import shutil
6
+ import subprocess
7
+ import time
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from pathlib import Path
10
+
11
+ MAX_RETRIES = 5
12
+ BACKOFF_BASE = 1.0 # seconds
13
+
14
+ # Resolve full executable paths once at module load (S607)
15
+ _UVX_PATH = shutil.which("uvx") or "uvx"
16
+
17
+
18
+ def run_ruff(target_dir: str) -> list[dict]:
19
+ """Run ruff --fix, then collect remaining violations as JSON."""
20
+ # First pass: auto-fix what ruff can handle on its own
21
+ subprocess.run( # noqa: S603
22
+ [_UVX_PATH, "ruff", "check", "--fix", target_dir],
23
+ capture_output=True,
24
+ check=False,
25
+ )
26
+
27
+ # Second pass: report whatever is left
28
+ res = subprocess.run( # noqa: S603
29
+ [_UVX_PATH, "ruff", "check", "--output-format", "json", target_dir],
30
+ capture_output=True,
31
+ text=True,
32
+ check=False,
33
+ )
34
+
35
+ if not res.stdout.strip():
36
+ return []
37
+ return json.loads(res.stdout)
38
+
39
+
40
+ def _build_scope_map(filepath: str) -> list[tuple[range, str]]:
41
+ """Parse a Python file's AST and return a list of (line_range, scope_name) tuples.
42
+
43
+ Scopes are functions, methods, and classes. Nested scopes use dotted names
44
+ (e.g. "MyClass.my_method"). The list is sorted innermost-first so that a
45
+ violation on a line inside a method matches the method, not the enclosing class.
46
+ """
47
+ try:
48
+ source = Path(filepath).read_text()
49
+ tree = ast.parse(source, filename=filepath)
50
+ except (OSError, SyntaxError):
51
+ return []
52
+
53
+ scopes: list[tuple[range, str]] = []
54
+
55
+ def _walk(node: ast.AST, prefix: str = "") -> None:
56
+ for child in ast.iter_child_nodes(node):
57
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
58
+ name = f"{prefix}.{child.name}" if prefix else child.name
59
+ end = child.end_lineno if child.end_lineno is not None else child.lineno
60
+ scopes.append((range(child.lineno, end + 1), name))
61
+ _walk(child, name)
62
+ else:
63
+ _walk(child, prefix)
64
+
65
+ _walk(tree)
66
+
67
+ # Sort by range size ascending so innermost scopes match first
68
+ scopes.sort(key=lambda s: len(s[0]))
69
+ return scopes
70
+
71
+
72
+ def _scope_for_line(scopes: list[tuple[range, str]], line: int) -> str:
73
+ """Return the narrowest scope name containing `line`, or '<module>' for top-level."""
74
+ for line_range, name in scopes:
75
+ if line in line_range:
76
+ return name
77
+ return "<module>"
78
+
79
+
80
+ def build_work_units(violations: list[dict]) -> list[dict]:
81
+ """Build per-violation work units, merging violations that share a scope to avoid conflicts.
82
+
83
+ Two violations in the same file and same function/method/class are given to
84
+ one droid. Violations in different scopes (even in the same file) become
85
+ separate work units so they can run in parallel without conflicts.
86
+ """
87
+ by_file: dict[str, list[dict]] = {}
88
+ for v in violations:
89
+ by_file.setdefault(v.get("filename", ""), []).append(v)
90
+
91
+ work_units: list[dict] = []
92
+
93
+ for filepath, file_violations in by_file.items():
94
+ scope_map = _build_scope_map(filepath)
95
+
96
+ by_scope: dict[str, list[dict]] = {}
97
+ for v in file_violations:
98
+ line = v.get("location", {}).get("row", 0)
99
+ scope = _scope_for_line(scope_map, line)
100
+ by_scope.setdefault(scope, []).append(v)
101
+
102
+ for scope, scope_violations in by_scope.items():
103
+ codes = sorted({v.get("code", "?") for v in scope_violations})
104
+ work_units.append({
105
+ "file": filepath,
106
+ "scope": scope,
107
+ "codes": codes,
108
+ "violations": scope_violations,
109
+ "description": f"Fix {len(scope_violations)} violation(s) [{', '.join(codes)}] in {filepath}:{scope}",
110
+ })
111
+
112
+ return work_units
113
+
114
+
115
+ def _build_droid_prompt(unit: dict) -> str:
116
+ """Build a natural-language prompt for `droid exec` from a work unit."""
117
+ lines = [f"Fix the following ruff lint violations in {unit['file']}:\n"]
118
+ for v in unit["violations"]:
119
+ loc = v.get("location", {})
120
+ lines.append(f" - {v['code']} (line {loc.get('row', '?')}): {v['message']}")
121
+ lines.append(
122
+ "\nEdit the file to resolve each violation. "
123
+ "Run `uvx ruff check --select " + ",".join(unit["codes"]) + " " + unit["file"] + "` "
124
+ "to verify the fixes.",
125
+ )
126
+ return "\n".join(lines)
127
+
128
+
129
+ def _exec_droid_unit(target_dir: str, unit: dict, _unit_index: int) -> tuple[int, dict]:
130
+ """Run a single droid exec for one work unit, with exponential backoff on failure."""
131
+ prompt = _build_droid_prompt(unit)
132
+
133
+ cmd = [
134
+ "droid", "exec",
135
+ "--auto", "medium",
136
+ "--cwd", target_dir,
137
+ "-o", "json",
138
+ prompt,
139
+ ]
140
+
141
+ for attempt in range(MAX_RETRIES):
142
+ result = subprocess.run(cmd, capture_output=True, text=True, check=False) # noqa: S603
143
+ if result.returncode == 0:
144
+ return 0, unit
145
+
146
+ delay = BACKOFF_BASE * (2 ** attempt)
147
+ print(
148
+ f" [retry] {unit['description']} failed (attempt {attempt + 1}/{MAX_RETRIES}), "
149
+ f"retrying in {delay:.1f}s...",
150
+ )
151
+ time.sleep(delay)
152
+
153
+ print(f" [failed] {unit['description']} — exhausted {MAX_RETRIES} retries")
154
+ return 1, unit
155
+
156
+
157
+ def run_droid_exec(target_dir: str, work_units: list[dict], *, concurrency: int = 4) -> int:
158
+ """Dispatch all work units to droids in parallel with exponential backoff."""
159
+ failed: list[dict] = []
160
+
161
+ with ThreadPoolExecutor(max_workers=concurrency) as pool:
162
+ futures = {
163
+ pool.submit(_exec_droid_unit, target_dir, unit, i): unit
164
+ for i, unit in enumerate(work_units)
165
+ }
166
+ for future in as_completed(futures):
167
+ returncode, unit = future.result()
168
+ if returncode == 0:
169
+ print(f" [done] {unit['description']}")
170
+ else:
171
+ failed.append(unit)
172
+
173
+ if failed:
174
+ print(f"\n[ruff-droids] {len(failed)} work unit(s) failed:")
175
+ for u in failed:
176
+ print(f" - {u['description']}")
177
+ return 1
178
+ return 0
179
+
180
+
181
+ def run_lint_fix(target_dir: str, *, concurrency: int = 4) -> int:
182
+ """Top-level flow: ruff auto-fix -> collect remaining violations -> confirm -> droid exec."""
183
+ print(f"[ruff-droids] Running ruff --fix on {target_dir} ...")
184
+ violations = run_ruff(target_dir)
185
+
186
+ if not violations:
187
+ print("[ruff-droids] No remaining violations after auto-fix. Done!")
188
+ return 0
189
+
190
+ work_units = build_work_units(violations)
191
+
192
+ print(f"\n[ruff-droids] Found {len(violations)} linter violation(s), "
193
+ f"will spin up {len(work_units)} droid(s) to fix them.\n")
194
+ for u in work_units:
195
+ print(f" - {u['description']}")
196
+
197
+ answer = input("\nWould you like to continue? [y/N] ").strip().lower()
198
+ if answer not in ("y", "yes"):
199
+ print("[ruff-droids] Aborted.")
200
+ return 1
201
+
202
+ print(f"\n[ruff-droids] Dispatching {len(work_units)} droid(s) (concurrency={concurrency}) ...")
203
+ return run_droid_exec(target_dir, work_units, concurrency=concurrency)
@@ -0,0 +1,6 @@
1
+ import os
2
+ import sys
3
+ import json
4
+
5
+ def hello():
6
+ print("hello")
@@ -0,0 +1,9 @@
1
+ x=1
2
+ y =2
3
+ z= 3
4
+
5
+ def add(a,b):
6
+ return a+b
7
+
8
+ def long_function_name(argument_one, argument_two, argument_three, argument_four, argument_five, argument_six, argument_seven_is_really_long):
9
+ return argument_one + argument_two + argument_three + argument_four + argument_five + argument_six + argument_seven_is_really_long
@@ -0,0 +1,16 @@
1
+ class UserManager:
2
+ def create_user(self, name, email):
3
+ return {"name": name, "email": email}
4
+
5
+ def delete_user(self, user_id):
6
+ pass
7
+
8
+ def get_user(self, user_id):
9
+ return None
10
+
11
+
12
+ def process_data(data):
13
+ result = []
14
+ for item in data:
15
+ result.append(item * 2)
16
+ return result
@@ -0,0 +1,20 @@
1
+ import os
2
+ import re
3
+ import collections
4
+
5
+ class DataProcessor:
6
+ def transform(self, data, flag):
7
+ x = 1
8
+ if flag == True:
9
+ return [i for i in data if i > 0]
10
+ elif flag == False:
11
+ return data
12
+ else:
13
+ return None
14
+
15
+ def validate(self,input):
16
+ if type(input) == str:
17
+ return True
18
+ if type(input) == int:
19
+ return True
20
+ return False
File without changes
@@ -0,0 +1,158 @@
1
+ """Tests that run ruff-droids against test_repo and verify violations are fixed."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ TEST_REPO = Path(__file__).resolve().parent.parent / "test_repo"
11
+
12
+
13
+ @pytest.fixture
14
+ def workspace(tmp_path: Path) -> Path:
15
+ """Copy test_repo into a temp directory so originals stay dirty."""
16
+ dest = tmp_path / "test_repo"
17
+ shutil.copytree(TEST_REPO, dest)
18
+ return dest
19
+
20
+
21
+ def _ruff_violations(target: Path) -> list[dict]:
22
+ """Return current ruff violations as parsed JSON."""
23
+ res = subprocess.run(
24
+ ["uvx", "ruff", "check", "--output-format", "json", str(target)],
25
+ capture_output=True,
26
+ text=True,
27
+ check=False,
28
+ )
29
+ if not res.stdout.strip():
30
+ return []
31
+ return json.loads(res.stdout)
32
+
33
+
34
+ def _violation_codes(violations: list[dict]) -> set[str]:
35
+ return {v["code"] for v in violations}
36
+
37
+
38
+ def _run_ruff_fix(workspace: Path) -> None:
39
+ subprocess.run(
40
+ ["uvx", "ruff", "check", "--fix", str(workspace)],
41
+ capture_output=True,
42
+ check=False,
43
+ )
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Pre-condition: violations exist before any fix
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ def test_precondition_violations_exist(workspace: Path) -> None:
52
+ """Sanity check: the test repo actually has violations before we do anything."""
53
+ violations = _ruff_violations(workspace)
54
+ assert len(violations) > 0
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # did_fix_F401: unused imports removed (safe fix)
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ def test_did_fix_F401_unused_imports(workspace: Path) -> None:
63
+ """After ruff --fix, unused imports (F401) should be gone."""
64
+ _run_ruff_fix(workspace)
65
+ codes = _violation_codes(_ruff_violations(workspace))
66
+ assert "F401" not in codes
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # did_fix_I001: import sorting (safe fix)
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ def test_did_fix_I001_import_sorting(workspace: Path) -> None:
75
+ """After ruff --fix, import blocks (I001) should be sorted."""
76
+ _run_ruff_fix(workspace)
77
+ codes = _violation_codes(_ruff_violations(workspace))
78
+ assert "I001" not in codes
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # did_fix_RET505: unnecessary elif after return (safe fix)
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ def test_did_fix_RET505_elif_after_return(workspace: Path) -> None:
87
+ """After ruff --fix, unnecessary elif after return (RET505) should be gone."""
88
+ _run_ruff_fix(workspace)
89
+ codes = _violation_codes(_ruff_violations(workspace))
90
+ assert "RET505" not in codes
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Unsafe fixes: these remain after --fix and need droids
95
+ # ---------------------------------------------------------------------------
96
+
97
+
98
+ def test_needs_droid_E712_bool_comparison(workspace: Path) -> None:
99
+ """E712 (== True/False) is an unsafe fix — needs a droid."""
100
+ _run_ruff_fix(workspace)
101
+ codes = _violation_codes(_ruff_violations(workspace))
102
+ assert "E712" in codes
103
+
104
+
105
+ def test_needs_droid_F841_unused_variable(workspace: Path) -> None:
106
+ """F841 (unused variable) is an unsafe fix — needs a droid."""
107
+ _run_ruff_fix(workspace)
108
+ codes = _violation_codes(_ruff_violations(workspace))
109
+ assert "F841" in codes
110
+
111
+
112
+ def test_needs_droid_T201_print(workspace: Path) -> None:
113
+ """T201 (print found) is an unsafe fix — needs a droid."""
114
+ _run_ruff_fix(workspace)
115
+ codes = _violation_codes(_ruff_violations(workspace))
116
+ assert "T201" in codes
117
+
118
+
119
+ def test_needs_droid_SIM103_simplify_return(workspace: Path) -> None:
120
+ """SIM103 (return condition directly) is an unsafe fix — needs a droid."""
121
+ _run_ruff_fix(workspace)
122
+ codes = _violation_codes(_ruff_violations(workspace))
123
+ assert "SIM103" in codes
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Unfixable violations remain (droids needed)
128
+ # ---------------------------------------------------------------------------
129
+
130
+
131
+ def test_unfixable_violations_remain(workspace: Path) -> None:
132
+ """After ruff --fix, violations that need droids should still be present."""
133
+ _run_ruff_fix(workspace)
134
+ codes = _violation_codes(_ruff_violations(workspace))
135
+
136
+ expected_remaining = {"D100", "D101", "D102", "D103", "ANN001", "E501", "E721"}
137
+ for code in expected_remaining:
138
+ assert code in codes, f"Expected {code} to still be present after auto-fix"
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Scope grouping: violations in the same method become one work unit
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ def test_scope_grouping_merges_same_method(workspace: Path) -> None:
147
+ """Violations in the same scope should be merged into a single work unit."""
148
+ from ruff_droids.orchestrator import build_work_units
149
+
150
+ _run_ruff_fix(workspace)
151
+ violations = _ruff_violations(workspace)
152
+ work_units = build_work_units(violations)
153
+
154
+ # mixed_issues.py:DataProcessor.validate has multiple violations (E721, D102, etc.)
155
+ # They should all land in one work unit, not separate ones
156
+ validate_units = [u for u in work_units if u["scope"] == "DataProcessor.validate"]
157
+ assert len(validate_units) == 1, f"Expected 1 work unit for validate, got {len(validate_units)}"
158
+ assert len(validate_units[0]["violations"]) > 1, "Expected multiple violations merged into one unit"
@@ -0,0 +1,185 @@
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.0"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
31
+ wheels = [
32
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "packaging"
37
+ version = "26.0"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
40
+ wheels = [
41
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "pluggy"
46
+ version = "1.6.0"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "pygments"
55
+ version = "2.19.2"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
58
+ wheels = [
59
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "pytest"
64
+ version = "9.0.2"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ dependencies = [
67
+ { name = "colorama", marker = "sys_platform == 'win32'" },
68
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
69
+ { name = "iniconfig" },
70
+ { name = "packaging" },
71
+ { name = "pluggy" },
72
+ { name = "pygments" },
73
+ { name = "tomli", marker = "python_full_version < '3.11'" },
74
+ ]
75
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
76
+ wheels = [
77
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
78
+ ]
79
+
80
+ [[package]]
81
+ name = "ruff"
82
+ version = "0.15.0"
83
+ source = { registry = "https://pypi.org/simple" }
84
+ sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" }
85
+ wheels = [
86
+ { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" },
87
+ { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" },
88
+ { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" },
89
+ { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" },
90
+ { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" },
91
+ { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" },
92
+ { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" },
93
+ { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" },
94
+ { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" },
95
+ { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" },
96
+ { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" },
97
+ { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" },
98
+ { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" },
99
+ { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" },
100
+ { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" },
101
+ { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" },
102
+ { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "ruff-droids"
107
+ version = "0.1.0"
108
+ source = { editable = "." }
109
+ dependencies = [
110
+ { name = "pytest" },
111
+ ]
112
+
113
+ [package.dev-dependencies]
114
+ dev = [
115
+ { name = "ruff" },
116
+ ]
117
+
118
+ [package.metadata]
119
+ requires-dist = [{ name = "pytest", specifier = ">=9.0.2" }]
120
+
121
+ [package.metadata.requires-dev]
122
+ dev = [{ name = "ruff", specifier = ">=0.15.0" }]
123
+
124
+ [[package]]
125
+ name = "tomli"
126
+ version = "2.4.0"
127
+ source = { registry = "https://pypi.org/simple" }
128
+ sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
129
+ wheels = [
130
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
131
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
132
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
133
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
134
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
135
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
136
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
137
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
138
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
139
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
140
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
141
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
142
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
143
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
144
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
145
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
146
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
147
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
148
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
149
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
150
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
151
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
152
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
153
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
154
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
155
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
156
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
157
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
158
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
159
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
160
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
161
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
162
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
163
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
164
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
165
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
166
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
167
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
168
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
169
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
170
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
171
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
172
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
173
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
174
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
175
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
176
+ ]
177
+
178
+ [[package]]
179
+ name = "typing-extensions"
180
+ version = "4.15.0"
181
+ source = { registry = "https://pypi.org/simple" }
182
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
183
+ wheels = [
184
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
185
+ ]