seatlayer 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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-08-04
4
+
5
+ First public release: PyPI `seatlayer`.
6
+
7
+ Initial contents of the SeatLayer Python server SDK.
8
+
9
+ - `inventory.extend_hold` — keep a server-side hold alive past the checkout window.
10
+ - `charts.list` / `events.list` take `limit` and `cursor`; `list_all()` pages transparently as a
11
+ generator and skips the per-event availability fanout.
12
+ - `SeatLayer` client with secret-key auth, per-attempt timeouts, and a typed escape hatch.
13
+ - Resources: `charts`, `events`, `inventory`, `sessions`, `webhooks`, `workspaces`.
14
+ - Automatic `Idempotency-Key` on every mutation, reused across retries so a retried
15
+ booking cannot become two bookings.
16
+ - Retries on 429/408/5xx with exponential backoff and full jitter; honours `Retry-After`.
17
+ 4xx is never retried.
18
+ - Typed errors: `SeatLayerAuthError` (with `is_mode_mismatch`), `SeatLayerConflictError`
19
+ (with `conflicts` and `is_sold_out`), `SeatLayerRateLimitError`, `SeatLayerValidationError`,
20
+ `SeatLayerNotFoundError`, `SeatLayerConnectionError`.
21
+ - `verify_webhook` — raw-body HMAC-SHA256 verification via `hmac.compare_digest`.
22
+ - `create_manage_session` requires explicit `capabilities`; the API's default grants
23
+ `event:cancel`, which unbooks paid seats and authorises gateway refunds.
24
+ - Constructor rejects a `pk_` key by name rather than failing as a 401 later.
25
+ - Ships a PEP 561 `py.typed` marker, so the annotations are visible to mypy and pyright in
26
+ your own project rather than only used internally.
27
+ - Zero runtime dependencies; standard library only.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SeatLayer
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,337 @@
1
+ Metadata-Version: 2.4
2
+ Name: seatlayer
3
+ Version: 0.1.0
4
+ Summary: Official Python server SDK for the SeatLayer reserved-seating API.
5
+ Project-URL: Homepage, https://seatlayer.io
6
+ Project-URL: Documentation, https://docs.seatlayer.io/server-sdk/install/
7
+ Project-URL: Changelog, https://github.com/seatlayer/seatlayer-python/blob/main/CHANGELOG.md
8
+ Project-URL: Source, https://github.com/seatlayer/seatlayer-python
9
+ Project-URL: Issues, https://github.com/seatlayer/seatlayer-python/issues
10
+ Author-email: SeatLayer <hello@seatlayer.io>
11
+ Maintainer-email: SeatLayer <hello@seatlayer.io>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: box-office,reserved-seating,seating-chart,seatlayer,ticketing
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Office/Business
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Provides-Extra: dev
30
+ Requires-Dist: mypy>=1.11; extra == 'dev'
31
+ Requires-Dist: pytest>=8; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # SeatLayer Python SDK
36
+
37
+ Official Python server SDK for the [SeatLayer](https://seatlayer.io) reserved-seating API.
38
+
39
+ > **Server-side only.** This package authenticates with your secret key. Never run it anywhere a
40
+ > ticket buyer can reach — browser surfaces get short-lived, origin-bound tokens that you mint here.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install seatlayer
46
+ ```
47
+
48
+ Requires Python 3.10 or newer. No runtime dependencies.
49
+
50
+ ## Quick start
51
+
52
+ ```python
53
+ import os
54
+ from seatlayer import SeatLayer
55
+
56
+ seatlayer = SeatLayer(os.environ["SEATLAYER_SECRET_KEY"])
57
+
58
+ # 1. Provision a venue for a new organiser from one of your templates.
59
+ chart = seatlayer.charts.copy("c_template_arena")["meta"]
60
+ seatlayer.charts.publish(chart["id"])
61
+
62
+ # 2. Create an event on it.
63
+ event = seatlayer.events.create(chart_id=chart["id"], name="Spring Gala")["meta"]
64
+
65
+ # 3. Sell four seats over the phone.
66
+ held = seatlayer.inventory.hold_best_available(event["key"], qty=4)
67
+ # … take payment against held["items"], which carry authoritative prices …
68
+ seatlayer.inventory.book(event["key"], hold_id=held["holdId"], booking_ref="order-8842")
69
+ ```
70
+
71
+ ## Test vs live
72
+
73
+ Keys carry their own mode. `sk_test_…` keys can only touch test-mode events and `sk_live_…` only
74
+ live ones; crossing them returns `403 mode_mismatch`, surfaced as `SeatLayerAuthError` with
75
+ `is_mode_mismatch`.
76
+
77
+ ```python
78
+ seatlayer = SeatLayer(os.environ["SEATLAYER_SECRET_KEY"])
79
+ if os.environ.get("ENV") == "production" and seatlayer.mode != "live":
80
+ raise RuntimeError("Refusing to boot production against test-mode seating data.")
81
+ ```
82
+
83
+ ## The two selling flows
84
+
85
+ **Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and
86
+ books. Never price from what the browser sent you — `retrieve_hold` is authoritative.
87
+
88
+ ```python
89
+ hold = seatlayer.inventory.retrieve_hold(event_key, hold_id)
90
+ total = sum(item["unitPrice"] for item in hold["items"])
91
+ # … charge `total` in hold["currency"] …
92
+ seatlayer.inventory.book(event_key, hold_id=hold_id, booking_ref=charge.id)
93
+ ```
94
+
95
+ **Your backend picks the seats.** Phone orders, box office, comps.
96
+
97
+ ```python
98
+ # Payment already taken — book outright, so nothing is stranded if a second call fails.
99
+ seatlayer.inventory.book_best_available(event_key, qty=2, booking_ref="phone-1183")
100
+
101
+ # Or name the seats yourself.
102
+ seatlayer.inventory.box_office_book(event_key, labels=["A-1", "A-2"], booking_ref="comp-14")
103
+ ```
104
+
105
+ ## Listing and pagination
106
+
107
+ `list()` returns one page plus a `nextCursor`. When you want everything, `list_all()` pages for you
108
+ and yields as it goes — a generator rather than a list, because the point of paginating is to *not*
109
+ hold an unbounded result set in memory.
110
+
111
+ ```python
112
+ # One page, your own paging.
113
+ page = seatlayer.events.list(limit=50)
114
+ page["events"]
115
+ page.get("nextCursor") # absent once exhausted
116
+
117
+ # Or let the SDK walk it.
118
+ for event in seatlayer.events.list_all():
119
+ sync(event)
120
+ ```
121
+
122
+ Listing events includes live availability `counts` by default, which costs the server one
123
+ round-trip **per event**. `list_all()` turns them off automatically — walking a whole catalogue is
124
+ exactly when you don't want that — and you can control it explicitly:
125
+
126
+ ```python
127
+ seatlayer.events.list(limit=50, counts=False)
128
+ ```
129
+
130
+ ## Keeping a hold alive
131
+
132
+ When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than
133
+ release and re-hold. Releasing first hands the seats to whoever is racing for them in between.
134
+
135
+ ```python
136
+ from seatlayer import SeatLayerConflictError
137
+
138
+ try:
139
+ seatlayer.inventory.extend_hold(event_key, hold_id, ttl_ms=10 * 60_000)
140
+ except SeatLayerConflictError:
141
+ # Gone, expired, or at its renewal cap — the buyer has to re-pick.
142
+ ...
143
+ ```
144
+
145
+ ## Embedding the control room
146
+
147
+ Your secret key never reaches a browser. Mint a scoped token instead.
148
+
149
+ ```python
150
+ session = seatlayer.sessions.create_manage_session(
151
+ event_key,
152
+ allowed_origin="https://box-office.yourplatform.com",
153
+ capabilities=["event:view", "event:block"],
154
+ expires_in_seconds=3600,
155
+ )
156
+ ```
157
+
158
+ `capabilities` is **required** by this SDK even though the API defaults it. Omit it at the API level
159
+ and you get `event:view`, `event:block`, `event:cancel` and `event:reports` — including
160
+ `event:cancel`, which unbooks paid seats **and authorises refunds against the organiser's connected
161
+ payment gateway**. That is real money, moved by a token you handed to a browser; it should not
162
+ arrive by forgetting an argument. Grant the smallest set the page needs.
163
+
164
+ The full set, all opt-in:
165
+
166
+ | Capability | Grants |
167
+ |---|---|
168
+ | `event:view` | Read the seat map and its live states |
169
+ | `event:block` | Block and unblock seats |
170
+ | `event:cancel` | Unbook paid seats and issue gateway refunds — destructive, moves money |
171
+ | `event:reports` | Read sales and availability reports |
172
+ | `event:channels:view` | Read sales channels and their allocations |
173
+ | `event:channels:manage` | Create, pause and archive channels; rotate access links |
174
+
175
+ The two `event:channels:*` capabilities are **not** in the default — a token minted before sales
176
+ channels existed must not silently acquire channel authority — so ask for them explicitly if the
177
+ page manages channels.
178
+
179
+ The same pattern embeds the Designer in your own UI:
180
+
181
+ ```python
182
+ chart = seatlayer.charts.create(name="Riverside Theatre")["meta"]
183
+ designer = seatlayer.sessions.create_designer_session(
184
+ workspace_id=workspace_id,
185
+ chart_id=chart["id"],
186
+ allowed_origin="https://app.yourplatform.com",
187
+ authority="edit",
188
+ )
189
+ ```
190
+
191
+ ## Webhooks
192
+
193
+ Verify every delivery against the **raw** body. Re-serialising it changes the bytes and
194
+ verification will fail.
195
+
196
+ ```python
197
+ from flask import request
198
+ from seatlayer import verify_webhook, WebhookVerificationError
199
+
200
+ @app.post("/webhooks/seatlayer")
201
+ def seatlayer_webhook():
202
+ try:
203
+ event = verify_webhook(
204
+ request.get_data(), # raw bytes, not request.json
205
+ request.headers.get("X-SeatLayer-Signature"),
206
+ os.environ["SEATLAYER_WEBHOOK_SECRET"],
207
+ )
208
+ except WebhookVerificationError:
209
+ return "", 400
210
+
211
+ # The signed body carries `at`, but nothing enforces a freshness window, so
212
+ # a captured delivery stays valid indefinitely. Deduplicate on occurrenceId —
213
+ # this is your replay protection, not an optimisation.
214
+ if already_processed(event["occurrenceId"]):
215
+ return "", 200
216
+
217
+ handle(event)
218
+ return "", 200
219
+ ```
220
+
221
+ ## Errors
222
+
223
+ ```python
224
+ from seatlayer import SeatLayerAuthError, SeatLayerConflictError, SeatLayerRateLimitError
225
+
226
+ try:
227
+ seatlayer.inventory.hold_best_available(event_key, qty=6)
228
+ except SeatLayerConflictError as error:
229
+ if error.is_sold_out:
230
+ return show_alternative_dates() # a business outcome, not a bug
231
+ raise
232
+ except SeatLayerRateLimitError as error:
233
+ return retry_after(error.retry_after_seconds)
234
+ except SeatLayerAuthError as error:
235
+ if error.is_mode_mismatch:
236
+ raise RuntimeError("Test key pointed at a live event (or the reverse.)") from error
237
+ raise
238
+ ```
239
+
240
+ Every error carries `status`, `code`, `body`, and `request_id` — quote the request id in support
241
+ requests.
242
+
243
+ ## Reliability
244
+
245
+ **Retries.** 429, 408 and 5xx are retried with exponential backoff and full jitter; `Retry-After`
246
+ wins when the server sends it. 4xx is never retried — it will not start succeeding.
247
+
248
+ **Idempotency.** Every mutating request carries an `Idempotency-Key`, generated if you do not supply
249
+ one, and **reused across retries** so a retried booking cannot become two bookings. Pass your own
250
+ order id for end-to-end deduplication:
251
+
252
+ ```python
253
+ seatlayer.inventory.book(event_key, hold_id=hold_id, idempotency_key=f"order-{order_id}")
254
+ ```
255
+
256
+ ```python
257
+ SeatLayer(
258
+ os.environ["SEATLAYER_SECRET_KEY"],
259
+ max_retries=3, # total attempts
260
+ timeout=30.0, # seconds, per attempt
261
+ )
262
+ ```
263
+
264
+ ## Escape hatch
265
+
266
+ For surface this SDK does not wrap yet — same auth, retries, idempotency and error mapping:
267
+
268
+ ```python
269
+ seatlayer.request("POST", "/v1/events/ev_1/some-new-route", body={...})
270
+ ```
271
+
272
+ ## API surface
273
+
274
+ | Resource | Methods |
275
+ | --- | --- |
276
+ | `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
277
+ | `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
278
+ | `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
279
+ | `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
280
+ | `webhooks` | `list` `create` `update` `delete` `list_deliveries` |
281
+ | `workspaces` | `list` `create` `retrieve` `update` |
282
+
283
+ Full reference: [docs.seatlayer.io/server-api](https://docs.seatlayer.io/server-api/)
284
+
285
+ ### Deliberately not in this SDK
286
+
287
+ Some API surface is intentionally unwrapped, not merely pending:
288
+
289
+ - **Hosted-checkout orders and refunds.** Reading or refunding a SeatLayer-hosted-checkout sale is
290
+ not a server-SDK capability. Those records only exist for organisations using hosted checkout; if
291
+ you run your own commerce store you refund in that store, through your own gateway.
292
+ - **Connecting or assigning payment gateways.** Connecting one is a dashboard flow, so shipping only
293
+ the assignment half across seven SDKs would hand you a method that cannot yet succeed.
294
+ - **Realtime seat updates.** Live seat state reaches the *browser* through the widget's own socket.
295
+ There is no server-side subscribe; a secret-key caller gets authoritative state from
296
+ `events.retrieve_report()` and `inventory.retrieve_availability()`.
297
+
298
+ None of these are reachable through `request()` as a supported path either — they are excluded from
299
+ the public manifest, not just from the wrapper.
300
+
301
+ ## Related resources
302
+
303
+ - [Server SDK guide](https://docs.seatlayer.io/server-sdk/install/)
304
+ - [Errors, retries and idempotency](https://docs.seatlayer.io/server-sdk/reliability/)
305
+ - [Webhook verification](https://docs.seatlayer.io/server-sdk/webhooks/)
306
+ - [Server API reference](https://docs.seatlayer.io/server-api/events/)
307
+ - [OpenAPI description](https://docs.seatlayer.io/openapi.json)
308
+ - [Agent-readable documentation](https://docs.seatlayer.io/llms.txt)
309
+ - [SeatLayer GitHub organization](https://github.com/seatlayer)
310
+
311
+ ### Other SeatLayer SDKs
312
+
313
+ | Surface | Package |
314
+ |---|---|
315
+ | Browser (vanilla) | [`@seatlayer/js`](https://github.com/seatlayer/seatlayer-sdk) |
316
+ | React | [`@seatlayer/react`](https://github.com/seatlayer/seatlayer-sdk) |
317
+ | React Native | [`@seatlayer/react-native`](https://github.com/seatlayer/seatlayer-react-native) |
318
+ | iOS | [`seatlayer-ios`](https://github.com/seatlayer/seatlayer-ios) |
319
+ | Android | [`seatlayer-android`](https://github.com/seatlayer/seatlayer-android) |
320
+ | Flutter | [`seatlayer_flutter`](https://github.com/seatlayer/seatlayer-flutter) |
321
+ | Node.js (server) | [`@seatlayer/server`](https://github.com/seatlayer/seatlayer-node) |
322
+ | PHP (server) | [`seatlayer/seatlayer-php`](https://github.com/seatlayer/seatlayer-php) |
323
+ | Java (server) | [`io.seatlayer:seatlayer-java`](https://github.com/seatlayer/seatlayer-java) |
324
+ | Go (server) | [`github.com/seatlayer/seatlayer-go`](https://github.com/seatlayer/seatlayer-go) |
325
+ | Ruby (server) | [`seatlayer`](https://github.com/seatlayer/seatlayer-ruby) |
326
+ | .NET (server) | [`SeatLayer`](https://github.com/seatlayer/seatlayer-dotnet) |
327
+
328
+ ## Development
329
+
330
+ ```bash
331
+ pip install -e ".[dev]"
332
+ ruff check src tests && mypy && pytest -q
333
+ ```
334
+
335
+ ## License
336
+
337
+ MIT