dbos 1.5.0a5__tar.gz → 1.5.0a10__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.
- {dbos-1.5.0a5 → dbos-1.5.0a10}/PKG-INFO +1 -1
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_admin_server.py +1 -1
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_core.py +1 -1
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_dbos_config.py +5 -2
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_error.py +5 -2
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_logger.py +5 -2
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_outcome.py +11 -7
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_tracer.py +5 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/cli/cli.py +31 -2
- {dbos-1.5.0a5 → dbos-1.5.0a10}/pyproject.toml +1 -1
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_admin_server.py +14 -5
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_config.py +21 -10
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_failures.py +7 -1
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_outcome.py +2 -2
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_queue.py +54 -1
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_spans.py +7 -2
- {dbos-1.5.0a5 → dbos-1.5.0a10}/LICENSE +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/README.md +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/__init__.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/__main__.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_app_db.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_classproperty.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_client.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_conductor/conductor.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_conductor/protocol.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_context.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_croniter.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_dbos.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_debug.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_docker_pg_helper.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_event_loop.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_fastapi.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_flask.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_kafka.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_kafka_message.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/env.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/script.py.mako +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_queue.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_recovery.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_registrations.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_roles.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_scheduler.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_schemas/__init__.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_schemas/application_database.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_schemas/system_database.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_serialization.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_sys_db.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_utils.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_workflow_commands.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/cli/_github_init.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/cli/_template_init.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/dbos-config.schema.json +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/py.typed +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/__init__.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/atexit_no_ctor.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/atexit_no_launch.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/classdefs.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/client_collateral.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/client_worker.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/conftest.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/dupname_classdefs1.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/dupname_classdefsa.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/more_classdefs.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/queuedworkflow.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_async.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_classdecorators.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_cli.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_client.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_concurrency.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_croniter.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_dbos.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_debug.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_docker_secrets.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_fastapi.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_fastapi_roles.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_flask.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_kafka.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_package.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_scheduler.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_schema_migration.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_singleton.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_sqlalchemy.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_workflow_introspection.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/tests/test_workflow_management.py +0 -0
- {dbos-1.5.0a5 → dbos-1.5.0a10}/version/__init__.py +0 -0
@@ -5,7 +5,7 @@ import re
|
|
5
5
|
import threading
|
6
6
|
from functools import partial
|
7
7
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
8
|
-
from typing import TYPE_CHECKING, Any, List, Optional, TypedDict
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypedDict
|
9
9
|
|
10
10
|
from dbos._workflow_commands import garbage_collect, global_timeout
|
11
11
|
|
@@ -1123,7 +1123,7 @@ def decorate_step(
|
|
1123
1123
|
stepOutcome = stepOutcome.retry(
|
1124
1124
|
max_attempts,
|
1125
1125
|
on_exception,
|
1126
|
-
lambda i: DBOSMaxStepRetriesExceeded(func.__name__, i),
|
1126
|
+
lambda i, e: DBOSMaxStepRetriesExceeded(func.__name__, i, e),
|
1127
1127
|
)
|
1128
1128
|
|
1129
1129
|
outcome = (
|
@@ -31,6 +31,7 @@ class DBOSConfig(TypedDict, total=False):
|
|
31
31
|
otlp_logs_endpoints: List[str]: OTLP logs endpoints
|
32
32
|
admin_port (int): Admin port
|
33
33
|
run_admin_server (bool): Whether to run the DBOS admin server
|
34
|
+
otlp_attributes (dict[str, str]): A set of custom attributes to apply OTLP-exported logs and traces
|
34
35
|
"""
|
35
36
|
|
36
37
|
name: str
|
@@ -43,6 +44,7 @@ class DBOSConfig(TypedDict, total=False):
|
|
43
44
|
otlp_logs_endpoints: Optional[List[str]]
|
44
45
|
admin_port: Optional[int]
|
45
46
|
run_admin_server: Optional[bool]
|
47
|
+
otlp_attributes: Optional[dict[str, str]]
|
46
48
|
|
47
49
|
|
48
50
|
class RuntimeConfig(TypedDict, total=False):
|
@@ -84,6 +86,7 @@ class LoggerConfig(TypedDict, total=False):
|
|
84
86
|
class TelemetryConfig(TypedDict, total=False):
|
85
87
|
logs: Optional[LoggerConfig]
|
86
88
|
OTLPExporter: Optional[OTLPExporterConfig]
|
89
|
+
otlp_attributes: Optional[dict[str, str]]
|
87
90
|
|
88
91
|
|
89
92
|
class ConfigFile(TypedDict, total=False):
|
@@ -145,7 +148,8 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
|
|
145
148
|
|
146
149
|
# Telemetry config
|
147
150
|
telemetry: TelemetryConfig = {
|
148
|
-
"OTLPExporter": {"tracesEndpoint": [], "logsEndpoint": []}
|
151
|
+
"OTLPExporter": {"tracesEndpoint": [], "logsEndpoint": []},
|
152
|
+
"otlp_attributes": config.get("otlp_attributes", {}),
|
149
153
|
}
|
150
154
|
# For mypy
|
151
155
|
assert telemetry["OTLPExporter"] is not None
|
@@ -431,7 +435,6 @@ def is_valid_database_url(database_url: str) -> bool:
|
|
431
435
|
url = make_url(database_url)
|
432
436
|
required_fields = [
|
433
437
|
("username", "Username must be specified in the connection URL"),
|
434
|
-
("password", "Password must be specified in the connection URL"),
|
435
438
|
("host", "Host must be specified in the connection URL"),
|
436
439
|
("database", "Database name must be specified in the connection URL"),
|
437
440
|
]
|
@@ -150,9 +150,12 @@ class DBOSNotAuthorizedError(DBOSException):
|
|
150
150
|
class DBOSMaxStepRetriesExceeded(DBOSException):
|
151
151
|
"""Exception raised when a step was retried the maximimum number of times without success."""
|
152
152
|
|
153
|
-
def __init__(
|
153
|
+
def __init__(
|
154
|
+
self, step_name: str, max_retries: int, errors: list[Exception]
|
155
|
+
) -> None:
|
154
156
|
self.step_name = step_name
|
155
157
|
self.max_retries = max_retries
|
158
|
+
self.errors = errors
|
156
159
|
super().__init__(
|
157
160
|
f"Step {step_name} has exceeded its maximum of {max_retries} retries",
|
158
161
|
dbos_error_code=DBOSErrorCode.MaxStepRetriesExceeded.value,
|
@@ -160,7 +163,7 @@ class DBOSMaxStepRetriesExceeded(DBOSException):
|
|
160
163
|
|
161
164
|
def __reduce__(self) -> Any:
|
162
165
|
# Tell jsonpickle how to reconstruct this object
|
163
|
-
return (self.__class__, (self.step_name, self.max_retries))
|
166
|
+
return (self.__class__, (self.step_name, self.max_retries, self.errors))
|
164
167
|
|
165
168
|
|
166
169
|
class DBOSConflictingRegistrationError(DBOSException):
|
@@ -20,14 +20,17 @@ _otlp_handler, _dbos_log_transformer = None, None
|
|
20
20
|
|
21
21
|
|
22
22
|
class DBOSLogTransformer(logging.Filter):
|
23
|
-
def __init__(self) -> None:
|
23
|
+
def __init__(self, config: "ConfigFile") -> None:
|
24
24
|
super().__init__()
|
25
25
|
self.app_id = os.environ.get("DBOS__APPID", "")
|
26
|
+
self.otlp_attributes: dict[str, str] = config.get("telemetry", {}).get("otlp_attributes", {}) # type: ignore
|
26
27
|
|
27
28
|
def filter(self, record: Any) -> bool:
|
28
29
|
record.applicationID = self.app_id
|
29
30
|
record.applicationVersion = GlobalParams.app_version
|
30
31
|
record.executorID = GlobalParams.executor_id
|
32
|
+
for k, v in self.otlp_attributes.items():
|
33
|
+
setattr(record, k, v)
|
31
34
|
|
32
35
|
# If available, decorate the log entry with Workflow ID and Trace ID
|
33
36
|
from dbos._context import get_local_dbos_context
|
@@ -98,7 +101,7 @@ def config_logger(config: "ConfigFile") -> None:
|
|
98
101
|
|
99
102
|
# Attach DBOS-specific attributes to all log entries.
|
100
103
|
global _dbos_log_transformer
|
101
|
-
_dbos_log_transformer = DBOSLogTransformer()
|
104
|
+
_dbos_log_transformer = DBOSLogTransformer(config)
|
102
105
|
dbos_logger.addFilter(_dbos_log_transformer)
|
103
106
|
|
104
107
|
|
@@ -37,7 +37,7 @@ class Outcome(Protocol[T]):
|
|
37
37
|
self,
|
38
38
|
attempts: int,
|
39
39
|
on_exception: Callable[[int, BaseException], float],
|
40
|
-
exceeded_retries: Callable[[int],
|
40
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
41
41
|
) -> "Outcome[T]": ...
|
42
42
|
|
43
43
|
def intercept(
|
@@ -96,23 +96,25 @@ class Immediate(Outcome[T]):
|
|
96
96
|
func: Callable[[], T],
|
97
97
|
attempts: int,
|
98
98
|
on_exception: Callable[[int, BaseException], float],
|
99
|
-
exceeded_retries: Callable[[int],
|
99
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
100
100
|
) -> T:
|
101
|
+
errors: list[Exception] = []
|
101
102
|
for i in range(attempts):
|
102
103
|
try:
|
103
104
|
with EnterDBOSStepRetry(i, attempts):
|
104
105
|
return func()
|
105
106
|
except Exception as exp:
|
107
|
+
errors.append(exp)
|
106
108
|
wait_time = on_exception(i, exp)
|
107
109
|
time.sleep(wait_time)
|
108
110
|
|
109
|
-
raise exceeded_retries(attempts)
|
111
|
+
raise exceeded_retries(attempts, errors)
|
110
112
|
|
111
113
|
def retry(
|
112
114
|
self,
|
113
115
|
attempts: int,
|
114
116
|
on_exception: Callable[[int, BaseException], float],
|
115
|
-
exceeded_retries: Callable[[int],
|
117
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
116
118
|
) -> "Immediate[T]":
|
117
119
|
assert attempts > 0
|
118
120
|
return Immediate[T](
|
@@ -183,23 +185,25 @@ class Pending(Outcome[T]):
|
|
183
185
|
func: Callable[[], Coroutine[Any, Any, T]],
|
184
186
|
attempts: int,
|
185
187
|
on_exception: Callable[[int, BaseException], float],
|
186
|
-
exceeded_retries: Callable[[int],
|
188
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
187
189
|
) -> T:
|
190
|
+
errors: list[Exception] = []
|
188
191
|
for i in range(attempts):
|
189
192
|
try:
|
190
193
|
with EnterDBOSStepRetry(i, attempts):
|
191
194
|
return await func()
|
192
195
|
except Exception as exp:
|
196
|
+
errors.append(exp)
|
193
197
|
wait_time = on_exception(i, exp)
|
194
198
|
await asyncio.sleep(wait_time)
|
195
199
|
|
196
|
-
raise exceeded_retries(attempts)
|
200
|
+
raise exceeded_retries(attempts, errors)
|
197
201
|
|
198
202
|
def retry(
|
199
203
|
self,
|
200
204
|
attempts: int,
|
201
205
|
on_exception: Callable[[int, BaseException], float],
|
202
|
-
exceeded_retries: Callable[[int],
|
206
|
+
exceeded_retries: Callable[[int, list[Exception]], Exception],
|
203
207
|
) -> "Pending[T]":
|
204
208
|
assert attempts > 0
|
205
209
|
return Pending[T](
|
@@ -19,11 +19,14 @@ if TYPE_CHECKING:
|
|
19
19
|
|
20
20
|
class DBOSTracer:
|
21
21
|
|
22
|
+
otlp_attributes: dict[str, str] = {}
|
23
|
+
|
22
24
|
def __init__(self) -> None:
|
23
25
|
self.app_id = os.environ.get("DBOS__APPID", None)
|
24
26
|
self.provider: Optional[TracerProvider] = None
|
25
27
|
|
26
28
|
def config(self, config: ConfigFile) -> None:
|
29
|
+
self.otlp_attributes = config.get("telemetry", {}).get("otlp_attributes", {}) # type: ignore
|
27
30
|
if not isinstance(trace.get_tracer_provider(), TracerProvider):
|
28
31
|
resource = Resource(
|
29
32
|
attributes={
|
@@ -63,6 +66,8 @@ class DBOSTracer:
|
|
63
66
|
for k, v in attributes.items():
|
64
67
|
if k != "name" and v is not None and isinstance(v, (str, bool, int, float)):
|
65
68
|
span.set_attribute(k, v)
|
69
|
+
for k, v in self.otlp_attributes.items():
|
70
|
+
span.set_attribute(k, v)
|
66
71
|
return span
|
67
72
|
|
68
73
|
def end_span(self, span: Span) -> None:
|
@@ -18,7 +18,12 @@ from dbos._debug import debug_workflow, parse_start_command
|
|
18
18
|
|
19
19
|
from .._app_db import ApplicationDatabase
|
20
20
|
from .._client import DBOSClient
|
21
|
-
from .._dbos_config import
|
21
|
+
from .._dbos_config import (
|
22
|
+
_app_name_to_db_name,
|
23
|
+
_is_valid_app_name,
|
24
|
+
is_valid_database_url,
|
25
|
+
load_config,
|
26
|
+
)
|
22
27
|
from .._docker_pg_helper import start_docker_pg, stop_docker_pg
|
23
28
|
from .._schemas.system_database import SystemSchema
|
24
29
|
from .._sys_db import SystemDatabase, reset_system_database
|
@@ -28,12 +33,36 @@ from ._template_init import copy_template, get_project_name, get_templates_direc
|
|
28
33
|
|
29
34
|
|
30
35
|
def _get_db_url(db_url: Optional[str]) -> str:
|
36
|
+
"""
|
37
|
+
Get the database URL to use for the DBOS application.
|
38
|
+
Order of precedence:
|
39
|
+
- If the `db_url` argument is provided, use it.
|
40
|
+
- If the `dbos-config.yaml` file is present, use the `database_url` from it.
|
41
|
+
- If the `DBOS_DATABASE_URL` environment variable is set, use it.
|
42
|
+
|
43
|
+
Otherwise fallback to the same default Postgres URL than the DBOS library.
|
44
|
+
Note that for the latter to be possible, a configuration file must have been found, with an application name set.
|
45
|
+
"""
|
31
46
|
database_url = db_url
|
47
|
+
_app_db_name = None
|
48
|
+
if database_url is None:
|
49
|
+
# Load from config file if present
|
50
|
+
try:
|
51
|
+
config = load_config(run_process_config=False, silent=True)
|
52
|
+
database_url = config.get("database_url")
|
53
|
+
_app_db_name = _app_name_to_db_name(config["name"])
|
54
|
+
except (FileNotFoundError, OSError):
|
55
|
+
# Config file doesn't exist, continue with other fallbacks
|
56
|
+
pass
|
32
57
|
if database_url is None:
|
33
58
|
database_url = os.getenv("DBOS_DATABASE_URL")
|
59
|
+
if database_url is None and _app_db_name is not None:
|
60
|
+
# Fallback on the same defaults than the DBOS library
|
61
|
+
_password = os.environ.get("PGPASSWORD", "dbos")
|
62
|
+
database_url = f"postgres://postgres:{_password}@localhost:5432/{_app_db_name}?connect_timeout=10&sslmode=prefer"
|
34
63
|
if database_url is None:
|
35
64
|
raise ValueError(
|
36
|
-
"Missing database URL: please set it using the --db-url flag
|
65
|
+
"Missing database URL: please set it using the --db-url flag, the DBOS_DATABASE_URL environment variable, or in your dbos-config.yaml file."
|
37
66
|
)
|
38
67
|
assert is_valid_database_url(database_url)
|
39
68
|
return database_url
|
@@ -557,6 +557,7 @@ def test_get_workflow_by_id(dbos: DBOS) -> None:
|
|
557
557
|
response.status_code == 404
|
558
558
|
), f"Expected status code 404, but got {response.status_code}"
|
559
559
|
|
560
|
+
|
560
561
|
def test_admin_garbage_collect(dbos: DBOS) -> None:
|
561
562
|
|
562
563
|
@DBOS.workflow()
|
@@ -617,11 +618,15 @@ def test_queued_workflows_endpoint(dbos: DBOS) -> None:
|
|
617
618
|
|
618
619
|
# Test basic queued workflows endpoint
|
619
620
|
response = requests.post("http://localhost:3001/queues", json={}, timeout=5)
|
620
|
-
assert
|
621
|
+
assert (
|
622
|
+
response.status_code == 200
|
623
|
+
), f"Expected status 200, got {response.status_code}"
|
621
624
|
|
622
625
|
queued_workflows = response.json()
|
623
626
|
assert isinstance(queued_workflows, list), "Response should be a list"
|
624
|
-
assert
|
627
|
+
assert (
|
628
|
+
len(queued_workflows) == 3
|
629
|
+
), f"Expected 3 queued workflows, got {len(queued_workflows)}"
|
625
630
|
|
626
631
|
# Test with filters
|
627
632
|
filters = {"queue_name": "test-queue-1", "limit": 1}
|
@@ -630,7 +635,9 @@ def test_queued_workflows_endpoint(dbos: DBOS) -> None:
|
|
630
635
|
|
631
636
|
filtered_workflows = response.json()
|
632
637
|
assert isinstance(filtered_workflows, list), "Response should be a list"
|
633
|
-
assert
|
638
|
+
assert (
|
639
|
+
len(filtered_workflows) == 1
|
640
|
+
), f"Expected 1 workflow, got {len(filtered_workflows)}"
|
634
641
|
|
635
642
|
# Test with non-existent queue name
|
636
643
|
filters = {"queue_name": "non-existent-queue"}
|
@@ -638,5 +645,7 @@ def test_queued_workflows_endpoint(dbos: DBOS) -> None:
|
|
638
645
|
assert response.status_code == 200
|
639
646
|
|
640
647
|
empty_result = response.json()
|
641
|
-
assert isinstance(
|
642
|
-
|
648
|
+
assert isinstance(
|
649
|
+
empty_result, list
|
650
|
+
), "Response should be a list even for non-existent queue"
|
651
|
+
assert len(empty_result) == 0, "Expected no workflows for non-existent queue"
|
@@ -10,7 +10,7 @@ from sqlalchemy import event
|
|
10
10
|
from sqlalchemy.exc import OperationalError
|
11
11
|
|
12
12
|
# Public API
|
13
|
-
from dbos import DBOS
|
13
|
+
from dbos import DBOS, DBOSClient
|
14
14
|
from dbos._dbos_config import (
|
15
15
|
ConfigFile,
|
16
16
|
DBOSConfig,
|
@@ -598,15 +598,6 @@ def test_process_config_with_wrong_db_url():
|
|
598
598
|
process_config(data=config)
|
599
599
|
assert "Username must be specified in the connection URL" in str(exc_info.value)
|
600
600
|
|
601
|
-
# Missing password
|
602
|
-
config: ConfigFile = {
|
603
|
-
"name": "some-app",
|
604
|
-
"database_url": "postgres://user:@h:1234/dbname",
|
605
|
-
}
|
606
|
-
with pytest.raises(DBOSInitializationError) as exc_info:
|
607
|
-
process_config(data=config)
|
608
|
-
assert "Password must be specified in the connection URL" in str(exc_info.value)
|
609
|
-
|
610
601
|
# Missing host
|
611
602
|
config: ConfigFile = {
|
612
603
|
"name": "some-app",
|
@@ -628,6 +619,26 @@ def test_process_config_with_wrong_db_url():
|
|
628
619
|
)
|
629
620
|
|
630
621
|
|
622
|
+
def test_database_url_no_password():
|
623
|
+
"""Test that the database URL can be provided without a password."""
|
624
|
+
expected_url = "postgresql://postgres@localhost:5432/dbostestpy?sslmode=disable"
|
625
|
+
config: DBOSConfig = {
|
626
|
+
"name": "some-app",
|
627
|
+
"database_url": expected_url,
|
628
|
+
}
|
629
|
+
processed_config = translate_dbos_config_to_config_file(config)
|
630
|
+
assert processed_config["name"] == "some-app"
|
631
|
+
assert processed_config["database_url"] == expected_url
|
632
|
+
|
633
|
+
# Make sure we can use it to construct a DBOS Client and connect to the database without a password
|
634
|
+
client = DBOSClient(expected_url)
|
635
|
+
try:
|
636
|
+
res = client.list_queued_workflows()
|
637
|
+
assert res is not None
|
638
|
+
finally:
|
639
|
+
client.destroy()
|
640
|
+
|
641
|
+
|
631
642
|
####################
|
632
643
|
# TRANSLATE DBOSConfig to ConfigFile
|
633
644
|
####################
|
@@ -332,6 +332,11 @@ def test_step_retries(dbos: DBOS) -> None:
|
|
332
332
|
failing_step()
|
333
333
|
assert error_message in str(excinfo.value)
|
334
334
|
assert step_counter == max_attempts
|
335
|
+
assert len(excinfo.value.errors) == max_attempts
|
336
|
+
for error in excinfo.value.errors:
|
337
|
+
assert isinstance(error, Exception)
|
338
|
+
assert error
|
339
|
+
assert "fail" in str(error)
|
335
340
|
|
336
341
|
# Test calling the workflow
|
337
342
|
step_counter = 0
|
@@ -448,10 +453,11 @@ def test_error_serialization() -> None:
|
|
448
453
|
# Verify that each exception that can be thrown in a workflow
|
449
454
|
# is serializable and deserializable
|
450
455
|
# DBOSMaxStepRetriesExceeded
|
451
|
-
e: Exception = DBOSMaxStepRetriesExceeded("step", 1)
|
456
|
+
e: Exception = DBOSMaxStepRetriesExceeded("step", 1, [Exception()])
|
452
457
|
d = deserialize_exception(serialize_exception(e))
|
453
458
|
assert isinstance(d, DBOSMaxStepRetriesExceeded)
|
454
459
|
assert str(d) == str(e)
|
460
|
+
assert isinstance(d.errors[0], Exception)
|
455
461
|
# DBOSNotAuthorizedError
|
456
462
|
e = DBOSNotAuthorizedError("no")
|
457
463
|
d = deserialize_exception(serialize_exception(e))
|
@@ -56,7 +56,7 @@ def test_immediate_retry() -> None:
|
|
56
56
|
raise Exception("Error")
|
57
57
|
|
58
58
|
o1 = Outcome[int].make(raiser)
|
59
|
-
o2 = o1.retry(3, lambda i, e: 0.1, lambda i: ExceededRetries())
|
59
|
+
o2 = o1.retry(3, lambda i, e: 0.1, lambda i, e: ExceededRetries())
|
60
60
|
|
61
61
|
assert isinstance(o2, Immediate)
|
62
62
|
with pytest.raises(ExceededRetries):
|
@@ -105,7 +105,7 @@ async def test_pending_retry() -> None:
|
|
105
105
|
raise Exception("Error")
|
106
106
|
|
107
107
|
o1 = Outcome[int].make(raiser)
|
108
|
-
o2 = o1.retry(3, lambda i, e: 0.1, lambda i: ExceededRetries())
|
108
|
+
o2 = o1.retry(3, lambda i, e: 0.1, lambda i, e: ExceededRetries())
|
109
109
|
|
110
110
|
assert isinstance(o2, Pending)
|
111
111
|
with pytest.raises(ExceededRetries):
|
@@ -7,11 +7,12 @@ import subprocess
|
|
7
7
|
import threading
|
8
8
|
import time
|
9
9
|
import uuid
|
10
|
-
from typing import List
|
10
|
+
from typing import Any, List
|
11
11
|
from urllib.parse import quote
|
12
12
|
|
13
13
|
import pytest
|
14
14
|
import sqlalchemy as sa
|
15
|
+
from pydantic import BaseModel
|
15
16
|
|
16
17
|
from dbos import (
|
17
18
|
DBOS,
|
@@ -1462,3 +1463,55 @@ def test_queue_executor_id(dbos: DBOS) -> None:
|
|
1462
1463
|
handle = queue.enqueue(example_workflow)
|
1463
1464
|
assert handle.get_result() == wfid
|
1464
1465
|
assert handle.get_status().executor_id == original_executor_id
|
1466
|
+
|
1467
|
+
|
1468
|
+
# Non-basic types must be declared in an importable scope (so not inside a function)
|
1469
|
+
# to be serializable and deserializable
|
1470
|
+
class InnerType(BaseModel):
|
1471
|
+
one: str
|
1472
|
+
two: int
|
1473
|
+
|
1474
|
+
|
1475
|
+
class OuterType(BaseModel):
|
1476
|
+
inner: InnerType
|
1477
|
+
|
1478
|
+
|
1479
|
+
def test_complex_type(dbos: DBOS) -> None:
|
1480
|
+
queue = Queue("test_queue")
|
1481
|
+
|
1482
|
+
@DBOS.workflow()
|
1483
|
+
def workflow(input: OuterType) -> OuterType:
|
1484
|
+
return input
|
1485
|
+
|
1486
|
+
# Verify a workflow with non-basic inputs and outputs can be enqueued
|
1487
|
+
inner = InnerType(one="one", two=2)
|
1488
|
+
outer = OuterType(inner=inner)
|
1489
|
+
|
1490
|
+
handle = queue.enqueue(workflow, outer)
|
1491
|
+
result = handle.get_result()
|
1492
|
+
|
1493
|
+
def check(result: Any) -> None:
|
1494
|
+
assert isinstance(result, OuterType)
|
1495
|
+
assert isinstance(result.inner, InnerType)
|
1496
|
+
assert result.inner.one == outer.inner.one
|
1497
|
+
assert result.inner.two == outer.inner.two
|
1498
|
+
|
1499
|
+
check(result)
|
1500
|
+
|
1501
|
+
# Verify a workflow with non-basic inputs and outputs can be recovered
|
1502
|
+
start_event = threading.Event()
|
1503
|
+
event = threading.Event()
|
1504
|
+
|
1505
|
+
@DBOS.workflow()
|
1506
|
+
def blocked_workflow(input: OuterType) -> OuterType:
|
1507
|
+
start_event.set()
|
1508
|
+
event.wait()
|
1509
|
+
return input
|
1510
|
+
|
1511
|
+
handle = queue.enqueue(blocked_workflow, outer)
|
1512
|
+
|
1513
|
+
start_event.wait()
|
1514
|
+
recovery_handle = DBOS._recover_pending_workflows()[0]
|
1515
|
+
event.set()
|
1516
|
+
check(handle.get_result())
|
1517
|
+
check(recovery_handle.get_result())
|
@@ -7,12 +7,16 @@ from opentelemetry.sdk import trace as tracesdk
|
|
7
7
|
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
8
8
|
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
|
9
9
|
|
10
|
-
from dbos import DBOS
|
10
|
+
from dbos import DBOS, DBOSConfig
|
11
11
|
from dbos._tracer import dbos_tracer
|
12
12
|
from dbos._utils import GlobalParams
|
13
13
|
|
14
14
|
|
15
|
-
def test_spans(
|
15
|
+
def test_spans(config: DBOSConfig) -> None:
|
16
|
+
DBOS.destroy(destroy_registry=True)
|
17
|
+
config["otlp_attributes"] = {"foo": "bar"}
|
18
|
+
DBOS(config=config)
|
19
|
+
DBOS.launch()
|
16
20
|
|
17
21
|
@DBOS.workflow()
|
18
22
|
def test_workflow() -> None:
|
@@ -44,6 +48,7 @@ def test_spans(dbos: DBOS) -> None:
|
|
44
48
|
assert span.attributes["applicationVersion"] == GlobalParams.app_version
|
45
49
|
assert span.attributes["executorID"] == GlobalParams.executor_id
|
46
50
|
assert span.context is not None
|
51
|
+
assert span.attributes["foo"] == "bar"
|
47
52
|
|
48
53
|
assert spans[0].name == test_step.__name__
|
49
54
|
assert spans[1].name == "a new span"
|
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
|
File without changes
|
File without changes
|
{dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{dbos-1.5.0a5 → dbos-1.5.0a10}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py
RENAMED
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
|
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
|
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
|
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
|