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.
- ruff_droids-0.1.0/.gitignore +13 -0
- ruff_droids-0.1.0/.python-version +1 -0
- ruff_droids-0.1.0/PKG-INFO +31 -0
- ruff_droids-0.1.0/README.md +23 -0
- ruff_droids-0.1.0/pyproject.toml +33 -0
- ruff_droids-0.1.0/src/ruff_droids/__init__.py +1 -0
- ruff_droids-0.1.0/src/ruff_droids/cli.py +34 -0
- ruff_droids-0.1.0/src/ruff_droids/orchestrator.py +203 -0
- ruff_droids-0.1.0/test_repo/bad_imports.py +6 -0
- ruff_droids-0.1.0/test_repo/bad_style.py +9 -0
- ruff_droids-0.1.0/test_repo/missing_docstrings.py +16 -0
- ruff_droids-0.1.0/test_repo/mixed_issues.py +20 -0
- ruff_droids-0.1.0/tests/__init__.py +0 -0
- ruff_droids-0.1.0/tests/test_ruff_fix.py +158 -0
- ruff_droids-0.1.0/uv.lock +185 -0
|
@@ -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,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
|
+
]
|