dbos 0.19.0a4__py3-none-any.whl → 0.20.0__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/cli.py DELETED
@@ -1,337 +0,0 @@
1
- import os
2
- import platform
3
- import shutil
4
- import signal
5
- import subprocess
6
- import time
7
- import typing
8
- from os import path
9
- from typing import Any
10
-
11
- import sqlalchemy as sa
12
- import tomlkit
13
- import typer
14
- from rich import print
15
- from rich.prompt import Prompt
16
- from typing_extensions import Annotated
17
-
18
- from dbos._schemas.system_database import SystemSchema
19
-
20
- from . import load_config
21
- from ._app_db import ApplicationDatabase
22
- from ._dbos_config import _is_valid_app_name
23
- from ._sys_db import SystemDatabase
24
-
25
- app = typer.Typer()
26
-
27
-
28
- def _on_windows() -> bool:
29
- return platform.system() == "Windows"
30
-
31
-
32
- @app.command(
33
- help="Start your DBOS application using the start commands in 'dbos-config.yaml'"
34
- )
35
- def start() -> None:
36
- config = load_config()
37
- start_commands = config["runtimeConfig"]["start"]
38
- typer.echo("Executing start commands from 'dbos-config.yaml'")
39
- for command in start_commands:
40
- typer.echo(f"Executing: {command}")
41
-
42
- # Run the command in the child process.
43
- # On Unix-like systems, set its process group
44
- process = subprocess.Popen(
45
- command,
46
- shell=True,
47
- text=True,
48
- preexec_fn=os.setsid if not _on_windows() else None,
49
- )
50
-
51
- def signal_handler(signum: int, frame: Any) -> None:
52
- """
53
- Forward kill signals to children.
54
-
55
- When we receive a signal, send it to the entire process group of the child.
56
- If that doesn't work, SIGKILL them then exit.
57
- """
58
- # Send the signal to the child's entire process group
59
- if process.poll() is None:
60
- os.killpg(os.getpgid(process.pid), signum)
61
-
62
- # Give some time for the child to terminate
63
- for _ in range(10): # Wait up to 1 second
64
- if process.poll() is not None:
65
- break
66
- time.sleep(0.1)
67
-
68
- # If the child is still running, force kill it
69
- if process.poll() is None:
70
- os.killpg(os.getpgid(process.pid), signal.SIGKILL)
71
-
72
- # Exit immediately
73
- os._exit(process.returncode if process.returncode is not None else 1)
74
-
75
- # Configure the single handler only on Unix-like systems.
76
- # TODO: Also kill the children on Windows.
77
- if not _on_windows():
78
- signal.signal(signal.SIGINT, signal_handler)
79
- signal.signal(signal.SIGTERM, signal_handler)
80
- process.wait()
81
-
82
-
83
- def _get_templates_directory() -> str:
84
- import dbos
85
-
86
- package_dir = path.abspath(path.dirname(dbos.__file__))
87
- return path.join(package_dir, "_templates")
88
-
89
-
90
- def _copy_dbos_template(src: str, dst: str, ctx: dict[str, str]) -> None:
91
- with open(src, "r") as f:
92
- content = f.read()
93
-
94
- for key, value in ctx.items():
95
- content = content.replace(f"${{{key}}}", value)
96
-
97
- with open(dst, "w") as f:
98
- f.write(content)
99
-
100
-
101
- def _copy_template_dir(src_dir: str, dst_dir: str, ctx: dict[str, str]) -> None:
102
-
103
- for root, dirs, files in os.walk(src_dir, topdown=True):
104
- dirs[:] = [d for d in dirs if d != "__package"]
105
-
106
- dst_root = path.join(dst_dir, path.relpath(root, src_dir))
107
- if len(dirs) == 0:
108
- os.makedirs(dst_root, exist_ok=True)
109
- else:
110
- for dir in dirs:
111
- os.makedirs(path.join(dst_root, dir), exist_ok=True)
112
-
113
- for file in files:
114
- src = path.join(root, file)
115
- base, ext = path.splitext(file)
116
-
117
- dst = path.join(dst_root, base if ext == ".dbos" else file)
118
- if path.exists(dst):
119
- print(f"[yellow]File {dst} already exists, skipping[/yellow]")
120
- continue
121
-
122
- if ext == ".dbos":
123
- _copy_dbos_template(src, dst, ctx)
124
- else:
125
- shutil.copy(src, dst)
126
-
127
-
128
- def _copy_template(src_dir: str, project_name: str, config_mode: bool) -> None:
129
-
130
- dst_dir = path.abspath(".")
131
-
132
- package_name = project_name.replace("-", "_")
133
- ctx = {
134
- "project_name": project_name,
135
- "package_name": package_name,
136
- "migration_command": "alembic upgrade head",
137
- }
138
-
139
- if config_mode:
140
- ctx["package_name"] = "."
141
- ctx["migration_command"] = "echo 'No migrations specified'"
142
- _copy_dbos_template(
143
- os.path.join(src_dir, "dbos-config.yaml.dbos"),
144
- os.path.join(dst_dir, "dbos-config.yaml"),
145
- ctx,
146
- )
147
- else:
148
- _copy_template_dir(src_dir, dst_dir, ctx)
149
- _copy_template_dir(
150
- path.join(src_dir, "__package"), path.join(dst_dir, package_name), ctx
151
- )
152
-
153
-
154
- def _get_project_name() -> typing.Union[str, None]:
155
- name = None
156
- try:
157
- with open("pyproject.toml", "rb") as file:
158
- pyproj = typing.cast(dict[str, Any], tomlkit.load(file))
159
- name = typing.cast(str, pyproj["project"]["name"])
160
- except:
161
- pass
162
-
163
- if name == None:
164
- try:
165
- _, parent = path.split(path.abspath("."))
166
- name = parent
167
- except:
168
- pass
169
-
170
- return name
171
-
172
-
173
- @app.command(help="Initialize a new DBOS application from a template")
174
- def init(
175
- project_name: Annotated[
176
- typing.Optional[str], typer.Argument(help="Specify application name")
177
- ] = None,
178
- template: Annotated[
179
- typing.Optional[str],
180
- typer.Option("--template", "-t", help="Specify template to use"),
181
- ] = None,
182
- config: Annotated[
183
- bool,
184
- typer.Option("--config", "-c", help="Only add dbos-config.yaml"),
185
- ] = False,
186
- ) -> None:
187
- try:
188
- if project_name is None:
189
- project_name = typing.cast(
190
- str, typer.prompt("What is your project's name?", _get_project_name())
191
- )
192
-
193
- if not _is_valid_app_name(project_name):
194
- raise Exception(
195
- f"{project_name} is an invalid DBOS app name. App names must be between 3 and 30 characters long and contain only lowercase letters, numbers, dashes, and underscores."
196
- )
197
-
198
- templates_dir = _get_templates_directory()
199
- templates = [x.name for x in os.scandir(templates_dir) if x.is_dir()]
200
- if len(templates) == 0:
201
- raise Exception(f"no DBOS templates found in {templates_dir} ")
202
-
203
- if template == None:
204
- if len(templates) == 1:
205
- template = templates[0]
206
- else:
207
- template = Prompt.ask(
208
- "Which project template do you want to use?", choices=templates
209
- )
210
- else:
211
- if template not in templates:
212
- raise Exception(f"template {template} not found in {templates_dir}")
213
-
214
- _copy_template(
215
- path.join(templates_dir, template), project_name, config_mode=config
216
- )
217
- except Exception as e:
218
- print(f"[red]{e}[/red]")
219
-
220
-
221
- @app.command(
222
- help="Run your database schema migrations using the migration commands in 'dbos-config.yaml'"
223
- )
224
- def migrate() -> None:
225
- config = load_config()
226
- if not config["database"]["password"]:
227
- typer.echo(
228
- "DBOS configuration does not contain database password, please check your config file and retry!"
229
- )
230
- raise typer.Exit(code=1)
231
- app_db_name = config["database"]["app_db_name"]
232
-
233
- typer.echo(f"Starting schema migration for database {app_db_name}")
234
-
235
- # First, run DBOS migrations on the system database and the application database
236
- app_db = None
237
- sys_db = None
238
- try:
239
- sys_db = SystemDatabase(config)
240
- app_db = ApplicationDatabase(config)
241
- except Exception as e:
242
- typer.echo(f"DBOS system schema migration failed: {e}")
243
- finally:
244
- if sys_db:
245
- sys_db.destroy()
246
- if app_db:
247
- app_db.destroy()
248
-
249
- # Next, run any custom migration commands specified in the configuration
250
- typer.echo("Executing migration commands from 'dbos-config.yaml'")
251
- try:
252
- migrate_commands = (
253
- config["database"]["migrate"]
254
- if "migrate" in config["database"] and config["database"]["migrate"]
255
- else []
256
- )
257
- for command in migrate_commands:
258
- typer.echo(f"Executing migration command: {command}")
259
- result = subprocess.run(command, shell=True, text=True)
260
- if result.returncode != 0:
261
- typer.echo(f"Migration command failed: {command}")
262
- typer.echo(result.stderr)
263
- raise typer.Exit(1)
264
- if result.stdout:
265
- typer.echo(result.stdout.rstrip())
266
- except Exception as e:
267
- typer.echo(f"An error occurred during schema migration: {e}")
268
- raise typer.Exit(code=1)
269
-
270
- typer.echo(f"Completed schema migration for database {app_db_name}")
271
-
272
-
273
- @app.command(help="Reset the DBOS system database")
274
- def reset(
275
- yes: bool = typer.Option(False, "-y", "--yes", help="Skip confirmation prompt")
276
- ) -> None:
277
- if not yes:
278
- confirm = typer.confirm(
279
- "This command resets your DBOS system database, deleting metadata about past workflows and steps. Are you sure you want to proceed?"
280
- )
281
- if not confirm:
282
- typer.echo("Operation cancelled.")
283
- raise typer.Exit()
284
- config = load_config()
285
- sysdb_name = (
286
- config["database"]["sys_db_name"]
287
- if "sys_db_name" in config["database"] and config["database"]["sys_db_name"]
288
- else config["database"]["app_db_name"] + SystemSchema.sysdb_suffix
289
- )
290
- postgres_db_url = sa.URL.create(
291
- "postgresql+psycopg",
292
- username=config["database"]["username"],
293
- password=config["database"]["password"],
294
- host=config["database"]["hostname"],
295
- port=config["database"]["port"],
296
- database="postgres",
297
- )
298
- try:
299
- # Connect to postgres default database
300
- engine = sa.create_engine(postgres_db_url)
301
-
302
- with engine.connect() as conn:
303
- # Set autocommit required for database dropping
304
- conn.execution_options(isolation_level="AUTOCOMMIT")
305
-
306
- # Terminate existing connections
307
- conn.execute(
308
- sa.text(
309
- """
310
- SELECT pg_terminate_backend(pg_stat_activity.pid)
311
- FROM pg_stat_activity
312
- WHERE pg_stat_activity.datname = :db_name
313
- AND pid <> pg_backend_pid()
314
- """
315
- ),
316
- {"db_name": sysdb_name},
317
- )
318
-
319
- # Drop the database
320
- conn.execute(sa.text(f"DROP DATABASE IF EXISTS {sysdb_name}"))
321
-
322
- except sa.exc.SQLAlchemyError as e:
323
- typer.echo(f"Error dropping database: {str(e)}")
324
- return
325
-
326
- sys_db = None
327
- try:
328
- sys_db = SystemDatabase(config)
329
- except Exception as e:
330
- typer.echo(f"DBOS system schema migration failed: {e}")
331
- finally:
332
- if sys_db:
333
- sys_db.destroy()
334
-
335
-
336
- if __name__ == "__main__":
337
- app()
File without changes
File without changes
File without changes