specstream-haystack 0.1.0__py3-none-any.whl

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,4 @@
1
+ from haystack_integrations.components.fetchers.specstream._api import SpecStreamError
2
+ from haystack_integrations.components.fetchers.specstream.list_fetcher import SpecStreamListFetcher
3
+
4
+ __all__ = ["SpecStreamError", "SpecStreamListFetcher"]
@@ -0,0 +1,217 @@
1
+ """The SpecStream REST calls the tools share: read a request, price it, start it within a maximum, read its rows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import time
8
+ import urllib.error
9
+ import urllib.parse
10
+ import urllib.request
11
+ from dataclasses import dataclass
12
+ from typing import Any, Callable
13
+
14
+ DEFAULT_BASE_URL = "https://api.specstream.ai"
15
+ USER_AGENT = "langchain-specstream/0.1.2"
16
+ # A list runs for up to about five minutes, so the tools wait a little longer than that by default.
17
+ DEFAULT_WAIT_SECONDS = 420.0
18
+ FINISHED = {"completed", "partial"}
19
+ STOPPED = {"failed", "cancelled", "canceled", "expired"}
20
+ # Fields every row carries; a column with the same name is renamed so it can't overwrite them.
21
+ RESERVED = {"list_id", "list_link", "position", "source_url", "checked_at"}
22
+
23
+ Opener = Callable[[urllib.request.Request, float], Any]
24
+
25
+
26
+ class SpecStreamError(RuntimeError):
27
+ """SpecStream refused a call or answered with something this package can't read."""
28
+
29
+
30
+ def _dollars(cents: int) -> str:
31
+ return f"${cents / 100:.2f}"
32
+
33
+
34
+ def list_link(find_id: str) -> str:
35
+ return f"https://specstream.ai/workspace/collections/{urllib.parse.quote(find_id, safe='')}"
36
+
37
+
38
+ def flatten_row(find_id: str, record: dict[str, Any]) -> dict[str, Any]:
39
+ """One row as flat fields: each column's label holds what the list shows, plus where the row was read."""
40
+ row: dict[str, Any] = {"list_id": find_id, "position": record.get("ordinal")}
41
+ source: dict[str, Any] | None = None
42
+ for cell in record.get("details") or record.get("cells") or []:
43
+ label = str(cell.get("label") or cell.get("column_id") or "").strip()
44
+ if not label:
45
+ continue
46
+ if label in RESERVED:
47
+ label = f"{label} (column)"
48
+ if cell.get("absence"):
49
+ value: Any = ""
50
+ elif cell.get("display") is not None:
51
+ value = cell["display"]
52
+ else:
53
+ value = cell.get("value") if cell.get("value") is not None else ""
54
+ row[label] = value
55
+ provenance = cell.get("provenance") or {}
56
+ if source is None and str(provenance.get("source_url") or "").startswith("http"):
57
+ source = provenance
58
+ row["source_url"] = source.get("source_url", "") if source else ""
59
+ row["checked_at"] = source.get("observed_at", "") if source else ""
60
+ row["list_link"] = list_link(find_id)
61
+ return row
62
+
63
+
64
+ @dataclass
65
+ class SpecStreamAPI:
66
+ api_key: str
67
+ base_url: str = DEFAULT_BASE_URL
68
+ timeout: float = 150.0
69
+ opener: Opener | None = None
70
+ sleep: Callable[[float], None] = time.sleep
71
+ clock: Callable[[], float] = time.monotonic
72
+
73
+ def request(self, method: str, path: str, body: dict[str, Any] | None = None, *, allow_404: bool = False) -> dict[str, Any] | None:
74
+ key = (self.api_key or "").strip()
75
+ if not key:
76
+ raise SpecStreamError("Set SPECSTREAM_API_KEY or pass api_key. Create a key under Agents & API in your SpecStream workspace.")
77
+ request = urllib.request.Request(
78
+ self.base_url.rstrip("/") + path,
79
+ data=None if body is None else json.dumps(body).encode(),
80
+ method=method,
81
+ headers={
82
+ "Authorization": key if key.lower().startswith("bearer ") else f"Bearer {key}",
83
+ "Accept": "application/json",
84
+ "Content-Type": "application/json",
85
+ "User-Agent": USER_AGENT,
86
+ },
87
+ )
88
+ open_ = self.opener or (lambda req, timeout: urllib.request.urlopen(req, timeout=timeout))
89
+ try:
90
+ response = open_(request, self.timeout)
91
+ try:
92
+ raw = response.read()
93
+ finally:
94
+ response.close()
95
+ except urllib.error.HTTPError as error:
96
+ if allow_404 and error.code == 404:
97
+ return None
98
+ raise SpecStreamError(_error_message(error)) from None
99
+ except urllib.error.URLError as error:
100
+ raise SpecStreamError(f"Couldn't reach SpecStream: {error.reason}") from None
101
+ try:
102
+ payload = json.loads(raw or b"{}")
103
+ except ValueError:
104
+ payload = None
105
+ if not isinstance(payload, dict) or not isinstance(payload.get("data"), dict):
106
+ raise SpecStreamError("SpecStream returned a response this package couldn't read.")
107
+ return payload["data"]
108
+
109
+ def results(self, find_id: str) -> dict[str, Any]:
110
+ """Every row found so far for a list, following the pages."""
111
+ rows: list[dict[str, Any]] = []
112
+ cursor: str | None = None
113
+ status = "running"
114
+ while True:
115
+ query = {"limit": "100", **({"cursor": cursor} if cursor else {})}
116
+ page = self.request("GET", f"/v1/finds/{urllib.parse.quote(find_id, safe='')}/rows?{urllib.parse.urlencode(query)}") or {}
117
+ status = page.get("status") or status
118
+ rows.extend(flatten_row(find_id, record) for record in page.get("rows") or [])
119
+ cursor = page.get("next_cursor")
120
+ if not cursor:
121
+ break
122
+ return {"list_id": find_id, "status": status, "list_link": list_link(find_id), "row_count": len(rows), "rows": rows}
123
+
124
+ def find_list(
125
+ self,
126
+ request: str,
127
+ *,
128
+ max_results: int,
129
+ max_price_usd: float,
130
+ wait_seconds: float = DEFAULT_WAIT_SECONDS,
131
+ poll_interval: float = 3.0,
132
+ locale: str = "en-US",
133
+ key_prefix: str = "haystack",
134
+ ) -> dict[str, Any]:
135
+ q = (request or "").strip()
136
+ if not q:
137
+ raise ValueError("Describe the list to find.")
138
+ if not isinstance(max_results, int) or max_results < 1:
139
+ raise ValueError("max_results must be 1 or more.")
140
+ max_cents = round(float(max_price_usd) * 100)
141
+ if max_cents <= 0:
142
+ raise ValueError("max_price_usd must be greater than 0.")
143
+
144
+ # The same request with the same limits gets the run it already started in this hour or the one before,
145
+ # so a retried agent step never buys the list twice, even across the turn of the hour.
146
+ def key_for(at: float) -> str:
147
+ hour = time.strftime("%Y-%m-%dT%H", time.gmtime(at))
148
+ return f"{key_prefix}-" + hashlib.sha256(f"{q}|{max_results}|{max_cents}|{hour}".encode()).hexdigest()[:32]
149
+
150
+ now = time.time()
151
+ request_key = key_for(now)
152
+ existing = None
153
+ for candidate in (request_key, key_for(now - 3600)):
154
+ existing = self.request("GET", f"/v1/capacity/requests/{urllib.parse.quote(candidate, safe='')}", allow_404=True)
155
+ if existing and existing.get("find_id"):
156
+ break
157
+ if existing and existing.get("find_id"):
158
+ find_id = existing["find_id"]
159
+ maximum_cents = int(existing.get("maximum_cents") or max_cents)
160
+ else:
161
+ reading = self.request("POST", "/v1/parse", {"q": q, "locale": locale}) or {}
162
+ if reading.get("status") != "runnable":
163
+ read_as = reading.get("canonical_sentence") or q
164
+ return {
165
+ "status": "needs_edit",
166
+ "request": q,
167
+ "message": f'SpecStream needs a clearer request before it can price it. It read: "{read_as}". Add the place or what every row must meet.',
168
+ }
169
+ quote = (
170
+ self.request(
171
+ "POST", "/v1/capacity/quotes", {"ast_hash": reading["ast_hash"], "operation": "find", "requested_units": max_results}
172
+ )
173
+ or {}
174
+ )
175
+ maximum_cents = int(quote.get("maximum_cents") or 0)
176
+ if maximum_cents > max_cents:
177
+ return {
178
+ "status": "over_maximum",
179
+ "request": q,
180
+ "maximum_price_usd": maximum_cents / 100,
181
+ "message": f"Nothing ran: this list can cost up to {_dollars(maximum_cents)}, above the maximum of {_dollars(max_cents)}. Ask for fewer results or raise max_price_usd.",
182
+ }
183
+ run = (
184
+ self.request(
185
+ "POST",
186
+ "/v1/capacity/executions",
187
+ {
188
+ "quote_id": quote["quote_id"],
189
+ "quote_hash": quote["quote_hash"],
190
+ "confirm_max_cents": maximum_cents,
191
+ "idempotency_key": request_key,
192
+ },
193
+ )
194
+ or {}
195
+ )
196
+ find_id = run["find_id"]
197
+
198
+ deadline = self.clock() + max(0.0, wait_seconds)
199
+ while True:
200
+ result = self.results(find_id)
201
+ if result["status"] in FINISHED | STOPPED or self.clock() >= deadline:
202
+ break
203
+ self.sleep(poll_interval)
204
+ result.update({"request": q, "maximum_price_usd": maximum_cents / 100})
205
+ if result["status"] not in FINISHED | STOPPED:
206
+ result["message"] = "Still running. Run the same request again, or open list_link, for the rest."
207
+ return result
208
+
209
+
210
+ def _error_message(error: urllib.error.HTTPError) -> str:
211
+ try:
212
+ payload = json.loads(error.read() or b"{}")
213
+ except ValueError:
214
+ payload = {}
215
+ detail = payload.get("error") if isinstance(payload, dict) else None
216
+ message = detail.get("message") if isinstance(detail, dict) else detail if isinstance(detail, str) else None
217
+ return f"SpecStream answered {error.code}: {message or error.reason}"
@@ -0,0 +1,121 @@
1
+ import json
2
+ from typing import Any, Optional
3
+
4
+ from haystack import Document, component, default_from_dict, default_to_dict
5
+ from haystack.utils import Secret, deserialize_secrets_inplace
6
+
7
+ from haystack_integrations.components.fetchers.specstream._api import (
8
+ DEFAULT_BASE_URL,
9
+ DEFAULT_WAIT_SECONDS,
10
+ SpecStreamAPI,
11
+ SpecStreamError,
12
+ )
13
+
14
+ # Row fields that say where a row came from rather than what it says.
15
+ _META = ("list_id", "list_link", "position", "source_url", "checked_at")
16
+
17
+
18
+ @component
19
+ class SpecStreamListFetcher:
20
+ """
21
+ Turns a plain-English request into the finished list: one Document per matching row.
22
+
23
+ The Document content is the row as JSON. The metadata carries the page the row was read from
24
+ (`source_url`), when it was checked, the list it belongs to, and the row's fields.
25
+ The list starts only if its price is within `max_price_usd`, which you set in code.
26
+
27
+ ```python
28
+ from haystack_integrations.components.fetchers.specstream import SpecStreamListFetcher
29
+
30
+ fetcher = SpecStreamListFetcher(max_price_usd=3, max_results=25)
31
+ result = fetcher.run(request="Breweries in Portland, Oregon that host events, with their website")
32
+ for doc in result["documents"]:
33
+ print(doc.meta["source_url"], doc.content)
34
+ ```
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ max_price_usd: float,
40
+ api_key: Secret = Secret.from_env_var("SPECSTREAM_API_KEY"),
41
+ max_results: int = 25,
42
+ base_url: str = DEFAULT_BASE_URL,
43
+ wait_seconds: float = DEFAULT_WAIT_SECONDS,
44
+ poll_interval: float = 3.0,
45
+ locale: str = "en-US",
46
+ ):
47
+ """
48
+ :param max_price_usd: The most one list may cost, in US dollars. If the quoted price is higher, nothing runs.
49
+ :param api_key: A SpecStream API key. Read from `SPECSTREAM_API_KEY` by default.
50
+ :param max_results: How many matching rows to find when `run` doesn't say.
51
+ :param base_url: The SpecStream API address.
52
+ :param wait_seconds: How long to wait for the list to finish before returning the rows found so far.
53
+ :param poll_interval: Seconds between checks while the list runs.
54
+ :param locale: The language the request is read in.
55
+ """
56
+ if not max_price_usd or max_price_usd <= 0:
57
+ raise ValueError("max_price_usd must be greater than 0.")
58
+ if max_results < 1:
59
+ raise ValueError("max_results must be 1 or more.")
60
+ self.max_price_usd = max_price_usd
61
+ self.api_key = api_key
62
+ self.max_results = max_results
63
+ self.base_url = base_url
64
+ self.wait_seconds = wait_seconds
65
+ self.poll_interval = poll_interval
66
+ self.locale = locale
67
+
68
+ def to_dict(self) -> dict[str, Any]:
69
+ return default_to_dict(
70
+ self,
71
+ max_price_usd=self.max_price_usd,
72
+ api_key=self.api_key.to_dict(),
73
+ max_results=self.max_results,
74
+ base_url=self.base_url,
75
+ wait_seconds=self.wait_seconds,
76
+ poll_interval=self.poll_interval,
77
+ locale=self.locale,
78
+ )
79
+
80
+ @classmethod
81
+ def from_dict(cls, data: dict[str, Any]) -> "SpecStreamListFetcher":
82
+ deserialize_secrets_inplace(data["init_parameters"], keys=["api_key"])
83
+ return default_from_dict(cls, data)
84
+
85
+ @component.output_types(documents=list[Document], list_id=str, status=str, list_link=str, message=str)
86
+ def run(self, request: str, max_results: Optional[int] = None) -> dict[str, Any]:
87
+ """
88
+ Find the list and return its rows.
89
+
90
+ :param request: The list to find, in plain words: what every row must be or meet, and the details to bring back.
91
+ :param max_results: How many matching rows to find. Defaults to the value set at init.
92
+ :returns: `documents` (one per row), `list_id`, `status`, `list_link` and, when the list was still running, a `message`.
93
+ :raises SpecStreamError: When the request is too vague to price, or the price is above `max_price_usd`.
94
+ """
95
+ result = SpecStreamAPI(self.api_key.resolve_value() or "", self.base_url).find_list(
96
+ request,
97
+ max_results=max_results or self.max_results,
98
+ max_price_usd=self.max_price_usd,
99
+ wait_seconds=self.wait_seconds,
100
+ poll_interval=self.poll_interval,
101
+ locale=self.locale,
102
+ key_prefix="haystack",
103
+ )
104
+ if result["status"] in ("needs_edit", "over_maximum"):
105
+ raise SpecStreamError(result["message"])
106
+ documents = [self._to_document(row, result.get("request")) for row in result["rows"]]
107
+ return {
108
+ "documents": documents,
109
+ "list_id": result["list_id"],
110
+ "status": result["status"],
111
+ "list_link": result["list_link"],
112
+ "message": result.get("message", ""),
113
+ }
114
+
115
+ @staticmethod
116
+ def _to_document(row: dict[str, Any], request: Optional[str]) -> Document:
117
+ fields = {label: value for label, value in row.items() if label not in _META}
118
+ meta: dict[str, Any] = {key: row.get(key) for key in _META}
119
+ meta["request"] = request
120
+ meta["fields"] = fields
121
+ return Document(content=json.dumps(fields, ensure_ascii=False, default=str), meta=meta)
@@ -0,0 +1,3 @@
1
+ from haystack_integrations.tools.specstream.list_tool import SpecStreamListTool
2
+
3
+ __all__ = ["SpecStreamListTool"]
@@ -0,0 +1,50 @@
1
+ from typing import Any
2
+
3
+ from haystack.tools import ComponentTool
4
+ from haystack.utils import Secret
5
+
6
+ from haystack_integrations.components.fetchers.specstream import SpecStreamListFetcher
7
+
8
+ DESCRIPTION = (
9
+ "Find a list of real organizations, people, places or records on the live web from a plain-English request, "
10
+ "and get back every matching row with the page it was read from. Use it when the answer is many items that "
11
+ "meet conditions (companies in a city, clinics with a service, funding filings, open grants, tenders, "
12
+ "repositories), not a single fact. Results cost money: the list starts only if its price is within the "
13
+ "maximum the developer set, and only matching rows are paid for."
14
+ )
15
+
16
+
17
+ def _rows_as_text(documents: list) -> str:
18
+ return "\n".join(f"{doc.content} (source: {doc.meta.get('source_url') or 'n/a'})" for doc in documents)
19
+
20
+
21
+ class SpecStreamListTool(ComponentTool):
22
+ """
23
+ A Haystack tool an agent can call to get a finished list from the live web.
24
+
25
+ The model writes the request and picks how many rows. The price ceiling is set here, in code, and the model can't raise it.
26
+
27
+ ```python
28
+ from haystack.components.agents import Agent
29
+ from haystack.components.generators.chat import OpenAIChatGenerator
30
+ from haystack.dataclasses import ChatMessage
31
+ from haystack_integrations.tools.specstream import SpecStreamListTool
32
+
33
+ agent = Agent(chat_generator=OpenAIChatGenerator(model="gpt-4o"), tools=[SpecStreamListTool(max_price_usd=5)])
34
+ ```
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ max_price_usd: float,
40
+ api_key: Secret = Secret.from_env_var("SPECSTREAM_API_KEY"),
41
+ name: str = "specstream_find_list",
42
+ description: str = DESCRIPTION,
43
+ **fetcher_kwargs: Any,
44
+ ):
45
+ super().__init__(
46
+ component=SpecStreamListFetcher(max_price_usd=max_price_usd, api_key=api_key, **fetcher_kwargs),
47
+ name=name,
48
+ description=description,
49
+ outputs_to_string={"source": "documents", "handler": _rows_as_text},
50
+ )
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: specstream-haystack
3
+ Version: 0.1.0
4
+ Summary: Give a Haystack pipeline or agent any list from the live web: ask in plain words, get every matching row as a Document with the page it came from, within a price you set in code.
5
+ Project-URL: Homepage, https://specstream.ai
6
+ Project-URL: Documentation, https://specstream.ai/docs
7
+ Project-URL: Repository, https://github.com/Specstreamai/specstream-tools/tree/main/packages/specstream-haystack
8
+ Project-URL: Issues, https://github.com/Specstreamai/specstream-tools/issues
9
+ Project-URL: Pricing, https://specstream.ai/pricing
10
+ Author-email: SpecStream <support@specstream.ai>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: agents,haystack,haystack-ai,lead-lists,live-web,research,specstream,tools,web-data
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: docstring-parser
28
+ Requires-Dist: haystack-ai>=2.14.0
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=8; extra == 'test'
31
+ Requires-Dist: ruff; extra == 'test'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # specstream-haystack
35
+
36
+ Give your Haystack pipeline or agent any list from the live web. Search components hand a pipeline pages to read; SpecStream hands it the finished list. Ask in plain words – "dental clinics in Lisbon with online booking", "US startups that filed a $5M+ Form D this week" – and get back every matching row as a `Document`, each with the page it was read from. The list starts only within a price you set in code, and the model can't raise it.
37
+
38
+ ```bash
39
+ pip install -U specstream-haystack
40
+ export SPECSTREAM_API_KEY=ss_live_... # Agents & API in your SpecStream workspace
41
+ ```
42
+
43
+ A free account comes with $10 of work to use within 30 days and needs no card: [specstream.ai](https://specstream.ai/signup).
44
+
45
+ ## In a pipeline
46
+
47
+ ```python
48
+ from haystack_integrations.components.fetchers.specstream import SpecStreamListFetcher
49
+
50
+ fetcher = SpecStreamListFetcher(max_price_usd=3, max_results=25)
51
+ result = fetcher.run(request="Breweries in Portland, Oregon that host events, with their website")
52
+
53
+ for doc in result["documents"]:
54
+ print(doc.meta["source_url"], doc.content)
55
+ ```
56
+
57
+ Each `Document` is one row. `content` is the row as JSON. `meta` carries `source_url` (the page the row was read from), `checked_at`, `list_id`, `list_link`, `position`, `request` and `fields` (the row's columns as a dict).
58
+
59
+ `run` also returns `list_id`, `status`, `list_link` and a `message` when the list was still running. When the quoted price is above `max_price_usd`, or the request is too vague to price, nothing runs and `SpecStreamError` explains why.
60
+
61
+ ## As an agent tool
62
+
63
+ ```python
64
+ from haystack.components.agents import Agent
65
+ from haystack.components.generators.chat import OpenAIChatGenerator
66
+ from haystack.dataclasses import ChatMessage
67
+ from haystack_integrations.tools.specstream import SpecStreamListTool
68
+
69
+ agent = Agent(
70
+ chat_generator=OpenAIChatGenerator(model="gpt-4o"),
71
+ tools=[SpecStreamListTool(max_price_usd=5)],
72
+ )
73
+ result = agent.run(messages=[ChatMessage.from_user("Find open grants for urban beekeeping in the EU and list their deadlines.")])
74
+ print(result["last_message"].text)
75
+ ```
76
+
77
+ The agent writes the request and picks how many rows. You decide what one list may cost.
78
+
79
+ ## Retries don't buy twice
80
+
81
+ The same request with the same limits, in the same hour, reaches the run it already started instead of starting another. A retried step never pays for the list twice.
82
+
83
+ MIT licensed. Source: [Specstreamai/specstream-tools](https://github.com/Specstreamai/specstream-tools/tree/main/packages/specstream-haystack).
@@ -0,0 +1,10 @@
1
+ haystack_integrations/components/fetchers/specstream/__init__.py,sha256=-XhGjGz3lfhBzes6wbIxRr9f0QQJDkT43loVIJ9MNRc,242
2
+ haystack_integrations/components/fetchers/specstream/_api.py,sha256=jzt7Ol2JPhgjUrQPMiiN9zR0nEXuMjVAt5oOS5pHqig,9511
3
+ haystack_integrations/components/fetchers/specstream/list_fetcher.py,sha256=m325sq9oNcOvhNCkJotkmyQMfSbKnqQpJRLw_4VN6ZA,5274
4
+ haystack_integrations/components/fetchers/specstream/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ haystack_integrations/tools/specstream/__init__.py,sha256=Iud2Umtey-0ipE-fX_oK7ycyusoYSaYbFBFyuNjdNYk,114
6
+ haystack_integrations/tools/specstream/list_tool.py,sha256=QvwAeuOpm8GNmIySyuRgPOP31YipeGEtSClVKpdl0XM,2065
7
+ specstream_haystack-0.1.0.dist-info/METADATA,sha256=hvRz5iOWWWLt5BaklsnTzlBHCzjRBrpEp12eO1q9UaU,4134
8
+ specstream_haystack-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ specstream_haystack-0.1.0.dist-info/licenses/LICENSE,sha256=lvzGtfzsxc8Xe1_0sabdiWTcSDiSg3jqaFKV9x3F4oo,1071
10
+ specstream_haystack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpecStream SAS
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.