spot-sdk-python 1.2.0__py3-none-any.whl → 2.0.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.
Files changed (48) hide show
  1. spot_sdk/__init__.py +134 -72
  2. spot_sdk/_json.py +46 -0
  3. spot_sdk/app.py +669 -0
  4. spot_sdk/artifacts.py +205 -0
  5. spot_sdk/clients.py +267 -0
  6. spot_sdk/config.py +0 -8
  7. spot_sdk/correlation.py +65 -0
  8. spot_sdk/email.py +178 -129
  9. spot_sdk/knowledge.py +133 -120
  10. spot_sdk/llm.py +514 -0
  11. spot_sdk/logging.py +220 -69
  12. spot_sdk/manifest.py +281 -0
  13. spot_sdk/mime.py +2754 -0
  14. spot_sdk/model_input.py +190 -0
  15. spot_sdk/orchestrator.py +73 -60
  16. spot_sdk/proxy.py +94 -0
  17. spot_sdk/py.typed +0 -0
  18. spot_sdk/retriever.py +98 -232
  19. spot_sdk/settings.py +79 -0
  20. spot_sdk/signals.py +42 -0
  21. spot_sdk/testing/README.md +22 -74
  22. spot_sdk/testing/__init__.py +16 -18
  23. spot_sdk/testing/contract.py +219 -0
  24. spot_sdk/testing/fake_knowledge_client.py +79 -25
  25. spot_sdk/testing/fake_llm.py +70 -0
  26. spot_sdk/testing/fake_spot.py +122 -0
  27. spot_sdk/testing/settings.py +128 -0
  28. spot_sdk/testing/views.py +47 -0
  29. spot_sdk/urls.py +36 -0
  30. spot_sdk/verdict.py +127 -0
  31. spot_sdk/workflow.py +83 -23
  32. spot_sdk_python-2.0.0.dist-info/METADATA +117 -0
  33. spot_sdk_python-2.0.0.dist-info/RECORD +36 -0
  34. {spot_sdk_python-1.2.0.dist-info → spot_sdk_python-2.0.0.dist-info}/WHEEL +1 -1
  35. spot_sdk/analysis_context.py +0 -99
  36. spot_sdk/analyzer.py +0 -106
  37. spot_sdk/analyzer_base.py +0 -316
  38. spot_sdk/api_gateway.py +0 -271
  39. spot_sdk/config_client.py +0 -203
  40. spot_sdk/config_helpers.py +0 -25
  41. spot_sdk/ollama.py +0 -58
  42. spot_sdk/plugin.py +0 -35
  43. spot_sdk/results.py +0 -129
  44. spot_sdk/settings_schema.py +0 -56
  45. spot_sdk/testing/factories.py +0 -177
  46. spot_sdk/threat_levels.py +0 -33
  47. spot_sdk_python-1.2.0.dist-info/METADATA +0 -353
  48. spot_sdk_python-1.2.0.dist-info/RECORD +0 -28
spot_sdk/__init__.py CHANGED
@@ -1,99 +1,161 @@
1
- """SPOT API interfaces and data models."""
1
+ """SPOT SDK: the plugin contract v2, the plugin runtime and its clients."""
2
2
 
3
- from .analysis_context import AnalysisContextReader
4
- from .analyzer import AnalyzerCapability, AnalyzerInterface, ThreatLevel
5
- from .config import ConfigOption, ConfigReloadResult, ConfigStatus, ConfigUpdateRequest
6
- from .config_client import ConfigClient
7
- from .config_helpers import merge_settings
8
- from .email import Attachment, Email, EmailHeader
3
+ from importlib import import_module
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from .config import ConfigOption, ConfigStatus, ConfigUpdateRequest
7
+ from .email import Address, Anomaly, AuthResult, EmailView, Envelope, Header, Part, Url
9
8
  from .errors import ErrorResponse
10
- from .knowledge import KnowledgeClient, KnowledgeDocument, chunk_text, content_hash
11
9
  from .knowledge_tags import KnowledgeTag
12
10
  from .logging import configure_logging, get_logger, get_logging_config
13
- from .ollama import LLMResponse, OllamaClientProtocol
14
- from .orchestrator import AnalyzerResult, OrchestrationResult, WorkflowStageResult
15
- from .plugin import PluginKind
16
- from .results import (
17
- AnalysisIndicator,
18
- AnalysisMetadata,
19
- AnalysisResult,
20
- IndicatorType,
11
+ from .manifest import (
12
+ CONTRACT_VERSION,
13
+ Egress,
14
+ Manifest,
15
+ PluginKind,
16
+ Resources,
17
+ SettingField,
18
+ SettingType,
21
19
  )
22
- from .retriever import (
23
- IngestionRequest,
24
- IngestionResult,
25
- IngestionStatus,
26
- MailRetrieverClient,
20
+ from .orchestrator import (
21
+ AnalyzeRequest,
22
+ AnalyzerOutcome,
23
+ AttributedSignal,
24
+ OrchestrationResult,
25
+ RespondRequest,
26
+ StageOutcome,
27
27
  )
28
- from .settings_schema import register_settings_schema
29
- from .threat_levels import confidence_to_threat_level
28
+ from .proxy import PROXY_SECRET, apply_proxy_credentials
29
+ from .retriever import ActionReport, IngestionRequest, IngestionResult, IngestionStatus
30
+ from .settings import PluginSettings
31
+ from .verdict import AnalyzerVerdict, Score, Signal, VerdictClass
30
32
  from .workflow import (
33
+ AggregationMethod,
31
34
  AnalyzerConfig,
35
+ DecisiveSignal,
32
36
  FailureStrategy,
37
+ ResponseMode,
33
38
  RetrievalLimits,
34
39
  RetryConfig,
40
+ VerdictPolicy,
35
41
  Workflow,
36
42
  WorkflowStage,
37
43
  )
38
44
 
45
+ if TYPE_CHECKING:
46
+ from .app import CorrelationMiddleware, PluginHealth, SyncState, create_plugin_app
47
+ from .clients import SpotAPIError, SpotClient
48
+ from .knowledge import KnowledgeClient, KnowledgeDocument, chunk_text, content_hash
49
+ from .llm import LLMClient, LLMOutputError, LLMToolCall, LLMToolCalls, LLMToolRound
50
+
51
+ # The modules that import FastAPI, Starlette or httpx2 load on first use, so importing
52
+ # `spot_sdk.mime` or the models stays light (PEP 562).
53
+ _LAZY = {
54
+ **dict.fromkeys(
55
+ ("CorrelationMiddleware", "PluginHealth", "SyncState", "create_plugin_app"),
56
+ ".app",
57
+ ),
58
+ **dict.fromkeys(("SpotAPIError", "SpotClient"), ".clients"),
59
+ **dict.fromkeys(
60
+ ("KnowledgeClient", "KnowledgeDocument", "chunk_text", "content_hash"),
61
+ ".knowledge",
62
+ ),
63
+ **dict.fromkeys(
64
+ ("LLMClient", "LLMOutputError", "LLMToolCall", "LLMToolCalls", "LLMToolRound"),
65
+ ".llm",
66
+ ),
67
+ }
68
+
69
+
70
+ def __getattr__(name: str) -> Any:
71
+ module = _LAZY.get(name)
72
+ if module is None:
73
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
74
+ value = getattr(import_module(module, __name__), name)
75
+ globals()[name] = value
76
+ return value
77
+
78
+
79
+ def __dir__() -> list[str]:
80
+ return sorted({*globals(), *_LAZY})
81
+
82
+
39
83
  __all__ = [
40
- # Analyzer interface
41
- "AnalyzerInterface",
42
- "AnalyzerCapability",
43
- "ThreatLevel",
84
+ # Email view
85
+ "Address",
86
+ "Anomaly",
87
+ "AuthResult",
88
+ "EmailView",
89
+ "Envelope",
90
+ "Header",
91
+ "Part",
92
+ "Url",
93
+ # Verdicts
94
+ "AnalyzerVerdict",
95
+ "Score",
96
+ "Signal",
97
+ "VerdictClass",
98
+ # Workflow
99
+ "AggregationMethod",
100
+ "AnalyzerConfig",
101
+ "DecisiveSignal",
102
+ "FailureStrategy",
103
+ "ResponseMode",
104
+ "RetrievalLimits",
105
+ "RetryConfig",
106
+ "VerdictPolicy",
107
+ "Workflow",
108
+ "WorkflowStage",
109
+ # Analyzer and responder calls, results
110
+ "AnalyzeRequest",
111
+ "AnalyzerOutcome",
112
+ "AttributedSignal",
113
+ "OrchestrationResult",
114
+ "RespondRequest",
115
+ "StageOutcome",
116
+ # Ingestion
117
+ "ActionReport",
118
+ "IngestionRequest",
119
+ "IngestionResult",
120
+ "IngestionStatus",
121
+ # Plugin manifest
122
+ "CONTRACT_VERSION",
123
+ "Egress",
124
+ "Manifest",
125
+ "PluginKind",
126
+ "Resources",
127
+ "SettingField",
128
+ "SettingType",
129
+ # Plugin runtime
130
+ "CorrelationMiddleware",
131
+ "PROXY_SECRET",
132
+ "PluginHealth",
133
+ "PluginSettings",
134
+ "SyncState",
135
+ "apply_proxy_credentials",
136
+ "create_plugin_app",
137
+ # Clients
138
+ "LLMClient",
139
+ "LLMOutputError",
140
+ "LLMToolCall",
141
+ "LLMToolCalls",
142
+ "LLMToolRound",
143
+ "SpotAPIError",
144
+ "SpotClient",
145
+ # Knowledge Store
146
+ "KnowledgeClient",
147
+ "KnowledgeDocument",
148
+ "KnowledgeTag",
149
+ "chunk_text",
150
+ "content_hash",
44
151
  # Config models
45
152
  "ConfigOption",
46
153
  "ConfigStatus",
47
154
  "ConfigUpdateRequest",
48
- "ConfigReloadResult",
49
- "ConfigClient",
50
155
  # Error response
51
156
  "ErrorResponse",
52
- # Email models
53
- "Email",
54
- "EmailHeader",
55
- "Attachment",
56
- "AnalysisContextReader",
57
- # Analysis results
58
- "AnalysisResult",
59
- "AnalysisIndicator",
60
- "AnalysisMetadata",
61
- "IndicatorType",
62
157
  # Logging
63
158
  "configure_logging",
64
159
  "get_logger",
65
160
  "get_logging_config",
66
- # Workflow
67
- "Workflow",
68
- "WorkflowStage",
69
- "AnalyzerConfig",
70
- "RetryConfig",
71
- "RetrievalLimits",
72
- "FailureStrategy",
73
- # Knowledge Store
74
- "KnowledgeDocument",
75
- "KnowledgeClient",
76
- "KnowledgeTag",
77
- "chunk_text",
78
- "content_hash",
79
- # Plugin vocabulary
80
- "PluginKind",
81
- # Mail-retriever ingest contract
82
- "IngestionRequest",
83
- "IngestionResult",
84
- "IngestionStatus",
85
- "MailRetrieverClient",
86
- # Orchestration
87
- "OrchestrationResult",
88
- "AnalyzerResult",
89
- "WorkflowStageResult",
90
- # Ollama protocol
91
- "OllamaClientProtocol",
92
- "LLMResponse",
93
- # Settings schema
94
- "register_settings_schema",
95
- # Config helpers
96
- "merge_settings",
97
- # Threat levels
98
- "confidence_to_threat_level",
99
161
  ]
spot_sdk/_json.py ADDED
@@ -0,0 +1,46 @@
1
+ """Strings measured and cut by the size of their JSON.
2
+
3
+ The view and the LLM client's rendering are sent as JSON, where escapes make
4
+ a string larger than its text: a control character takes six bytes
5
+ (``\\u0001``). ``json.dumps`` with ``ensure_ascii=False`` and pydantic's
6
+ ``model_dump_json`` escape strings alike, so sizes here are theirs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+ _MAX_CHARACTER_BYTES = 6
14
+ """The most bytes one character takes: a control character's escape."""
15
+
16
+
17
+ def json_size(text: str) -> int:
18
+ """The UTF-8 bytes of ``text``'s JSON string between its quotes, escapes
19
+ included; a lone surrogate (possible in a string built in Python) counts
20
+ as three."""
21
+ escaped = json.dumps(text, ensure_ascii=False)[1:-1]
22
+ return len(escaped.encode("utf-8", "surrogatepass"))
23
+
24
+
25
+ def cut_json(text: str, max_bytes: int) -> tuple[str, bool]:
26
+ """The longest start of ``text`` whose JSON string takes at most
27
+ ``max_bytes`` bytes between its quotes (``json_size``), and whether that
28
+ cut anything.
29
+
30
+ A character is kept or dropped whole, so neither an escape nor a
31
+ character's UTF-8 bytes are split. The text is measured in chunks that
32
+ surely fit, a sixth of the room left each, then character by character:
33
+ the work follows ``max_bytes``, whatever the text's length.
34
+ """
35
+ if len(text) * _MAX_CHARACTER_BYTES <= max_bytes:
36
+ return text, False # it surely fits
37
+ kept = 0
38
+ room = max_bytes
39
+ while kept < len(text):
40
+ chunk = text[kept : kept + max(room // _MAX_CHARACTER_BYTES, 1)]
41
+ used = json_size(chunk)
42
+ if used > room: # one character, which does not fit
43
+ break
44
+ kept += len(chunk)
45
+ room -= used
46
+ return text[:kept], kept < len(text)