topstep-client-py 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.
Files changed (38) hide show
  1. topstep_client_py-0.1.0/LICENSE +21 -0
  2. topstep_client_py-0.1.0/PKG-INFO +233 -0
  3. topstep_client_py-0.1.0/README.md +205 -0
  4. topstep_client_py-0.1.0/pyproject.toml +43 -0
  5. topstep_client_py-0.1.0/setup.cfg +4 -0
  6. topstep_client_py-0.1.0/src/topstep/__init__.py +55 -0
  7. topstep_client_py-0.1.0/src/topstep/auth.py +50 -0
  8. topstep_client_py-0.1.0/src/topstep/client.py +118 -0
  9. topstep_client_py-0.1.0/src/topstep/endpoints/__init__.py +17 -0
  10. topstep_client_py-0.1.0/src/topstep/endpoints/accounts.py +18 -0
  11. topstep_client_py-0.1.0/src/topstep/endpoints/contracts.py +31 -0
  12. topstep_client_py-0.1.0/src/topstep/endpoints/history.py +48 -0
  13. topstep_client_py-0.1.0/src/topstep/endpoints/orders.py +75 -0
  14. topstep_client_py-0.1.0/src/topstep/endpoints/positions.py +33 -0
  15. topstep_client_py-0.1.0/src/topstep/endpoints/trades.py +31 -0
  16. topstep_client_py-0.1.0/src/topstep/exceptions.py +32 -0
  17. topstep_client_py-0.1.0/src/topstep/http.py +102 -0
  18. topstep_client_py-0.1.0/src/topstep/models/__init__.py +31 -0
  19. topstep_client_py-0.1.0/src/topstep/models/account.py +13 -0
  20. topstep_client_py-0.1.0/src/topstep/models/bar.py +26 -0
  21. topstep_client_py-0.1.0/src/topstep/models/contract.py +15 -0
  22. topstep_client_py-0.1.0/src/topstep/models/order.py +82 -0
  23. topstep_client_py-0.1.0/src/topstep/models/position.py +24 -0
  24. topstep_client_py-0.1.0/src/topstep/models/trade.py +22 -0
  25. topstep_client_py-0.1.0/src/topstep/py.typed +0 -0
  26. topstep_client_py-0.1.0/src/topstep/realtime/__init__.py +6 -0
  27. topstep_client_py-0.1.0/src/topstep/realtime/market_hub.py +236 -0
  28. topstep_client_py-0.1.0/src/topstep/realtime/user_hub.py +257 -0
  29. topstep_client_py-0.1.0/src/topstep_client_py.egg-info/PKG-INFO +233 -0
  30. topstep_client_py-0.1.0/src/topstep_client_py.egg-info/SOURCES.txt +36 -0
  31. topstep_client_py-0.1.0/src/topstep_client_py.egg-info/dependency_links.txt +1 -0
  32. topstep_client_py-0.1.0/src/topstep_client_py.egg-info/requires.txt +9 -0
  33. topstep_client_py-0.1.0/src/topstep_client_py.egg-info/top_level.txt +1 -0
  34. topstep_client_py-0.1.0/tests/test_accounts.py +41 -0
  35. topstep_client_py-0.1.0/tests/test_auth.py +52 -0
  36. topstep_client_py-0.1.0/tests/test_client.py +122 -0
  37. topstep_client_py-0.1.0/tests/test_http.py +65 -0
  38. topstep_client_py-0.1.0/tests/test_orders.py +90 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Icarus Research
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: topstep-client-py
3
+ Version: 0.1.0
4
+ Summary: Lightweight async Python client for the TopstepX / ProjectX Gateway API
5
+ License-Expression: MIT
6
+ Keywords: topstep,projectx,trading,futures,api,propfirm
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Office/Business :: Financial :: Investment
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: pydantic>=2.0.0
21
+ Requires-Dist: signalrcore>=0.9.5
22
+ Requires-Dist: websocket-client>=0.54.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0; extra == "dev"
25
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
26
+ Requires-Dist: respx>=0.22; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # topstep-client-py
30
+
31
+ Lightweight async Python client for the [TopstepX](https://www.topstep.com/) / [ProjectX Gateway API](https://gateway.docs.projectx.com/docs/intro/).
32
+
33
+ ## Disclaimer
34
+
35
+ This project is an independent community-built library. It is not officially affiliated with, endorsed by, maintained by, or otherwise directly associated with Topstep, TopstepX, or ProjectX.
36
+
37
+ The goal of this package is to provide a clean and practical Python client for the public API and realtime interfaces. It is maintained on a best-effort basis and will be kept up to date as quickly as reasonably possible when the upstream API changes.
38
+
39
+ This repository only provides the API client layer. It does not include trading strategies, alpha generation, signal logic, backtesting frameworks, portfolio tooling, or other trader-specific systems. Those decisions and tools are intentionally left to each user and their own workflow.
40
+
41
+ - Async-first (built on `httpx`)
42
+ - Fully typed responses with Pydantic models
43
+ - All 16 REST endpoints covered
44
+ - Real-time market & user data via SignalR WebSocket
45
+ - Retry with backoff + rate limit handling
46
+ - Clean exception hierarchy
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install topstep-client-py
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ```python
57
+ import asyncio
58
+ from datetime import datetime, timedelta, timezone
59
+ from topstep import TopstepClient, BarUnit
60
+
61
+ async def main():
62
+ async with await TopstepClient.create(
63
+ username="you@email.com",
64
+ api_key="your-api-key",
65
+ ) as client:
66
+
67
+ # Search accounts
68
+ accounts = await client.accounts.search()
69
+ account = accounts[0]
70
+ print(f"Account: {account.name} | Balance: {account.balance}")
71
+
72
+ # Find a contract
73
+ contracts = await client.contracts.search("Micro E-mini Nasdaq")
74
+ contract = contracts[0]
75
+ print(f"Contract: {contract.description} | Tick: {contract.tick_size}")
76
+
77
+ # Get historical bars
78
+ end = datetime.now(timezone.utc)
79
+ start = end - timedelta(hours=1)
80
+ bars = await client.history.retrieve_bars(
81
+ contract_id=contract.id,
82
+ start=start,
83
+ end=end,
84
+ unit=BarUnit.MINUTE,
85
+ unit_number=5,
86
+ )
87
+ for bar in bars[-3:]:
88
+ print(f" {bar.timestamp} O:{bar.open} H:{bar.high} L:{bar.low} C:{bar.close}")
89
+
90
+ asyncio.run(main())
91
+ ```
92
+
93
+ ## Placing Orders
94
+
95
+ ```python
96
+ from topstep import PlaceOrderRequest, OrderType, OrderSide, Bracket
97
+
98
+ order_id = await client.orders.place(PlaceOrderRequest(
99
+ account_id=account.id,
100
+ contract_id=contract.id,
101
+ type=OrderType.STOP,
102
+ side=OrderSide.BUY,
103
+ size=2,
104
+ stop_price=21500.0,
105
+ stop_loss_bracket=Bracket(ticks=-20, type=4),
106
+ take_profit_bracket=Bracket(ticks=40, type=1),
107
+ ))
108
+ print(f"Order placed: {order_id}")
109
+
110
+ # Check open orders
111
+ open_orders = await client.orders.search_open(account.id)
112
+
113
+ # Cancel an order
114
+ await client.orders.cancel(account.id, order_id)
115
+ ```
116
+
117
+ ## Positions
118
+
119
+ ```python
120
+ # Get open positions
121
+ positions = await client.positions.search_open(account.id)
122
+
123
+ # Close all positions for a contract
124
+ await client.positions.close(account.id, contract.id)
125
+
126
+ # Partial close
127
+ await client.positions.partial_close(account.id, contract.id, size=1)
128
+ ```
129
+
130
+ ## Trade History
131
+
132
+ ```python
133
+ from datetime import datetime, timezone
134
+
135
+ trades = await client.trades.search(
136
+ account_id=account.id,
137
+ start=datetime(2026, 3, 1, tzinfo=timezone.utc),
138
+ )
139
+ for trade in trades:
140
+ print(f" {trade.price} | P&L: {trade.profit_and_loss} | Fees: {trade.fees}")
141
+ ```
142
+
143
+ ## Real-Time Market Data (WebSocket)
144
+
145
+ ```python
146
+ import asyncio
147
+ from topstep import TopstepClient
148
+
149
+ async def main():
150
+ async with await TopstepClient.create(
151
+ username="you@email.com",
152
+ api_key="your-api-key",
153
+ ) as client:
154
+
155
+ # Register callbacks
156
+ client.market.on_quote(lambda *args: print("QUOTE:", args))
157
+ client.market.on_trade(lambda *args: print("TRADE:", args))
158
+ client.market.on_depth(lambda *args: print("DEPTH:", args))
159
+
160
+ # Connect and subscribe
161
+ await client.market.connect()
162
+ await client.market.subscribe_all("CON.F.US.ENQ.H26")
163
+
164
+ # Keep alive
165
+ try:
166
+ while True:
167
+ await asyncio.sleep(1)
168
+ except KeyboardInterrupt:
169
+ await client.market.stop()
170
+
171
+ asyncio.run(main())
172
+ ```
173
+
174
+ ## Real-Time User Data (WebSocket)
175
+
176
+ ```python
177
+ async with await TopstepClient.create(...) as client:
178
+
179
+ client.user.on_order(lambda *args: print("ORDER:", args))
180
+ client.user.on_position(lambda *args: print("POSITION:", args))
181
+ client.user.on_trade(lambda *args: print("TRADE:", args))
182
+ client.user.on_account(lambda *args: print("ACCOUNT:", args))
183
+
184
+ await client.user.connect()
185
+ await client.user.subscribe_all(account_id=account.id)
186
+
187
+ # ...
188
+ ```
189
+
190
+ ## Token Refresh
191
+
192
+ Tokens expire after 24 hours. Refresh manually:
193
+
194
+ ```python
195
+ await client.refresh_token()
196
+ ```
197
+
198
+ ## Error Handling
199
+
200
+ ```python
201
+ from topstep import TopstepError, AuthenticationError, APIError, RateLimitError
202
+
203
+ try:
204
+ accounts = await client.accounts.search()
205
+ except AuthenticationError:
206
+ # Invalid credentials or expired token
207
+ await client.refresh_token()
208
+ except RateLimitError:
209
+ # HTTP 429 — too many requests (auto-retried 3 times before raising)
210
+ pass
211
+ except APIError as e:
212
+ # API returned success=False
213
+ print(f"API error [{e.error_code}]: {e}")
214
+ except TopstepError:
215
+ # Any other client error
216
+ pass
217
+ ```
218
+
219
+ ## Rate Limits
220
+
221
+ The API enforces these limits (handled automatically with retry + backoff):
222
+
223
+ - `History/retrieveBars`: 50 requests per 30 seconds
224
+ - All other endpoints: 200 requests per 60 seconds
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ git clone https://github.com/YOUR_USER/topstep-client-py.git
230
+ cd topstep-client-py
231
+ pip install -e ".[dev]"
232
+ pytest
233
+ ```
@@ -0,0 +1,205 @@
1
+ # topstep-client-py
2
+
3
+ Lightweight async Python client for the [TopstepX](https://www.topstep.com/) / [ProjectX Gateway API](https://gateway.docs.projectx.com/docs/intro/).
4
+
5
+ ## Disclaimer
6
+
7
+ This project is an independent community-built library. It is not officially affiliated with, endorsed by, maintained by, or otherwise directly associated with Topstep, TopstepX, or ProjectX.
8
+
9
+ The goal of this package is to provide a clean and practical Python client for the public API and realtime interfaces. It is maintained on a best-effort basis and will be kept up to date as quickly as reasonably possible when the upstream API changes.
10
+
11
+ This repository only provides the API client layer. It does not include trading strategies, alpha generation, signal logic, backtesting frameworks, portfolio tooling, or other trader-specific systems. Those decisions and tools are intentionally left to each user and their own workflow.
12
+
13
+ - Async-first (built on `httpx`)
14
+ - Fully typed responses with Pydantic models
15
+ - All 16 REST endpoints covered
16
+ - Real-time market & user data via SignalR WebSocket
17
+ - Retry with backoff + rate limit handling
18
+ - Clean exception hierarchy
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install topstep-client-py
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```python
29
+ import asyncio
30
+ from datetime import datetime, timedelta, timezone
31
+ from topstep import TopstepClient, BarUnit
32
+
33
+ async def main():
34
+ async with await TopstepClient.create(
35
+ username="you@email.com",
36
+ api_key="your-api-key",
37
+ ) as client:
38
+
39
+ # Search accounts
40
+ accounts = await client.accounts.search()
41
+ account = accounts[0]
42
+ print(f"Account: {account.name} | Balance: {account.balance}")
43
+
44
+ # Find a contract
45
+ contracts = await client.contracts.search("Micro E-mini Nasdaq")
46
+ contract = contracts[0]
47
+ print(f"Contract: {contract.description} | Tick: {contract.tick_size}")
48
+
49
+ # Get historical bars
50
+ end = datetime.now(timezone.utc)
51
+ start = end - timedelta(hours=1)
52
+ bars = await client.history.retrieve_bars(
53
+ contract_id=contract.id,
54
+ start=start,
55
+ end=end,
56
+ unit=BarUnit.MINUTE,
57
+ unit_number=5,
58
+ )
59
+ for bar in bars[-3:]:
60
+ print(f" {bar.timestamp} O:{bar.open} H:{bar.high} L:{bar.low} C:{bar.close}")
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ## Placing Orders
66
+
67
+ ```python
68
+ from topstep import PlaceOrderRequest, OrderType, OrderSide, Bracket
69
+
70
+ order_id = await client.orders.place(PlaceOrderRequest(
71
+ account_id=account.id,
72
+ contract_id=contract.id,
73
+ type=OrderType.STOP,
74
+ side=OrderSide.BUY,
75
+ size=2,
76
+ stop_price=21500.0,
77
+ stop_loss_bracket=Bracket(ticks=-20, type=4),
78
+ take_profit_bracket=Bracket(ticks=40, type=1),
79
+ ))
80
+ print(f"Order placed: {order_id}")
81
+
82
+ # Check open orders
83
+ open_orders = await client.orders.search_open(account.id)
84
+
85
+ # Cancel an order
86
+ await client.orders.cancel(account.id, order_id)
87
+ ```
88
+
89
+ ## Positions
90
+
91
+ ```python
92
+ # Get open positions
93
+ positions = await client.positions.search_open(account.id)
94
+
95
+ # Close all positions for a contract
96
+ await client.positions.close(account.id, contract.id)
97
+
98
+ # Partial close
99
+ await client.positions.partial_close(account.id, contract.id, size=1)
100
+ ```
101
+
102
+ ## Trade History
103
+
104
+ ```python
105
+ from datetime import datetime, timezone
106
+
107
+ trades = await client.trades.search(
108
+ account_id=account.id,
109
+ start=datetime(2026, 3, 1, tzinfo=timezone.utc),
110
+ )
111
+ for trade in trades:
112
+ print(f" {trade.price} | P&L: {trade.profit_and_loss} | Fees: {trade.fees}")
113
+ ```
114
+
115
+ ## Real-Time Market Data (WebSocket)
116
+
117
+ ```python
118
+ import asyncio
119
+ from topstep import TopstepClient
120
+
121
+ async def main():
122
+ async with await TopstepClient.create(
123
+ username="you@email.com",
124
+ api_key="your-api-key",
125
+ ) as client:
126
+
127
+ # Register callbacks
128
+ client.market.on_quote(lambda *args: print("QUOTE:", args))
129
+ client.market.on_trade(lambda *args: print("TRADE:", args))
130
+ client.market.on_depth(lambda *args: print("DEPTH:", args))
131
+
132
+ # Connect and subscribe
133
+ await client.market.connect()
134
+ await client.market.subscribe_all("CON.F.US.ENQ.H26")
135
+
136
+ # Keep alive
137
+ try:
138
+ while True:
139
+ await asyncio.sleep(1)
140
+ except KeyboardInterrupt:
141
+ await client.market.stop()
142
+
143
+ asyncio.run(main())
144
+ ```
145
+
146
+ ## Real-Time User Data (WebSocket)
147
+
148
+ ```python
149
+ async with await TopstepClient.create(...) as client:
150
+
151
+ client.user.on_order(lambda *args: print("ORDER:", args))
152
+ client.user.on_position(lambda *args: print("POSITION:", args))
153
+ client.user.on_trade(lambda *args: print("TRADE:", args))
154
+ client.user.on_account(lambda *args: print("ACCOUNT:", args))
155
+
156
+ await client.user.connect()
157
+ await client.user.subscribe_all(account_id=account.id)
158
+
159
+ # ...
160
+ ```
161
+
162
+ ## Token Refresh
163
+
164
+ Tokens expire after 24 hours. Refresh manually:
165
+
166
+ ```python
167
+ await client.refresh_token()
168
+ ```
169
+
170
+ ## Error Handling
171
+
172
+ ```python
173
+ from topstep import TopstepError, AuthenticationError, APIError, RateLimitError
174
+
175
+ try:
176
+ accounts = await client.accounts.search()
177
+ except AuthenticationError:
178
+ # Invalid credentials or expired token
179
+ await client.refresh_token()
180
+ except RateLimitError:
181
+ # HTTP 429 — too many requests (auto-retried 3 times before raising)
182
+ pass
183
+ except APIError as e:
184
+ # API returned success=False
185
+ print(f"API error [{e.error_code}]: {e}")
186
+ except TopstepError:
187
+ # Any other client error
188
+ pass
189
+ ```
190
+
191
+ ## Rate Limits
192
+
193
+ The API enforces these limits (handled automatically with retry + backoff):
194
+
195
+ - `History/retrieveBars`: 50 requests per 30 seconds
196
+ - All other endpoints: 200 requests per 60 seconds
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ git clone https://github.com/YOUR_USER/topstep-client-py.git
202
+ cd topstep-client-py
203
+ pip install -e ".[dev]"
204
+ pytest
205
+ ```
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "topstep-client-py"
7
+ version = "0.1.0"
8
+ description = "Lightweight async Python client for the TopstepX / ProjectX Gateway API"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ keywords = ["topstep", "projectx", "trading", "futures", "api", "propfirm"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Office/Business :: Financial :: Investment",
22
+ "Typing :: Typed",
23
+ ]
24
+ dependencies = [
25
+ "httpx>=0.27.0",
26
+ "pydantic>=2.0.0",
27
+ "signalrcore>=0.9.5",
28
+ "websocket-client>=0.54.0",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=8.0",
34
+ "pytest-asyncio>=0.24",
35
+ "respx>=0.22",
36
+ ]
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,55 @@
1
+ """TopstepX Python Client — async SDK for the ProjectX Gateway API."""
2
+
3
+ from topstep.client import TopstepClient
4
+ from topstep.exceptions import (
5
+ APIError,
6
+ AuthenticationError,
7
+ HTTPError,
8
+ RateLimitError,
9
+ TopstepError,
10
+ )
11
+ from topstep.models import (
12
+ Account,
13
+ Bar,
14
+ BarUnit,
15
+ Bracket,
16
+ Contract,
17
+ Order,
18
+ OrderSide,
19
+ OrderStatus,
20
+ OrderType,
21
+ PlaceOrderRequest,
22
+ Position,
23
+ PositionType,
24
+ Trade,
25
+ )
26
+ from topstep.realtime import MarketHub, UserHub
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "TopstepClient",
32
+ # Exceptions
33
+ "TopstepError",
34
+ "AuthenticationError",
35
+ "APIError",
36
+ "HTTPError",
37
+ "RateLimitError",
38
+ # Models
39
+ "Account",
40
+ "Bar",
41
+ "BarUnit",
42
+ "Bracket",
43
+ "Contract",
44
+ "Order",
45
+ "OrderSide",
46
+ "OrderStatus",
47
+ "OrderType",
48
+ "PlaceOrderRequest",
49
+ "Position",
50
+ "PositionType",
51
+ "Trade",
52
+ # Realtime
53
+ "MarketHub",
54
+ "UserHub",
55
+ ]
@@ -0,0 +1,50 @@
1
+ """Authentication and token management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from topstep.exceptions import AuthenticationError
6
+ from topstep.http import HTTPClient
7
+
8
+
9
+ async def login_key(http: HTTPClient, username: str, api_key: str) -> str:
10
+ """Authenticate with username + API key. Returns the session token."""
11
+ data = await http.post("/api/Auth/loginKey", {
12
+ "userName": username,
13
+ "apiKey": api_key,
14
+ })
15
+
16
+ token = data.get("token")
17
+ if not token:
18
+ raise AuthenticationError("Login succeeded but no token returned")
19
+
20
+ return token
21
+
22
+
23
+ async def login_app(
24
+ http: HTTPClient,
25
+ username: str,
26
+ password: str,
27
+ device_id: str,
28
+ app_id: str,
29
+ verify_key: str,
30
+ ) -> str:
31
+ """Authenticate as an authorized application. Returns the session token."""
32
+ data = await http.post("/api/Auth/loginApp", {
33
+ "userName": username,
34
+ "password": password,
35
+ "deviceId": device_id,
36
+ "appId": app_id,
37
+ "verifyKey": verify_key,
38
+ })
39
+
40
+ token = data.get("token")
41
+ if not token:
42
+ raise AuthenticationError("Login succeeded but no token returned")
43
+
44
+ return token
45
+
46
+
47
+ async def validate_token(http: HTTPClient) -> str | None:
48
+ """Validate the current token and return a refreshed one if available."""
49
+ data = await http.post("/api/Auth/validate")
50
+ return data.get("newToken") or data.get("token")