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.
- pyrct2-0.1.0/.github/workflows/publish.yml +24 -0
- pyrct2-0.1.0/.gitignore +2 -0
- pyrct2-0.1.0/PKG-INFO +38 -0
- pyrct2-0.1.0/README.md +28 -0
- pyrct2-0.1.0/pyproject.toml +29 -0
- pyrct2-0.1.0/pyrct2/__init__.py +0 -0
- pyrct2-0.1.0/pyrct2/cli.py +42 -0
- pyrct2-0.1.0/pyrct2/client.py +68 -0
- pyrct2-0.1.0/pyrct2/connection.py +39 -0
- pyrct2-0.1.0/pyrct2/launcher.py +116 -0
- pyrct2-0.1.0/pyrct2/paths.py +121 -0
- pyrct2-0.1.0/uv.lock +226 -0
|
@@ -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
|
pyrct2-0.1.0/.gitignore
ADDED
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
|
+
]
|