From 72c725e02c46b734f6ff9e7d3f349b791d4d24c8 Mon Sep 17 00:00:00 2001 From: navin10sharma <3096611+navin10sharma@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:21:46 +0530 Subject: [PATCH 1/2] feat: add Performance Groups server resource --- src/seatlayer/client.py | 2 + src/seatlayer/resources.py | 141 +++++++++++++++++++++++++++++++++++++ tests/test_client.py | 56 +++++++++++++++ 3 files changed, 199 insertions(+) diff --git a/src/seatlayer/client.py b/src/seatlayer/client.py index 0145855..c62d86b 100644 --- a/src/seatlayer/client.py +++ b/src/seatlayer/client.py @@ -15,6 +15,7 @@ Charts, Events, Inventory, + PerformanceGroups, Sessions, Templates, Webhooks, @@ -45,6 +46,7 @@ def __init__( self.templates = Templates(self._http) self.events = Events(self._http) self.inventory = Inventory(self._http) + self.performance_groups = PerformanceGroups(self._http) self.channels = Channels(self._http) self.sessions = Sessions(self._http) self.webhooks = Webhooks(self._http) diff --git a/src/seatlayer/resources.py b/src/seatlayer/resources.py index d94f0e0..0636201 100644 --- a/src/seatlayer/resources.py +++ b/src/seatlayer/resources.py @@ -1152,3 +1152,144 @@ def update(self, workspace_id: str, **fields: Any) -> Any: 409 ``default_workspace_required``. Promote another one first. """ return self._http.patch(f"/v1/workspaces/{quote(workspace_id)}", body=fields) + + +class PerformanceGroups: + """Fixed multi-performance runs, kept entirely on your trusted server. + + Mint the one-time browser bearer here, then give it to + ``PerformanceGroupPicker`` in the browser SDK. Lifecycle and booking calls + remain secret-key operations because they coordinate inventory across every + performance in the run. + """ + + def __init__(self, http: HttpClient) -> None: + self._http = http + + @staticmethod + def _path(performance_group_key: str, suffix: str = "") -> str: + return f"/v1/performance-groups/{quote(performance_group_key)}{suffix}" + + def list( + self, + workspace_id: str | None = None, + external_ref: str | None = None, + state: Literal["draft", "active", "closing", "closed", "archived"] | None = None, + limit: int | None = None, + cursor: str | None = None, + ) -> Any: + return self._http.get("/v1/performance-groups", query={ + "workspaceId": workspace_id, + "externalRef": external_ref, + "state": state, + "limit": limit, + "cursor": cursor, + }) + + def create( + self, + name: str, + event_keys: Sequence[str], + external_ref: str | None | _Unset = _UNSET, + idempotency_key: str | None = None, + ) -> Any: + """Create a draft run with exact idempotency replay.""" + body: dict[str, Any] = {"name": name, "eventKeys": list(event_keys)} + if external_ref is not _UNSET: + body["externalRef"] = external_ref + return self._http.post_with_header_replay( + "/v1/performance-groups", body=body, idempotency_key=idempotency_key + ) + + def retrieve(self, performance_group_key: str) -> Any: + return self._http.get(self._path(performance_group_key)) + + def delete(self, performance_group_key: str) -> None: + """Delete a draft only. Activated runs remain available for audit.""" + self._http.delete(self._path(performance_group_key)) + + def activate(self, performance_group_key: str, expected_revision: int) -> Any: + """Start activation; poll ``retrieve_lifecycle`` if it is not terminal.""" + return self._http.post( + self._path(performance_group_key, "/activate"), + body={"expectedRevision": expected_revision}, + ) + + def close(self, performance_group_key: str, expected_revision: int) -> Any: + """Stop new sales; poll the returned lifecycle operation until terminal.""" + return self._http.post( + self._path(performance_group_key, "/close"), + body={"expectedRevision": expected_revision}, + ) + + def retrieve_lifecycle(self, performance_group_key: str, operation_id: str) -> Any: + return self._http.get( + self._path(performance_group_key, f"/lifecycle/{quote(operation_id)}") + ) + + def create_buyer_access_session( + self, + performance_group_key: str, + allowed_origin: str, + include_public: bool, + channel_ids_by_event: dict[str, Sequence[str]] | None = None, + expires_in_seconds: int | None = None, + max_quantity: int | None = None, + buyer_ref: str | None = None, + partner_ref: str | None = None, + ) -> Any: + """Reveal one browser token. Never retry this call automatically.""" + body: dict[str, Any] = { + "allowedOrigin": allowed_origin, + "includePublic": include_public, + } + for key, value in ( + ("channelIdsByEvent", channel_ids_by_event), + ("expiresInSeconds", expires_in_seconds), + ("maxQuantity", max_quantity), + ("buyerRef", buyer_ref), + ("partnerRef", partner_ref), + ): + if value is not None: + body[key] = value + return self._http.post( + self._path(performance_group_key, "/buyer-access-sessions"), body=body + ) + + def list_buyer_access_sessions( + self, performance_group_key: str, limit: int | None = None + ) -> Any: + return self._http.get( + self._path(performance_group_key, "/buyer-access-sessions"), + query={"limit": limit}, + ) + + def revoke_buyer_access_session( + self, performance_group_key: str, session_id: str + ) -> Any: + return self._http.delete( + self._path(performance_group_key, f"/buyer-access-sessions/{quote(session_id)}") + ) + + def retrieve_hold(self, performance_group_key: str, operation_id: str) -> Any: + return self._http.get( + self._path(performance_group_key, f"/holds/{quote(operation_id)}") + ) + + def book_hold( + self, + performance_group_key: str, + operation_id: str, + book_action_id: str, + booking_ref: str, + ) -> Any: + """Book a committed hold using stable IDs; poll ``retrieve_booking`` if pending.""" + return self._http.post( + self._path(performance_group_key, f"/holds/{quote(operation_id)}/book"), + body={"bookActionId": book_action_id, "bookingRef": booking_ref}, + ) + + def retrieve_booking(self, performance_group_key: str, action_id: str) -> Any: + return self._http.get( + self._path(performance_group_key, f"/bookings/{quote(action_id)}") + ) diff --git a/tests/test_client.py b/tests/test_client.py index a4dcbd3..47f0148 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -174,6 +174,62 @@ def test_drops_none_query_parameters(self) -> None: sdk.charts.list(workspace_id="ws_1") assert calls[0].full_url == "https://api.seatlayer.io/v1/charts?workspaceId=ws_1" + def test_maps_the_full_performance_group_lifecycle(self) -> None: + sdk, calls = make_client([ + {"status": 200, "body": {"performanceGroups": [], "nextCursor": None}}, + {"status": 201, "body": {"performanceGroup": {}}}, + {"status": 200, "body": {"performanceGroup": {}}}, + {"status": 204, "body": None}, + {"status": 202, "body": {"lifecycleOperation": {"terminal": False}}}, + {"status": 200, "body": {"lifecycleOperation": {"terminal": True}}}, + {"status": 200, "body": {"lifecycleOperation": {}}}, + {"status": 201, "body": {"token": "bsg_secret"}}, + {"status": 200, "body": {"sessions": []}}, + {"status": 200, "body": {"ok": True}}, + {"status": 200, "body": {"hold": {}}}, + {"status": 202, "body": {"booking": {"state": "book_pending"}}}, + {"status": 200, "body": {"booking": {"state": "booked"}}}, + ]) + group_key = "pg_a/b" + + sdk.performance_groups.list(workspace_id="ws_1", state="draft") + sdk.performance_groups.create( + "Weekend run", ["ev_1", "ev_2"], idempotency_key="weekend-run-1" + ) + sdk.performance_groups.retrieve(group_key) + assert sdk.performance_groups.delete(group_key) is None + sdk.performance_groups.activate(group_key, 1) + sdk.performance_groups.close(group_key, 2) + sdk.performance_groups.retrieve_lifecycle(group_key, "pga_1") + sdk.performance_groups.create_buyer_access_session( + group_key, "https://tickets.example.test", True + ) + sdk.performance_groups.list_buyer_access_sessions(group_key, limit=25) + sdk.performance_groups.revoke_buyer_access_session(group_key, "pgbs_1") + sdk.performance_groups.retrieve_hold(group_key, "pgh_1") + sdk.performance_groups.book_hold(group_key, "pgh_1", "book_1", "order_1") + sdk.performance_groups.retrieve_booking(group_key, "book_1") + + base = "https://api.seatlayer.io/v1/performance-groups/pg_a%2Fb" + assert calls[0].full_url == ( + "https://api.seatlayer.io/v1/performance-groups?workspaceId=ws_1&state=draft" + ) + assert calls[1].full_url.endswith("/v1/performance-groups") + assert calls[1].get_header("Idempotency-key") == "weekend-run-1" + assert calls[2].full_url == base + assert calls[3].method == "DELETE" + assert calls[4].full_url == f"{base}/activate" + assert calls[5].full_url == f"{base}/close" + assert calls[6].full_url == f"{base}/lifecycle/pga_1" + assert calls[7].full_url == f"{base}/buyer-access-sessions" + assert calls[7].get_header("Idempotency-key") is None + assert calls[8].full_url == f"{base}/buyer-access-sessions?limit=25" + assert calls[9].full_url == f"{base}/buyer-access-sessions/pgbs_1" + assert calls[10].full_url == f"{base}/holds/pgh_1" + assert calls[11].full_url == f"{base}/holds/pgh_1/book" + assert calls[11].get_header("Idempotency-key") is None + assert calls[12].full_url == f"{base}/bookings/book_1" + class TestErrors: @pytest.mark.parametrize( From ff32f75a2b8c209f1e34cfffa9b5a489f88d22ed Mon Sep 17 00:00:00 2001 From: navin10sharma <3096611+navin10sharma@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:27:52 +0530 Subject: [PATCH 2/2] release: prepare 0.5.0 --- CHANGELOG.md | 8 ++++++++ pyproject.toml | 2 +- src/seatlayer/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d9dea9..deb8068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +## 0.5.0 — 2026-08-21 + +- Added `performance_groups`, the trusted server resource for fixed two-to-eight + performance runs. It creates and activates groups, mints one-time browser + access, retrieves authoritative group holds, and confirms bookings with + stable action and order references. Browser-only group routes remain outside + this secret-key SDK. + - Added the public template-instantiation resource and ticket-release list, replace, and close operations. Template instantiation uses exact header replay; ticket-release mutations remain deliberately single-attempt. diff --git a/pyproject.toml b/pyproject.toml index b299cc7..9d33788 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" [project] name = "seatlayer" -version = "0.4.0" +version = "0.5.0" description = "Official Python server SDK for the SeatLayer reserved-seating API." readme = "README.md" license = "MIT" diff --git a/src/seatlayer/__init__.py b/src/seatlayer/__init__.py index a721898..d762d6d 100644 --- a/src/seatlayer/__init__.py +++ b/src/seatlayer/__init__.py @@ -46,7 +46,7 @@ ) from .webhooks import WebhookVerificationError, verify_webhook -__version__ = "0.4.0" +__version__ = "0.5.0" __all__ = [ "AccessLink",