pyrct2 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,24 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: release
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v4
19
+
20
+ - name: Build package
21
+ run: uv build
22
+
23
+ - name: Publish to PyPI
24
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,2 @@
1
+ .idea/
2
+ .venv/
pyrct2-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrct2
3
+ Version: 0.1.0
4
+ Summary: Interact with OpenRCT2 from python!
5
+ Author-email: Mauk Muller <mauk@elnino.tech>
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: pydantic>=2.10
9
+ Description-Content-Type: text/markdown
10
+
11
+ # pyrct2
12
+
13
+ Python client for OpenRCT2. Launches the game in headless mode, connects to the [openrct2-bridge](https://github.com/MaukWM/openrct2-bridge) plugin over TCP, and sends game actions.
14
+
15
+ ## Setup
16
+
17
+ ```bash
18
+ pip install pyrct2
19
+ pyrct2 setup
20
+ ```
21
+
22
+ `setup` finds your OpenRCT2 installation and installs the bridge plugin. Requires [OpenRCT2](https://openrct2.io/) to be installed.
23
+
24
+ ## Usage
25
+
26
+ ```python
27
+ from pyrct2.client import RCT2
28
+
29
+ # Launch a headless game and send commands
30
+ with RCT2.launch("path/to/scenario.SC6") as game:
31
+ game.get_status()
32
+ game.execute("ridecreate", {"rideType": 1, "rideObject": 0, "colour1": 5, "colour2": 10})
33
+ game.advance_ticks(100)
34
+
35
+ # Or connect to an already-running instance
36
+ with RCT2.connect() as game:
37
+ game.get_status()
38
+ ```
pyrct2-0.1.0/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # pyrct2
2
+
3
+ Python client for OpenRCT2. Launches the game in headless mode, connects to the [openrct2-bridge](https://github.com/MaukWM/openrct2-bridge) plugin over TCP, and sends game actions.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ pip install pyrct2
9
+ pyrct2 setup
10
+ ```
11
+
12
+ `setup` finds your OpenRCT2 installation and installs the bridge plugin. Requires [OpenRCT2](https://openrct2.io/) to be installed.
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from pyrct2.client import RCT2
18
+
19
+ # Launch a headless game and send commands
20
+ with RCT2.launch("path/to/scenario.SC6") as game:
21
+ game.get_status()
22
+ game.execute("ridecreate", {"rideType": 1, "rideObject": 0, "colour1": 5, "colour2": 10})
23
+ game.advance_ticks(100)
24
+
25
+ # Or connect to an already-running instance
26
+ with RCT2.connect() as game:
27
+ game.get_status()
28
+ ```
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "pyrct2"
3
+ version = "0.1.0"
4
+ description = "Interact with OpenRCT2 from python!"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Mauk Muller", email = "mauk@elnino.tech" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "pydantic>=2.10",
12
+ "click>=8.1",
13
+ ]
14
+
15
+ [project.scripts]
16
+ pyrct2 = "pyrct2.cli:main"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "pytest>=8.0",
21
+ "ruff>=0.9",
22
+ ]
23
+
24
+ [tool.uv]
25
+ package = true
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
File without changes
@@ -0,0 +1,42 @@
1
+ """CLI entry point for pyrct2."""
2
+
3
+ import urllib.request
4
+
5
+ import click
6
+
7
+ from pyrct2.paths import find_openrct2_binary, get_plugin_dir, save_config, validate_openrct2_binary
8
+
9
+ BRIDGE_VERSION = "v1.0.0"
10
+ BRIDGE_FILENAME = "openrct2-bridge.js"
11
+ BRIDGE_DOWNLOAD_URL = (
12
+ f"https://github.com/MaukWM/openrct2-bridge/releases/download/{BRIDGE_VERSION}/{BRIDGE_FILENAME}"
13
+ )
14
+
15
+
16
+ @click.group()
17
+ def main() -> None:
18
+ """pyrct2 — Python client for OpenRCT2."""
19
+
20
+
21
+ @main.command()
22
+ def setup() -> None:
23
+ """Download and install the openrct2-bridge plugin."""
24
+ # Find and validate OpenRCT2 binary
25
+ click.echo("Searching for OpenRCT2...")
26
+ binary = find_openrct2_binary()
27
+ if binary is None:
28
+ click.echo("Could not find OpenRCT2. Please install it or set PYRCT2_OPENRCT2_PATH.")
29
+ return
30
+
31
+ version = validate_openrct2_binary(binary)
32
+ save_config({"openrct2_path": str(binary)})
33
+ click.echo(f"Found {version} at {binary}")
34
+
35
+ # Install bridge plugin
36
+ plugin_dir = get_plugin_dir()
37
+ plugin_dir.mkdir(parents=True, exist_ok=True)
38
+ dest = plugin_dir / BRIDGE_FILENAME
39
+
40
+ click.echo(f"Downloading {BRIDGE_FILENAME} {BRIDGE_VERSION}...")
41
+ urllib.request.urlretrieve(BRIDGE_DOWNLOAD_URL, dest)
42
+ click.echo(f"Installed to {dest}")
@@ -0,0 +1,68 @@
1
+ """RCT2 client — main entry point for interacting with a running game."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from pyrct2.connection import Connection, DEFAULT_HOST, DEFAULT_PORT
8
+ from pyrct2.launcher import GameInstance, launch
9
+
10
+
11
+ class RCT2:
12
+ """Client for interacting with an OpenRCT2 game via the bridge plugin.
13
+
14
+ Two modes of operation:
15
+ - RCT2.launch(park_file) — spawns a headless game and owns its lifecycle
16
+ - RCT2.connect(host, port) — attaches to an already-running instance
17
+ """
18
+
19
+ def __init__(self, connection: Connection, instance: GameInstance | None = None):
20
+ self._connection = connection
21
+ self._instance = instance
22
+
23
+ @classmethod
24
+ def launch(cls, park_file: str | Path, port: int = DEFAULT_PORT) -> RCT2:
25
+ """Launch OpenRCT2 headless and return a connected client."""
26
+ instance = launch(park_file, port)
27
+ return cls(instance.connection, instance)
28
+
29
+ @classmethod
30
+ def connect(cls, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> RCT2:
31
+ """Connect to an already-running OpenRCT2 instance."""
32
+ connection = Connection(host=host, port=port)
33
+ result = connection.send("health")
34
+ if not result.get("success"):
35
+ connection.close()
36
+ raise ConnectionError("Bridge responded but health check failed")
37
+ return cls(connection)
38
+
39
+ def execute(self, endpoint: str, params: dict | None = None) -> dict:
40
+ """Send a command to the bridge and return the response."""
41
+ if self._instance is not None:
42
+ self._instance.check_alive()
43
+ return self._connection.send(endpoint, params)
44
+
45
+ def get_status(self) -> dict:
46
+ """Get current game status (paused state, date, ticks)."""
47
+ return self.execute("get_status")
48
+
49
+ def get_version(self) -> dict:
50
+ """Get bridge plugin and API version info."""
51
+ return self.execute("get_version")
52
+
53
+ def advance_ticks(self, ticks: int) -> dict:
54
+ """Advance the game by N ticks (unpause → count → re-pause)."""
55
+ return self.execute("advance_ticks", {"ticks": ticks})
56
+
57
+ def close(self) -> None:
58
+ """Shut down the connection and game process (if launched)."""
59
+ if self._instance is not None:
60
+ self._instance.stop()
61
+ else:
62
+ self._connection.close()
63
+
64
+ def __enter__(self):
65
+ return self
66
+
67
+ def __exit__(self, *args):
68
+ self.close()
@@ -0,0 +1,39 @@
1
+ """TCP connection to openrct2-bridge (NDJSON protocol on port 9090)."""
2
+
3
+ import json
4
+ import socket
5
+
6
+ DEFAULT_HOST = "127.0.0.1"
7
+ DEFAULT_PORT = 9090
8
+ DEFAULT_TIMEOUT = 10.0
9
+
10
+
11
+ class Connection:
12
+ """Single TCP connection to an openrct2-bridge plugin instance."""
13
+
14
+ def __init__(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, timeout: float = DEFAULT_TIMEOUT):
15
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
16
+ self._socket.settimeout(timeout)
17
+ self._socket.connect((host, port))
18
+ self._buffer = ""
19
+
20
+ def send(self, endpoint: str, params: dict | None = None) -> dict:
21
+ """Send a request and return the parsed response."""
22
+ msg: dict = {"endpoint": endpoint}
23
+ if params is not None:
24
+ msg["params"] = params
25
+
26
+ self._socket.sendall(json.dumps(msg).encode() + b"\n")
27
+
28
+ while "\n" not in self._buffer:
29
+ chunk = self._socket.recv(4096).decode()
30
+ if not chunk:
31
+ raise ConnectionError("Bridge closed the connection")
32
+ self._buffer += chunk
33
+
34
+ line, self._buffer = self._buffer.split("\n", 1)
35
+ return json.loads(line)
36
+
37
+ def close(self) -> None:
38
+ """Close the TCP connection."""
39
+ self._socket.close()
@@ -0,0 +1,116 @@
1
+ """Launch OpenRCT2 in headless mode and wait for plugin readiness."""
2
+
3
+ import atexit
4
+ import socket
5
+ import subprocess
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from pyrct2.connection import Connection, DEFAULT_HOST, DEFAULT_PORT
10
+ from pyrct2.paths import load_config
11
+
12
+ HEALTH_POLL_INTERVAL = 0.5
13
+ LAUNCH_TIMEOUT = 30.0
14
+
15
+
16
+ class GameInstance:
17
+ """A running OpenRCT2 process with an active bridge connection."""
18
+
19
+ def __init__(self, process: subprocess.Popen, connection: Connection):
20
+ self._process = process
21
+ self.connection = connection
22
+ atexit.register(self._cleanup)
23
+
24
+ def check_alive(self) -> None:
25
+ """Raise if the OpenRCT2 process has exited."""
26
+ _check_process(self._process)
27
+
28
+ def stop(self) -> None:
29
+ """Terminate the game process and close the connection."""
30
+ self.connection.close()
31
+ self._process.terminate()
32
+ self._process.wait(timeout=5)
33
+ atexit.unregister(self._cleanup)
34
+
35
+ def _cleanup(self) -> None:
36
+ """Safety net called on interpreter shutdown."""
37
+ try:
38
+ self._process.terminate()
39
+ self._process.wait(timeout=5)
40
+ except (OSError, subprocess.TimeoutExpired):
41
+ # Process may already be dead — nothing to do at shutdown
42
+ pass
43
+
44
+ def __enter__(self):
45
+ return self
46
+
47
+ def __exit__(self, *args):
48
+ self.stop()
49
+
50
+
51
+ def launch(park_file: str | Path, port: int = DEFAULT_PORT) -> GameInstance:
52
+ """Launch OpenRCT2 headless and return a connected GameInstance.
53
+
54
+ Blocks until the bridge plugin is ready (up to 30s).
55
+ """
56
+ park_path = Path(park_file)
57
+ if not park_path.exists():
58
+ raise FileNotFoundError(f"Park file not found: {park_path}")
59
+
60
+ config = load_config()
61
+ binary = config.get("openrct2_path")
62
+ if not binary:
63
+ raise RuntimeError("OpenRCT2 not configured. Run `pyrct2 setup` first.")
64
+
65
+ if _port_in_use(port):
66
+ raise RuntimeError(f"Port {port} already in use. Is OpenRCT2 already running?")
67
+
68
+ process = subprocess.Popen(
69
+ [binary, "host", str(park_path), "--headless"],
70
+ stdout=subprocess.DEVNULL,
71
+ stderr=subprocess.DEVNULL,
72
+ )
73
+
74
+ connection = _wait_for_bridge(port, process)
75
+ return GameInstance(process, connection)
76
+
77
+
78
+ def _check_process(process: subprocess.Popen) -> None:
79
+ """Raise if the process has exited."""
80
+ if process.poll() is not None:
81
+ raise RuntimeError(f"OpenRCT2 exited unexpectedly (exit code {process.returncode})")
82
+
83
+
84
+ def _port_in_use(port: int, host: str = DEFAULT_HOST) -> bool:
85
+ """Check if something is already listening on the given port."""
86
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
87
+ s.settimeout(1)
88
+ try:
89
+ s.connect((host, port))
90
+ s.close()
91
+ return True
92
+ except (ConnectionRefusedError, socket.timeout):
93
+ return False
94
+
95
+
96
+ def _wait_for_bridge(port: int, process: subprocess.Popen) -> Connection:
97
+ """Poll the health endpoint until the bridge responds or timeout."""
98
+ deadline = time.monotonic() + LAUNCH_TIMEOUT
99
+
100
+ while time.monotonic() < deadline:
101
+ _check_process(process)
102
+
103
+ try:
104
+ conn = Connection(port=port, timeout=5)
105
+ result = conn.send("health")
106
+ if result.get("success"):
107
+ return conn
108
+ conn.close()
109
+ except (ConnectionRefusedError, socket.timeout, OSError):
110
+ # Bridge not ready yet — retry after interval
111
+ pass
112
+
113
+ time.sleep(HEALTH_POLL_INTERVAL)
114
+
115
+ process.terminate()
116
+ raise TimeoutError(f"Bridge did not respond within {LAUNCH_TIMEOUT}s")
@@ -0,0 +1,121 @@
1
+ """OS-specific path resolution for OpenRCT2 directories."""
2
+
3
+ import json
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ CONFIG_DIR = Path.home() / ".pyrct2"
11
+ CONFIG_FILE = CONFIG_DIR / "config.json"
12
+
13
+
14
+ def get_plugin_dir() -> Path:
15
+ """Return the OpenRCT2 plugin directory for the current OS."""
16
+ system = platform.system()
17
+
18
+ if system == "Darwin":
19
+ return Path.home() / "Library" / "Application Support" / "OpenRCT2" / "plugin"
20
+ elif system == "Windows":
21
+ import ctypes.wintypes
22
+ buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
23
+ ctypes.windll.shell32.SHGetFolderPathW(None, 5, None, 0, buf) # 5 = CSIDL_PERSONAL (Documents)
24
+ return Path(buf.value) / "OpenRCT2" / "plugin"
25
+ elif system == "Linux":
26
+ config_home = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
27
+ return Path(config_home) / "OpenRCT2" / "plugin"
28
+ else:
29
+ raise RuntimeError(f"Unsupported platform: {system}")
30
+
31
+
32
+ def find_openrct2_binary() -> Path | None:
33
+ """Search for the OpenRCT2 binary on this system.
34
+
35
+ Search order:
36
+ 1. PYRCT2_OPENRCT2_PATH environment variable
37
+ 2. OS-specific discovery (Spotlight on macOS, known dirs on Windows)
38
+ 3. shutil.which() fallback (covers PATH installs on all platforms)
39
+ """
40
+ # Environment variable override
41
+ env_path = os.environ.get("PYRCT2_OPENRCT2_PATH")
42
+ if env_path:
43
+ p = Path(env_path)
44
+ if p.exists():
45
+ return p
46
+
47
+ system = platform.system()
48
+
49
+ if system == "Darwin":
50
+ found = _find_macos()
51
+ if found:
52
+ return found
53
+ elif system == "Windows":
54
+ found = _find_windows()
55
+ if found:
56
+ return found
57
+
58
+ # Fallback: check PATH (works on all platforms including Linux)
59
+ on_path = shutil.which("openrct2")
60
+ if on_path:
61
+ return Path(on_path)
62
+
63
+ return None
64
+
65
+
66
+ def _find_macos() -> Path | None:
67
+ """Use Spotlight to find OpenRCT2.app on macOS."""
68
+ try:
69
+ result = subprocess.run(
70
+ ["mdfind", "kMDItemFSName == 'OpenRCT2.app'"],
71
+ capture_output=True, text=True, timeout=5,
72
+ )
73
+ for line in result.stdout.strip().splitlines():
74
+ binary = Path(line) / "Contents" / "MacOS" / "OpenRCT2"
75
+ if binary.exists():
76
+ return binary
77
+ except (subprocess.TimeoutExpired, FileNotFoundError):
78
+ pass
79
+ return None
80
+
81
+
82
+ def _find_windows() -> Path | None:
83
+ """Check known installation directories on Windows."""
84
+ candidates = [
85
+ Path(os.environ.get("PROGRAMFILES", "C:/Program Files")) / "OpenRCT2" / "openrct2.com",
86
+ Path(os.environ.get("PROGRAMFILES(X86)", "C:/Program Files (x86)")) / "OpenRCT2" / "openrct2.com",
87
+ Path(os.environ.get("PROGRAMFILES(X86)", "C:/Program Files (x86)")) / "GOG Galaxy" / "Games"
88
+ / "RollerCoaster Tycoon 2 Triple Thrill Pack" / "OpenRCT2" / "openrct2.com",
89
+ ]
90
+ for candidate in candidates:
91
+ if candidate.exists():
92
+ return candidate
93
+ return None
94
+
95
+
96
+ def validate_openrct2_binary(path: Path) -> str:
97
+ """Verify the binary is a real OpenRCT2 install. Returns the version string."""
98
+ try:
99
+ result = subprocess.run(
100
+ [str(path), "--version"],
101
+ capture_output=True, text=True, timeout=10,
102
+ )
103
+ first_line = result.stdout.strip().splitlines()[0]
104
+ if not first_line.startswith("OpenRCT2"):
105
+ raise RuntimeError(f"Not an OpenRCT2 binary: {path}")
106
+ return first_line
107
+ except (subprocess.TimeoutExpired, FileNotFoundError, IndexError) as e:
108
+ raise RuntimeError(f"Failed to validate OpenRCT2 binary at {path}: {e}")
109
+
110
+
111
+ def load_config() -> dict:
112
+ """Load pyrct2 config from ~/.pyrct2/config.json."""
113
+ if CONFIG_FILE.exists():
114
+ return json.loads(CONFIG_FILE.read_text())
115
+ return {}
116
+
117
+
118
+ def save_config(data: dict) -> None:
119
+ """Save pyrct2 config to ~/.pyrct2/config.json."""
120
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
121
+ CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n")
pyrct2-0.1.0/uv.lock ADDED
@@ -0,0 +1,226 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "annotated-types"
7
+ version = "0.7.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "click"
16
+ version = "8.3.1"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "colorama", marker = "sys_platform == 'win32'" },
20
+ ]
21
+ sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
22
+ wheels = [
23
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
24
+ ]
25
+
26
+ [[package]]
27
+ name = "colorama"
28
+ version = "0.4.6"
29
+ source = { registry = "https://pypi.org/simple" }
30
+ 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" }
31
+ wheels = [
32
+ { 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" },
33
+ ]
34
+
35
+ [[package]]
36
+ name = "iniconfig"
37
+ version = "2.3.0"
38
+ source = { registry = "https://pypi.org/simple" }
39
+ 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" }
40
+ wheels = [
41
+ { 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" },
42
+ ]
43
+
44
+ [[package]]
45
+ name = "packaging"
46
+ version = "26.0"
47
+ source = { registry = "https://pypi.org/simple" }
48
+ sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
49
+ wheels = [
50
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
51
+ ]
52
+
53
+ [[package]]
54
+ name = "pluggy"
55
+ version = "1.6.0"
56
+ source = { registry = "https://pypi.org/simple" }
57
+ 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" }
58
+ wheels = [
59
+ { 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" },
60
+ ]
61
+
62
+ [[package]]
63
+ name = "pydantic"
64
+ version = "2.12.5"
65
+ source = { registry = "https://pypi.org/simple" }
66
+ dependencies = [
67
+ { name = "annotated-types" },
68
+ { name = "pydantic-core" },
69
+ { name = "typing-extensions" },
70
+ { name = "typing-inspection" },
71
+ ]
72
+ sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
73
+ wheels = [
74
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
75
+ ]
76
+
77
+ [[package]]
78
+ name = "pydantic-core"
79
+ version = "2.41.5"
80
+ source = { registry = "https://pypi.org/simple" }
81
+ dependencies = [
82
+ { name = "typing-extensions" },
83
+ ]
84
+ sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
85
+ wheels = [
86
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
87
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
88
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
89
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
90
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
91
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
92
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
93
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
94
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
95
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
96
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
97
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
98
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
99
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
100
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
101
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
102
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
103
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
104
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
105
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
106
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
107
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
108
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
109
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
110
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
111
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
112
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
113
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
114
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
115
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
116
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
117
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
118
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
119
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
120
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
121
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
122
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
123
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
124
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
125
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
126
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
127
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
128
+ ]
129
+
130
+ [[package]]
131
+ name = "pygments"
132
+ version = "2.19.2"
133
+ source = { registry = "https://pypi.org/simple" }
134
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
135
+ wheels = [
136
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
137
+ ]
138
+
139
+ [[package]]
140
+ name = "pyrct2"
141
+ version = "0.1.0"
142
+ source = { editable = "." }
143
+ dependencies = [
144
+ { name = "click" },
145
+ { name = "pydantic" },
146
+ ]
147
+
148
+ [package.dev-dependencies]
149
+ dev = [
150
+ { name = "pytest" },
151
+ { name = "ruff" },
152
+ ]
153
+
154
+ [package.metadata]
155
+ requires-dist = [
156
+ { name = "click", specifier = ">=8.1" },
157
+ { name = "pydantic", specifier = ">=2.10" },
158
+ ]
159
+
160
+ [package.metadata.requires-dev]
161
+ dev = [
162
+ { name = "pytest", specifier = ">=8.0" },
163
+ { name = "ruff", specifier = ">=0.9" },
164
+ ]
165
+
166
+ [[package]]
167
+ name = "pytest"
168
+ version = "9.0.2"
169
+ source = { registry = "https://pypi.org/simple" }
170
+ dependencies = [
171
+ { name = "colorama", marker = "sys_platform == 'win32'" },
172
+ { name = "iniconfig" },
173
+ { name = "packaging" },
174
+ { name = "pluggy" },
175
+ { name = "pygments" },
176
+ ]
177
+ sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
178
+ wheels = [
179
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
180
+ ]
181
+
182
+ [[package]]
183
+ name = "ruff"
184
+ version = "0.15.6"
185
+ source = { registry = "https://pypi.org/simple" }
186
+ sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" }
187
+ wheels = [
188
+ { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" },
189
+ { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" },
190
+ { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" },
191
+ { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" },
192
+ { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" },
193
+ { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" },
194
+ { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" },
195
+ { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" },
196
+ { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" },
197
+ { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" },
198
+ { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" },
199
+ { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" },
200
+ { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" },
201
+ { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" },
202
+ { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" },
203
+ { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" },
204
+ { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
205
+ ]
206
+
207
+ [[package]]
208
+ name = "typing-extensions"
209
+ version = "4.15.0"
210
+ source = { registry = "https://pypi.org/simple" }
211
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
212
+ wheels = [
213
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
214
+ ]
215
+
216
+ [[package]]
217
+ name = "typing-inspection"
218
+ version = "0.4.2"
219
+ source = { registry = "https://pypi.org/simple" }
220
+ dependencies = [
221
+ { name = "typing-extensions" },
222
+ ]
223
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
224
+ wheels = [
225
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
226
+ ]