dbos 0.26.0a8__py3-none-any.whl → 0.26.0a9__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
@@ -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
dbos/cli/cli.py CHANGED
@@ -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",
@@ -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
@@ -1,25 +1,22 @@
1
- dbos-0.26.0a8.dist-info/METADATA,sha256=Ll7QmuHMijtBbtmGEJuVaYbcmNMKKSbZTh1aWcQUU9I,5553
2
- dbos-0.26.0a8.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- dbos-0.26.0a8.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
- dbos-0.26.0a8.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
1
+ dbos-0.26.0a9.dist-info/METADATA,sha256=Olpmk9xnLEbfqKPtj7_cLfCShqh_RFowiykc6KtBTNQ,5553
2
+ dbos-0.26.0a9.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ dbos-0.26.0a9.dist-info/entry_points.txt,sha256=_QOQ3tVfEjtjBlr1jS4sHqHya9lI2aIEIWkz8dqYp14,58
4
+ dbos-0.26.0a9.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
5
5
  dbos/__init__.py,sha256=3NQfGlBiiUSM_v88STdVP3rNZvGkUL_9WbSotKb8Voo,873
6
6
  dbos/__main__.py,sha256=G7Exn-MhGrVJVDbgNlpzhfh8WMX_72t3_oJaFT9Lmt8,653
7
7
  dbos/_admin_server.py,sha256=vxPG_YJ6lYrkfPCSp42FiATVLBOij7Fm52Yngg5Z_tE,7027
8
8
  dbos/_app_db.py,sha256=IwnNlHEQYp2bl5BM66vVPFa40h8DOtvRgUWTJ1dz20A,8963
9
9
  dbos/_classproperty.py,sha256=f0X-_BySzn3yFDRKB2JpCbLYQ9tLwt1XftfshvY7CBs,626
10
10
  dbos/_client.py,sha256=fzW_Gagh-oyWyDYtREcQDBesoVl_LsEoMeJAsn5-C5s,7262
11
- dbos/_cloudutils/authentication.py,sha256=V0fCWQN9stCkhbuuxgPTGpvuQcDqfU3KAxPAh01vKW4,5007
12
- dbos/_cloudutils/cloudutils.py,sha256=YC7jGsIopT0KveLsqbRpQk2KlRBk-nIRC_UCgep4f3o,7797
13
- dbos/_cloudutils/databases.py,sha256=_shqaqSvhY4n2ScgQ8IP5PDZvzvcx3YBKV8fj-cxhSY,8543
14
11
  dbos/_conductor/conductor.py,sha256=HYzVL29IMMrs2Mnms_7cHJynCnmmEN5SDQOMjzn3UoU,16840
15
12
  dbos/_conductor/protocol.py,sha256=xN7pmooyF1pqbH1b6WhllU5718P7zSb_b0KCwA6bzcs,6716
16
13
  dbos/_context.py,sha256=I8sLkdKTTkZEz7wG-MjynaQB6XEF2bLXuwNksiauP7w,19430
17
14
  dbos/_core.py,sha256=tjBGVbSgOn59lR29gcYi5f6fcKNKQM5EP1QXrQGUkXA,45426
18
15
  dbos/_croniter.py,sha256=XHAyUyibs_59sJQfSNWkP7rqQY6_XrlfuuCxk4jYqek,47559
19
- dbos/_db_wizard.py,sha256=VnMa6OL87Lc-XPDD1RnXp8NjsJE8YgiQLj3wtWAXp-8,8252
20
16
  dbos/_dbos.py,sha256=TOLi95Aca50huyOAWl9H5fii4nMYaGwN-zQ8GlLWdOg,45569
21
- dbos/_dbos_config.py,sha256=rTn30Hgh-RzTxqHbnYh2pC3Ioo30eJV9K4YxhJd-Gj4,22718
17
+ dbos/_dbos_config.py,sha256=m05IFjM0jSwZBsnFMF_4qP2JkjVFc0gqyM2tnotXq20,20636
22
18
  dbos/_debug.py,sha256=MNlQVZ6TscGCRQeEEL0VE8Uignvr6dPeDDDefS3xgIE,1823
19
+ dbos/_docker_pg_helper.py,sha256=9OGbuavRA_cwE-uPiLZJSdpbQu-6PPgl9clQZB2zT_U,5852
23
20
  dbos/_error.py,sha256=HtdV6Qy7qRyGD57wxLwE7YT0WdYtlx5ZLEe_Kv_gC-U,5953
24
21
  dbos/_fastapi.py,sha256=PhaKftbApHnjtYEOw0EYna_3K0cmz__J9of7mRJWzu4,3704
25
22
  dbos/_flask.py,sha256=DZKUZR5-xOzPI7tYZ53r2PvvHVoAb8SYwLzMVFsVfjI,2608
@@ -63,8 +60,8 @@ dbos/_utils.py,sha256=nFRUHzVjXG5AusF85AlYHikj63Tzi-kQm992ihsrAxA,201
63
60
  dbos/_workflow_commands.py,sha256=Tf7_hZQoPgP90KHQjMNlBggCNrLLCNRJxHtAJLvarc4,6153
64
61
  dbos/cli/_github_init.py,sha256=Y_bDF9gfO2jB1id4FV5h1oIxEJRWyqVjhb7bNEa5nQ0,3224
65
62
  dbos/cli/_template_init.py,sha256=-WW3kbq0W_Tq4WbMqb1UGJG3xvJb3woEY5VspG95Srk,2857
66
- dbos/cli/cli.py,sha256=FnI5ZAo-kAic-ij5wBqNJ2EJiYoBK1Ot-tTMh1WcXEM,16132
67
- dbos/dbos-config.schema.json,sha256=4z2OXPfp7H0uNT1m5dKxjg31qbAfPyKkFXwHufuUMec,5910
63
+ dbos/cli/cli.py,sha256=Lb_RYmXoT5KH0xDbwaYpROE4c-svZ0eCq2Kxg7cAxTw,16537
64
+ dbos/dbos-config.schema.json,sha256=i7jcxXqByKq0Jzv3nAUavONtj03vTwj6vWP4ylmBr8o,5694
68
65
  dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
69
66
  version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
70
- dbos-0.26.0a8.dist-info/RECORD,,
67
+ dbos-0.26.0a9.dist-info/RECORD,,
@@ -1,163 +0,0 @@
1
- import os
2
- import time
3
- from dataclasses import dataclass
4
- from typing import Any, Dict, Optional
5
-
6
- import jwt
7
- import requests
8
- from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
9
- from cryptography.x509 import load_pem_x509_certificate
10
- from rich import print
11
-
12
- from .._logger import dbos_logger
13
-
14
- # Constants
15
- DBOS_CLOUD_HOST = os.getenv("DBOS_DOMAIN", "cloud.dbos.dev")
16
- PRODUCTION_ENVIRONMENT = DBOS_CLOUD_HOST == "cloud.dbos.dev"
17
- AUTH0_DOMAIN = "login.dbos.dev" if PRODUCTION_ENVIRONMENT else "dbos-inc.us.auth0.com"
18
- DBOS_CLIENT_ID = (
19
- "6p7Sjxf13cyLMkdwn14MxlH7JdhILled"
20
- if PRODUCTION_ENVIRONMENT
21
- else "G38fLmVErczEo9ioCFjVIHea6yd0qMZu"
22
- )
23
- DBOS_CLOUD_IDENTIFIER = "dbos-cloud-api"
24
-
25
-
26
- @dataclass
27
- class DeviceCodeResponse:
28
- device_code: str
29
- user_code: str
30
- verification_uri: str
31
- verification_uri_complete: str
32
- expires_in: int
33
- interval: int
34
-
35
- @classmethod
36
- def from_dict(cls, data: Dict[str, Any]) -> "DeviceCodeResponse":
37
- return cls(
38
- device_code=data["device_code"],
39
- user_code=data["user_code"],
40
- verification_uri=data["verification_uri"],
41
- verification_uri_complete=data["verification_uri_complete"],
42
- expires_in=data["expires_in"],
43
- interval=data["interval"],
44
- )
45
-
46
-
47
- @dataclass
48
- class TokenResponse:
49
- access_token: str
50
- token_type: str
51
- expires_in: int
52
- refresh_token: Optional[str] = None
53
-
54
- @classmethod
55
- def from_dict(cls, data: Dict[str, Any]) -> "TokenResponse":
56
- return cls(
57
- access_token=data["access_token"],
58
- token_type=data["token_type"],
59
- expires_in=data["expires_in"],
60
- refresh_token=data.get("refresh_token"),
61
- )
62
-
63
-
64
- @dataclass
65
- class AuthenticationResponse:
66
- token: str
67
- refresh_token: Optional[str] = None
68
-
69
-
70
- class JWKSClient:
71
- def __init__(self, jwks_uri: str):
72
- self.jwks_uri = jwks_uri
73
-
74
- def get_signing_key(self, kid: str) -> RSAPublicKey:
75
- response = requests.get(self.jwks_uri)
76
- jwks = response.json()
77
- for key in jwks["keys"]:
78
- if key["kid"] == kid:
79
- cert_text = f"-----BEGIN CERTIFICATE-----\n{key['x5c'][0]}\n-----END CERTIFICATE-----"
80
- cert = load_pem_x509_certificate(cert_text.encode())
81
- return cert.public_key() # type: ignore
82
- raise Exception(f"Unable to find signing key with kid: {kid}")
83
-
84
-
85
- def verify_token(token: str) -> None:
86
- header = jwt.get_unverified_header(token)
87
-
88
- if not header.get("kid"):
89
- raise ValueError("Invalid token: No 'kid' in header")
90
-
91
- client = JWKSClient(f"https://{AUTH0_DOMAIN}/.well-known/jwks.json")
92
- signing_key = client.get_signing_key(header["kid"])
93
- jwt.decode(
94
- token,
95
- signing_key,
96
- algorithms=["RS256"],
97
- audience=DBOS_CLOUD_IDENTIFIER,
98
- options={
99
- "verify_iat": False,
100
- "clock_tolerance": 60,
101
- },
102
- )
103
-
104
-
105
- def authenticate(get_refresh_token: bool = False) -> Optional[AuthenticationResponse]:
106
- print(
107
- "[bold blue]Please authenticate with DBOS Cloud to access a Postgres database[/bold blue]"
108
- )
109
-
110
- # Get device code
111
- device_code_data = {
112
- "client_id": DBOS_CLIENT_ID,
113
- "scope": "offline_access" if get_refresh_token else "sub",
114
- "audience": DBOS_CLOUD_IDENTIFIER,
115
- }
116
-
117
- try:
118
- response = requests.post(
119
- f"https://{AUTH0_DOMAIN}/oauth/device/code",
120
- data=device_code_data,
121
- headers={"content-type": "application/x-www-form-urlencoded"},
122
- )
123
- device_code_response = DeviceCodeResponse.from_dict(response.json())
124
- except Exception as e:
125
- dbos_logger.error(f"Failed to log in: {str(e)}")
126
- return None
127
-
128
- login_url = device_code_response.verification_uri_complete
129
- print(f"[bold blue]Login URL:[/bold blue] {login_url}")
130
-
131
- # Poll for token
132
- token_data = {
133
- "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
134
- "device_code": device_code_response.device_code,
135
- "client_id": DBOS_CLIENT_ID,
136
- }
137
-
138
- elapsed_time_sec = 0
139
- token_response = None
140
-
141
- while elapsed_time_sec < device_code_response.expires_in:
142
- try:
143
- time.sleep(device_code_response.interval)
144
- elapsed_time_sec += device_code_response.interval
145
-
146
- response = requests.post(
147
- f"https://{AUTH0_DOMAIN}/oauth/token",
148
- data=token_data,
149
- headers={"content-type": "application/x-www-form-urlencoded"},
150
- )
151
- if response.status_code == 200:
152
- token_response = TokenResponse.from_dict(response.json())
153
- break
154
- except Exception:
155
- dbos_logger.info("Waiting for login...")
156
-
157
- if not token_response:
158
- return None
159
-
160
- verify_token(token_response.access_token)
161
- return AuthenticationResponse(
162
- token=token_response.access_token, refresh_token=token_response.refresh_token
163
- )
@@ -1,254 +0,0 @@
1
- import json
2
- import os
3
- import re
4
- import time
5
- from dataclasses import dataclass
6
- from enum import Enum
7
- from typing import Any, Optional, Union
8
-
9
- import jwt
10
- import requests
11
- import typer
12
- from rich import print
13
-
14
- from dbos._error import DBOSInitializationError
15
-
16
- from .._logger import dbos_logger
17
- from .authentication import authenticate
18
-
19
-
20
- # Must be the same as in TS
21
- @dataclass
22
- class DBOSCloudCredentials:
23
- token: str
24
- userName: str
25
- organization: str
26
- refreshToken: Optional[str] = None
27
-
28
-
29
- @dataclass
30
- class UserProfile:
31
- Name: str
32
- Organization: str
33
-
34
- def __init__(self, **kwargs: Any) -> None:
35
- self.Name = kwargs.get("Name", "")
36
- self.Organization = kwargs.get("Organization", "")
37
-
38
-
39
- class AppLanguages(Enum):
40
- Node = "node"
41
- Python = "python"
42
-
43
-
44
- dbos_config_file_path = "dbos-config.yaml"
45
- DBOS_CLOUD_HOST = os.getenv("DBOS_DOMAIN", "cloud.dbos.dev")
46
- dbos_env_path = ".dbos"
47
-
48
-
49
- def is_token_expired(token: str) -> bool:
50
- try:
51
- decoded = jwt.decode(token, options={"verify_signature": False})
52
- exp: int = decoded.get("exp")
53
- if not exp:
54
- return False
55
- return time.time() >= exp
56
- except Exception:
57
- return True
58
-
59
-
60
- def credentials_exist() -> bool:
61
- return os.path.exists(os.path.join(dbos_env_path, "credentials"))
62
-
63
-
64
- def delete_credentials() -> None:
65
- credentials_path = os.path.join(dbos_env_path, "credentials")
66
- if os.path.exists(credentials_path):
67
- os.unlink(credentials_path)
68
-
69
-
70
- def write_credentials(credentials: DBOSCloudCredentials) -> None:
71
- os.makedirs(dbos_env_path, exist_ok=True)
72
- with open(os.path.join(dbos_env_path, "credentials"), "w", encoding="utf-8") as f:
73
- json.dump(credentials.__dict__, f)
74
-
75
-
76
- def check_read_file(path: str, encoding: str = "utf-8") -> Union[str, bytes]:
77
- # Check if file exists and is a file
78
- if not os.path.exists(path):
79
- raise FileNotFoundError(f"File {path} does not exist")
80
- if not os.path.isfile(path):
81
- raise IsADirectoryError(f"Path {path} is not a file")
82
-
83
- # Read file content
84
- with open(path, encoding=encoding) as f:
85
- return f.read()
86
-
87
-
88
- @dataclass
89
- class CloudAPIErrorResponse:
90
- message: str
91
- status_code: int
92
- request_id: str
93
- detailed_error: Optional[str] = None
94
-
95
-
96
- def is_cloud_api_error_response(obj: Any) -> bool:
97
- return (
98
- isinstance(obj, dict)
99
- and "message" in obj
100
- and isinstance(obj["message"], str)
101
- and "statusCode" in obj
102
- and isinstance(obj["statusCode"], int)
103
- and "requestID" in obj
104
- and isinstance(obj["requestID"], str)
105
- )
106
-
107
-
108
- def handle_api_errors(label: str, e: requests.exceptions.RequestException) -> None:
109
- if hasattr(e, "response") and e.response is not None:
110
- resp = e.response.json()
111
- if is_cloud_api_error_response(resp):
112
- message = f"[{resp['requestID']}] {label}: {resp['message']}."
113
- dbos_logger.error(message)
114
- raise DBOSInitializationError(message)
115
-
116
-
117
- def is_valid_username(value: str) -> Union[bool, str]:
118
- if len(value) < 3 or len(value) > 30:
119
- return "Username must be 3~30 characters long"
120
- if not re.match("^[a-z0-9_]+$", value):
121
- return "Username must contain only lowercase letters, numbers, and underscores."
122
- return True
123
-
124
-
125
- def check_user_profile(credentials: DBOSCloudCredentials) -> bool:
126
- bearer_token = f"Bearer {credentials.token}"
127
- try:
128
- response = requests.get(
129
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/user/profile",
130
- headers={
131
- "Content-Type": "application/json",
132
- "Authorization": bearer_token,
133
- },
134
- )
135
- response.raise_for_status()
136
- profile = UserProfile(**response.json())
137
- credentials.userName = profile.Name
138
- credentials.organization = profile.Organization
139
- return True
140
- except requests.exceptions.RequestException as e:
141
- error_label = "Failed to login"
142
- if hasattr(e, "response") and e.response is not None:
143
- resp = e.response.json()
144
- if is_cloud_api_error_response(resp):
145
- if "user not found in DBOS Cloud" not in resp["message"]:
146
- handle_api_errors(error_label, e)
147
- exit(1)
148
- else:
149
- dbos_logger.error(f"{error_label}: {str(e)}")
150
- exit(1)
151
- return False
152
-
153
-
154
- def register_user(credentials: DBOSCloudCredentials) -> None:
155
- print("Please register for DBOS Cloud")
156
-
157
- user_name = None
158
- while not user_name:
159
- user_name = typer.prompt("Choose your username")
160
- validation_result = is_valid_username(user_name)
161
- if validation_result is not True:
162
- print(f"[red]Invalid username: {validation_result}[/red]")
163
- user_name = None
164
- continue
165
-
166
- bearer_token = f"Bearer {credentials.token}"
167
- try:
168
- # Register user
169
- response = requests.put(
170
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/user",
171
- json={
172
- "name": user_name,
173
- },
174
- headers={
175
- "Content-Type": "application/json",
176
- "Authorization": bearer_token,
177
- },
178
- )
179
- response.raise_for_status()
180
-
181
- # Get user profile
182
- response = requests.get(
183
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/user/profile",
184
- headers={
185
- "Content-Type": "application/json",
186
- "Authorization": bearer_token,
187
- },
188
- )
189
- response.raise_for_status()
190
- profile = UserProfile(**response.json())
191
- credentials.userName = profile.Name
192
- credentials.organization = profile.Organization
193
- print(f"[green]Successfully registered as {credentials.userName}[/green]")
194
-
195
- except requests.exceptions.RequestException as e:
196
- error_label = f"Failed to register user {user_name}"
197
- if hasattr(e, "response") and e.response is not None:
198
- handle_api_errors(error_label, e)
199
- else:
200
- dbos_logger.error(f"{error_label}: {str(e)}")
201
- exit(1)
202
-
203
-
204
- def check_credentials() -> DBOSCloudCredentials:
205
- empty_credentials = DBOSCloudCredentials(token="", userName="", organization="")
206
-
207
- if not credentials_exist():
208
- return empty_credentials
209
-
210
- try:
211
- with open(os.path.join(dbos_env_path, "credentials"), "r") as f:
212
- cred_data = json.load(f)
213
- credentials = DBOSCloudCredentials(**cred_data)
214
-
215
- # Trim trailing /r /n
216
- credentials.token = credentials.token.strip()
217
-
218
- if is_token_expired(credentials.token):
219
- print("Credentials expired. Logging in again...")
220
- delete_credentials()
221
- return empty_credentials
222
-
223
- return credentials
224
- except Exception as e:
225
- dbos_logger.error(f"Error loading credentials: {str(e)}")
226
- return empty_credentials
227
-
228
-
229
- def get_cloud_credentials() -> DBOSCloudCredentials:
230
- # Check if credentials exist and are not expired
231
- credentials = check_credentials()
232
-
233
- # Log in the user
234
- if not credentials.token:
235
- auth_response = authenticate()
236
- if auth_response is None:
237
- dbos_logger.error("Failed to login. Exiting...")
238
- exit(1)
239
- credentials.token = auth_response.token
240
- credentials.refreshToken = auth_response.refresh_token
241
- write_credentials(credentials)
242
-
243
- # Check if the user exists in DBOS Cloud
244
- user_exists = check_user_profile(credentials)
245
- if user_exists:
246
- write_credentials(credentials)
247
- print(f"[green]Successfully logged in as {credentials.userName}[/green]")
248
- return credentials
249
-
250
- # User doesn't exist, register the user in DBOS Cloud
251
- register_user(credentials)
252
- write_credentials(credentials)
253
-
254
- return credentials
@@ -1,241 +0,0 @@
1
- import base64
2
- import random
3
- import time
4
- from dataclasses import dataclass
5
- from typing import Any, List, Optional
6
-
7
- import requests
8
- from rich import print
9
-
10
- from dbos._cloudutils.cloudutils import (
11
- DBOS_CLOUD_HOST,
12
- DBOSCloudCredentials,
13
- handle_api_errors,
14
- is_cloud_api_error_response,
15
- )
16
- from dbos._error import DBOSInitializationError
17
-
18
- from .._logger import dbos_logger
19
-
20
-
21
- @dataclass
22
- class UserDBCredentials:
23
- RoleName: str
24
- Password: str
25
-
26
- def __init__(self, **kwargs: Any) -> None:
27
- self.RoleName = kwargs.get("RoleName", "")
28
- self.Password = kwargs.get("Password", "")
29
-
30
-
31
- @dataclass
32
- class UserDBInstance:
33
- PostgresInstanceName: str = ""
34
- Status: str = ""
35
- HostName: str = ""
36
- Port: int = 0
37
- DatabaseUsername: str = ""
38
- IsLinked: bool = False
39
- SupabaseReference: Optional[str] = None
40
-
41
- def __init__(self, **kwargs: Any) -> None:
42
- self.PostgresInstanceName = kwargs.get("PostgresInstanceName", "")
43
- self.Status = kwargs.get("Status", "")
44
- self.HostName = kwargs.get("HostName", "")
45
- self.Port = kwargs.get("Port", 0)
46
- self.DatabaseUsername = kwargs.get("DatabaseUsername", "")
47
- self.IsLinked = kwargs.get("IsLinked", False)
48
- self.SupabaseReference = kwargs.get("SupabaseReference", None)
49
-
50
-
51
- def get_user_db_info(credentials: DBOSCloudCredentials, db_name: str) -> UserDBInstance:
52
- bearer_token = f"Bearer {credentials.token}"
53
-
54
- try:
55
- response = requests.get(
56
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/info/{db_name}",
57
- headers={
58
- "Content-Type": "application/json",
59
- "Authorization": bearer_token,
60
- },
61
- )
62
- response.raise_for_status()
63
- data = response.json()
64
- return UserDBInstance(**data)
65
- except requests.exceptions.RequestException as e:
66
- error_label = f"Failed to get status of database {db_name}"
67
- if hasattr(e, "response") and e.response is not None:
68
- resp = e.response.json()
69
- if is_cloud_api_error_response(resp):
70
- handle_api_errors(error_label, e)
71
- else:
72
- dbos_logger.error(f"{error_label}: {str(e)}")
73
- raise DBOSInitializationError(f"{error_label}: {str(e)}")
74
-
75
-
76
- def get_user_db_credentials(
77
- credentials: DBOSCloudCredentials, db_name: str
78
- ) -> UserDBCredentials:
79
- bearer_token = f"Bearer {credentials.token}"
80
- try:
81
- response = requests.get(
82
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/{db_name}/credentials",
83
- headers={
84
- "Content-Type": "application/json",
85
- "Authorization": bearer_token,
86
- },
87
- )
88
- response.raise_for_status()
89
- data = response.json()
90
- return UserDBCredentials(**data)
91
- except requests.exceptions.RequestException as e:
92
- error_label = f"Failed to get credentials for database {db_name}"
93
- if hasattr(e, "response") and e.response is not None:
94
- resp = e.response.json()
95
- if is_cloud_api_error_response(resp):
96
- handle_api_errors(error_label, e)
97
- else:
98
- dbos_logger.error(f"{error_label}: {str(e)}")
99
- raise DBOSInitializationError(f"{error_label}: {str(e)}")
100
-
101
-
102
- def create_user_role(credentials: DBOSCloudCredentials, db_name: str) -> None:
103
- bearer_token = f"Bearer {credentials.token}"
104
- try:
105
- response = requests.post(
106
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb/{db_name}/createuserdbrole",
107
- headers={
108
- "Content-Type": "application/json",
109
- "Authorization": bearer_token,
110
- },
111
- )
112
- response.raise_for_status()
113
- except requests.exceptions.RequestException as e:
114
- error_label = f"Failed to create a user role for database {db_name}"
115
- if hasattr(e, "response") and e.response is not None:
116
- resp = e.response.json()
117
- if is_cloud_api_error_response(resp):
118
- handle_api_errors(error_label, e)
119
- else:
120
- dbos_logger.error(f"{error_label}: {str(e)}")
121
- raise DBOSInitializationError(f"{error_label}: {str(e)}")
122
-
123
-
124
- def create_user_db(
125
- credentials: DBOSCloudCredentials,
126
- db_name: str,
127
- app_db_username: str,
128
- app_db_password: str,
129
- ) -> int:
130
- bearer_token = f"Bearer {credentials.token}"
131
-
132
- try:
133
- response = requests.post(
134
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases/userdb",
135
- json={
136
- "Name": db_name,
137
- "AdminName": app_db_username,
138
- "AdminPassword": app_db_password,
139
- },
140
- headers={
141
- "Content-Type": "application/json",
142
- "Authorization": bearer_token,
143
- },
144
- )
145
- response.raise_for_status()
146
-
147
- print(f"Successfully started provisioning {db_name}")
148
-
149
- status = ""
150
- while status not in ["available", "backing-up"]:
151
- if status == "":
152
- time.sleep(5) # First time sleep 5 sec
153
- else:
154
- time.sleep(30) # Otherwise, sleep 30 sec
155
-
156
- user_db_info = get_user_db_info(credentials, db_name)
157
- status = user_db_info.Status
158
- print(
159
- f"[bold blue]Waiting for cloud database to finish provisioning. Status:[/bold blue] [yellow]{status}[/yellow]"
160
- )
161
-
162
- print("[green]Database successfully provisioned![/green]")
163
- return 0
164
-
165
- except requests.exceptions.RequestException as e:
166
- error_label = f"Failed to create database {db_name}"
167
- if hasattr(e, "response") and e.response is not None:
168
- resp = e.response.json()
169
- if is_cloud_api_error_response(resp):
170
- handle_api_errors(error_label, e)
171
- else:
172
- dbos_logger.error(f"{error_label}: {str(e)}")
173
- return 1
174
-
175
-
176
- def choose_database(credentials: DBOSCloudCredentials) -> Optional[UserDBInstance]:
177
- # List existing database instances
178
- user_dbs: List[UserDBInstance] = []
179
- bearer_token = f"Bearer {credentials.token}"
180
-
181
- try:
182
- response = requests.get(
183
- f"https://{DBOS_CLOUD_HOST}/v1alpha1/{credentials.organization}/databases",
184
- headers={
185
- "Content-Type": "application/json",
186
- "Authorization": bearer_token,
187
- },
188
- )
189
- response.raise_for_status()
190
- data = response.json()
191
- user_dbs = [UserDBInstance(**db) for db in data]
192
-
193
- except requests.exceptions.RequestException as e:
194
- error_label = "Failed to list databases"
195
- if hasattr(e, "response") and e.response is not None:
196
- resp = e.response.json()
197
- if is_cloud_api_error_response(resp):
198
- handle_api_errors(error_label, e)
199
- else:
200
- dbos_logger.error(f"{error_label}: {str(e)}")
201
- return None
202
-
203
- if not user_dbs:
204
- # If not, prompt the user to provision one
205
- print("Provisioning a cloud Postgres database server")
206
- user_db_name = f"{credentials.userName}-db-server"
207
-
208
- # Use a default user name and auto generated password
209
- app_db_username = "dbos_user"
210
- app_db_password = base64.b64encode(str(random.random()).encode()).decode()
211
- res = create_user_db(
212
- credentials, user_db_name, app_db_username, app_db_password
213
- )
214
- if res != 0:
215
- return None
216
- elif len(user_dbs) > 1:
217
- # If there is more than one database instance, prompt the user to select one
218
- choices = [db.PostgresInstanceName for db in user_dbs]
219
- print("Choose a database instance for this app:")
220
- for i, entry in enumerate(choices, 1):
221
- print(f"{i}. {entry}")
222
- while True:
223
- try:
224
- choice = int(input("Enter number: ")) - 1
225
- if 0 <= choice < len(choices):
226
- user_db_name = choices[choice]
227
- break
228
- except ValueError:
229
- continue
230
- print("Invalid choice, please try again")
231
- else:
232
- # Use the only available database server
233
- user_db_name = user_dbs[0].PostgresInstanceName
234
- print(f"[green]Using database instance:[/green] {user_db_name}")
235
-
236
- info = get_user_db_info(credentials, user_db_name)
237
-
238
- if not info.IsLinked:
239
- create_user_role(credentials, user_db_name)
240
-
241
- return info
dbos/_db_wizard.py DELETED
@@ -1,220 +0,0 @@
1
- import json
2
- import os
3
- import time
4
- from typing import TYPE_CHECKING, Optional, TypedDict, cast
5
-
6
- import docker # type: ignore
7
- import typer
8
- import yaml
9
- from rich import print
10
- from sqlalchemy import URL, create_engine, text
11
-
12
- if TYPE_CHECKING:
13
- from ._dbos_config import ConfigFile
14
-
15
- from ._cloudutils.cloudutils import get_cloud_credentials
16
- from ._cloudutils.databases import choose_database, get_user_db_credentials
17
- from ._error import DBOSInitializationError
18
- from ._logger import dbos_logger
19
-
20
- DB_CONNECTION_PATH = os.path.join(".dbos", "db_connection")
21
-
22
-
23
- class DatabaseConnection(TypedDict):
24
- hostname: Optional[str]
25
- port: Optional[int]
26
- username: Optional[str]
27
- password: Optional[str]
28
- local_suffix: Optional[bool]
29
-
30
-
31
- def db_wizard(config: "ConfigFile") -> "ConfigFile":
32
- """Checks database connectivity and helps the user start a database if needed
33
-
34
- First, check connectivity to the database configured in the provided `config` object.
35
- If it fails:
36
- - Return an error if the connection failed due to incorrect credentials.
37
- - Return an error if it detects a non-default configuration.
38
- - Otherwise assume the configured database is not running and guide the user through setting it up.
39
-
40
- The wizard will first attempt to start a local Postgres instance using Docker.
41
- If Docker is not available, it will prompt the user to connect to a DBOS Cloud database.
42
-
43
- Finally, if a database was configured, its connection details will be saved in the local `.dbos/db_connection` file.
44
- """
45
- # 1. Check the connectivity to the database. Return if successful. If cannot connect, continue to the following steps.
46
- db_connection_error = _check_db_connectivity(config)
47
- if db_connection_error is None:
48
- return config
49
-
50
- # 2. If the error is due to password authentication or the configuration is non-default, surface the error and exit.
51
- error_str = str(db_connection_error)
52
- dbos_logger.debug(f"Error connecting to Postgres: {error_str}")
53
- if (
54
- "password authentication failed" in error_str
55
- or "28P01" in error_str
56
- or "no password supplied" in error_str
57
- ):
58
- raise DBOSInitializationError(
59
- f"Could not connect to Postgres: password authentication failed: {db_connection_error}"
60
- )
61
-
62
- # If the database config is not the default one, surface the error and exit.
63
- db_config = config["database"] # FIXME: what if database is not in config?
64
- if (
65
- db_config["hostname"] != "localhost"
66
- or db_config["port"] != 5432
67
- or db_config["username"] != "postgres"
68
- ):
69
- raise DBOSInitializationError(
70
- f"Could not connect to the database. Exception: {db_connection_error}"
71
- )
72
-
73
- print("[yellow]Postgres not detected locally[/yellow]")
74
-
75
- # 3. If the database config is the default one, check if the user has Docker properly installed.
76
- print("Attempting to start Postgres via Docker")
77
- has_docker = _check_docker_installed()
78
-
79
- # 4. If Docker is installed, prompt the user to start a local Docker based Postgres, and then set the PGPASSWORD to 'dbos' and try to connect to the database.
80
- docker_started = False
81
- if has_docker:
82
- docker_started = _start_docker_postgres(config)
83
- else:
84
- print("[yellow]Docker not detected locally[/yellow]")
85
-
86
- # 5. If no Docker, then prompt the user to log in to DBOS Cloud and provision a DB there. Wait for the remote DB to be ready, and then create a copy of the original config file, and then load the remote connection string to the local config file.
87
- if not docker_started:
88
- print("Attempting to connect to Postgres via DBOS Cloud")
89
- cred = get_cloud_credentials()
90
- db = choose_database(cred)
91
- if db is None:
92
- raise DBOSInitializationError("Error connecting to cloud database")
93
- config["database"]["hostname"] = db.HostName
94
- config["database"]["port"] = db.Port
95
- if db.SupabaseReference is not None:
96
- config["database"]["username"] = f"postgres.{db.SupabaseReference}"
97
- supabase_password = typer.prompt(
98
- "Enter your Supabase database password", hide_input=True
99
- )
100
- config["database"]["password"] = supabase_password
101
- else:
102
- config["database"]["username"] = db.DatabaseUsername
103
- db_credentials = get_user_db_credentials(cred, db.PostgresInstanceName)
104
- config["database"]["password"] = db_credentials.Password
105
- config["database"]["local_suffix"] = True
106
-
107
- # Verify these new credentials work
108
- db_connection_error = _check_db_connectivity(config)
109
- if db_connection_error is not None:
110
- raise DBOSInitializationError(
111
- f"Could not connect to the database. Exception: {db_connection_error}"
112
- )
113
-
114
- # 6. Save the config to the database connection file
115
- updated_connection = DatabaseConnection(
116
- hostname=config["database"]["hostname"],
117
- port=config["database"]["port"],
118
- username=config["database"]["username"],
119
- password=config["database"]["password"],
120
- local_suffix=config["database"]["local_suffix"],
121
- )
122
- save_db_connection(updated_connection)
123
- return config
124
-
125
-
126
- def _start_docker_postgres(config: "ConfigFile") -> bool:
127
- print("Starting a Postgres Docker container...")
128
- client = docker.from_env()
129
- pg_data = "/var/lib/postgresql/data"
130
- container_name = "dbos-db"
131
- client.containers.run(
132
- image="pgvector/pgvector:pg16",
133
- detach=True,
134
- environment={
135
- "POSTGRES_PASSWORD": config["database"]["password"],
136
- "PGDATA": pg_data,
137
- },
138
- volumes={pg_data: {"bind": pg_data, "mode": "rw"}},
139
- ports={"5432/tcp": config["database"]["port"]},
140
- name=container_name,
141
- remove=True,
142
- )
143
-
144
- container = client.containers.get(container_name)
145
- attempts = 30
146
- while attempts > 0:
147
- if attempts % 5 == 0:
148
- print("Waiting for Postgres Docker container to start...")
149
- try:
150
- res = container.exec_run("psql -U postgres -c 'SELECT 1;'")
151
- if res.exit_code != 0:
152
- attempts -= 1
153
- time.sleep(1)
154
- continue
155
- print("[green]Postgres Docker container started successfully![/green]")
156
- break
157
- except:
158
- attempts -= 1
159
- time.sleep(1)
160
-
161
- if attempts == 0:
162
- print("[yellow]Failed to start Postgres Docker container.[/yellow]")
163
- return False
164
-
165
- return True
166
-
167
-
168
- def _check_docker_installed() -> bool:
169
- # Check if Docker is installed
170
- try:
171
- client = docker.from_env()
172
- client.ping()
173
- except Exception:
174
- return False
175
- return True
176
-
177
-
178
- def _check_db_connectivity(config: "ConfigFile") -> Optional[Exception]:
179
- postgres_db_url = URL.create(
180
- "postgresql+psycopg",
181
- username=config["database"]["username"],
182
- password=config["database"]["password"],
183
- host=config["database"]["hostname"],
184
- port=config["database"]["port"],
185
- database="postgres",
186
- query={"connect_timeout": "2"},
187
- )
188
- postgres_db_engine = create_engine(postgres_db_url)
189
- try:
190
- with postgres_db_engine.connect() as conn:
191
- conn.execute(text("SELECT 1")).scalar()
192
- except Exception as e:
193
- return e
194
- finally:
195
- postgres_db_engine.dispose()
196
-
197
- return None
198
-
199
-
200
- def load_db_connection() -> DatabaseConnection:
201
- try:
202
- with open(DB_CONNECTION_PATH, "r") as f:
203
- data = json.load(f)
204
- return DatabaseConnection(
205
- hostname=data.get("hostname", None),
206
- port=data.get("port", None),
207
- username=data.get("username", None),
208
- password=data.get("password", None),
209
- local_suffix=data.get("local_suffix", None),
210
- )
211
- except:
212
- return DatabaseConnection(
213
- hostname=None, port=None, username=None, password=None, local_suffix=None
214
- )
215
-
216
-
217
- def save_db_connection(connection: DatabaseConnection) -> None:
218
- os.makedirs(".dbos", exist_ok=True)
219
- with open(DB_CONNECTION_PATH, "w") as f:
220
- json.dump(connection, f)