dbos 0.28.0a12__py3-none-any.whl → 0.28.0a15__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/_app_db.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import List, Optional, TypedDict
1
+ from typing import Any, Dict, List, Optional, TypedDict
2
2
 
3
3
  import sqlalchemy as sa
4
4
  import sqlalchemy.dialects.postgresql as pg
@@ -7,7 +7,6 @@ from sqlalchemy.exc import DBAPIError
7
7
  from sqlalchemy.orm import Session, sessionmaker
8
8
 
9
9
  from . import _serialization
10
- from ._dbos_config import DatabaseConfig
11
10
  from ._error import DBOSUnexpectedStepError, DBOSWorkflowConflictIDError
12
11
  from ._schemas.application_database import ApplicationSchema
13
12
  from ._sys_db import StepInfo
@@ -31,67 +30,33 @@ class RecordedResult(TypedDict):
31
30
 
32
31
  class ApplicationDatabase:
33
32
 
34
- def __init__(self, database: DatabaseConfig, *, debug_mode: bool = False):
35
-
36
- app_db_name = database["app_db_name"]
33
+ def __init__(
34
+ self,
35
+ *,
36
+ database_url: str,
37
+ engine_kwargs: Dict[str, Any],
38
+ debug_mode: bool = False,
39
+ ):
40
+ app_db_url = sa.make_url(database_url).set(drivername="postgresql+psycopg")
37
41
 
38
42
  # If the application database does not already exist, create it
39
43
  if not debug_mode:
40
- postgres_db_url = sa.URL.create(
41
- "postgresql+psycopg",
42
- username=database["username"],
43
- password=database["password"],
44
- host=database["hostname"],
45
- port=database["port"],
46
- database="postgres",
44
+ postgres_db_engine = sa.create_engine(
45
+ app_db_url.set(database="postgres"),
46
+ **engine_kwargs,
47
47
  )
48
- postgres_db_engine = sa.create_engine(postgres_db_url)
49
48
  with postgres_db_engine.connect() as conn:
50
49
  conn.execution_options(isolation_level="AUTOCOMMIT")
51
50
  if not conn.execute(
52
51
  sa.text("SELECT 1 FROM pg_database WHERE datname=:db_name"),
53
- parameters={"db_name": app_db_name},
52
+ parameters={"db_name": app_db_url.database},
54
53
  ).scalar():
55
- conn.execute(sa.text(f"CREATE DATABASE {app_db_name}"))
54
+ conn.execute(sa.text(f"CREATE DATABASE {app_db_url.database}"))
56
55
  postgres_db_engine.dispose()
57
56
 
58
- # Create a connection pool for the application database
59
- app_db_url = sa.URL.create(
60
- "postgresql+psycopg",
61
- username=database["username"],
62
- password=database["password"],
63
- host=database["hostname"],
64
- port=database["port"],
65
- database=app_db_name,
66
- )
67
-
68
- connect_args = {}
69
- if (
70
- "connectionTimeoutMillis" in database
71
- and database["connectionTimeoutMillis"]
72
- ):
73
- connect_args["connect_timeout"] = int(
74
- database["connectionTimeoutMillis"] / 1000
75
- )
76
-
77
- pool_size = database.get("app_db_pool_size")
78
- if pool_size is None:
79
- pool_size = 20
80
-
81
- engine_kwargs = database.get("db_engine_kwargs")
82
57
  if engine_kwargs is None:
83
58
  engine_kwargs = {}
84
59
 
85
- # Respect user-provided values. Otherwise, set defaults.
86
- if "pool_size" not in engine_kwargs:
87
- engine_kwargs["pool_size"] = pool_size
88
- if "max_overflow" not in engine_kwargs:
89
- engine_kwargs["max_overflow"] = 0
90
- if "pool_timeout" not in engine_kwargs:
91
- engine_kwargs["pool_timeout"] = 30
92
- if "connect_args" not in engine_kwargs:
93
- engine_kwargs["connect_args"] = connect_args
94
-
95
60
  self.engine = sa.create_engine(
96
61
  app_db_url,
97
62
  **engine_kwargs,
dbos/_client.py CHANGED
@@ -15,7 +15,6 @@ else:
15
15
 
16
16
  from dbos import _serialization
17
17
  from dbos._dbos import WorkflowHandle, WorkflowHandleAsync
18
- from dbos._dbos_config import parse_database_url_to_dbconfig
19
18
  from dbos._error import DBOSException, DBOSNonExistentWorkflowError
20
19
  from dbos._registrations import DEFAULT_MAX_RECOVERY_ATTEMPTS
21
20
  from dbos._serialization import WorkflowInputs
@@ -100,11 +99,23 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
100
99
 
101
100
  class DBOSClient:
102
101
  def __init__(self, database_url: str, *, system_database: Optional[str] = None):
103
- db_config = parse_database_url_to_dbconfig(database_url)
104
- if system_database is not None:
105
- db_config["sys_db_name"] = system_database
106
- self._sys_db = SystemDatabase(db_config)
107
- self._app_db = ApplicationDatabase(db_config)
102
+ self._sys_db = SystemDatabase(
103
+ database_url=database_url,
104
+ engine_kwargs={
105
+ "pool_timeout": 30,
106
+ "max_overflow": 0,
107
+ "pool_size": 2,
108
+ },
109
+ sys_db_name=system_database,
110
+ )
111
+ self._app_db = ApplicationDatabase(
112
+ database_url=database_url,
113
+ engine_kwargs={
114
+ "pool_timeout": 30,
115
+ "max_overflow": 0,
116
+ "pool_size": 2,
117
+ },
118
+ )
108
119
  self._db_url = database_url
109
120
 
110
121
  def destroy(self) -> None:
@@ -2,6 +2,7 @@ import socket
2
2
  import threading
3
3
  import time
4
4
  import traceback
5
+ import uuid
5
6
  from importlib.metadata import version
6
7
  from typing import TYPE_CHECKING, Optional
7
8
 
@@ -9,6 +10,7 @@ from websockets import ConnectionClosed, ConnectionClosedOK, InvalidStatus
9
10
  from websockets.sync.client import connect
10
11
  from websockets.sync.connection import Connection
11
12
 
13
+ from dbos._context import SetWorkflowID
12
14
  from dbos._utils import GlobalParams
13
15
  from dbos._workflow_commands import (
14
16
  get_workflow,
@@ -168,10 +170,11 @@ class ConductorWebsocket(threading.Thread):
168
170
  )
169
171
  websocket.send(resume_response.to_json())
170
172
  elif msg_type == p.MessageType.RESTART:
173
+ # TODO: deprecate this message type in favor of Fork
171
174
  restart_message = p.RestartRequest.from_json(message)
172
175
  success = True
173
176
  try:
174
- self.dbos.restart_workflow(restart_message.workflow_id)
177
+ self.dbos.fork_workflow(restart_message.workflow_id, 1)
175
178
  except Exception as e:
176
179
  error_message = f"Exception encountered when restarting workflow {restart_message.workflow_id}: {traceback.format_exc()}"
177
180
  self.dbos.logger.error(error_message)
@@ -183,6 +186,34 @@ class ConductorWebsocket(threading.Thread):
183
186
  error_message=error_message,
184
187
  )
185
188
  websocket.send(restart_response.to_json())
189
+ elif msg_type == p.MessageType.FORK_WORKFLOW:
190
+ fork_message = p.ForkWorkflowRequest.from_json(message)
191
+ new_workflow_id = fork_message.body["new_workflow_id"]
192
+ if new_workflow_id is None:
193
+ new_workflow_id = str(uuid.uuid4())
194
+ workflow_id = fork_message.body["workflow_id"]
195
+ start_step = fork_message.body["start_step"]
196
+ app_version = fork_message.body["application_version"]
197
+ try:
198
+ with SetWorkflowID(new_workflow_id):
199
+ new_handle = self.dbos.fork_workflow(
200
+ workflow_id,
201
+ start_step,
202
+ application_version=app_version,
203
+ )
204
+ new_workflow_id = new_handle.workflow_id
205
+ except Exception as e:
206
+ error_message = f"Exception encountered when forking workflow {workflow_id} to new workflow {new_workflow_id} on step {start_step}, app version {app_version}: {traceback.format_exc()}"
207
+ self.dbos.logger.error(error_message)
208
+ new_workflow_id = None
209
+
210
+ fork_response = p.ForkWorkflowResponse(
211
+ type=p.MessageType.FORK_WORKFLOW,
212
+ request_id=base_message.request_id,
213
+ new_workflow_id=new_workflow_id,
214
+ error_message=error_message,
215
+ )
216
+ websocket.send(fork_response.to_json())
186
217
  elif msg_type == p.MessageType.LIST_WORKFLOWS:
187
218
  list_workflows_message = p.ListWorkflowsRequest.from_json(
188
219
  message
@@ -17,6 +17,7 @@ class MessageType(str, Enum):
17
17
  GET_WORKFLOW = "get_workflow"
18
18
  EXIST_PENDING_WORKFLOWS = "exist_pending_workflows"
19
19
  LIST_STEPS = "list_steps"
20
+ FORK_WORKFLOW = "fork_workflow"
20
21
 
21
22
 
22
23
  T = TypeVar("T", bound="BaseMessage")
@@ -133,7 +134,6 @@ class WorkflowsOutput:
133
134
  AuthenticatedRoles: Optional[str]
134
135
  Input: Optional[str]
135
136
  Output: Optional[str]
136
- Request: Optional[str]
137
137
  Error: Optional[str]
138
138
  CreatedAt: Optional[str]
139
139
  UpdatedAt: Optional[str]
@@ -167,7 +167,6 @@ class WorkflowsOutput:
167
167
  AuthenticatedRoles=roles_str,
168
168
  Input=inputs_str,
169
169
  Output=outputs_str,
170
- Request=request_str,
171
170
  Error=error_str,
172
171
  CreatedAt=created_at_str,
173
172
  UpdatedAt=updated_at_str,
@@ -263,3 +262,21 @@ class ListStepsRequest(BaseMessage):
263
262
  class ListStepsResponse(BaseMessage):
264
263
  output: Optional[List[WorkflowSteps]]
265
264
  error_message: Optional[str] = None
265
+
266
+
267
+ class ForkWorkflowBody(TypedDict):
268
+ workflow_id: str
269
+ start_step: int
270
+ application_version: Optional[str]
271
+ new_workflow_id: Optional[str]
272
+
273
+
274
+ @dataclass
275
+ class ForkWorkflowRequest(BaseMessage):
276
+ body: ForkWorkflowBody
277
+
278
+
279
+ @dataclass
280
+ class ForkWorkflowResponse(BaseMessage):
281
+ new_workflow_id: Optional[str]
282
+ error_message: Optional[str] = None
dbos/_dbos.py CHANGED
@@ -24,8 +24,6 @@ from typing import (
24
24
  Tuple,
25
25
  Type,
26
26
  TypeVar,
27
- Union,
28
- cast,
29
27
  )
30
28
 
31
29
  from opentelemetry.trace import Span
@@ -64,7 +62,6 @@ from ._registrations import (
64
62
  )
65
63
  from ._roles import default_required_roles, required_roles
66
64
  from ._scheduler import ScheduledWorkflow, scheduled
67
- from ._schemas.system_database import SystemSchema
68
65
  from ._sys_db import StepInfo, SystemDatabase, WorkflowStatus, reset_system_database
69
66
  from ._tracer import DBOSTracer, dbos_tracer
70
67
 
@@ -73,7 +70,7 @@ if TYPE_CHECKING:
73
70
  from ._kafka import _KafkaConsumerWorkflow
74
71
  from flask import Flask
75
72
 
76
- from sqlalchemy import URL
73
+ from sqlalchemy import make_url
77
74
  from sqlalchemy.orm import Session
78
75
 
79
76
  if sys.version_info < (3, 10):
@@ -418,11 +415,19 @@ class DBOS:
418
415
  dbos_logger.info(f"Application version: {GlobalParams.app_version}")
419
416
  self._executor_field = ThreadPoolExecutor(max_workers=64)
420
417
  self._background_event_loop.start()
418
+ assert self._config["database_url"] is not None
419
+ assert self._config["database"]["sys_db_engine_kwargs"] is not None
421
420
  self._sys_db_field = SystemDatabase(
422
- self._config["database"], debug_mode=debug_mode
421
+ database_url=self._config["database_url"],
422
+ engine_kwargs=self._config["database"]["sys_db_engine_kwargs"],
423
+ sys_db_name=self._config["database"]["sys_db_name"],
424
+ debug_mode=debug_mode,
423
425
  )
426
+ assert self._config["database"]["db_engine_kwargs"] is not None
424
427
  self._app_db_field = ApplicationDatabase(
425
- self._config["database"], debug_mode=debug_mode
428
+ database_url=self._config["database_url"],
429
+ engine_kwargs=self._config["database"]["db_engine_kwargs"],
430
+ debug_mode=debug_mode,
426
431
  )
427
432
 
428
433
  if debug_mode:
@@ -530,21 +535,13 @@ class DBOS:
530
535
  not self._launched
531
536
  ), "The system database cannot be reset after DBOS is launched. Resetting the system database is a destructive operation that should only be used in a test environment."
532
537
 
533
- sysdb_name = (
534
- self._config["database"]["sys_db_name"]
535
- if "sys_db_name" in self._config["database"]
536
- and self._config["database"]["sys_db_name"]
537
- else self._config["database"]["app_db_name"] + SystemSchema.sysdb_suffix
538
- )
539
- postgres_db_url = URL.create(
540
- "postgresql+psycopg",
541
- username=self._config["database"]["username"],
542
- password=self._config["database"]["password"],
543
- host=self._config["database"]["hostname"],
544
- port=self._config["database"]["port"],
545
- database="postgres",
546
- )
547
- reset_system_database(postgres_db_url, sysdb_name)
538
+ sysdb_name = self._config["database"]["sys_db_name"]
539
+ assert sysdb_name is not None
540
+
541
+ assert self._config["database_url"] is not None
542
+ pg_db_url = make_url(self._config["database_url"]).set(database="postgres")
543
+
544
+ reset_system_database(pg_db_url, sysdb_name)
548
545
 
549
546
  def _destroy(self) -> None:
550
547
  self._initialized = False
dbos/_dbos_config.py CHANGED
@@ -11,6 +11,7 @@ from sqlalchemy import make_url
11
11
 
12
12
  from ._error import DBOSInitializationError
13
13
  from ._logger import dbos_logger
14
+ from ._schemas.system_database import SystemSchema
14
15
 
15
16
  DBOS_CONFIG_PATH = "dbos-config.yaml"
16
17
 
@@ -22,7 +23,6 @@ class DBOSConfig(TypedDict, total=False):
22
23
  Attributes:
23
24
  name (str): Application name
24
25
  database_url (str): Database connection string
25
- app_db_pool_size (int): Application database pool size
26
26
  sys_db_name (str): System database name
27
27
  sys_db_pool_size (int): System database pool size
28
28
  db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs (See https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
@@ -35,7 +35,6 @@ class DBOSConfig(TypedDict, total=False):
35
35
 
36
36
  name: str
37
37
  database_url: Optional[str]
38
- app_db_pool_size: Optional[int]
39
38
  sys_db_name: Optional[str]
40
39
  sys_db_pool_size: Optional[int]
41
40
  db_engine_kwargs: Optional[Dict[str, Any]]
@@ -57,49 +56,22 @@ class DatabaseConfig(TypedDict, total=False):
57
56
  """
58
57
  Internal data structure containing the DBOS database configuration.
59
58
  Attributes:
60
- app_db_pool_size (int): Application database pool size
61
59
  sys_db_name (str): System database name
62
60
  sys_db_pool_size (int): System database pool size
63
61
  db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs
64
62
  migrate (List[str]): Migration commands to run on startup
65
63
  """
66
64
 
67
- hostname: str # Will be removed in a future version
68
- port: int # Will be removed in a future version
69
- username: str # Will be removed in a future version
70
- password: str # Will be removed in a future version
71
- connectionTimeoutMillis: Optional[int] # Will be removed in a future version
72
- app_db_name: str # Will be removed in a future version
73
- app_db_pool_size: Optional[int]
74
65
  sys_db_name: Optional[str]
75
- sys_db_pool_size: Optional[int]
66
+ sys_db_pool_size: Optional[
67
+ int
68
+ ] # For internal use, will be removed in a future version
76
69
  db_engine_kwargs: Optional[Dict[str, Any]]
77
- ssl: Optional[bool] # Will be removed in a future version
78
- ssl_ca: Optional[str] # Will be removed in a future version
70
+ sys_db_engine_kwargs: Optional[Dict[str, Any]]
79
71
  migrate: Optional[List[str]]
80
72
  rollback: Optional[List[str]] # Will be removed in a future version
81
73
 
82
74
 
83
- def parse_database_url_to_dbconfig(database_url: str) -> DatabaseConfig:
84
- db_url = make_url(database_url)
85
- db_config = {
86
- "hostname": db_url.host,
87
- "port": db_url.port or 5432,
88
- "username": db_url.username,
89
- "password": db_url.password,
90
- "app_db_name": db_url.database,
91
- }
92
- for key, value in db_url.query.items():
93
- str_value = value[0] if isinstance(value, tuple) else value
94
- if key == "connect_timeout":
95
- db_config["connectionTimeoutMillis"] = int(str_value) * 1000
96
- elif key == "sslmode":
97
- db_config["ssl"] = str_value == "require"
98
- elif key == "sslrootcert":
99
- db_config["ssl_ca"] = str_value
100
- return cast(DatabaseConfig, db_config)
101
-
102
-
103
75
  class OTLPExporterConfig(TypedDict, total=False):
104
76
  logsEndpoint: Optional[List[str]]
105
77
  tracesEndpoint: Optional[List[str]]
@@ -150,13 +122,8 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
150
122
 
151
123
  # Database config
152
124
  db_config: DatabaseConfig = {}
153
- database_url = config.get("database_url")
154
- if database_url:
155
- db_config = parse_database_url_to_dbconfig(database_url)
156
125
  if "sys_db_name" in config:
157
126
  db_config["sys_db_name"] = config.get("sys_db_name")
158
- if "app_db_pool_size" in config:
159
- db_config["app_db_pool_size"] = config.get("app_db_pool_size")
160
127
  if "sys_db_pool_size" in config:
161
128
  db_config["sys_db_pool_size"] = config.get("sys_db_pool_size")
162
129
  if "db_engine_kwargs" in config:
@@ -164,6 +131,9 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
164
131
  if db_config:
165
132
  translated_config["database"] = db_config
166
133
 
134
+ if "database_url" in config:
135
+ translated_config["database_url"] = config.get("database_url")
136
+
167
137
  # Runtime config
168
138
  translated_config["runtimeConfig"] = {"run_admin_server": True}
169
139
  if "admin_port" in config:
@@ -306,6 +276,24 @@ def process_config(
306
276
  data: ConfigFile,
307
277
  silent: bool = False,
308
278
  ) -> ConfigFile:
279
+ """
280
+ If a database_url is provided, pass it as is in the config.
281
+
282
+ Else, build a database_url from defaults.
283
+
284
+ Also build SQL Alchemy "kwargs" base on user input + defaults.
285
+ Specifically, db_engine_kwargs takes precedence over app_db_pool_size
286
+
287
+ In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
288
+
289
+ Default configuration:
290
+ - Hostname: localhost
291
+ - Port: 5432
292
+ - Username: postgres
293
+ - Password: $PGPASSWORD
294
+ - Database name: transformed application name.
295
+ """
296
+
309
297
  if "name" not in data:
310
298
  raise DBOSInitializationError(f"Configuration must specify an application name")
311
299
 
@@ -323,55 +311,7 @@ def process_config(
323
311
  if logs.get("logLevel") is None:
324
312
  logs["logLevel"] = "INFO"
325
313
 
326
- if "database" not in data:
327
- data["database"] = {}
328
-
329
- # database_url takes precedence over database config, but we need to preserve rollback and migrate if they exist
330
- migrate = data["database"].get("migrate", False)
331
- rollback = data["database"].get("rollback", False)
332
- if data.get("database_url"):
333
- dbconfig = parse_database_url_to_dbconfig(cast(str, data["database_url"]))
334
- if migrate:
335
- dbconfig["migrate"] = cast(List[str], migrate)
336
- if rollback:
337
- dbconfig["rollback"] = cast(List[str], rollback)
338
- data["database"] = dbconfig
339
-
340
- if "app_db_name" not in data["database"] or not (data["database"]["app_db_name"]):
341
- data["database"]["app_db_name"] = _app_name_to_db_name(data["name"])
342
-
343
- connection_passed_in = data["database"].get("hostname", None) is not None
344
-
345
- dbos_dbport: Optional[int] = None
346
- dbport_env = os.getenv("DBOS_DBPORT")
347
- if dbport_env:
348
- try:
349
- dbos_dbport = int(dbport_env)
350
- except ValueError:
351
- pass
352
-
353
- data["database"]["hostname"] = (
354
- os.getenv("DBOS_DBHOST") or data["database"].get("hostname") or "localhost"
355
- )
356
-
357
- data["database"]["port"] = dbos_dbport or data["database"].get("port") or 5432
358
- data["database"]["username"] = (
359
- os.getenv("DBOS_DBUSER") or data["database"].get("username") or "postgres"
360
- )
361
- data["database"]["password"] = (
362
- os.getenv("DBOS_DBPASSWORD")
363
- or data["database"].get("password")
364
- or os.environ.get("PGPASSWORD")
365
- or "dbos"
366
- )
367
-
368
- if not data["database"].get("app_db_pool_size"):
369
- data["database"]["app_db_pool_size"] = 20
370
- if not data["database"].get("sys_db_pool_size"):
371
- data["database"]["sys_db_pool_size"] = 20
372
- if not data["database"].get("connectionTimeoutMillis"):
373
- data["database"]["connectionTimeoutMillis"] = 10000
374
-
314
+ # Handle admin server config
375
315
  if not data.get("runtimeConfig"):
376
316
  data["runtimeConfig"] = {
377
317
  "run_admin_server": True,
@@ -379,27 +319,118 @@ def process_config(
379
319
  elif "run_admin_server" not in data["runtimeConfig"]:
380
320
  data["runtimeConfig"]["run_admin_server"] = True
381
321
 
322
+ isDebugMode = os.getenv("DBOS_DBHOST") is not None
323
+
324
+ # Ensure database dict exists
325
+ data.setdefault("database", {})
326
+
327
+ # Database URL resolution
328
+ connect_timeout = None
329
+ if data.get("database_url") is not None and data["database_url"] != "":
330
+ # Parse the db string and check required fields
331
+ assert data["database_url"] is not None
332
+ url = make_url(data["database_url"])
333
+ required_fields = [
334
+ ("username", "Username must be specified in the connection URL"),
335
+ ("password", "Password must be specified in the connection URL"),
336
+ ("host", "Host must be specified in the connection URL"),
337
+ ("database", "Database name must be specified in the connection URL"),
338
+ ]
339
+ for field_name, error_message in required_fields:
340
+ field_value = getattr(url, field_name, None)
341
+ if not field_value:
342
+ raise DBOSInitializationError(error_message)
343
+
344
+ if not data["database"].get("sys_db_name"):
345
+ assert url.database is not None
346
+ data["database"]["sys_db_name"] = url.database + SystemSchema.sysdb_suffix
347
+
348
+ # Gather connect_timeout from the URL if provided. It should be used in engine kwargs if not provided there (instead of our default)
349
+ connect_timeout_str = url.query.get("connect_timeout")
350
+ if connect_timeout_str is not None:
351
+ assert isinstance(
352
+ connect_timeout_str, str
353
+ ), "connect_timeout must be a string and defined once in the URL"
354
+ if connect_timeout_str.isdigit():
355
+ connect_timeout = int(connect_timeout_str)
356
+
357
+ # In debug mode perform env vars overrides
358
+ if isDebugMode:
359
+ # Override the username, password, host, and port
360
+ port_str = os.getenv("DBOS_DBPORT")
361
+ port = (
362
+ int(port_str)
363
+ if port_str is not None and port_str.isdigit()
364
+ else url.port
365
+ )
366
+ data["database_url"] = url.set(
367
+ username=os.getenv("DBOS_DBUSER", url.username),
368
+ password=os.getenv("DBOS_DBPASSWORD", url.password),
369
+ host=os.getenv("DBOS_DBHOST", url.host),
370
+ port=port,
371
+ ).render_as_string(hide_password=False)
372
+ else:
373
+ _app_db_name = _app_name_to_db_name(data["name"])
374
+ _password = os.environ.get("PGPASSWORD", "dbos")
375
+ data["database_url"] = (
376
+ f"postgres://postgres:{_password}@localhost:5432/{_app_db_name}?connect_timeout=10&sslmode=prefer"
377
+ )
378
+ if not data["database"].get("sys_db_name"):
379
+ data["database"]["sys_db_name"] = _app_db_name + SystemSchema.sysdb_suffix
380
+ assert data["database_url"] is not None
381
+
382
+ configure_db_engine_parameters(data["database"], connect_timeout=connect_timeout)
383
+
382
384
  # Pretty-print where we've loaded database connection information from, respecting the log level
383
385
  if not silent and logs["logLevel"] == "INFO" or logs["logLevel"] == "DEBUG":
384
- d = data["database"]
385
- conn_string = f"postgresql://{d['username']}:*****@{d['hostname']}:{d['port']}/{d['app_db_name']}"
386
- if os.getenv("DBOS_DBHOST"):
387
- print(
388
- f"[bold blue]Loading database connection string from debug environment variables: {conn_string}[/bold blue]"
389
- )
390
- elif connection_passed_in:
391
- print(
392
- f"[bold blue]Using database connection string: {conn_string}[/bold blue]"
393
- )
394
- else:
395
- print(
396
- f"[bold blue]Using default database connection string: {conn_string}[/bold blue]"
397
- )
386
+ log_url = make_url(data["database_url"]).render_as_string(hide_password=True)
387
+ print(f"[bold blue]Using database connection string: {log_url}[/bold blue]")
398
388
 
399
389
  # Return data as ConfigFile type
400
390
  return data
401
391
 
402
392
 
393
+ def configure_db_engine_parameters(
394
+ data: DatabaseConfig, connect_timeout: Optional[int] = None
395
+ ) -> None:
396
+ """
397
+ Configure SQLAlchemy engine parameters for both user and system databases.
398
+
399
+ If provided, sys_db_pool_size will take precedence over user_kwargs for the system db engine.
400
+
401
+ Args:
402
+ data: Configuration dictionary containing database settings
403
+ """
404
+
405
+ # Configure user database engine parameters
406
+ app_engine_kwargs: dict[str, Any] = {
407
+ "pool_timeout": 30,
408
+ "max_overflow": 0,
409
+ "pool_size": 20,
410
+ }
411
+ # If user-provided kwargs are present, use them instead
412
+ user_kwargs = data.get("db_engine_kwargs")
413
+ if user_kwargs is not None:
414
+ app_engine_kwargs.update(user_kwargs)
415
+
416
+ # If user-provided kwargs do not contain connect_timeout, check if their URL did (this function connect_timeout parameter).
417
+ # Else default to 10
418
+ if "connect_args" not in app_engine_kwargs:
419
+ app_engine_kwargs["connect_args"] = {}
420
+ if "connect_timeout" not in app_engine_kwargs["connect_args"]:
421
+ app_engine_kwargs["connect_args"]["connect_timeout"] = (
422
+ connect_timeout if connect_timeout else 10
423
+ )
424
+
425
+ # Configure system database engine parameters. User-provided sys_db_pool_size takes precedence
426
+ system_engine_kwargs = app_engine_kwargs.copy()
427
+ if data.get("sys_db_pool_size") is not None:
428
+ system_engine_kwargs["pool_size"] = data["sys_db_pool_size"]
429
+
430
+ data["db_engine_kwargs"] = app_engine_kwargs
431
+ data["sys_db_engine_kwargs"] = system_engine_kwargs
432
+
433
+
403
434
  def _is_valid_app_name(name: str) -> bool:
404
435
  name_len = len(name)
405
436
  if name_len < 3 or name_len > 30:
@@ -409,7 +440,7 @@ def _is_valid_app_name(name: str) -> bool:
409
440
 
410
441
 
411
442
  def _app_name_to_db_name(app_name: str) -> str:
412
- name = app_name.replace("-", "_")
443
+ name = app_name.replace("-", "_").replace(" ", "_").lower()
413
444
  return name if not name[0].isdigit() else f"_{name}"
414
445
 
415
446
 
@@ -421,7 +452,7 @@ def set_env_vars(config: ConfigFile) -> None:
421
452
 
422
453
  def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
423
454
  # Load the DBOS configuration file and force the use of:
424
- # 1. The database connection parameters (sub the file data to the provided config)
455
+ # 1. The database url provided by DBOS_DATABASE_URL
425
456
  # 2. OTLP traces endpoints (add the config data to the provided config)
426
457
  # 3. Use the application name from the file. This is a defensive measure to ensure the application name is whatever it was registered with in the cloud
427
458
  # 4. Remove admin_port is provided in code
@@ -436,22 +467,19 @@ def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
436
467
  # Name
437
468
  provided_config["name"] = config_from_file["name"]
438
469
 
439
- # Database config. Note we disregard a potential database_url in config_from_file because it is not expected from DBOS Cloud
470
+ # Database config. Expects DBOS_DATABASE_URL to be set
440
471
  if "database" not in provided_config:
441
472
  provided_config["database"] = {}
442
- provided_config["database"]["hostname"] = config_from_file["database"]["hostname"]
443
- provided_config["database"]["port"] = config_from_file["database"]["port"]
444
- provided_config["database"]["username"] = config_from_file["database"]["username"]
445
- provided_config["database"]["password"] = config_from_file["database"]["password"]
446
- provided_config["database"]["app_db_name"] = config_from_file["database"][
447
- "app_db_name"
448
- ]
449
473
  provided_config["database"]["sys_db_name"] = config_from_file["database"][
450
474
  "sys_db_name"
451
475
  ]
452
- provided_config["database"]["ssl"] = config_from_file["database"]["ssl"]
453
- if "ssl_ca" in config_from_file["database"]:
454
- provided_config["database"]["ssl_ca"] = config_from_file["database"]["ssl_ca"]
476
+
477
+ db_url = os.environ.get("DBOS_DATABASE_URL")
478
+ if db_url is None:
479
+ raise DBOSInitializationError(
480
+ "DBOS_DATABASE_URL environment variable is not set. This is required to connect to the database."
481
+ )
482
+ provided_config["database_url"] = db_url
455
483
 
456
484
  # Telemetry config
457
485
  if "telemetry" not in provided_config or provided_config["telemetry"] is None:
dbos/_sys_db.py CHANGED
@@ -5,7 +5,6 @@ import os
5
5
  import re
6
6
  import threading
7
7
  import time
8
- import uuid
9
8
  from enum import Enum
10
9
  from typing import (
11
10
  TYPE_CHECKING,
@@ -28,11 +27,10 @@ from alembic.config import Config
28
27
  from sqlalchemy.exc import DBAPIError
29
28
  from sqlalchemy.sql import func
30
29
 
31
- from dbos._utils import INTERNAL_QUEUE_NAME, GlobalParams
30
+ from dbos._utils import INTERNAL_QUEUE_NAME
32
31
 
33
32
  from . import _serialization
34
33
  from ._context import get_local_dbos_context
35
- from ._dbos_config import ConfigFile, DatabaseConfig
36
34
  from ._error import (
37
35
  DBOSConflictingWorkflowError,
38
36
  DBOSDeadLetterQueueError,
@@ -227,26 +225,28 @@ _dbos_null_topic = "__null__topic__"
227
225
 
228
226
  class SystemDatabase:
229
227
 
230
- def __init__(self, database: DatabaseConfig, *, debug_mode: bool = False):
231
- sysdb_name = (
232
- database["sys_db_name"]
233
- if "sys_db_name" in database and database["sys_db_name"]
234
- else database["app_db_name"] + SystemSchema.sysdb_suffix
235
- )
228
+ def __init__(
229
+ self,
230
+ *,
231
+ database_url: str,
232
+ engine_kwargs: Dict[str, Any],
233
+ sys_db_name: Optional[str] = None,
234
+ debug_mode: bool = False,
235
+ ):
236
+ # Set driver
237
+ system_db_url = sa.make_url(database_url).set(drivername="postgresql+psycopg")
238
+ # Resolve system database name
239
+ sysdb_name = sys_db_name
240
+ if not sysdb_name:
241
+ assert system_db_url.database is not None
242
+ sysdb_name = system_db_url.database + SystemSchema.sysdb_suffix
243
+ system_db_url = system_db_url.set(database=sysdb_name)
236
244
 
237
245
  if not debug_mode:
238
246
  # If the system database does not already exist, create it
239
- postgres_db_url = sa.URL.create(
240
- "postgresql+psycopg",
241
- username=database["username"],
242
- password=database["password"],
243
- host=database["hostname"],
244
- port=database["port"],
245
- database="postgres",
246
- # fills the "application_name" column in pg_stat_activity
247
- query={"application_name": f"dbos_transact_{GlobalParams.executor_id}"},
247
+ engine = sa.create_engine(
248
+ system_db_url.set(database="postgres"), **engine_kwargs
248
249
  )
249
- engine = sa.create_engine(postgres_db_url)
250
250
  with engine.connect() as conn:
251
251
  conn.execution_options(isolation_level="AUTOCOMMIT")
252
252
  if not conn.execute(
@@ -257,36 +257,6 @@ class SystemDatabase:
257
257
  conn.execute(sa.text(f"CREATE DATABASE {sysdb_name}"))
258
258
  engine.dispose()
259
259
 
260
- system_db_url = sa.URL.create(
261
- "postgresql+psycopg",
262
- username=database["username"],
263
- password=database["password"],
264
- host=database["hostname"],
265
- port=database["port"],
266
- database=sysdb_name,
267
- # fills the "application_name" column in pg_stat_activity
268
- query={"application_name": f"dbos_transact_{GlobalParams.executor_id}"},
269
- )
270
-
271
- # Create a connection pool for the system database
272
- pool_size = database.get("sys_db_pool_size")
273
- if pool_size is None:
274
- pool_size = 20
275
-
276
- engine_kwargs = database.get("db_engine_kwargs")
277
- if engine_kwargs is None:
278
- engine_kwargs = {}
279
-
280
- # Respect user-provided values. Otherwise, set defaults.
281
- if "pool_size" not in engine_kwargs:
282
- engine_kwargs["pool_size"] = pool_size
283
- if "max_overflow" not in engine_kwargs:
284
- engine_kwargs["max_overflow"] = 0
285
- if "pool_timeout" not in engine_kwargs:
286
- engine_kwargs["pool_timeout"] = 30
287
- if "connect_args" not in engine_kwargs:
288
- engine_kwargs["connect_args"] = {"connect_timeout": 10}
289
-
290
260
  self.engine = sa.create_engine(
291
261
  system_db_url,
292
262
  **engine_kwargs,
@@ -1958,7 +1928,10 @@ class SystemDatabase:
1958
1928
  def reset_system_database(postgres_db_url: sa.URL, sysdb_name: str) -> None:
1959
1929
  try:
1960
1930
  # Connect to postgres default database
1961
- engine = sa.create_engine(postgres_db_url)
1931
+ engine = sa.create_engine(
1932
+ postgres_db_url.set(drivername="postgresql+psycopg"),
1933
+ connect_args={"connect_timeout": 10},
1934
+ )
1962
1935
 
1963
1936
  with engine.connect() as conn:
1964
1937
  # Set autocommit required for database dropping
dbos/cli/cli.py CHANGED
@@ -27,7 +27,7 @@ from ..cli._github_init import create_template_from_github
27
27
  from ._template_init import copy_template, get_project_name, get_templates_directory
28
28
 
29
29
 
30
- def start_client(db_url: Optional[str] = None) -> DBOSClient:
30
+ def _get_db_url(db_url: Optional[str]) -> str:
31
31
  database_url = db_url
32
32
  if database_url is None:
33
33
  database_url = os.getenv("DBOS_DATABASE_URL")
@@ -35,7 +35,11 @@ def start_client(db_url: Optional[str] = None) -> DBOSClient:
35
35
  raise ValueError(
36
36
  "Missing database URL: please set it using the --db-url flag or the DBOS_DATABASE_URL environment variable."
37
37
  )
38
+ return database_url
39
+
38
40
 
41
+ def start_client(db_url: Optional[str] = None) -> DBOSClient:
42
+ database_url = _get_db_url(db_url)
39
43
  return DBOSClient(database_url=database_url)
40
44
 
41
45
 
@@ -206,14 +210,30 @@ def init(
206
210
  @app.command(
207
211
  help="Run your database schema migrations using the migration commands in 'dbos-config.yaml'"
208
212
  )
209
- def migrate() -> None:
210
- config = load_config()
211
- if not config["database"]["password"]:
212
- typer.echo(
213
- "DBOS configuration does not contain database password, please check your config file and retry!"
214
- )
215
- raise typer.Exit(code=1)
216
- app_db_name = config["database"]["app_db_name"]
213
+ def migrate(
214
+ db_url: Annotated[
215
+ typing.Optional[str],
216
+ typer.Option(
217
+ "--db-url",
218
+ "-D",
219
+ help="Your DBOS application database URL",
220
+ ),
221
+ ] = None,
222
+ sys_db_name: Annotated[
223
+ typing.Optional[str],
224
+ typer.Option(
225
+ "--sys-db-name",
226
+ "-s",
227
+ help="Specify the name of the system database to reset",
228
+ ),
229
+ ] = None,
230
+ ) -> None:
231
+ config = load_config(run_process_config=False, silent=True)
232
+ connection_string = _get_db_url(db_url)
233
+ app_db_name = sa.make_url(connection_string).database
234
+ assert app_db_name is not None, "Database name is required in URL"
235
+ if sys_db_name is None:
236
+ sys_db_name = app_db_name + SystemSchema.sysdb_suffix
217
237
 
218
238
  typer.echo(f"Starting schema migration for database {app_db_name}")
219
239
 
@@ -221,8 +241,23 @@ def migrate() -> None:
221
241
  app_db = None
222
242
  sys_db = None
223
243
  try:
224
- sys_db = SystemDatabase(config["database"])
225
- app_db = ApplicationDatabase(config["database"])
244
+ sys_db = SystemDatabase(
245
+ database_url=connection_string,
246
+ engine_kwargs={
247
+ "pool_timeout": 30,
248
+ "max_overflow": 0,
249
+ "pool_size": 2,
250
+ },
251
+ sys_db_name=sys_db_name,
252
+ )
253
+ app_db = ApplicationDatabase(
254
+ database_url=connection_string,
255
+ engine_kwargs={
256
+ "pool_timeout": 30,
257
+ "max_overflow": 0,
258
+ "pool_size": 2,
259
+ },
260
+ )
226
261
  except Exception as e:
227
262
  typer.echo(f"DBOS system schema migration failed: {e}")
228
263
  finally:
@@ -234,6 +269,9 @@ def migrate() -> None:
234
269
  # Next, run any custom migration commands specified in the configuration
235
270
  typer.echo("Executing migration commands from 'dbos-config.yaml'")
236
271
  try:
272
+ # handle the case where the user has not specified migrations commands
273
+ if "database" not in config:
274
+ config["database"] = {}
237
275
  migrate_commands = (
238
276
  config["database"]["migrate"]
239
277
  if "migrate" in config["database"] and config["database"]["migrate"]
@@ -283,17 +321,21 @@ def reset(
283
321
  typer.echo("Operation cancelled.")
284
322
  raise typer.Exit()
285
323
  try:
286
- client = start_client(db_url=db_url)
287
- pg_db_url = sa.make_url(client._db_url).set(drivername="postgresql+psycopg")
324
+ # Make a SA url out of the user-provided URL and verify a database name is present
325
+ database_url = _get_db_url(db_url)
326
+ pg_db_url = sa.make_url(database_url)
288
327
  assert (
289
328
  pg_db_url.database is not None
290
329
  ), f"Database name is required in URL: {pg_db_url.render_as_string(hide_password=True)}"
330
+ # Resolve system database name
291
331
  sysdb_name = (
292
332
  sys_db_name
293
333
  if sys_db_name
294
334
  else (pg_db_url.database + SystemSchema.sysdb_suffix)
295
335
  )
296
- reset_system_database(pg_db_url.set(database="postgres"), sysdb_name)
336
+ reset_system_database(
337
+ postgres_db_url=pg_db_url.set(database="postgres"), sysdb_name=sysdb_name
338
+ )
297
339
  except sa.exc.SQLAlchemyError as e:
298
340
  typer.echo(f"Error resetting system database: {str(e)}")
299
341
  return
@@ -70,16 +70,6 @@
70
70
  "description": "If using SSL/TLS to securely connect to a database, path to an SSL root certificate file. DEPRECATED: Use database_url instead",
71
71
  "deprecated": true
72
72
  },
73
- "app_db_client": {
74
- "type": "string",
75
- "description": "Specify the database client to use to connect to the application database",
76
- "enum": [
77
- "pg-node",
78
- "prisma",
79
- "typeorm",
80
- "knex"
81
- ]
82
- },
83
73
  "migrate": {
84
74
  "type": "array",
85
75
  "description": "Specify a list of user DB migration commands to run"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.28.0a12
3
+ Version: 0.28.0a15
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,20 +1,20 @@
1
- dbos-0.28.0a12.dist-info/METADATA,sha256=nxjIfoX9CdMPTCpI-L-iIAHRyTjDDbjJKNbwI6gtiqc,13269
2
- dbos-0.28.0a12.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- dbos-0.28.0a12.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-0.28.0a12.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-0.28.0a15.dist-info/METADATA,sha256=zpjEzcFWmk2kxievyVaZBkvakcjpprw6H8842nJMuPE,13269
2
+ dbos-0.28.0a15.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ dbos-0.28.0a15.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-0.28.0a15.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
7
  dbos/_admin_server.py,sha256=A_28_nJ1nBBYDmCxtklJR9O2v14JRMtD1rAo_D4y8Kc,9764
8
- dbos/_app_db.py,sha256=3j8_5-MlSDY0otLRszFE-GfenU6JC20fcfSL-drSNYk,11800
8
+ dbos/_app_db.py,sha256=56jqU0oxcIkMscOg6DxC8dZ0uEJwG7iMu3aXYs2u66k,10428
9
9
  dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
10
- dbos/_client.py,sha256=PPktnvaLfl8Fj27YoOjy_kfgeQm_c3QQZztPyts5GZo,13924
11
- dbos/_conductor/conductor.py,sha256=qP3fTe74gdaIN9SU9nCn2B5GYMBnhQ6shEhGezEcSPg,19693
12
- dbos/_conductor/protocol.py,sha256=jwX8ZjmAIlXu1vw9R3b5PfHSNdwofeYOKj8rkfAFVg0,6630
10
+ dbos/_client.py,sha256=aPOAVWsH7uovSIsgJRamksra9y9-BVW1Jw4Gg5WjuZA,14114
11
+ dbos/_conductor/conductor.py,sha256=o0IaZjwnZ2TOyHeP2H4iSX6UnXLXQ4uODvWAKD9hHMs,21703
12
+ dbos/_conductor/protocol.py,sha256=wgOFZxmS81bv0WCB9dAyg0s6QzldpzVKQDoSPeaX0Ws,6967
13
13
  dbos/_context.py,sha256=Ly1CXF1nWxICQgIpDZSaONGlz1yERBs63gqmR-yqCzM,24476
14
14
  dbos/_core.py,sha256=UDpSgRA9m_YuViNXR9tVgNFLC-zxKZPxjlkj2a-Kj00,48317
15
15
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
16
- dbos/_dbos.py,sha256=tPpf_X9oLtpPICryXDMc589XuFeJtZ4cIq7vmsNDcls,46220
17
- dbos/_dbos_config.py,sha256=pgzE5UP3LV_nhwIV4d1-W9q2LumQb1kla75xdx9mBcI,20136
16
+ dbos/_dbos.py,sha256=7fQPKfaePD3HwxSjBhziJcVd2heLKefe92skgmuHr34,46275
17
+ dbos/_dbos_config.py,sha256=IufNrIC-M2xSNTXyT_KXlEdfB3j03pPLv_nE0fEq4_U,20955
18
18
  dbos/_debug.py,sha256=MNlQVZ6TscGCRQeEEL0VE8Uignvr6dPeDDDefS3xgIE,1823
19
19
  dbos/_docker_pg_helper.py,sha256=tLJXWqZ4S-ExcaPnxg_i6cVxL6ZxrYlZjaGsklY-s2I,6115
20
20
  dbos/_error.py,sha256=EN4eVBjMT3k7O7hfqJl6mIf4sxWPsiAOM086yhcGH_g,8012
@@ -47,7 +47,7 @@ dbos/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  dbos/_schemas/application_database.py,sha256=SypAS9l9EsaBHFn9FR8jmnqt01M74d9AF1AMa4m2hhI,1040
48
48
  dbos/_schemas/system_database.py,sha256=3Z0L72bOgHnusK1hBaETWU9RfiLBP0QnS-fdu41i0yY,5835
49
49
  dbos/_serialization.py,sha256=bWuwhXSQcGmiazvhJHA5gwhrRWxtmFmcCFQSDJnqqkU,3666
50
- dbos/_sys_db.py,sha256=lceVny4RzQP7DeEJ8CpQ0crg8cJmuTznUsiGmv1oM-E,82486
50
+ dbos/_sys_db.py,sha256=VJ-wLjukSVMXx2EGyc3ZTL9GFDpHHdFU4iXRSFobrRU,81229
51
51
  dbos/_templates/dbos-db-starter/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
52
52
  dbos/_templates/dbos-db-starter/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  dbos/_templates/dbos-db-starter/__package/main.py.dbos,sha256=aQnBPSSQpkB8ERfhf7gB7P9tsU6OPKhZscfeh0yiaD8,2702
@@ -63,8 +63,8 @@ dbos/_utils.py,sha256=UbpMYRBSyvJqdXeWAnfSw8xXM1R1mfnyl1oTunhEjJM,513
63
63
  dbos/_workflow_commands.py,sha256=2E8FRUv_nLYkpBTwfhh_ELhySYpMrm8qGB9J44g6DSE,3872
64
64
  dbos/cli/_github_init.py,sha256=Y_bDF9gfO2jB1id4FV5h1oIxEJRWyqVjhb7bNEa5nQ0,3224
65
65
  dbos/cli/_template_init.py,sha256=7JBcpMqP1r2mfCnvWatu33z8ctEGHJarlZYKgB83cXE,2972
66
- dbos/cli/cli.py,sha256=SFmaHlCHtOojETd-P8BpbENQvwQoyq2l2yXZhSVo-50,18892
67
- dbos/dbos-config.schema.json,sha256=8KcwJb_sQc4-6tQG2TLmjE_nratfrQa0qVLl9XPsvWE,6367
66
+ dbos/cli/cli.py,sha256=YPXZyAD3GIh1cw_kBTAcJxUGO6OgBHWhjQLVe66AY8k,20143
67
+ dbos/dbos-config.schema.json,sha256=CjaspeYmOkx6Ip_pcxtmfXJTn_YGdSx_0pcPBF7KZmo,6060
68
68
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
69
69
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
70
- dbos-0.28.0a12.dist-info/RECORD,,
70
+ dbos-0.28.0a15.dist-info/RECORD,,