dbos 0.28.0a14__tar.gz → 0.28.0a18__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.
Files changed (105) hide show
  1. {dbos-0.28.0a14 → dbos-0.28.0a18}/PKG-INFO +1 -1
  2. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_app_db.py +14 -49
  3. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_client.py +17 -6
  4. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_dbos.py +18 -21
  5. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_dbos_config.py +134 -103
  6. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_logger.py +2 -1
  7. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_registrations.py +1 -1
  8. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_sys_db.py +24 -51
  9. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/cli/cli.py +56 -14
  10. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/dbos-config.schema.json +0 -10
  11. {dbos-0.28.0a14 → dbos-0.28.0a18}/pyproject.toml +1 -1
  12. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/conftest.py +32 -15
  13. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_config.py +436 -423
  14. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_docker_secrets.py +20 -185
  15. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_workflow_management.py +5 -0
  16. {dbos-0.28.0a14 → dbos-0.28.0a18}/LICENSE +0 -0
  17. {dbos-0.28.0a14 → dbos-0.28.0a18}/README.md +0 -0
  18. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/__init__.py +0 -0
  19. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/__main__.py +0 -0
  20. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_admin_server.py +0 -0
  21. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_classproperty.py +0 -0
  22. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_conductor/conductor.py +0 -0
  23. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_conductor/protocol.py +0 -0
  24. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_context.py +0 -0
  25. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_core.py +0 -0
  26. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_croniter.py +0 -0
  27. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_debug.py +0 -0
  28. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_docker_pg_helper.py +0 -0
  29. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_error.py +0 -0
  30. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_event_loop.py +0 -0
  31. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_fastapi.py +0 -0
  32. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_flask.py +0 -0
  33. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_kafka.py +0 -0
  34. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_kafka_message.py +0 -0
  35. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/env.py +0 -0
  36. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/script.py.mako +0 -0
  37. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  38. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
  39. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  40. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  41. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
  42. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
  43. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  44. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  45. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  46. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  47. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  48. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_outcome.py +0 -0
  49. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_queue.py +0 -0
  50. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_recovery.py +0 -0
  51. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_roles.py +0 -0
  52. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_scheduler.py +0 -0
  53. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_schemas/__init__.py +0 -0
  54. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_schemas/application_database.py +0 -0
  55. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_schemas/system_database.py +0 -0
  56. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_serialization.py +0 -0
  57. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/README.md +0 -0
  58. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  59. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
  60. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  61. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  62. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  63. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  64. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  65. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  66. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  67. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_tracer.py +0 -0
  68. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_utils.py +0 -0
  69. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/_workflow_commands.py +0 -0
  70. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/cli/_github_init.py +0 -0
  71. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/cli/_template_init.py +0 -0
  72. {dbos-0.28.0a14 → dbos-0.28.0a18}/dbos/py.typed +0 -0
  73. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/__init__.py +0 -0
  74. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/atexit_no_ctor.py +0 -0
  75. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/atexit_no_launch.py +0 -0
  76. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/classdefs.py +0 -0
  77. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/client_collateral.py +0 -0
  78. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/client_worker.py +0 -0
  79. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/dupname_classdefs1.py +0 -0
  80. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/dupname_classdefsa.py +0 -0
  81. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/more_classdefs.py +0 -0
  82. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/queuedworkflow.py +0 -0
  83. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_admin_server.py +0 -0
  84. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_async.py +0 -0
  85. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_classdecorators.py +0 -0
  86. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_client.py +0 -0
  87. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_concurrency.py +0 -0
  88. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_croniter.py +0 -0
  89. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_dbos.py +0 -0
  90. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_debug.py +0 -0
  91. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_failures.py +0 -0
  92. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_fastapi.py +0 -0
  93. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_fastapi_roles.py +0 -0
  94. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_flask.py +0 -0
  95. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_kafka.py +0 -0
  96. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_outcome.py +0 -0
  97. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_package.py +0 -0
  98. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_queue.py +0 -0
  99. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_scheduler.py +0 -0
  100. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_schema_migration.py +0 -0
  101. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_singleton.py +0 -0
  102. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_spans.py +0 -0
  103. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_sqlalchemy.py +0 -0
  104. {dbos-0.28.0a14 → dbos-0.28.0a18}/tests/test_workflow_introspection.py +0 -0
  105. {dbos-0.28.0a14 → dbos-0.28.0a18}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.28.0a14
3
+ Version: 0.28.0a18
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -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,
@@ -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:
@@ -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
@@ -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
@@ -7,6 +7,7 @@ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
7
7
  from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
8
8
  from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
9
9
  from opentelemetry.sdk.resources import Resource
10
+ from opentelemetry.semconv.resource import ResourceAttributes
10
11
  from opentelemetry.trace.span import format_trace_id
11
12
 
12
13
  from dbos._utils import GlobalParams
@@ -77,7 +78,7 @@ def config_logger(config: "ConfigFile") -> None:
77
78
  log_provider = PatchedOTLPLoggerProvider(
78
79
  Resource.create(
79
80
  attributes={
80
- "service.name": "dbos-application",
81
+ ResourceAttributes.SERVICE_NAME: config["name"],
81
82
  }
82
83
  )
83
84
  )
@@ -4,7 +4,7 @@ from enum import Enum
4
4
  from types import FunctionType
5
5
  from typing import Any, Callable, List, Literal, Optional, Tuple, Type, cast
6
6
 
7
- DEFAULT_MAX_RECOVERY_ATTEMPTS = 50
7
+ DEFAULT_MAX_RECOVERY_ATTEMPTS = 100
8
8
 
9
9
 
10
10
  def get_dbos_func_name(f: Any) -> str: