Skip to content

Latest commit

 

History

History
412 lines (376 loc) · 23.2 KB

File metadata and controls

412 lines (376 loc) · 23.2 KB

Changelog

All notable changes to this project are documented here. This project adheres to Semantic Versioning.

[1.7.0] — 2026-08-18

Added

  • Signal frames on the push feed — PushStream(signals=[…]). The same opt-in vocabulary as the native streamer: signals=["break_point"] subscribes the signal channels (signal:slate, or signal:match:<id> per entry of match_ids) and yields the same BreakPoint / BreakPointResult objects LiveScoreStream yields; signals=["divergence"] adds the divergence events, each a generic PushFrame with type == "divergence" (no dedicated model on either streamer). The signal channels carry every family, so the stream filters to the families you asked for — the exact behaviour of the native signals subscription. Signal frames are events with no replay: a stream that connects mid-break-point does not receive the onset. Break-point and divergence users no longer need the capacity-capped native feed for them.
    • Like the point channels, the signal channels are subscribed ONLY from the token mint's advertised vocabulary: when the mint does not advertise them (server gate off, or the plan lacks them), a non-empty signals raises PushRefused immediately, naming that cause — never a guessed channel name, never a reconnect loop.
    • signals=["points"] on PushStream raises ValueError pointing at points=True — on the push feed the per-point stream is its own opt-in with its own resume machinery.
    • WSToken gains the matching vocabulary helpers: signal_slate_channel and signal_match_channel(match_id), returning None when the mint does not advertise the family.
  • livetennis watch --push streams over the push feed instead of the native WebSocket, with identical output — recommended for continuous use (the native feed is capacity-capped shared infrastructure). Without the flag the command behaves exactly as before.

Changed

  • README: PushStream is now the first streaming example; the native streamer follows with its shared-capacity ceiling stated. The claim that break-point signal frames exist only on the native streamer is gone — as of this release it is no longer true.

Notes

  • Fully backwards compatible. A PushStream without signals behaves exactly as before; the CLI without --push is unchanged.

[1.6.0] — 2026-08-18

Added

  • draw — singles vs doubles, three-valued and filterable. Match gains draw: "singles" | "doubles" | None — and the None is an answer, not a gap. Team ties and team exhibitions never state which discipline a rubber was, so those matches carry a null draw rather than a guess; the existing is_doubles stays untouched but is lossy (it cannot say "unknown"), so branch on draw. The same word filters: list_matches, list_completed_matches, list_tournaments and list_fixtures take draw="singles" | "doubles", passed through to the server as given (the server owns validation — an invalid value is a 400 bad_draw with the allowed list in the body). A null-draw row matches NEITHER filter value, so draw="singles" plus draw="doubles" is not everything.
  • get_history_coverage() — the measured point-completeness table. BASIC, or any History plan. One object for the whole completed archive, typed as HistoryCoverage: per-tour_draw bucket (atp_singles, itf_doubles, …) how many completed matches we hold (completed), how many carry any tape (any_tape), how many have a complete point-by-point tape AVAILABLE (point_complete), how many a default read serves complete (complete_on_default_read), and the share — plus totals across every bucket. The two completeness counts differ on purpose: a complete tape can exist for a match a default read does not serve complete. The numbers are a built artifact (as_of — parsed — stamps the build, method how they were measured); while it is not built the endpoint answers 503 coverage_unavailable, raised as ServiceUnavailable with the code readable — never an empty object.
  • list_fixtures gains tour= and draw=. The server accepted a tour filter on /fixtures all along; this client never passed it. That gap closes here, alongside the new draw.
  • The per-row tape block on /history/matches rows also carries starts_at_love and computed_at on servers that measure them — reachable today through the models' forward-compatible field pass-through. An absent field there means an older server or "not measured", never "no".

Notes

  • Fully backwards compatible. Every addition is a new method, a new optional keyword argument, a new optional field, or a new model.

[1.5.0] — 2026-08-17

Added

  • The per-point stream, on every surface the SDK has. One record per committed point — who served, who won it, the score after — keyed by the per-match seq: monotonic, gapless, starting at 1, and identical across REST and both streamers, so any two reads deduplicate against each other by seq alone. All of it is ULTRA.
    • REST: get_match_points(match_id, after_seq=0) returns a PointsPage (every committed point with seq > after_seq, ≤500 per page), and iter_match_points walks the pages on the sequence cursor — has_more/last_seq, never the page length (a live match's newest page is routinely short while more points are coming), stopping rather than looping on a page that makes no forward progress. Both on the async client too. PointsPage.covers_from_start says whether seq 1 really is the match's first point — None when the server did not state it (older servers omit the field), which means "not measured", never "no". A 400 points_disabled means the feature is off server-side; no retry changes that.
    • Native streamer (LiveScoreStream): signals=["points"] yields a PointUpdate per point (match_id, the nested LivePoint, pbp_coverage, quality), interleaved with score frames. point frames were previously dropped with the protocol noise; they now dispatch — a subscription that asked for points actually sees them.
    • Push streamer (PushStream): points=True subscribes the point channels — point:slate, or point:match:<id> per entry of match_ids — read STRICTLY from the token mint's advertised channel vocabulary. Because point frames are events (not self-correcting state), the push side adds a resume: points_resume=True (the default) keeps per-match last-seq cursors, REST-catches-up every tracked match on each (re)connect — fetched points are yielded before live frames, so the caller's per-match order never skips — drops any point at or below the cursor (live or fetched), and fills a mid-stream gap synchronously before yielding the frame that revealed it. The optional on_gap(match_id, expected_seq, got_seq) callback is informational; filling happens regardless. Catch-up covers matches the stream has already seen a point for — a from-start read of a match is iter_match_points.
  • LivePoint and PointsPage models; PointUpdate joins the native streamer's StreamFrame union and the push streamer's PushStreamFrame union. WSToken grows point_match_channel(id) / point_slate_channel beside the existing helpers — reading the mint's vocabulary and returning None when the point family is not advertised, never guessing a name.
  • list_rankings gains the Elo companion parameters on both clients: tour, surface, archive_player, min_matches, activity_weeks, passed to the server as given. system="elo" is a valid system — and Elo is never included implicitly: no mode returns Elo records unless system names it. The Elo leaderboard (the listing mode) requires tour.

Notes

  • The push point channels are server-gated. They are subscribed only when the mint's channel vocabulary advertises them; when it does not — the server's point feature gate is off, or the plan lacks point streams — points=True raises PushRefused immediately, naming that cause. That is an honest refusal, not a retry case: the same key gets the same vocabulary on every reconnect, and each doomed retry would mint a REST token against a refusal that cannot clear.
  • Read pbp_coverage (point | game) and quality (clean | revised) before treating any point stream as one-row-per-point truth — they describe the whole match's stream, on every surface that serves it.
  • Backwards compatible, with one deliberate exception. Without signals=["points"] / points=True both streamers behave as 1.4.0, and the new list_rankings parameters default to absent and send nothing. The exception: a PushStream that subscribed a point channel through the raw channels escape hatch now receives typed PointUpdate frames where 1.4.0 yielded generic PushFrame objects — dispatch is by frame type, as documented. The resume machinery (cursors, dedup drops, REST catch-up) stays off for that path; it belongs to the points=True opt-in only.

[1.4.0] — 2026-08-16

Added

  • PushStream — a client for the high-fan-out push feed. The second of the SDK's two streamers, recommended for continuous / production streaming (no shared connection ceiling). It mints a short-lived token via /ws-token, connects to the push endpoint, subscribes — the whole slate (slate:all, the default) or specific matches (match_ids=[…]) — answers server heartbeats, and reconnects with backoff, minting a fresh token on every reconnect (tokens are never reused). Ergonomics mirror LiveScoreStream: same constructor shape, iterate for frames, context manager, close(). score frames are yielded as the same ScoreUpdate the native feed produces (nested score, ULTRA model fields included); frame types newer than this SDK are yielded as a generic PushFrame rather than dropped. Auth/tier refusals surface from the token mint as the SDK's normal exceptions — an ULTRA gate raises UpgradeRequired naming the tier — and are never retried; neither are the abuse throttle and the daily cap, which hold for hours; nor are deterministic connect/subscribe refusals — an unknown or unpermitted channel raises the new PushRefused (or Unauthorized) immediately rather than reconnect-looping, since every doomed reconnect would mint a token against the daily quota (only error replies the server itself marks temporary are retried, and when reconnect attempts run out the terminal APIConnectionError chains the last underlying failure as its __cause__). A per-minute rate-limited mint honours the server's Retry-After before reconnecting, the same rule as the REST client's backoff. Steady-state reads are bounded by twice the ping cadence the connect reply advertises (60s fallback), so a half-open TCP connection is treated as dead and reconnected instead of blocking the stream forever. Newline-batched messages are always drained in full — a publication or server ping packed into the same message as a handshake ack is never dropped or left unanswered. The push feed carries score frames only today: the native streamer's opt-in break_point signal frames do not exist there yet.
  • PushFrame and the PushStreamFrame union are exported from the package (lazily, like the rest of the WebSocket surface, so websockets stays optional).
  • New typed exceptions: PushRefused (a deterministic push-feed connect/subscribe refusal; carries the server's numeric code) and MissingDependencyError (an optional extra is not installed). Both streamers now raise MissingDependencyError — never retried, and before any REST traffic — when the websockets package is missing, instead of a base LiveTennisAPIError that the reconnect loop would retry forever.

Notes

  • Fully backwards compatible. The native LiveScoreStream is untouched apart from the typed (and now non-retried) missing-websockets error — a subclass of the LiveTennisAPIError it raised before; PushStream is additive.

[1.3.2] — 2026-08-16

Fixed

  • History-package docstrings told the truth about only two of the four package kinds: they implied every non-tape kind needs ULTRA and that every period is YYYY-MM. Corrected — rankings/rally are ULTRA, archive rides the tape entitlement, and the yearly kinds (rally, archive) take a bare YYYY period.

[1.3.1] — 2026-08-07

Fixed

  • WebSocket score frames parsed as all-None. The wire nests the score object — a frame is {"type": "score", "match_id": N, "score": {sets, games, points, server, is_tiebreak, timestamp, win_probability_p1?, danger?}} with the ULTRA model fields INSIDE the score object — but ScoreUpdate.from_dict built its Score from the whole frame, assuming the fields sat inline. On real frames every field of update.score (including sets and the model fields) came back None. The nested object is now parsed when present, with the inline read kept only as a defensive fallback, so a flat emitter still parses rather than yielding an all-None score. break_point / break_point_result frames are flat on the wire and were never affected.

[1.3.0] — 2026-08-07

Added

  • The point-by-point tape, typed. get_match_tape(match_id, sequence=) (BASIC, or any History plan) — the chronological score sequence for one match, live matches included. TapeRow is a Score plus point_winner (clean-sequence rows only, where the transition is a single attributable point); HistoryTape carries per-set tiebreak final scores (tiebreaks), model profiles and a typed TapeMeta (coverage, point_source, row counts).
  • In-play statistics. get_match_statistics(match_id) (ULTRA) — aces, double faults, the serve split, hold/break percentages, break points, service and return points, in the API's two deliberately-unmerged families (derived vs measured) with per-family freshness.
  • Point-in-time rankings. list_rankings() in both modes with both gates: the full published table for one system (PRO), and per-player as-of records via player= (an id or a list, repeated on the wire, max 50 — ULTRA). Typed RankingRecord including previous_rank (ATP/WTA), rank_movement (ITF) and rating (UTR).
  • Rally construction and charting (ULTRA). list_rally_matches() / get_rally_match() over the charted corpus's own id space, get_match_rally() by OUR match id (404 not_charted stays distinguishable from "no such match"), get_charting_player() / get_charting_match() for the Match Charting Project stat families. RallyPoint exposes the charter's verbatim string as .notation (the wire calls it raw, which every model already uses for its payload).
  • Bulk packages. list_history_packages(kind=, year=) and get_history_package(period, kind=) (PRO; non-tape kinds and the year= archive listing are ULTRA).
  • Push-feed token. get_ws_token() (ULTRA) — typed WSToken with ws_url, expires_in and the channel vocabulary (match_channel(match_id), slate_channelslate:all).
  • Usage. get_usage() — your tier, limits, today's calls (current to the second) and a 30-day history; the read itself is quota-exempt.
  • Errors, sharpened. The abuse throttle is its own type: AbuseThrottled (a RateLimited, so existing handlers keep working) with .retry_at_epoch / .retry_at — and it is never auto-retried, since the retry loop is what earns the 24h block. The daily-cap 429 now surfaces .scope, .limit_per_day and .resets_at (parsed; an absolute instant derived from a local midnight — never assume a UTC midnight) and is not auto-retried either.
  • Parameter-aware 403s. UpgradeRequired.required_tier now understands endpoints gated by their parameters: /rankings names PRO for the listing and ULTRA for per-player mode, /history/packages names ULTRA for non-tape kinds and year=, and /matches?status=completed names BASIC.

Changed

  • WebSocket score frames carry the model fields. win_probability_p1 and danger arrive on ULTRA score frames over the stream exactly as on REST — a None means the model had no output for that state, not that the feed withholds them. Docs saying otherwise were wrong and are gone.
  • README now states the current quota grid (2026-08-06: FREE 100/day, BASIC 1,000/day, PRO 10,000/day, ULTRA 500,000/day) with FREE polling guidance, the full endpoint/tier table, and the five-tour coverage phrasing (ATP, WTA, Challenger, ITF and juniors).

Notes

  • Fully backwards compatible. Every addition is a new method, a new optional keyword argument, a new optional field, or an exception subclass.

[1.2.0] — 2026-08-03

Added

  • The results archive (1968–2022). Five new methods (on both clients) over the licensed historical results corpus — ATP and WTA, main draws, qualifying and the ITF/futures tiers, ending 2022-12-31 exactly where the point-by-point tape (2023→now) begins:
    • list_archive_matches() / get_archive_match() — winner/loser-shaped results with final score, seeds, ranks at the time, and (on the detail read) per-match serve statistics where the era recorded them. event_date is the tournament START date.
    • list_archive_players() — archive bios: hand, DOB, country, height, career-high rank and the earliest week it was reached.
    • get_archive_career(name) — career aggregates: W-L by surface/level/year, titles, summed serve stats with honest coverage (serve["matches_with_stats"]).
    • get_h2h(p1, p2) — cross-era head-to-head over the archive PLUS our own completed matches, name-keyed; each meeting's winner is 1|2 of the request. Ambiguous name fragments raise BadRequest with error_code == "ambiguous_name" and the candidate list in exc.body["candidates"] (also true of get_archive_career); all four BASIC-gated reads name BASIC on a 403.
  • Tournament catalogue. list_tournaments() / get_tournament(id) (FREE) — the stable id space Match.tournament_id joins, with surface, indoor, curated city/country, and category (set only where the catalogues agree unambiguously, never derived from the name).
  • New list filters. list_matches() takes tour, player (an id or a list of ids — repeated on the wire, max 50), from_ / to (play-date bounds; from is a Python keyword, the wire parameter is still from) and country (IOC-style lowercase 3-letter codes, as player.country returns them — not ISO-3166); list_completed_matches() takes those plus coverage.
  • New match fields, typed. Match gains tour (the same vocabulary the filter accepts), tournament_id, round_code (normalized round — the field to branch on) and withdrew. Fixture gains start_time, player1_id / player2_id and round_code. ListMeta gains total and has_more.
  • New models, all exported: Tournament, ArchiveMatch, ArchiveParticipant, ArchivePlayerBio, ArchiveCareer, HeadToHead.

Notes

  • Fully backwards compatible. Every addition is a new method, a new optional keyword argument, or a new optional field following the same forward-compatible rules as the rest.

[1.1.0] — 2026-07-24

Added

  • Break-point signals over the WebSocket feed. LiveScoreStream takes a new signals= argument; pass signals=["break_point"] and the stream also yields a BreakPoint the instant a break point arises and a BreakPointResult when it resolves, alongside the usual ScoreUpdate. Previously the subscribe frame carried no signals key and listen() swallowed every non-score frame, so the headline break-point feed was unreachable from this client. Switch on the yielded object's type (or its .type field) to tell frames apart.
  • BreakPoint, BreakPointResult and the StreamFrame union are exported from the package (lazily, so websockets stays optional). Both models follow the same forward-compatible rules as the rest — unknown fields are preserved in .raw and readable as attributes.

Notes

  • Fully backwards compatible. With no signals (the default) the subscribe frame and everything listen() yields are byte-for-byte identical to 1.0.2 — score frames only.
  • The break-point feed is ULTRA-only, like the rest of the WebSocket surface.

[1.0.2] — 2026-07-21

Fixed

  • A 403 on list_completed_matches() could not be attributed to a tier. /history/matches used to be the entitlement floor, so nothing needed to name a tier for it. With the new FREE tier below it, a free key calling that method got an UpgradeRequired with no required_tier, leaving the caller with the API's bare upgrade_required and no idea which plan to buy. /history now maps to BASIC.

[1.0.1] — 2026-07-19

Fixed

  • WebSocket backoff never grew against a flapping server. The retry counter reset on a successful subscribe, so a server that accepted then immediately dropped the socket pinned the delay at step one forever and max_reconnect_attempts was never reached. The counter now resets only after a connection has stayed up for 60s.
  • WebSocket leaked a socket per failed handshake. send/recv during the subscribe exchange were outside any try, so a recv timeout or an early close escaped with the socket still open — once per reconnect attempt, indefinitely.
  • livetennis --json was honoured by only three of the eight subcommands, and livetennis live --json was an argparse error. Every command now emits JSON, and --json works before or after the subcommand.
  • format_score used zip, silently dropping the in-progress set when the two per-player game lists differed in length. Now uses zip_longest, matching the JavaScript client.
  • livetennisapi.AsyncLiveScoreStream was advertised by the lazy importer but never existed, producing a confusing ImportError. LiveScoreStream and ScoreUpdate are now correctly exported instead.
  • [tool.mypy] python_version = "3.9" made mypy ≥1.18 refuse to run at all. Removed; ruff's target-version already enforces 3.9-compatible syntax.

[1.0.0] — 2026-07-19

First release.

Added

  • LiveTennisAPI and AsyncLiveTennisAPI covering all 12 REST endpoints.
  • LiveScoreStream — reconnecting WebSocket live-score feed (ULTRA).
  • livetennis CLI: health, live, match, score, players, fixtures, history, watch.
  • Typed error hierarchy. UpgradeRequired carries .required_tier; RateLimited carries .retry_after.
  • Automatic retries on 429 and 5xx only, honouring Retry-After with exponential backoff and jitter. Other 4xx are never retried.
  • paginate() for walking list endpoints on both clients.
  • Full type hints and a py.typed marker.

Notes

  • Models never reject unknown fields. The API ships additive changes within v1, so unrecognised fields are preserved in .raw and readable as attributes — a new server field works without upgrading this package.
  • Score.games is player-major ([games_p1, games_p2], each a per-set list). Score.games_for_set() reads it safely.