dbos 1.5.0a5__py3-none-any.whl → 1.6.0a1__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.
dbos/_admin_server.py CHANGED
@@ -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, Dict
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
 
dbos/_core.py CHANGED
@@ -445,7 +445,8 @@ def execute_workflow_by_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[An
445
445
  wf_func = dbos._registry.workflow_info_map.get(status["name"], None)
446
446
  if not wf_func:
447
447
  raise DBOSWorkflowFunctionNotFoundError(
448
- workflow_id, "Workflow function not found"
448
+ workflow_id,
449
+ f"Cannot execute workflow because {status['name']} is not a registered workflow function",
449
450
  )
450
451
  with DBOSContextEnsure():
451
452
  # If this function belongs to a configured class, add that class instance as its first argument
@@ -1123,7 +1124,7 @@ def decorate_step(
1123
1124
  stepOutcome = stepOutcome.retry(
1124
1125
  max_attempts,
1125
1126
  on_exception,
1126
- lambda i: DBOSMaxStepRetriesExceeded(func.__name__, i),
1127
+ lambda i, e: DBOSMaxStepRetriesExceeded(func.__name__, i, e),
1127
1128
  )
1128
1129
 
1129
1130
  outcome = (
dbos/_dbos_config.py CHANGED
@@ -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
  ]
dbos/_error.py CHANGED
@@ -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__(self, step_name: str, max_retries: int) -> None:
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):
dbos/_logger.py CHANGED
@@ -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
 
dbos/_outcome.py CHANGED
@@ -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], BaseException],
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], BaseException],
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], BaseException],
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], BaseException],
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], BaseException],
206
+ exceeded_retries: Callable[[int, list[Exception]], Exception],
203
207
  ) -> "Pending[T]":
204
208
  assert attempts > 0
205
209
  return Pending[T](
dbos/_tracer.py CHANGED
@@ -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:
dbos/cli/cli.py CHANGED
@@ -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 _is_valid_app_name, is_valid_database_url, load_config
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 or the DBOS_DATABASE_URL environment variable."
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.5.0a5
3
+ Version: 1.6.0a1
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,29 +1,29 @@
1
- dbos-1.5.0a5.dist-info/METADATA,sha256=PmP2OiVSSpeLfQI7zia2CeglXOeQXDi2QLxWK88OlxM,13267
2
- dbos-1.5.0a5.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- dbos-1.5.0a5.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-1.5.0a5.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-1.6.0a1.dist-info/METADATA,sha256=uefRlKIxEPVJEM0vqwL7TRtTXp48I0YLll8XfJeSFRk,13267
2
+ dbos-1.6.0a1.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ dbos-1.6.0a1.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-1.6.0a1.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
5
  dbos/__init__.py,sha256=NssPCubaBxdiKarOWa-wViz1hdJSkmBGcpLX_gQ4NeA,891
6
6
  dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
7
- dbos/_admin_server.py,sha256=t0GU32Z9FZEPBQSzVXDrhj0lvVjkDhH_CN6p0qScaB8,15465
7
+ dbos/_admin_server.py,sha256=l46ZX4NpvBP9W8cl9gE7OqMNwUCevLMt2VztM7crBv0,15465
8
8
  dbos/_app_db.py,sha256=htblDPfqrpb_uZoFcvaud7cgQ-PDyn6Bn-cBidxdCTA,10603
9
9
  dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
10
10
  dbos/_client.py,sha256=cQxw1Nbh_vKZ03lONt0EmUhwXBk3B3NczZrmfXXeefY,14667
11
11
  dbos/_conductor/conductor.py,sha256=y_T-8kEHwKWt6W8LtcFMctB_6EvYFWsuGLxiFuuKKBU,23702
12
12
  dbos/_conductor/protocol.py,sha256=DOTprPSd7oHDcvwWSyZpnlPds_JfILtcKzHZa-qBsF4,7330
13
13
  dbos/_context.py,sha256=5ajoWAmToAfzzmMLylnJZoL4Ny9rBwZWuG05sXadMIA,24798
14
- dbos/_core.py,sha256=mEtrgZ3KbOx99qtPsNXLQXyAx6QD4nhfBjP89zoD2Dg,48517
14
+ dbos/_core.py,sha256=OFxmPjgnOh-uv7-G1f1GrQrSSp-X6WzoIvlX9O0cBqE,48596
15
15
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
16
16
  dbos/_dbos.py,sha256=HHMo-PLUa6m5jFgdA-YYAwaTx2kyA1Te1VslAyd0hIw,47257
17
- dbos/_dbos_config.py,sha256=2CC1YR8lP9W-_NsMUMnTnW-v-70KN4XkbJEeNJ78RlQ,20373
17
+ dbos/_dbos_config.py,sha256=JUG4V1rrP0p1AYESgih4ea80qOH_13UsgoIIm8X84pw,20562
18
18
  dbos/_debug.py,sha256=99j2SChWmCPAlZoDmjsJGe77tpU2LEa8E2TtLAnnh7o,1831
19
19
  dbos/_docker_pg_helper.py,sha256=tLJXWqZ4S-ExcaPnxg_i6cVxL6ZxrYlZjaGsklY-s2I,6115
20
- dbos/_error.py,sha256=q0OQJZTbR8FFHV9hEpAGpz9oWBT5L509zUhmyff7FJw,8500
20
+ dbos/_error.py,sha256=nS7KuXJHhuNXZRErxdEUGT38Hb0VPyxNwSyADiVpHcE,8581
21
21
  dbos/_event_loop.py,sha256=cvaFN9-II3MsHEOq8QoICc_8qSKrjikMlLfuhC3Y8Dk,2923
22
22
  dbos/_fastapi.py,sha256=T7YlVY77ASqyTqq0aAPclZ9YzlXdGTT0lEYSwSgt1EE,3151
23
23
  dbos/_flask.py,sha256=Npnakt-a3W5OykONFRkDRnumaDhTQmA0NPdUCGRYKXE,1652
24
24
  dbos/_kafka.py,sha256=pz0xZ9F3X9Ky1k-VSbeF3tfPhP3UPr3lUUhUfE41__U,4198
25
25
  dbos/_kafka_message.py,sha256=NYvOXNG3Qn7bghn1pv3fg4Pbs86ILZGcK4IB-MLUNu0,409
26
- dbos/_logger.py,sha256=Yde-w0MZt9xT2NBmzcKY9G3mn6OJ5aVJuC5Elm3p4p4,4390
26
+ dbos/_logger.py,sha256=Dp6bHZKUtcm5gWwYHj_HA5Wj5OMuJGUrpl2g2i4xDZg,4620
27
27
  dbos/_migrations/env.py,sha256=38SIGVbmn_VV2x2u1aHLcPOoWgZ84eCymf3g_NljmbU,1626
28
28
  dbos/_migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
29
29
  dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py,sha256=ICLPl8CN9tQXMsLDsAj8z1TsL831-Z3F8jSBvrR-wyw,736
@@ -39,7 +39,7 @@ dbos/_migrations/versions/d76646551a6c_workflow_queue.py,sha256=G942nophZ2uC2vc4
39
39
  dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py,sha256=_J0jP247fuo66fzOmLlKFO9FJ_CRBXlqa2lnLrcXugQ,672
40
40
  dbos/_migrations/versions/eab0cc1d9a14_job_queue.py,sha256=uvhFOtqbBreCePhAxZfIT0qCAI7BiZTou9wt6QnbY7c,1412
41
41
  dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py,sha256=m90Lc5YH0ZISSq1MyxND6oq3RZrZKrIqEsZtwJ1jWxA,1049
42
- dbos/_outcome.py,sha256=EXxBg4jXCVJsByDQ1VOCIedmbeq_03S6d-p1vqQrLFU,6810
42
+ dbos/_outcome.py,sha256=Kz3aL7517q9UEFTx3Cq9zzztjWyWVOx_08fZyHo9dvg,7035
43
43
  dbos/_queue.py,sha256=Kq7aldTDLRF7cZtkXmsCy6wV2PR24enkhghEG25NtaU,4080
44
44
  dbos/_recovery.py,sha256=TBNjkmSEqBU-g5YXExsLJ9XoCe4iekqtREsskXZECEg,2507
45
45
  dbos/_registrations.py,sha256=CZt1ElqDjCT7hz6iyT-1av76Yu-iuwu_c9lozO87wvM,7303
@@ -60,13 +60,13 @@ dbos/_templates/dbos-db-starter/migrations/env.py.dbos,sha256=IBB_gz9RjC20HPfOTG
60
60
  dbos/_templates/dbos-db-starter/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
61
61
  dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py,sha256=MpS7LGaJS0CpvsjhfDkp9EJqvMvVCjRPfUp4c0aE2ys,941
62
62
  dbos/_templates/dbos-db-starter/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
63
- dbos/_tracer.py,sha256=yN6GRDKu_1p-EqtQLNarMocPfga2ZuqpzStzzSPYhzo,2732
63
+ dbos/_tracer.py,sha256=RnlcaOJEx_58hr2J9L9g6E7gjAHAeEtEGugJZmCwNfQ,2963
64
64
  dbos/_utils.py,sha256=uywq1QrjMwy17btjxW4bES49povlQwYwYbvKwMT6C2U,1575
65
65
  dbos/_workflow_commands.py,sha256=Fi-sQxQvFkDkMlCv7EyRJIWxqk3fG6DGlgvvwkjWbS4,4485
66
66
  dbos/cli/_github_init.py,sha256=Y_bDF9gfO2jB1id4FV5h1oIxEJRWyqVjhb7bNEa5nQ0,3224
67
67
  dbos/cli/_template_init.py,sha256=7JBcpMqP1r2mfCnvWatu33z8ctEGHJarlZYKgB83cXE,2972
68
- dbos/cli/cli.py,sha256=EemOMqNpzSU2BQhAxV_e59pBRITDLwt49HF6W3uWBZg,20775
68
+ dbos/cli/cli.py,sha256=IcfaX4rrSrk6f24S2jrlR33snYMyNyEIx_lNQtuVr2E,22081
69
69
  dbos/dbos-config.schema.json,sha256=CjaspeYmOkx6Ip_pcxtmfXJTn_YGdSx_0pcPBF7KZmo,6060
70
70
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
71
71
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
72
- dbos-1.5.0a5.dist-info/RECORD,,
72
+ dbos-1.6.0a1.dist-info/RECORD,,
File without changes