From 7783d32471b4352e38c7e263f9e45877d598730c Mon Sep 17 00:00:00 2001 From: navin10sharma <3096611+navin10sharma@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:28:52 +0530 Subject: [PATCH] Docs: specific README intro, plain punctuation, current release guide --- README.md | 34 ++++----- RELEASE.md | 214 ++++++++++------------------------------------------- 2 files changed, 57 insertions(+), 191 deletions(-) diff --git a/README.md b/README.md index fef3a71..61e2fe1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Python](https://img.shields.io/pypi/pyversions/seatlayer.svg)](https://pypi.org/project/seatlayer/) [![License: MIT](https://img.shields.io/badge/license-MIT-111827.svg)](LICENSE) -SeatLayer is interactive seating chart software built for stadium scale. Platforms embed the white-label seat picker with their own checkout; organizers sell seated events on their own website with their own payment gateway. +The official Python client for the SeatLayer API. The `seatlayer` package lets a Python backend inspect seat holds, price orders from server data, book reserved seats and verify webhooks, with no runtime dependencies. SeatLayer is seating chart and reserved-seat ticketing software built for venues up to stadium scale. SeatLayer's official Python server SDK is the trusted side of its reserved seating and seat booking API. Inspect what a hold really contains, price from server-owned seating-chart data, @@ -94,7 +94,7 @@ Version `0.7.0` exposes all 48 trusted organizer operations through After the test hold/book/cancel journey and matching webhook deliveries, `validate_season_buyer_rehearsal(season_key)` sends no evidence body; SeatLayer discovers the retained chain automatically. Retrieved Season holds contain -inventory identity, not an authoritative amount—your platform owns package +inventory identity, not an authoritative amount. Your platform owns package price, payment, order, tax, refunds, benefits, and ticket or pass delivery. ```python @@ -123,7 +123,7 @@ if os.environ.get("ENV") == "production" and seatlayer.mode != "live": ## Book reserved seats from Python **Buyer picks seats in the browser.** Your frontend holds them; your backend confirms the price and -books. Never price from what the browser sent you — `retrieve_hold` is authoritative. +books. Never price from what the browser sent you: `retrieve_hold` is authoritative. ```python hold = seatlayer.inventory.retrieve_hold(event_key, hold_id) @@ -139,7 +139,7 @@ seatlayer.inventory.book(event_key, hold_id=hold_id, booking_ref=charge.id) **Your backend picks the seats.** Phone orders, box office, comps. ```python -# Payment already taken — book outright, so nothing is stranded if a second call fails. +# Payment already taken: book outright, so nothing is stranded if a second call fails. seatlayer.inventory.book_best_available(event_key, qty=2, booking_ref="phone-1183") # Or name the seats yourself. @@ -170,7 +170,7 @@ pause/archive controls, audit-safe session listing, and channel reports are on ## Listing and pagination `list()` returns one page plus a `nextCursor`. When you want everything, `list_all()` pages for you -and yields as it goes — a generator rather than a list, because the point of paginating is to *not* +and yields as it goes. It is a generator rather than a list, because the point of paginating is to *not* hold an unbounded result set in memory. ```python @@ -185,8 +185,8 @@ for event in seatlayer.events.list_all(): ``` Listing events includes live availability `counts` by default, which costs the server one -round-trip **per event**. `list_all()` turns them off automatically — walking a whole catalogue is -exactly when you don't want that — and you can control it explicitly: +round-trip **per event**. `list_all()` turns them off automatically, since walking a whole catalogue is +exactly when you don't want that, and you can control it explicitly: ```python seatlayer.events.list(limit=50, counts=False) @@ -194,7 +194,7 @@ seatlayer.events.list(limit=50, counts=False) ## Keeping a hold alive -When an order takes longer than the checkout window — an invoice, a phone sale — extend rather than +When an order takes longer than the checkout window (an invoice, a phone sale), extend rather than release and re-hold. Releasing first hands the seats to whoever is racing for them in between. ```python @@ -203,7 +203,7 @@ from seatlayer import SeatLayerConflictError try: seatlayer.inventory.extend_hold(event_key, hold_id, ttl_ms=10 * 60_000) except SeatLayerConflictError: - # Gone, expired, or at its renewal cap — the buyer has to re-pick. + # Gone, expired, or at its renewal cap: the buyer has to re-pick. ... ``` @@ -241,8 +241,8 @@ The full set, all opt-in: | `event:door:checkin` | Check tickets in and out | | `event:boxoffice` | Use the managed box-office surface | -The two `event:channels:*` capabilities are **not** in the default — a token minted before sales -channels existed must not silently acquire channel authority — so ask for them explicitly if the +The two `event:channels:*` capabilities are **not** in the default (a token minted before sales +channels existed must not silently acquire channel authority), so ask for them explicitly if the page manages channels. The same pattern embeds the Designer in your own UI: @@ -287,7 +287,7 @@ def seatlayer_webhook(): return "", 400 # The signed body carries `at`, but nothing enforces a freshness window, so - # a captured delivery stays valid indefinitely. Deduplicate on occurrenceId — + # a captured delivery stays valid indefinitely. Deduplicate on occurrenceId: # this is your replay protection, not an optimisation. if already_processed(event["occurrenceId"]): return "", 200 @@ -315,7 +315,7 @@ except SeatLayerAuthError as error: raise ``` -Every error carries `status`, `code`, `body`, and `request_id` — quote the request id in support +Every error carries `status`, `code`, `body`, and `request_id`. Quote the request id in support requests. ## Reliability @@ -386,15 +386,15 @@ Some API surface is intentionally unwrapped, not merely pending: There is no server-side subscribe; a secret-key caller gets authoritative state from `events.retrieve_report()` and `inventory.retrieve_availability()`. -None of these are reachable through `request()` as a supported path either — they are excluded from +None of these are reachable through `request()` as a supported path either; they are excluded from the public manifest, not just from the wrapper. ## Frequently asked questions ### How do I book seats from Python? -Create a client with your secret key, obtain a hold id — either from the buyer's -browser session or by holding server-side — and call `inventory.book(event_key, hold_id=..., booking_ref=...)`. +Create a client with your secret key, obtain a hold id (either from the buyer's +browser session or by holding server-side) and call `inventory.book(event_key, hold_id=..., booking_ref=...)`. `booking_ref` is your own stable order id and is the join between SeatLayer inventory and your commercial order, so the same reference identifies the booking in Booking History and when you later cancel it. For phone orders, box office, and @@ -417,7 +417,7 @@ and at what price, so charge from its `items` rather than from anything the brow sent you. When an order runs longer than the checkout window, `inventory.extend_hold` renews the hold instead of releasing and re-holding, which would hand the seats to whoever is racing for them. Bookings carry the server's exact-selection plus -`booking_ref` safeguard, but the SDK sends each booking once — reconcile an unknown +`booking_ref` safeguard, but the SDK sends each booking once; reconcile an unknown outcome before trying again. ### Can I use my own payment provider? diff --git a/RELEASE.md b/RELEASE.md index e7a06c1..cf6c985 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,192 +1,58 @@ # Releasing `seatlayer` to PyPI -Publishing checklist for the SeatLayer Python server SDK. This package has **never been -published** — the first upload permanently claims the name `seatlayer` on PyPI. +Releases are published by `.github/workflows/release.yml` through PyPI Trusted +Publishing (OIDC). No API token is stored in this repository or in its Actions +secrets: PyPI issues a short-lived token for the one workflow run. -Everything below has been validated locally except the steps marked -**[needs a live account]**, which cannot be run until the account exists. +## One-time setup ---- +- The `seatlayer` project on PyPI lists this repository and `release.yml` as a + trusted publisher. +- The publish job runs in the `pypi` GitHub environment. Add required reviewers + to that environment if a tag push should wait for approval before publishing. -## 1. What the owner must create first +## Release steps -### PyPI account +1. Update the version in **both** places, which must agree: + - `pyproject.toml` (`version`) + - `src/seatlayer/__init__.py` (`__version__`) +2. Add a dated entry at the top of `CHANGELOG.md`. +3. Run the gate locally: -| Item | Value | -|---|---| -| Registry | https://pypi.org (and https://test.pypi.org for the rehearsal) | -| Project name | `seatlayer` — verified **unclaimed** as of 2026-08-04 | -| Account | A PyPI account with **2FA enabled** (mandatory for all uploads since 2024) | -| Recovery | Save the 2FA recovery codes somewhere the team can reach them | + ```bash + python -m venv .venv && .venv/bin/pip install -e ".[dev]" build twine + .venv/bin/ruff check src tests + .venv/bin/mypy + .venv/bin/pytest -q + ``` -> **Name squatting is the one mistake nothing can undo.** If `seatlayer` is taken between now and -> the upload, the name is gone permanently — PyPI does not transfer names on request except in -> narrow dispute cases. If the sweep is going to be delayed, claim the name early with a `0.0.0` -> placeholder rather than waiting. +4. Merge the release commit to `main`, then tag it and push the tag: -### Credential: pick ONE of these two paths + ```bash + git tag v0.8.1 && git push origin v0.8.1 + ``` -#### Path A — API token (simplest; right for a manual first publish) +The workflow re-runs the gate, refuses to publish if the tag and either version +literal disagree, builds the sdist and wheel, checks that the wheel carries +`py.typed` and no `tests/` or `.github/` files, and then publishes. -1. PyPI → Account settings → **API tokens** → *Add API token*. -2. **First upload only:** the project does not exist yet, so a project-scoped token cannot be - created. You must issue an **account-scoped** token (scope: *Entire account*). -3. Upload once (step 4 below). The project now exists. -4. **Immediately after:** create a new token scoped to **Project: seatlayer**, use that from then - on, and **revoke the account-scoped token**. An account-scoped token can publish or yank *every* - project you own; it should not outlive its one job. - -Token format is `pypi-AgEIcHlwaS5vcmc…`. Use it with username `__token__`. - -#### Path B — Trusted Publishing / OIDC (better long term; recommended once CI publishes) - -No long-lived secret exists at all: PyPI mints a short-lived token for a specific GitHub Actions -workflow run. This is the better path and it is what the fleet should converge on, but note: - -- It requires the GitHub repo `seatlayer/seatlayer-python` to **exist and be pushed** — it does not - today (`git remote -v` points at it, but nothing has been pushed). -- Configure it as a **pending publisher** (PyPI → *Your projects* → *Publishing* → *Add a pending - publisher*) so it works for the very first upload of a project that does not exist yet. You need: - owner `seatlayer`, repo `seatlayer-python`, workflow filename (e.g. `release.yml`), and - optionally a GitHub environment name. -- The publishing workflow needs `permissions: id-token: write` and - `pypa/gh-action-pypi-publish@release/v1`. **No release workflow exists in this repo yet** — only - `.github/workflows/ci.yml`. Writing it is a prerequisite for Path B. - -**Recommendation:** use Path A for this first manual sweep, then move to Path B before 0.2.0. - ---- - -## 2. Pre-flight (all verified locally on 2026-08-04) - -```bash -cd python -python -m venv .venv && .venv/bin/pip install -e ".[dev]" build twine - -.venv/bin/ruff check src tests # → All checks passed! -.venv/bin/mypy # → Success: no issues found in 6 source files -.venv/bin/pytest -q # → 33 passed -``` - -Confirm before building: - -- [ ] `CHANGELOG.md` top entry reads `## 0.1.0 — 2026-08-04`. **If the sweep has slipped past that - date, change it** — the date is baked into the sdist and shown on the PyPI page. -- [ ] `pyproject.toml` `version` and `src/seatlayer/__init__.py` `__version__` both read `0.1.0`. - These are two separate literals with nothing enforcing agreement; check them by eye. -- [ ] Working tree is clean and the release commit is tagged (`git tag v0.1.0`). - ---- - -## 3. Build and inspect +## Verify ```bash -rm -rf dist -.venv/bin/python -m build -.venv/bin/twine check dist/* # → PASSED for both artifacts +python -m venv /tmp/verify +/tmp/verify/bin/pip install seatlayer==0.8.1 +/tmp/verify/bin/python -c "import seatlayer; print(seatlayer.__version__)" ``` -The wheel must contain **exactly** these seven package files plus `dist-info`: - -``` -seatlayer/__init__.py client.py errors.py http.py py.typed resources.py webhooks.py -seatlayer-0.1.0.dist-info/{METADATA,WHEEL,RECORD,licenses/LICENSE} -``` - -- `py.typed` **must** be present — without it every downstream mypy/pyright user silently loses all - type information and sees `module is installed, but missing library stubs or py.typed marker`. -- No `tests/`, no `AGENTS.md`, no `.github/` in the wheel. -- The sdist deliberately **does** keep `tests/` (redistributors build from it) but must **not** - contain `AGENTS.md` or `.github/`. - -Verify the type marker actually works before uploading: - -```bash -python -m venv /tmp/tc && /tmp/tc/bin/pip install dist/*.whl mypy -printf 'from seatlayer import SeatLayer\nreveal_type(SeatLayer("sk_test_x").mode)\n' > /tmp/c.py -/tmp/tc/bin/mypy --strict /tmp/c.py # must reveal "str", NOT "Any" -``` - ---- - -## 4. Publish - -Rehearse on TestPyPI first — it is a separate account and a separate token, and it is the only -way to see the rendered project page before the real name is spent. - -```bash -# Rehearsal (needs a separate test.pypi.org account + token) -.venv/bin/twine upload --repository testpypi dist/* - -# Real thing -.venv/bin/twine upload dist/* -# Username: __token__ -# Password: pypi-AgEIcHlwaS5vcmc… -``` - -Non-interactive alternative: - -```bash -TWINE_USERNAME=__token__ TWINE_PASSWORD='pypi-…' .venv/bin/twine upload dist/* -``` - -Then push the tag: - -```bash -git push origin main --tags -``` - ---- - -## 5. Verify the published artifact - -```bash -python -m venv /tmp/verify && cd /tmp/verify -bin/pip install seatlayer # must resolve from PyPI, not a local path -bin/python -c " -import seatlayer, os -print(seatlayer.__version__) -print('typed:', os.path.exists(os.path.join(os.path.dirname(seatlayer.__file__), 'py.typed'))) -c = seatlayer.SeatLayer('sk_test_x'); print(c.mode, c._http.base_url) -" -``` - -Expected: `0.1.0`, `typed: True`, `test https://api.seatlayer.io`. - -Also eyeball https://pypi.org/project/seatlayer/ for: - -- README rendering (Description-Content-Type is `text/markdown` — confirmed in the built metadata) -- License showing as **MIT** (PEP 639 `License-Expression`, not a wall of licence text) -- Author `SeatLayer ` -- The five sidebar links: Homepage, Documentation, Changelog, Source, Issues -- The `Typing :: Typed` and Python 3.10–3.13 classifiers - ---- - -## 6. If it goes wrong - -**PyPI does not allow re-uploading a version.** Once `0.1.0` is up, that exact filename is spent -forever, even if you delete it. There is no `--force`. - -| Situation | Action | -|---|---| -| Bad artifact, caught fast | `twine yank` is not a thing — yank in the web UI: project → *Manage* → *Releases* → *Yank*. Yanking hides it from new resolutions but keeps it installable for anyone who pinned it exactly. This is the correct, non-destructive fix. | -| Need a fixed build out | Bump to `0.1.1` and upload that. Never try to reuse `0.1.0`. | -| Leaked secret in the artifact | Yank **and** delete the release in the UI, then rotate the leaked credential. Assume it was already mirrored — deletion is not containment. | -| Wrong project entirely | Project → *Manage* → *Settings* → *Delete project*. Only possible while you are the sole owner; the **name is not released for reuse by others** immediately, but you also cannot re-upload the same version. | - -Deleting is almost always the wrong reflex: it breaks anyone who already installed. **Prefer yank + -a patch release.** +Also check https://pypi.org/project/seatlayer/ for the rendered README, the MIT +license and the sidebar links. ---- +## If a release is wrong -## 7. Post-publish +PyPI never accepts the same version twice, even after deletion. -- [ ] Revoke the account-scoped token; replace with a project-scoped one (Path A step 4). -- [ ] Add `Programming Language :: Python :: 3.14` to `pyproject.toml` **after** adding `3.14` to the - CI matrix. The suite passes on 3.14.5 locally today, but the classifier should follow tested - support, not precede it. -- [ ] Consider a `release.yml` + Trusted Publishing (Path B) before 0.2.0. -- [ ] Fleet nit, not a blocker: the `User-Agent` is the bare string `seatlayer-python` with no - version, matching node/go/php/ruby. Once these are in customers' hands, support cannot tell - which SDK version a request came from. Worth changing across all seven at once, never in one. +- **Bad build:** yank it in the PyPI web UI (project, Manage, Releases, Yank) and + publish a patch version. Yanking hides the release from new installs but keeps + exact pins working. +- **Leaked secret in an artifact:** yank and delete the release, then rotate the + credential. Assume it was already mirrored.