polygit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ .pytest_cache/
8
+ .DS_Store
9
+ *.egg
10
+ .venv/
@@ -0,0 +1,44 @@
1
+ stages:
2
+ - test
3
+ - build
4
+ - deploy
5
+
6
+ variables:
7
+ UV_NO_SYNC: "0"
8
+
9
+ test:
10
+ image: gschaetz/python:latest
11
+ stage: test
12
+ tags:
13
+ - docker
14
+ - kubernetes
15
+ script:
16
+ - uv sync
17
+ - uv run pytest
18
+
19
+ build:
20
+ image: gschaetz/python:latest
21
+ stage: build
22
+ tags:
23
+ - docker
24
+ - kubernetes
25
+ rules:
26
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
27
+ script:
28
+ - uv build
29
+ artifacts:
30
+ paths:
31
+ - dist/
32
+
33
+ deploy:
34
+ image: gschaetz/python:latest
35
+ stage: deploy
36
+ tags:
37
+ - docker
38
+ - kubernetes
39
+ rules:
40
+ - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
41
+ dependencies:
42
+ - build
43
+ script:
44
+ - uv publish --token $PYPI_API_TOKEN
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Gary Schaetz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
polygit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: polygit
3
+ Version: 0.1.0
4
+ Summary: Manage multiple git repos from a single .pgit config file.
5
+ Project-URL: Repository, https://gitlab.com/gary.schaetz/public/polygit
6
+ Author-email: Gary Schaetz <gary@schaetzkc.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2025 Gary Schaetz
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE.txt
29
+ Keywords: devtools,git,multi-repo,pgit,polygit,repos
30
+ Requires-Python: >=3.8
31
+ Requires-Dist: pyyaml>=6.0
32
+ Description-Content-Type: text/markdown
33
+
34
+ # polygit
35
+
36
+ Manage multiple git repos from a single `.pgit` config file.
37
+
38
+ ```bash
39
+ pip install polygit
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```bash
45
+ pgit sync # clone missing repos, fetch all
46
+ pgit status # show repos with uncommitted changes
47
+ pgit pull # pull all repos, auto-stashing local changes
48
+ pgit push # interactively commit and push dirty repos
49
+ pgit repos # discover drift between .pgit and the filesystem
50
+ ```
51
+
52
+ ## Configuration
53
+
54
+ Create a `.pgit` file in the root directory that contains your repos:
55
+
56
+ ```yaml
57
+ github:
58
+ your-username:
59
+ repo-one:
60
+ repo-two:
61
+ some-org:
62
+ their-repo: {remote: "some-org/their-repo"}
63
+
64
+ gitlab:
65
+ your-namespace/group:
66
+ private-repo:
67
+ another-repo:
68
+ ```
69
+
70
+ `pgit` discovers this file by walking up from the current directory — so running `pgit status` anywhere inside your repos root just works.
71
+
72
+ ### Remote overrides
73
+
74
+ If a repo is cloned to a local path that differs from its remote path, use the `remote` key:
75
+
76
+ ```yaml
77
+ github:
78
+ my-local-name:
79
+ project: {remote: "upstream-org/project"}
80
+ ```
81
+
82
+ ## push modes
83
+
84
+ ```bash
85
+ pgit push # prompt for a message per repo (default)
86
+ pgit push --all # one message for all dirty repos
87
+ pgit push --wip # auto-commit with "WIP <timestamp>"
88
+ ```
89
+
90
+ ## repos — drift detection
91
+
92
+ `pgit repos` compares what's actually on disk against your `.pgit` config and reports:
93
+
94
+ - **Remote mismatch** — repo is at the right path but the git remote differs (e.g. SSH vs HTTPS)
95
+ - **Wrong location** — remote is tracked in `.pgit` but cloned to a different directory
96
+ - **Untracked** — repo on disk not referenced in `.pgit` at all
97
+ - **Missing** — in `.pgit` but not yet cloned (run `pgit sync`)
98
+
99
+ It then offers to update `.pgit` or rename directories interactively.
100
+
101
+ ## Migrating from repos.yaml
102
+
103
+ If you were using the `sync.sh` predecessor, rename your `repos.yaml` to `.pgit` — the format is identical.
104
+
105
+ ## Requirements
106
+
107
+ - Python 3.8+
108
+ - git
@@ -0,0 +1,75 @@
1
+ # polygit
2
+
3
+ Manage multiple git repos from a single `.pgit` config file.
4
+
5
+ ```bash
6
+ pip install polygit
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ pgit sync # clone missing repos, fetch all
13
+ pgit status # show repos with uncommitted changes
14
+ pgit pull # pull all repos, auto-stashing local changes
15
+ pgit push # interactively commit and push dirty repos
16
+ pgit repos # discover drift between .pgit and the filesystem
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ Create a `.pgit` file in the root directory that contains your repos:
22
+
23
+ ```yaml
24
+ github:
25
+ your-username:
26
+ repo-one:
27
+ repo-two:
28
+ some-org:
29
+ their-repo: {remote: "some-org/their-repo"}
30
+
31
+ gitlab:
32
+ your-namespace/group:
33
+ private-repo:
34
+ another-repo:
35
+ ```
36
+
37
+ `pgit` discovers this file by walking up from the current directory — so running `pgit status` anywhere inside your repos root just works.
38
+
39
+ ### Remote overrides
40
+
41
+ If a repo is cloned to a local path that differs from its remote path, use the `remote` key:
42
+
43
+ ```yaml
44
+ github:
45
+ my-local-name:
46
+ project: {remote: "upstream-org/project"}
47
+ ```
48
+
49
+ ## push modes
50
+
51
+ ```bash
52
+ pgit push # prompt for a message per repo (default)
53
+ pgit push --all # one message for all dirty repos
54
+ pgit push --wip # auto-commit with "WIP <timestamp>"
55
+ ```
56
+
57
+ ## repos — drift detection
58
+
59
+ `pgit repos` compares what's actually on disk against your `.pgit` config and reports:
60
+
61
+ - **Remote mismatch** — repo is at the right path but the git remote differs (e.g. SSH vs HTTPS)
62
+ - **Wrong location** — remote is tracked in `.pgit` but cloned to a different directory
63
+ - **Untracked** — repo on disk not referenced in `.pgit` at all
64
+ - **Missing** — in `.pgit` but not yet cloned (run `pgit sync`)
65
+
66
+ It then offers to update `.pgit` or rename directories interactively.
67
+
68
+ ## Migrating from repos.yaml
69
+
70
+ If you were using the `sync.sh` predecessor, rename your `repos.yaml` to `.pgit` — the format is identical.
71
+
72
+ ## Requirements
73
+
74
+ - Python 3.8+
75
+ - git
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "polygit"
7
+ version = "0.1.0"
8
+ description = "Manage multiple git repos from a single .pgit config file."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE.txt" }
11
+ authors = [{ name = "Gary Schaetz", email = "gary@schaetzkc.com" }]
12
+ requires-python = ">=3.8"
13
+ keywords = ["git", "repos", "multi-repo", "polygit", "pgit", "devtools"]
14
+ dependencies = [
15
+ "pyyaml>=6.0",
16
+ ]
17
+
18
+ [project.urls]
19
+ Repository = "https://gitlab.com/gary.schaetz/public/polygit"
20
+
21
+ [project.scripts]
22
+ pgit = "polygit.cli:main"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/polygit"]
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=8.0",
30
+ ]
File without changes
@@ -0,0 +1,57 @@
1
+ import sys
2
+ import argparse
3
+ from .config import find_config, load_config
4
+ from .repo import cmd_sync, cmd_status, cmd_pull, cmd_push
5
+ from .repos import cmd_repos
6
+
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(
10
+ prog="pgit",
11
+ description="Manage multiple git repos defined in a .pgit config file."
12
+ )
13
+ sub = parser.add_subparsers(dest="command", metavar="command")
14
+
15
+ sub.add_parser("sync", help="Clone missing repos and fetch all")
16
+ sub.add_parser("status", help="Show repos with uncommitted changes")
17
+ sub.add_parser("pull", help="Pull all repos, auto-stashing local changes")
18
+
19
+ push_p = sub.add_parser("push", help="Commit and push dirty repos")
20
+ mode_g = push_p.add_mutually_exclusive_group()
21
+ mode_g.add_argument("--interactive", dest="push_mode", action="store_const",
22
+ const="interactive", help="Prompt per repo (default)")
23
+ mode_g.add_argument("--all", dest="push_mode", action="store_const",
24
+ const="all", help="One commit message for all dirty repos")
25
+ mode_g.add_argument("--wip", dest="push_mode", action="store_const",
26
+ const="wip", help='Auto-commit with "WIP <timestamp>"')
27
+ push_p.set_defaults(push_mode="interactive")
28
+
29
+ sub.add_parser("repos", help="Discover drift between .pgit and the filesystem")
30
+
31
+ args = parser.parse_args()
32
+
33
+ if not args.command:
34
+ parser.print_help()
35
+ sys.exit(0)
36
+
37
+ config_path = find_config()
38
+ if config_path is None:
39
+ print("Error: no .pgit config file found in this directory or any parent.", file=sys.stderr)
40
+ print("Create a .pgit file in your repos root directory.", file=sys.stderr)
41
+ sys.exit(1)
42
+
43
+ base = config_path.parent
44
+ config = load_config(config_path)
45
+
46
+ commands = {
47
+ "sync": lambda: cmd_sync(config, base),
48
+ "status": lambda: cmd_status(config, base),
49
+ "pull": lambda: cmd_pull(config, base),
50
+ "push": lambda: cmd_push(config, base, mode=args.push_mode),
51
+ "repos": lambda: cmd_repos(config, base, config_path),
52
+ }
53
+ commands[args.command]()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,46 @@
1
+ import yaml
2
+ from pathlib import Path
3
+
4
+ HOSTS = {
5
+ "github": "git@github.com",
6
+ "gitlab": "git@gitlab.com",
7
+ }
8
+
9
+ CONFIG_FILE = ".pgit"
10
+
11
+
12
+ def find_config(start=None):
13
+ """Walk up from start (default: cwd) to find a .pgit config file."""
14
+ current = Path(start or Path.cwd())
15
+ for path in [current] + list(current.parents):
16
+ candidate = path / CONFIG_FILE
17
+ if candidate.exists():
18
+ return candidate
19
+ return None
20
+
21
+
22
+ def load_config(path):
23
+ """Load and return the .pgit YAML config."""
24
+ with open(path) as f:
25
+ return yaml.safe_load(f) or {}
26
+
27
+
28
+ def write_config(data, path):
29
+ """Write .pgit config using only stdlib — preserves null and inline-remote styles."""
30
+ def _lines(node, indent):
31
+ prefix = " " * indent
32
+ out = []
33
+ for key, value in node.items():
34
+ if value is None:
35
+ out.append(f"{prefix}{key}:")
36
+ elif isinstance(value, dict) and set(value.keys()) == {"remote"}:
37
+ out.append(f'{prefix}{key}: {{remote: "{value["remote"]}"}}' )
38
+ elif isinstance(value, dict):
39
+ out.append(f"{prefix}{key}:")
40
+ out.extend(_lines(value, indent + 1))
41
+ else:
42
+ out.append(f"{prefix}{key}: {value}")
43
+ return out
44
+
45
+ with open(path, "w") as f:
46
+ f.write("\n".join(_lines(data, 0)) + "\n")
@@ -0,0 +1,151 @@
1
+ import subprocess
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from .config import HOSTS
5
+
6
+
7
+ def walk(node, prefix):
8
+ """Yield (local_path, remote_path) for every repo in the tree."""
9
+ if node is None:
10
+ yield prefix, prefix
11
+ elif isinstance(node, dict) and set(node.keys()) == {"remote"}:
12
+ yield prefix, node["remote"]
13
+ elif isinstance(node, dict):
14
+ for key, value in node.items():
15
+ yield from walk(value, f"{prefix}/{key}")
16
+
17
+
18
+ def each_repo(config, base):
19
+ """Yield (platform, local_path, remote_path, dir) for all configured repos."""
20
+ for platform, namespaces in config.items():
21
+ if platform not in HOSTS:
22
+ continue
23
+ for ns_key, ns_value in namespaces.items():
24
+ for local_path, remote_path in walk(ns_value, ns_key):
25
+ yield platform, local_path, remote_path, base / platform / local_path
26
+
27
+
28
+ def each_existing_repo(config, base):
29
+ """Yield only repos that are already cloned locally."""
30
+ for platform, local_path, remote_path, d in each_repo(config, base):
31
+ if (d / ".git").exists():
32
+ yield platform, local_path, remote_path, d
33
+
34
+
35
+ def is_dirty(d):
36
+ result = subprocess.run(
37
+ ["git", "-C", str(d), "status", "--porcelain"],
38
+ capture_output=True, text=True
39
+ )
40
+ return bool(result.stdout.strip())
41
+
42
+
43
+ def _commit_and_push(d, label, msg):
44
+ print(f"Committing {label}...")
45
+ subprocess.run(["git", "-C", str(d), "add", "-A"])
46
+ subprocess.run(["git", "-C", str(d), "commit", "-m", msg])
47
+ result = subprocess.run(["git", "-C", str(d), "push"], capture_output=True, text=True)
48
+ if result.returncode != 0:
49
+ print(f" Push failed: {result.stderr.strip()}")
50
+ else:
51
+ print(f" Pushed.")
52
+
53
+
54
+ def cmd_sync(config, base):
55
+ for platform, local_path, remote_path, d in each_repo(config, base):
56
+ url = f"{HOSTS[platform]}:{remote_path}.git"
57
+ if (d / ".git").exists():
58
+ print(f"Fetching {platform}/{local_path}...")
59
+ subprocess.run(["git", "-C", str(d), "fetch", "--all", "-q"])
60
+ else:
61
+ print(f"Cloning {platform}/{local_path}...")
62
+ d.parent.mkdir(parents=True, exist_ok=True)
63
+ subprocess.run(["git", "clone", url, str(d)])
64
+
65
+
66
+ def cmd_status(config, base):
67
+ found_dirty = False
68
+ for platform, local_path, _, d in each_existing_repo(config, base):
69
+ result = subprocess.run(
70
+ ["git", "-C", str(d), "status", "--short"],
71
+ capture_output=True, text=True
72
+ )
73
+ if result.stdout.strip():
74
+ found_dirty = True
75
+ print(f"\n{platform}/{local_path}:")
76
+ for line in result.stdout.strip().splitlines():
77
+ print(f" {line}")
78
+ if not found_dirty:
79
+ print("All repos are clean.")
80
+
81
+
82
+ def cmd_pull(config, base):
83
+ for platform, local_path, _, d in each_existing_repo(config, base):
84
+ label = f"{platform}/{local_path}"
85
+ stashed = False
86
+ if is_dirty(d):
87
+ print(f"Stashing {label}...")
88
+ subprocess.run(["git", "-C", str(d), "stash"], capture_output=True)
89
+ stashed = True
90
+ print(f"Pulling {label}...")
91
+ result = subprocess.run(
92
+ ["git", "-C", str(d), "pull", "--rebase"],
93
+ capture_output=True, text=True
94
+ )
95
+ if result.returncode != 0:
96
+ print(f" Error: {result.stderr.strip()}")
97
+ elif result.stdout.strip() and result.stdout.strip() != "Already up to date.":
98
+ print(f" {result.stdout.strip()}")
99
+ if stashed:
100
+ pop = subprocess.run(
101
+ ["git", "-C", str(d), "stash", "pop"],
102
+ capture_output=True, text=True
103
+ )
104
+ if pop.returncode != 0:
105
+ print(f" Warning: stash pop had conflicts in {label}")
106
+ else:
107
+ print(f" Stash restored.")
108
+
109
+
110
+ def cmd_push(config, base, mode="interactive"):
111
+ dirty = [
112
+ (platform, local_path, d)
113
+ for platform, local_path, _, d in each_existing_repo(config, base)
114
+ if is_dirty(d)
115
+ ]
116
+
117
+ if not dirty:
118
+ print("No dirty repos to commit.")
119
+ return
120
+
121
+ if mode == "wip":
122
+ msg = f"WIP {datetime.now().strftime('%Y-%m-%d %H:%M')}"
123
+ for platform, local_path, d in dirty:
124
+ _commit_and_push(d, f"{platform}/{local_path}", msg)
125
+
126
+ elif mode == "all":
127
+ print("Dirty repos:")
128
+ for platform, local_path, _ in dirty:
129
+ print(f" {platform}/{local_path}")
130
+ msg = input("\nCommit message for all repos (blank to abort): ").strip()
131
+ if not msg:
132
+ print("Aborted.")
133
+ return
134
+ for platform, local_path, d in dirty:
135
+ _commit_and_push(d, f"{platform}/{local_path}", msg)
136
+
137
+ else: # interactive
138
+ for platform, local_path, d in dirty:
139
+ label = f"{platform}/{local_path}"
140
+ status = subprocess.run(
141
+ ["git", "-C", str(d), "status", "--short"],
142
+ capture_output=True, text=True
143
+ ).stdout.strip()
144
+ print(f"\n{label}:")
145
+ for line in status.splitlines():
146
+ print(f" {line}")
147
+ msg = input(" Commit message (blank to skip): ").strip()
148
+ if not msg:
149
+ print(" Skipped.")
150
+ continue
151
+ _commit_and_push(d, label, msg)
@@ -0,0 +1,204 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+ from .config import HOSTS, write_config
4
+ from .repo import walk
5
+
6
+
7
+ def parse_remote_url(url):
8
+ """Parse a git remote URL into (platform, remote_path).
9
+
10
+ git@github.com:org/repo.git -> (github, org/repo)
11
+ git@gitlab.com:ns/sub/repo.git -> (gitlab, ns/sub/repo)
12
+ https://github.com/org/repo.git -> (github, org/repo)
13
+ """
14
+ if not url:
15
+ return None, None
16
+ url = url.strip()
17
+ if url.endswith(".git"):
18
+ url = url[:-4]
19
+ if url.startswith("git@"):
20
+ host, path = url[4:].split(":", 1)
21
+ elif "://" in url:
22
+ rest = url.split("://", 1)[1]
23
+ host, path = rest.split("/", 1)
24
+ else:
25
+ return None, None
26
+ platform = "github" if "github" in host else ("gitlab" if "gitlab" in host else None)
27
+ return platform, path
28
+
29
+
30
+ def del_nested(d, keys):
31
+ """Delete d[k0][k1]...[kn], pruning empty parent dicts."""
32
+ if not keys or keys[0] not in d:
33
+ return
34
+ if len(keys) == 1:
35
+ del d[keys[0]]
36
+ else:
37
+ del_nested(d[keys[0]], keys[1:])
38
+ if isinstance(d[keys[0]], dict) and not d[keys[0]]:
39
+ del d[keys[0]]
40
+
41
+
42
+ def _add_to_config(config, fs_platform, local_path, remote_url):
43
+ """Insert a repo entry into the config data structure."""
44
+ local_parts = local_path.split("/")
45
+ local_name = local_parts[-1]
46
+ _, remote_path = parse_remote_url(remote_url)
47
+ platform_data = config.setdefault(fs_platform, {})
48
+
49
+ if remote_path:
50
+ r_parts = remote_path.split("/")
51
+ r_name = r_parts[-1]
52
+ r_ns = "/".join(r_parts[:-1])
53
+ ns_data = platform_data.setdefault(r_ns, {})
54
+ if local_path == f"{r_ns}/{r_name}":
55
+ ns_data[r_name] = None
56
+ else:
57
+ sub = local_path[len(r_ns)+1:] if local_path.startswith(r_ns + "/") else local_name
58
+ node = ns_data
59
+ for part in sub.split("/")[:-1]:
60
+ node = node.setdefault(part, {})
61
+ node[sub.split("/")[-1]] = {"remote": remote_path}
62
+ else:
63
+ local_ns = "/".join(local_parts[:-1])
64
+ platform_data.setdefault(local_ns, {})[local_name] = None
65
+
66
+
67
+ def cmd_repos(config, base, config_path):
68
+ """Diff .pgit config against the filesystem and offer to update."""
69
+
70
+ # ── 1. Scan filesystem ──────────────────────────────────────────────────
71
+ local_repos = {} # "platform/local_path" -> (dir, remote_url)
72
+ for platform in ["github", "gitlab"]:
73
+ platform_dir = base / platform
74
+ if not platform_dir.exists():
75
+ continue
76
+ for git_marker in sorted(platform_dir.rglob(".git")):
77
+ if not git_marker.is_dir():
78
+ continue
79
+ repo_dir = git_marker.parent
80
+ rel = repo_dir.relative_to(platform_dir)
81
+ parts = rel.parts
82
+ # Skip repos nested inside another git repo
83
+ if any((platform_dir.joinpath(*parts[:i]) / ".git").is_dir()
84
+ for i in range(1, len(parts))):
85
+ continue
86
+ result = subprocess.run(
87
+ ["git", "-C", str(repo_dir), "remote", "get-url", "origin"],
88
+ capture_output=True, text=True
89
+ )
90
+ remote_url = result.stdout.strip() if result.returncode == 0 else None
91
+ local_repos[f"{platform}/{rel}"] = (repo_dir, remote_url)
92
+
93
+ # ── 2. Build tracked indices ─────────────────────────────────────────────
94
+ tracked_by_local = {} # "platform/local_path" -> (remote_path, expected_url)
95
+ tracked_by_remote = {} # "platform/remote_path" -> "platform/local_path"
96
+
97
+ for platform, namespaces in config.items():
98
+ host = HOSTS.get(platform, "")
99
+ for ns_key, ns_value in namespaces.items():
100
+ for local_path, remote_path in walk(ns_value, ns_key):
101
+ lkey = f"{platform}/{local_path}"
102
+ rkey = f"{platform}/{remote_path}"
103
+ tracked_by_local[lkey] = (remote_path, f"{host}:{remote_path}.git")
104
+ tracked_by_remote[rkey] = lkey
105
+
106
+ # ── 3. Categorize ────────────────────────────────────────────────────────
107
+ untracked = {} # key -> (dir, url)
108
+ wrong_location = {} # key -> (dir, url, config_key)
109
+ remote_mismatch = {} # key -> (expected_url, actual_url)
110
+ missing = {k for k in tracked_by_local if k not in local_repos}
111
+
112
+ for key, (repo_dir, remote_url) in local_repos.items():
113
+ fs_platform = key.split("/")[0]
114
+ _, remote_path = parse_remote_url(remote_url)
115
+ rkey = f"{fs_platform}/{remote_path}" if remote_path else None
116
+
117
+ if key in tracked_by_local:
118
+ _, expected_url = tracked_by_local[key]
119
+ if remote_url and remote_url != expected_url:
120
+ remote_mismatch[key] = (expected_url, remote_url)
121
+ elif rkey and rkey in tracked_by_remote:
122
+ wrong_location[key] = (repo_dir, remote_url, tracked_by_remote[rkey])
123
+ else:
124
+ untracked[key] = (repo_dir, remote_url)
125
+
126
+ # ── 4. Report ─────────────────────────────────────────────────────────────
127
+ if not any([untracked, wrong_location, remote_mismatch, missing]):
128
+ print(".pgit is in sync with the filesystem.")
129
+ return
130
+
131
+ if remote_mismatch:
132
+ print("\nRemote mismatch (local path matches config but git remote differs):")
133
+ for key, (expected, actual) in sorted(remote_mismatch.items()):
134
+ print(f" ~ {key}")
135
+ print(f" config: {expected}")
136
+ print(f" actual: {actual}")
137
+
138
+ if wrong_location:
139
+ print("\nCloned to a different path than .pgit expects:")
140
+ for key, (_, url, config_key) in sorted(wrong_location.items()):
141
+ actual_dir = base / Path(*key.split("/"))
142
+ expected_dir = base / Path(*config_key.split("/"))
143
+ is_rename = actual_dir.parent == expected_dir.parent
144
+ hint = f" (rename: {actual_dir.name} → {expected_dir.name})" if is_rename else ""
145
+ print(f" ? {key}")
146
+ print(f" remote: {url}")
147
+ print(f" expected: {config_key}{hint}")
148
+
149
+ if untracked:
150
+ print("\nOn disk but not in .pgit:")
151
+ for key, (_, url) in sorted(untracked.items()):
152
+ print(f" + {key} ({url or 'no remote'})")
153
+
154
+ if missing:
155
+ print("\nIn .pgit but not cloned locally (run: pgit sync):")
156
+ for key in sorted(missing):
157
+ print(f" - {key}")
158
+
159
+ # ── 5. Add untracked ──────────────────────────────────────────────────────
160
+ if untracked:
161
+ print()
162
+ if input("Add untracked repos to .pgit? [y/N] ").strip().lower() == "y":
163
+ for key, (_, remote_url) in sorted(untracked.items()):
164
+ fs_platform, local_path = key.split("/", 1)
165
+ _add_to_config(config, fs_platform, local_path, remote_url)
166
+ print(f" Added {key}")
167
+ write_config(config, config_path)
168
+ print(f"\nUpdated {config_path}")
169
+
170
+ # ── 6. Fix wrong-location ─────────────────────────────────────────────────
171
+ if wrong_location:
172
+ for key, (_, remote_url, config_key) in sorted(wrong_location.items()):
173
+ actual_dir = base / Path(*key.split("/"))
174
+ expected_dir = base / Path(*config_key.split("/"))
175
+ is_rename = actual_dir.parent == expected_dir.parent
176
+ print()
177
+ if is_rename:
178
+ if input(f"Rename {actual_dir.name} → {expected_dir.name}? [y/N] ").strip().lower() == "y":
179
+ actual_dir.rename(expected_dir)
180
+ print(f" Renamed.")
181
+ else:
182
+ if input(f"Add {key} to .pgit with remote override? [y/N] ").strip().lower() == "y":
183
+ fs_platform, local_path = key.split("/", 1)
184
+ _add_to_config(config, fs_platform, local_path, remote_url)
185
+ print(f" Added {key}")
186
+ write_config(config, config_path)
187
+ print(f"\nUpdated {config_path}")
188
+
189
+ # ── 7. Remove missing ─────────────────────────────────────────────────────
190
+ if missing:
191
+ print()
192
+ if input("Remove missing repos from .pgit? [y/N] ").strip().lower() == "y":
193
+ for key in sorted(missing):
194
+ fs_platform, local_path = key.split("/", 1)
195
+ for ns_key in list(config.get(fs_platform, {}).keys()):
196
+ if local_path.startswith(ns_key + "/"):
197
+ sub = local_path[len(ns_key)+1:].split("/")
198
+ del_nested(config[fs_platform][ns_key], sub)
199
+ if not config[fs_platform][ns_key]:
200
+ del config[fs_platform][ns_key]
201
+ break
202
+ print(f" Removed {key}")
203
+ write_config(config, config_path)
204
+ print(f"\nUpdated {config_path}")
File without changes
@@ -0,0 +1,97 @@
1
+ import tempfile
2
+ import pytest
3
+ from pathlib import Path
4
+ from polygit.config import find_config, load_config, write_config
5
+ from polygit.repos import parse_remote_url, del_nested
6
+ from polygit.repo import walk
7
+
8
+
9
+ # ── parse_remote_url ──────────────────────────────────────────────────────────
10
+
11
+ def test_parse_github_ssh():
12
+ platform, path = parse_remote_url("git@github.com:gschaetz/mousehat.git")
13
+ assert platform == "github"
14
+ assert path == "gschaetz/mousehat"
15
+
16
+ def test_parse_gitlab_ssh_nested():
17
+ platform, path = parse_remote_url("git@gitlab.com:gary.schaetz/private/dev-setup.git")
18
+ assert platform == "gitlab"
19
+ assert path == "gary.schaetz/private/dev-setup"
20
+
21
+ def test_parse_https():
22
+ platform, path = parse_remote_url("https://github.com/openclaw/openclaw")
23
+ assert platform == "github"
24
+ assert path == "openclaw/openclaw"
25
+
26
+ def test_parse_none():
27
+ platform, path = parse_remote_url(None)
28
+ assert platform is None
29
+ assert path is None
30
+
31
+
32
+ # ── walk ──────────────────────────────────────────────────────────────────────
33
+
34
+ def test_walk_null():
35
+ results = list(walk(None, "ns/repo"))
36
+ assert results == [("ns/repo", "ns/repo")]
37
+
38
+ def test_walk_remote_override():
39
+ results = list(walk({"remote": "other/repo"}, "ns/local"))
40
+ assert results == [("ns/local", "other/repo")]
41
+
42
+ def test_walk_nested():
43
+ node = {"a": None, "b": {"c": None}}
44
+ results = list(walk(node, "ns"))
45
+ assert ("ns/a", "ns/a") in results
46
+ assert ("ns/b/c", "ns/b/c") in results
47
+
48
+
49
+ # ── del_nested ────────────────────────────────────────────────────────────────
50
+
51
+ def test_del_nested_simple():
52
+ d = {"a": {"b": None}}
53
+ del_nested(d, ["a", "b"])
54
+ assert d == {}
55
+
56
+ def test_del_nested_preserves_siblings():
57
+ d = {"a": {"b": None, "c": None}}
58
+ del_nested(d, ["a", "b"])
59
+ assert d == {"a": {"c": None}}
60
+
61
+
62
+ # ── find_config / write_config / load_config ──────────────────────────────────
63
+
64
+ def test_find_config_in_cwd():
65
+ with tempfile.TemporaryDirectory() as tmp:
66
+ config_path = Path(tmp) / ".pgit"
67
+ config_path.write_text("github:\n org:\n repo:\n")
68
+ found = find_config(start=tmp)
69
+ assert found == config_path
70
+
71
+ def test_find_config_walks_up():
72
+ with tempfile.TemporaryDirectory() as tmp:
73
+ config_path = Path(tmp) / ".pgit"
74
+ config_path.write_text("github:\n org:\n repo:\n")
75
+ subdir = Path(tmp) / "sub" / "dir"
76
+ subdir.mkdir(parents=True)
77
+ found = find_config(start=subdir)
78
+ assert found == config_path
79
+
80
+ def test_find_config_not_found():
81
+ with tempfile.TemporaryDirectory() as tmp:
82
+ found = find_config(start=tmp)
83
+ assert found is None
84
+
85
+ def test_write_and_load_roundtrip():
86
+ with tempfile.TemporaryDirectory() as tmp:
87
+ path = Path(tmp) / ".pgit"
88
+ data = {
89
+ "github": {
90
+ "gschaetz": {"mousehat": None, "dotfiles": None},
91
+ "community": {"openclaw": {"remote": "openclaw/openclaw"}},
92
+ }
93
+ }
94
+ write_config(data, path)
95
+ loaded = load_config(path)
96
+ assert loaded["github"]["gschaetz"]["mousehat"] is None
97
+ assert loaded["github"]["community"]["openclaw"] == {"remote": "openclaw/openclaw"}
polygit-0.1.0/uv.lock ADDED
@@ -0,0 +1,340 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.8"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.10'",
6
+ "python_full_version == '3.9.*'",
7
+ "python_full_version < '3.9'",
8
+ ]
9
+
10
+ [[package]]
11
+ name = "colorama"
12
+ version = "0.4.6"
13
+ source = { registry = "https://pypi.org/simple" }
14
+ 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" }
15
+ wheels = [
16
+ { 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" },
17
+ ]
18
+
19
+ [[package]]
20
+ name = "exceptiongroup"
21
+ version = "1.3.1"
22
+ source = { registry = "https://pypi.org/simple" }
23
+ dependencies = [
24
+ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9' or python_full_version >= '3.11'" },
25
+ { name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
26
+ ]
27
+ 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" }
28
+ wheels = [
29
+ { 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" },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "iniconfig"
34
+ version = "2.1.0"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ resolution-markers = [
37
+ "python_full_version == '3.9.*'",
38
+ "python_full_version < '3.9'",
39
+ ]
40
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
41
+ wheels = [
42
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
43
+ ]
44
+
45
+ [[package]]
46
+ name = "iniconfig"
47
+ version = "2.3.0"
48
+ source = { registry = "https://pypi.org/simple" }
49
+ resolution-markers = [
50
+ "python_full_version >= '3.10'",
51
+ ]
52
+ 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" }
53
+ wheels = [
54
+ { 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" },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "packaging"
59
+ version = "26.2"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
62
+ wheels = [
63
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
64
+ ]
65
+
66
+ [[package]]
67
+ name = "pluggy"
68
+ version = "1.5.0"
69
+ source = { registry = "https://pypi.org/simple" }
70
+ resolution-markers = [
71
+ "python_full_version < '3.9'",
72
+ ]
73
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
74
+ wheels = [
75
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "pluggy"
80
+ version = "1.6.0"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ resolution-markers = [
83
+ "python_full_version >= '3.10'",
84
+ "python_full_version == '3.9.*'",
85
+ ]
86
+ 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" }
87
+ wheels = [
88
+ { 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" },
89
+ ]
90
+
91
+ [[package]]
92
+ name = "polygit"
93
+ version = "0.1.0"
94
+ source = { editable = "." }
95
+ dependencies = [
96
+ { name = "pyyaml" },
97
+ ]
98
+
99
+ [package.dev-dependencies]
100
+ dev = [
101
+ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
102
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
103
+ { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
104
+ ]
105
+
106
+ [package.metadata]
107
+ requires-dist = [{ name = "pyyaml", specifier = ">=6.0" }]
108
+
109
+ [package.metadata.requires-dev]
110
+ dev = [{ name = "pytest", specifier = ">=8.0" }]
111
+
112
+ [[package]]
113
+ name = "pygments"
114
+ version = "2.20.0"
115
+ source = { registry = "https://pypi.org/simple" }
116
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
117
+ wheels = [
118
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
119
+ ]
120
+
121
+ [[package]]
122
+ name = "pytest"
123
+ version = "8.3.5"
124
+ source = { registry = "https://pypi.org/simple" }
125
+ resolution-markers = [
126
+ "python_full_version < '3.9'",
127
+ ]
128
+ dependencies = [
129
+ { name = "colorama", marker = "sys_platform == 'win32'" },
130
+ { name = "exceptiongroup" },
131
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } },
132
+ { name = "packaging" },
133
+ { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" } },
134
+ { name = "tomli" },
135
+ ]
136
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
137
+ wheels = [
138
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
139
+ ]
140
+
141
+ [[package]]
142
+ name = "pytest"
143
+ version = "8.4.2"
144
+ source = { registry = "https://pypi.org/simple" }
145
+ resolution-markers = [
146
+ "python_full_version == '3.9.*'",
147
+ ]
148
+ dependencies = [
149
+ { name = "colorama", marker = "sys_platform == 'win32'" },
150
+ { name = "exceptiongroup" },
151
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" } },
152
+ { name = "packaging" },
153
+ { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" } },
154
+ { name = "pygments" },
155
+ { name = "tomli" },
156
+ ]
157
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
158
+ wheels = [
159
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
160
+ ]
161
+
162
+ [[package]]
163
+ name = "pytest"
164
+ version = "9.1.1"
165
+ source = { registry = "https://pypi.org/simple" }
166
+ resolution-markers = [
167
+ "python_full_version >= '3.10'",
168
+ ]
169
+ dependencies = [
170
+ { name = "colorama", marker = "sys_platform == 'win32'" },
171
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
172
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" } },
173
+ { name = "packaging" },
174
+ { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" } },
175
+ { name = "pygments" },
176
+ { name = "tomli", marker = "python_full_version < '3.11'" },
177
+ ]
178
+ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
179
+ wheels = [
180
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
181
+ ]
182
+
183
+ [[package]]
184
+ name = "pyyaml"
185
+ version = "6.0.3"
186
+ source = { registry = "https://pypi.org/simple" }
187
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
188
+ wheels = [
189
+ { url = "https://files.pythonhosted.org/packages/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" },
190
+ { url = "https://files.pythonhosted.org/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" },
191
+ { url = "https://files.pythonhosted.org/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" },
192
+ { url = "https://files.pythonhosted.org/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" },
193
+ { url = "https://files.pythonhosted.org/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" },
194
+ { url = "https://files.pythonhosted.org/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" },
195
+ { url = "https://files.pythonhosted.org/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" },
196
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
197
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
198
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
199
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
200
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
201
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
202
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
203
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
204
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
205
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
206
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
207
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
208
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
209
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
210
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
211
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
212
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
213
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
214
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
215
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
216
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
217
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
218
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
219
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
220
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
221
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
222
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
223
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
224
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
225
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
226
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
227
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
228
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
229
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
230
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
231
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
232
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
233
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
234
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
235
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
236
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
237
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
238
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
239
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
240
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
241
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
242
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
243
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
244
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
245
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
246
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
247
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
248
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
249
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
250
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
251
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
252
+ { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" },
253
+ { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" },
254
+ { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" },
255
+ { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" },
256
+ { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" },
257
+ { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" },
258
+ { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" },
259
+ { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" },
260
+ { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
261
+ ]
262
+
263
+ [[package]]
264
+ name = "tomli"
265
+ version = "2.4.1"
266
+ source = { registry = "https://pypi.org/simple" }
267
+ sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
268
+ wheels = [
269
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
270
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
271
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
272
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
273
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
274
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
275
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
276
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
277
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
278
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
279
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
280
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
281
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
282
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
283
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
284
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
285
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
286
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
287
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
288
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
289
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
290
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
291
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
292
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
293
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
294
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
295
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
296
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
297
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
298
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
299
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
300
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
301
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
302
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
303
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
304
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
305
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
306
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
307
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
308
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
309
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
310
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
311
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
312
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
313
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
314
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
315
+ ]
316
+
317
+ [[package]]
318
+ name = "typing-extensions"
319
+ version = "4.13.2"
320
+ source = { registry = "https://pypi.org/simple" }
321
+ resolution-markers = [
322
+ "python_full_version < '3.9'",
323
+ ]
324
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
325
+ wheels = [
326
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
327
+ ]
328
+
329
+ [[package]]
330
+ name = "typing-extensions"
331
+ version = "4.16.0"
332
+ source = { registry = "https://pypi.org/simple" }
333
+ resolution-markers = [
334
+ "python_full_version >= '3.10'",
335
+ "python_full_version == '3.9.*'",
336
+ ]
337
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
338
+ wheels = [
339
+ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
340
+ ]