openbox-temporal-sdk-python 1.0.1__tar.gz → 1.0.2__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.
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/.github/workflows/publish.yml +2 -3
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/.github/workflows/sonarqube.yaml +1 -1
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/PKG-INFO +20 -4
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/README.md +19 -3
- openbox_temporal_sdk_python-1.0.2/docs/configuration.md +63 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/otel_setup.py +42 -4
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/worker.py +6 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/pyproject.toml +1 -1
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_otel_setup.py +120 -3
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_worker.py +73 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/.gitignore +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/.python-version +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/.repomixignore +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/LICENSE +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/code-standards.md +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/codebase-summary.md +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/project-overview-pdr.md +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/system-architecture.md +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/__init__.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/activities.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/activity_interceptor.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/config.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/py.typed +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/span_processor.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/tracing.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/types.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/workflow_interceptor.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/release-manifest.json +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/repomix-output.xml +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/sonar-project.properties +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/__init__.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_activities.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_activity_interceptor.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_config.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_span_processor.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_tracing.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_types.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_workflow_interceptor.py +0 -0
- {openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/uv.lock +0 -0
|
@@ -117,7 +117,7 @@ jobs:
|
|
|
117
117
|
cat qg.md >> "$GITHUB_STEP_SUMMARY"
|
|
118
118
|
echo "STATUS=${STATUS}" >> "$GITHUB_ENV"
|
|
119
119
|
echo "QG_NAME=${QG_NAME}" >> "$GITHUB_ENV"
|
|
120
|
-
echo "PNAME=${PNAME}" >> "$GITHUB_ENV"
|
|
120
|
+
echo "PNAME=${PNAME}" >> "$GITHUB_ENV"
|
|
121
121
|
|
|
122
122
|
# (2) Upsert Issue cho
|
|
123
123
|
- name: Comment (or create) Issue with QG report
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbox-temporal-sdk-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: OpenBox SDK - Governance and observability for Temporal workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenBox-AI/temporal-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/OpenBox-AI/temporal-sdk-python#readme
|
|
@@ -140,7 +140,8 @@ worker = create_openbox_worker(
|
|
|
140
140
|
|
|
141
141
|
# Database instrumentation
|
|
142
142
|
instrument_databases=True,
|
|
143
|
-
db_libraries={"psycopg2", "
|
|
143
|
+
db_libraries={"psycopg2", "sqlalchemy"}, # None = all available
|
|
144
|
+
sqlalchemy_engine=engine, # pass pre-existing engine for query capture
|
|
144
145
|
|
|
145
146
|
# File I/O instrumentation
|
|
146
147
|
instrument_file_io=False, # disabled by default
|
|
@@ -238,6 +239,18 @@ Configure error policy via `on_api_error`:
|
|
|
238
239
|
- Redis: `redis`
|
|
239
240
|
- ORM: `sqlalchemy`
|
|
240
241
|
|
|
242
|
+
**SQLAlchemy Note:** If your SQLAlchemy engine is created before `create_openbox_worker()` runs (e.g., at module import time), you must pass it via the `sqlalchemy_engine` parameter. Without this, `SQLAlchemyInstrumentor` only patches future `create_engine()` calls and won't capture queries on pre-existing engines.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from db.engine import engine
|
|
246
|
+
|
|
247
|
+
worker = create_openbox_worker(
|
|
248
|
+
...,
|
|
249
|
+
db_libraries={"psycopg2", "sqlalchemy"},
|
|
250
|
+
sqlalchemy_engine=engine,
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
241
254
|
### File I/O
|
|
242
255
|
- `open()`, `read()`, `write()`, `readline()`, `readlines()`
|
|
243
256
|
- Skips system paths (`/dev/`, `/proc/`, `/sys/`, `__pycache__`)
|
|
@@ -284,7 +297,10 @@ span_processor = WorkflowSpanProcessor(
|
|
|
284
297
|
)
|
|
285
298
|
|
|
286
299
|
# 3. Setup OTel instrumentation
|
|
287
|
-
setup_opentelemetry_for_governance(
|
|
300
|
+
setup_opentelemetry_for_governance(
|
|
301
|
+
span_processor,
|
|
302
|
+
sqlalchemy_engine=engine, # optional: instrument pre-existing engine
|
|
303
|
+
)
|
|
288
304
|
|
|
289
305
|
# 4. Create governance config
|
|
290
306
|
config = GovernanceConfig(
|
|
@@ -355,4 +371,4 @@ MIT License - See LICENSE file for details
|
|
|
355
371
|
|
|
356
372
|
---
|
|
357
373
|
|
|
358
|
-
**Version:** 1.0.
|
|
374
|
+
**Version:** 1.0.2 | **Last Updated:** 2026-02-12
|
|
@@ -91,7 +91,8 @@ worker = create_openbox_worker(
|
|
|
91
91
|
|
|
92
92
|
# Database instrumentation
|
|
93
93
|
instrument_databases=True,
|
|
94
|
-
db_libraries={"psycopg2", "
|
|
94
|
+
db_libraries={"psycopg2", "sqlalchemy"}, # None = all available
|
|
95
|
+
sqlalchemy_engine=engine, # pass pre-existing engine for query capture
|
|
95
96
|
|
|
96
97
|
# File I/O instrumentation
|
|
97
98
|
instrument_file_io=False, # disabled by default
|
|
@@ -189,6 +190,18 @@ Configure error policy via `on_api_error`:
|
|
|
189
190
|
- Redis: `redis`
|
|
190
191
|
- ORM: `sqlalchemy`
|
|
191
192
|
|
|
193
|
+
**SQLAlchemy Note:** If your SQLAlchemy engine is created before `create_openbox_worker()` runs (e.g., at module import time), you must pass it via the `sqlalchemy_engine` parameter. Without this, `SQLAlchemyInstrumentor` only patches future `create_engine()` calls and won't capture queries on pre-existing engines.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from db.engine import engine
|
|
197
|
+
|
|
198
|
+
worker = create_openbox_worker(
|
|
199
|
+
...,
|
|
200
|
+
db_libraries={"psycopg2", "sqlalchemy"},
|
|
201
|
+
sqlalchemy_engine=engine,
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
192
205
|
### File I/O
|
|
193
206
|
- `open()`, `read()`, `write()`, `readline()`, `readlines()`
|
|
194
207
|
- Skips system paths (`/dev/`, `/proc/`, `/sys/`, `__pycache__`)
|
|
@@ -235,7 +248,10 @@ span_processor = WorkflowSpanProcessor(
|
|
|
235
248
|
)
|
|
236
249
|
|
|
237
250
|
# 3. Setup OTel instrumentation
|
|
238
|
-
setup_opentelemetry_for_governance(
|
|
251
|
+
setup_opentelemetry_for_governance(
|
|
252
|
+
span_processor,
|
|
253
|
+
sqlalchemy_engine=engine, # optional: instrument pre-existing engine
|
|
254
|
+
)
|
|
239
255
|
|
|
240
256
|
# 4. Create governance config
|
|
241
257
|
config = GovernanceConfig(
|
|
@@ -306,4 +322,4 @@ MIT License - See LICENSE file for details
|
|
|
306
322
|
|
|
307
323
|
---
|
|
308
324
|
|
|
309
|
-
**Version:** 1.0.
|
|
325
|
+
**Version:** 1.0.2 | **Last Updated:** 2026-02-12
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# OpenBox SDK Configuration
|
|
2
|
+
|
|
3
|
+
## Required
|
|
4
|
+
|
|
5
|
+
| Parameter | Type | Description |
|
|
6
|
+
|-----------|------|-------------|
|
|
7
|
+
| `openbox_url` | `str` | OpenBox Core API URL (HTTPS required for non-localhost) |
|
|
8
|
+
| `openbox_api_key` | `str` | API key (`obx_live_*` or `obx_test_*`) |
|
|
9
|
+
|
|
10
|
+
## Governance
|
|
11
|
+
|
|
12
|
+
| Parameter | Type | Default | Description |
|
|
13
|
+
|-----------|------|---------|-------------|
|
|
14
|
+
| `governance_timeout` | `float` | `30.0` | API timeout in seconds |
|
|
15
|
+
| `governance_policy` | `str` | `"fail_open"` | `"fail_open"` or `"fail_closed"` |
|
|
16
|
+
|
|
17
|
+
## Event Filtering
|
|
18
|
+
|
|
19
|
+
| Parameter | Type | Default | Description |
|
|
20
|
+
|-----------|------|---------|-------------|
|
|
21
|
+
| `send_start_event` | `bool` | `True` | Send WorkflowStarted events |
|
|
22
|
+
| `send_activity_start_event` | `bool` | `True` | Send ActivityStarted events |
|
|
23
|
+
| `skip_workflow_types` | `set` | `None` | Workflow types to skip |
|
|
24
|
+
| `skip_activity_types` | `set` | `None` | Activity types to skip |
|
|
25
|
+
| `skip_signals` | `set` | `None` | Signal names to skip |
|
|
26
|
+
|
|
27
|
+
## Human-in-the-Loop
|
|
28
|
+
|
|
29
|
+
| Parameter | Type | Default | Description |
|
|
30
|
+
|-----------|------|---------|-------------|
|
|
31
|
+
| `hitl_enabled` | `bool` | `True` | Enable approval polling for `REQUIRE_APPROVAL` |
|
|
32
|
+
|
|
33
|
+
## Instrumentation
|
|
34
|
+
|
|
35
|
+
| Parameter | Type | Default | Description |
|
|
36
|
+
|-----------|------|---------|-------------|
|
|
37
|
+
| `instrument_databases` | `bool` | `True` | Capture database queries |
|
|
38
|
+
| `db_libraries` | `set` | `None` | `"psycopg2"`, `"asyncpg"`, `"mysql"`, `"pymysql"`, `"pymongo"`, `"redis"`, `"sqlalchemy"` |
|
|
39
|
+
| `instrument_file_io` | `bool` | `False` | Capture file operations |
|
|
40
|
+
|
|
41
|
+
## Example
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
worker = create_openbox_worker(
|
|
45
|
+
client=client,
|
|
46
|
+
task_queue="my-queue",
|
|
47
|
+
workflows=[MyWorkflow],
|
|
48
|
+
activities=[my_activity],
|
|
49
|
+
|
|
50
|
+
# Required
|
|
51
|
+
openbox_url=os.getenv("OPENBOX_URL"),
|
|
52
|
+
openbox_api_key=os.getenv("OPENBOX_API_KEY"),
|
|
53
|
+
|
|
54
|
+
# Optional
|
|
55
|
+
governance_policy="fail_closed",
|
|
56
|
+
governance_timeout=15.0,
|
|
57
|
+
hitl_enabled=True,
|
|
58
|
+
skip_workflow_types={"InternalWorkflow"},
|
|
59
|
+
instrument_databases=True,
|
|
60
|
+
db_libraries={"psycopg2", "redis"},
|
|
61
|
+
instrument_file_io=False,
|
|
62
|
+
)
|
|
63
|
+
```
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/otel_setup.py
RENAMED
|
@@ -22,7 +22,7 @@ Supported database libraries:
|
|
|
22
22
|
- sqlalchemy (ORM)
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
from typing import TYPE_CHECKING, Optional, Set, List
|
|
25
|
+
from typing import TYPE_CHECKING, Any, Optional, Set, List
|
|
26
26
|
import logging
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
@@ -70,6 +70,7 @@ def setup_opentelemetry_for_governance(
|
|
|
70
70
|
instrument_databases: bool = True,
|
|
71
71
|
db_libraries: Optional[Set[str]] = None,
|
|
72
72
|
instrument_file_io: bool = False,
|
|
73
|
+
sqlalchemy_engine: Optional[Any] = None,
|
|
73
74
|
) -> None:
|
|
74
75
|
"""
|
|
75
76
|
Setup OpenTelemetry instrumentors with body capture hooks.
|
|
@@ -87,6 +88,10 @@ def setup_opentelemetry_for_governance(
|
|
|
87
88
|
Valid values: "psycopg2", "asyncpg", "mysql", "pymysql",
|
|
88
89
|
"pymongo", "redis", "sqlalchemy"
|
|
89
90
|
instrument_file_io: Whether to instrument file I/O operations (default: False)
|
|
91
|
+
sqlalchemy_engine: Optional SQLAlchemy Engine instance to instrument. Required
|
|
92
|
+
when the engine is created before instrumentation runs (e.g.,
|
|
93
|
+
at module import time). If not provided, only future engines
|
|
94
|
+
created via create_engine() will be instrumented.
|
|
90
95
|
"""
|
|
91
96
|
global _span_processor, _ignored_url_prefixes
|
|
92
97
|
_span_processor = span_processor
|
|
@@ -172,8 +177,13 @@ def setup_opentelemetry_for_governance(
|
|
|
172
177
|
logger.info(f"OpenTelemetry HTTP instrumentation complete. Instrumented: {instrumented}")
|
|
173
178
|
|
|
174
179
|
# 6. Database instrumentation (optional)
|
|
180
|
+
if sqlalchemy_engine is not None and not instrument_databases:
|
|
181
|
+
logger.warning(
|
|
182
|
+
"sqlalchemy_engine was provided but instrument_databases=False; "
|
|
183
|
+
"engine will not be instrumented"
|
|
184
|
+
)
|
|
175
185
|
if instrument_databases:
|
|
176
|
-
db_instrumented = setup_database_instrumentation(db_libraries)
|
|
186
|
+
db_instrumented = setup_database_instrumentation(db_libraries, sqlalchemy_engine)
|
|
177
187
|
if db_instrumented:
|
|
178
188
|
instrumented.extend(db_instrumented)
|
|
179
189
|
|
|
@@ -334,6 +344,7 @@ def uninstrument_file_io() -> None:
|
|
|
334
344
|
|
|
335
345
|
def setup_database_instrumentation(
|
|
336
346
|
db_libraries: Optional[Set[str]] = None,
|
|
347
|
+
sqlalchemy_engine: Optional[Any] = None,
|
|
337
348
|
) -> List[str]:
|
|
338
349
|
"""
|
|
339
350
|
Setup OpenTelemetry database instrumentors.
|
|
@@ -351,6 +362,10 @@ def setup_database_instrumentation(
|
|
|
351
362
|
- "pymongo" (MongoDB)
|
|
352
363
|
- "redis"
|
|
353
364
|
- "sqlalchemy" (ORM)
|
|
365
|
+
sqlalchemy_engine: Optional SQLAlchemy Engine instance to instrument. When
|
|
366
|
+
provided, registers event listeners on this engine to capture
|
|
367
|
+
queries. Without this, only engines created after this call
|
|
368
|
+
(via patched create_engine) will be instrumented.
|
|
354
369
|
|
|
355
370
|
Returns:
|
|
356
371
|
List of successfully instrumented library names
|
|
@@ -424,13 +439,36 @@ def setup_database_instrumentation(
|
|
|
424
439
|
logger.debug("redis instrumentation not available")
|
|
425
440
|
|
|
426
441
|
# sqlalchemy (ORM)
|
|
442
|
+
if sqlalchemy_engine is not None and db_libraries is not None and "sqlalchemy" not in db_libraries:
|
|
443
|
+
logger.warning(
|
|
444
|
+
"sqlalchemy_engine was provided but 'sqlalchemy' is not in db_libraries; "
|
|
445
|
+
"engine will not be instrumented"
|
|
446
|
+
)
|
|
427
447
|
if db_libraries is None or "sqlalchemy" in db_libraries:
|
|
428
448
|
try:
|
|
429
449
|
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
|
430
450
|
|
|
431
|
-
|
|
451
|
+
if sqlalchemy_engine is not None:
|
|
452
|
+
# Validate engine type before passing to instrumentor
|
|
453
|
+
try:
|
|
454
|
+
from sqlalchemy.engine import Engine as _SAEngine
|
|
455
|
+
except ImportError:
|
|
456
|
+
raise TypeError(
|
|
457
|
+
"sqlalchemy_engine was provided but sqlalchemy is not installed"
|
|
458
|
+
)
|
|
459
|
+
if not isinstance(sqlalchemy_engine, _SAEngine):
|
|
460
|
+
raise TypeError(
|
|
461
|
+
f"sqlalchemy_engine must be a sqlalchemy.engine.Engine instance, "
|
|
462
|
+
f"got {type(sqlalchemy_engine).__name__}"
|
|
463
|
+
)
|
|
464
|
+
# Instrument the existing engine directly (registers event listeners)
|
|
465
|
+
SQLAlchemyInstrumentor().instrument(engine=sqlalchemy_engine)
|
|
466
|
+
logger.info("Instrumented: sqlalchemy (existing engine)")
|
|
467
|
+
else:
|
|
468
|
+
# Patch create_engine() for future engines only
|
|
469
|
+
SQLAlchemyInstrumentor().instrument()
|
|
470
|
+
logger.info("Instrumented: sqlalchemy (future engines)")
|
|
432
471
|
instrumented.append("sqlalchemy")
|
|
433
|
-
logger.info("Instrumented: sqlalchemy")
|
|
434
472
|
except ImportError:
|
|
435
473
|
logger.debug("sqlalchemy instrumentation not available")
|
|
436
474
|
|
|
@@ -57,6 +57,7 @@ def create_openbox_worker(
|
|
|
57
57
|
# Database instrumentation
|
|
58
58
|
instrument_databases: bool = True,
|
|
59
59
|
db_libraries: Optional[set] = None,
|
|
60
|
+
sqlalchemy_engine: Optional[Any] = None,
|
|
60
61
|
# File I/O instrumentation
|
|
61
62
|
instrument_file_io: bool = False,
|
|
62
63
|
# Standard Worker options
|
|
@@ -117,6 +118,10 @@ def create_openbox_worker(
|
|
|
117
118
|
db_libraries: Set of database libraries to instrument (None = all available).
|
|
118
119
|
Valid values: "psycopg2", "asyncpg", "mysql", "pymysql",
|
|
119
120
|
"pymongo", "redis", "sqlalchemy"
|
|
121
|
+
sqlalchemy_engine: SQLAlchemy Engine instance to instrument. Pass this when
|
|
122
|
+
the engine is created before create_openbox_worker() runs
|
|
123
|
+
(e.g., at module import time). This ensures query-level
|
|
124
|
+
instrumentation works on pre-existing engines.
|
|
120
125
|
|
|
121
126
|
# File I/O instrumentation
|
|
122
127
|
instrument_file_io: Instrument file I/O operations (default: False)
|
|
@@ -171,6 +176,7 @@ def create_openbox_worker(
|
|
|
171
176
|
instrument_databases=instrument_databases,
|
|
172
177
|
db_libraries=db_libraries,
|
|
173
178
|
instrument_file_io=instrument_file_io,
|
|
179
|
+
sqlalchemy_engine=sqlalchemy_engine,
|
|
174
180
|
)
|
|
175
181
|
|
|
176
182
|
# 4. Create governance config
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_otel_setup.py
RENAMED
|
@@ -462,7 +462,7 @@ class TestSetupOpentelemetryForGovernance:
|
|
|
462
462
|
instrument_file_io=False,
|
|
463
463
|
)
|
|
464
464
|
|
|
465
|
-
mock_db_setup.assert_called_once_with({"psycopg2"})
|
|
465
|
+
mock_db_setup.assert_called_once_with({"psycopg2"}, None)
|
|
466
466
|
|
|
467
467
|
def test_calls_setup_file_io_instrumentation_when_enabled(self, mock_span_processor):
|
|
468
468
|
"""Should call setup_file_io_instrumentation when instrument_file_io=True."""
|
|
@@ -868,7 +868,7 @@ class TestSetupDatabaseInstrumentation:
|
|
|
868
868
|
assert "redis" in result
|
|
869
869
|
|
|
870
870
|
def test_instruments_sqlalchemy(self):
|
|
871
|
-
"""Should instrument sqlalchemy library."""
|
|
871
|
+
"""Should instrument sqlalchemy library (future engines path)."""
|
|
872
872
|
from openbox.otel_setup import setup_database_instrumentation
|
|
873
873
|
|
|
874
874
|
with patch('opentelemetry.instrumentation.sqlalchemy.SQLAlchemyInstrumentor') as mock_sqlalchemy:
|
|
@@ -877,9 +877,126 @@ class TestSetupDatabaseInstrumentation:
|
|
|
877
877
|
|
|
878
878
|
result = setup_database_instrumentation(db_libraries={"sqlalchemy"})
|
|
879
879
|
|
|
880
|
-
mock_instance.instrument.
|
|
880
|
+
mock_instance.instrument.assert_called_once_with()
|
|
881
881
|
assert "sqlalchemy" in result
|
|
882
882
|
|
|
883
|
+
def test_instruments_sqlalchemy_with_existing_engine(self):
|
|
884
|
+
"""Should instrument sqlalchemy with existing engine when provided."""
|
|
885
|
+
from openbox.otel_setup import setup_database_instrumentation
|
|
886
|
+
|
|
887
|
+
with patch('opentelemetry.instrumentation.sqlalchemy.SQLAlchemyInstrumentor') as mock_sqlalchemy:
|
|
888
|
+
mock_instance = MagicMock()
|
|
889
|
+
mock_sqlalchemy.return_value = mock_instance
|
|
890
|
+
|
|
891
|
+
# Create a mock that passes the isinstance check
|
|
892
|
+
with patch('openbox.otel_setup.setup_database_instrumentation') as _:
|
|
893
|
+
pass # just to show we need a real-ish engine
|
|
894
|
+
|
|
895
|
+
# Use a mock Engine that passes isinstance check
|
|
896
|
+
from unittest.mock import create_autospec
|
|
897
|
+
try:
|
|
898
|
+
from sqlalchemy.engine import Engine
|
|
899
|
+
mock_engine = create_autospec(Engine, instance=True)
|
|
900
|
+
except ImportError:
|
|
901
|
+
pytest.skip("sqlalchemy not installed")
|
|
902
|
+
|
|
903
|
+
result = setup_database_instrumentation(
|
|
904
|
+
db_libraries={"sqlalchemy"},
|
|
905
|
+
sqlalchemy_engine=mock_engine,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
mock_instance.instrument.assert_called_once_with(engine=mock_engine)
|
|
909
|
+
assert "sqlalchemy" in result
|
|
910
|
+
|
|
911
|
+
def test_sqlalchemy_engine_rejects_non_engine_type(self):
|
|
912
|
+
"""Should raise TypeError when sqlalchemy_engine is not an Engine instance."""
|
|
913
|
+
from openbox.otel_setup import setup_database_instrumentation
|
|
914
|
+
|
|
915
|
+
try:
|
|
916
|
+
import sqlalchemy # noqa
|
|
917
|
+
except ImportError:
|
|
918
|
+
pytest.skip("sqlalchemy not installed")
|
|
919
|
+
|
|
920
|
+
with patch('opentelemetry.instrumentation.sqlalchemy.SQLAlchemyInstrumentor'):
|
|
921
|
+
with pytest.raises(TypeError, match="must be a sqlalchemy.engine.Engine instance"):
|
|
922
|
+
setup_database_instrumentation(
|
|
923
|
+
db_libraries={"sqlalchemy"},
|
|
924
|
+
sqlalchemy_engine="not-an-engine",
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
def test_sqlalchemy_engine_rejects_when_sqlalchemy_not_installed(self):
|
|
928
|
+
"""Should raise TypeError when engine provided but sqlalchemy not installed."""
|
|
929
|
+
from openbox.otel_setup import setup_database_instrumentation
|
|
930
|
+
|
|
931
|
+
with patch('opentelemetry.instrumentation.sqlalchemy.SQLAlchemyInstrumentor'):
|
|
932
|
+
with patch.dict('sys.modules', {'sqlalchemy': None, 'sqlalchemy.engine': None}):
|
|
933
|
+
with pytest.raises(TypeError, match="sqlalchemy is not installed"):
|
|
934
|
+
setup_database_instrumentation(
|
|
935
|
+
db_libraries={"sqlalchemy"},
|
|
936
|
+
sqlalchemy_engine=MagicMock(),
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
def test_warns_when_engine_provided_but_sqlalchemy_not_in_db_libraries(self):
|
|
940
|
+
"""Should warn when sqlalchemy_engine provided but 'sqlalchemy' not in db_libraries."""
|
|
941
|
+
from openbox.otel_setup import setup_database_instrumentation
|
|
942
|
+
import logging
|
|
943
|
+
|
|
944
|
+
with patch('opentelemetry.instrumentation.psycopg2.Psycopg2Instrumentor') as mock_psycopg2:
|
|
945
|
+
mock_psycopg2.return_value = MagicMock()
|
|
946
|
+
|
|
947
|
+
with patch.object(logging.getLogger('openbox.otel_setup'), 'warning') as mock_warn:
|
|
948
|
+
setup_database_instrumentation(
|
|
949
|
+
db_libraries={"psycopg2"},
|
|
950
|
+
sqlalchemy_engine=MagicMock(),
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
mock_warn.assert_called_once()
|
|
954
|
+
assert "not in db_libraries" in mock_warn.call_args[0][0]
|
|
955
|
+
|
|
956
|
+
def test_warns_when_engine_provided_but_databases_disabled(self, mock_span_processor):
|
|
957
|
+
"""Should warn when sqlalchemy_engine provided but instrument_databases=False."""
|
|
958
|
+
import openbox.otel_setup as otel_setup
|
|
959
|
+
import logging
|
|
960
|
+
|
|
961
|
+
with patch.object(otel_setup, 'setup_httpx_body_capture'):
|
|
962
|
+
with patch('opentelemetry.trace.get_tracer_provider') as mock_get_provider:
|
|
963
|
+
mock_provider = MagicMock()
|
|
964
|
+
mock_get_provider.return_value = mock_provider
|
|
965
|
+
|
|
966
|
+
with patch.object(logging.getLogger('openbox.otel_setup'), 'warning') as mock_warn:
|
|
967
|
+
otel_setup.setup_opentelemetry_for_governance(
|
|
968
|
+
span_processor=mock_span_processor,
|
|
969
|
+
instrument_databases=False,
|
|
970
|
+
instrument_file_io=False,
|
|
971
|
+
sqlalchemy_engine=MagicMock(),
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
mock_warn.assert_called_once()
|
|
975
|
+
assert "instrument_databases=False" in mock_warn.call_args[0][0]
|
|
976
|
+
|
|
977
|
+
def test_sqlalchemy_engine_passthrough_from_setup_governance(self, mock_span_processor):
|
|
978
|
+
"""Should pass sqlalchemy_engine through to setup_database_instrumentation."""
|
|
979
|
+
import openbox.otel_setup as otel_setup
|
|
980
|
+
|
|
981
|
+
mock_engine = MagicMock()
|
|
982
|
+
|
|
983
|
+
with patch.object(otel_setup, 'setup_httpx_body_capture'):
|
|
984
|
+
with patch.object(otel_setup, 'setup_database_instrumentation') as mock_db_setup:
|
|
985
|
+
mock_db_setup.return_value = ["sqlalchemy"]
|
|
986
|
+
with patch('opentelemetry.trace.get_tracer_provider') as mock_get_provider:
|
|
987
|
+
mock_provider = MagicMock()
|
|
988
|
+
mock_get_provider.return_value = mock_provider
|
|
989
|
+
|
|
990
|
+
otel_setup.setup_opentelemetry_for_governance(
|
|
991
|
+
span_processor=mock_span_processor,
|
|
992
|
+
instrument_databases=True,
|
|
993
|
+
db_libraries={"sqlalchemy"},
|
|
994
|
+
instrument_file_io=False,
|
|
995
|
+
sqlalchemy_engine=mock_engine,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
mock_db_setup.assert_called_once_with({"sqlalchemy"}, mock_engine)
|
|
999
|
+
|
|
883
1000
|
|
|
884
1001
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
885
1002
|
# Tests for uninstrument_all()
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_worker.py
RENAMED
|
@@ -135,6 +135,7 @@ class TestCreateOpenboxWorkerWithConfig:
|
|
|
135
135
|
instrument_databases=True,
|
|
136
136
|
db_libraries={"psycopg2", "redis"},
|
|
137
137
|
instrument_file_io=True,
|
|
138
|
+
sqlalchemy_engine=None,
|
|
138
139
|
)
|
|
139
140
|
|
|
140
141
|
@patch("openbox.worker.Worker")
|
|
@@ -1380,6 +1381,78 @@ class TestConfigurationOptions:
|
|
|
1380
1381
|
call_kwargs = mock_setup_otel.call_args[1]
|
|
1381
1382
|
assert call_kwargs["instrument_file_io"] is True
|
|
1382
1383
|
|
|
1384
|
+
@patch("openbox.worker.Worker")
|
|
1385
|
+
@patch("openbox.worker.validate_api_key")
|
|
1386
|
+
@patch("openbox.worker.WorkflowSpanProcessor")
|
|
1387
|
+
@patch("openbox.worker.GovernanceConfig")
|
|
1388
|
+
@patch("openbox.otel_setup.setup_opentelemetry_for_governance")
|
|
1389
|
+
@patch("openbox.workflow_interceptor.GovernanceInterceptor")
|
|
1390
|
+
@patch("openbox.activity_interceptor.ActivityGovernanceInterceptor")
|
|
1391
|
+
@patch("openbox.activities.send_governance_event")
|
|
1392
|
+
def test_sqlalchemy_engine_passed_to_setup(
|
|
1393
|
+
self,
|
|
1394
|
+
mock_send_governance_event,
|
|
1395
|
+
mock_activity_interceptor,
|
|
1396
|
+
mock_governance_interceptor,
|
|
1397
|
+
mock_setup_otel,
|
|
1398
|
+
mock_governance_config,
|
|
1399
|
+
mock_span_processor_class,
|
|
1400
|
+
mock_validate_api_key,
|
|
1401
|
+
mock_worker_class,
|
|
1402
|
+
):
|
|
1403
|
+
"""Test sqlalchemy_engine is passed to setup_opentelemetry_for_governance."""
|
|
1404
|
+
from openbox.worker import create_openbox_worker
|
|
1405
|
+
|
|
1406
|
+
mock_client = Mock()
|
|
1407
|
+
mock_engine = Mock()
|
|
1408
|
+
|
|
1409
|
+
create_openbox_worker(
|
|
1410
|
+
client=mock_client,
|
|
1411
|
+
task_queue="test-queue",
|
|
1412
|
+
openbox_url="http://localhost:8086",
|
|
1413
|
+
openbox_api_key="obx_test_key123",
|
|
1414
|
+
sqlalchemy_engine=mock_engine,
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
mock_setup_otel.assert_called_once()
|
|
1418
|
+
call_kwargs = mock_setup_otel.call_args[1]
|
|
1419
|
+
assert call_kwargs["sqlalchemy_engine"] is mock_engine
|
|
1420
|
+
|
|
1421
|
+
@patch("openbox.worker.Worker")
|
|
1422
|
+
@patch("openbox.worker.validate_api_key")
|
|
1423
|
+
@patch("openbox.worker.WorkflowSpanProcessor")
|
|
1424
|
+
@patch("openbox.worker.GovernanceConfig")
|
|
1425
|
+
@patch("openbox.otel_setup.setup_opentelemetry_for_governance")
|
|
1426
|
+
@patch("openbox.workflow_interceptor.GovernanceInterceptor")
|
|
1427
|
+
@patch("openbox.activity_interceptor.ActivityGovernanceInterceptor")
|
|
1428
|
+
@patch("openbox.activities.send_governance_event")
|
|
1429
|
+
def test_sqlalchemy_engine_defaults_to_none(
|
|
1430
|
+
self,
|
|
1431
|
+
mock_send_governance_event,
|
|
1432
|
+
mock_activity_interceptor,
|
|
1433
|
+
mock_governance_interceptor,
|
|
1434
|
+
mock_setup_otel,
|
|
1435
|
+
mock_governance_config,
|
|
1436
|
+
mock_span_processor_class,
|
|
1437
|
+
mock_validate_api_key,
|
|
1438
|
+
mock_worker_class,
|
|
1439
|
+
):
|
|
1440
|
+
"""Test sqlalchemy_engine defaults to None when not provided."""
|
|
1441
|
+
from openbox.worker import create_openbox_worker
|
|
1442
|
+
|
|
1443
|
+
mock_client = Mock()
|
|
1444
|
+
|
|
1445
|
+
create_openbox_worker(
|
|
1446
|
+
client=mock_client,
|
|
1447
|
+
task_queue="test-queue",
|
|
1448
|
+
openbox_url="http://localhost:8086",
|
|
1449
|
+
openbox_api_key="obx_test_key123",
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
mock_setup_otel.assert_called_once()
|
|
1453
|
+
call_kwargs = mock_setup_otel.call_args[1]
|
|
1454
|
+
assert call_kwargs["sqlalchemy_engine"] is None
|
|
1455
|
+
|
|
1383
1456
|
|
|
1384
1457
|
# ===============================================================================
|
|
1385
1458
|
# Print Output Tests
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/code-standards.md
RENAMED
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/codebase-summary.md
RENAMED
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/project-overview-pdr.md
RENAMED
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/docs/system-architecture.md
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/activities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/openbox/span_processor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/release-manifest.json
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/sonar-project.properties
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_activities.py
RENAMED
|
File without changes
|
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_config.py
RENAMED
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_span_processor.py
RENAMED
|
File without changes
|
{openbox_temporal_sdk_python-1.0.1 → openbox_temporal_sdk_python-1.0.2}/tests/test_tracing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|