status302 1.5.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.
- status302-1.5.0/.gitignore +5 -0
- status302-1.5.0/PKG-INFO +139 -0
- status302-1.5.0/README.md +119 -0
- status302-1.5.0/pyproject.toml +42 -0
- status302-1.5.0/status302/__init__.py +69 -0
- status302-1.5.0/status302/_base.py +444 -0
- status302-1.5.0/status302/_client.py +100 -0
- status302-1.5.0/status302/_errors.py +125 -0
- status302-1.5.0/status302/_operations.py +9193 -0
- status302-1.5.0/status302/_ratelimit.py +81 -0
- status302-1.5.0/status302/_version.py +4 -0
- status302-1.5.0/status302/py.typed +0 -0
- status302-1.5.0/status302/stream.py +352 -0
- status302-1.5.0/status302/types.py +2898 -0
- status302-1.5.0/status302/webhooks.py +89 -0
status302-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: status302
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Official Python SDK for the Status302 API: typed REST client, realtime stream client and webhook verification.
|
|
5
|
+
Project-URL: Homepage, https://status302.com
|
|
6
|
+
Project-URL: Documentation, https://status302.com/docs/sdks/python/
|
|
7
|
+
Author: Status302
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: api,market data,status302,tickets,websocket
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Requires-Dist: httpx>=0.27
|
|
17
|
+
Requires-Dist: msgpack>=1.0
|
|
18
|
+
Requires-Dist: websockets>=13
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# status302 — Python SDK
|
|
22
|
+
|
|
23
|
+
Official Python client for the [Status302 API](https://status302.com/docs/): real-time ticket market data (catalog, metrics, sales, seat maps) over REST, a realtime WebSocket stream and signed webhooks.
|
|
24
|
+
|
|
25
|
+
- Typed methods for every public operation (`TypedDict` models generated from the public OpenAPI spec)
|
|
26
|
+
- Blocking `Status302` and asyncio `AsyncStatus302` clients on [httpx](https://www.python-httpx.org/)
|
|
27
|
+
- Automatic retries on `429` and `5xx` (idempotent requests) honouring `Retry-After`
|
|
28
|
+
- Pagination helpers (cursors and alert `nextBefore` ids) and typed errors (`PlanRequiredError`, `RateLimitError`, …) carrying the response's `request_id` (also appended to the message as `(request <id>)`; quote it to support)
|
|
29
|
+
- Conditional requests: `if_none_match` with a typed `NotModified` result, response ETags, and an opt-in ETag cache
|
|
30
|
+
- Stream client: bearer or ticket auth, reconnects with backoff, `since` resume per topic, resync notices, seq deduplication
|
|
31
|
+
- Webhook signature verification
|
|
32
|
+
|
|
33
|
+
The package version equals the version of the public API spec it was generated from.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install status302 # or: uv add status302
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python ≥ 3.11.
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from status302 import Status302
|
|
48
|
+
|
|
49
|
+
with Status302(api_key=os.environ["STATUS302_API_KEY"]) as status302:
|
|
50
|
+
page = status302.list_events(country="US", limit=5)
|
|
51
|
+
for event in page["items"]:
|
|
52
|
+
print(event["id"], event["name"], event["startsAt"])
|
|
53
|
+
|
|
54
|
+
metrics = status302.get_event_metrics(page["items"][0]["id"])
|
|
55
|
+
for row in metrics["items"]:
|
|
56
|
+
print(row["scope"], row["floor"], row["currency"]) # money is integer minor units
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Create an API key in the app under **Settings → Developers**. Keys look like `tx_live_<keyId>_<secret>` and are sent as `Authorization: Bearer …`.
|
|
60
|
+
|
|
61
|
+
### Pagination
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
for event in status302.paginate(status302.list_events, country="US", limit=200):
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`pages(...)` yields whole pages instead. Both follow `nextCursor` until it is null, or `nextBefore` (as `before`) for `list_alert_rules` and `list_alert_events`.
|
|
69
|
+
|
|
70
|
+
### Conditional requests
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from status302 import MemoryETagCache, NotModified, Status302
|
|
74
|
+
|
|
75
|
+
first = status302.with_response.get_event_metrics(4102) # APIResponse with .data, .etag, .status, .rate_limit
|
|
76
|
+
nxt = status302.get_event_metrics(4102, if_none_match=first.etag) # types.EventMetricsList | NotModified
|
|
77
|
+
if isinstance(nxt, NotModified):
|
|
78
|
+
print("unchanged since", nxt.etag)
|
|
79
|
+
|
|
80
|
+
cached = Status302(api_key=os.environ["STATUS302_API_KEY"], etag_cache=MemoryETagCache())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Operations that accept `If-None-Match` take `if_none_match`; a `304` returns `NotModified` instead of raising, and the return type includes it only when the argument is passed. `with_response` has every operation returning the whole `APIResponse`. With `etag_cache` the client revalidates every `GET` by itself and returns the stored body on `304`; `MemoryETagCache(max_entries=1000)` is thread-safe and LRU, and any object with `get`/`set` (the `ETagCache` protocol) works.
|
|
84
|
+
|
|
85
|
+
### Errors and rate limits
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from status302 import PlanRequiredError, RateLimitError
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
status302.list_movers(window="24h")
|
|
92
|
+
except PlanRequiredError as e:
|
|
93
|
+
print("needs plan", e.required_plan)
|
|
94
|
+
except RateLimitError as e:
|
|
95
|
+
print("retry in", e.retry_after, "s; quota:", e.rate_limit)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`429` responses are retried after `Retry-After` (up to `max_retry_after`, default 60 s); `5xx` and connection errors are retried for idempotent methods with exponential backoff (`max_retries`, default 3). `status302.request(...)` returns the body together with status, headers, ETag and the parsed `RateLimit` state.
|
|
99
|
+
|
|
100
|
+
### Realtime stream
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import asyncio, os
|
|
104
|
+
from status302 import AsyncStatus302, DataMessage, ResyncMessage
|
|
105
|
+
|
|
106
|
+
async def main() -> None:
|
|
107
|
+
async with AsyncStatus302(api_key=os.environ["STATUS302_API_KEY"]) as status302:
|
|
108
|
+
async with status302.stream() as stream:
|
|
109
|
+
stream.subscribe("sales", filter={"country": ["US"]})
|
|
110
|
+
async for message in stream:
|
|
111
|
+
if isinstance(message, DataMessage):
|
|
112
|
+
print(message.seq, message.data)
|
|
113
|
+
elif isinstance(message, ResyncMessage):
|
|
114
|
+
snapshot = await stream.fetch_snapshot(message)
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The key needs the `stream` scope to connect, and each topic the scope of its REST equivalent (`event.{id}.kpis` → `market:read`, `sales`/`event.{id}.sales` → `sales:read`, `event.{id}.primary` → `maps:read` or `market:read`, `onsales` → `catalog:read`, `alerts` → `alerts:write`); without it the subscription fails with a `forbidden_topic` `SubscriptionError`. Server-side code authenticates with the key on the upgrade request (`auth="bearer"`, default with a key); pass `auth="ticket"` and `get_ticket=` to connect with single-use tickets instead (same scopes as the key).
|
|
120
|
+
|
|
121
|
+
#### Binary encoding (MessagePack)
|
|
122
|
+
|
|
123
|
+
`stream(encoding="msgpack")` asks the server for binary MessagePack frames (`encoding=msgpack` on the URL) instead of JSON text. Messages and `data` payloads decode to the same values as with JSON, so iteration doesn't change; the client's own messages stay JSON. Default: `"json"`.
|
|
124
|
+
|
|
125
|
+
### Webhooks
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from status302 import verify_webhook, WebhookVerificationError
|
|
129
|
+
|
|
130
|
+
event = verify_webhook(raw_body, headers["Status302-Signature"], os.environ["STATUS302_WEBHOOK_SECRET"])
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Versioning
|
|
134
|
+
|
|
135
|
+
Releases share the version of the API spec they were generated from (`status302.__version__`); `/v1` changes are additive only, so new fields, operations and enum values arrive in minor releases. See the [changelog](https://status302.com/docs/changelog/).
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# status302 — Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Status302 API](https://status302.com/docs/): real-time ticket market data (catalog, metrics, sales, seat maps) over REST, a realtime WebSocket stream and signed webhooks.
|
|
4
|
+
|
|
5
|
+
- Typed methods for every public operation (`TypedDict` models generated from the public OpenAPI spec)
|
|
6
|
+
- Blocking `Status302` and asyncio `AsyncStatus302` clients on [httpx](https://www.python-httpx.org/)
|
|
7
|
+
- Automatic retries on `429` and `5xx` (idempotent requests) honouring `Retry-After`
|
|
8
|
+
- Pagination helpers (cursors and alert `nextBefore` ids) and typed errors (`PlanRequiredError`, `RateLimitError`, …) carrying the response's `request_id` (also appended to the message as `(request <id>)`; quote it to support)
|
|
9
|
+
- Conditional requests: `if_none_match` with a typed `NotModified` result, response ETags, and an opt-in ETag cache
|
|
10
|
+
- Stream client: bearer or ticket auth, reconnects with backoff, `since` resume per topic, resync notices, seq deduplication
|
|
11
|
+
- Webhook signature verification
|
|
12
|
+
|
|
13
|
+
The package version equals the version of the public API spec it was generated from.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install status302 # or: uv add status302
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Python ≥ 3.11.
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import os
|
|
27
|
+
from status302 import Status302
|
|
28
|
+
|
|
29
|
+
with Status302(api_key=os.environ["STATUS302_API_KEY"]) as status302:
|
|
30
|
+
page = status302.list_events(country="US", limit=5)
|
|
31
|
+
for event in page["items"]:
|
|
32
|
+
print(event["id"], event["name"], event["startsAt"])
|
|
33
|
+
|
|
34
|
+
metrics = status302.get_event_metrics(page["items"][0]["id"])
|
|
35
|
+
for row in metrics["items"]:
|
|
36
|
+
print(row["scope"], row["floor"], row["currency"]) # money is integer minor units
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Create an API key in the app under **Settings → Developers**. Keys look like `tx_live_<keyId>_<secret>` and are sent as `Authorization: Bearer …`.
|
|
40
|
+
|
|
41
|
+
### Pagination
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
for event in status302.paginate(status302.list_events, country="US", limit=200):
|
|
45
|
+
...
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`pages(...)` yields whole pages instead. Both follow `nextCursor` until it is null, or `nextBefore` (as `before`) for `list_alert_rules` and `list_alert_events`.
|
|
49
|
+
|
|
50
|
+
### Conditional requests
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from status302 import MemoryETagCache, NotModified, Status302
|
|
54
|
+
|
|
55
|
+
first = status302.with_response.get_event_metrics(4102) # APIResponse with .data, .etag, .status, .rate_limit
|
|
56
|
+
nxt = status302.get_event_metrics(4102, if_none_match=first.etag) # types.EventMetricsList | NotModified
|
|
57
|
+
if isinstance(nxt, NotModified):
|
|
58
|
+
print("unchanged since", nxt.etag)
|
|
59
|
+
|
|
60
|
+
cached = Status302(api_key=os.environ["STATUS302_API_KEY"], etag_cache=MemoryETagCache())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Operations that accept `If-None-Match` take `if_none_match`; a `304` returns `NotModified` instead of raising, and the return type includes it only when the argument is passed. `with_response` has every operation returning the whole `APIResponse`. With `etag_cache` the client revalidates every `GET` by itself and returns the stored body on `304`; `MemoryETagCache(max_entries=1000)` is thread-safe and LRU, and any object with `get`/`set` (the `ETagCache` protocol) works.
|
|
64
|
+
|
|
65
|
+
### Errors and rate limits
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from status302 import PlanRequiredError, RateLimitError
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
status302.list_movers(window="24h")
|
|
72
|
+
except PlanRequiredError as e:
|
|
73
|
+
print("needs plan", e.required_plan)
|
|
74
|
+
except RateLimitError as e:
|
|
75
|
+
print("retry in", e.retry_after, "s; quota:", e.rate_limit)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`429` responses are retried after `Retry-After` (up to `max_retry_after`, default 60 s); `5xx` and connection errors are retried for idempotent methods with exponential backoff (`max_retries`, default 3). `status302.request(...)` returns the body together with status, headers, ETag and the parsed `RateLimit` state.
|
|
79
|
+
|
|
80
|
+
### Realtime stream
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import asyncio, os
|
|
84
|
+
from status302 import AsyncStatus302, DataMessage, ResyncMessage
|
|
85
|
+
|
|
86
|
+
async def main() -> None:
|
|
87
|
+
async with AsyncStatus302(api_key=os.environ["STATUS302_API_KEY"]) as status302:
|
|
88
|
+
async with status302.stream() as stream:
|
|
89
|
+
stream.subscribe("sales", filter={"country": ["US"]})
|
|
90
|
+
async for message in stream:
|
|
91
|
+
if isinstance(message, DataMessage):
|
|
92
|
+
print(message.seq, message.data)
|
|
93
|
+
elif isinstance(message, ResyncMessage):
|
|
94
|
+
snapshot = await stream.fetch_snapshot(message)
|
|
95
|
+
|
|
96
|
+
asyncio.run(main())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The key needs the `stream` scope to connect, and each topic the scope of its REST equivalent (`event.{id}.kpis` → `market:read`, `sales`/`event.{id}.sales` → `sales:read`, `event.{id}.primary` → `maps:read` or `market:read`, `onsales` → `catalog:read`, `alerts` → `alerts:write`); without it the subscription fails with a `forbidden_topic` `SubscriptionError`. Server-side code authenticates with the key on the upgrade request (`auth="bearer"`, default with a key); pass `auth="ticket"` and `get_ticket=` to connect with single-use tickets instead (same scopes as the key).
|
|
100
|
+
|
|
101
|
+
#### Binary encoding (MessagePack)
|
|
102
|
+
|
|
103
|
+
`stream(encoding="msgpack")` asks the server for binary MessagePack frames (`encoding=msgpack` on the URL) instead of JSON text. Messages and `data` payloads decode to the same values as with JSON, so iteration doesn't change; the client's own messages stay JSON. Default: `"json"`.
|
|
104
|
+
|
|
105
|
+
### Webhooks
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from status302 import verify_webhook, WebhookVerificationError
|
|
109
|
+
|
|
110
|
+
event = verify_webhook(raw_body, headers["Status302-Signature"], os.environ["STATUS302_WEBHOOK_SECRET"])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Versioning
|
|
114
|
+
|
|
115
|
+
Releases share the version of the API spec they were generated from (`status302.__version__`); `/v1` changes are additive only, so new fields, operations and enum values arrive in minor releases. See the [changelog](https://status302.com/docs/changelog/).
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "status302"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Official Python SDK for the Status302 API: typed REST client, realtime stream client and webhook verification."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Status302" }]
|
|
9
|
+
keywords = ["status302", "tickets", "market data", "api", "websocket"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
dependencies = ["httpx>=0.27", "msgpack>=1.0", "websockets>=13"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://status302.com"
|
|
21
|
+
Documentation = "https://status302.com/docs/sdks/python/"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = ["pytest>=8", "pyyaml>=6", "mypy>=1.13"]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling>=1.26"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.version]
|
|
31
|
+
path = "status302/_version.py"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.sdist]
|
|
34
|
+
include = ["status302", "README.md"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
|
|
39
|
+
[tool.mypy]
|
|
40
|
+
strict = true
|
|
41
|
+
# tests/test_client.py asserts the overloads of the generated methods (assert_type).
|
|
42
|
+
files = ["status302", "examples", "tests/test_client.py"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Official Python SDK for the Status302 API."""
|
|
2
|
+
|
|
3
|
+
from . import types
|
|
4
|
+
from ._base import APIResponse, CachedResponse, ETagCache, MemoryETagCache, NotModified
|
|
5
|
+
from ._client import AsyncStatus302, Status302
|
|
6
|
+
from ._errors import (
|
|
7
|
+
APIConnectionError,
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
BadRequestError,
|
|
11
|
+
ConflictError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
PermissionDeniedError,
|
|
14
|
+
PlanRequiredError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
ServerError,
|
|
17
|
+
Status302Error,
|
|
18
|
+
)
|
|
19
|
+
from ._ratelimit import RateLimitInfo, RateLimitPolicy, parse_rate_limit, parse_retry_after
|
|
20
|
+
from ._version import __version__
|
|
21
|
+
from .stream import (
|
|
22
|
+
DataMessage,
|
|
23
|
+
ErrorMessage,
|
|
24
|
+
ResyncMessage,
|
|
25
|
+
StreamClosedError,
|
|
26
|
+
Subscription,
|
|
27
|
+
SubscriptionError,
|
|
28
|
+
Status302Stream,
|
|
29
|
+
stream_url,
|
|
30
|
+
)
|
|
31
|
+
from .webhooks import WebhookVerificationError, sign_webhook, verify_webhook
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"APIConnectionError",
|
|
35
|
+
"APIError",
|
|
36
|
+
"APIResponse",
|
|
37
|
+
"AsyncStatus302",
|
|
38
|
+
"AuthenticationError",
|
|
39
|
+
"BadRequestError",
|
|
40
|
+
"CachedResponse",
|
|
41
|
+
"ConflictError",
|
|
42
|
+
"DataMessage",
|
|
43
|
+
"ETagCache",
|
|
44
|
+
"ErrorMessage",
|
|
45
|
+
"MemoryETagCache",
|
|
46
|
+
"NotFoundError",
|
|
47
|
+
"NotModified",
|
|
48
|
+
"PermissionDeniedError",
|
|
49
|
+
"PlanRequiredError",
|
|
50
|
+
"RateLimitError",
|
|
51
|
+
"RateLimitInfo",
|
|
52
|
+
"RateLimitPolicy",
|
|
53
|
+
"ResyncMessage",
|
|
54
|
+
"ServerError",
|
|
55
|
+
"StreamClosedError",
|
|
56
|
+
"Subscription",
|
|
57
|
+
"SubscriptionError",
|
|
58
|
+
"Status302",
|
|
59
|
+
"Status302Error",
|
|
60
|
+
"Status302Stream",
|
|
61
|
+
"WebhookVerificationError",
|
|
62
|
+
"__version__",
|
|
63
|
+
"parse_rate_limit",
|
|
64
|
+
"parse_retry_after",
|
|
65
|
+
"sign_webhook",
|
|
66
|
+
"stream_url",
|
|
67
|
+
"types",
|
|
68
|
+
"verify_webhook",
|
|
69
|
+
]
|