├── .github
└── workflows
│ ├── adopt-ruff.yml
│ └── pre-commit.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .vscode
└── launch.json
├── README.md
├── action.yml
├── adopt_ruff
├── __init__.py
├── main.py
├── models
│ ├── __init__.py
│ ├── ruff_config.py
│ ├── ruff_output.py
│ └── rule.py
└── utils.py
├── pyproject.toml
└── uv.lock
/.github/workflows/adopt-ruff.yml:
--------------------------------------------------------------------------------
1 | name: Adopt Ruff
2 | on:
3 | workflow_dispatch:
4 | pull_request:
5 | schedule:
6 | - cron: '0 0 * * *'
7 | push:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | adopt-ruff:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Check out repository code
16 | uses: actions/checkout@v4
17 |
18 | - name: Install ruff
19 | run: pip install ruff
20 | shell: bash
21 |
22 | - name: Set up python
23 | id: setup-python
24 | uses: actions/setup-python@v5
25 | with:
26 | python-version: "3.11"
27 |
28 | - name: Run the adopt-ruff action
29 | uses: ./
30 | with:
31 | ref: ${{ github.sha }}
32 |
--------------------------------------------------------------------------------
/.github/workflows/pre-commit.yml:
--------------------------------------------------------------------------------
1 | name: pre-commit
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 |
8 | jobs:
9 | pre-commit:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: tox-dev/action-pre-commit-uv@v1
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .ruff_cache
2 | __pycache__
3 | /rules.json
4 | /violations.json
5 | /result.md
6 | /artifacts
7 | .python-version
8 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/astral-sh/ruff-pre-commit
3 | rev: v0.8.4
4 | hooks:
5 | - id: ruff
6 | args: [--fix]
7 | - id: ruff-format
8 | - repo: https://github.com/pre-commit/mirrors-mypy
9 | rev: v1.14.0
10 | hooks:
11 | - id: mypy
12 | additional_dependencies:
13 | - types-tabulate
14 | - repo: https://github.com/pre-commit/pre-commit-hooks
15 | rev: v5.0.0
16 | hooks:
17 | - id: trailing-whitespace
18 | - id: end-of-file-fixer
19 | - id: check-toml
20 | - id: check-yaml
21 | - id: check-json
22 | - id: check-ast
23 | - id: check-merge-conflict
24 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Python: Current File",
6 | "type": "python",
7 | "request": "launch",
8 | "program": "${file}",
9 | "cwd": "${workspaceFolder}",
10 | "console": "integratedTerminal",
11 | "justMyCode": true
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Adopt-Ruff
2 | [](https://github.com/astral-sh/ruff)
3 |
4 | Adopt [Ruff⚡](https://ruff.rs) in your repo faster 😎
5 |
6 | A tool for finding Ruff rules that are not yet configured, and can be added to your repository easily:
7 |
8 | - Rules your code already follows 👏
9 | - Rules that can be automatically fixed by Ruff 🪄
10 | - Rules violated in the repository, sorted by ascending violation count 🔎
11 |
12 | The output is a markdown report, easy to check as a Github action summary and CSV files with relevant Rule information per category.
13 |
14 | _See example at the bottom of this page_
15 |
16 |
17 |
18 | ## Configurations
19 | To decide whether to consider a rule, `adopt-ruff` uses arguments. See below on how to configure them.
20 |
21 | - Ruff considers some new or experimental rules `in preview`. These should be used with caution. You can choose whether `adopt-ruff` considers them or not. Read more about Ruff's preview [here](https://docs.astral.sh/ruff/preview/).
22 | - Ruff supports many autofixable rules - some are always autofixable, and some can only be autofixed by Ruff in specific cases. It's up to you whether to consider "sometimes-autofixable" as autofixable.
23 |
24 | ## Usage
25 |
26 | ### As a Github action (recommended)
27 | Create a yaml file under `.github/workspaces` in your repo, with the following content:
28 |
29 | ```yaml
30 | name: Adopt Ruff
31 | on: workflow_dispatch
32 |
33 | jobs:
34 | adopt-ruff:
35 | runs-on: ubuntu-latest
36 | steps:
37 | - name: Check out repository code
38 | uses: actions/checkout@v4
39 |
40 | - name: Set up python
41 | id: setup-python
42 | uses: actions/setup-python@v5
43 | with:
44 | python-version: 3.x
45 |
46 | - name: Install ruff
47 | run: pip install ruff
48 |
49 | - name: Run the adopt-ruff action
50 | uses: ScDor/adopt-ruff@master
51 |
52 | ```
53 | Now, manually trigger the workflow from the `Actions` panel of your repository.\
54 | _Learn more about manually triggering a workflow [here](https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow)._
55 |
56 | When the action is done running, head to the [job summary](https://github.blog/wp-content/uploads/2022/05/newjobsummary.png) to watch the report.
57 |
58 | The output tables are also availalbe as CSV files, under `Artifacts`.
59 |
60 | Re-run this action to get insights about applicable rules when Ruff updates with more exciting rules ⚡
61 |
62 | #### Notes:
63 | - This flow always installs Ruff's latest version. If your project already has Ruff as a dependency (i.e. in `pyproject.toml` or `requirements.txt`), replace the `Install ruff` step with an appropriate command.
64 |
65 | - adopt-ruff calls Ruff, assuming it's installed in the environment. adopt-ruff will fail if ruff is not found.
66 |
67 |
68 | #### Arguments
69 | To pass arguments different than the default, use the `with` key. The values mentioned below are the default. Omit any argument to use the default.
70 |
71 | ```yaml
72 | - name: Run the adopt-ruff action
73 | uses: ScDor/adopt-ruff@master
74 | with:
75 | path: "."
76 | repo-name: "" # won't show in report if blank
77 | config-file-path: "" # will be searched in `path` if empty
78 | include-preview: True
79 | include-sometimes-fixable: False
80 | ```
81 |
82 | ### Locally
83 | Install by running `pip install git+https://github.com/ScDor/adopt-ruff`.\
84 | Run `adopt-ruff --help` for more information.\
85 |
86 |
87 | #### Arguments
88 | - `--path`: directory adopt-ruff will search for ruff violations.
89 | - `--ruff-conf-path`: Path to the `pyproject.toml` ,`ruff.toml` or `.ruff.toml`. When not provided, `adopt-ruff` will attempt to seach one of those under `path`.
90 | - `--sometimes-fixable`: whether to consider sometimes-fixable rules as fixable.
91 | - `--preview`/`--no-preview`: whether to include preview rules.
92 | - `--repo-name`: Will be shown in the output. When not provided, won't be shown.
93 |
94 |
95 | # Report Example
96 | ## adopt-ruff report for ScDor/my-dummy-repo (ruff 0.1.13)
97 |
98 | ## Respected Ruff rules
99 |
100 | 374 Ruff rules are already respected in the repo - they can be added right away 🚀
101 |
102 | Details
103 |
104 | | Code | Name | Fixable | Preview | Linter |
105 | |----------|----------------------------------------------|-----------|-----------|----------------------------|
106 | | A003 | builtin-attribute-shadowing | No | False | flake8-builtins |
107 | | AIR001 | airflow-variable-name-task-id-mismatch | No | False | Airflow |
108 | | ASYNC100 | blocking-http-call-in-async-function | No | False | flake8-async |
109 | | ASYNC101 | open-sleep-or-subprocess-in-async-function | No | False | Perflint |
110 | | PERF403 | manual-dict-comprehension | No | True | Perflint |
111 |
112 | (table truncated for example purposes)
113 |
114 |
115 |
116 | ## Autofixable Ruff rules
117 |
118 | 65 Ruff rules are violated in the repo, but can be auto-fixed 🪄
119 |
120 | Details
121 |
122 | | Code | Name | Fixable | Preview | Linter |
123 | |---------|---------------------------------------------------|-----------|-----------|-----------------------|
124 | | B010 | set-attr-with-constant | Always | False | flake8-bugbear |
125 | | B011 | assert-false | Always | False | flake8-bugbear |
126 | | C401 | unnecessary-generator-set | Always | False | flake8-comprehensions |
127 |
128 | (table truncated for example purposes)
129 |
130 |
131 |
132 | ## Applicable Rules
133 |
134 | 194 other Ruff rules are not yet configured in the repository
135 |
136 | Details
137 |
138 | | Code | Name | Fixable | Preview | Linter | Violations |
139 | |---------|--------------------------------------------------|-----------|-----------|----------------------------|--------------|
140 | | PLW0127 | self-assigning-variable | No | False | Pylint | 1 |
141 | | RUF009 | function-call-in-dataclass-default-argument | No | False | Ruff-specific rules | 1 |
142 | | S314 | suspicious-xml-element-tree-usage | No | False | flake8-bandit | 1 |
143 | | B005 | strip-with-multi-characters | No | False | flake8-bugbear | 1 |
144 | | PTH116 | os-stat | No | False | flake8-use-pathlib | 6 |
145 | | N803 | invalid-argument-name | No | False | pep8-naming | 6 |
146 | | UP032 | f-string | Sometimes | False | pyupgrade | 6 |
147 | | B019 | cached-instance-method | No | False | flake8-bugbear | 7 |
148 | | B017 | assert-raises-exception | No | False | flake8-bugbear | 8 |
149 | | TCH002 | typing-only-third-party-import | Sometimes | False | flake8-type-checking | 8 |
150 | | PLW1508 | invalid-envvar-default | No | False | Pylint | 9 |
151 | | S607 | start-process-with-partial-path | No | False | flake8-bandit | 9 |
152 | | DTZ007 | call-datetime-strptime-without-zone | No | False | flake8-datetimez | 10 |
153 | | D205 | blank-line-after-summary | Sometimes | False | pydocstyle | 2860 |
154 |
155 | (table truncated for example purposes)
156 |
157 |
158 |
159 | | Configuration | Value |
160 | |---------------------------------|---------|
161 | | Include sometimes-fixable rules | False |
162 | | Include preview rules | True |
163 |
--------------------------------------------------------------------------------
/action.yml:
--------------------------------------------------------------------------------
1 | name: "adopt-ruff"
2 | branding:
3 | icon: "arrow-up"
4 | color: "yellow"
5 |
6 | description: "Adopt ruff easily"
7 | inputs:
8 | ref:
9 | description: "adopt-ruff reference to install"
10 | default: master
11 | path:
12 | description: "directory adopt-ruff will search for ruff violations. Default is CWD."
13 | default: "."
14 | config-file-path:
15 | description: "Path to the pyproject.toml/ruff.toml file. If not provided, adopt-ruff will attempt to locate it under cwd"
16 | default: ""
17 | repo-name:
18 | description: "name of the repository being checked"
19 | default: ${{ github.repository }}
20 | include-sometimes-fixable:
21 | description: "whether to count sometimes-fixable rules as fixable"
22 | default: "False"
23 | include-preview:
24 | description: "whether to count preview rules"
25 | default: "True"
26 |
27 | runs:
28 | using: "composite"
29 | steps:
30 | - name: Install adopt-ruff
31 | run: pip install git+https://github.com/ScDor/adopt-ruff@${{inputs.ref}} -q
32 | shell: bash
33 |
34 | - name: Run adopt-ruff
35 | run: |
36 | adopt-ruff
37 | cat result.md > $GITHUB_STEP_SUMMARY
38 | env:
39 | ADOPT_RUFF_REPO_NAME: ${{ inputs.repo-name }}
40 | ADOPT_RUFF_SOMETIMES_FIXABLE: ${{ inputs.include-sometimes-fixable }}
41 | ADOPT_RUFF_PREVIEW: ${{ inputs.include-preview }}
42 | ADOPT_RUFF_CODE_PATH: ${{ inputs.path }}
43 | ADOPT_RUFF_CONFIG_FILE_PATH: ${{ inputs.config-file-path }}
44 | shell: bash
45 |
46 | - uses: actions/upload-artifact@v4
47 | with:
48 | path: artifacts
49 |
--------------------------------------------------------------------------------
/adopt_ruff/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ScDor/adopt-ruff/59474307bc4c44fda046cfbbc35c14076c7a7fd3/adopt_ruff/__init__.py
--------------------------------------------------------------------------------
/adopt_ruff/main.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | import json
3 | import subprocess
4 | import sys
5 | from collections.abc import Iterable
6 | from pathlib import Path
7 | from typing import Annotated, Optional
8 |
9 | import more_itertools
10 | import typer
11 | from mdutils.mdutils import MdUtils
12 | from packaging.version import Version
13 | from tabulate import tabulate
14 |
15 | from adopt_ruff.models.ruff_config import RuffConfig
16 | from adopt_ruff.models.ruff_output import Violation
17 | from adopt_ruff.models.rule import FixAvailability, Rule
18 | from adopt_ruff.utils import ARTIFACTS_PATH, logger, output_table, search_config_file
19 |
20 |
21 | def run_ruff(path: Path) -> tuple[set[Rule], tuple[Violation, ...], Version]:
22 | try:
23 | ruff_version = Version(
24 | subprocess.run(
25 | ["ruff", "--version"],
26 | check=True,
27 | text=True,
28 | capture_output=True,
29 | ).stdout.split()[1] # ruff's output is `ruff x.y.z`
30 | )
31 | logger.debug(f"parsed {ruff_version=!s}")
32 |
33 | except FileNotFoundError:
34 | logger.error("Make sure ruff is installed (pip install ruff)")
35 | sys.exit(1)
36 |
37 | # Now when ruff is found, assume the following commands will run properly
38 | rules = {
39 | Rule(**value)
40 | for value in json.loads(
41 | subprocess.run(
42 | ["ruff", "rule", "--all", "--output-format=json"],
43 | check=True,
44 | text=True,
45 | capture_output=True,
46 | ).stdout
47 | )
48 | }
49 | logger.debug(f"read {len(rules)} rules from JSON output")
50 |
51 | violations = tuple(
52 | Violation(**value)
53 | for value in json.loads(
54 | subprocess.run(
55 | [
56 | *["ruff" if ruff_version < Version("0.3.0") else "ruff", "check"],
57 | str(path),
58 | "--output-format=json",
59 | "--select=ALL",
60 | "--exit-zero",
61 | ],
62 | check=True,
63 | text=True,
64 | capture_output=True,
65 | ).stdout
66 | )
67 | )
68 | logger.debug(f"read {len(violations)} violations from JSON output")
69 | return rules, violations, ruff_version
70 |
71 |
72 | def run(
73 | rules: set[Rule],
74 | violations: tuple[Violation, ...],
75 | config: RuffConfig,
76 | ruff_version: Version,
77 | include_sometimes_fixable: bool,
78 | include_preview: bool,
79 | repo_name: str | None = None,
80 | ) -> str:
81 | md = MdUtils("output")
82 |
83 | repo_name_header = f"for {repo_name} " if repo_name else ""
84 | md.new_header(1, f"adopt-ruff report {repo_name_header}(ruff {ruff_version!s})")
85 |
86 | configured_rules = config.all_rules
87 |
88 | if respected := sorted(
89 | respected_rules(
90 | violations,
91 | rules,
92 | configured_rules,
93 | include_preview,
94 | ),
95 | key=lambda rule: rule.code,
96 | ):
97 | md.new_header(2, "Respected Ruff rules")
98 | md.new_line(
99 | f"{len(respected)} Ruff rules are already respected in the repo - "
100 | "they can be added right away 🚀"
101 | )
102 | output_table(
103 | items=([r.as_dict() for r in respected]),
104 | path=ARTIFACTS_PATH / "respected.csv",
105 | md=md,
106 | collapsible=True,
107 | )
108 |
109 | if autofixable := sort_by_code(
110 | autofixable_rules(
111 | violations,
112 | rules,
113 | configured_rules,
114 | include_sometimes_fixable,
115 | include_preview,
116 | )
117 | ):
118 | md.new_header(2, "Autofixable Ruff rules")
119 | always_status = " (sometimes)" if include_sometimes_fixable else ""
120 | md.new_line(
121 | f"{len(autofixable)} Ruff rules are violated in the repo, but can{always_status} be auto-fixed 🪄"
122 | )
123 | output_table(
124 | items=([r.as_dict() for r in autofixable]),
125 | path=ARTIFACTS_PATH / "autofixable.csv",
126 | md=md,
127 | collapsible=True,
128 | )
129 |
130 | if violated_rule_to_violations := (
131 | violated_rules(
132 | violations,
133 | rules,
134 | excluded_rules=(
135 | itertools.chain.from_iterable(
136 | (configured_rules, respected, autofixable)
137 | )
138 | ),
139 | include_preview=include_preview,
140 | )
141 | ):
142 | rule_to_violation_count = {
143 | rule: len(violations_)
144 | for rule, violations_ in violated_rule_to_violations.items()
145 | }
146 |
147 | applicable_rules = sorted(
148 | violated_rule_to_violations.keys(),
149 | key=lambda rule: (rule_to_violation_count[rule], rule.linter, rule.code),
150 | )
151 |
152 | md.new_header(2, "Applicable Rules")
153 | md.new_line(
154 | f"{len(applicable_rules)} other Ruff rules are not yet configured in the repository"
155 | )
156 | output_table(
157 | items=(
158 | [
159 | r.as_dict() | {"Violations": rule_to_violation_count[r]}
160 | for r in applicable_rules
161 | ]
162 | ),
163 | path=ARTIFACTS_PATH / "applicable.csv",
164 | md=md,
165 | collapsible=True,
166 | )
167 |
168 | if not any((respected, autofixable, violated_rule_to_violations)):
169 | md.new_line(
170 | f"You adopted Ruff well! 👏 {len(rules)} ruff rules are either selected or ignored."
171 | )
172 | if not include_preview:
173 | md.new_line(
174 | "You used --no-preview, ignoring rules in preview-mode.\n"
175 | "Consider running adopt-ruff again with the `--preview` flag: there may be more useful rules there 🔍\n"
176 | "Visit ⚡[Ruff's docs](https://docs.astral.sh/ruff/faq/#what-is-preview) for more information."
177 | )
178 | elif not include_sometimes_fixable:
179 | md.new_line(
180 | "Consider running adopt-ruff again with the `--sometimes-fixable flag`"
181 | )
182 |
183 | md.new_line(
184 | tabulate(
185 | [
186 | ["Include sometimes-fixable rules", include_sometimes_fixable],
187 | ["Include preview rules", include_preview],
188 | ],
189 | tablefmt="github",
190 | headers=["Configuration", "Value"],
191 | )
192 | )
193 | return md.get_md_text()
194 |
195 |
196 | def sort_by_code(rules: Iterable[Rule]) -> list[Rule]:
197 | return sorted(rules, key=lambda rule: rule.code)
198 |
199 |
200 | def respected_rules(
201 | violations: tuple[Violation, ...],
202 | rules: Iterable[Rule],
203 | configured_rules: set[Rule],
204 | include_preview: bool,
205 | ) -> set[Rule]:
206 | violated_codes = {v.code for v in violations}
207 |
208 | return {
209 | rule
210 | for rule in rules
211 | if (rule.code not in violated_codes)
212 | and (rule not in configured_rules)
213 | and (include_preview or (not rule.preview))
214 | }
215 |
216 |
217 | def autofixable_rules(
218 | violations: tuple[Violation, ...],
219 | rules: Iterable[Rule],
220 | configured_rules: set[Rule],
221 | include_sometimes_fixable: bool = False,
222 | include_preview: bool = False,
223 | ) -> set[Rule]:
224 | violated_codes = {v.code for v in violations}
225 |
226 | return {
227 | rule
228 | for rule in rules
229 | if rule.code in violated_codes
230 | and rule not in configured_rules
231 | and rule.is_fixable
232 | and (not rule.preview if not include_preview else True)
233 | and (
234 | rule.fix == FixAvailability.ALWAYS
235 | if not include_sometimes_fixable
236 | else True
237 | )
238 | }
239 |
240 |
241 | def violated_rules(
242 | violations: tuple[Violation, ...],
243 | rules: set[Rule],
244 | excluded_rules: Iterable[Rule],
245 | include_preview: bool,
246 | ) -> dict[Rule, tuple[Violation, ...]]:
247 | ignore_codes = {rule.code for rule in excluded_rules}
248 | return {
249 | rule: violations
250 | for rule, violations in map_rules_to_violations(rules, violations).items()
251 | if (rule.code not in ignore_codes) and (include_preview or (not rule.preview))
252 | }
253 |
254 |
255 | def map_rules_to_violations(
256 | rules: Iterable[Rule],
257 | violations: Iterable[Violation],
258 | ) -> dict[Rule, tuple[Violation, ...]]:
259 | code_to_violations = more_itertools.bucket(violations, key=lambda v: v.code)
260 | code_to_rule = {rule.code: rule for rule in rules}
261 |
262 | return {
263 | code_to_rule[code]: tuple(code_to_violations[code])
264 | for code in code_to_violations
265 | }
266 |
267 |
268 | def _main(
269 | code_path: Annotated[
270 | Path,
271 | typer.Option(
272 | help="The directory on which ruff should be run. If not provided, the current directory will be used.",
273 | envvar="ADOPT_RUFF_CODE_PATH",
274 | exists=True,
275 | dir_okay=True,
276 | file_okay=False,
277 | ),
278 | ] = Path(),
279 | ruff_conf_path: Annotated[
280 | Optional[Path], # noqa: UP007
281 | typer.Option(
282 | help="Path to the pyproject.toml/ruff.toml file. If not provided, adopt-ruff will attempt to locate it.",
283 | envvar="ADOPT_RUFF_CONFIG_FILE_PATH",
284 | exists=True,
285 | dir_okay=False,
286 | ),
287 | ] = None,
288 | include_sometimes_fixable: Annotated[
289 | bool,
290 | typer.Option(
291 | "--sometimes-fixable",
292 | help="consider sometimes-fixable rules as fixable",
293 | envvar="ADOPT_RUFF_SOMETIMES_FIXABLE",
294 | is_flag=True,
295 | rich_help_panel="Rule configurations",
296 | ),
297 | ] = False,
298 | include_preview: Annotated[
299 | bool,
300 | typer.Option(
301 | "--preview/--no-preview",
302 | help="include preview rules. See https://docs.astral.sh/ruff/faq/#what-is-preview",
303 | envvar="ADOPT_RUFF_PREVIEW",
304 | is_flag=True,
305 | rich_help_panel="Rule configurations",
306 | ),
307 | ] = True,
308 | repo_name: Annotated[
309 | Optional[str], # noqa: UP007
310 | typer.Option(
311 | help="The repository name for the report",
312 | envvar="ADOPT_RUFF_REPO_NAME",
313 | ),
314 | ] = None,
315 | ):
316 | logger.debug(f"{code_path.resolve()=!s}")
317 | logger.debug(f"{ruff_conf_path=!s}")
318 | logger.debug(f"{include_preview=}")
319 | logger.debug(f"{include_sometimes_fixable=}")
320 | logger.debug(f"{repo_name=}")
321 |
322 | rules, violations, ruff_version = run_ruff(code_path)
323 | config: RuffConfig = RuffConfig.from_file(
324 | path=ruff_conf_path or search_config_file(code_path),
325 | rules=rules,
326 | )
327 |
328 | result = run(
329 | rules=rules,
330 | violations=violations,
331 | config=config,
332 | ruff_version=ruff_version,
333 | include_preview=include_preview,
334 | include_sometimes_fixable=include_sometimes_fixable,
335 | repo_name=repo_name,
336 | )
337 |
338 | Path("result.md").write_text(result)
339 | logger.debug("wrote output to result.md")
340 |
341 |
342 | def main():
343 | typer.run(_main)
344 |
345 |
346 | if __name__ == "__main__":
347 | main()
348 |
--------------------------------------------------------------------------------
/adopt_ruff/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ScDor/adopt-ruff/59474307bc4c44fda046cfbbc35c14076c7a7fd3/adopt_ruff/models/__init__.py
--------------------------------------------------------------------------------
/adopt_ruff/models/ruff_config.py:
--------------------------------------------------------------------------------
1 | import tomllib
2 | from pathlib import Path
3 |
4 | from pydantic import BaseModel
5 |
6 | from adopt_ruff.models.rule import Rule
7 | from adopt_ruff.utils import logger
8 |
9 | MIN_RULE_CODE_LEN = 4
10 | DEFAULT_SELECT_RULES = ("E", "F")
11 |
12 |
13 | class RawRuffConfig(BaseModel):
14 | selected_codes: set[str]
15 | ignored_codes: set[str]
16 |
17 | @classmethod
18 | def read_toml(cls, path: Path) -> "RawRuffConfig":
19 | try:
20 | toml = tomllib.loads(path.read_text())
21 | except ValueError as e:
22 | raise ValueError(f"make sure that {path.name} is a valid toml") from e
23 |
24 | match path.name:
25 | case "pyproject.toml":
26 | if (ruff_section := toml.get("tool", {}).get("ruff")) is None:
27 | logger.warning(
28 | f"could not find `tool` or `tool.ruff` in {path.name}, using default"
29 | )
30 | return cls.default_config()
31 |
32 | case "ruff.toml" | ".ruff.toml":
33 | ruff_section = toml
34 | case _:
35 | raise ValueError(
36 | f"config file must be pyproject.toml, ruff.toml or .ruff.toml, got {path.name}"
37 | )
38 |
39 | ruff_lint_section = ruff_section.get(
40 | "lint", ruff_section
41 | ) # both are valid, `lint` was added at some point
42 |
43 | raw_select_codes = set(ruff_lint_section.get("select", ()))
44 | raw_ignored_codes = set(ruff_lint_section.get("ignore", ()))
45 |
46 | logger.debug(f"{len(raw_select_codes)=}, {len(raw_ignored_codes)=}")
47 | return RawRuffConfig(
48 | selected_codes=raw_select_codes,
49 | ignored_codes=raw_ignored_codes,
50 | )
51 |
52 | @classmethod
53 | def default_config(cls) -> "RawRuffConfig":
54 | return RawRuffConfig(
55 | selected_codes=set(DEFAULT_SELECT_RULES),
56 | ignored_codes=set(),
57 | )
58 |
59 |
60 | class RuffConfig(BaseModel):
61 | selected_rules: set[Rule]
62 | ignored_rules: set[Rule]
63 |
64 | @staticmethod
65 | def from_file(path: Path | None, rules: set[Rule]) -> "RuffConfig":
66 | if path:
67 | logger.debug(f"reading ruff config file from {path!s}")
68 | raw_config = RawRuffConfig.read_toml(path)
69 | else:
70 | logger.warning(
71 | f"Config path was not specified, using default={DEFAULT_SELECT_RULES}"
72 | )
73 | raw_config = RawRuffConfig.default_config()
74 |
75 | return RuffConfig(
76 | selected_rules=_parse_raw_rules(raw_config.selected_codes, rules),
77 | ignored_rules=_parse_raw_rules(raw_config.ignored_codes, rules),
78 | )
79 |
80 | @property
81 | def all_rules(self) -> set[Rule]:
82 | return self.selected_rules | self.ignored_rules
83 |
84 |
85 | def _parse_raw_rules(raw_codes: set[str], rules: set[Rule]) -> set[Rule]:
86 | """
87 | Convert code values (E401), categories (E) and ALL, into Rule objects
88 | """
89 | if "ALL" in raw_codes:
90 | return rules
91 |
92 | code_to_rule = {rule.code: rule for rule in rules}
93 |
94 | result: set[Rule] = set()
95 |
96 | for code in raw_codes:
97 | if code.isalpha() or len(code) < MIN_RULE_CODE_LEN:
98 | code_rules = tuple(
99 | rule for rule in rules if rule.code.removeprefix(code).isnumeric()
100 | )
101 | logger.debug(
102 | f"assuming {code} is a category, adding {len(code_rules)} rules: {sorted(r.code for r in code_rules)!s}"
103 | )
104 | result.update(code_rules)
105 | # TODO are there cases of category names with len>=MIN_RULE_CODE_LEN, mixing alpha&digits?
106 | else:
107 | result.add(code_to_rule[code])
108 |
109 | logger.debug(f"converted {len(raw_codes)} raw codes into {len(result)} rules")
110 |
111 | return result
112 |
--------------------------------------------------------------------------------
/adopt_ruff/models/ruff_output.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Location(BaseModel):
7 | column: int
8 | row: int
9 |
10 |
11 | class Edit(BaseModel):
12 | content: str
13 | end_location: Location
14 | location: Location
15 |
16 |
17 | class Fix(BaseModel):
18 | applicability: str
19 | edits: tuple[Edit, ...]
20 | message: str | None
21 |
22 |
23 | class Violation(BaseModel):
24 | cell: None # TODO handle notebook output
25 | code: str
26 | end_location: Location
27 | filename: str
28 | fix: Fix | None
29 | location: Location
30 | message: str
31 | noqa_row: int
32 | url: str
33 |
--------------------------------------------------------------------------------
/adopt_ruff/models/rule.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from enum import Enum
4 | from typing import Any, Literal
5 |
6 | from pydantic import BaseModel
7 |
8 |
9 | class FixAvailability(Enum):
10 | ALWAYS = "Fix is always available."
11 | SOMETIMES = "Fix is sometimes available."
12 | NOT = "Fix is not available."
13 |
14 | @property
15 | def one_word(self) -> str:
16 | match self:
17 | case FixAvailability.ALWAYS:
18 | return "Always"
19 | case FixAvailability.SOMETIMES:
20 | return "Sometimes"
21 | case FixAvailability.NOT:
22 | return "No"
23 | case _:
24 | raise ValueError
25 |
26 |
27 | class Rule(BaseModel):
28 | class Config:
29 | frozen = True
30 | use_enum_values: Literal[True]
31 |
32 | name: str
33 | code: str
34 | linter: str
35 | summary: str
36 | message_formats: tuple[str, ...]
37 | fix: FixAvailability
38 | explanation: str
39 | preview: bool
40 |
41 | def as_dict(self) -> dict[str, Any]:
42 | return {
43 | "Code": self.code,
44 | "Name": self.name,
45 | "Fixable": self.fix.one_word,
46 | "Preview": self.preview,
47 | "Linter": self.linter,
48 | }
49 |
50 | @property
51 | def is_fixable(self):
52 | return self.fix in {FixAvailability.ALWAYS, FixAvailability.SOMETIMES}
53 |
--------------------------------------------------------------------------------
/adopt_ruff/utils.py:
--------------------------------------------------------------------------------
1 | import csv
2 | from collections.abc import Iterable
3 | from pathlib import Path
4 |
5 | from loguru import logger
6 | from mdutils.mdutils import MdUtils
7 | from tabulate import tabulate
8 |
9 | (ARTIFACTS_PATH := Path("artifacts")).mkdir(exist_ok=True)
10 |
11 | logger.add((ARTIFACTS_PATH / "adopt-ruff.log"), level="DEBUG")
12 |
13 |
14 | def make_collapsible(content: str, summary: str) -> str:
15 | return f"""
16 | {summary}
17 |
18 | {content}
19 |
20 |
21 | """
22 |
23 |
24 | def table_to_csv(items: list[dict], path: Path) -> None:
25 | if not items:
26 | logger.warning(f"no items to write to {path.name}, skipping")
27 | return
28 |
29 | if len(items) > 1 and not all(
30 | # assert all keys are in the same order
31 | item.keys() == items[0].keys()
32 | for item in items[1:]
33 | ):
34 | raise ValueError("All table row keys must be identical, and in the same order")
35 |
36 | with path.open("w") as f:
37 | writer = csv.writer(f)
38 | writer.writerow(items[0].keys()) # Headers
39 | writer.writerows(item.values() for item in items)
40 |
41 | logger.debug(f"wrote {len(items)} to {path.absolute()!s}")
42 |
43 |
44 | def output_table(
45 | items: Iterable[dict],
46 | path: Path,
47 | md: MdUtils,
48 | collapsible: bool,
49 | collapsible_summary: str = "Details",
50 | ) -> None:
51 | """
52 | Creates a markdown table, and saves to a CSV
53 | """
54 |
55 | md_table = tabulate(
56 | [make_name_clickable(item) for item in items],
57 | tablefmt="github",
58 | headers="keys",
59 | )
60 |
61 | if collapsible:
62 | md_table = make_collapsible(md_table, summary=collapsible_summary)
63 |
64 | md.new_line(md_table)
65 | table_to_csv(list(items), path)
66 |
67 |
68 | def make_name_clickable(item: dict) -> dict:
69 | if not (name := item.get("Name")):
70 | return item
71 | return item | {"Name": f"[{name}](https://docs.astral.sh/ruff/rules/{name})"}
72 |
73 |
74 | def search_config_file(path: Path) -> Path | None:
75 | """
76 | Searches for common configuration files under the given directory.
77 | """
78 | for name in ("pyproject.toml", "ruff.toml", ".ruff.toml"):
79 | if (file_path := path / name).exists():
80 | logger.info(f"found config file at {file_path.resolve()!s}")
81 | return file_path
82 | return None
83 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "adopt-ruff"
3 | version = "0.1.0"
4 | description = "Adopt Ruff faster"
5 | readme = "README.md"
6 | requires-python = ">=3.11"
7 | dependencies = [
8 | "loguru>=0.7.3",
9 | "mdutils>=1.6.0",
10 | "more-itertools>=10.5.0",
11 | "packaging>=24.2",
12 | "pydantic>=2.10.4",
13 | "tabulate>=0.9.0",
14 | "typer>=0.15.1",
15 | ]
16 |
17 | [build-system]
18 | requires = ["hatchling"]
19 | build-backend = "hatchling.build"
20 |
21 | [dependency-groups]
22 | dev = [
23 | "mypy>=1.14.0",
24 | "pre-commit>=4.0.1",
25 | "ruff>=0.8.4",
26 | ]
27 | typing = [
28 | "types-tabulate>=0.9.0.20241207",
29 | ]
30 | [project.scripts]
31 | adopt-ruff = "adopt_ruff.main:main"
32 |
33 |
34 | [tool.ruff]
35 | target-version = "py311"
36 |
37 | [tool.ruff.lint]
38 | select = ["ALL"]
39 | ignore = [
40 | "ISC001",
41 | "ANN",
42 | "D",
43 | "DTZ",
44 | "COM812",
45 | "EM102",
46 | "TRY003",
47 | "S113",
48 | "G003",
49 | "G004",
50 | "INP001",
51 | "S501",
52 | "PD901",
53 | "E501",
54 | "TD002",
55 | "TD003",
56 | "TD004",
57 | "FIX002",
58 | "EM101",
59 | "FBT001",
60 | "FBT002",
61 | "S603",
62 | "S607",
63 | "PLR0913",
64 | ]
65 |
--------------------------------------------------------------------------------
/uv.lock:
--------------------------------------------------------------------------------
1 | version = 1
2 | requires-python = ">=3.11"
3 |
4 | [[package]]
5 | name = "adopt-ruff"
6 | version = "0.1.0"
7 | source = { editable = "." }
8 | dependencies = [
9 | { name = "loguru" },
10 | { name = "mdutils" },
11 | { name = "more-itertools" },
12 | { name = "packaging" },
13 | { name = "pydantic" },
14 | { name = "tabulate" },
15 | { name = "typer" },
16 | ]
17 |
18 | [package.dev-dependencies]
19 | dev = [
20 | { name = "mypy" },
21 | { name = "pre-commit" },
22 | { name = "ruff" },
23 | ]
24 | typing = [
25 | { name = "types-tabulate" },
26 | ]
27 |
28 | [package.metadata]
29 | requires-dist = [
30 | { name = "loguru", specifier = ">=0.7.3" },
31 | { name = "mdutils", specifier = ">=1.6.0" },
32 | { name = "more-itertools", specifier = ">=10.5.0" },
33 | { name = "packaging", specifier = ">=24.2" },
34 | { name = "pydantic", specifier = ">=2.10.4" },
35 | { name = "tabulate", specifier = ">=0.9.0" },
36 | { name = "typer", specifier = ">=0.15.1" },
37 | ]
38 |
39 | [package.metadata.requires-dev]
40 | dev = [
41 | { name = "mypy", specifier = ">=1.14.0" },
42 | { name = "pre-commit", specifier = ">=4.0.1" },
43 | { name = "ruff", specifier = ">=0.8.4" },
44 | ]
45 | typing = [{ name = "types-tabulate", specifier = ">=0.9.0.20241207" }]
46 |
47 | [[package]]
48 | name = "annotated-types"
49 | version = "0.7.0"
50 | source = { registry = "https://pypi.org/simple" }
51 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
52 | wheels = [
53 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
54 | ]
55 |
56 | [[package]]
57 | name = "cfgv"
58 | version = "3.4.0"
59 | source = { registry = "https://pypi.org/simple" }
60 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
61 | wheels = [
62 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
63 | ]
64 |
65 | [[package]]
66 | name = "click"
67 | version = "8.1.7"
68 | source = { registry = "https://pypi.org/simple" }
69 | dependencies = [
70 | { name = "colorama", marker = "sys_platform == 'win32'" },
71 | ]
72 | sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
73 | wheels = [
74 | { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
75 | ]
76 |
77 | [[package]]
78 | name = "colorama"
79 | version = "0.4.6"
80 | source = { registry = "https://pypi.org/simple" }
81 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
82 | wheels = [
83 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
84 | ]
85 |
86 | [[package]]
87 | name = "distlib"
88 | version = "0.3.9"
89 | source = { registry = "https://pypi.org/simple" }
90 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
91 | wheels = [
92 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
93 | ]
94 |
95 | [[package]]
96 | name = "filelock"
97 | version = "3.16.1"
98 | source = { registry = "https://pypi.org/simple" }
99 | sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 }
100 | wheels = [
101 | { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
102 | ]
103 |
104 | [[package]]
105 | name = "identify"
106 | version = "2.6.3"
107 | source = { registry = "https://pypi.org/simple" }
108 | sdist = { url = "https://files.pythonhosted.org/packages/1a/5f/05f0d167be94585d502b4adf8c7af31f1dc0b1c7e14f9938a88fdbbcf4a7/identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02", size = 99179 }
109 | wheels = [
110 | { url = "https://files.pythonhosted.org/packages/c9/f5/09644a3ad803fae9eca8efa17e1f2aef380c7f0b02f7ec4e8d446e51d64a/identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd", size = 99049 },
111 | ]
112 |
113 | [[package]]
114 | name = "loguru"
115 | version = "0.7.3"
116 | source = { registry = "https://pypi.org/simple" }
117 | dependencies = [
118 | { name = "colorama", marker = "sys_platform == 'win32'" },
119 | { name = "win32-setctime", marker = "sys_platform == 'win32'" },
120 | ]
121 | sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
122 | wheels = [
123 | { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
124 | ]
125 |
126 | [[package]]
127 | name = "markdown-it-py"
128 | version = "3.0.0"
129 | source = { registry = "https://pypi.org/simple" }
130 | dependencies = [
131 | { name = "mdurl" },
132 | ]
133 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
134 | wheels = [
135 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
136 | ]
137 |
138 | [[package]]
139 | name = "mdurl"
140 | version = "0.1.2"
141 | source = { registry = "https://pypi.org/simple" }
142 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
143 | wheels = [
144 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
145 | ]
146 |
147 | [[package]]
148 | name = "mdutils"
149 | version = "1.6.0"
150 | source = { registry = "https://pypi.org/simple" }
151 | sdist = { url = "https://files.pythonhosted.org/packages/b1/ec/6240f147530a2c8d362ed3f2f7985aca92cda68c25ffc2fc216504b17148/mdutils-1.6.0.tar.gz", hash = "sha256:647f3cf00df39fee6c57fa6738dc1160fce1788276b5530c87d43a70cdefdaf1", size = 22881 }
152 |
153 | [[package]]
154 | name = "more-itertools"
155 | version = "10.5.0"
156 | source = { registry = "https://pypi.org/simple" }
157 | sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 }
158 | wheels = [
159 | { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 },
160 | ]
161 |
162 | [[package]]
163 | name = "mypy"
164 | version = "1.14.0"
165 | source = { registry = "https://pypi.org/simple" }
166 | dependencies = [
167 | { name = "mypy-extensions" },
168 | { name = "typing-extensions" },
169 | ]
170 | sdist = { url = "https://files.pythonhosted.org/packages/8c/7b/08046ef9330735f536a09a2e31b00f42bccdb2795dcd979636ba43bb2d63/mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6", size = 3215684 }
171 | wheels = [
172 | { url = "https://files.pythonhosted.org/packages/34/c1/b9dd3e955953aec1c728992545b7877c9f6fa742a623ce4c200da0f62540/mypy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e73c8a154eed31db3445fe28f63ad2d97b674b911c00191416cf7f6459fd49a", size = 11121032 },
173 | { url = "https://files.pythonhosted.org/packages/ee/96/c52d5d516819ab95bf41f4a1ada828a3decc302f8c152ff4fc5feb0e4529/mypy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:273e70fcb2e38c5405a188425aa60b984ffdcef65d6c746ea5813024b68c73dc", size = 10286294 },
174 | { url = "https://files.pythonhosted.org/packages/69/2c/3dbe51877a24daa467f8d8631f9ffd1aabbf0f6d9367a01c44a59df81fe0/mypy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1daca283d732943731a6a9f20fdbcaa927f160bc51602b1d4ef880a6fb252015", size = 12746528 },
175 | { url = "https://files.pythonhosted.org/packages/a1/a8/eb20cde4ba9c4c3e20d958918a7c5d92210f4d1a0200c27de9a641f70996/mypy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7e68047bedb04c1c25bba9901ea46ff60d5eaac2d71b1f2161f33107e2b368eb", size = 12883489 },
176 | { url = "https://files.pythonhosted.org/packages/91/17/a1fc6c70f31d52c99299320cf81c3cb2c6b91ec7269414e0718a6d138e34/mypy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:7a52f26b9c9b1664a60d87675f3bae00b5c7f2806e0c2800545a32c325920bcc", size = 9780113 },
177 | { url = "https://files.pythonhosted.org/packages/fe/d8/0e72175ee0253217f5c44524f5e95251c02e95ba9749fb87b0e2074d203a/mypy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5326ab70a6db8e856d59ad4cb72741124950cbbf32e7b70e30166ba7bbf61dd", size = 11269011 },
178 | { url = "https://files.pythonhosted.org/packages/e9/6d/4ea13839dabe5db588dc6a1b766da16f420d33cf118a7b7172cdf6c7fcb2/mypy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf4ec4980bec1e0e24e5075f449d014011527ae0055884c7e3abc6a99cd2c7f1", size = 10253076 },
179 | { url = "https://files.pythonhosted.org/packages/3e/38/7db2c5d0f4d290e998f7a52b2e2616c7bbad96b8e04278ab09d11978a29e/mypy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:390dfb898239c25289495500f12fa73aa7f24a4c6d90ccdc165762462b998d63", size = 12862786 },
180 | { url = "https://files.pythonhosted.org/packages/bf/4b/62d59c801b34141040989949c2b5c157d0408b45357335d3ec5b2845b0f6/mypy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e026d55ddcd76e29e87865c08cbe2d0104e2b3153a523c529de584759379d3d", size = 12971568 },
181 | { url = "https://files.pythonhosted.org/packages/f1/9c/e0f281b32d70c87b9e4d2939e302b1ff77ada4d7b0f2fb32890c144bc1d6/mypy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:585ed36031d0b3ee362e5107ef449a8b5dfd4e9c90ccbe36414ee405ee6b32ba", size = 9879477 },
182 | { url = "https://files.pythonhosted.org/packages/13/33/8380efd0ebdfdfac7fc0bf065f03a049800ca1e6c296ec1afc634340d992/mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741", size = 11251509 },
183 | { url = "https://files.pythonhosted.org/packages/15/6d/4e1c21c60fee11af7d8e4f2902a29886d1387d6a836be16229eb3982a963/mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7", size = 10244282 },
184 | { url = "https://files.pythonhosted.org/packages/8b/cf/7a8ae5c0161edae15d25c2c67c68ce8b150cbdc45aefc13a8be271ee80b2/mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8", size = 12867676 },
185 | { url = "https://files.pythonhosted.org/packages/9c/d0/71f7bbdcc7cfd0f2892db5b13b1e8857673f2cc9e0c30e3e4340523dc186/mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc", size = 12964189 },
186 | { url = "https://files.pythonhosted.org/packages/a7/40/fb4ad65d6d5f8c51396ecf6305ec0269b66013a5bf02d0e9528053640b4a/mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f", size = 9888247 },
187 | { url = "https://files.pythonhosted.org/packages/39/32/0214608af400cdf8f5102144bb8af10d880675c65ed0b58f7e0e77175d50/mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab", size = 2752803 },
188 | ]
189 |
190 | [[package]]
191 | name = "mypy-extensions"
192 | version = "1.0.0"
193 | source = { registry = "https://pypi.org/simple" }
194 | sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
195 | wheels = [
196 | { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
197 | ]
198 |
199 | [[package]]
200 | name = "nodeenv"
201 | version = "1.9.1"
202 | source = { registry = "https://pypi.org/simple" }
203 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
204 | wheels = [
205 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
206 | ]
207 |
208 | [[package]]
209 | name = "packaging"
210 | version = "24.2"
211 | source = { registry = "https://pypi.org/simple" }
212 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
213 | wheels = [
214 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
215 | ]
216 |
217 | [[package]]
218 | name = "platformdirs"
219 | version = "4.3.6"
220 | source = { registry = "https://pypi.org/simple" }
221 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
222 | wheels = [
223 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
224 | ]
225 |
226 | [[package]]
227 | name = "pre-commit"
228 | version = "4.0.1"
229 | source = { registry = "https://pypi.org/simple" }
230 | dependencies = [
231 | { name = "cfgv" },
232 | { name = "identify" },
233 | { name = "nodeenv" },
234 | { name = "pyyaml" },
235 | { name = "virtualenv" },
236 | ]
237 | sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 }
238 | wheels = [
239 | { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 },
240 | ]
241 |
242 | [[package]]
243 | name = "pydantic"
244 | version = "2.10.4"
245 | source = { registry = "https://pypi.org/simple" }
246 | dependencies = [
247 | { name = "annotated-types" },
248 | { name = "pydantic-core" },
249 | { name = "typing-extensions" },
250 | ]
251 | sdist = { url = "https://files.pythonhosted.org/packages/70/7e/fb60e6fee04d0ef8f15e4e01ff187a196fa976eb0f0ab524af4599e5754c/pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06", size = 762094 }
252 | wheels = [
253 | { url = "https://files.pythonhosted.org/packages/f3/26/3e1bbe954fde7ee22a6e7d31582c642aad9e84ffe4b5fb61e63b87cd326f/pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", size = 431765 },
254 | ]
255 |
256 | [[package]]
257 | name = "pydantic-core"
258 | version = "2.27.2"
259 | source = { registry = "https://pypi.org/simple" }
260 | dependencies = [
261 | { name = "typing-extensions" },
262 | ]
263 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
264 | wheels = [
265 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
266 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
267 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
268 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
269 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
270 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
271 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
272 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
273 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
274 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
275 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
276 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
277 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
278 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
279 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
280 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
281 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
282 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
283 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
284 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
285 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
286 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
287 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
288 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
289 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
290 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
291 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
292 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
293 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
294 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
295 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
296 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
297 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
298 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
299 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
300 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
301 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
302 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
303 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
304 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
305 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
306 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
307 | ]
308 |
309 | [[package]]
310 | name = "pygments"
311 | version = "2.18.0"
312 | source = { registry = "https://pypi.org/simple" }
313 | sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 }
314 | wheels = [
315 | { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
316 | ]
317 |
318 | [[package]]
319 | name = "pyyaml"
320 | version = "6.0.2"
321 | source = { registry = "https://pypi.org/simple" }
322 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
323 | wheels = [
324 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
325 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
326 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
327 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
328 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
329 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
330 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
331 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
332 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
333 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
334 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
335 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
336 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
337 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
338 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
339 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
340 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
341 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
342 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
343 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
344 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
345 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
346 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
347 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
348 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
349 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
350 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
351 | ]
352 |
353 | [[package]]
354 | name = "rich"
355 | version = "13.9.4"
356 | source = { registry = "https://pypi.org/simple" }
357 | dependencies = [
358 | { name = "markdown-it-py" },
359 | { name = "pygments" },
360 | ]
361 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
362 | wheels = [
363 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
364 | ]
365 |
366 | [[package]]
367 | name = "ruff"
368 | version = "0.8.4"
369 | source = { registry = "https://pypi.org/simple" }
370 | sdist = { url = "https://files.pythonhosted.org/packages/34/37/9c02181ef38d55b77d97c68b78e705fd14c0de0e5d085202bb2b52ce5be9/ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8", size = 3402103 }
371 | wheels = [
372 | { url = "https://files.pythonhosted.org/packages/05/67/f480bf2f2723b2e49af38ed2be75ccdb2798fca7d56279b585c8f553aaab/ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60", size = 10546415 },
373 | { url = "https://files.pythonhosted.org/packages/eb/7a/5aba20312c73f1ce61814e520d1920edf68ca3b9c507bd84d8546a8ecaa8/ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac", size = 10346113 },
374 | { url = "https://files.pythonhosted.org/packages/76/f4/c41de22b3728486f0aa95383a44c42657b2db4062f3234ca36fc8cf52d8b/ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296", size = 9943564 },
375 | { url = "https://files.pythonhosted.org/packages/0e/f0/afa0d2191af495ac82d4cbbfd7a94e3df6f62a04ca412033e073b871fc6d/ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643", size = 10805522 },
376 | { url = "https://files.pythonhosted.org/packages/12/57/5d1e9a0fd0c228e663894e8e3a8e7063e5ee90f8e8e60cf2085f362bfa1a/ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e", size = 10306763 },
377 | { url = "https://files.pythonhosted.org/packages/04/df/f069fdb02e408be8aac6853583572a2873f87f866fe8515de65873caf6b8/ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3", size = 11359574 },
378 | { url = "https://files.pythonhosted.org/packages/d3/04/37c27494cd02e4a8315680debfc6dfabcb97e597c07cce0044db1f9dfbe2/ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f", size = 12094851 },
379 | { url = "https://files.pythonhosted.org/packages/81/b1/c5d7fb68506cab9832d208d03ea4668da9a9887a4a392f4f328b1bf734ad/ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604", size = 11655539 },
380 | { url = "https://files.pythonhosted.org/packages/ef/38/8f8f2c8898dc8a7a49bc340cf6f00226917f0f5cb489e37075bcb2ce3671/ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf", size = 12912805 },
381 | { url = "https://files.pythonhosted.org/packages/06/dd/fa6660c279f4eb320788876d0cff4ea18d9af7d9ed7216d7bd66877468d0/ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720", size = 11205976 },
382 | { url = "https://files.pythonhosted.org/packages/a8/d7/de94cc89833b5de455750686c17c9e10f4e1ab7ccdc5521b8fe911d1477e/ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae", size = 10792039 },
383 | { url = "https://files.pythonhosted.org/packages/6d/15/3e4906559248bdbb74854af684314608297a05b996062c9d72e0ef7c7097/ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7", size = 10400088 },
384 | { url = "https://files.pythonhosted.org/packages/a2/21/9ed4c0e8133cb4a87a18d470f534ad1a8a66d7bec493bcb8bda2d1a5d5be/ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111", size = 10900814 },
385 | { url = "https://files.pythonhosted.org/packages/0d/5d/122a65a18955bd9da2616b69bc839351f8baf23b2805b543aa2f0aed72b5/ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8", size = 11268828 },
386 | { url = "https://files.pythonhosted.org/packages/43/a9/1676ee9106995381e3d34bccac5bb28df70194167337ed4854c20f27c7ba/ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835", size = 8805621 },
387 | { url = "https://files.pythonhosted.org/packages/10/98/ed6b56a30ee76771c193ff7ceeaf1d2acc98d33a1a27b8479cbdb5c17a23/ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d", size = 9660086 },
388 | { url = "https://files.pythonhosted.org/packages/13/9f/026e18ca7d7766783d779dae5e9c656746c6ede36ef73c6d934aaf4a6dec/ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08", size = 9074500 },
389 | ]
390 |
391 | [[package]]
392 | name = "shellingham"
393 | version = "1.5.4"
394 | source = { registry = "https://pypi.org/simple" }
395 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
396 | wheels = [
397 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
398 | ]
399 |
400 | [[package]]
401 | name = "tabulate"
402 | version = "0.9.0"
403 | source = { registry = "https://pypi.org/simple" }
404 | sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 }
405 | wheels = [
406 | { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 },
407 | ]
408 |
409 | [[package]]
410 | name = "typer"
411 | version = "0.15.1"
412 | source = { registry = "https://pypi.org/simple" }
413 | dependencies = [
414 | { name = "click" },
415 | { name = "rich" },
416 | { name = "shellingham" },
417 | { name = "typing-extensions" },
418 | ]
419 | sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 }
420 | wheels = [
421 | { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 },
422 | ]
423 |
424 | [[package]]
425 | name = "types-tabulate"
426 | version = "0.9.0.20241207"
427 | source = { registry = "https://pypi.org/simple" }
428 | sdist = { url = "https://files.pythonhosted.org/packages/3f/43/16030404a327e4ff8c692f2273854019ed36718667b2993609dc37d14dd4/types_tabulate-0.9.0.20241207.tar.gz", hash = "sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230", size = 8195 }
429 | wheels = [
430 | { url = "https://files.pythonhosted.org/packages/5e/86/a9ebfd509cbe74471106dffed320e208c72537f9aeb0a55eaa6b1b5e4d17/types_tabulate-0.9.0.20241207-py3-none-any.whl", hash = "sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85", size = 8307 },
431 | ]
432 |
433 | [[package]]
434 | name = "typing-extensions"
435 | version = "4.12.2"
436 | source = { registry = "https://pypi.org/simple" }
437 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
438 | wheels = [
439 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
440 | ]
441 |
442 | [[package]]
443 | name = "virtualenv"
444 | version = "20.28.0"
445 | source = { registry = "https://pypi.org/simple" }
446 | dependencies = [
447 | { name = "distlib" },
448 | { name = "filelock" },
449 | { name = "platformdirs" },
450 | ]
451 | sdist = { url = "https://files.pythonhosted.org/packages/bf/75/53316a5a8050069228a2f6d11f32046cfa94fbb6cc3f08703f59b873de2e/virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa", size = 7650368 }
452 | wheels = [
453 | { url = "https://files.pythonhosted.org/packages/10/f9/0919cf6f1432a8c4baa62511f8f8da8225432d22e83e3476f5be1a1edc6e/virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0", size = 4276702 },
454 | ]
455 |
456 | [[package]]
457 | name = "win32-setctime"
458 | version = "1.2.0"
459 | source = { registry = "https://pypi.org/simple" }
460 | sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
461 | wheels = [
462 | { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
463 | ]
464 |
--------------------------------------------------------------------------------