spot-sdk-python 2.0.0b9__tar.gz → 2.0.0b10__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 (34) hide show
  1. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/PKG-INFO +2 -2
  2. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/README.md +1 -1
  3. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/pyproject.toml +1 -1
  4. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/__init__.py +2 -1
  5. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/clients.py +16 -1
  6. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/orchestrator.py +1 -1
  7. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/retriever.py +16 -0
  8. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_spot.py +19 -3
  9. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/workflow.py +1 -5
  10. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/_json.py +0 -0
  11. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/app.py +0 -0
  12. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/artifacts.py +0 -0
  13. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/config.py +0 -0
  14. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/email.py +0 -0
  15. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/errors.py +0 -0
  16. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/knowledge.py +0 -0
  17. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/knowledge_tags.py +0 -0
  18. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/llm.py +0 -0
  19. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/logging.py +0 -0
  20. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/manifest.py +0 -0
  21. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/mime.py +0 -0
  22. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/model_input.py +0 -0
  23. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/py.typed +0 -0
  24. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/settings.py +0 -0
  25. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/signals.py +0 -0
  26. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/README.md +0 -0
  27. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/__init__.py +0 -0
  28. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/contract.py +0 -0
  29. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_knowledge_client.py +0 -0
  30. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_llm.py +0 -0
  31. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/settings.py +0 -0
  32. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/views.py +0 -0
  33. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/urls.py +0 -0
  34. {spot_sdk_python-2.0.0b9 → spot_sdk_python-2.0.0b10}/spot_sdk/verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spot-sdk-python
3
- Version: 2.0.0b9
3
+ Version: 2.0.0b10
4
4
  Summary: SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers
5
5
  License: Apache-2.0
6
6
  Author: SPOT Project
@@ -51,7 +51,7 @@ plugins share:
51
51
  ## Install
52
52
 
53
53
  ```bash
54
- pip install spot-sdk-python==2.0.0b9
54
+ pip install spot-sdk-python==2.0.0b10
55
55
  ```
56
56
 
57
57
  Python 3.11 to 3.15. Version 2 implements plugin contract 2, for
@@ -26,7 +26,7 @@ plugins share:
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- pip install spot-sdk-python==2.0.0b9
29
+ pip install spot-sdk-python==2.0.0b10
30
30
  ```
31
31
 
32
32
  Python 3.11 to 3.15. Version 2 implements plugin contract 2, for
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "spot-sdk-python"
7
- version = "2.0.0b9"
7
+ version = "2.0.0b10"
8
8
  description = "SPOT plugin SDK: the plugin contract, the plugin runtime, its clients and test helpers"
9
9
  authors = ["SPOT Project <spot@sonn.lu>"]
10
10
  license = "Apache-2.0"
@@ -25,7 +25,7 @@ from .orchestrator import (
25
25
  RespondRequest,
26
26
  StageOutcome,
27
27
  )
28
- from .retriever import IngestionRequest, IngestionResult, IngestionStatus
28
+ from .retriever import ActionReport, IngestionRequest, IngestionResult, IngestionStatus
29
29
  from .settings import PluginSettings
30
30
  from .verdict import AnalyzerVerdict, Score, Signal, VerdictClass
31
31
  from .workflow import (
@@ -110,6 +110,7 @@ __all__ = [
110
110
  "RespondRequest",
111
111
  "StageOutcome",
112
112
  # Ingestion
113
+ "ActionReport",
113
114
  "IngestionRequest",
114
115
  "IngestionResult",
115
116
  "IngestionStatus",
@@ -27,7 +27,12 @@ from pydantic import BaseModel, SecretStr, ValidationError
27
27
 
28
28
  from .email import Envelope
29
29
  from .orchestrator import OrchestrationResult
30
- from .retriever import IngestionPriority, IngestionRequest, IngestionResult
30
+ from .retriever import (
31
+ ActionReport,
32
+ IngestionPriority,
33
+ IngestionRequest,
34
+ IngestionResult,
35
+ )
31
36
  from .settings import PluginSettings
32
37
 
33
38
  # The bytes of a raw message encoded at a time: a few milliseconds of a thread
@@ -219,6 +224,16 @@ class SpotClient:
219
224
  )
220
225
  return read_answer(IngestionResult, response, "POST", "/internal/ingest")
221
226
 
227
+ async def report_action(self, job_id: str, action: str, *, applied: bool) -> None:
228
+ """Tell SPOT what the retriever did with a message: ``action``, and whether
229
+ it was ``applied`` (in observe, it is not) (scope ``ingest``)."""
230
+ report = ActionReport(action=action, applied=applied)
231
+ await self._call(
232
+ "POST",
233
+ f"/internal/jobs/{quote(job_id, safe='')}/action",
234
+ content=json_body(report.model_dump(mode="json")),
235
+ )
236
+
222
237
  async def raw(self, email_id: UUID) -> bytes:
223
238
  """The stored message, as received (scope ``content:read``)."""
224
239
  response = await self._call("GET", f"/internal/emails/{email_id}/raw")
@@ -88,5 +88,5 @@ class RespondRequest(BaseModel):
88
88
  decided_by: str
89
89
  signals: list[AttributedSignal]
90
90
  mode: ResponseMode = Field(
91
- description="The workflow's response_mode; a responder acts only in enforce"
91
+ description="The job's mode (its connection's and its route's); a responder acts only in enforce"
92
92
  )
@@ -35,6 +35,7 @@ from pydantic import (
35
35
 
36
36
  from .email import Envelope
37
37
  from .verdict import Score, VerdictClass
38
+ from .workflow import ResponseMode
38
39
 
39
40
 
40
41
  def _decode_message(value: object) -> bytes:
@@ -115,3 +116,18 @@ class IngestionResult(BaseModel):
115
116
  error: str | None = Field(
116
117
  default=None, description="Why there is no verdict, when the status is timeout"
117
118
  )
119
+ mode: ResponseMode = Field(
120
+ default="observe",
121
+ description=(
122
+ "The job's mode: enforce only when its connection and its route both "
123
+ "are; a retriever applies its actions only in enforce"
124
+ ),
125
+ )
126
+
127
+
128
+ class ActionReport(BaseModel):
129
+ """The body of ``POST /internal/jobs/{job_id}/action``: what a retriever
130
+ did with a message (or, in observe, would have done)."""
131
+
132
+ action: str = Field(pattern=r"^[a-z][a-z0-9_]{0,31}$")
133
+ applied: bool
@@ -21,24 +21,33 @@ from ..email import Envelope
21
21
  from ..mime import PartNotFoundError, part_bytes
22
22
  from ..orchestrator import OrchestrationResult
23
23
  from ..retriever import (
24
+ ActionReport,
24
25
  IngestionPriority,
25
26
  IngestionRequest,
26
27
  IngestionResult,
27
28
  IngestionStatus,
28
29
  )
29
30
  from ..verdict import VerdictClass
31
+ from ..workflow import ResponseMode
30
32
 
31
33
 
32
34
  class FakeSpot:
33
35
  """In-memory gateway with the methods of ``SpotClient``."""
34
36
 
35
37
  def __init__(
36
- self, *, verdict: VerdictClass = VerdictClass.BENIGN, score: int = 0
38
+ self,
39
+ *,
40
+ verdict: VerdictClass = VerdictClass.BENIGN,
41
+ score: int = 0,
42
+ mode: ResponseMode = "observe",
37
43
  ) -> None:
38
- """``verdict`` and ``score`` answer an ingest that waits for the verdict."""
44
+ """``verdict`` and ``score`` answer an ingest that waits for the verdict;
45
+ ``mode`` is the mode of every job."""
39
46
  self.verdict = verdict
40
47
  self.score = score
48
+ self.mode = mode
41
49
  self.ingested: list[IngestionRequest] = []
50
+ self.actions: list[tuple[str, ActionReport]] = []
42
51
  self._emails: dict[UUID, bytes] = {}
43
52
  self._results: dict[str, OrchestrationResult] = {}
44
53
 
@@ -73,7 +82,10 @@ class FakeSpot:
73
82
  email_id = self.add_email(request.raw)
74
83
  if not wait_for_verdict:
75
84
  return IngestionResult(
76
- job_id=job_id, email_id=email_id, status=IngestionStatus.ACCEPTED
85
+ job_id=job_id,
86
+ email_id=email_id,
87
+ status=IngestionStatus.ACCEPTED,
88
+ mode=self.mode,
77
89
  )
78
90
  return IngestionResult(
79
91
  job_id=job_id,
@@ -81,8 +93,12 @@ class FakeSpot:
81
93
  status=IngestionStatus.ANALYZED,
82
94
  verdict=self.verdict,
83
95
  score=self.score,
96
+ mode=self.mode,
84
97
  )
85
98
 
99
+ async def report_action(self, job_id: str, action: str, *, applied: bool) -> None:
100
+ self.actions.append((job_id, ActionReport(action=action, applied=applied)))
101
+
86
102
  async def raw(self, email_id: UUID) -> bytes:
87
103
  return self._stored(email_id, f"/internal/emails/{email_id}/raw")
88
104
 
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, model_validator
10
10
  from .verdict import VerdictClass
11
11
 
12
12
  ResponseMode = Literal["observe", "enforce"]
13
- """Whether responders act on verdicts (``enforce``) or only record them (``observe``)."""
13
+ """Whether responders and retrievers act on verdicts (``enforce``) or only record them (``observe``): the mode of a job, from its connection and its route."""
14
14
 
15
15
  DECISIVE_SIGNAL_PATTERN = r"^[a-z0-9_]+(\.[a-z0-9_]+)*\.([a-z0-9_]+|\*)$"
16
16
  """A signal name, or a prefix ending in ``.*``."""
@@ -181,10 +181,6 @@ class Workflow(BaseModel):
181
181
  on_verdict: dict[VerdictClass, list[str]] = Field(
182
182
  default_factory=dict, description="Responder ids to call, by verdict"
183
183
  )
184
- response_mode: ResponseMode = Field(
185
- default="observe",
186
- description="Sent to responders as mode; a responder acts only in enforce",
187
- )
188
184
 
189
185
  @model_validator(mode="after")
190
186
  def _decision_stages_exist(self) -> Self: