Skip to content

Commit 5719578

Browse files
committed
Add template and ticket release APIs
1 parent 72d73ac commit 5719578

8 files changed

Lines changed: 207 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
## Unreleased
44

5+
- Added the public template-instantiation resource and ticket-release list, replace, and close
6+
operations. Template instantiation uses exact header replay; ticket-release mutations remain
7+
deliberately single-attempt.
8+
- Prepared the additive `0.4.0` package version and replaced the obsolete chart-copy template
9+
quick start with the public template catalog flow.
10+
511
- **Security/reliability:** Mutations now default to a single attempt. Automatic header-replay
6-
retries are limited to chart create/copy, event create, and workspace create, preventing
12+
retries are limited to chart create/copy, template instantiation, event create, and workspace create, preventing
713
transient failures from duplicating holds or best-available results and from issuing extra
814
show-once credentials.
915
- Aligned event, inventory, webhook, manage-session, and Designer requests with the generated

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ from seatlayer import SeatLayer
2626

2727
seatlayer = SeatLayer(os.environ["SEATLAYER_SECRET_KEY"])
2828

29-
# 1. Provision a venue for a new organiser from one of your templates.
30-
chart = seatlayer.charts.copy("c_template_arena")["meta"]
29+
# 1. Provision a venue from the public template catalog as a new draft chart.
30+
chart = seatlayer.templates.instantiate_template("tpl_arena")["meta"]
3131
seatlayer.charts.publish(chart["id"])
3232

3333
# 2. Create an event on it.
@@ -251,7 +251,7 @@ requests.
251251
## Reliability
252252

253253
**Retries and idempotency.** Reads retry 408, 429 and 5xx responses with backoff. Only chart create,
254-
chart copy, event create and workspace create opt into mutation retries: the SDK generates one
254+
chart copy, template instantiation, event create and workspace create opt into mutation retries: the SDK generates one
255255
`Idempotency-Key` and reuses it for every attempt. All other mutations are single-attempt, even if
256256
you supply a key.
257257

@@ -263,7 +263,7 @@ before trying again.
263263
```python
264264
SeatLayer(
265265
os.environ["SEATLAYER_SECRET_KEY"],
266-
max_retries=3, # attempts for reads and the four replay-safe creates
266+
max_retries=3, # attempts for reads and the five replay-safe creates
267267
timeout=30.0, # seconds, per attempt
268268
)
269269
```
@@ -282,7 +282,8 @@ seatlayer.request("POST", "/v1/events/ev_1/some-new-route", body={...})
282282
| Resource | Methods |
283283
| --- | --- |
284284
| `charts` | `list` `list_all` `create` `retrieve` `update` `delete` `copy` `archive` `unarchive` `publish` |
285-
| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
285+
| `templates` | `instantiate_template` |
286+
| `events` | `list` `list_all` `create` `retrieve` `update` `delete` `update_poster` `delete_poster` `update_chart` `close` `reopen` `archive` `list_ticket_releases` `update_ticket_releases` `close_ticket_release` `retrieve_hold_ttl` `update_hold_ttl` `retrieve_report` `retrieve_log` |
286287
| `inventory` | `hold` `hold_best_available` `book_best_available` `extend_hold` `retrieve_hold` `release` `book` `box_office_book` `unbook` `list_bookings` `retrieve_booking` `block` `unblock` `unblock_all` `retrieve_availability` `update_availability` |
287288
| `channels` | `list_channels` `create_channel` `update_channel` `update_assignments` `list_allocation` `retrieve_access_preview` `retrieve_report` `pause` `unpause` `archive` `create_buyer_access_session` `list_buyer_access_sessions` `revoke_buyer_access_session` `create_access_link` `list_access_links` `rotate_access_link` `revoke_access_link` |
288289
| `sessions` | `create_manage_session` `revoke_manage_session` `create_designer_session` `revoke_designer_session` |
@@ -329,7 +330,7 @@ the public manifest, not just from the wrapper.
329330
| Flutter | [`seatlayer`](https://pub.dev/packages/seatlayer) |
330331
| Node.js (server) | [`@seatlayer/server`](https://www.npmjs.com/package/@seatlayer/server) |
331332
| PHP (server) | [`seatlayer/seatlayer-php`](https://packagist.org/packages/seatlayer/seatlayer-php) |
332-
| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java/0.1.0) |
333+
| Java (server) | [`io.seatlayer:seatlayer-java`](https://central.sonatype.com/artifact/io.seatlayer/seatlayer-java) |
333334
| Go (server) | [`github.com/seatlayer/seatlayer-go`](https://pkg.go.dev/github.com/seatlayer/seatlayer-go) |
334335
| Ruby (server) | [`seatlayer`](https://rubygems.org/gems/seatlayer) |
335336
| .NET (server) | [`SeatLayer`](https://www.nuget.org/packages/SeatLayer) |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
88

99
[project]
1010
name = "seatlayer"
11-
version = "0.3.0"
11+
version = "0.4.0"
1212
description = "Official Python server SDK for the SeatLayer reserved-seating API."
1313
readme = "README.md"
1414
license = "MIT"

src/seatlayer/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
InventoryItem,
3232
ManageCapability,
3333
ManageSession,
34+
TemplateInstantiateRequest,
35+
TicketRelease,
36+
TicketReleaseAction,
37+
TicketReleaseList,
38+
TicketReleaseReplaceInput,
3439
WebhookCreateEnvelope,
3540
WebhookDelivery,
3641
WebhookDeliveryPage,
@@ -41,7 +46,7 @@
4146
)
4247
from .webhooks import WebhookVerificationError, verify_webhook
4348

44-
__version__ = "0.3.0"
49+
__version__ = "0.4.0"
4550

4651
__all__ = [
4752
"AccessLink",
@@ -69,6 +74,11 @@
6974
"SeatLayerNotFoundError",
7075
"SeatLayerRateLimitError",
7176
"SeatLayerValidationError",
77+
"TemplateInstantiateRequest",
78+
"TicketRelease",
79+
"TicketReleaseAction",
80+
"TicketReleaseList",
81+
"TicketReleaseReplaceInput",
7282
"WebhookCreateEnvelope",
7383
"WebhookDelivery",
7484
"WebhookDeliveryPage",

src/seatlayer/client.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@
1010
from typing import Any
1111

1212
from .http import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, HttpClient
13-
from .resources import Channels, Charts, Events, Inventory, Sessions, Webhooks, Workspaces
13+
from .resources import (
14+
Channels,
15+
Charts,
16+
Events,
17+
Inventory,
18+
Sessions,
19+
Templates,
20+
Webhooks,
21+
Workspaces,
22+
)
1423

1524

1625
class SeatLayer:
@@ -33,6 +42,7 @@ def __init__(
3342
self.mode = self._http.mode
3443

3544
self.charts = Charts(self._http)
45+
self.templates = Templates(self._http)
3646
self.events = Events(self._http)
3747
self.inventory = Inventory(self._http)
3848
self.channels = Channels(self._http)

src/seatlayer/resources.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
HoldInspection,
2121
ManageCapability,
2222
ManageSession,
23+
TemplateInstantiateRequest,
24+
TicketReleaseList,
25+
TicketReleaseReplaceInput,
2326
WebhookCreateEnvelope,
2427
WebhookDeliveryPage,
2528
WebhookEnvelope,
@@ -184,6 +187,42 @@ def publish(self, chart_id: str) -> Any:
184187
return self._http.post(f"/v1/charts/{quote(chart_id)}/publish")
185188

186189

190+
class Templates:
191+
"""Versioned catalog templates that can be instantiated as draft charts."""
192+
193+
def __init__(self, http: HttpClient) -> None:
194+
self._http = http
195+
196+
def instantiate_template(
197+
self,
198+
template_id: str,
199+
*,
200+
name: str | None = None,
201+
workspace_id: str | None = None,
202+
edited_doc: dict[str, Any] | None = None,
203+
version: int | None = None,
204+
sha256: str | None = None,
205+
idempotency_key: str | None = None,
206+
) -> Any:
207+
"""Create a draft chart from a catalog template using exact header replay."""
208+
body: TemplateInstantiateRequest = {}
209+
if name is not None:
210+
body["name"] = name
211+
if workspace_id is not None:
212+
body["workspaceId"] = workspace_id
213+
if edited_doc is not None:
214+
body["editedDoc"] = edited_doc
215+
if version is not None:
216+
body["version"] = version
217+
if sha256 is not None:
218+
body["sha256"] = sha256
219+
return self._http.post_with_header_replay(
220+
f"/v1/templates/{quote(template_id)}/instantiate",
221+
body=body,
222+
idempotency_key=idempotency_key,
223+
)
224+
225+
187226
class Events:
188227
def __init__(self, http: HttpClient) -> None:
189228
self._http = http
@@ -324,6 +363,33 @@ def reopen(self, event_key: str) -> Any:
324363
def archive(self, event_key: str) -> Any:
325364
return self._http.post(f"/v1/events/{quote(event_key)}/archive")
326365

366+
def list_ticket_releases(self, event_key: str) -> TicketReleaseList:
367+
return cast(
368+
TicketReleaseList,
369+
self._http.get(f"/v1/events/{quote(event_key)}/releases"),
370+
)
371+
372+
def update_ticket_releases(
373+
self,
374+
event_key: str,
375+
releases: Sequence[TicketReleaseReplaceInput],
376+
) -> TicketReleaseList:
377+
return cast(
378+
TicketReleaseList,
379+
self._http.put(
380+
f"/v1/events/{quote(event_key)}/releases",
381+
body={"releases": list(releases)},
382+
),
383+
)
384+
385+
def close_ticket_release(self, event_key: str, release_id: str) -> TicketReleaseList:
386+
return cast(
387+
TicketReleaseList,
388+
self._http.post(
389+
f"/v1/events/{quote(event_key)}/releases/{quote(release_id)}/close"
390+
),
391+
)
392+
327393
def retrieve_hold_ttl(self, event_key: str) -> Any:
328394
return self._http.get(f"/v1/events/{quote(event_key)}/hold-ttl")
329395

src/seatlayer/types.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,56 @@
3030
"event:boxoffice",
3131
]
3232

33+
TicketReleaseAction = Literal["buy", "apply", "invoice"]
34+
35+
36+
class TemplateInstantiateRequest(TypedDict, total=False):
37+
name: str
38+
workspaceId: str
39+
editedDoc: dict[str, Any]
40+
version: int
41+
sha256: str
42+
43+
44+
class _TicketReleaseOptional(TypedDict, total=False):
45+
consumed: int
46+
remaining: int | None
47+
48+
49+
class TicketRelease(_TicketReleaseOptional):
50+
id: str
51+
position: int
52+
name: str
53+
categoryKey: str | None
54+
price: int
55+
previousPrice: int | None
56+
quota: int | None
57+
startsAt: int | None
58+
endsAt: int | None
59+
action: TicketReleaseAction
60+
actionUrl: str | None
61+
soldOutAt: int | None
62+
63+
64+
class _TicketReleaseReplaceOptional(TypedDict, total=False):
65+
id: str | None
66+
categoryKey: str | None
67+
previousPrice: int | None
68+
quota: int | None
69+
startsAt: int | None
70+
endsAt: int | None
71+
action: TicketReleaseAction
72+
actionUrl: str | None
73+
74+
75+
class TicketReleaseReplaceInput(_TicketReleaseReplaceOptional):
76+
name: str
77+
price: int
78+
79+
80+
class TicketReleaseList(TypedDict):
81+
releases: list[TicketRelease]
82+
3383

3484
class _InventoryItemOptional(TypedDict, total=False):
3585
quantity: int

tests/test_client.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,51 @@ def test_generated_idempotency_key_only_on_header_replay_mutations(self) -> None
113113
{"status": 201, "body": {}},
114114
{"status": 201, "body": {}},
115115
{"status": 201, "body": {}},
116+
{"status": 201, "body": {}},
116117
{"status": 200, "body": {}},
117118
])
118119
sdk.events.list()
119120
sdk.events.create(chart_id="c_1")
120121
sdk.charts.create(name="Arena")
121122
sdk.charts.copy("c_1")
122123
sdk.workspaces.create(name="Promoter")
124+
sdk.templates.instantiate_template("tpl_1")
123125
sdk.inventory.hold("ev_1", labels=["A-1"])
124126

125127
assert calls[0].get_header("Idempotency-key") is None
126-
for call in calls[1:5]:
128+
for call in calls[1:6]:
127129
assert call.get_header("Idempotency-key")
128-
assert calls[5].get_header("Idempotency-key") is None
130+
assert calls[6].get_header("Idempotency-key") is None
131+
132+
def test_template_and_ticket_release_wire_contracts(self) -> None:
133+
sdk, calls = make_client([
134+
{"status": 201, "body": {"meta": {"id": "c_1"}}},
135+
{"status": 200, "body": {"releases": []}},
136+
{"status": 200, "body": {"releases": []}},
137+
{"status": 200, "body": {"releases": []}},
138+
])
139+
140+
sdk.templates.instantiate_template("tpl/a")
141+
sdk.events.list_ticket_releases("ev/a")
142+
sdk.events.update_ticket_releases(
143+
"ev/a",
144+
[{"name": "Early", "price": 2500, "action": "buy"}],
145+
)
146+
sdk.events.close_ticket_release("ev/a", "rel/a")
147+
148+
assert calls[0].full_url.endswith("/v1/templates/tpl%2Fa/instantiate")
149+
assert json.loads(calls[0].data) == {}
150+
assert calls[0].get_header("Idempotency-key")
151+
assert calls[1].method == "GET"
152+
assert calls[1].full_url.endswith("/v1/events/ev%2Fa/releases")
153+
assert calls[2].method == "PUT"
154+
assert json.loads(calls[2].data) == {
155+
"releases": [{"name": "Early", "price": 2500, "action": "buy"}]
156+
}
157+
assert calls[2].get_header("Idempotency-key") is None
158+
assert calls[3].method == "POST"
159+
assert calls[3].full_url.endswith("/v1/events/ev%2Fa/releases/rel%2Fa/close")
160+
assert calls[3].get_header("Idempotency-key") is None
129161

130162
def test_honours_caller_supplied_idempotency_key(self) -> None:
131163
sdk, calls = make_client([{"status": 201, "body": {}}])
@@ -242,6 +274,26 @@ def test_retries_429_and_reuses_idempotency_key(self) -> None:
242274
# Same key on the retry, or the server would create two events.
243275
assert calls[0].get_header("Idempotency-key") == calls[1].get_header("Idempotency-key")
244276

277+
def test_template_instantiate_retries_but_release_mutations_do_not(self) -> None:
278+
template, template_calls = make_client([
279+
{"status": 503, "body": {"error": "unavailable"}},
280+
{"status": 201, "body": {"meta": {"id": "c_1"}}},
281+
])
282+
template.templates.instantiate_template("tpl_1")
283+
assert len(template_calls) == 2
284+
assert template_calls[0].get_header("Idempotency-key") == template_calls[1].get_header(
285+
"Idempotency-key"
286+
)
287+
288+
releases, release_calls = make_client([
289+
{"status": 503, "body": {"error": "unavailable"}},
290+
])
291+
with pytest.raises(SeatLayerError):
292+
releases.events.update_ticket_releases(
293+
"ev_1", [{"name": "Early", "price": 2500}]
294+
)
295+
assert len(release_calls) == 1
296+
245297
def test_booking_mutations_are_single_attempt_even_with_a_supplied_key(self) -> None:
246298
sdk, calls = make_client([
247299
{"status": 500, "body": {"error": "internal"}},

0 commit comments

Comments
 (0)