screenstocks-bridge 0.2.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,7 @@
1
+ MIT No Attribution
2
+
3
+ Copyright 2026 fuubox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: screenstocks-bridge
3
+ Version: 0.2.0
4
+ Summary: Python client for the local Screen Stocks BepInEx bridge
5
+ License-Expression: MIT-0
6
+ Project-URL: Homepage, https://github.com/fuubox/screen-stocks-bridge
7
+ Project-URL: Documentation, https://github.com/fuubox/screen-stocks-bridge#readme
8
+ Project-URL: Repository, https://github.com/fuubox/screen-stocks-bridge
9
+ Project-URL: Issues, https://github.com/fuubox/screen-stocks-bridge/issues
10
+ Keywords: screen-stocks,bepinex,game-modding,bridge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # Screen Stocks Python Bridge
25
+
26
+ The `screenstocks-bridge` package is a Python client for the local API exposed
27
+ by the Screen Stocks Bridge BepInEx plugin. It requires the plugin to be
28
+ installed in the game and the game to be running. The package itself does not
29
+ install or contact the game backend; it connects to the plugin on loopback.
30
+
31
+ ## Install
32
+
33
+ ```powershell
34
+ python -m pip install screenstocks-bridge
35
+ ```
36
+
37
+ Install the matching BepInEx plugin from the
38
+ [GitHub releases](https://github.com/fuubox/screen-stocks-bridge/releases),
39
+ then launch Screen Stocks. The default bridge address is `127.0.0.1:48721`.
40
+ Read the token from `BepInEx/config/screenstocks.bridge.cfg` or the BepInEx
41
+ log.
42
+
43
+ ## Connect
44
+
45
+ ```python
46
+ from screenstocks_bridge import BridgeClient
47
+
48
+ with BridgeClient(token="YOUR_TOKEN") as bridge:
49
+ state = bridge.snapshot()
50
+ print(state["ready"], state["cash"])
51
+ print(bridge.upgrades())
52
+ ```
53
+
54
+ The client supports state and activity reads, trades, upgrade purchases, and
55
+ auto-action controls. See the
56
+ [bridge usage guide](https://github.com/fuubox/screen-stocks-bridge/blob/main/docs/bridge-usage.md)
57
+ for the full API and its online behavior.
@@ -0,0 +1,34 @@
1
+ # Screen Stocks Python Bridge
2
+
3
+ The `screenstocks-bridge` package is a Python client for the local API exposed
4
+ by the Screen Stocks Bridge BepInEx plugin. It requires the plugin to be
5
+ installed in the game and the game to be running. The package itself does not
6
+ install or contact the game backend; it connects to the plugin on loopback.
7
+
8
+ ## Install
9
+
10
+ ```powershell
11
+ python -m pip install screenstocks-bridge
12
+ ```
13
+
14
+ Install the matching BepInEx plugin from the
15
+ [GitHub releases](https://github.com/fuubox/screen-stocks-bridge/releases),
16
+ then launch Screen Stocks. The default bridge address is `127.0.0.1:48721`.
17
+ Read the token from `BepInEx/config/screenstocks.bridge.cfg` or the BepInEx
18
+ log.
19
+
20
+ ## Connect
21
+
22
+ ```python
23
+ from screenstocks_bridge import BridgeClient
24
+
25
+ with BridgeClient(token="YOUR_TOKEN") as bridge:
26
+ state = bridge.snapshot()
27
+ print(state["ready"], state["cash"])
28
+ print(bridge.upgrades())
29
+ ```
30
+
31
+ The client supports state and activity reads, trades, upgrade purchases, and
32
+ auto-action controls. See the
33
+ [bridge usage guide](https://github.com/fuubox/screen-stocks-bridge/blob/main/docs/bridge-usage.md)
34
+ for the full API and its online behavior.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "screenstocks-bridge"
7
+ version = "0.2.0"
8
+ description = "Python client for the local Screen Stocks BepInEx bridge"
9
+ readme = "README.md"
10
+ license = "MIT-0"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ dependencies = []
14
+ keywords = ["screen-stocks", "bepinex", "game-modding", "bridge"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/fuubox/screen-stocks-bridge"
28
+ Documentation = "https://github.com/fuubox/screen-stocks-bridge#readme"
29
+ Repository = "https://github.com/fuubox/screen-stocks-bridge"
30
+ Issues = "https://github.com/fuubox/screen-stocks-bridge/issues"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """Python client for the local Screen Stocks BepInEx bridge."""
2
+
3
+ from .client import BridgeClient
4
+ from .models import AutoAction, AutoActions, Cooldown, HumanActivity, HumanActivityPage, Position, Snapshot, Stock, TradeCooldowns, TradeReceipt
5
+ from .protocol import BridgeError
6
+
7
+ __all__ = ["AutoAction", "AutoActions", "BridgeClient", "BridgeError", "Cooldown", "HumanActivity", "HumanActivityPage", "Position",
8
+ "Snapshot", "Stock", "TradeCooldowns", "TradeReceipt"]
@@ -0,0 +1,272 @@
1
+ """Thread-safe JSON-lines client for the local Screen Stocks bridge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import queue
7
+ import socket
8
+ import threading
9
+ import uuid
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ from .protocol import MAX_FRAME_BYTES, BridgeError, _ReaderStopped
14
+
15
+
16
+ class BridgeClient:
17
+ """Connect to the in-game loopback bridge and issue state or trade calls."""
18
+
19
+ def __init__(self, host: str = "127.0.0.1", port: int = 48721,
20
+ token: str = "", timeout: float = 5.0) -> None:
21
+ self.host = host
22
+ self.port = port
23
+ self.token = token
24
+ self.timeout = timeout
25
+ self._socket: socket.socket | None = None
26
+ self._send_lock = threading.Lock()
27
+ self._pending_lock = threading.Lock()
28
+ self._pending: dict[str, queue.Queue[dict[str, Any] | BaseException]] = {}
29
+ self._callbacks_lock = threading.Lock()
30
+ self._callbacks: list[Callable[[dict[str, Any]], None]] = []
31
+ self._events: queue.Queue[dict[str, Any] | None] = queue.Queue(maxsize=128)
32
+ self._trade_events: queue.Queue[dict[str, Any] | None] = queue.Queue(maxsize=128)
33
+ self._closed = threading.Event()
34
+ self._reader: threading.Thread | None = None
35
+ self._event_worker: threading.Thread | None = None
36
+
37
+ def connect(self) -> BridgeClient:
38
+ """Open the authenticated TCP connection. Returns this client."""
39
+ if self._socket is not None:
40
+ return self
41
+ if not self.token:
42
+ raise ValueError("A bridge token is required.")
43
+ sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
44
+ sock.settimeout(None)
45
+ self._socket = sock
46
+ self._closed.clear()
47
+ self._reader = threading.Thread(target=self._read_loop, name="screenstocks-reader", daemon=True)
48
+ self._event_worker = threading.Thread(target=self._event_loop, name="screenstocks-events", daemon=True)
49
+ self._reader.start()
50
+ self._event_worker.start()
51
+ return self
52
+
53
+ def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
54
+ """Send an RPC call and return its result object."""
55
+ if self._socket is None or self._closed.is_set():
56
+ raise BridgeError("not_connected", "Call connect() before making a request.")
57
+ request_id = uuid.uuid4().hex
58
+ waiter: queue.Queue[dict[str, Any] | BaseException] = queue.Queue(maxsize=1)
59
+ with self._pending_lock:
60
+ self._pending[request_id] = waiter
61
+ frame = {
62
+ "id": request_id,
63
+ "method": method,
64
+ "token": self.token,
65
+ "params": params or {},
66
+ }
67
+ try:
68
+ encoded = (json.dumps(frame, separators=(",", ":"), allow_nan=False) + "\n").encode("utf-8")
69
+ if len(encoded) - 1 > MAX_FRAME_BYTES:
70
+ raise BridgeError("frame_too_large", "Request exceeds the 16 KiB frame limit.")
71
+ with self._send_lock:
72
+ if self._closed.is_set() or self._socket is None:
73
+ raise BridgeError("not_connected", "The bridge connection is closed.")
74
+ self._socket.sendall(encoded)
75
+ try:
76
+ response = waiter.get(timeout=self.timeout)
77
+ except queue.Empty as exc:
78
+ raise BridgeError("timeout", "The bridge did not answer before the request timeout.") from exc
79
+ if isinstance(response, BaseException):
80
+ raise BridgeError("disconnected", str(response)) from response
81
+ if not response.get("ok"):
82
+ error = response.get("error") or {}
83
+ raise BridgeError(str(error.get("code", "remote_error")), str(error.get("message", "Bridge request failed.")))
84
+ result = response.get("result")
85
+ return result if isinstance(result, dict) else {}
86
+ finally:
87
+ with self._pending_lock:
88
+ self._pending.pop(request_id, None)
89
+
90
+ def snapshot(self) -> dict[str, Any]:
91
+ """Return the current in-memory player and visible-market snapshot."""
92
+ return self.request("state.snapshot")
93
+
94
+ def human_activity(self, stock_id: str, limit: int = 32,
95
+ before_tick: int | None = None) -> dict[str, Any]:
96
+ """Return one page of aggregate graph activity for a visible stock."""
97
+ params: dict[str, Any] = {"stockId": stock_id, "limit": limit}
98
+ if before_tick is not None:
99
+ params["beforeTick"] = before_tick
100
+ return self.request("market.human_activity", params)
101
+
102
+ def subscribe_market(self, callback: Callable[[dict[str, Any]], None]) -> None:
103
+ """Subscribe to market.updated and trade.completed event dictionaries."""
104
+ with self._callbacks_lock:
105
+ if callback not in self._callbacks:
106
+ self._callbacks.append(callback)
107
+ self.request("state.subscribe")
108
+
109
+ def trade(self, action: str, stock_id: str, percent: float | None = None) -> dict[str, Any]:
110
+ """Submit one explicit allowlisted trade; completion arrives as an event."""
111
+ params: dict[str, Any] = {"action": action, "stockId": stock_id}
112
+ if percent is not None:
113
+ params["percent"] = percent
114
+ return self.request("trade.submit", params)
115
+
116
+ def upgrades(self) -> dict[str, Any]:
117
+ """Return the game's dynamically discovered upgrade catalog and current levels."""
118
+ return self.request("upgrades.snapshot")
119
+
120
+ def purchase_upgrade(self, upgrade_id: str, quantity: int = 1) -> dict[str, Any]:
121
+ """Ask the running game to process an upgrade through its in-process method.
122
+
123
+ The bridge itself only talks to localhost. The game may communicate with its
124
+ server in online mode; server state remains authoritative. The returned status
125
+ is ``submitted``.
126
+ Refresh :meth:`upgrades` to observe the resulting level.
127
+ """
128
+ return self.request("upgrades.purchase", {"upgradeId": upgrade_id, "quantity": quantity})
129
+
130
+ def auto_actions(self) -> dict[str, Any]:
131
+ """Return dynamically discovered auto-action slots and cooldown state."""
132
+ return self.request("auto_actions.snapshot")
133
+
134
+ def add_auto_action(self, stock_id: str, action_type: str, condition: str,
135
+ target_price: float, amount_percentage: int) -> dict[str, Any]:
136
+ """Add an auto action in an available game slot and return its slot index."""
137
+ return self.request("auto_actions.add", {
138
+ "stockId": stock_id, "actionType": action_type, "condition": condition,
139
+ "targetPrice": target_price, "amountPercentage": amount_percentage,
140
+ })
141
+
142
+ def update_auto_action(self, slot_index: int, stock_id: str, action_type: str,
143
+ condition: str, target_price: float,
144
+ amount_percentage: int) -> dict[str, Any]:
145
+ """Replace an auto action's complete configuration by its discovered slot index."""
146
+ return self.request("auto_actions.update", {
147
+ "slotIndex": slot_index, "stockId": stock_id, "actionType": action_type,
148
+ "condition": condition, "targetPrice": target_price,
149
+ "amountPercentage": amount_percentage,
150
+ })
151
+
152
+ def remove_auto_action(self, slot_index: int) -> dict[str, Any]:
153
+ """Remove the configured auto action occupying slot_index."""
154
+ return self.request("auto_actions.remove", {"slotIndex": slot_index})
155
+
156
+ def set_auto_action_enabled(self, slot_index: int, enabled: bool) -> dict[str, Any]:
157
+ """Enable or disable one configured auto action."""
158
+ return self.request("auto_actions.set_enabled", {"slotIndex": slot_index, "enabled": enabled})
159
+
160
+ def set_auto_actions_active(self, active: bool) -> dict[str, Any]:
161
+ """Set the game's global auto-action switch."""
162
+ return self.request("auto_actions.set_active", {"active": active})
163
+
164
+ def close(self) -> None:
165
+ """Close the socket and stop its reader and event worker threads."""
166
+ self._fail_pending(_ReaderStopped("Client closed."))
167
+ self._closed.set()
168
+ sock, self._socket = self._socket, None
169
+ if sock is not None:
170
+ try:
171
+ sock.shutdown(socket.SHUT_RDWR)
172
+ except OSError:
173
+ pass
174
+ try:
175
+ sock.close()
176
+ except OSError:
177
+ pass
178
+ try:
179
+ self._events.put_nowait(None)
180
+ except queue.Full:
181
+ pass
182
+ try:
183
+ self._trade_events.put_nowait(None)
184
+ except queue.Full:
185
+ pass
186
+
187
+ def _read_loop(self) -> None:
188
+ buffer = bytearray()
189
+ sock = self._socket
190
+ try:
191
+ if sock is None:
192
+ return
193
+ while not self._closed.is_set():
194
+ chunk = sock.recv(4096)
195
+ if not chunk:
196
+ raise _ReaderStopped("Bridge closed the connection.")
197
+ buffer.extend(chunk)
198
+ while True:
199
+ newline = buffer.find(b"\n")
200
+ if newline < 0:
201
+ if len(buffer) > MAX_FRAME_BYTES:
202
+ raise BridgeError("frame_too_large", "Bridge response exceeds the 16 KiB frame limit.")
203
+ break
204
+ if newline > MAX_FRAME_BYTES:
205
+ raise BridgeError("frame_too_large", "Bridge response exceeds the 16 KiB frame limit.")
206
+ raw = bytes(buffer[:newline]).rstrip(b"\r")
207
+ del buffer[:newline + 1]
208
+ message = json.loads(raw.decode("utf-8"))
209
+ if "event" in message:
210
+ self._queue_event(message)
211
+ else:
212
+ with self._pending_lock:
213
+ waiter = self._pending.get(str(message.get("id", "")))
214
+ if waiter is not None:
215
+ waiter.put(message)
216
+ except BaseException as exc:
217
+ if not self._closed.is_set():
218
+ self._fail_pending(exc)
219
+ self._closed.set()
220
+
221
+ def _queue_event(self, event: dict[str, Any]) -> None:
222
+ if event.get("event") == "trade.completed":
223
+ # Preserve trade outcomes; a full completion queue applies TCP backpressure.
224
+ self._trade_events.put(event)
225
+ return
226
+ try:
227
+ self._events.put_nowait(event)
228
+ except queue.Full:
229
+ try:
230
+ self._events.get_nowait()
231
+ except queue.Empty:
232
+ pass
233
+ try:
234
+ self._events.put_nowait(event)
235
+ except queue.Full:
236
+ pass
237
+
238
+ def _event_loop(self) -> None:
239
+ while True:
240
+ if self._closed.is_set() and self._events.empty() and self._trade_events.empty():
241
+ return
242
+ try:
243
+ event = self._trade_events.get_nowait()
244
+ except queue.Empty:
245
+ try:
246
+ event = self._events.get(timeout=0.25)
247
+ except queue.Empty:
248
+ continue
249
+ if event is None:
250
+ continue
251
+ with self._callbacks_lock:
252
+ callbacks = tuple(self._callbacks)
253
+ for callback in callbacks:
254
+ try:
255
+ callback(event)
256
+ except Exception:
257
+ continue
258
+
259
+ def _fail_pending(self, error: BaseException) -> None:
260
+ with self._pending_lock:
261
+ waiters = tuple(self._pending.values())
262
+ for waiter in waiters:
263
+ try:
264
+ waiter.put_nowait(error)
265
+ except queue.Full:
266
+ pass
267
+
268
+ def __enter__(self) -> BridgeClient:
269
+ return self.connect()
270
+
271
+ def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
272
+ self.close()
@@ -0,0 +1,188 @@
1
+ """Optional typed views over bridge dictionaries.
2
+
3
+ Each model retains its source dictionary as ``raw`` so callers can access fields
4
+ added by later protocol versions without waiting for a client release.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class HumanActivity:
15
+ sample_tick: int
16
+ up: int
17
+ down: int
18
+ total: int
19
+ net: int
20
+ up_impact: float
21
+ down_impact: float
22
+ total_impact: float
23
+ net_impact: float
24
+ raw: dict[str, Any] = field(repr=False)
25
+
26
+ @classmethod
27
+ def from_dict(cls, value: dict[str, Any]) -> HumanActivity:
28
+ return cls(int(value.get("sampleTick", 0)), int(value.get("up", 0)),
29
+ int(value.get("down", 0)), int(value.get("total", 0)),
30
+ int(value.get("net", 0)), float(value.get("upImpact", 0)),
31
+ float(value.get("downImpact", 0)), float(value.get("totalImpact", 0)),
32
+ float(value.get("netImpact", 0)), value)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class Stock:
37
+ stock_id: str
38
+ name: str
39
+ price: float
40
+ unlocked: bool
41
+ raw: dict[str, Any] = field(repr=False)
42
+
43
+ @classmethod
44
+ def from_dict(cls, value: dict[str, Any]) -> Stock:
45
+ return cls(str(value.get("stockId", "")), str(value.get("name", "")),
46
+ float(value.get("price", 0)), bool(value.get("unlocked", False)),
47
+ value)
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class HumanActivityPage:
52
+ stock_id: str
53
+ samples: list[HumanActivity]
54
+ has_more: bool
55
+ next_before_tick: int
56
+ raw: dict[str, Any] = field(repr=False)
57
+
58
+ @classmethod
59
+ def from_dict(cls, value: dict[str, Any]) -> HumanActivityPage:
60
+ return cls(str(value.get("stockId", "")),
61
+ [HumanActivity.from_dict(item) for item in value.get("samples", [])],
62
+ bool(value.get("hasMore", False)), int(value.get("nextBeforeTick", 0)), value)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class Position:
67
+ stock_id: str
68
+ shares_owned: str
69
+ average_buy_price: str
70
+ shares_shorted: str
71
+ average_short_price: str
72
+ raw: dict[str, Any] = field(repr=False)
73
+
74
+ @classmethod
75
+ def from_dict(cls, value: dict[str, Any]) -> Position:
76
+ return cls(str(value.get("stockId", "")), str(value.get("sharesOwned", "0")),
77
+ str(value.get("averageBuyPrice", "0")), str(value.get("sharesShorted", "0")),
78
+ str(value.get("averageShortPrice", "0")), value)
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class Cooldown:
83
+ duration_seconds: int
84
+ available_at_unix_seconds: int
85
+ remaining_seconds: int
86
+ active: bool
87
+ raw: dict[str, Any] = field(repr=False)
88
+
89
+ @classmethod
90
+ def from_dict(cls, value: dict[str, Any]) -> Cooldown:
91
+ return cls(int(value.get("durationSeconds", 0)),
92
+ int(value.get("availableAtUnixSeconds", 0)),
93
+ int(value.get("remainingSeconds", 0)), bool(value.get("active", False)), value)
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class TradeCooldowns:
98
+ server_now_unix_seconds: int
99
+ server_clock_synchronized: bool
100
+ buy: Cooldown
101
+ short_trade: Cooldown
102
+ raw: dict[str, Any] = field(repr=False)
103
+
104
+ @classmethod
105
+ def from_dict(cls, value: dict[str, Any]) -> TradeCooldowns:
106
+ return cls(int(value.get("serverNowUnixSeconds", 0)),
107
+ bool(value.get("serverClockSynchronized", False)),
108
+ Cooldown.from_dict(value.get("buy", {})),
109
+ Cooldown.from_dict(value.get("shortTrade", {})), value)
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class AutoAction:
114
+ slot_index: int
115
+ stock_id: str
116
+ action_type: str
117
+ condition: str
118
+ target_price: float
119
+ amount_percentage: int
120
+ enabled: bool
121
+ cooldown_until_unix_seconds: int
122
+ cooldown_duration_seconds: int
123
+ cooldown_remaining_seconds: int
124
+ on_cooldown: bool
125
+ raw: dict[str, Any] = field(repr=False)
126
+
127
+ @classmethod
128
+ def from_dict(cls, value: dict[str, Any]) -> AutoAction:
129
+ return cls(int(value.get("slotIndex", -1)), str(value.get("stockId", "")),
130
+ str(value.get("actionType", "")), str(value.get("condition", "")),
131
+ float(value.get("targetPrice", 0)), int(value.get("amountPercentage", 0)),
132
+ bool(value.get("enabled", False)), int(value.get("cooldownUntilUnixSeconds", 0)),
133
+ int(value.get("cooldownDurationSeconds", 0)),
134
+ int(value.get("cooldownRemainingSeconds", 0)),
135
+ bool(value.get("onCooldown", False)), value)
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class AutoActions:
140
+ ready: bool
141
+ unlocked: bool
142
+ active: bool
143
+ slot_limit: int
144
+ configured_count: int
145
+ can_add_action: bool
146
+ cooldown_duration_seconds: int
147
+ actions: list[AutoAction]
148
+ raw: dict[str, Any] = field(repr=False)
149
+
150
+ @classmethod
151
+ def from_dict(cls, value: dict[str, Any]) -> AutoActions:
152
+ return cls(bool(value.get("ready", False)), bool(value.get("unlocked", False)),
153
+ bool(value.get("active", False)), int(value.get("slotLimit", 0)),
154
+ int(value.get("configuredCount", 0)), bool(value.get("canAddAction", False)),
155
+ int(value.get("cooldownDurationSeconds", 0)),
156
+ [AutoAction.from_dict(item) for item in value.get("actions", [])], value)
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class Snapshot:
161
+ ready: bool
162
+ server_tick: int
163
+ cash: str
164
+ level: int
165
+ stocks: list[Stock]
166
+ positions: list[Position]
167
+ cooldowns: TradeCooldowns
168
+ auto_actions: AutoActions
169
+ raw: dict[str, Any] = field(repr=False)
170
+
171
+ @classmethod
172
+ def from_dict(cls, value: dict[str, Any]) -> Snapshot:
173
+ return cls(bool(value.get("ready", False)), int(value.get("serverTick", 0)),
174
+ str(value.get("cash", "0")), int(value.get("level", 0)),
175
+ [Stock.from_dict(item) for item in value.get("stocks", [])],
176
+ [Position.from_dict(item) for item in value.get("positions", [])],
177
+ TradeCooldowns.from_dict(value.get("cooldowns", {})),
178
+ AutoActions.from_dict(value.get("autoActions", {})), value)
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class TradeReceipt:
183
+ status: str
184
+ raw: dict[str, Any] = field(repr=False)
185
+
186
+ @classmethod
187
+ def from_dict(cls, value: dict[str, Any]) -> TradeReceipt:
188
+ return cls(str(value.get("status", "")), value)
@@ -0,0 +1,16 @@
1
+ """Protocol constants and errors shared by the Python bridge client."""
2
+
3
+ MAX_FRAME_BYTES = 16 * 1024
4
+
5
+
6
+ class BridgeError(RuntimeError):
7
+ """A transport, protocol, or game-side API error."""
8
+
9
+ def __init__(self, code: str, message: str) -> None:
10
+ super().__init__(message)
11
+ self.code = code
12
+ self.message = message
13
+
14
+
15
+ class _ReaderStopped(Exception):
16
+ pass
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: screenstocks-bridge
3
+ Version: 0.2.0
4
+ Summary: Python client for the local Screen Stocks BepInEx bridge
5
+ License-Expression: MIT-0
6
+ Project-URL: Homepage, https://github.com/fuubox/screen-stocks-bridge
7
+ Project-URL: Documentation, https://github.com/fuubox/screen-stocks-bridge#readme
8
+ Project-URL: Repository, https://github.com/fuubox/screen-stocks-bridge
9
+ Project-URL: Issues, https://github.com/fuubox/screen-stocks-bridge/issues
10
+ Keywords: screen-stocks,bepinex,game-modding,bridge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # Screen Stocks Python Bridge
25
+
26
+ The `screenstocks-bridge` package is a Python client for the local API exposed
27
+ by the Screen Stocks Bridge BepInEx plugin. It requires the plugin to be
28
+ installed in the game and the game to be running. The package itself does not
29
+ install or contact the game backend; it connects to the plugin on loopback.
30
+
31
+ ## Install
32
+
33
+ ```powershell
34
+ python -m pip install screenstocks-bridge
35
+ ```
36
+
37
+ Install the matching BepInEx plugin from the
38
+ [GitHub releases](https://github.com/fuubox/screen-stocks-bridge/releases),
39
+ then launch Screen Stocks. The default bridge address is `127.0.0.1:48721`.
40
+ Read the token from `BepInEx/config/screenstocks.bridge.cfg` or the BepInEx
41
+ log.
42
+
43
+ ## Connect
44
+
45
+ ```python
46
+ from screenstocks_bridge import BridgeClient
47
+
48
+ with BridgeClient(token="YOUR_TOKEN") as bridge:
49
+ state = bridge.snapshot()
50
+ print(state["ready"], state["cash"])
51
+ print(bridge.upgrades())
52
+ ```
53
+
54
+ The client supports state and activity reads, trades, upgrade purchases, and
55
+ auto-action controls. See the
56
+ [bridge usage guide](https://github.com/fuubox/screen-stocks-bridge/blob/main/docs/bridge-usage.md)
57
+ for the full API and its online behavior.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/screenstocks_bridge/__init__.py
5
+ src/screenstocks_bridge/client.py
6
+ src/screenstocks_bridge/models.py
7
+ src/screenstocks_bridge/protocol.py
8
+ src/screenstocks_bridge.egg-info/PKG-INFO
9
+ src/screenstocks_bridge.egg-info/SOURCES.txt
10
+ src/screenstocks_bridge.egg-info/dependency_links.txt
11
+ src/screenstocks_bridge.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ screenstocks_bridge