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.
- {dbos-0.26.0a8 → dbos-0.26.0a9}/PKG-INFO +1 -1
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_dbos_config.py +4 -54
- dbos-0.26.0a9/dbos/_docker_pg_helper.py +191 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/cli/cli.py +17 -1
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/dbos-config.schema.json +0 -4
- {dbos-0.26.0a8 → dbos-0.26.0a9}/pyproject.toml +2 -1
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_config.py +6 -117
- dbos-0.26.0a8/dbos/_cloudutils/authentication.py +0 -163
- dbos-0.26.0a8/dbos/_cloudutils/cloudutils.py +0 -254
- dbos-0.26.0a8/dbos/_cloudutils/databases.py +0 -241
- dbos-0.26.0a8/dbos/_db_wizard.py +0 -220
- dbos-0.26.0a8/tests/test_dbwizard.py +0 -84
- {dbos-0.26.0a8 → dbos-0.26.0a9}/LICENSE +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/README.md +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/__init__.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/__main__.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_admin_server.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_app_db.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_classproperty.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_client.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_conductor/conductor.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_conductor/protocol.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_context.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_core.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_croniter.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_dbos.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_debug.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_error.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_fastapi.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_flask.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_kafka.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_kafka_message.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_logger.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/env.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/script.py.mako +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_outcome.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_queue.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_recovery.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_registrations.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_request.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_roles.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_scheduler.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_schemas/__init__.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_schemas/application_database.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_schemas/system_database.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_serialization.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_sys_db.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/__package/main.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_tracer.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_utils.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/_workflow_commands.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/cli/_github_init.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/cli/_template_init.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/dbos/py.typed +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/__init__.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/atexit_no_ctor.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/atexit_no_launch.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/classdefs.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/client_collateral.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/client_worker.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/conftest.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/more_classdefs.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/queuedworkflow.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_admin_server.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_async.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_classdecorators.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_client.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_concurrency.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_croniter.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_dbos.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_debug.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_docker_secrets.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_failures.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_fastapi.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_fastapi_roles.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_flask.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_kafka.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_outcome.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_package.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_queue.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_scheduler.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_schema_migration.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_singleton.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_spans.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_sqlalchemy.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_workflow_introspection.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/tests/test_workflow_management.py +0 -0
- {dbos-0.26.0a8 → dbos-0.26.0a9}/version/__init__.py +0 -0
@@ -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,
|
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
|
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.
|
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
|
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
|
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
|
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"] == "
|
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
|
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
|
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
|