spot-sdk-python 2.0.0b8__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.
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/PKG-INFO +2 -2
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/README.md +1 -1
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/pyproject.toml +1 -1
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/__init__.py +2 -1
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/clients.py +16 -7
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/orchestrator.py +1 -1
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/retriever.py +29 -4
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_spot.py +19 -5
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/workflow.py +1 -5
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/_json.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/app.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/artifacts.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/config.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/email.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/errors.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/knowledge.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/knowledge_tags.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/llm.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/logging.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/manifest.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/mime.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/model_input.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/py.typed +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/settings.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/signals.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/README.md +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/__init__.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/contract.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_knowledge_client.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_llm.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/settings.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/views.py +0 -0
- {spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/urls.py +0 -0
- {spot_sdk_python-2.0.0b8 → 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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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
|
|
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
|
|
@@ -111,7 +116,6 @@ def ingestion_request(
|
|
|
111
116
|
raw: bytes,
|
|
112
117
|
*,
|
|
113
118
|
envelope: Envelope | None = None,
|
|
114
|
-
workflow_id: str | None = None,
|
|
115
119
|
priority: IngestionPriority = "normal",
|
|
116
120
|
wait_for_verdict: bool = False,
|
|
117
121
|
timeout_ms: int = 30_000,
|
|
@@ -121,7 +125,6 @@ def ingestion_request(
|
|
|
121
125
|
return IngestionRequest(
|
|
122
126
|
raw=base64.b64encode(raw).decode("ascii"),
|
|
123
127
|
envelope=envelope,
|
|
124
|
-
workflow_id=workflow_id,
|
|
125
128
|
priority=priority,
|
|
126
129
|
wait_for_verdict=wait_for_verdict,
|
|
127
130
|
timeout_ms=timeout_ms,
|
|
@@ -132,7 +135,6 @@ def ingestion_body(
|
|
|
132
135
|
raw: bytes,
|
|
133
136
|
*,
|
|
134
137
|
envelope: Envelope | None = None,
|
|
135
|
-
workflow_id: str | None = None,
|
|
136
138
|
priority: IngestionPriority = "normal",
|
|
137
139
|
wait_for_verdict: bool = False,
|
|
138
140
|
timeout_ms: int = 30_000,
|
|
@@ -149,7 +151,6 @@ def ingestion_body(
|
|
|
149
151
|
request = IngestionRequest(
|
|
150
152
|
raw=(pieces[0] if pieces else b"").decode("ascii"),
|
|
151
153
|
envelope=envelope,
|
|
152
|
-
workflow_id=workflow_id,
|
|
153
154
|
priority=priority,
|
|
154
155
|
wait_for_verdict=wait_for_verdict,
|
|
155
156
|
timeout_ms=timeout_ms,
|
|
@@ -191,7 +192,6 @@ class SpotClient:
|
|
|
191
192
|
raw: bytes,
|
|
192
193
|
*,
|
|
193
194
|
envelope: Envelope | None = None,
|
|
194
|
-
workflow_id: str | None = None,
|
|
195
195
|
priority: IngestionPriority = "normal",
|
|
196
196
|
wait_for_verdict: bool = False,
|
|
197
197
|
timeout_ms: int = 30_000,
|
|
@@ -211,7 +211,6 @@ class SpotClient:
|
|
|
211
211
|
ingestion_body,
|
|
212
212
|
raw,
|
|
213
213
|
envelope=envelope,
|
|
214
|
-
workflow_id=workflow_id,
|
|
215
214
|
priority=priority,
|
|
216
215
|
wait_for_verdict=wait_for_verdict,
|
|
217
216
|
timeout_ms=timeout_ms,
|
|
@@ -225,6 +224,16 @@ class SpotClient:
|
|
|
225
224
|
)
|
|
226
225
|
return read_answer(IngestionResult, response, "POST", "/internal/ingest")
|
|
227
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
|
+
|
|
228
237
|
async def raw(self, email_id: UUID) -> bytes:
|
|
229
238
|
"""The stored message, as received (scope ``content:read``)."""
|
|
230
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
|
|
91
|
+
description="The job's mode (its connection's and its route's); a responder acts only in enforce"
|
|
92
92
|
)
|
|
@@ -21,7 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
import base64
|
|
22
22
|
import binascii
|
|
23
23
|
from enum import StrEnum
|
|
24
|
-
from typing import Annotated, Literal
|
|
24
|
+
from typing import Annotated, Any, Literal
|
|
25
25
|
from uuid import UUID
|
|
26
26
|
|
|
27
27
|
from pydantic import (
|
|
@@ -30,10 +30,12 @@ from pydantic import (
|
|
|
30
30
|
Field,
|
|
31
31
|
PlainSerializer,
|
|
32
32
|
WithJsonSchema,
|
|
33
|
+
model_validator,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
from .email import Envelope
|
|
36
37
|
from .verdict import Score, VerdictClass
|
|
38
|
+
from .workflow import ResponseMode
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def _decode_message(value: object) -> bytes:
|
|
@@ -78,9 +80,6 @@ class IngestionRequest(BaseModel):
|
|
|
78
80
|
|
|
79
81
|
raw: Base64Message = Field(description="The RFC 5322 message")
|
|
80
82
|
envelope: Envelope | None = None
|
|
81
|
-
workflow_id: str | None = Field(
|
|
82
|
-
default=None, description="None: the platform's default workflow"
|
|
83
|
-
)
|
|
84
83
|
priority: IngestionPriority = "normal"
|
|
85
84
|
wait_for_verdict: bool = False
|
|
86
85
|
timeout_ms: int = Field(
|
|
@@ -90,6 +89,17 @@ class IngestionRequest(BaseModel):
|
|
|
90
89
|
description="The caller's budget when wait_for_verdict is set",
|
|
91
90
|
)
|
|
92
91
|
|
|
92
|
+
@model_validator(mode="before")
|
|
93
|
+
@classmethod
|
|
94
|
+
def _refuse_workflow_id(cls, data: Any) -> Any:
|
|
95
|
+
"""Refuse ``workflow_id`` rather than drop it as other unknown fields are:
|
|
96
|
+
a retriever that still names a workflow would otherwise be silently routed."""
|
|
97
|
+
if isinstance(data, dict) and "workflow_id" in data:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
"workflow_id is no longer taken: SPOT's routes choose the workflow"
|
|
100
|
+
)
|
|
101
|
+
return data
|
|
102
|
+
|
|
93
103
|
|
|
94
104
|
class IngestionResult(BaseModel):
|
|
95
105
|
"""The response of ``POST /internal/ingest`` for a stored message."""
|
|
@@ -106,3 +116,18 @@ class IngestionResult(BaseModel):
|
|
|
106
116
|
error: str | None = Field(
|
|
107
117
|
default=None, description="Why there is no verdict, when the status is timeout"
|
|
108
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,
|
|
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
|
|
|
@@ -57,7 +66,6 @@ class FakeSpot:
|
|
|
57
66
|
raw: bytes,
|
|
58
67
|
*,
|
|
59
68
|
envelope: Envelope | None = None,
|
|
60
|
-
workflow_id: str | None = None,
|
|
61
69
|
priority: IngestionPriority = "normal",
|
|
62
70
|
wait_for_verdict: bool = False,
|
|
63
71
|
timeout_ms: int = 30_000,
|
|
@@ -65,7 +73,6 @@ class FakeSpot:
|
|
|
65
73
|
request = ingestion_request(
|
|
66
74
|
raw,
|
|
67
75
|
envelope=envelope,
|
|
68
|
-
workflow_id=workflow_id,
|
|
69
76
|
priority=priority,
|
|
70
77
|
wait_for_verdict=wait_for_verdict,
|
|
71
78
|
timeout_ms=timeout_ms,
|
|
@@ -75,7 +82,10 @@ class FakeSpot:
|
|
|
75
82
|
email_id = self.add_email(request.raw)
|
|
76
83
|
if not wait_for_verdict:
|
|
77
84
|
return IngestionResult(
|
|
78
|
-
job_id=job_id,
|
|
85
|
+
job_id=job_id,
|
|
86
|
+
email_id=email_id,
|
|
87
|
+
status=IngestionStatus.ACCEPTED,
|
|
88
|
+
mode=self.mode,
|
|
79
89
|
)
|
|
80
90
|
return IngestionResult(
|
|
81
91
|
job_id=job_id,
|
|
@@ -83,8 +93,12 @@ class FakeSpot:
|
|
|
83
93
|
status=IngestionStatus.ANALYZED,
|
|
84
94
|
verdict=self.verdict,
|
|
85
95
|
score=self.score,
|
|
96
|
+
mode=self.mode,
|
|
86
97
|
)
|
|
87
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
|
+
|
|
88
102
|
async def raw(self, email_id: UUID) -> bytes:
|
|
89
103
|
return self._stored(email_id, f"/internal/emails/{email_id}/raw")
|
|
90
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spot_sdk_python-2.0.0b8 → spot_sdk_python-2.0.0b10}/spot_sdk/testing/fake_knowledge_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|