spot-sdk-python 2.1.0b3__py3-none-any.whl → 2.2.0b1__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.
spot_sdk/__init__.py CHANGED
@@ -44,7 +44,7 @@ from .workflow import (
44
44
 
45
45
  if TYPE_CHECKING:
46
46
  from .app import CorrelationMiddleware, PluginHealth, SyncState, create_plugin_app
47
- from .clients import SpotAPIError, SpotClient
47
+ from .clients import SpotAPIError, SpotClient, SpotGateway
48
48
  from .knowledge import KnowledgeClient, KnowledgeDocument, chunk_text, content_hash
49
49
  from .llm import LLMClient, LLMOutputError, LLMToolCall, LLMToolCalls, LLMToolRound
50
50
 
@@ -55,7 +55,7 @@ _LAZY = {
55
55
  ("CorrelationMiddleware", "PluginHealth", "SyncState", "create_plugin_app"),
56
56
  ".app",
57
57
  ),
58
- **dict.fromkeys(("SpotAPIError", "SpotClient"), ".clients"),
58
+ **dict.fromkeys(("SpotAPIError", "SpotClient", "SpotGateway"), ".clients"),
59
59
  **dict.fromkeys(
60
60
  ("KnowledgeClient", "KnowledgeDocument", "chunk_text", "content_hash"),
61
61
  ".knowledge",
@@ -142,6 +142,7 @@ __all__ = [
142
142
  "LLMToolRound",
143
143
  "SpotAPIError",
144
144
  "SpotClient",
145
+ "SpotGateway",
145
146
  # Knowledge Store
146
147
  "KnowledgeClient",
147
148
  "KnowledgeDocument",
spot_sdk/clients.py CHANGED
@@ -10,6 +10,7 @@ the route and the status, never the content.
10
10
  - Analyzers fetch the raw message or one decoded part with
11
11
  :meth:`SpotClient.raw` and :meth:`SpotClient.part`.
12
12
  - Responders read a workflow's result with :meth:`SpotClient.result`.
13
+ - :class:`SpotGateway` types a plugin's client parameter, so a test passes a fake.
13
14
  """
14
15
 
15
16
  from __future__ import annotations
@@ -17,7 +18,7 @@ from __future__ import annotations
17
18
  import asyncio
18
19
  import base64
19
20
  import json
20
- from typing import Any, TypeVar
21
+ from typing import Any, Protocol, TypeVar
21
22
  from urllib.parse import quote
22
23
  from uuid import UUID
23
24
 
@@ -155,6 +156,35 @@ def ingestion_body(
155
156
  return b"".join([b'{"raw": "', *pieces, b'", ', others[1:]])
156
157
 
157
158
 
159
+ class SpotGateway(Protocol):
160
+ """The gateway's plugin routes, as a plugin calls them.
161
+
162
+ A plugin types its parameter with it. ``SpotClient`` calls the gateway over
163
+ HTTP, and ``spot_sdk.testing.FakeSpot`` holds it in memory, so a test passes
164
+ a ``FakeSpot`` with no cast.
165
+ """
166
+
167
+ async def ingest(
168
+ self,
169
+ raw: bytes,
170
+ *,
171
+ envelope: Envelope | None = None,
172
+ priority: IngestionPriority = "normal",
173
+ wait_for_verdict: bool = False,
174
+ timeout_ms: int = 30_000,
175
+ ) -> IngestionResult: ...
176
+
177
+ async def report_action(
178
+ self, job_id: str, action: str, *, applied: bool
179
+ ) -> None: ...
180
+
181
+ async def raw(self, email_id: UUID) -> bytes: ...
182
+
183
+ async def part(self, email_id: UUID, part_id: str) -> tuple[bytes, str]: ...
184
+
185
+ async def result(self, job_id: str) -> OrchestrationResult: ...
186
+
187
+
158
188
  class SpotClient:
159
189
  """The api-gateway's plugin routes, called with the plugin's own token."""
160
190
 
spot_sdk/hosting.py CHANGED
@@ -44,7 +44,11 @@ could expand past the bound once decoded."""
44
44
 
45
45
 
46
46
  class AnswerRefusedError(Exception):
47
- """A plugin's answer that is not read: encoded, or longer than ``MAX_ANSWER_BYTES``."""
47
+ """A plugin's answer that is not read: encoded, or longer than its bound."""
48
+
49
+
50
+ class AnswerTooLargeError(AnswerRefusedError):
51
+ """An answer longer than its bound, which a caller may treat apart from an encoded one."""
48
52
 
49
53
 
50
54
  class InvalidResponseError(Exception):
@@ -121,11 +125,14 @@ def secrets_archive(secrets: Mapping[str, str]) -> bytes:
121
125
  return buffer.getvalue()
122
126
 
123
127
 
124
- async def read_answer(response: httpx2.Response) -> bytes:
125
- """A streamed answer's body, as sent, read up to ``MAX_ANSWER_BYTES``.
128
+ async def read_answer(
129
+ response: httpx2.Response, max_bytes: int = MAX_ANSWER_BYTES
130
+ ) -> bytes:
131
+ """A streamed answer's body, as sent, read up to ``max_bytes``.
126
132
 
127
133
  Raises:
128
- AnswerRefusedError: the answer is encoded, or longer than ``MAX_ANSWER_BYTES``
134
+ AnswerRefusedError: the answer is encoded
135
+ AnswerTooLargeError: the answer is longer than ``max_bytes``
129
136
  """
130
137
  if (
131
138
  response.headers.get("content-encoding", "identity").strip().lower()
@@ -135,8 +142,8 @@ async def read_answer(response: httpx2.Response) -> bytes:
135
142
  body = bytearray()
136
143
  async for chunk in response.aiter_raw():
137
144
  body += chunk
138
- if len(body) > MAX_ANSWER_BYTES:
139
- raise AnswerRefusedError("response too large")
145
+ if len(body) > max_bytes:
146
+ raise AnswerTooLargeError(f"response too large: over {max_bytes} bytes")
140
147
  return bytes(body)
141
148
 
142
149
 
spot_sdk/mime.py CHANGED
@@ -1772,6 +1772,18 @@ def _addresses(value: str, cut: _Cutter, punycode: _Punycode) -> Iterator[Addres
1772
1772
  )
1773
1773
 
1774
1774
 
1775
+ def mailbox(value: str) -> Address | None:
1776
+ """The first mailbox of an address header's ``value``, as :func:`parse` reads
1777
+ a From into ``EmailView.from_``.
1778
+
1779
+ ``value`` is a header's value as the view keeps it (unfolded, RFC 2047
1780
+ decoded). A value that does not parse is kept whole, with no domain; an
1781
+ empty one is None. Its strings are cut to the view's budgets, its A-labels
1782
+ decoded within one message's budget, as :func:`parse` does.
1783
+ """
1784
+ return next(_addresses(value, _Cutter(), _Punycode()), None)
1785
+
1786
+
1775
1787
  def _date(value: str) -> datetime | None:
1776
1788
  try:
1777
1789
  return parsedate_to_datetime(value)
@@ -11,7 +11,7 @@ shows them in use.
11
11
  | `assert_contract(app, eml, *, token, timeout=10.0)` | waits until the plugin has started (`/health` is `healthy`), runs the app's `/internal/analyze` on a parsed `.eml` as core does, and checks the token and the `AnalyzerVerdict` |
12
12
  | `assert_bounds_agree(manifest, settings_cls)` | checks that the manifest states the bounds (`ge`, `le`, `pattern`...) of the settings model |
13
13
  | `assert_provider_contract(app, knowledge, *, token)` | runs two syncs of a context provider as core does, and checks the token, `/internal/sync`, `/internal/state` and the documents written to `knowledge` |
14
- | `FakeSpot` | `SpotClient` in memory: `ingest`, `raw`, `part`, `result`; `app(token=, knowledge=)` serves it over HTTP |
14
+ | `FakeSpot` | `SpotClient` in memory: `ingest`, `report_action`, `raw`, `part`, `result`; `app(token=, knowledge=)` serves it over HTTP |
15
15
  | `FakeKnowledgeClient` | `KnowledgeClient` in memory, with naive word matching |
16
16
  | `FakeLLM` | `LLMClient` with scripted answers, recording each request |
17
17
 
@@ -58,7 +58,7 @@ class _Query(BaseModel):
58
58
 
59
59
 
60
60
  class FakeSpot:
61
- """In-memory gateway with the methods of ``SpotClient``."""
61
+ """In-memory gateway with the methods of ``SpotClient``: a ``SpotGateway``."""
62
62
 
63
63
  def __init__(
64
64
  self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spot-sdk-python
3
- Version: 2.1.0b3
3
+ Version: 2.2.0b1
4
4
  Summary: SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers
5
5
  Author: SPOT Project
6
6
  Author-email: SPOT Project <spot@sonn.lu>
@@ -1,19 +1,19 @@
1
- spot_sdk/__init__.py,sha256=l-o_lRVVvHz6eLt3gtUbSEghPCJg-L8XNFxTQXeRokk,4077
1
+ spot_sdk/__init__.py,sha256=8ncEYjaoAewQBh20V9HtZDzvhv4jfYbCpedu8ihk21g,4124
2
2
  spot_sdk/_json.py,sha256=qK1b4EEgc6pHQn-M3_WnMN988N6sw4kvOUr7V-iEk74,1766
3
3
  spot_sdk/app.py,sha256=xVEZatG4KPnJZT5MBT_pjJxa389X-eljjFSBNkW1Nzo,25979
4
4
  spot_sdk/artifacts.py,sha256=mcWKIv1A-v3-bNQPxDVopJLGOfPk6YdiskIRmyV024U,7883
5
- spot_sdk/clients.py,sha256=sf6Mbc9Hw8pcAiD0XuTdM3Yg6b-oQJ6Uyq99Qsdw3-Y,9703
5
+ spot_sdk/clients.py,sha256=GYjVP3duiMo-jLv215JLmMyfFLJL0EavfhHCek231FU,10664
6
6
  spot_sdk/config.py,sha256=sMSCOI_eVlcNiPamj4Nyuq7p44KZr06LFZVDlYgcqiI,1560
7
7
  spot_sdk/correlation.py,sha256=tgIL122V_d0htaR005zBHt2iLOoQnj3E-X-yKyPT6dc,2184
8
8
  spot_sdk/email.py,sha256=ifZHG9M0x-sTrOtohrU1NStsL4qeVddn18JoqLFawIQ,7313
9
9
  spot_sdk/errors.py,sha256=HTaRaoDfn5XV4v03qD4eCM-IgNaR7upQ08UF-I-1qaI,813
10
- spot_sdk/hosting.py,sha256=Dna2jXiTNJFc_38mNSQyMDqZvolg-zo6IkvrCcEyY1A,5618
10
+ spot_sdk/hosting.py,sha256=3yQl5xEiFlVAts065e-LT6-qsajxAVhQ4KetHHmYpNE,5836
11
11
  spot_sdk/knowledge.py,sha256=TiBFPTekknqPuOGcH4FnkEsYBc5zZkmW7ToZTfTstqc,12405
12
12
  spot_sdk/knowledge_tags.py,sha256=BGhDsNeF8Q0MMNEIZjWrgQWjwH_Vf7dGBNN_hlBqTXs,978
13
13
  spot_sdk/llm.py,sha256=IEWhBbEat-7oHSNDg9_QOb7ylzv2g0B9uACpZq9-vMk,18610
14
14
  spot_sdk/logging.py,sha256=Tn_InLtrmy0NBFbkVXreVeXBazaz2bSdqEL1gxQa_Oo,10772
15
15
  spot_sdk/manifest.py,sha256=P38sJ0lPzAeAcd1hY2Qa9QSPrlLSVLpE6mTt-3l7j1Q,12245
16
- spot_sdk/mime.py,sha256=5PTxMpat1wq0k-6OtNAfj57de9pDviuMuWD2WbtynEk,121120
16
+ spot_sdk/mime.py,sha256=cq93SPA6GLX-okjbNZQ9hVHbPmG4LUTbCzhyK48wE6E,121652
17
17
  spot_sdk/model_input.py,sha256=9sWrix9rH2h1oe49GWDOTy9zwsxWLSXtBfyhcYXsZh4,7469
18
18
  spot_sdk/orchestrator.py,sha256=r_-SWCrDLEjBcb_zn7KdMXttzbFvf1LhLx4FW8kq3q4,2665
19
19
  spot_sdk/proxy.py,sha256=HJUPnzxw3e2aZQXMBZmQEGDRPEnFI3Kyh0zaSPJcE5g,3529
@@ -21,7 +21,7 @@ spot_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  spot_sdk/retriever.py,sha256=fQh09-KusdO3EMhIeHX_H2G6N_K57XUk-AKZ7fLTAGk,4374
22
22
  spot_sdk/settings.py,sha256=4FN8WYSEoxY-Gf4D0jaSU-5z6o0EqrQXFbnD03qhd0Q,2884
23
23
  spot_sdk/signals.py,sha256=DkTqCSCcGbbtJjoTFrXExY9TcKFDXb-4SLR8wAgJUx0,1517
24
- spot_sdk/testing/README.md,sha256=N-42IBZOCxf0KbrfZ0nO64eBEC-l2hlQAjq4qIRJLQU,2786
24
+ spot_sdk/testing/README.md,sha256=PBLlLySANJIXGGcSoSXQtUtA2kjfSkSx8w4F4o60ia8,2803
25
25
  spot_sdk/testing/__init__.py,sha256=4pTiWDdyYBFJwQnrhiC8PK8Ag-puKGEAHQG4hv8zfRs,555
26
26
  spot_sdk/testing/container.py,sha256=I4R-K-sWjT-AHPf-DoXacrBvLO-2fVqxPLwTgv0XofM,18504
27
27
  spot_sdk/testing/contract.py,sha256=t4J9HYIw2GF9esViY9362HYWoQunI3t9q6SraQhl0gA,10720
@@ -29,7 +29,7 @@ spot_sdk/testing/corpus.py,sha256=O1tEjwOR2VHPI_kWelQmvFjUXs_0OUtUEFtKfpdighc,92
29
29
  spot_sdk/testing/engine.py,sha256=IviyvQGvSGUiUSaK4Bbpv5O8sRs0LCpw49pXhR3kCZI,16280
30
30
  spot_sdk/testing/fake_knowledge_client.py,sha256=akq8N0xkI5HUlH8XHBq4ZKdEiNBiLwT4OrVaZZRadgs,6395
31
31
  spot_sdk/testing/fake_llm.py,sha256=8CZV1ZiEXOd6AK1oSk_9R8nvzzf5w1EEfPOYu3f3ruo,2538
32
- spot_sdk/testing/fake_spot.py,sha256=ep1BvYybhQKwO5uLe0KC0TwM5685Jv7HXANm2RLB1tM,8008
32
+ spot_sdk/testing/fake_spot.py,sha256=ReZcw-rJU_-bJw3nayjaiUbSsSkLbvTtbah9b6vPgG4,8027
33
33
  spot_sdk/testing/metrics.py,sha256=sXdTw7WPCgyobmjZFk1WlX_qL7oaOGXfQ67zqd8QFY4,5062
34
34
  spot_sdk/testing/report.py,sha256=eToJgYPt0uYMyso87kskDZngKIEHU00RVGYDgsR4hFw,4248
35
35
  spot_sdk/testing/settings.py,sha256=lRwLD41qIV4ejmJJQbIPAJO2x7u8ieRpl1aTYWg9j0U,5113
@@ -37,6 +37,6 @@ spot_sdk/testing/views.py,sha256=Su45wOqCQG_O2WcpZhFnCZCLEc8yd9KsUl3YkjvvM9g,164
37
37
  spot_sdk/urls.py,sha256=pAz5Zara9UkNRoN2Vtx-33pARfBDy3JIA8xi-bkFdmk,1637
38
38
  spot_sdk/verdict.py,sha256=FUtAcsajJKgO_LaAfEJtHYq1K9BwG7a6sfpVkITjs_w,4429
39
39
  spot_sdk/workflow.py,sha256=sxD_Ms-t8Emv2vn4vf7ftJRIsPR9Fy7LumwFji60LP0,6993
40
- spot_sdk_python-2.1.0b3.dist-info/WHEEL,sha256=rp-k_VZaX8oABFEtLwG_Df95p8LSKt9-uPyQ4jKH0P4,81
41
- spot_sdk_python-2.1.0b3.dist-info/METADATA,sha256=d-XJ-fmvs7l7lX1b3QQDB-EIrouRfRaqLDI70Hm7K9A,4078
42
- spot_sdk_python-2.1.0b3.dist-info/RECORD,,
40
+ spot_sdk_python-2.2.0b1.dist-info/WHEEL,sha256=rp-k_VZaX8oABFEtLwG_Df95p8LSKt9-uPyQ4jKH0P4,81
41
+ spot_sdk_python-2.2.0b1.dist-info/METADATA,sha256=9Fo_3t1E7NO4fbQy6p1v39ROAzhKM7TMotYqzyDHwVE,4078
42
+ spot_sdk_python-2.2.0b1.dist-info/RECORD,,