openbox-temporal-sdk-python 1.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.
- openbox/__init__.py +107 -0
- openbox/activities.py +163 -0
- openbox/activity_interceptor.py +755 -0
- openbox/config.py +274 -0
- openbox/otel_setup.py +969 -0
- openbox/py.typed +0 -0
- openbox/span_processor.py +361 -0
- openbox/tracing.py +228 -0
- openbox/types.py +166 -0
- openbox/worker.py +257 -0
- openbox/workflow_interceptor.py +264 -0
- openbox_temporal_sdk_python-1.0.0.dist-info/METADATA +1214 -0
- openbox_temporal_sdk_python-1.0.0.dist-info/RECORD +15 -0
- openbox_temporal_sdk_python-1.0.0.dist-info/WHEEL +4 -0
- openbox_temporal_sdk_python-1.0.0.dist-info/licenses/LICENSE +21 -0
openbox/config.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# openbox/config.py
|
|
2
|
+
"""
|
|
3
|
+
OpenBox SDK - Configuration for workflow-boundary governance (SPEC-003).
|
|
4
|
+
|
|
5
|
+
GovernanceConfig: Configuration for interceptors
|
|
6
|
+
Global config singleton with initialize() function
|
|
7
|
+
|
|
8
|
+
IMPORTANT: No module-level logging import! Python's logging module uses
|
|
9
|
+
linecache -> os.stat which triggers Temporal sandbox restrictions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Set, Optional
|
|
15
|
+
|
|
16
|
+
# NOTE: urllib and logging imports are lazy to avoid Temporal sandbox restrictions.
|
|
17
|
+
# Both use os.stat internally which triggers sandbox errors.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_logger():
|
|
21
|
+
"""Lazy logger to avoid sandbox restrictions."""
|
|
22
|
+
import logging
|
|
23
|
+
return logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# API key format pattern (obx_live_... or obx_test_...)
|
|
26
|
+
API_KEY_PATTERN = re.compile(r"^obx_(live|test)_[a-zA-Z0-9_]+$")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
30
|
+
# Exceptions
|
|
31
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OpenBoxConfigError(Exception):
|
|
35
|
+
"""Raised when OpenBox configuration fails."""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenBoxAuthError(OpenBoxConfigError):
|
|
41
|
+
"""Raised when API key validation fails."""
|
|
42
|
+
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class OpenBoxNetworkError(OpenBoxConfigError):
|
|
47
|
+
"""Raised when network connectivity fails."""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
53
|
+
# GovernanceConfig - Configuration for interceptors
|
|
54
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class GovernanceConfig:
|
|
59
|
+
"""
|
|
60
|
+
Configuration for governance interceptors.
|
|
61
|
+
|
|
62
|
+
Used by both GovernanceInterceptor (workflow-level) and
|
|
63
|
+
ActivityGovernanceInterceptor (activity-level).
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
skip_workflow_types: Workflow types to skip governance for
|
|
67
|
+
skip_signals: Signal names to skip governance for
|
|
68
|
+
enforce_task_queues: Task queues to enforce governance on (None = all)
|
|
69
|
+
on_api_error: Behavior when OpenBox API is unreachable
|
|
70
|
+
- "fail_open" (default) = allow workflow to continue
|
|
71
|
+
- "fail_closed" = deny/stop workflow execution
|
|
72
|
+
api_timeout: Timeout for governance API calls (seconds)
|
|
73
|
+
max_body_size: Maximum body size to capture (None = no limit)
|
|
74
|
+
send_start_event: Send WorkflowStarted event (can disable for performance)
|
|
75
|
+
send_activity_start_event: Send ActivityStarted event (can disable for performance)
|
|
76
|
+
skip_activity_types: Activity types to skip governance for
|
|
77
|
+
hitl_enabled: Enable approval polling for require-approval verdicts (default: True)
|
|
78
|
+
skip_hitl_activity_types: Activity types to skip approval checks (avoids infinite loops)
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
# Workflow types to skip governance for
|
|
82
|
+
skip_workflow_types: Set[str] = field(default_factory=set)
|
|
83
|
+
|
|
84
|
+
# Signal names to skip governance for
|
|
85
|
+
skip_signals: Set[str] = field(default_factory=set)
|
|
86
|
+
|
|
87
|
+
# Task queues to enforce governance on (None = all)
|
|
88
|
+
enforce_task_queues: Optional[Set[str]] = None
|
|
89
|
+
|
|
90
|
+
# Behavior when OpenBox API is unreachable
|
|
91
|
+
# "fail_open" (default) = allow workflow to continue
|
|
92
|
+
# "fail_closed" = deny/stop workflow execution
|
|
93
|
+
on_api_error: str = "fail_open"
|
|
94
|
+
|
|
95
|
+
# Timeout for governance API calls (seconds)
|
|
96
|
+
api_timeout: float = 30.0
|
|
97
|
+
|
|
98
|
+
# Maximum body size to capture (None = no limit)
|
|
99
|
+
max_body_size: Optional[int] = None
|
|
100
|
+
|
|
101
|
+
# Send WorkflowStarted event (can disable for performance)
|
|
102
|
+
send_start_event: bool = True
|
|
103
|
+
|
|
104
|
+
# Send ActivityStarted event before each activity (can disable for performance)
|
|
105
|
+
send_activity_start_event: bool = True
|
|
106
|
+
|
|
107
|
+
# Activity types to skip governance for
|
|
108
|
+
# By default, skip the governance event activity to avoid infinite loops
|
|
109
|
+
skip_activity_types: Set[str] = field(default_factory=lambda: {"send_governance_event"})
|
|
110
|
+
|
|
111
|
+
# Approval polling configuration
|
|
112
|
+
# Enable approval polling for require-approval verdicts
|
|
113
|
+
hitl_enabled: bool = True
|
|
114
|
+
|
|
115
|
+
# Activity types to skip approval checks (to avoid infinite loops)
|
|
116
|
+
# By default, skip the governance event activity
|
|
117
|
+
skip_hitl_activity_types: Set[str] = field(
|
|
118
|
+
default_factory=lambda: {"send_governance_event"}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
123
|
+
# Global Configuration Singleton
|
|
124
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _validate_api_key_format(api_key: str) -> bool:
|
|
128
|
+
"""Validate API key format (obx_live_... or obx_test_...)."""
|
|
129
|
+
return bool(API_KEY_PATTERN.match(api_key))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_api_key_with_server(api_url: str, api_key: str, timeout: float) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Validate API key by calling /v1/auth/validate endpoint.
|
|
135
|
+
Raises OpenBoxAuthError for invalid key, OpenBoxNetworkError for connectivity issues.
|
|
136
|
+
|
|
137
|
+
NOTE: urllib imports are lazy to avoid Temporal sandbox restrictions.
|
|
138
|
+
urllib.request uses os.stat internally which triggers sandbox errors.
|
|
139
|
+
"""
|
|
140
|
+
# Lazy imports to avoid sandbox restrictions
|
|
141
|
+
from urllib.request import Request, urlopen
|
|
142
|
+
from urllib.error import HTTPError, URLError
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
req = Request(
|
|
146
|
+
f"{api_url}/api/v1/auth/validate",
|
|
147
|
+
headers={
|
|
148
|
+
"Authorization": f"Bearer {api_key}",
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
},
|
|
151
|
+
method="GET",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
with urlopen(req, timeout=timeout) as response:
|
|
155
|
+
status_code = response.getcode()
|
|
156
|
+
if status_code != 200:
|
|
157
|
+
raise OpenBoxAuthError(
|
|
158
|
+
"Invalid API key. Check your API key at dashboard.openbox.ai"
|
|
159
|
+
)
|
|
160
|
+
_get_logger().info("OpenBox API key validated successfully")
|
|
161
|
+
|
|
162
|
+
except HTTPError as e:
|
|
163
|
+
if e.code == 401 or e.code == 403:
|
|
164
|
+
raise OpenBoxAuthError(
|
|
165
|
+
"Invalid API key. Check your API key at dashboard.openbox.ai"
|
|
166
|
+
)
|
|
167
|
+
raise OpenBoxNetworkError(f"Cannot reach OpenBox Core at {api_url}: HTTP {e.code}")
|
|
168
|
+
|
|
169
|
+
except URLError as e:
|
|
170
|
+
raise OpenBoxNetworkError(f"Cannot reach OpenBox Core at {api_url}: {e.reason}")
|
|
171
|
+
|
|
172
|
+
except (OpenBoxAuthError, OpenBoxNetworkError):
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
raise OpenBoxNetworkError(f"Cannot reach OpenBox Core at {api_url}: {e}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class _GlobalConfig:
|
|
180
|
+
"""Global OpenBox configuration singleton."""
|
|
181
|
+
|
|
182
|
+
def __init__(self):
|
|
183
|
+
self.api_url: str = ""
|
|
184
|
+
self.api_key: str = ""
|
|
185
|
+
self.governance_timeout: float = 30.0
|
|
186
|
+
|
|
187
|
+
def configure(
|
|
188
|
+
self,
|
|
189
|
+
api_url: str,
|
|
190
|
+
api_key: str,
|
|
191
|
+
governance_timeout: float = 30.0,
|
|
192
|
+
):
|
|
193
|
+
"""Configure OpenBox settings."""
|
|
194
|
+
self.api_url = api_url.rstrip("/")
|
|
195
|
+
self.api_key = api_key
|
|
196
|
+
self.governance_timeout = governance_timeout
|
|
197
|
+
|
|
198
|
+
def is_configured(self) -> bool:
|
|
199
|
+
"""Check if OpenBox is configured."""
|
|
200
|
+
return bool(self.api_url and self.api_key)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Global singleton
|
|
204
|
+
_config = _GlobalConfig()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_global_config() -> _GlobalConfig:
|
|
208
|
+
"""Get global config singleton."""
|
|
209
|
+
return _config
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def initialize(
|
|
213
|
+
api_url: str,
|
|
214
|
+
api_key: str,
|
|
215
|
+
governance_timeout: float = 30.0,
|
|
216
|
+
validate: bool = True,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Initialize OpenBox SDK.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
api_url: OpenBox Core API endpoint URL
|
|
223
|
+
api_key: API key (format: obx_live_... or obx_test_...)
|
|
224
|
+
governance_timeout: Timeout for governance requests in seconds (default: 30.0)
|
|
225
|
+
validate: Validate API key with server on initialization (default: True)
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
OpenBoxAuthError: Invalid API key
|
|
229
|
+
OpenBoxNetworkError: Cannot reach OpenBox Core
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
from openbox import (
|
|
233
|
+
initialize,
|
|
234
|
+
setup_opentelemetry_for_governance,
|
|
235
|
+
WorkflowSpanProcessor,
|
|
236
|
+
GovernanceInterceptor,
|
|
237
|
+
ActivityGovernanceInterceptor,
|
|
238
|
+
OpenBoxClient,
|
|
239
|
+
GovernanceConfig,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# 1. Initialize SDK
|
|
243
|
+
initialize(api_url="http://localhost:8086", api_key="obx_live_...")
|
|
244
|
+
|
|
245
|
+
# 2. Setup OTel and span processor
|
|
246
|
+
span_processor = WorkflowSpanProcessor()
|
|
247
|
+
setup_opentelemetry_for_governance(span_processor)
|
|
248
|
+
|
|
249
|
+
# 3. Create client and config
|
|
250
|
+
client = OpenBoxClient(api_url="...", api_key="...")
|
|
251
|
+
config = GovernanceConfig()
|
|
252
|
+
|
|
253
|
+
# 4. Create interceptors (BOTH workflow and activity)
|
|
254
|
+
workflow_interceptor = GovernanceInterceptor(client, span_processor, config)
|
|
255
|
+
activity_interceptor = ActivityGovernanceInterceptor(api_url, api_key, span_processor, config)
|
|
256
|
+
|
|
257
|
+
# 5. Add to Temporal worker
|
|
258
|
+
worker = Worker(..., interceptors=[workflow_interceptor, activity_interceptor])
|
|
259
|
+
"""
|
|
260
|
+
# Validate API key format
|
|
261
|
+
if not _validate_api_key_format(api_key):
|
|
262
|
+
_get_logger().warning("API key format may be invalid. Expected: obx_live_... or obx_test_...")
|
|
263
|
+
|
|
264
|
+
_config.configure(
|
|
265
|
+
api_url=api_url,
|
|
266
|
+
api_key=api_key,
|
|
267
|
+
governance_timeout=governance_timeout,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Validate API key with server
|
|
271
|
+
if validate:
|
|
272
|
+
_validate_api_key_with_server(api_url.rstrip("/"), api_key, governance_timeout)
|
|
273
|
+
|
|
274
|
+
_get_logger().info(f"OpenBox SDK initialized with API URL: {api_url}")
|