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.
@@ -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,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
@@ -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())