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/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}")