livetennisapi-haystack 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.
- livetennisapi_haystack-0.1.0/.github/workflows/publish.yml +22 -0
- livetennisapi_haystack-0.1.0/.gitignore +10 -0
- livetennisapi_haystack-0.1.0/BUILD_PLAN.md +104 -0
- livetennisapi_haystack-0.1.0/LICENSE +21 -0
- livetennisapi_haystack-0.1.0/PKG-INFO +162 -0
- livetennisapi_haystack-0.1.0/README.md +138 -0
- livetennisapi_haystack-0.1.0/catalog-entry.md +138 -0
- livetennisapi_haystack-0.1.0/examples/live_demo.py +50 -0
- livetennisapi_haystack-0.1.0/pyproject.toml +59 -0
- livetennisapi_haystack-0.1.0/src/livetennisapi_haystack/__init__.py +6 -0
- livetennisapi_haystack-0.1.0/src/livetennisapi_haystack/_util.py +56 -0
- livetennisapi_haystack-0.1.0/src/livetennisapi_haystack/match_fetcher.py +336 -0
- livetennisapi_haystack-0.1.0/src/livetennisapi_haystack/player_search.py +183 -0
- livetennisapi_haystack-0.1.0/src/livetennisapi_haystack/py.typed +0 -0
- livetennisapi_haystack-0.1.0/tests/__init__.py +0 -0
- livetennisapi_haystack-0.1.0/tests/test_match_fetcher.py +362 -0
- livetennisapi_haystack-0.1.0/tests/test_player_search.py +161 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
workflow_dispatch: {}
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: python -m pip install build
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# BUILD_PLAN — livetennisapi-haystack
|
|
2
|
+
|
|
3
|
+
> **Status (2026-07-24): ALL UNITS DONE.** BUILD-001..004 landed as commits 1-4; BUILD-005
|
|
4
|
+
> verification passed (clean-venv install, 31 unit tests + 2 live integration tests green,
|
|
5
|
+
> ruff clean, live pipeline smoke incl. doubles/null-server/tour-filter/serialization
|
|
6
|
+
> round-trip, key-leak sweep 0 hits) and surfaced one real-data fix (points [None, None] on
|
|
7
|
+
> completed matches), landed as commit 5. Verification finding: Haystack 3.0 requires
|
|
8
|
+
> `allowed_modules=` to deserialize third-party components — documented in README.
|
|
9
|
+
> Not done on purpose (out of scope by directive): PyPI publish, GitHub repo, catalog PR.
|
|
10
|
+
|
|
11
|
+
## Build target & source of truth
|
|
12
|
+
Haystack 2.x integration package for the Live Tennis API. Greenfield standalone repo at
|
|
13
|
+
`/var/tmp/haystack-build/livetennisapi-haystack`. No `PLAN.md`; source of truth is the task
|
|
14
|
+
directive (explicit spec: components, Secret handling, tests, README, catalog-entry draft,
|
|
15
|
+
verify steps) plus:
|
|
16
|
+
- Official client: `livetennisapi` 1.0.2 (PyPI == local checkout
|
|
17
|
+
`~/Documents/ben-is-a-dev/livetennisapi-oss/livetennisapi-python`) — reused, no hand-rolled HTTP.
|
|
18
|
+
- OpenAPI: `~/Documents/ben-is-a-dev/livetennisapi-oss/openapi/openapi.yaml`.
|
|
19
|
+
- Haystack conventions modeled on July-2026 catalog merges: DDGS (#548, `integrations/ddgs.md`),
|
|
20
|
+
Linkup (#549, component source `linkup_websearch.py` + tests + pyproject in
|
|
21
|
+
deepset-ai/haystack-core-integrations), Perseus Vault (#538, third-party frontmatter, MIT).
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
Ready to build as specified. [FACT] Haystack latest on PyPI is 3.0.0 (2026-07); recent catalog
|
|
25
|
+
entries still declare `version: Haystack 2.0` and the `@component`/`Secret` API is unchanged in
|
|
26
|
+
the 3.0 docs — pin `haystack-ai>=2.24.1` (Linkup's pin) and verify against what installs.
|
|
27
|
+
[FACT] client v1.0.2 `list_matches()` does not expose the API's documented `tour` query param
|
|
28
|
+
(client.py:121) — tour support must go through the client's transport (`_request`), a deliberate,
|
|
29
|
+
commented deviation (never hand-rolled HTTP). Top risks: Haystack 3.0 API drift (verified by
|
|
30
|
+
tests), live smoke dependent on what's actually on court (report what's real).
|
|
31
|
+
|
|
32
|
+
## Build units (ordered)
|
|
33
|
+
|
|
34
|
+
### BUILD-001 scaffold
|
|
35
|
+
- Delivers: git repo, `pyproject.toml` (hatchling, MIT, deps `haystack-ai>=2.24.1`,
|
|
36
|
+
`livetennisapi>=1.0.2`), `LICENSE`, `.gitignore`, `src/livetennisapi_haystack/{__init__.py,py.typed}`,
|
|
37
|
+
README stub. Depends-on: —.
|
|
38
|
+
- Acceptance: `pip install -e .` succeeds in clean venv; `import livetennisapi_haystack` works.
|
|
39
|
+
- Output strategy: one-shot. Becomes commit 1.
|
|
40
|
+
|
|
41
|
+
### BUILD-002 LiveTennisMatchFetcher + tests
|
|
42
|
+
- Delivers: `src/livetennisapi_haystack/match_fetcher.py` — `@component` class; init
|
|
43
|
+
`api_key: Secret = Secret.from_env_var("LIVETENNISAPI_KEY")`, `status`, `tour`, `limit`,
|
|
44
|
+
`base_url`, `timeout`; `warm_up()` lazy `LiveTennisAPI`; `run(status, tour, limit, match_id)`
|
|
45
|
+
→ `documents: list[Document]` (content = human-readable summary; meta = ids/players/scores,
|
|
46
|
+
JSON-safe). Handles: null `score.server` (omit serving clause), doubles/null
|
|
47
|
+
`data_completeness`, string points, completed+winner, `UpgradeRequired` → one readable-message
|
|
48
|
+
Document (`meta.error="upgrade_required"`), `NotFound` on match_id → `[]` + warning.
|
|
49
|
+
`to_dict`/`from_dict` via `default_to_dict`/`default_from_dict` + `deserialize_secrets_inplace`.
|
|
50
|
+
Tests: `tests/test_match_fetcher.py`, mocked client, no net.
|
|
51
|
+
- Depends-on: BUILD-001.
|
|
52
|
+
- Acceptance: pytest green incl. null-server, doubles, 403-message, tour, serialization
|
|
53
|
+
round-trip cases.
|
|
54
|
+
- Output strategy: staged (component skeleton → summary builder → error paths → tests).
|
|
55
|
+
Becomes commit 2.
|
|
56
|
+
|
|
57
|
+
### BUILD-003 LiveTennisPlayerSearch + tests
|
|
58
|
+
- Delivers: `src/livetennisapi_haystack/player_search.py` — same shape; `run(query, limit)` →
|
|
59
|
+
`documents`. Tests incl. empty result + 403. Depends-on: BUILD-001.
|
|
60
|
+
- Acceptance: pytest green. Output strategy: one-shot. Becomes commit 3.
|
|
61
|
+
|
|
62
|
+
### BUILD-004 README + example + catalog-entry draft
|
|
63
|
+
- Delivers: full `README.md` (runnable pipeline example), `examples/live_demo.py` (env-var key,
|
|
64
|
+
builds a real Pipeline, prints Documents), `catalog-entry.md` matching current
|
|
65
|
+
`integrations/*.md` frontmatter exactly (third-party author form per Perseus Vault #538).
|
|
66
|
+
- Depends-on: BUILD-002/003. Acceptance: example runs against live API in smoke step;
|
|
67
|
+
frontmatter fields match the fetched templates field-for-field. One-shot. Becomes commit 4.
|
|
68
|
+
|
|
69
|
+
### BUILD-005 verify (no new product code)
|
|
70
|
+
- Clean venv (TMPDIR=/var/tmp/haystack-build/tmp): install, pytest, ruff, live smoke via
|
|
71
|
+
`examples/live_demo.py` (key from env only), key-leak sweep over tree + git history (0 hits).
|
|
72
|
+
- Acceptance: all green; real live output captured for the report. Fixes, if any, land as
|
|
73
|
+
their own commits.
|
|
74
|
+
|
|
75
|
+
## Conventions to follow
|
|
76
|
+
- Component shape: `linkup_websearch.py` (fetched ref): `@component` class, Secret default from
|
|
77
|
+
env var (never plain string), `warm_up()` lazy client, `@component.output_types`,
|
|
78
|
+
`documents` output naming, module-level `logger = logging.getLogger(__name__)`.
|
|
79
|
+
- Tests: `test_linkup_websearch.py` (fetched ref): class-based, `MagicMock` client injected via
|
|
80
|
+
`_client`, `component_to_dict`/`component_from_dict` assertions, `@pytest.mark.integration`
|
|
81
|
+
gated on env key.
|
|
82
|
+
- Packaging: Linkup `pyproject.toml` (hatchling, ruff config, pytest markers) minus monorepo
|
|
83
|
+
hatch-vcs; static version.
|
|
84
|
+
- Catalog entry: `ddgs.md`/`linkup.md` frontmatter + section order; third-party `authors:` per
|
|
85
|
+
`perseus-vault-haystack.md`; MIT license line per Perseus.
|
|
86
|
+
- Client reuse: only `livetennisapi` public API, except the documented `tour` param (OpenAPI
|
|
87
|
+
openapi.yaml:302-315) via `client._request` — called out in code.
|
|
88
|
+
|
|
89
|
+
## Open questions & assumptions
|
|
90
|
+
- [ASSUMPTION][blocking→resolved by directive] Task is an explicit approved build+verify
|
|
91
|
+
directive; proceeding through Phase 2 without an interactive gate.
|
|
92
|
+
- [ASSUMPTION] Sync-only `run` (no `run_async`): the official client has an async twin, but
|
|
93
|
+
httpx.AsyncClient lifecycle across event loops is a real cost; conventions allow sync-only
|
|
94
|
+
components. Noted as future work in README.
|
|
95
|
+
- [ASSUMPTION] `haystack-ai` resolves to 3.0.0; `@component`/Secret API verified unchanged by
|
|
96
|
+
the test suite, and catalog `version:` stays "Haystack 2.0" per current merged entries.
|
|
97
|
+
- [ASSUMPTION] 403 policy: an `UpgradeRequired` becomes a single Document carrying the client's
|
|
98
|
+
readable tier message (meta-tagged `error`) rather than an exception — per the directive
|
|
99
|
+
"readable message, not a crash"; other errors still raise (a dead network should fail loud).
|
|
100
|
+
|
|
101
|
+
## Handoff
|
|
102
|
+
Build, then `/full-review`. Most relevant personas: `/code-logic-review` (summary builder edge
|
|
103
|
+
cases), `/qa-automation` (mock coverage vs live truths), `/security-audit` (secret handling,
|
|
104
|
+
key-leak). No UI — browser-driven QA not applicable.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben
|
|
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,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: livetennisapi-haystack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Haystack 2.x integration for the Live Tennis API: live scores, matches and players as Documents
|
|
5
|
+
Project-URL: Documentation, https://github.com/livetennisapi/livetennisapi-haystack#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/livetennisapi/livetennisapi-haystack/issues
|
|
7
|
+
Project-URL: Source, https://github.com/livetennisapi/livetennisapi-haystack
|
|
8
|
+
Author-email: Ben <ben@ben-is-a.dev>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: Agents,Haystack,Live Scores,RAG,Sports Data,Tennis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: haystack-ai>=2.24.1
|
|
22
|
+
Requires-Dist: livetennisapi>=1.0.2
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# livetennisapi-haystack
|
|
26
|
+
|
|
27
|
+
[Haystack](https://haystack.deepset.ai) 2.x integration for the
|
|
28
|
+
[Live Tennis API](https://livetennisapi.com): live scores, matches and players as Haystack
|
|
29
|
+
`Document`s for RAG and agent pipelines.
|
|
30
|
+
|
|
31
|
+
- **`LiveTennisMatchFetcher`** — live / upcoming / completed matches (optionally one match by
|
|
32
|
+
id, optionally filtered by tour) as `Document`s. `content` is a clean human-readable match
|
|
33
|
+
summary; `meta` carries the structured fields (ids, players, sets/games/points, server,
|
|
34
|
+
winner).
|
|
35
|
+
- **`LiveTennisPlayerSearch`** — player search by name, ranked players first, same
|
|
36
|
+
`Document` shape.
|
|
37
|
+
|
|
38
|
+
Built on the official [`livetennisapi`](https://pypi.org/project/livetennisapi/) Python
|
|
39
|
+
client (retries, error mapping, typed models) — no hand-rolled HTTP.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install livetennisapi-haystack
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You need a Live Tennis API key (free tier: 1000 requests/day, 30/min). Export it as an
|
|
48
|
+
environment variable — the components read `LIVETENNISAPI_KEY` by default and never accept a
|
|
49
|
+
plain-string key:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export LIVETENNISAPI_KEY="your-key"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### Standalone
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher
|
|
61
|
+
|
|
62
|
+
fetcher = LiveTennisMatchFetcher() # key from LIVETENNISAPI_KEY
|
|
63
|
+
result = fetcher.run(status="live", limit=5)
|
|
64
|
+
for doc in result["documents"]:
|
|
65
|
+
print(doc.content)
|
|
66
|
+
# e.g. "Carlos Alcaraz (ESP, #2) vs Jannik Sinner (ITA, #1) — match at Wimbledon,
|
|
67
|
+
# grass court, round QF, best of 5. Live now. Score: sets 1-1, games 6-4, 3-6,
|
|
68
|
+
# 2-1, points 30-15. Carlos Alcaraz (ESP, #2) is serving."
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### In a pipeline (runnable with only `LIVETENNISAPI_KEY`)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from haystack import Pipeline
|
|
75
|
+
|
|
76
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher, LiveTennisPlayerSearch
|
|
77
|
+
|
|
78
|
+
pipe = Pipeline()
|
|
79
|
+
pipe.add_component("matches", LiveTennisMatchFetcher(limit=5))
|
|
80
|
+
pipe.add_component("players", LiveTennisPlayerSearch(limit=3))
|
|
81
|
+
|
|
82
|
+
result = pipe.run({"matches": {"status": "live"}, "players": {"query": "alcaraz"}})
|
|
83
|
+
for doc in result["matches"]["documents"] + result["players"]["documents"]:
|
|
84
|
+
print("-", doc.content)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### RAG over live scores
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from haystack import Pipeline
|
|
91
|
+
from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder
|
|
92
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
93
|
+
from haystack.dataclasses import ChatMessage
|
|
94
|
+
|
|
95
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher
|
|
96
|
+
|
|
97
|
+
prompt_template = [
|
|
98
|
+
ChatMessage.from_system("You are a tennis commentator."),
|
|
99
|
+
ChatMessage.from_user(
|
|
100
|
+
"Current matches:\n"
|
|
101
|
+
"{% for document in documents %}{{ document.content }}\n{% endfor %}\n"
|
|
102
|
+
"Answer the following question: {{ query }}\nAnswer:"
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
pipe = Pipeline()
|
|
107
|
+
pipe.add_component("matches", LiveTennisMatchFetcher(limit=10))
|
|
108
|
+
pipe.add_component("prompt_builder", ChatPromptBuilder(template=prompt_template, required_variables={"query", "documents"}))
|
|
109
|
+
pipe.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
110
|
+
pipe.connect("matches.documents", "prompt_builder.documents")
|
|
111
|
+
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
112
|
+
|
|
113
|
+
query = "Who is closest to winning right now?"
|
|
114
|
+
result = pipe.run({"matches": {"status": "live"}, "prompt_builder": {"query": query}})
|
|
115
|
+
print(result["llm"]["replies"][0].text)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A complete runnable script lives at [`examples/live_demo.py`](examples/live_demo.py).
|
|
119
|
+
|
|
120
|
+
## Behavior worth knowing
|
|
121
|
+
|
|
122
|
+
- **403 tier wall**: when your key is valid but the plan does not unlock an endpoint, the
|
|
123
|
+
component returns a single readable `Document` (tagged `meta["error"] = "upgrade_required"`)
|
|
124
|
+
instead of raising — an agent can tell the user; a RAG pipeline can filter it out. All
|
|
125
|
+
other errors (bad key, network down, rate limit) still raise the official client's typed
|
|
126
|
+
exceptions.
|
|
127
|
+
- **Sparse data is normal**: `score.server` is nullable (between points the feed may not
|
|
128
|
+
know who serves next — the summary simply omits the serving sentence), doubles teams have
|
|
129
|
+
no individual rankings and a null `data_completeness`, and points are strings
|
|
130
|
+
(`"0"`, `"15"`, `"30"`, `"40"`, `"AD"`). The components tolerate all of it.
|
|
131
|
+
- **Serialization**: both components implement `to_dict`/`from_dict`; the API key is stored
|
|
132
|
+
as a `Secret` environment-variable reference, never as a value, so pipelines serialize
|
|
133
|
+
safely to YAML. Note that Haystack 3.0 refuses to deserialize third-party components
|
|
134
|
+
unless their module is allow-listed, so reload pipelines with
|
|
135
|
+
`Pipeline.loads(yaml_str, allowed_modules=["livetennisapi_haystack.match_fetcher", "livetennisapi_haystack.player_search"])`
|
|
136
|
+
(or `haystack.core.serialization.allow_deserialization_module(...)`).
|
|
137
|
+
- **`tour` filter**: the API's documented `tour` query parameter is not yet exposed by
|
|
138
|
+
`livetennisapi` 1.0.2's `list_matches()`, so the component routes that one call through
|
|
139
|
+
the official client's transport layer (same auth/retries/error mapping).
|
|
140
|
+
- **Sync only** for now: `run()` — no `run_async` yet, although the official client has an
|
|
141
|
+
async twin. Planned.
|
|
142
|
+
|
|
143
|
+
## Parameters
|
|
144
|
+
|
|
145
|
+
`LiveTennisMatchFetcher(api_key, status="live", tour=None, limit=10, base_url=None, timeout=30.0)`
|
|
146
|
+
— `status`/`tour`/`limit` can be overridden per `run()`, and `run(match_id=...)` fetches a
|
|
147
|
+
single match. `LiveTennisPlayerSearch(api_key, limit=10, base_url=None, timeout=30.0)` —
|
|
148
|
+
`run(query, limit=None)`.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e . pytest ruff
|
|
154
|
+
pytest # unit tests, fully mocked, no network
|
|
155
|
+
ruff check src tests examples
|
|
156
|
+
LIVETENNISAPI_KEY=... pytest -m integration # live tests, needs a key
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
`livetennisapi-haystack` is distributed under the terms of the
|
|
162
|
+
[MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# livetennisapi-haystack
|
|
2
|
+
|
|
3
|
+
[Haystack](https://haystack.deepset.ai) 2.x integration for the
|
|
4
|
+
[Live Tennis API](https://livetennisapi.com): live scores, matches and players as Haystack
|
|
5
|
+
`Document`s for RAG and agent pipelines.
|
|
6
|
+
|
|
7
|
+
- **`LiveTennisMatchFetcher`** — live / upcoming / completed matches (optionally one match by
|
|
8
|
+
id, optionally filtered by tour) as `Document`s. `content` is a clean human-readable match
|
|
9
|
+
summary; `meta` carries the structured fields (ids, players, sets/games/points, server,
|
|
10
|
+
winner).
|
|
11
|
+
- **`LiveTennisPlayerSearch`** — player search by name, ranked players first, same
|
|
12
|
+
`Document` shape.
|
|
13
|
+
|
|
14
|
+
Built on the official [`livetennisapi`](https://pypi.org/project/livetennisapi/) Python
|
|
15
|
+
client (retries, error mapping, typed models) — no hand-rolled HTTP.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install livetennisapi-haystack
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You need a Live Tennis API key (free tier: 1000 requests/day, 30/min). Export it as an
|
|
24
|
+
environment variable — the components read `LIVETENNISAPI_KEY` by default and never accept a
|
|
25
|
+
plain-string key:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export LIVETENNISAPI_KEY="your-key"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Standalone
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher
|
|
37
|
+
|
|
38
|
+
fetcher = LiveTennisMatchFetcher() # key from LIVETENNISAPI_KEY
|
|
39
|
+
result = fetcher.run(status="live", limit=5)
|
|
40
|
+
for doc in result["documents"]:
|
|
41
|
+
print(doc.content)
|
|
42
|
+
# e.g. "Carlos Alcaraz (ESP, #2) vs Jannik Sinner (ITA, #1) — match at Wimbledon,
|
|
43
|
+
# grass court, round QF, best of 5. Live now. Score: sets 1-1, games 6-4, 3-6,
|
|
44
|
+
# 2-1, points 30-15. Carlos Alcaraz (ESP, #2) is serving."
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### In a pipeline (runnable with only `LIVETENNISAPI_KEY`)
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from haystack import Pipeline
|
|
51
|
+
|
|
52
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher, LiveTennisPlayerSearch
|
|
53
|
+
|
|
54
|
+
pipe = Pipeline()
|
|
55
|
+
pipe.add_component("matches", LiveTennisMatchFetcher(limit=5))
|
|
56
|
+
pipe.add_component("players", LiveTennisPlayerSearch(limit=3))
|
|
57
|
+
|
|
58
|
+
result = pipe.run({"matches": {"status": "live"}, "players": {"query": "alcaraz"}})
|
|
59
|
+
for doc in result["matches"]["documents"] + result["players"]["documents"]:
|
|
60
|
+
print("-", doc.content)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### RAG over live scores
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from haystack import Pipeline
|
|
67
|
+
from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder
|
|
68
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
69
|
+
from haystack.dataclasses import ChatMessage
|
|
70
|
+
|
|
71
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher
|
|
72
|
+
|
|
73
|
+
prompt_template = [
|
|
74
|
+
ChatMessage.from_system("You are a tennis commentator."),
|
|
75
|
+
ChatMessage.from_user(
|
|
76
|
+
"Current matches:\n"
|
|
77
|
+
"{% for document in documents %}{{ document.content }}\n{% endfor %}\n"
|
|
78
|
+
"Answer the following question: {{ query }}\nAnswer:"
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
pipe = Pipeline()
|
|
83
|
+
pipe.add_component("matches", LiveTennisMatchFetcher(limit=10))
|
|
84
|
+
pipe.add_component("prompt_builder", ChatPromptBuilder(template=prompt_template, required_variables={"query", "documents"}))
|
|
85
|
+
pipe.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
86
|
+
pipe.connect("matches.documents", "prompt_builder.documents")
|
|
87
|
+
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
88
|
+
|
|
89
|
+
query = "Who is closest to winning right now?"
|
|
90
|
+
result = pipe.run({"matches": {"status": "live"}, "prompt_builder": {"query": query}})
|
|
91
|
+
print(result["llm"]["replies"][0].text)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A complete runnable script lives at [`examples/live_demo.py`](examples/live_demo.py).
|
|
95
|
+
|
|
96
|
+
## Behavior worth knowing
|
|
97
|
+
|
|
98
|
+
- **403 tier wall**: when your key is valid but the plan does not unlock an endpoint, the
|
|
99
|
+
component returns a single readable `Document` (tagged `meta["error"] = "upgrade_required"`)
|
|
100
|
+
instead of raising — an agent can tell the user; a RAG pipeline can filter it out. All
|
|
101
|
+
other errors (bad key, network down, rate limit) still raise the official client's typed
|
|
102
|
+
exceptions.
|
|
103
|
+
- **Sparse data is normal**: `score.server` is nullable (between points the feed may not
|
|
104
|
+
know who serves next — the summary simply omits the serving sentence), doubles teams have
|
|
105
|
+
no individual rankings and a null `data_completeness`, and points are strings
|
|
106
|
+
(`"0"`, `"15"`, `"30"`, `"40"`, `"AD"`). The components tolerate all of it.
|
|
107
|
+
- **Serialization**: both components implement `to_dict`/`from_dict`; the API key is stored
|
|
108
|
+
as a `Secret` environment-variable reference, never as a value, so pipelines serialize
|
|
109
|
+
safely to YAML. Note that Haystack 3.0 refuses to deserialize third-party components
|
|
110
|
+
unless their module is allow-listed, so reload pipelines with
|
|
111
|
+
`Pipeline.loads(yaml_str, allowed_modules=["livetennisapi_haystack.match_fetcher", "livetennisapi_haystack.player_search"])`
|
|
112
|
+
(or `haystack.core.serialization.allow_deserialization_module(...)`).
|
|
113
|
+
- **`tour` filter**: the API's documented `tour` query parameter is not yet exposed by
|
|
114
|
+
`livetennisapi` 1.0.2's `list_matches()`, so the component routes that one call through
|
|
115
|
+
the official client's transport layer (same auth/retries/error mapping).
|
|
116
|
+
- **Sync only** for now: `run()` — no `run_async` yet, although the official client has an
|
|
117
|
+
async twin. Planned.
|
|
118
|
+
|
|
119
|
+
## Parameters
|
|
120
|
+
|
|
121
|
+
`LiveTennisMatchFetcher(api_key, status="live", tour=None, limit=10, base_url=None, timeout=30.0)`
|
|
122
|
+
— `status`/`tour`/`limit` can be overridden per `run()`, and `run(match_id=...)` fetches a
|
|
123
|
+
single match. `LiveTennisPlayerSearch(api_key, limit=10, base_url=None, timeout=30.0)` —
|
|
124
|
+
`run(query, limit=None)`.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e . pytest ruff
|
|
130
|
+
pytest # unit tests, fully mocked, no network
|
|
131
|
+
ruff check src tests examples
|
|
132
|
+
LIVETENNISAPI_KEY=... pytest -m integration # live tests, needs a key
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
`livetennisapi-haystack` is distributed under the terms of the
|
|
138
|
+
[MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: integration
|
|
3
|
+
name: Live Tennis API
|
|
4
|
+
description: Live tennis scores, matches and players as Haystack Documents for RAG and agent pipelines
|
|
5
|
+
authors:
|
|
6
|
+
- name: Ben
|
|
7
|
+
socials:
|
|
8
|
+
github: bensynapse
|
|
9
|
+
pypi: https://pypi.org/project/livetennisapi-haystack/
|
|
10
|
+
repo: https://github.com/livetennisapi/livetennisapi-haystack
|
|
11
|
+
type: Data Ingestion
|
|
12
|
+
report_issue: https://github.com/livetennisapi/livetennisapi-haystack/issues
|
|
13
|
+
version: Haystack 2.0
|
|
14
|
+
toc: true
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### Table of Contents
|
|
18
|
+
|
|
19
|
+
- [Overview](#overview)
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [Usage](#usage)
|
|
22
|
+
- [LiveTennisMatchFetcher](#livetennismatchfetcher)
|
|
23
|
+
- [LiveTennisPlayerSearch](#livetennisplayersearch)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
The [Live Tennis API](https://livetennisapi.com) serves live scores, matches and player data
|
|
29
|
+
across the ATP, WTA, Challenger, ITF and junior tours.
|
|
30
|
+
|
|
31
|
+
This integration provides:
|
|
32
|
+
|
|
33
|
+
- **`LiveTennisMatchFetcher`**: fetches live, upcoming or completed matches (optionally one
|
|
34
|
+
match by id, optionally filtered by tour) and returns them as Haystack `Document` objects.
|
|
35
|
+
Each Document's `content` is a clean human-readable match summary and its `meta` carries
|
|
36
|
+
the structured fields (ids, players, sets/games/points, server, winner).
|
|
37
|
+
- **`LiveTennisPlayerSearch`**: searches players by name (ranked players first) and returns
|
|
38
|
+
them as `Document` objects with the same content/meta split.
|
|
39
|
+
|
|
40
|
+
You need a Live Tennis API key to use this integration (a free tier is available). The
|
|
41
|
+
components read it from the `LIVETENNISAPI_KEY` environment variable via Haystack's `Secret`,
|
|
42
|
+
so serialized pipelines never contain the key. Built on the official
|
|
43
|
+
[`livetennisapi`](https://pypi.org/project/livetennisapi/) Python client.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install livetennisapi-haystack
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
Set your API key as the `LIVETENNISAPI_KEY` environment variable.
|
|
54
|
+
|
|
55
|
+
### LiveTennisMatchFetcher
|
|
56
|
+
|
|
57
|
+
#### Basic Example
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher
|
|
61
|
+
|
|
62
|
+
fetcher = LiveTennisMatchFetcher(limit=5)
|
|
63
|
+
result = fetcher.run(status="live")
|
|
64
|
+
for doc in result["documents"]:
|
|
65
|
+
print(doc.content)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### In a Pipeline
|
|
69
|
+
|
|
70
|
+
A RAG pipeline that answers questions about the matches on court right now:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from haystack import Pipeline
|
|
74
|
+
from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder
|
|
75
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
76
|
+
from haystack.dataclasses import ChatMessage
|
|
77
|
+
|
|
78
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher
|
|
79
|
+
|
|
80
|
+
prompt_template = [
|
|
81
|
+
ChatMessage.from_system("You are a tennis commentator."),
|
|
82
|
+
ChatMessage.from_user(
|
|
83
|
+
"Current matches:\n"
|
|
84
|
+
"{% for document in documents %}{{ document.content }}\n{% endfor %}\n"
|
|
85
|
+
"Answer the following question: {{ query }}\nAnswer:"
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
pipe = Pipeline()
|
|
90
|
+
pipe.add_component("matches", LiveTennisMatchFetcher(limit=10))
|
|
91
|
+
pipe.add_component(
|
|
92
|
+
"prompt_builder",
|
|
93
|
+
ChatPromptBuilder(template=prompt_template, required_variables={"query", "documents"}),
|
|
94
|
+
)
|
|
95
|
+
pipe.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
96
|
+
pipe.connect("matches.documents", "prompt_builder.documents")
|
|
97
|
+
pipe.connect("prompt_builder.prompt", "llm.messages")
|
|
98
|
+
|
|
99
|
+
query = "Who is closest to winning right now?"
|
|
100
|
+
result = pipe.run({"matches": {"status": "live"}, "prompt_builder": {"query": query}})
|
|
101
|
+
print(result["llm"]["replies"][0].text)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Parameters
|
|
105
|
+
|
|
106
|
+
- **`api_key`**: API key for the Live Tennis API. Defaults to the `LIVETENNISAPI_KEY`
|
|
107
|
+
environment variable.
|
|
108
|
+
- **`status`**: Default lifecycle status: `"live"`, `"upcoming"` or `"completed"`.
|
|
109
|
+
Overridable per `run()`.
|
|
110
|
+
- **`tour`**: Optional tour filter: `"atp"`, `"wta"`, `"challenger"`, `"itf"` or
|
|
111
|
+
`"juniors"`. Each value covers its singles and doubles draws.
|
|
112
|
+
- **`limit`**: Maximum number of matches to return (1-200). Defaults to 10.
|
|
113
|
+
- **`run(match_id=...)`**: fetches one specific match instead of a listing.
|
|
114
|
+
|
|
115
|
+
If your key's tier does not unlock an endpoint (HTTP 403), the component returns a single
|
|
116
|
+
readable Document tagged `meta["error"] = "upgrade_required"` instead of raising, so agents
|
|
117
|
+
can surface the message and RAG pipelines can filter it.
|
|
118
|
+
|
|
119
|
+
### LiveTennisPlayerSearch
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from livetennisapi_haystack import LiveTennisPlayerSearch
|
|
123
|
+
|
|
124
|
+
search = LiveTennisPlayerSearch(limit=3)
|
|
125
|
+
result = search.run(query="alcaraz")
|
|
126
|
+
for doc in result["documents"]:
|
|
127
|
+
print(doc.content)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Parameters
|
|
131
|
+
|
|
132
|
+
- **`api_key`**: API key. Defaults to the `LIVETENNISAPI_KEY` environment variable.
|
|
133
|
+
- **`limit`**: Maximum number of players to return (1-200). Defaults to 10.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
`livetennisapi-haystack` is distributed under the terms of the
|
|
138
|
+
[MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Live demo: a real Haystack pipeline over the Live Tennis API.
|
|
2
|
+
|
|
3
|
+
Needs only LIVETENNISAPI_KEY in the environment:
|
|
4
|
+
|
|
5
|
+
export LIVETENNISAPI_KEY="your-key"
|
|
6
|
+
python examples/live_demo.py
|
|
7
|
+
|
|
8
|
+
Fetches live matches (falling back to upcoming, then completed, if no tennis is
|
|
9
|
+
on right now) plus a player search, and prints every Document.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from haystack import Pipeline
|
|
16
|
+
|
|
17
|
+
from livetennisapi_haystack import LiveTennisMatchFetcher, LiveTennisPlayerSearch
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
if not os.environ.get("LIVETENNISAPI_KEY"):
|
|
22
|
+
print("Set LIVETENNISAPI_KEY first.", file=sys.stderr)
|
|
23
|
+
return 1
|
|
24
|
+
|
|
25
|
+
pipe = Pipeline()
|
|
26
|
+
pipe.add_component("matches", LiveTennisMatchFetcher(limit=10))
|
|
27
|
+
pipe.add_component("players", LiveTennisPlayerSearch(limit=3))
|
|
28
|
+
|
|
29
|
+
docs = []
|
|
30
|
+
for status in ("live", "upcoming", "completed"):
|
|
31
|
+
result = pipe.run({"matches": {"status": status}, "players": {"query": "alcaraz"}})
|
|
32
|
+
docs = result["matches"]["documents"]
|
|
33
|
+
if docs:
|
|
34
|
+
print(f"== {status} matches ({len(docs)}) ==")
|
|
35
|
+
break
|
|
36
|
+
print(f"== no {status} matches right now ==")
|
|
37
|
+
|
|
38
|
+
for doc in docs:
|
|
39
|
+
print("-", doc.content)
|
|
40
|
+
print(" meta:", {k: v for k, v in doc.meta.items() if v is not None})
|
|
41
|
+
|
|
42
|
+
player_docs = result["players"]["documents"]
|
|
43
|
+
print(f"\n== player search 'alcaraz' ({len(player_docs)}) ==")
|
|
44
|
+
for doc in player_docs:
|
|
45
|
+
print("-", doc.content)
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
raise SystemExit(main())
|