dbos 1.0.0__py3-none-any.whl → 1.1.0a2__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
@@ -8,6 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker
8
8
 
9
9
  from . import _serialization
10
10
  from ._error import DBOSUnexpectedStepError, DBOSWorkflowConflictIDError
11
+ from ._logger import dbos_logger
11
12
  from ._schemas.application_database import ApplicationSchema
12
13
  from ._sys_db import StepInfo
13
14
 
@@ -39,21 +40,6 @@ class ApplicationDatabase:
39
40
  ):
40
41
  app_db_url = sa.make_url(database_url).set(drivername="postgresql+psycopg")
41
42
 
42
- # If the application database does not already exist, create it
43
- if not debug_mode:
44
- postgres_db_engine = sa.create_engine(
45
- app_db_url.set(database="postgres"),
46
- **engine_kwargs,
47
- )
48
- with postgres_db_engine.connect() as conn:
49
- conn.execution_options(isolation_level="AUTOCOMMIT")
50
- if not conn.execute(
51
- sa.text("SELECT 1 FROM pg_database WHERE datname=:db_name"),
52
- parameters={"db_name": app_db_url.database},
53
- ).scalar():
54
- conn.execute(sa.text(f"CREATE DATABASE {app_db_url.database}"))
55
- postgres_db_engine.dispose()
56
-
57
43
  if engine_kwargs is None:
58
44
  engine_kwargs = {}
59
45
 
@@ -61,40 +47,61 @@ class ApplicationDatabase:
61
47
  app_db_url,
62
48
  **engine_kwargs,
63
49
  )
50
+ self._engine_kwargs = engine_kwargs
64
51
  self.sessionmaker = sessionmaker(bind=self.engine)
65
52
  self.debug_mode = debug_mode
66
53
 
54
+ def run_migrations(self) -> None:
55
+ if self.debug_mode:
56
+ dbos_logger.warning(
57
+ "Application database migrations are skipped in debug mode."
58
+ )
59
+ return
60
+ # Check if the database exists
61
+ app_db_url = self.engine.url
62
+ postgres_db_engine = sa.create_engine(
63
+ app_db_url.set(database="postgres"),
64
+ **self._engine_kwargs,
65
+ )
66
+ with postgres_db_engine.connect() as conn:
67
+ conn.execution_options(isolation_level="AUTOCOMMIT")
68
+ if not conn.execute(
69
+ sa.text("SELECT 1 FROM pg_database WHERE datname=:db_name"),
70
+ parameters={"db_name": app_db_url.database},
71
+ ).scalar():
72
+ conn.execute(sa.text(f"CREATE DATABASE {app_db_url.database}"))
73
+ postgres_db_engine.dispose()
74
+
67
75
  # Create the dbos schema and transaction_outputs table in the application database
68
- if not debug_mode:
69
- with self.engine.begin() as conn:
70
- schema_creation_query = sa.text(
71
- f"CREATE SCHEMA IF NOT EXISTS {ApplicationSchema.schema}"
72
- )
73
- conn.execute(schema_creation_query)
76
+ with self.engine.begin() as conn:
77
+ schema_creation_query = sa.text(
78
+ f"CREATE SCHEMA IF NOT EXISTS {ApplicationSchema.schema}"
79
+ )
80
+ conn.execute(schema_creation_query)
74
81
 
75
- inspector = inspect(self.engine)
76
- if not inspector.has_table(
82
+ inspector = inspect(self.engine)
83
+ if not inspector.has_table(
84
+ "transaction_outputs", schema=ApplicationSchema.schema
85
+ ):
86
+ ApplicationSchema.metadata_obj.create_all(self.engine)
87
+ else:
88
+ columns = inspector.get_columns(
77
89
  "transaction_outputs", schema=ApplicationSchema.schema
78
- ):
79
- ApplicationSchema.metadata_obj.create_all(self.engine)
80
- else:
81
- columns = inspector.get_columns(
82
- "transaction_outputs", schema=ApplicationSchema.schema
83
- )
84
- column_names = [col["name"] for col in columns]
90
+ )
91
+ column_names = [col["name"] for col in columns]
85
92
 
86
- if "function_name" not in column_names:
87
- # Column missing, alter table to add it
88
- with self.engine.connect() as conn:
89
- conn.execute(
90
- text(
91
- f"""
92
- ALTER TABLE {ApplicationSchema.schema}.transaction_outputs
93
- ADD COLUMN function_name TEXT NOT NULL DEFAULT '';
94
- """
95
- )
93
+ if "function_name" not in column_names:
94
+ # Column missing, alter table to add it
95
+ with self.engine.connect() as conn:
96
+ conn.execute(
97
+ text(
98
+ f"""
99
+ ALTER TABLE {ApplicationSchema.schema}.transaction_outputs
100
+ ADD COLUMN function_name TEXT NOT NULL DEFAULT '';
101
+ """
96
102
  )
97
- conn.commit()
103
+ )
104
+ conn.commit()
98
105
 
99
106
  def destroy(self) -> None:
100
107
  self.engine.dispose()
dbos/_client.py CHANGED
@@ -99,6 +99,7 @@ class WorkflowHandleClientAsyncPolling(Generic[R]):
99
99
 
100
100
  class DBOSClient:
101
101
  def __init__(self, database_url: str, *, system_database: Optional[str] = None):
102
+ # We only create database connections but do not run migrations
102
103
  self._sys_db = SystemDatabase(
103
104
  database_url=database_url,
104
105
  engine_kwargs={
dbos/_dbos.py CHANGED
@@ -433,6 +433,10 @@ class DBOS:
433
433
  if debug_mode:
434
434
  return
435
435
 
436
+ # Run migrations for the system and application databases
437
+ self._sys_db.run_migrations()
438
+ self._app_db.run_migrations()
439
+
436
440
  admin_port = self._config.get("runtimeConfig", {}).get("admin_port")
437
441
  if admin_port is None:
438
442
  admin_port = 3001
dbos/_sys_db.py CHANGED
@@ -241,34 +241,63 @@ class SystemDatabase:
241
241
  sysdb_name = system_db_url.database + SystemSchema.sysdb_suffix
242
242
  system_db_url = system_db_url.set(database=sysdb_name)
243
243
 
244
- if not debug_mode:
245
- # If the system database does not already exist, create it
246
- engine = sa.create_engine(
247
- system_db_url.set(database="postgres"), **engine_kwargs
248
- )
249
- with engine.connect() as conn:
250
- conn.execution_options(isolation_level="AUTOCOMMIT")
251
- if not conn.execute(
252
- sa.text("SELECT 1 FROM pg_database WHERE datname=:db_name"),
253
- parameters={"db_name": sysdb_name},
254
- ).scalar():
255
- dbos_logger.info(f"Creating system database {sysdb_name}")
256
- conn.execute(sa.text(f"CREATE DATABASE {sysdb_name}"))
257
- engine.dispose()
258
-
259
244
  self.engine = sa.create_engine(
260
245
  system_db_url,
261
246
  **engine_kwargs,
262
247
  )
248
+ self._engine_kwargs = engine_kwargs
249
+
250
+ self.notification_conn: Optional[psycopg.connection.Connection] = None
251
+ self.notifications_map: Dict[str, threading.Condition] = {}
252
+ self.workflow_events_map: Dict[str, threading.Condition] = {}
253
+
254
+ # Now we can run background processes
255
+ self._run_background_processes = True
256
+ self._debug_mode = debug_mode
257
+
258
+ # Run migrations
259
+ def run_migrations(self) -> None:
260
+ if self._debug_mode:
261
+ dbos_logger.warning("System database migrations are skipped in debug mode.")
262
+ return
263
+ system_db_url = self.engine.url
264
+ sysdb_name = system_db_url.database
265
+ # If the system database does not already exist, create it
266
+ engine = sa.create_engine(
267
+ system_db_url.set(database="postgres"), **self._engine_kwargs
268
+ )
269
+ with engine.connect() as conn:
270
+ conn.execution_options(isolation_level="AUTOCOMMIT")
271
+ if not conn.execute(
272
+ sa.text("SELECT 1 FROM pg_database WHERE datname=:db_name"),
273
+ parameters={"db_name": sysdb_name},
274
+ ).scalar():
275
+ dbos_logger.info(f"Creating system database {sysdb_name}")
276
+ conn.execute(sa.text(f"CREATE DATABASE {sysdb_name}"))
277
+ engine.dispose()
263
278
 
264
279
  # Run a schema migration for the system database
265
- if not debug_mode:
266
- migration_dir = os.path.join(
267
- os.path.dirname(os.path.realpath(__file__)), "_migrations"
280
+ migration_dir = os.path.join(
281
+ os.path.dirname(os.path.realpath(__file__)), "_migrations"
282
+ )
283
+ alembic_cfg = Config()
284
+ alembic_cfg.set_main_option("script_location", migration_dir)
285
+ logging.getLogger("alembic").setLevel(logging.WARNING)
286
+ # Alembic requires the % in URL-escaped parameters to itself be escaped to %%.
287
+ escaped_conn_string = re.sub(
288
+ r"%(?=[0-9A-Fa-f]{2})",
289
+ "%%",
290
+ self.engine.url.render_as_string(hide_password=False),
291
+ )
292
+ alembic_cfg.set_main_option("sqlalchemy.url", escaped_conn_string)
293
+ try:
294
+ command.upgrade(alembic_cfg, "head")
295
+ except Exception as e:
296
+ dbos_logger.warning(
297
+ f"Exception during system database construction. This is most likely because the system database was configured using a later version of DBOS: {e}"
268
298
  )
269
299
  alembic_cfg = Config()
270
300
  alembic_cfg.set_main_option("script_location", migration_dir)
271
- logging.getLogger("alembic").setLevel(logging.WARNING)
272
301
  # Alembic requires the % in URL-escaped parameters to itself be escaped to %%.
273
302
  escaped_conn_string = re.sub(
274
303
  r"%(?=[0-9A-Fa-f]{2})",
@@ -282,29 +311,6 @@ class SystemDatabase:
282
311
  dbos_logger.warning(
283
312
  f"Exception during system database construction. This is most likely because the system database was configured using a later version of DBOS: {e}"
284
313
  )
285
- alembic_cfg = Config()
286
- alembic_cfg.set_main_option("script_location", migration_dir)
287
- # Alembic requires the % in URL-escaped parameters to itself be escaped to %%.
288
- escaped_conn_string = re.sub(
289
- r"%(?=[0-9A-Fa-f]{2})",
290
- "%%",
291
- self.engine.url.render_as_string(hide_password=False),
292
- )
293
- alembic_cfg.set_main_option("sqlalchemy.url", escaped_conn_string)
294
- try:
295
- command.upgrade(alembic_cfg, "head")
296
- except Exception as e:
297
- dbos_logger.warning(
298
- f"Exception during system database construction. This is most likely because the system database was configured using a later version of DBOS: {e}"
299
- )
300
-
301
- self.notification_conn: Optional[psycopg.connection.Connection] = None
302
- self.notifications_map: Dict[str, threading.Condition] = {}
303
- self.workflow_events_map: Dict[str, threading.Condition] = {}
304
-
305
- # Now we can run background processes
306
- self._run_background_processes = True
307
- self._debug_mode = debug_mode
308
314
 
309
315
  # Destroy the pool when finished
310
316
  def destroy(self) -> None:
dbos/cli/cli.py CHANGED
@@ -12,7 +12,7 @@ import sqlalchemy as sa
12
12
  import typer
13
13
  from rich import print
14
14
  from rich.prompt import IntPrompt
15
- from typing_extensions import Annotated
15
+ from typing_extensions import Annotated, List
16
16
 
17
17
  from dbos._debug import debug_workflow, parse_start_command
18
18
 
@@ -147,55 +147,16 @@ def init(
147
147
  ] = False,
148
148
  ) -> None:
149
149
  try:
150
-
151
150
  git_templates = ["dbos-toolbox", "dbos-app-starter", "dbos-cron-starter"]
152
151
  templates_dir = get_templates_directory()
153
- templates = git_templates + [
154
- x.name for x in os.scandir(templates_dir) if x.is_dir()
155
- ]
156
-
157
- if config and template is None:
158
- template = templates[-1]
159
152
 
160
- if template:
161
- if template not in templates:
162
- raise Exception(f"Template {template} not found in {templates_dir}")
163
- else:
164
- print("\n[bold]Available templates:[/bold]")
165
- for idx, template_name in enumerate(templates, 1):
166
- print(f" {idx}. {template_name}")
167
- while True:
168
- try:
169
- choice = IntPrompt.ask(
170
- "\nSelect template number",
171
- show_choices=False,
172
- show_default=False,
173
- )
174
- if 1 <= choice <= len(templates):
175
- template = templates[choice - 1]
176
- break
177
- else:
178
- print(
179
- "[red]Invalid selection. Please choose a number from the list.[/red]"
180
- )
181
- except (KeyboardInterrupt, EOFError):
182
- raise typer.Abort()
183
- except ValueError:
184
- print("[red]Please enter a valid number.[/red]")
185
-
186
- if template in git_templates:
187
- project_name = template
188
- else:
189
- if project_name is None:
190
- project_name = typing.cast(
191
- str,
192
- typer.prompt("What is your project's name?", get_project_name()),
193
- )
194
-
195
- if not _is_valid_app_name(project_name):
196
- raise Exception(
197
- f"{project_name} is an invalid DBOS app name. App names must be between 3 and 30 characters long and contain only lowercase letters, numbers, dashes, and underscores."
198
- )
153
+ project_name, template = _resolve_project_name_and_template(
154
+ project_name=project_name,
155
+ template=template,
156
+ config=config,
157
+ git_templates=git_templates,
158
+ templates_dir=templates_dir,
159
+ )
199
160
 
200
161
  if template in git_templates:
201
162
  create_template_from_github(app_name=project_name, template_name=template)
@@ -207,6 +168,67 @@ def init(
207
168
  print(f"[red]{e}[/red]")
208
169
 
209
170
 
171
+ def _resolve_project_name_and_template(
172
+ project_name: Optional[str],
173
+ template: Optional[str],
174
+ config: bool,
175
+ git_templates: List[str],
176
+ templates_dir: str,
177
+ ) -> tuple[str, str]:
178
+ templates = git_templates + [
179
+ x.name for x in os.scandir(templates_dir) if x.is_dir()
180
+ ]
181
+
182
+ if config and template is None:
183
+ template = templates[-1]
184
+
185
+ if template:
186
+ if template not in templates:
187
+ raise Exception(f"Template {template} not found in {templates_dir}")
188
+ else:
189
+ print("\n[bold]Available templates:[/bold]")
190
+ for idx, template_name in enumerate(templates, 1):
191
+ print(f" {idx}. {template_name}")
192
+ while True:
193
+ try:
194
+ choice = IntPrompt.ask(
195
+ "\nSelect template number",
196
+ show_choices=False,
197
+ show_default=False,
198
+ )
199
+ if 1 <= choice <= len(templates):
200
+ template = templates[choice - 1]
201
+ break
202
+ else:
203
+ print(
204
+ "[red]Invalid selection. Please choose a number from the list.[/red]"
205
+ )
206
+ except (KeyboardInterrupt, EOFError):
207
+ raise typer.Abort()
208
+ except ValueError:
209
+ print("[red]Please enter a valid number.[/red]")
210
+
211
+ if template in git_templates:
212
+ if project_name is None:
213
+ project_name = template
214
+ else:
215
+ if project_name is None:
216
+ project_name = typing.cast(
217
+ str,
218
+ typer.prompt("What is your project's name?", get_project_name()),
219
+ )
220
+
221
+ if not _is_valid_app_name(project_name):
222
+ raise Exception(
223
+ f"{project_name} is an invalid DBOS app name. App names must be between 3 and 30 characters long and contain only lowercase letters, numbers, dashes, and underscores."
224
+ )
225
+
226
+ assert project_name is not None, "Project name cannot be None"
227
+ assert template is not None, "Template name cannot be None"
228
+
229
+ return project_name, template
230
+
231
+
210
232
  @app.command(
211
233
  help="Run your database schema migrations using the migration commands in 'dbos-config.yaml'"
212
234
  )
@@ -258,6 +280,8 @@ def migrate(
258
280
  "pool_size": 2,
259
281
  },
260
282
  )
283
+ sys_db.run_migrations()
284
+ app_db.run_migrations()
261
285
  except Exception as e:
262
286
  typer.echo(f"DBOS system schema migration failed: {e}")
263
287
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 1.0.0
3
+ Version: 1.1.0a2
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -1,19 +1,19 @@
1
- dbos-1.0.0.dist-info/METADATA,sha256=NCphJidtQbDmGGSyBfu9NBmbcyTjKvNler7hEaMkM54,13265
2
- dbos-1.0.0.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- dbos-1.0.0.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-1.0.0.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-1.1.0a2.dist-info/METADATA,sha256=X9RcRNberl8VXnm_V76fVezhh20QcbKpoFto07FXjhs,13267
2
+ dbos-1.1.0a2.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ dbos-1.1.0a2.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-1.1.0a2.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=56jqU0oxcIkMscOg6DxC8dZ0uEJwG7iMu3aXYs2u66k,10428
8
+ dbos/_app_db.py,sha256=wxZz3ja9QgVuyp5YLsAqa_MpuyD5tl0C5GHTLl8fwF0,10514
9
9
  dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
10
- dbos/_client.py,sha256=aPOAVWsH7uovSIsgJRamksra9y9-BVW1Jw4Gg5WjuZA,14114
10
+ dbos/_client.py,sha256=-nK2GjS9D0qnD2DkRDs7gKxNECwYlsvW6hFCjADlnv0,14186
11
11
  dbos/_conductor/conductor.py,sha256=o0IaZjwnZ2TOyHeP2H4iSX6UnXLXQ4uODvWAKD9hHMs,21703
12
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=7fQPKfaePD3HwxSjBhziJcVd2heLKefe92skgmuHr34,46275
16
+ dbos/_dbos.py,sha256=f5s9cVgsiMkAkpvctLHE6sjVAEuC-eFEpRddYBIKxiA,46430
17
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
@@ -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=qLc9A2igeNeRNvJwnEZhK720g0PWlx_zZmj1F8OyrRc,81223
50
+ dbos/_sys_db.py,sha256=P4cxqDIF8EXfYuWA36gt-JHneDVO_lMwtnNrT0WVWB4,81311
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=YPXZyAD3GIh1cw_kBTAcJxUGO6OgBHWhjQLVe66AY8k,20143
66
+ dbos/cli/cli.py,sha256=HinoCGrAUTiSeq7AAoCFfhdiE0uDw7vLMuDMN1_YTLI,20705
67
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-1.0.0.dist-info/RECORD,,
70
+ dbos-1.1.0a2.dist-info/RECORD,,
File without changes