dbos 0.13.0a2__tar.gz → 0.14.0a5__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.

Potentially problematic release.


This version of dbos might be problematic. Click here for more details.

Files changed (75) hide show
  1. {dbos-0.13.0a2 → dbos-0.14.0a5}/PKG-INFO +1 -1
  2. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_dbos.py +16 -8
  3. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_dbos_config.py +2 -2
  4. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_sys_db.py +7 -0
  5. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/cli.py +76 -5
  6. {dbos-0.13.0a2 → dbos-0.14.0a5}/pyproject.toml +1 -1
  7. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_config.py +3 -0
  8. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_failures.py +43 -2
  9. {dbos-0.13.0a2 → dbos-0.14.0a5}/LICENSE +0 -0
  10. {dbos-0.13.0a2 → dbos-0.14.0a5}/README.md +0 -0
  11. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/__init__.py +0 -0
  12. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_admin_sever.py +0 -0
  13. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_app_db.py +0 -0
  14. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_classproperty.py +0 -0
  15. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_context.py +0 -0
  16. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_core.py +0 -0
  17. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_croniter.py +0 -0
  18. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_error.py +0 -0
  19. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_fastapi.py +0 -0
  20. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_flask.py +0 -0
  21. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_kafka.py +0 -0
  22. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_kafka_message.py +0 -0
  23. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_logger.py +0 -0
  24. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/env.py +0 -0
  25. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/script.py.mako +0 -0
  26. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  27. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  28. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  29. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  30. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  31. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  32. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_queue.py +0 -0
  33. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_recovery.py +0 -0
  34. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_registrations.py +0 -0
  35. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_request.py +0 -0
  36. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_roles.py +0 -0
  37. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_scheduler.py +0 -0
  38. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_schemas/__init__.py +0 -0
  39. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_schemas/application_database.py +0 -0
  40. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_schemas/system_database.py +0 -0
  41. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_serialization.py +0 -0
  42. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/README.md +0 -0
  43. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/__package/__init__.py +0 -0
  44. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/__package/main.py +0 -0
  45. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/__package/schema.py +0 -0
  46. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/alembic.ini +0 -0
  47. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/dbos-config.yaml.dbos +0 -0
  48. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/migrations/env.py.dbos +0 -0
  49. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/migrations/script.py.mako +0 -0
  50. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/migrations/versions/2024_07_31_180642_init.py +0 -0
  51. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_templates/hello/start_postgres_docker.py +0 -0
  52. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/_tracer.py +0 -0
  53. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/dbos-config.schema.json +0 -0
  54. {dbos-0.13.0a2 → dbos-0.14.0a5}/dbos/py.typed +0 -0
  55. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/__init__.py +0 -0
  56. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/atexit_no_ctor.py +0 -0
  57. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/atexit_no_launch.py +0 -0
  58. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/classdefs.py +0 -0
  59. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/conftest.py +0 -0
  60. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/more_classdefs.py +0 -0
  61. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_admin_server.py +0 -0
  62. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_classdecorators.py +0 -0
  63. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_concurrency.py +0 -0
  64. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_croniter.py +0 -0
  65. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_dbos.py +0 -0
  66. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_fastapi.py +0 -0
  67. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_fastapi_roles.py +0 -0
  68. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_flask.py +0 -0
  69. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_kafka.py +0 -0
  70. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_package.py +0 -0
  71. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_queue.py +0 -0
  72. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_scheduler.py +0 -0
  73. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_schema_migration.py +0 -0
  74. {dbos-0.13.0a2 → dbos-0.14.0a5}/tests/test_singleton.py +0 -0
  75. {dbos-0.13.0a2 → dbos-0.14.0a5}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.13.0a2
3
+ Version: 0.14.0a5
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -619,16 +619,24 @@ class DBOS:
619
619
  It is important to use `DBOS.sleep` (as opposed to any other sleep) within workflows,
620
620
  as the `DBOS.sleep`s are durable and completed sleeps will be skipped during recovery.
621
621
  """
622
-
623
- attributes: TracedAttributes = {
624
- "name": "sleep",
625
- }
626
622
  if seconds <= 0:
627
623
  return
628
- with EnterDBOSStep(attributes) as ctx:
629
- _get_dbos_instance()._sys_db.sleep(
630
- ctx.workflow_id, ctx.curr_step_function_id, seconds
631
- )
624
+ cur_ctx = get_local_dbos_context()
625
+ if cur_ctx is not None:
626
+ # Must call it within a workflow
627
+ assert (
628
+ cur_ctx.is_workflow()
629
+ ), "sleep() must be called from within a workflow"
630
+ attributes: TracedAttributes = {
631
+ "name": "sleep",
632
+ }
633
+ with EnterDBOSStep(attributes) as ctx:
634
+ _get_dbos_instance()._sys_db.sleep(
635
+ ctx.workflow_id, ctx.curr_step_function_id, seconds
636
+ )
637
+ else:
638
+ # Cannot call it from outside of a workflow
639
+ raise DBOSException("sleep() must be called from within a workflow")
632
640
 
633
641
  @classmethod
634
642
  def set_event(cls, key: str, value: Any) -> None:
@@ -169,7 +169,7 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
169
169
 
170
170
  if not _is_valid_app_name(data["name"]):
171
171
  raise DBOSInitializationError(
172
- f'Invalid app name {data["name"]}. App names must be between 3 and 30 characters and contain only alphanumeric characters, dashes, and underscores.'
172
+ f'Invalid app name {data["name"]}. App names must be between 3 and 30 characters long and contain only lowercase letters, numbers, dashes, and underscores.'
173
173
  )
174
174
 
175
175
  if "app_db_name" not in data["database"]:
@@ -198,4 +198,4 @@ def _app_name_to_db_name(app_name: str) -> str:
198
198
  def _set_env_vars(config: ConfigFile) -> None:
199
199
  for env, value in config.get("env", {}).items():
200
200
  if value is not None:
201
- os.environ[env] = value
201
+ os.environ[env] = str(value)
@@ -299,6 +299,12 @@ class SystemDatabase:
299
299
  recovery_attempts: int = row[0]
300
300
  if recovery_attempts > max_recovery_attempts:
301
301
  with self.engine.begin() as c:
302
+ c.execute(
303
+ sa.delete(SystemSchema.workflow_queue).where(
304
+ SystemSchema.workflow_queue.c.workflow_uuid
305
+ == status["workflow_uuid"]
306
+ )
307
+ )
302
308
  c.execute(
303
309
  sa.update(SystemSchema.workflow_status)
304
310
  .where(
@@ -311,6 +317,7 @@ class SystemDatabase:
311
317
  )
312
318
  .values(
313
319
  status=WorkflowStatusString.RETRIES_EXCEEDED.value,
320
+ queue_name=None,
314
321
  )
315
322
  )
316
323
  raise DBOSDeadLetterQueueError(
@@ -1,6 +1,5 @@
1
1
  import os
2
2
  import platform
3
- import re
4
3
  import shutil
5
4
  import signal
6
5
  import subprocess
@@ -9,12 +8,15 @@ import typing
9
8
  from os import path
10
9
  from typing import Any
11
10
 
11
+ import sqlalchemy as sa
12
12
  import tomlkit
13
13
  import typer
14
14
  from rich import print
15
15
  from rich.prompt import Prompt
16
16
  from typing_extensions import Annotated
17
17
 
18
+ from dbos._schemas.system_database import SystemSchema
19
+
18
20
  from . import load_config
19
21
  from ._app_db import ApplicationDatabase
20
22
  from ._dbos_config import _is_valid_app_name
@@ -27,7 +29,9 @@ def _on_windows() -> bool:
27
29
  return platform.system() == "Windows"
28
30
 
29
31
 
30
- @app.command()
32
+ @app.command(
33
+ help="Start your DBOS application using the start commands in 'dbos-config.yaml'"
34
+ )
31
35
  def start() -> None:
32
36
  config = load_config()
33
37
  start_commands = config["runtimeConfig"]["start"]
@@ -166,7 +170,7 @@ def _get_project_name() -> typing.Union[str, None]:
166
170
  return name
167
171
 
168
172
 
169
- @app.command()
173
+ @app.command(help="Initialize a new DBOS application from a template")
170
174
  def init(
171
175
  project_name: Annotated[
172
176
  typing.Optional[str], typer.Argument(help="Specify application name")
@@ -187,7 +191,9 @@ def init(
187
191
  )
188
192
 
189
193
  if not _is_valid_app_name(project_name):
190
- raise Exception(f"{project_name} is an invalid DBOS app 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
+ )
191
197
 
192
198
  templates_dir = _get_templates_directory()
193
199
  templates = [x.name for x in os.scandir(templates_dir) if x.is_dir()]
@@ -212,7 +218,9 @@ def init(
212
218
  print(f"[red]{e}[/red]")
213
219
 
214
220
 
215
- @app.command()
221
+ @app.command(
222
+ help="Run your database schema migrations using the migration commands in 'dbos-config.yaml'"
223
+ )
216
224
  def migrate() -> None:
217
225
  config = load_config()
218
226
  if not config["database"]["password"]:
@@ -262,5 +270,68 @@ def migrate() -> None:
262
270
  typer.echo(f"Completed schema migration for database {app_db_name}")
263
271
 
264
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
+
265
336
  if __name__ == "__main__":
266
337
  app()
@@ -22,7 +22,7 @@ dependencies = [
22
22
  ]
23
23
  requires-python = ">=3.9"
24
24
  readme = "README.md"
25
- version = "0.13.0a2"
25
+ version = "0.14.0a5"
26
26
 
27
27
  [project.license]
28
28
  text = "MIT"
@@ -44,6 +44,7 @@ def test_valid_config(mocker):
44
44
  foo: ${BARBAR}
45
45
  bazbaz: BAZBAZ
46
46
  bob: ${BOBBOB}
47
+ test_number: 123
47
48
  """
48
49
  os.environ["BARBAR"] = "FOOFOO"
49
50
  mocker.patch(
@@ -61,10 +62,12 @@ def test_valid_config(mocker):
61
62
  assert configFile["database"]["connectionTimeoutMillis"] == 3000
62
63
  assert configFile["env"]["foo"] == "FOOFOO"
63
64
  assert configFile["env"]["bob"] is None # Unset environment variable
65
+ assert configFile["env"]["test_number"] == 123
64
66
 
65
67
  _set_env_vars(configFile)
66
68
  assert os.environ["bazbaz"] == "BAZBAZ"
67
69
  assert os.environ["foo"] == "FOOFOO"
70
+ assert os.environ["test_number"] == "123"
68
71
  assert "bob" not in os.environ
69
72
 
70
73
 
@@ -8,8 +8,8 @@ from psycopg.errors import SerializationFailure
8
8
  from sqlalchemy.exc import OperationalError
9
9
 
10
10
  # Public API
11
- from dbos import DBOS, GetWorkflowsInput, SetWorkflowID
12
- from dbos._error import DBOSDeadLetterQueueError, DBOSErrorCode, DBOSException
11
+ from dbos import DBOS, GetWorkflowsInput, Queue, SetWorkflowID
12
+ from dbos._error import DBOSDeadLetterQueueError
13
13
  from dbos._sys_db import WorkflowStatusString
14
14
 
15
15
 
@@ -150,3 +150,44 @@ def test_dead_letter_queue(dbos: DBOS) -> None:
150
150
  assert handle.get_result() == None
151
151
  dbos._sys_db.wait_for_buffer_flush()
152
152
  assert handle.get_status().status == WorkflowStatusString.SUCCESS.value
153
+
154
+
155
+ def test_enqueued_dead_letter_queue(dbos: DBOS) -> None:
156
+ function_started_event = threading.Event()
157
+ event = threading.Event()
158
+ max_concurrency = 1
159
+ max_recovery_attempts = 10
160
+ recovery_count = 0
161
+
162
+ @DBOS.workflow(max_recovery_attempts=max_recovery_attempts)
163
+ def dead_letter_workflow() -> None:
164
+ function_started_event.set()
165
+ nonlocal recovery_count
166
+ recovery_count += 1
167
+ event.wait()
168
+
169
+ @DBOS.workflow()
170
+ def regular_workflow() -> None:
171
+ return
172
+
173
+ queue = Queue("test_queue", concurrency=max_concurrency)
174
+ handle = queue.enqueue(dead_letter_workflow)
175
+ function_started_event.wait()
176
+
177
+ for i in range(max_recovery_attempts):
178
+ DBOS.recover_pending_workflows()
179
+ assert recovery_count == i + 2
180
+
181
+ regular_handle = queue.enqueue(regular_workflow)
182
+
183
+ with pytest.raises(Exception) as exc_info:
184
+ DBOS.recover_pending_workflows()
185
+ assert exc_info.errisinstance(DBOSDeadLetterQueueError)
186
+ assert handle.get_status().status == WorkflowStatusString.RETRIES_EXCEEDED.value
187
+
188
+ assert regular_handle.get_result() == None
189
+
190
+ event.set()
191
+ assert handle.get_result() == None
192
+ dbos._sys_db.wait_for_buffer_flush()
193
+ assert handle.get_status().status == WorkflowStatusString.SUCCESS.value
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes