predigy-edge-sdk 2.0.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,289 @@
1
+ Metadata-Version: 2.4
2
+ Name: predigy-edge-sdk
3
+ Version: 2.0.0
4
+ Summary: Official Python SDK for the EDGE by Predigy prediction market API (retail, parlay, compliance/MICS, LP)
5
+ Author: Predigy LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://edge-by-predigy.netlify.app
8
+ Project-URL: Repository, https://github.com/predigy/edge
9
+ Project-URL: Documentation, https://edge-production-7b77.up.railway.app/docs
10
+ Project-URL: Bug Tracker, https://github.com/predigy/edge/issues
11
+ Keywords: edge,predigy,prediction-market,lmsr,sdk,retail,parlay,compliance,mics,liquidity-provider
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: httpx>=0.25.0
21
+ Requires-Dist: pydantic>=2.0.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
25
+
26
+ # EDGE Python SDK
27
+
28
+ Official Python SDK for the [EDGE by Predigy](https://edge-by-predigy.netlify.app) prediction market API.
29
+
30
+ EDGE is a prediction market pricing engine that integrates with sportsbook platforms. This SDK provides a fully-typed async client for all API operations.
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install edge-sdk
38
+ ```
39
+
40
+ **Requirements:** Python 3.11+
41
+
42
+ ---
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ import asyncio
48
+ from edge_sdk import EdgeClient
49
+
50
+ async def main():
51
+ async with EdgeClient(
52
+ base_url="https://edge-production-7b77.up.railway.app",
53
+ api_key="your-api-key",
54
+ ) as client:
55
+ # List open markets
56
+ result = await client.list_markets(status="OPEN")
57
+ for market in result.markets:
58
+ print(f"{market.title}: YES={market.prices.yes:.1%}, NO={market.prices.no:.1%}")
59
+
60
+ # Get a quote before trading
61
+ quote = await client.get_quote("mkt_abc123", side="YES", amount=50.0)
62
+ print(f"Cost: ${quote.total_cost:.2f} for {quote.contracts:.1f} contracts")
63
+
64
+ # Execute the trade
65
+ trade = await client.execute_trade("mkt_abc123", side="YES", amount=50.0)
66
+ print(f"Trade {trade.trade_id} executed! New balance: ${trade.new_balance:.2f}")
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Authentication
74
+
75
+ Every request requires an API key passed in the `X-API-Key` header. The SDK handles this automatically:
76
+
77
+ ```python
78
+ client = EdgeClient(
79
+ base_url="https://edge-production-7b77.up.railway.app",
80
+ api_key="your-api-key",
81
+ )
82
+ ```
83
+
84
+ You receive your API key when your operator account is created by the Predigy team.
85
+
86
+ ---
87
+
88
+ ## API Reference
89
+
90
+ ### Markets
91
+
92
+ ```python
93
+ # List markets with optional filters
94
+ markets = await client.list_markets(status="OPEN", category="NBA", limit=10)
95
+
96
+ # Get a single market by external ID
97
+ market = await client.get_market("mkt_abc123")
98
+
99
+ # Create a new market (admin)
100
+ market = await client.create_market(
101
+ title="Lakers vs Celtics — Lakers Win",
102
+ category="NBA",
103
+ description="Will the Lakers win tonight's game?",
104
+ b_base=5000.0,
105
+ initial_price_yes=0.55,
106
+ )
107
+ ```
108
+
109
+ ### Quotes and Trades
110
+
111
+ ```python
112
+ # Get a price quote (does not execute a trade)
113
+ quote = await client.get_quote("mkt_abc123", side="YES", amount=100.0)
114
+ print(f"Contracts: {quote.contracts}")
115
+ print(f"Avg price: ${quote.avg_fill_price:.4f}")
116
+ print(f"Fee: ${quote.fee:.2f} ({quote.fee_rate:.2%})")
117
+ print(f"Total cost: ${quote.total_cost:.2f}")
118
+
119
+ # Execute a trade
120
+ trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0)
121
+
122
+ # Execute with slippage protection
123
+ trade = await client.execute_trade(
124
+ "mkt_abc123", side="YES", amount=100.0,
125
+ max_avg_price=0.60, # Reject if avg price exceeds $0.60
126
+ )
127
+
128
+ # Sell (cash out) contracts from an existing position
129
+ sell = await client.sell_position("mkt_abc123", side="YES", contracts=50.0)
130
+ print(f"Net payout: ${sell.net_payout:.2f}")
131
+ ```
132
+
133
+ ### Portfolio
134
+
135
+ ```python
136
+ portfolio = await client.get_portfolio()
137
+ print(f"Balance: ${portfolio.balance:.2f}")
138
+ print(f"Unrealized P&L: ${portfolio.total_unrealized_pnl:.2f}")
139
+
140
+ for pos in portfolio.positions:
141
+ print(f" {pos.market_title} ({pos.side}): {pos.contracts} contracts, P&L: ${pos.unrealized_pnl:.2f}")
142
+ ```
143
+
144
+ ### Admin Operations
145
+
146
+ ```python
147
+ # Get platform statistics
148
+ stats = await client.get_stats()
149
+ print(f"Total markets: {stats.total_markets}")
150
+ print(f"Total volume: ${stats.total_volume:,.2f}")
151
+
152
+ # Settle a market
153
+ result = await client.settle_market("mkt_abc123", outcome="YES")
154
+
155
+ # Reset sandbox data
156
+ await client.reset_sandbox()
157
+ ```
158
+
159
+ ### Webhooks
160
+
161
+ ```python
162
+ # Register a webhook endpoint
163
+ webhook = await client.create_webhook(
164
+ url="https://your-app.com/webhook",
165
+ events=["trade.executed", "market.settled"],
166
+ description="Production trade notifications",
167
+ )
168
+ print(f"Webhook ID: {webhook.webhook.external_id}")
169
+ print(f"Secret: {webhook.secret}") # Store this — shown only once!
170
+
171
+ # List webhooks
172
+ webhooks = await client.list_webhooks()
173
+
174
+ # Delete a webhook
175
+ await client.delete_webhook("whk_abc123")
176
+ ```
177
+
178
+ ### Health Check
179
+
180
+ ```python
181
+ health = await client.health_check()
182
+ print(health) # {"status": "healthy"}
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Error Handling
188
+
189
+ The SDK raises typed exceptions for different error scenarios:
190
+
191
+ ```python
192
+ from edge_sdk.exceptions import (
193
+ EdgeAPIError, # Base class for all API errors
194
+ EdgeAuthError, # 401 — Invalid or missing API key
195
+ EdgeRateLimitError, # 429 — Too many requests
196
+ EdgeValidationError,# 422 — Invalid request data
197
+ )
198
+
199
+ try:
200
+ trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0)
201
+ except EdgeAuthError as e:
202
+ print(f"Authentication failed: {e.detail}")
203
+ except EdgeRateLimitError as e:
204
+ print(f"Rate limited. Retry after {e.retry_after} seconds")
205
+ except EdgeValidationError as e:
206
+ print(f"Invalid request: {e.detail}")
207
+ except EdgeAPIError as e:
208
+ print(f"API error {e.status_code}: {e.detail}")
209
+ print(f"Request ID: {e.request_id}") # Useful for support
210
+ ```
211
+
212
+ All exceptions include a `request_id` field that you can reference when contacting support.
213
+
214
+ ---
215
+
216
+ ## Webhook Verification
217
+
218
+ When receiving webhook deliveries, verify the HMAC-SHA256 signature to ensure the payload is authentic:
219
+
220
+ ```python
221
+ from edge_sdk import verify_signature
222
+
223
+ # In your webhook handler (e.g., FastAPI)
224
+ @app.post("/webhook")
225
+ async def handle_webhook(request: Request):
226
+ body = await request.body()
227
+ signature = request.headers.get("X-Edge-Signature", "")
228
+
229
+ if not verify_signature(body, signature, WEBHOOK_SECRET):
230
+ raise HTTPException(401, "Invalid signature")
231
+
232
+ event = json.loads(body)
233
+ print(f"Received event: {event['event_type']}")
234
+ # Process event...
235
+ ```
236
+
237
+ The `X-Edge-Signature` header format is `sha256=<hex_digest>`.
238
+
239
+ **Webhook event types:**
240
+ - `trade.executed` — A trade was placed
241
+ - `market.created` — A new market was created
242
+ - `market.settled` — A market was settled with an outcome
243
+ - `market.suspended` — A market was suspended
244
+ - `surge.activated` — Surge pricing was triggered
245
+ - `liquidity.adjusted` — Dynamic liquidity parameter changed
246
+
247
+ ---
248
+
249
+ ## Advanced Usage
250
+
251
+ ### Custom HTTP Client
252
+
253
+ You can provide your own `httpx.AsyncClient` for custom timeouts, proxies, or connection pooling:
254
+
255
+ ```python
256
+ import httpx
257
+
258
+ custom_client = httpx.AsyncClient(
259
+ base_url="https://edge-production-7b77.up.railway.app",
260
+ timeout=60.0,
261
+ headers={"X-API-Key": "your-api-key", "Content-Type": "application/json"},
262
+ limits=httpx.Limits(max_connections=20),
263
+ )
264
+
265
+ client = EdgeClient(
266
+ base_url="https://edge-production-7b77.up.railway.app",
267
+ api_key="your-api-key",
268
+ http_client=custom_client,
269
+ )
270
+ ```
271
+
272
+ ### Type Safety
273
+
274
+ The SDK is fully typed with Pydantic models. All responses are validated and provide IDE autocompletion. The `py.typed` marker (PEP 561) enables type checking in tools like mypy and pyright.
275
+
276
+ ---
277
+
278
+ ## Links
279
+
280
+ - **API Documentation:** [docs/API.md](../../docs/API.md)
281
+ - **Integration Guide:** [docs/EDGE_INTEGRATION_GUIDE.md](../../docs/EDGE_INTEGRATION_GUIDE.md)
282
+ - **Live API (Swagger):** [edge-production-7b77.up.railway.app/docs](https://edge-production-7b77.up.railway.app/docs)
283
+ - **Frontend Demo:** [edge-by-predigy.netlify.app](https://edge-by-predigy.netlify.app)
284
+
285
+ ---
286
+
287
+ ## License
288
+
289
+ Proprietary. Copyright Predigy LLC. All rights reserved.
@@ -0,0 +1,264 @@
1
+ # EDGE Python SDK
2
+
3
+ Official Python SDK for the [EDGE by Predigy](https://edge-by-predigy.netlify.app) prediction market API.
4
+
5
+ EDGE is a prediction market pricing engine that integrates with sportsbook platforms. This SDK provides a fully-typed async client for all API operations.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install edge-sdk
13
+ ```
14
+
15
+ **Requirements:** Python 3.11+
16
+
17
+ ---
18
+
19
+ ## Quick Start
20
+
21
+ ```python
22
+ import asyncio
23
+ from edge_sdk import EdgeClient
24
+
25
+ async def main():
26
+ async with EdgeClient(
27
+ base_url="https://edge-production-7b77.up.railway.app",
28
+ api_key="your-api-key",
29
+ ) as client:
30
+ # List open markets
31
+ result = await client.list_markets(status="OPEN")
32
+ for market in result.markets:
33
+ print(f"{market.title}: YES={market.prices.yes:.1%}, NO={market.prices.no:.1%}")
34
+
35
+ # Get a quote before trading
36
+ quote = await client.get_quote("mkt_abc123", side="YES", amount=50.0)
37
+ print(f"Cost: ${quote.total_cost:.2f} for {quote.contracts:.1f} contracts")
38
+
39
+ # Execute the trade
40
+ trade = await client.execute_trade("mkt_abc123", side="YES", amount=50.0)
41
+ print(f"Trade {trade.trade_id} executed! New balance: ${trade.new_balance:.2f}")
42
+
43
+ asyncio.run(main())
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Authentication
49
+
50
+ Every request requires an API key passed in the `X-API-Key` header. The SDK handles this automatically:
51
+
52
+ ```python
53
+ client = EdgeClient(
54
+ base_url="https://edge-production-7b77.up.railway.app",
55
+ api_key="your-api-key",
56
+ )
57
+ ```
58
+
59
+ You receive your API key when your operator account is created by the Predigy team.
60
+
61
+ ---
62
+
63
+ ## API Reference
64
+
65
+ ### Markets
66
+
67
+ ```python
68
+ # List markets with optional filters
69
+ markets = await client.list_markets(status="OPEN", category="NBA", limit=10)
70
+
71
+ # Get a single market by external ID
72
+ market = await client.get_market("mkt_abc123")
73
+
74
+ # Create a new market (admin)
75
+ market = await client.create_market(
76
+ title="Lakers vs Celtics — Lakers Win",
77
+ category="NBA",
78
+ description="Will the Lakers win tonight's game?",
79
+ b_base=5000.0,
80
+ initial_price_yes=0.55,
81
+ )
82
+ ```
83
+
84
+ ### Quotes and Trades
85
+
86
+ ```python
87
+ # Get a price quote (does not execute a trade)
88
+ quote = await client.get_quote("mkt_abc123", side="YES", amount=100.0)
89
+ print(f"Contracts: {quote.contracts}")
90
+ print(f"Avg price: ${quote.avg_fill_price:.4f}")
91
+ print(f"Fee: ${quote.fee:.2f} ({quote.fee_rate:.2%})")
92
+ print(f"Total cost: ${quote.total_cost:.2f}")
93
+
94
+ # Execute a trade
95
+ trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0)
96
+
97
+ # Execute with slippage protection
98
+ trade = await client.execute_trade(
99
+ "mkt_abc123", side="YES", amount=100.0,
100
+ max_avg_price=0.60, # Reject if avg price exceeds $0.60
101
+ )
102
+
103
+ # Sell (cash out) contracts from an existing position
104
+ sell = await client.sell_position("mkt_abc123", side="YES", contracts=50.0)
105
+ print(f"Net payout: ${sell.net_payout:.2f}")
106
+ ```
107
+
108
+ ### Portfolio
109
+
110
+ ```python
111
+ portfolio = await client.get_portfolio()
112
+ print(f"Balance: ${portfolio.balance:.2f}")
113
+ print(f"Unrealized P&L: ${portfolio.total_unrealized_pnl:.2f}")
114
+
115
+ for pos in portfolio.positions:
116
+ print(f" {pos.market_title} ({pos.side}): {pos.contracts} contracts, P&L: ${pos.unrealized_pnl:.2f}")
117
+ ```
118
+
119
+ ### Admin Operations
120
+
121
+ ```python
122
+ # Get platform statistics
123
+ stats = await client.get_stats()
124
+ print(f"Total markets: {stats.total_markets}")
125
+ print(f"Total volume: ${stats.total_volume:,.2f}")
126
+
127
+ # Settle a market
128
+ result = await client.settle_market("mkt_abc123", outcome="YES")
129
+
130
+ # Reset sandbox data
131
+ await client.reset_sandbox()
132
+ ```
133
+
134
+ ### Webhooks
135
+
136
+ ```python
137
+ # Register a webhook endpoint
138
+ webhook = await client.create_webhook(
139
+ url="https://your-app.com/webhook",
140
+ events=["trade.executed", "market.settled"],
141
+ description="Production trade notifications",
142
+ )
143
+ print(f"Webhook ID: {webhook.webhook.external_id}")
144
+ print(f"Secret: {webhook.secret}") # Store this — shown only once!
145
+
146
+ # List webhooks
147
+ webhooks = await client.list_webhooks()
148
+
149
+ # Delete a webhook
150
+ await client.delete_webhook("whk_abc123")
151
+ ```
152
+
153
+ ### Health Check
154
+
155
+ ```python
156
+ health = await client.health_check()
157
+ print(health) # {"status": "healthy"}
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Error Handling
163
+
164
+ The SDK raises typed exceptions for different error scenarios:
165
+
166
+ ```python
167
+ from edge_sdk.exceptions import (
168
+ EdgeAPIError, # Base class for all API errors
169
+ EdgeAuthError, # 401 — Invalid or missing API key
170
+ EdgeRateLimitError, # 429 — Too many requests
171
+ EdgeValidationError,# 422 — Invalid request data
172
+ )
173
+
174
+ try:
175
+ trade = await client.execute_trade("mkt_abc123", side="YES", amount=100.0)
176
+ except EdgeAuthError as e:
177
+ print(f"Authentication failed: {e.detail}")
178
+ except EdgeRateLimitError as e:
179
+ print(f"Rate limited. Retry after {e.retry_after} seconds")
180
+ except EdgeValidationError as e:
181
+ print(f"Invalid request: {e.detail}")
182
+ except EdgeAPIError as e:
183
+ print(f"API error {e.status_code}: {e.detail}")
184
+ print(f"Request ID: {e.request_id}") # Useful for support
185
+ ```
186
+
187
+ All exceptions include a `request_id` field that you can reference when contacting support.
188
+
189
+ ---
190
+
191
+ ## Webhook Verification
192
+
193
+ When receiving webhook deliveries, verify the HMAC-SHA256 signature to ensure the payload is authentic:
194
+
195
+ ```python
196
+ from edge_sdk import verify_signature
197
+
198
+ # In your webhook handler (e.g., FastAPI)
199
+ @app.post("/webhook")
200
+ async def handle_webhook(request: Request):
201
+ body = await request.body()
202
+ signature = request.headers.get("X-Edge-Signature", "")
203
+
204
+ if not verify_signature(body, signature, WEBHOOK_SECRET):
205
+ raise HTTPException(401, "Invalid signature")
206
+
207
+ event = json.loads(body)
208
+ print(f"Received event: {event['event_type']}")
209
+ # Process event...
210
+ ```
211
+
212
+ The `X-Edge-Signature` header format is `sha256=<hex_digest>`.
213
+
214
+ **Webhook event types:**
215
+ - `trade.executed` — A trade was placed
216
+ - `market.created` — A new market was created
217
+ - `market.settled` — A market was settled with an outcome
218
+ - `market.suspended` — A market was suspended
219
+ - `surge.activated` — Surge pricing was triggered
220
+ - `liquidity.adjusted` — Dynamic liquidity parameter changed
221
+
222
+ ---
223
+
224
+ ## Advanced Usage
225
+
226
+ ### Custom HTTP Client
227
+
228
+ You can provide your own `httpx.AsyncClient` for custom timeouts, proxies, or connection pooling:
229
+
230
+ ```python
231
+ import httpx
232
+
233
+ custom_client = httpx.AsyncClient(
234
+ base_url="https://edge-production-7b77.up.railway.app",
235
+ timeout=60.0,
236
+ headers={"X-API-Key": "your-api-key", "Content-Type": "application/json"},
237
+ limits=httpx.Limits(max_connections=20),
238
+ )
239
+
240
+ client = EdgeClient(
241
+ base_url="https://edge-production-7b77.up.railway.app",
242
+ api_key="your-api-key",
243
+ http_client=custom_client,
244
+ )
245
+ ```
246
+
247
+ ### Type Safety
248
+
249
+ The SDK is fully typed with Pydantic models. All responses are validated and provide IDE autocompletion. The `py.typed` marker (PEP 561) enables type checking in tools like mypy and pyright.
250
+
251
+ ---
252
+
253
+ ## Links
254
+
255
+ - **API Documentation:** [docs/API.md](../../docs/API.md)
256
+ - **Integration Guide:** [docs/EDGE_INTEGRATION_GUIDE.md](../../docs/EDGE_INTEGRATION_GUIDE.md)
257
+ - **Live API (Swagger):** [edge-production-7b77.up.railway.app/docs](https://edge-production-7b77.up.railway.app/docs)
258
+ - **Frontend Demo:** [edge-by-predigy.netlify.app](https://edge-by-predigy.netlify.app)
259
+
260
+ ---
261
+
262
+ ## License
263
+
264
+ Proprietary. Copyright Predigy LLC. All rights reserved.
@@ -0,0 +1,35 @@
1
+ """
2
+ EDGE by Predigy — Official Python SDK
3
+
4
+ Usage:
5
+ from edge_sdk import EdgeClient
6
+
7
+ async with EdgeClient(base_url="...", api_key="...") as client:
8
+ markets = await client.list_markets() # 1.x flat
9
+ ticket = await client.retail.mint_ticket(...) # 2.x grouped
10
+ parlay = await client.parlay.create_parlay(...) # 2.x grouped
11
+ report = await client.compliance.daily_results(...) # 2.x grouped
12
+ lp = await client.lp.designate_lp(...) # 2.x grouped
13
+ """
14
+
15
+ __version__ = "2.0.0"
16
+
17
+ from edge_sdk.client import EdgeClient
18
+ from edge_sdk.compliance import ComplianceClient
19
+ from edge_sdk.lp import LPClient
20
+ from edge_sdk.parlay import ParlayClient
21
+ from edge_sdk.retail import RetailClient
22
+ from edge_sdk.webhook import verify_signature
23
+ from edge_sdk.exceptions import EdgeAPIError, EdgeAuthError, EdgeRateLimitError
24
+
25
+ __all__ = [
26
+ "EdgeClient",
27
+ "RetailClient",
28
+ "ParlayClient",
29
+ "ComplianceClient",
30
+ "LPClient",
31
+ "verify_signature",
32
+ "EdgeAPIError",
33
+ "EdgeAuthError",
34
+ "EdgeRateLimitError",
35
+ ]
@@ -0,0 +1,69 @@
1
+ """
2
+ Internal HTTP client for EdgeClient and all 2.0 sub-clients.
3
+
4
+ Kept in a leading-underscore module so it stays private. Sub-clients
5
+ receive an instance via constructor injection. Same status-code -> typed
6
+ error mapping the 1.x EdgeClient._request used, now shareable.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+ from edge_sdk.exceptions import (
15
+ EdgeAPIError,
16
+ EdgeAuthError,
17
+ EdgeRateLimitError,
18
+ EdgeValidationError,
19
+ )
20
+
21
+
22
+ class HttpClient:
23
+ def __init__(self, client: httpx.AsyncClient) -> None:
24
+ self._client = client
25
+
26
+ @property
27
+ def raw(self) -> httpx.AsyncClient:
28
+ """Raw httpx client — exposed so EdgeClient.close() can reach it."""
29
+ return self._client
30
+
31
+ async def request(
32
+ self,
33
+ method: str,
34
+ path: str,
35
+ json: dict | None = None,
36
+ params: dict | None = None,
37
+ ) -> Any:
38
+ """Make an authenticated API request with typed-error mapping."""
39
+ if params:
40
+ params = {k: v for k, v in params.items() if v is not None}
41
+
42
+ response = await self._client.request(method, path, json=json, params=params)
43
+ request_id = response.headers.get("x-request-id")
44
+
45
+ if response.status_code == 401:
46
+ raise EdgeAuthError(self._detail(response), request_id)
47
+ if response.status_code == 429:
48
+ retry_after = response.headers.get("retry-after")
49
+ raise EdgeRateLimitError(
50
+ self._detail(response),
51
+ int(retry_after) if retry_after else None,
52
+ request_id,
53
+ )
54
+ if response.status_code == 422:
55
+ raise EdgeValidationError(self._detail(response), request_id)
56
+ if response.status_code >= 400:
57
+ raise EdgeAPIError(response.status_code, self._detail(response), request_id)
58
+
59
+ if response.status_code == 204:
60
+ return None
61
+ return response.json()
62
+
63
+ @staticmethod
64
+ def _detail(response: httpx.Response) -> str:
65
+ try:
66
+ body = response.json()
67
+ return body.get("detail", response.reason_phrase or "Unknown error")
68
+ except Exception:
69
+ return response.reason_phrase or "Unknown error"