spot-sdk-python 2.1.2__tar.gz → 2.2.0b2__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.
Files changed (43) hide show
  1. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/PKG-INFO +1 -1
  2. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/pyproject.toml +1 -1
  3. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/pyproject.toml.orig +1 -1
  4. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/__init__.py +3 -2
  5. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/clients.py +31 -1
  6. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/mime.py +12 -0
  7. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/README.md +1 -1
  8. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/fake_spot.py +1 -1
  9. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/README.md +0 -0
  10. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/_json.py +0 -0
  11. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/app.py +0 -0
  12. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/artifacts.py +0 -0
  13. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/config.py +0 -0
  14. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/correlation.py +0 -0
  15. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/email.py +0 -0
  16. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/errors.py +0 -0
  17. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/hosting.py +0 -0
  18. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/knowledge.py +0 -0
  19. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/knowledge_tags.py +0 -0
  20. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/llm.py +0 -0
  21. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/logging.py +0 -0
  22. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/manifest.py +0 -0
  23. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/model_input.py +0 -0
  24. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/orchestrator.py +0 -0
  25. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/proxy.py +0 -0
  26. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/py.typed +0 -0
  27. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/retriever.py +0 -0
  28. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/settings.py +0 -0
  29. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/signals.py +0 -0
  30. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/__init__.py +0 -0
  31. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/container.py +0 -0
  32. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/contract.py +0 -0
  33. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/corpus.py +0 -0
  34. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/engine.py +0 -0
  35. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/fake_knowledge_client.py +0 -0
  36. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/fake_llm.py +0 -0
  37. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/metrics.py +0 -0
  38. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/report.py +0 -0
  39. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/settings.py +0 -0
  40. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/testing/views.py +0 -0
  41. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/urls.py +0 -0
  42. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/verdict.py +0 -0
  43. {spot_sdk_python-2.1.2 → spot_sdk_python-2.2.0b2}/spot_sdk/workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spot-sdk-python
3
- Version: 2.1.2
3
+ Version: 2.2.0b2
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>
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "spot-sdk-python"
7
- version = "2.1.2"
7
+ version = "2.2.0b2"
8
8
  description = "SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "spot-sdk-python"
7
- version = "2.1.2"
7
+ version = "2.2.0b2"
8
8
  description = "SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -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",
@@ -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
 
@@ -1776,6 +1776,18 @@ def _addresses(value: str, cut: _Cutter, punycode: _Punycode) -> Iterator[Addres
1776
1776
  )
1777
1777
 
1778
1778
 
1779
+ def mailbox(value: str) -> Address | None:
1780
+ """The first mailbox of an address header's ``value``, as :func:`parse` reads
1781
+ a From into ``EmailView.from_``.
1782
+
1783
+ ``value`` is a header's value as the view keeps it (unfolded, RFC 2047
1784
+ decoded). A value that does not parse is kept whole, with no domain; an
1785
+ empty one is None. Its strings are cut to the view's budgets, its A-labels
1786
+ decoded within one message's budget, as :func:`parse` does.
1787
+ """
1788
+ return next(_addresses(value, _Cutter(), _Punycode()), None)
1789
+
1790
+
1779
1791
  def _date(value: str) -> datetime | None:
1780
1792
  try:
1781
1793
  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,