dbos 1.13.0a3__py3-none-any.whl → 1.13.0a6__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/_dbos_config.py CHANGED
@@ -22,9 +22,10 @@ class DBOSConfig(TypedDict, total=False):
22
22
 
23
23
  Attributes:
24
24
  name (str): Application name
25
- database_url (str): Database connection string
26
- system_database_url (str): Connection string for the system database (if different from the application database)
27
- sys_db_name (str): System database name (deprecated)
25
+ system_database_url (str): Connection string for the DBOS system database. Defaults to sqlite:///{name} if not provided.
26
+ application_database_url (str): Connection string for the DBOS application database, in which DBOS @Transaction functions run. Optional. Should be the same type of database (SQLite or Postgres) as the system database.
27
+ database_url (str): (DEPRECATED) Database connection string
28
+ sys_db_name (str): (DEPRECATED) System database name
28
29
  sys_db_pool_size (int): System database pool size
29
30
  db_engine_kwargs (Dict[str, Any]): SQLAlchemy engine kwargs (See https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine)
30
31
  log_level (str): Log level
@@ -39,8 +40,9 @@ class DBOSConfig(TypedDict, total=False):
39
40
  """
40
41
 
41
42
  name: str
42
- database_url: Optional[str]
43
43
  system_database_url: Optional[str]
44
+ application_database_url: Optional[str]
45
+ database_url: Optional[str]
44
46
  sys_db_name: Optional[str]
45
47
  sys_db_pool_size: Optional[int]
46
48
  db_engine_kwargs: Optional[Dict[str, Any]]
@@ -107,12 +109,12 @@ class ConfigFile(TypedDict, total=False):
107
109
 
108
110
  Attributes:
109
111
  name (str): Application name
110
- runtimeConfig (RuntimeConfig): Configuration for request serving
112
+ runtimeConfig (RuntimeConfig): Configuration for DBOS Cloud
111
113
  database (DatabaseConfig): Configure pool sizes, migrate commands
112
- database_url (str): Database connection string
114
+ database_url (str): Application database URL
115
+ system_database_url (str): System database URL
113
116
  telemetry (TelemetryConfig): Configuration for tracing / logging
114
- env (Dict[str,str]): Environment varialbes
115
- application (Dict[str, Any]): Application-specific configuration section
117
+ env (Dict[str,str]): Environment variables
116
118
 
117
119
  """
118
120
 
@@ -144,8 +146,12 @@ def translate_dbos_config_to_config_file(config: DBOSConfig) -> ConfigFile:
144
146
  if db_config:
145
147
  translated_config["database"] = db_config
146
148
 
149
+ # Use application_database_url instead of the deprecated database_url if provided
147
150
  if "database_url" in config:
148
151
  translated_config["database_url"] = config.get("database_url")
152
+ elif "application_database_url" in config:
153
+ translated_config["database_url"] = config.get("application_database_url")
154
+
149
155
  if "system_database_url" in config:
150
156
  translated_config["system_database_url"] = config.get("system_database_url")
151
157
 
@@ -233,7 +239,6 @@ def _substitute_env_vars(content: str, silent: bool = False) -> str:
233
239
  def load_config(
234
240
  config_file_path: str = DBOS_CONFIG_PATH,
235
241
  *,
236
- run_process_config: bool = True,
237
242
  silent: bool = False,
238
243
  ) -> ConfigFile:
239
244
  """
@@ -283,8 +288,6 @@ def load_config(
283
288
  ]
284
289
 
285
290
  data = cast(ConfigFile, data)
286
- if run_process_config:
287
- data = process_config(data=data, silent=silent)
288
291
  return data # type: ignore
289
292
 
290
293
 
@@ -296,19 +299,12 @@ def process_config(
296
299
  """
297
300
  If a database_url is provided, pass it as is in the config.
298
301
 
299
- Else, build a database_url from defaults.
302
+ Else, default to SQLite.
300
303
 
301
304
  Also build SQL Alchemy "kwargs" base on user input + defaults.
302
305
  Specifically, db_engine_kwargs takes precedence over app_db_pool_size
303
306
 
304
307
  In debug mode, apply overrides from DBOS_DBHOST, DBOS_DBPORT, DBOS_DBUSER, and DBOS_DBPASSWORD.
305
-
306
- Default configuration:
307
- - Hostname: localhost
308
- - Port: 5432
309
- - Username: postgres
310
- - Password: $PGPASSWORD
311
- - Database name: transformed application name.
312
308
  """
313
309
 
314
310
  if "name" not in data:
@@ -340,20 +336,16 @@ def process_config(
340
336
 
341
337
  # Ensure database dict exists
342
338
  data.setdefault("database", {})
343
-
344
- # Database URL resolution
345
339
  connect_timeout = None
346
- if data.get("database_url") is not None and data["database_url"] != "":
340
+
341
+ # Process the application database URL, if provided
342
+ if data.get("database_url"):
347
343
  # Parse the db string and check required fields
348
344
  assert data["database_url"] is not None
349
345
  assert is_valid_database_url(data["database_url"])
350
346
 
351
347
  url = make_url(data["database_url"])
352
348
 
353
- if not data["database"].get("sys_db_name"):
354
- assert url.database is not None
355
- data["database"]["sys_db_name"] = url.database + SystemSchema.sysdb_suffix
356
-
357
349
  # Gather connect_timeout from the URL if provided. It should be used in engine kwargs if not provided there (instead of our default)
358
350
  connect_timeout_str = url.query.get("connect_timeout")
359
351
  if connect_timeout_str is not None:
@@ -378,25 +370,88 @@ def process_config(
378
370
  host=os.getenv("DBOS_DBHOST", url.host),
379
371
  port=port,
380
372
  ).render_as_string(hide_password=False)
381
- else:
373
+
374
+ # Process the system database URL, if provided
375
+ if data.get("system_database_url"):
376
+ # Parse the db string and check required fields
377
+ assert data["system_database_url"]
378
+ assert is_valid_database_url(data["system_database_url"])
379
+
380
+ url = make_url(data["system_database_url"])
381
+
382
+ # Gather connect_timeout from the URL if provided. It should be used in engine kwargs if not provided there (instead of our default). This overrides a timeout from the application database, if any.
383
+ connect_timeout_str = url.query.get("connect_timeout")
384
+ if connect_timeout_str is not None:
385
+ assert isinstance(
386
+ connect_timeout_str, str
387
+ ), "connect_timeout must be a string and defined once in the URL"
388
+ if connect_timeout_str.isdigit():
389
+ connect_timeout = int(connect_timeout_str)
390
+
391
+ # In debug mode perform env vars overrides
392
+ if isDebugMode:
393
+ # Override the username, password, host, and port
394
+ port_str = os.getenv("DBOS_DBPORT")
395
+ port = (
396
+ int(port_str)
397
+ if port_str is not None and port_str.isdigit()
398
+ else url.port
399
+ )
400
+ data["system_database_url"] = url.set(
401
+ username=os.getenv("DBOS_DBUSER", url.username),
402
+ password=os.getenv("DBOS_DBPASSWORD", url.password),
403
+ host=os.getenv("DBOS_DBHOST", url.host),
404
+ port=port,
405
+ ).render_as_string(hide_password=False)
406
+
407
+ # If an application database URL is provided but not the system database URL,
408
+ # construct the system database URL.
409
+ if data.get("database_url") and not data.get("system_database_url"):
410
+ assert data["database_url"]
411
+ if data["database_url"].startswith("sqlite"):
412
+ data["system_database_url"] = data["database_url"]
413
+ else:
414
+ url = make_url(data["database_url"])
415
+ assert url.database
416
+ if data["database"].get("sys_db_name"):
417
+ url = url.set(database=data["database"]["sys_db_name"])
418
+ else:
419
+ url = url.set(database=f"{url.database}{SystemSchema.sysdb_suffix}")
420
+ data["system_database_url"] = url.render_as_string(hide_password=False)
421
+
422
+ # If a system database URL is provided but not an application database URL, set the
423
+ # application database URL to the system database URL.
424
+ if data.get("system_database_url") and not data.get("database_url"):
425
+ assert data["system_database_url"]
426
+ data["database_url"] = data["system_database_url"]
427
+
428
+ # If neither URL is provided, use a default SQLite database URL.
429
+ if not data.get("database_url") and not data.get("system_database_url"):
382
430
  _app_db_name = _app_name_to_db_name(data["name"])
383
- _password = os.environ.get("PGPASSWORD", "dbos")
384
- data["database_url"] = (
385
- f"postgres://postgres:{_password}@localhost:5432/{_app_db_name}?connect_timeout=10&sslmode=prefer"
431
+ data["system_database_url"] = data["database_url"] = (
432
+ f"sqlite:///{_app_db_name}.sqlite"
386
433
  )
387
- if not data["database"].get("sys_db_name"):
388
- data["database"]["sys_db_name"] = _app_db_name + SystemSchema.sysdb_suffix
389
- assert data["database_url"] is not None
390
434
 
391
435
  configure_db_engine_parameters(data["database"], connect_timeout=connect_timeout)
392
436
 
393
- # Pretty-print where we've loaded database connection information from, respecting the log level
437
+ assert data["database_url"] is not None
438
+ assert data["system_database_url"] is not None
439
+ # Pretty-print connection information, respecting log level
394
440
  if not silent and logs["logLevel"] == "INFO" or logs["logLevel"] == "DEBUG":
395
- log_url = make_url(data["database_url"]).render_as_string(hide_password=True)
396
- print(f"[bold blue]Using database connection string: {log_url}[/bold blue]")
441
+ printable_sys_db_url = make_url(data["system_database_url"]).render_as_string(
442
+ hide_password=True
443
+ )
397
444
  print(
398
- f"[bold blue]Database engine parameters: {data['database']['db_engine_kwargs']}[/bold blue]"
445
+ f"[bold blue]DBOS system database URL: {printable_sys_db_url}[/bold blue]"
399
446
  )
447
+ if data["database_url"].startswith("sqlite"):
448
+ print(
449
+ f"[bold blue]Using SQLite as a system database. The SQLite system database is for development and testing. PostgreSQL is recommended for production use.[/bold blue]"
450
+ )
451
+ else:
452
+ print(
453
+ f"[bold blue]Database engine parameters: {data['database']['db_engine_kwargs']}[/bold blue]"
454
+ )
400
455
 
401
456
  # Return data as ConfigFile type
402
457
  return data
@@ -445,6 +500,8 @@ def configure_db_engine_parameters(
445
500
 
446
501
 
447
502
  def is_valid_database_url(database_url: str) -> bool:
503
+ if database_url.startswith("sqlite"):
504
+ return True
448
505
  url = make_url(database_url)
449
506
  required_fields = [
450
507
  ("username", "Username must be specified in the connection URL"),
@@ -473,36 +530,34 @@ def _app_name_to_db_name(app_name: str) -> str:
473
530
 
474
531
  def overwrite_config(provided_config: ConfigFile) -> ConfigFile:
475
532
  # Load the DBOS configuration file and force the use of:
476
- # 1. The database url provided by DBOS_DATABASE_URL
533
+ # 1. The application and system database url provided by DBOS_DATABASE_URL and DBOS_SYSTEM_DATABASE_URL
477
534
  # 2. OTLP traces endpoints (add the config data to the provided config)
478
535
  # 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
479
536
  # 4. Remove admin_port is provided in code
480
537
  # 5. Remove env vars if provided in code
481
538
  # Optimistically assume that expected fields in config_from_file are present
482
539
 
483
- config_from_file = load_config(run_process_config=False)
540
+ config_from_file = load_config()
484
541
  # Be defensive
485
542
  if config_from_file is None:
486
543
  return provided_config
487
544
 
488
- # Name
545
+ # Set the application name to the cloud app name
489
546
  provided_config["name"] = config_from_file["name"]
490
547
 
491
- # Database config. Expects DBOS_DATABASE_URL to be set
492
- if "database" not in provided_config:
493
- provided_config["database"] = {}
494
- provided_config["database"]["sys_db_name"] = config_from_file["database"][
495
- "sys_db_name"
496
- ]
497
-
548
+ # Use the DBOS Cloud application and system database URLs
498
549
  db_url = os.environ.get("DBOS_DATABASE_URL")
499
550
  if db_url is None:
500
551
  raise DBOSInitializationError(
501
552
  "DBOS_DATABASE_URL environment variable is not set. This is required to connect to the database."
502
553
  )
503
554
  provided_config["database_url"] = db_url
504
- if "system_database_url" in provided_config:
505
- del provided_config["system_database_url"]
555
+ system_db_url = os.environ.get("DBOS_SYSTEM_DATABASE_URL")
556
+ if system_db_url is None:
557
+ raise DBOSInitializationError(
558
+ "DBOS_SYSTEM_DATABASE_URL environment variable is not set. This is required to connect to the database."
559
+ )
560
+ provided_config["system_database_url"] = system_db_url
506
561
 
507
562
  # Telemetry config
508
563
  if "telemetry" not in provided_config or provided_config["telemetry"] is None:
@@ -559,8 +614,10 @@ def get_system_database_url(config: ConfigFile) -> str:
559
614
  return config["system_database_url"]
560
615
  else:
561
616
  assert config["database_url"] is not None
617
+ if config["database_url"].startswith("sqlite"):
618
+ return config["database_url"]
562
619
  app_db_url = make_url(config["database_url"])
563
- if config["database"].get("sys_db_name") is not None:
620
+ if config.get("database") and config["database"].get("sys_db_name") is not None:
564
621
  sys_db_name = config["database"]["sys_db_name"]
565
622
  else:
566
623
  assert app_db_url.database is not None
@@ -568,3 +625,14 @@ def get_system_database_url(config: ConfigFile) -> str:
568
625
  return app_db_url.set(database=sys_db_name).render_as_string(
569
626
  hide_password=False
570
627
  )
628
+
629
+
630
+ def get_application_database_url(config: ConfigFile) -> str:
631
+ # For backwards compatibility, the application database URL is "database_url"
632
+ if config.get("database_url"):
633
+ assert config["database_url"]
634
+ return config["database_url"]
635
+ else:
636
+ # If the application database URL is not specified, set it to the system database URL
637
+ assert config["system_database_url"]
638
+ return config["system_database_url"]
dbos/_migration.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import os
3
3
  import re
4
+ import sys
4
5
 
5
6
  import sqlalchemy as sa
6
7
  from alembic import command
@@ -230,4 +231,92 @@ CREATE TABLE dbos.event_dispatch_kv (
230
231
  );
231
232
  """
232
233
 
234
+
235
+ def get_sqlite_timestamp_expr() -> str:
236
+ """Get SQLite timestamp expression with millisecond precision for Python >= 3.12."""
237
+ if sys.version_info >= (3, 12):
238
+ return "(unixepoch('subsec') * 1000)"
239
+ else:
240
+ return "(strftime('%s','now') * 1000)"
241
+
242
+
243
+ sqlite_migration_one = f"""
244
+ CREATE TABLE workflow_status (
245
+ workflow_uuid TEXT PRIMARY KEY,
246
+ status TEXT,
247
+ name TEXT,
248
+ authenticated_user TEXT,
249
+ assumed_role TEXT,
250
+ authenticated_roles TEXT,
251
+ request TEXT,
252
+ output TEXT,
253
+ error TEXT,
254
+ executor_id TEXT,
255
+ created_at INTEGER NOT NULL DEFAULT {get_sqlite_timestamp_expr()},
256
+ updated_at INTEGER NOT NULL DEFAULT {get_sqlite_timestamp_expr()},
257
+ application_version TEXT,
258
+ application_id TEXT,
259
+ class_name TEXT DEFAULT NULL,
260
+ config_name TEXT DEFAULT NULL,
261
+ recovery_attempts INTEGER DEFAULT 0,
262
+ queue_name TEXT,
263
+ workflow_timeout_ms INTEGER,
264
+ workflow_deadline_epoch_ms INTEGER,
265
+ inputs TEXT,
266
+ started_at_epoch_ms INTEGER,
267
+ deduplication_id TEXT,
268
+ priority INTEGER NOT NULL DEFAULT 0
269
+ );
270
+
271
+ CREATE INDEX workflow_status_created_at_index ON workflow_status (created_at);
272
+ CREATE INDEX workflow_status_executor_id_index ON workflow_status (executor_id);
273
+ CREATE INDEX workflow_status_status_index ON workflow_status (status);
274
+
275
+ CREATE UNIQUE INDEX uq_workflow_status_queue_name_dedup_id
276
+ ON workflow_status (queue_name, deduplication_id);
277
+
278
+ CREATE TABLE operation_outputs (
279
+ workflow_uuid TEXT NOT NULL,
280
+ function_id INTEGER NOT NULL,
281
+ function_name TEXT NOT NULL DEFAULT '',
282
+ output TEXT,
283
+ error TEXT,
284
+ child_workflow_id TEXT,
285
+ PRIMARY KEY (workflow_uuid, function_id),
286
+ FOREIGN KEY (workflow_uuid) REFERENCES workflow_status(workflow_uuid)
287
+ ON UPDATE CASCADE ON DELETE CASCADE
288
+ );
289
+
290
+ CREATE TABLE notifications (
291
+ destination_uuid TEXT NOT NULL,
292
+ topic TEXT,
293
+ message TEXT NOT NULL,
294
+ created_at_epoch_ms INTEGER NOT NULL DEFAULT {get_sqlite_timestamp_expr()},
295
+ message_uuid TEXT NOT NULL DEFAULT (hex(randomblob(16))),
296
+ FOREIGN KEY (destination_uuid) REFERENCES workflow_status(workflow_uuid)
297
+ ON UPDATE CASCADE ON DELETE CASCADE
298
+ );
299
+ CREATE INDEX idx_workflow_topic ON notifications (destination_uuid, topic);
300
+
301
+ CREATE TABLE workflow_events (
302
+ workflow_uuid TEXT NOT NULL,
303
+ key TEXT NOT NULL,
304
+ value TEXT NOT NULL,
305
+ PRIMARY KEY (workflow_uuid, key),
306
+ FOREIGN KEY (workflow_uuid) REFERENCES workflow_status(workflow_uuid)
307
+ ON UPDATE CASCADE ON DELETE CASCADE
308
+ );
309
+
310
+ CREATE TABLE streams (
311
+ workflow_uuid TEXT NOT NULL,
312
+ key TEXT NOT NULL,
313
+ value TEXT NOT NULL,
314
+ "offset" INTEGER NOT NULL,
315
+ PRIMARY KEY (workflow_uuid, key, "offset"),
316
+ FOREIGN KEY (workflow_uuid) REFERENCES workflow_status(workflow_uuid)
317
+ ON UPDATE CASCADE ON DELETE CASCADE
318
+ );
319
+ """
320
+
233
321
  dbos_migrations = [dbos_migration_one]
322
+ sqlite_migrations = [sqlite_migration_one]