dbos 0.26.0a8__tar.gz → 0.26.0a9__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.26.0a8 → dbos-0.26.0a9}/PKG-INFO +1 -1
  2. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_dbos_config.py +4 -54
  3. dbos-0.26.0a9/dbos/_docker_pg_helper.py +191 -0
  4. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/cli/cli.py +17 -1
  5. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/dbos-config.schema.json +0 -4
  6. {dbos-0.26.0a8 → dbos-0.26.0a9}/pyproject.toml +2 -1
  7. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_config.py +6 -117
  8. dbos-0.26.0a8/dbos/_cloudutils/authentication.py +0 -163
  9. dbos-0.26.0a8/dbos/_cloudutils/cloudutils.py +0 -254
  10. dbos-0.26.0a8/dbos/_cloudutils/databases.py +0 -241
  11. dbos-0.26.0a8/dbos/_db_wizard.py +0 -220
  12. dbos-0.26.0a8/tests/test_dbwizard.py +0 -84
  13. {dbos-0.26.0a8 → dbos-0.26.0a9}/LICENSE +0 -0
  14. {dbos-0.26.0a8 → dbos-0.26.0a9}/README.md +0 -0
  15. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/__init__.py +0 -0
  16. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/__main__.py +0 -0
  17. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_admin_server.py +0 -0
  18. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_app_db.py +0 -0
  19. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_classproperty.py +0 -0
  20. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_client.py +0 -0
  21. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_conductor/conductor.py +0 -0
  22. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_conductor/protocol.py +0 -0
  23. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_context.py +0 -0
  24. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_core.py +0 -0
  25. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_croniter.py +0 -0
  26. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_dbos.py +0 -0
  27. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_debug.py +0 -0
  28. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_error.py +0 -0
  29. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_fastapi.py +0 -0
  30. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_flask.py +0 -0
  31. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_kafka.py +0 -0
  32. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_kafka_message.py +0 -0
  33. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_logger.py +0 -0
  34. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/env.py +0 -0
  35. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/script.py.mako +0 -0
  36. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
  37. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  38. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  39. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  40. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  41. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  42. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  43. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
  44. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_outcome.py +0 -0
  45. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_queue.py +0 -0
  46. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_recovery.py +0 -0
  47. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_registrations.py +0 -0
  48. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_request.py +0 -0
  49. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_roles.py +0 -0
  50. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_scheduler.py +0 -0
  51. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_schemas/__init__.py +0 -0
  52. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_schemas/application_database.py +0 -0
  53. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_schemas/system_database.py +0 -0
  54. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_serialization.py +0 -0
  55. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_sys_db.py +0 -0
  56. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/README.md +0 -0
  57. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
  58. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
  59. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
  60. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
  61. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
  62. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
  63. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
  64. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
  65. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
  66. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_tracer.py +0 -0
  67. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_utils.py +0 -0
  68. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_workflow_commands.py +0 -0
  69. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/cli/_github_init.py +0 -0
  70. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/cli/_template_init.py +0 -0
  71. {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/py.typed +0 -0
  72. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/__init__.py +0 -0
  73. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/atexit_no_ctor.py +0 -0
  74. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/atexit_no_launch.py +0 -0
  75. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/classdefs.py +0 -0
  76. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/client_collateral.py +0 -0
  77. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/client_worker.py +0 -0
  78. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/conftest.py +0 -0
  79. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/more_classdefs.py +0 -0
  80. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/queuedworkflow.py +0 -0
  81. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_admin_server.py +0 -0
  82. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_async.py +0 -0
  83. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_classdecorators.py +0 -0
  84. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_client.py +0 -0
  85. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_concurrency.py +0 -0
  86. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_croniter.py +0 -0
  87. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_dbos.py +0 -0
  88. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_debug.py +0 -0
  89. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_docker_secrets.py +0 -0
  90. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_failures.py +0 -0
  91. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_fastapi.py +0 -0
  92. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_fastapi_roles.py +0 -0
  93. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_flask.py +0 -0
  94. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_kafka.py +0 -0
  95. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_outcome.py +0 -0
  96. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_package.py +0 -0
  97. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_queue.py +0 -0
  98. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_scheduler.py +0 -0
  99. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_schema_migration.py +0 -0
  100. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_singleton.py +0 -0
  101. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_spans.py +0 -0
  102. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_sqlalchemy.py +0 -0
  103. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_workflow_introspection.py +0 -0
  104. {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_workflow_management.py +0 -0
  105. {dbos-0.26.0a8 → dbos-0.26.0a9}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.26.0a8
3
+ Version: 0.26.0a9
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -15,7 +15,6 @@ from jsonschema import ValidationError, validate
15
15
  from rich import print
16
16
  from sqlalchemy import URL, make_url
17
17
 
18
- from ._db_wizard import db_wizard, load_db_connection
19
18
  from ._error import DBOSInitializationError
20
19
  from ._logger import dbos_logger
21
20
 
@@ -70,7 +69,6 @@ class DatabaseConfig(TypedDict, total=False):
70
69
  sys_db_pool_size: Optional[int]
71
70
  ssl: Optional[bool]
72
71
  ssl_ca: Optional[str]
73
- local_suffix: Optional[bool]
74
72
  migrate: Optional[List[str]]
75
73
  rollback: Optional[List[str]]
76
74
 
@@ -288,7 +286,6 @@ def load_config(
288
286
  config_file_path: str = DBOS_CONFIG_PATH,
289
287
  *,
290
288
  run_process_config: bool = True,
291
- use_db_wizard: bool = True,
292
289
  silent: bool = False,
293
290
  ) -> ConfigFile:
294
291
  """
@@ -339,13 +336,12 @@ def load_config(
339
336
 
340
337
  data = cast(ConfigFile, data)
341
338
  if run_process_config:
342
- data = process_config(data=data, use_db_wizard=use_db_wizard, silent=silent)
339
+ data = process_config(data=data, silent=silent)
343
340
  return data # type: ignore
344
341
 
345
342
 
346
343
  def process_config(
347
344
  *,
348
- use_db_wizard: bool = True,
349
345
  data: ConfigFile,
350
346
  silent: bool = False,
351
347
  ) -> ConfigFile:
@@ -372,22 +368,17 @@ def process_config(
372
368
  # database_url takes precedence over database config, but we need to preserve rollback and migrate if they exist
373
369
  migrate = data["database"].get("migrate", False)
374
370
  rollback = data["database"].get("rollback", False)
375
- local_suffix = data["database"].get("local_suffix", False)
376
371
  if data.get("database_url"):
377
372
  dbconfig = parse_database_url_to_dbconfig(cast(str, data["database_url"]))
378
373
  if migrate:
379
374
  dbconfig["migrate"] = cast(List[str], migrate)
380
375
  if rollback:
381
376
  dbconfig["rollback"] = cast(List[str], rollback)
382
- if local_suffix:
383
- dbconfig["local_suffix"] = cast(bool, local_suffix)
384
377
  data["database"] = dbconfig
385
378
 
386
379
  if "app_db_name" not in data["database"] or not (data["database"]["app_db_name"]):
387
380
  data["database"]["app_db_name"] = _app_name_to_db_name(data["name"])
388
381
 
389
- # Load the DB connection file. Use its values for missing connection parameters. Use defaults otherwise.
390
- db_connection = load_db_connection()
391
382
  connection_passed_in = data["database"].get("hostname", None) is not None
392
383
 
393
384
  dbos_dbport: Optional[int] = None
@@ -397,49 +388,22 @@ def process_config(
397
388
  dbos_dbport = int(dbport_env)
398
389
  except ValueError:
399
390
  pass
400
- dbos_dblocalsuffix: Optional[bool] = None
401
- dblocalsuffix_env = os.getenv("DBOS_DBLOCALSUFFIX")
402
- if dblocalsuffix_env:
403
- try:
404
- dbos_dblocalsuffix = dblocalsuffix_env.casefold() == "true".casefold()
405
- except ValueError:
406
- pass
407
391
 
408
392
  data["database"]["hostname"] = (
409
- os.getenv("DBOS_DBHOST")
410
- or data["database"].get("hostname")
411
- or db_connection.get("hostname")
412
- or "localhost"
393
+ os.getenv("DBOS_DBHOST") or data["database"].get("hostname") or "localhost"
413
394
  )
414
395
 
415
- data["database"]["port"] = (
416
- dbos_dbport or data["database"].get("port") or db_connection.get("port") or 5432
417
- )
396
+ data["database"]["port"] = dbos_dbport or data["database"].get("port") or 5432
418
397
  data["database"]["username"] = (
419
- os.getenv("DBOS_DBUSER")
420
- or data["database"].get("username")
421
- or db_connection.get("username")
422
- or "postgres"
398
+ os.getenv("DBOS_DBUSER") or data["database"].get("username") or "postgres"
423
399
  )
424
400
  data["database"]["password"] = (
425
401
  os.getenv("DBOS_DBPASSWORD")
426
402
  or data["database"].get("password")
427
- or db_connection.get("password")
428
403
  or os.environ.get("PGPASSWORD")
429
404
  or "dbos"
430
405
  )
431
406
 
432
- local_suffix = False
433
- dbcon_local_suffix = db_connection.get("local_suffix")
434
- if dbcon_local_suffix is not None:
435
- local_suffix = dbcon_local_suffix
436
- db_local_suffix = data["database"].get("local_suffix")
437
- if db_local_suffix is not None:
438
- local_suffix = db_local_suffix
439
- if dbos_dblocalsuffix is not None:
440
- local_suffix = dbos_dblocalsuffix
441
- data["database"]["local_suffix"] = local_suffix
442
-
443
407
  if not data["database"].get("app_db_pool_size"):
444
408
  data["database"]["app_db_pool_size"] = 20
445
409
  if not data["database"].get("sys_db_pool_size"):
@@ -454,10 +418,6 @@ def process_config(
454
418
  elif "run_admin_server" not in data["runtimeConfig"]:
455
419
  data["runtimeConfig"]["run_admin_server"] = True
456
420
 
457
- # Check the connectivity to the database and make sure it's properly configured
458
- # Note, never use db wizard if the DBOS is running in debug mode (i.e. DBOS_DEBUG_WORKFLOW_ID env var is set)
459
- debugWorkflowId = os.getenv("DBOS_DEBUG_WORKFLOW_ID")
460
-
461
421
  # Pretty-print where we've loaded database connection information from, respecting the log level
462
422
  if not silent and logs["logLevel"] == "INFO" or logs["logLevel"] == "DEBUG":
463
423
  d = data["database"]
@@ -470,21 +430,11 @@ def process_config(
470
430
  print(
471
431
  f"[bold blue]Using database connection string: {conn_string}[/bold blue]"
472
432
  )
473
- elif db_connection.get("hostname"):
474
- print(
475
- f"[bold blue]Loading database connection string from .dbos/db_connection: {conn_string}[/bold blue]"
476
- )
477
433
  else:
478
434
  print(
479
435
  f"[bold blue]Using default database connection string: {conn_string}[/bold blue]"
480
436
  )
481
437
 
482
- if use_db_wizard and debugWorkflowId is None:
483
- data = db_wizard(data)
484
-
485
- if "local_suffix" in data["database"] and data["database"]["local_suffix"]:
486
- data["database"]["app_db_name"] = f"{data['database']['app_db_name']}_local"
487
-
488
438
  # Return data as ConfigFile type
489
439
  return data
490
440
 
@@ -0,0 +1,191 @@
1
+ import logging
2
+ import os
3
+ import subprocess
4
+ import time
5
+
6
+ import docker
7
+ import psycopg
8
+ from docker.errors import APIError, NotFound
9
+
10
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+
14
+ def start_docker_pg() -> None:
15
+ """
16
+ Starts a PostgreSQL database in a Docker container.
17
+
18
+ This function checks if Docker is installed, and if so, starts a local PostgreSQL
19
+ database in a Docker container. It configures the database with default settings
20
+ and provides connection information upon successful startup.
21
+
22
+ The function uses environment variable PGPASSWORD if available, otherwise
23
+ defaults to 'dbos' as the database password.
24
+
25
+ Returns:
26
+ None
27
+
28
+ Raises:
29
+ Exception: If there is an error starting the Docker container or if the
30
+ PostgreSQL service does not become available within the timeout period.
31
+ """
32
+
33
+ logging.info("Attempting to create a Docker Postgres container...")
34
+ has_docker = check_docker_installed()
35
+
36
+ pool_config = {
37
+ "host": "localhost",
38
+ "port": 5432,
39
+ "password": os.environ.get("PGPASSWORD", "dbos"),
40
+ "user": "postgres",
41
+ "database": "postgres",
42
+ "connect_timeout": 2,
43
+ }
44
+
45
+ # If Docker is installed, start a local Docker based Postgres
46
+ if has_docker:
47
+ start_docker_postgres(pool_config)
48
+ logging.info(
49
+ f"Postgres available at postgres://postgres:{pool_config['password']}@{pool_config['host']}:{pool_config['port']}"
50
+ )
51
+ else:
52
+ logging.warning("Docker not detected locally")
53
+
54
+
55
+ def check_db_connectivity(config: Dict[str, Any]) -> Optional[Exception]:
56
+ conn = None
57
+ try:
58
+ conn = psycopg.connect(
59
+ host=config["host"],
60
+ port=config["port"],
61
+ user=config["user"],
62
+ password=config["password"],
63
+ dbname=config["database"],
64
+ connect_timeout=config.get("connect_timeout", 30),
65
+ )
66
+ cursor = conn.cursor()
67
+ cursor.execute("SELECT 1;")
68
+ cursor.close()
69
+ return None
70
+ except Exception as error:
71
+ return error
72
+ finally:
73
+ if conn is not None:
74
+ conn.close()
75
+
76
+
77
+ def exec_sync(cmd: str) -> Tuple[str, str]:
78
+ result = subprocess.run(cmd, shell=True, text=True, capture_output=True, check=True)
79
+ return result.stdout, result.stderr
80
+
81
+
82
+ def start_docker_postgres(pool_config: Dict[str, Any]) -> bool:
83
+ logging.info("Starting a Postgres Docker container...")
84
+ container_name = "dbos-db"
85
+ pg_data = "/var/lib/postgresql/data"
86
+
87
+ try:
88
+ client = docker.from_env()
89
+
90
+ # Check if the container already exists
91
+ try:
92
+ container = client.containers.get(container_name)
93
+ if container.status == "running":
94
+ logging.info(f"Container '{container_name}' is already running.")
95
+ return True
96
+ elif container.status == "exited":
97
+ container.start()
98
+ logging.info(
99
+ f"Container '{container_name}' was stopped and has been restarted."
100
+ )
101
+ return True
102
+ except NotFound:
103
+ # Container doesn't exist, proceed with creation
104
+ pass
105
+
106
+ # Create and start the container
107
+ container = client.containers.run(
108
+ image="pgvector/pgvector:pg16",
109
+ name=container_name,
110
+ detach=True,
111
+ environment={
112
+ "POSTGRES_PASSWORD": pool_config["password"],
113
+ "PGDATA": pg_data,
114
+ },
115
+ ports={"5432/tcp": pool_config["port"]},
116
+ volumes={pg_data: {"bind": pg_data, "mode": "rw"}},
117
+ remove=True, # Equivalent to --rm
118
+ )
119
+
120
+ logging.info(f"Created container: {container.id}")
121
+
122
+ except APIError as e:
123
+ raise Exception(f"Docker API error: {str(e)}")
124
+
125
+ # Wait for PostgreSQL to be ready
126
+ attempts = 30
127
+ while attempts > 0:
128
+ if attempts % 5 == 0:
129
+ logging.info("Waiting for Postgres Docker container to start...")
130
+
131
+ if check_db_connectivity(pool_config) is None:
132
+ return True
133
+
134
+ attempts -= 1
135
+ time.sleep(1)
136
+
137
+ raise Exception(
138
+ f"Failed to start Docker container: Container {container_name} did not start in time."
139
+ )
140
+
141
+
142
+ def check_docker_installed() -> bool:
143
+ """
144
+ Check if Docker is installed and running using the docker library.
145
+
146
+ Returns:
147
+ bool: True if Docker is installed and running, False otherwise.
148
+ """
149
+ try:
150
+ client = docker.from_env()
151
+ client.ping() # type: ignore
152
+ return True
153
+ except Exception:
154
+ return False
155
+
156
+
157
+ def stop_docker_pg() -> None:
158
+ """
159
+ Stops the Docker Postgres container.
160
+
161
+ Returns:
162
+ bool: True if the container was successfully stopped, False if it wasn't running
163
+
164
+ Raises:
165
+ Exception: If there was an error stopping the container
166
+ """
167
+ logger = logging.getLogger()
168
+ container_name = "dbos-db"
169
+ try:
170
+ logger.info(f"Stopping Docker Postgres container {container_name}...")
171
+
172
+ client = docker.from_env()
173
+
174
+ try:
175
+ container = client.containers.get(container_name)
176
+
177
+ if container.status == "running":
178
+ container.stop()
179
+ logger.info(
180
+ f"Successfully stopped Docker Postgres container {container_name}."
181
+ )
182
+ else:
183
+ logger.info(f"Container {container_name} exists but is not running.")
184
+
185
+ except docker.errors.NotFound:
186
+ logger.info(f"Container {container_name} does not exist.")
187
+
188
+ except Exception as error:
189
+ error_message = str(error)
190
+ logger.error(f"Failed to stop Docker Postgres container: {error_message}")
191
+ raise
@@ -20,6 +20,7 @@ from dbos._debug import debug_workflow, parse_start_command
20
20
  from .. import load_config
21
21
  from .._app_db import ApplicationDatabase
22
22
  from .._dbos_config import _is_valid_app_name
23
+ from .._docker_pg_helper import start_docker_pg, stop_docker_pg
23
24
  from .._sys_db import SystemDatabase, reset_system_database
24
25
  from .._workflow_commands import (
25
26
  get_workflow,
@@ -37,6 +38,21 @@ queue = typer.Typer()
37
38
  app.add_typer(workflow, name="workflow", help="Manage DBOS workflows")
38
39
  workflow.add_typer(queue, name="queue", help="Manage enqueued workflows")
39
40
 
41
+ postgres = typer.Typer()
42
+ app.add_typer(
43
+ postgres, name="postgres", help="Manage local Postgres database with Docker"
44
+ )
45
+
46
+
47
+ @postgres.command(name="start", help="Start a local Postgres database")
48
+ def pg_start() -> None:
49
+ start_docker_pg()
50
+
51
+
52
+ @postgres.command(name="stop", help="Stop the local Postgres database")
53
+ def pg_stop() -> None:
54
+ stop_docker_pg()
55
+
40
56
 
41
57
  def _on_windows() -> bool:
42
58
  return platform.system() == "Windows"
@@ -246,7 +262,7 @@ def reset(
246
262
  def debug(
247
263
  workflow_id: Annotated[str, typer.Argument(help="Workflow ID to debug")],
248
264
  ) -> None:
249
- config = load_config(silent=True, use_db_wizard=False)
265
+ config = load_config(silent=True)
250
266
  start = config["runtimeConfig"]["start"]
251
267
  if not start:
252
268
  typer.echo("No start commands found in 'dbos-config.yaml'")
@@ -62,10 +62,6 @@
62
62
  "type": "string",
63
63
  "description": "If using SSL/TLS to securely connect to a database, path to an SSL root certificate file"
64
64
  },
65
- "local_suffix": {
66
- "type": "boolean",
67
- "description": "Whether to suffix app_db_name with '_local'. Set to true when doing local development using a DBOS Cloud database."
68
- },
69
65
  "app_db_client": {
70
66
  "type": "string",
71
67
  "description": "Specify the database client to use to connect to the application database",
@@ -28,7 +28,7 @@ dependencies = [
28
28
  ]
29
29
  requires-python = ">=3.9"
30
30
  readme = "README.md"
31
- version = "0.26.0a8"
31
+ version = "0.26.0a9"
32
32
 
33
33
  [project.license]
34
34
  text = "MIT"
@@ -88,4 +88,5 @@ dev = [
88
88
  "pdm-backend>=2.4.2",
89
89
  "pytest-asyncio>=0.25.0",
90
90
  "pyright>=1.1.398",
91
+ "types-docker>=7.1.0.20241229",
91
92
  ]
@@ -327,7 +327,6 @@ def test_process_config_full():
327
327
  "sys_db_pool_size": 27,
328
328
  "ssl": True,
329
329
  "ssl_ca": "ca.pem",
330
- "local_suffix": False,
331
330
  "migrate": ["alembic upgrade head"],
332
331
  "rollback": ["alembic downgrade base"],
333
332
  },
@@ -351,7 +350,7 @@ def test_process_config_full():
351
350
  },
352
351
  }
353
352
 
354
- configFile = process_config(data=config, use_db_wizard=False)
353
+ configFile = process_config(data=config)
355
354
  assert configFile["name"] == "some-app"
356
355
  assert configFile["database"]["hostname"] == "example.com"
357
356
  assert configFile["database"]["port"] == 2345
@@ -362,7 +361,6 @@ def test_process_config_full():
362
361
  assert configFile["database"]["sys_db_name"] == "sys_db"
363
362
  assert configFile["database"]["ssl"] == True
364
363
  assert configFile["database"]["ssl_ca"] == "ca.pem"
365
- assert configFile["database"]["local_suffix"] == False
366
364
  assert configFile["database"]["migrate"] == ["alembic upgrade head"]
367
365
  assert configFile["database"]["rollback"] == ["alembic downgrade base"]
368
366
  assert configFile["database"]["app_db_pool_size"] == 45
@@ -386,7 +384,7 @@ def test_process_config_with_db_url():
386
384
  "name": "some-app",
387
385
  "database_url": "postgresql://user:password@localhost:7777/dbn?connect_timeout=1&sslmode=require&sslrootcert=ca.pem",
388
386
  }
389
- processed_config = process_config(data=config, use_db_wizard=False)
387
+ processed_config = process_config(data=config)
390
388
  assert processed_config["name"] == "some-app"
391
389
  assert processed_config["database"]["hostname"] == "localhost"
392
390
  assert processed_config["database"]["port"] == 7777
@@ -396,7 +394,6 @@ def test_process_config_with_db_url():
396
394
  assert processed_config["database"]["connectionTimeoutMillis"] == 1000
397
395
  assert processed_config["database"]["ssl"] == True
398
396
  assert processed_config["database"]["ssl_ca"] == "ca.pem"
399
- assert processed_config["database"]["local_suffix"] == False
400
397
  assert processed_config["database"]["app_db_pool_size"] == 20
401
398
  assert processed_config["database"]["sys_db_pool_size"] == 20
402
399
  assert "rollback" not in processed_config["database"]
@@ -417,22 +414,20 @@ def test_process_config_with_db_url_taking_precedence_over_database():
417
414
  "sys_db_name": "sys_db",
418
415
  "ssl": True,
419
416
  "ssl_ca": "ca.pem",
420
- "local_suffix": True,
421
417
  "migrate": ["alembic upgrade head"],
422
418
  "rollback": ["alembic downgrade base"],
423
419
  },
424
420
  "database_url": "postgresql://boo:whoisdiz@remotehost:7777/takesprecedence",
425
421
  }
426
- processed_config = process_config(data=config, use_db_wizard=False)
422
+ processed_config = process_config(data=config)
427
423
  assert processed_config["name"] == "some-app"
428
424
  assert processed_config["database"]["hostname"] == "remotehost"
429
425
  assert processed_config["database"]["port"] == 7777
430
426
  assert processed_config["database"]["username"] == "boo"
431
427
  assert processed_config["database"]["password"] == "whoisdiz"
432
- assert processed_config["database"]["app_db_name"] == "takesprecedence_local"
428
+ assert processed_config["database"]["app_db_name"] == "takesprecedence"
433
429
  assert processed_config["database"]["migrate"] == ["alembic upgrade head"]
434
430
  assert processed_config["database"]["rollback"] == ["alembic downgrade base"]
435
- assert processed_config["database"]["local_suffix"] == True
436
431
  assert processed_config["database"]["app_db_pool_size"] == 20
437
432
  assert processed_config["database"]["sys_db_pool_size"] == 20
438
433
  assert processed_config["database"]["connectionTimeoutMillis"] == 10000
@@ -550,32 +545,6 @@ def test_config_bad_name():
550
545
  assert "Invalid app name" in str(exc_info.value)
551
546
 
552
547
 
553
- def test_load_config_load_db_connection(mocker):
554
- mock_db_connection = """
555
- {"hostname": "example.com", "port": 2345, "username": "example", "password": "password", "local_suffix": true}
556
- """
557
- mocker.patch(
558
- "builtins.open",
559
- side_effect=generate_mock_open([".dbos/db_connection"], [mock_db_connection]),
560
- )
561
-
562
- config = {
563
- "name": "some-app",
564
- }
565
-
566
- configFile = process_config(data=config, use_db_wizard=False)
567
- assert configFile["name"] == "some-app"
568
- assert configFile["database"]["hostname"] == "example.com"
569
- assert configFile["database"]["port"] == 2345
570
- assert configFile["database"]["username"] == "example"
571
- assert configFile["database"]["password"] == "password"
572
- assert configFile["database"]["local_suffix"] == True
573
- assert configFile["database"]["app_db_name"] == "some_app_local"
574
- assert configFile["database"]["app_db_pool_size"] == 20
575
- assert configFile["database"]["sys_db_pool_size"] == 20
576
- assert configFile["database"]["connectionTimeoutMillis"] == 10000
577
-
578
-
579
548
  def test_config_mixed_params():
580
549
  config = {
581
550
  "name": "some-app",
@@ -587,7 +556,7 @@ def test_config_mixed_params():
587
556
  },
588
557
  }
589
558
 
590
- configFile = process_config(data=config, use_db_wizard=False)
559
+ configFile = process_config(data=config)
591
560
  assert configFile["name"] == "some-app"
592
561
  assert configFile["database"]["hostname"] == "localhost"
593
562
  assert configFile["database"]["port"] == 1234
@@ -606,77 +575,22 @@ def test_debug_override(mocker: pytest_mock.MockFixture):
606
575
  "DBOS_DBPORT": "1234",
607
576
  "DBOS_DBUSER": "fakeuser",
608
577
  "DBOS_DBPASSWORD": "fakepassword",
609
- "DBOS_DBLOCALSUFFIX": "false",
610
578
  },
611
579
  )
612
580
 
613
581
  config: Configfile = {
614
582
  "name": "some-app",
615
583
  }
616
- configFile = process_config(data=config, use_db_wizard=False)
584
+ configFile = process_config(data=config)
617
585
  assert configFile["database"]["hostname"] == "fakehost"
618
586
  assert configFile["database"]["port"] == 1234
619
587
  assert configFile["database"]["username"] == "fakeuser"
620
588
  assert configFile["database"]["password"] == "fakepassword"
621
- assert configFile["database"]["local_suffix"] == False
622
589
  assert configFile["database"]["app_db_pool_size"] == 20
623
590
  assert configFile["database"]["sys_db_pool_size"] == 20
624
591
  assert configFile["database"]["connectionTimeoutMillis"] == 10000
625
592
 
626
593
 
627
- def test_local_config():
628
- config: ConfigFile = {
629
- "name": "some-app",
630
- "database": {
631
- "hostname": "localhost",
632
- "port": 5432,
633
- "username": "postgres",
634
- "password": os.environ["PGPASSWORD"],
635
- "app_db_name": "some_db",
636
- "connectionTimeoutMillis": 3000,
637
- "local_suffix": True,
638
- },
639
- }
640
- processed_config = process_config(data=config)
641
-
642
- assert processed_config["name"] == "some-app"
643
- assert processed_config["database"]["local_suffix"] == True
644
- assert processed_config["database"]["hostname"] == "localhost"
645
- assert processed_config["database"]["port"] == 5432
646
- assert processed_config["database"]["username"] == "postgres"
647
- assert processed_config["database"]["password"] == os.environ["PGPASSWORD"]
648
- assert processed_config["database"]["app_db_name"] == "some_db_local"
649
- assert processed_config["database"]["connectionTimeoutMillis"] == 3000
650
- assert processed_config["database"]["app_db_pool_size"] == 20
651
- assert processed_config["database"]["sys_db_pool_size"] == 20
652
-
653
-
654
- def test_local_config_without_name(mocker):
655
- config: ConfigFile = {
656
- "name": "some-app",
657
- "database": {
658
- "hostname": "localhost",
659
- "port": 5432,
660
- "username": "postgres",
661
- "password": os.environ["PGPASSWORD"],
662
- "connectionTimeoutMillis": 3000,
663
- "local_suffix": True,
664
- },
665
- }
666
- processed_config = process_config(data=config)
667
-
668
- assert processed_config["name"] == "some-app"
669
- assert processed_config["database"]["local_suffix"] == True
670
- assert processed_config["database"]["hostname"] == "localhost"
671
- assert processed_config["database"]["port"] == 5432
672
- assert processed_config["database"]["username"] == "postgres"
673
- assert processed_config["database"]["password"] == os.environ["PGPASSWORD"]
674
- assert processed_config["database"]["app_db_name"] == "some_app_local"
675
- assert processed_config["database"]["connectionTimeoutMillis"] == 3000
676
- assert processed_config["database"]["app_db_pool_size"] == 20
677
- assert processed_config["database"]["sys_db_pool_size"] == 20
678
-
679
-
680
594
  ####################
681
595
  # DB STRING PARSING
682
596
  ####################
@@ -1412,28 +1326,3 @@ def test_get_dbos_database_url(mocker):
1412
1326
  database="some_db",
1413
1327
  ).render_as_string(hide_password=False)
1414
1328
  assert get_dbos_database_url() == expected_url
1415
-
1416
-
1417
- def test_get_dbos_database_url_local_suffix(mocker):
1418
- mock_config = """
1419
- name: "some-app"
1420
- database:
1421
- hostname: 'localhost'
1422
- port: 5432
1423
- username: 'postgres'
1424
- password: ${PGPASSWORD}
1425
- app_db_name: 'some_db'
1426
- local_suffix: true
1427
- """
1428
- mocker.patch(
1429
- "builtins.open", side_effect=generate_mock_open(mock_filename, mock_config)
1430
- )
1431
- expected_url = URL.create(
1432
- "postgresql+psycopg",
1433
- username="postgres",
1434
- password=os.environ.get("PGPASSWORD"),
1435
- host="localhost",
1436
- port=5432,
1437
- database="some_db_local",
1438
- ).render_as_string(hide_password=False)
1439
- assert get_dbos_database_url() == expected_url