dbos 0.7.0a0__tar.gz → 0.7.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 (69) hide show
  1. {dbos-0.7.0a0 → dbos-0.7.0a5}/PKG-INFO +5 -5
  2. {dbos-0.7.0a0 → dbos-0.7.0a5}/README.md +3 -3
  3. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/application_database.py +6 -11
  4. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/cli.py +6 -1
  5. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/core.py +5 -5
  6. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/dbos.py +1 -24
  7. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/dbos_config.py +1 -1
  8. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/fastapi.py +46 -2
  9. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/scheduler/scheduler.py +1 -1
  10. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/system_database.py +51 -67
  11. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/dbos-config.yaml.dbos +1 -1
  12. {dbos-0.7.0a0 → dbos-0.7.0a5}/pyproject.toml +2 -3
  13. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/conftest.py +1 -1
  14. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/scheduler/test_scheduler.py +3 -3
  15. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_failures.py +7 -7
  16. {dbos-0.7.0a0 → dbos-0.7.0a5}/LICENSE +0 -0
  17. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/__init__.py +0 -0
  18. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/admin_sever.py +0 -0
  19. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/context.py +0 -0
  20. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/dbos-config.schema.json +0 -0
  21. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/decorators.py +0 -0
  22. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/error.py +0 -0
  23. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/flask.py +0 -0
  24. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/kafka.py +0 -0
  25. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/kafka_message.py +0 -0
  26. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/logger.py +0 -0
  27. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/env.py +0 -0
  28. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/script.py.mako +0 -0
  29. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  30. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  31. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/py.typed +0 -0
  32. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/recovery.py +0 -0
  33. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/registrations.py +0 -0
  34. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/request.py +0 -0
  35. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/roles.py +0 -0
  36. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/scheduler/croniter.py +0 -0
  37. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/schemas/__init__.py +0 -0
  38. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/schemas/application_database.py +0 -0
  39. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/schemas/system_database.py +0 -0
  40. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/README.md +0 -0
  41. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/__package/__init__.py +0 -0
  42. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/__package/main.py +0 -0
  43. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/__package/schema.py +0 -0
  44. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/alembic.ini +0 -0
  45. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/env.py.dbos +0 -0
  46. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/script.py.mako +0 -0
  47. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/migrations/versions/2024_07_31_180642_init.py +0 -0
  48. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/templates/hello/start_postgres_docker.py +0 -0
  49. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/tracer.py +0 -0
  50. {dbos-0.7.0a0 → dbos-0.7.0a5}/dbos/utils.py +0 -0
  51. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/__init__.py +0 -0
  52. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/atexit_no_ctor.py +0 -0
  53. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/atexit_no_launch.py +0 -0
  54. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/classdefs.py +0 -0
  55. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/more_classdefs.py +0 -0
  56. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/scheduler/test_croniter.py +0 -0
  57. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_admin_server.py +0 -0
  58. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_classdecorators.py +0 -0
  59. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_concurrency.py +0 -0
  60. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_config.py +0 -0
  61. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_dbos.py +0 -0
  62. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_fastapi.py +0 -0
  63. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_fastapi_roles.py +0 -0
  64. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_flask.py +0 -0
  65. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_kafka.py +0 -0
  66. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_package.py +0 -0
  67. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_schema_migration.py +0 -0
  68. {dbos-0.7.0a0 → dbos-0.7.0a5}/tests/test_singleton.py +0 -0
  69. {dbos-0.7.0a0 → dbos-0.7.0a5}/version/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.7.0a0
3
+ Version: 0.7.0a5
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
@@ -8,7 +8,6 @@ Requires-Python: >=3.9
8
8
  Requires-Dist: pyyaml>=6.0.2
9
9
  Requires-Dist: jsonschema>=4.23.0
10
10
  Requires-Dist: alembic>=1.13.2
11
- Requires-Dist: psycopg2-binary>=2.9.9
12
11
  Requires-Dist: typing-extensions>=4.12.2; python_version < "3.10"
13
12
  Requires-Dist: typer>=0.12.3
14
13
  Requires-Dist: jsonpickle>=3.2.2
@@ -19,6 +18,7 @@ Requires-Dist: python-dateutil>=2.9.0.post0
19
18
  Requires-Dist: fastapi[standard]>=0.112.1
20
19
  Requires-Dist: psutil>=6.0.0
21
20
  Requires-Dist: tomlkit>=0.13.2
21
+ Requires-Dist: psycopg>=3.2.1
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
 
@@ -91,7 +91,7 @@ def step_two():
91
91
  print("Step two completed!")
92
92
 
93
93
  @DBOS.workflow()
94
- def workflow():
94
+ def dbos_workflow():
95
95
  step_one()
96
96
  for _ in range(5):
97
97
  print("Press Control + \ to stop the app...")
@@ -99,8 +99,8 @@ def workflow():
99
99
  step_two()
100
100
 
101
101
  @app.get("/")
102
- def endpoint():
103
- workflow()
102
+ def fastapi_endpoint():
103
+ dbos_workflow()
104
104
  ```
105
105
 
106
106
  Save the program into `main.py`, edit `dbos-config.yaml` to configure your Postgres connection settings, and start it with `fastapi run`.
@@ -68,7 +68,7 @@ def step_two():
68
68
  print("Step two completed!")
69
69
 
70
70
  @DBOS.workflow()
71
- def workflow():
71
+ def dbos_workflow():
72
72
  step_one()
73
73
  for _ in range(5):
74
74
  print("Press Control + \ to stop the app...")
@@ -76,8 +76,8 @@ def workflow():
76
76
  step_two()
77
77
 
78
78
  @app.get("/")
79
- def endpoint():
80
- workflow()
79
+ def fastapi_endpoint():
80
+ dbos_workflow()
81
81
  ```
82
82
 
83
83
  Save the program into `main.py`, edit `dbos-config.yaml` to configure your Postgres connection settings, and start it with `fastapi run`.
@@ -2,7 +2,6 @@ from typing import Optional, TypedDict, cast
2
2
 
3
3
  import sqlalchemy as sa
4
4
  import sqlalchemy.dialects.postgresql as pg
5
- import sqlalchemy.exc as sa_exc
6
5
  from sqlalchemy.exc import DBAPIError
7
6
  from sqlalchemy.orm import Session, sessionmaker
8
7
 
@@ -36,7 +35,7 @@ class ApplicationDatabase:
36
35
 
37
36
  # If the application database does not already exist, create it
38
37
  postgres_db_url = sa.URL.create(
39
- "postgresql",
38
+ "postgresql+psycopg",
40
39
  username=config["database"]["username"],
41
40
  password=config["database"]["password"],
42
41
  host=config["database"]["hostname"],
@@ -55,7 +54,7 @@ class ApplicationDatabase:
55
54
 
56
55
  # Create a connection pool for the application database
57
56
  app_db_url = sa.URL.create(
58
- "postgresql",
57
+ "postgresql+psycopg",
59
58
  username=config["database"]["username"],
60
59
  password=config["database"]["password"],
61
60
  host=config["database"]["hostname"],
@@ -97,11 +96,9 @@ class ApplicationDatabase:
97
96
  )
98
97
  )
99
98
  except DBAPIError as dbapi_error:
100
- if dbapi_error.orig.pgcode == "23505": # type: ignore
99
+ if dbapi_error.orig.sqlstate == "23505": # type: ignore
101
100
  raise DBOSWorkflowConflictIDError(output["workflow_uuid"])
102
- raise dbapi_error
103
- except Exception as e:
104
- raise e
101
+ raise
105
102
 
106
103
  def record_transaction_error(self, output: TransactionResultInternal) -> None:
107
104
  try:
@@ -122,11 +119,9 @@ class ApplicationDatabase:
122
119
  )
123
120
  )
124
121
  except DBAPIError as dbapi_error:
125
- if dbapi_error.orig.pgcode == "23505": # type: ignore
122
+ if dbapi_error.orig.sqlstate == "23505": # type: ignore
126
123
  raise DBOSWorkflowConflictIDError(output["workflow_uuid"])
127
- raise dbapi_error
128
- except Exception as e:
129
- raise e
124
+ raise
130
125
 
131
126
  @staticmethod
132
127
  def check_transaction_execution(
@@ -12,7 +12,7 @@ from typing import Any
12
12
  import tomlkit
13
13
  import typer
14
14
  from rich import print
15
- from rich.prompt import Confirm, Prompt
15
+ from rich.prompt import Prompt
16
16
  from typing_extensions import Annotated
17
17
 
18
18
  from dbos import load_config
@@ -30,6 +30,7 @@ def on_windows() -> bool:
30
30
  def start() -> None:
31
31
  config = load_config()
32
32
  start_commands = config["runtimeConfig"]["start"]
33
+ typer.echo("Executing start commands from 'dbos-config.yaml'")
33
34
  for command in start_commands:
34
35
  typer.echo(f"Executing: {command}")
35
36
 
@@ -129,9 +130,12 @@ def copy_template(src_dir: str, project_name: str, config_mode: bool) -> None:
129
130
  "project_name": project_name,
130
131
  "package_name": package_name,
131
132
  "db_name": db_name,
133
+ "migration_command": "alembic upgrade head",
132
134
  }
133
135
 
134
136
  if config_mode:
137
+ ctx["package_name"] = "."
138
+ ctx["migration_command"] = "echo 'No migrations specified'"
135
139
  copy_dbos_template(
136
140
  os.path.join(src_dir, "dbos-config.yaml.dbos"),
137
141
  os.path.join(dst_dir, "dbos-config.yaml"),
@@ -244,6 +248,7 @@ def migrate() -> None:
244
248
  app_db.destroy()
245
249
 
246
250
  # Next, run any custom migration commands specified in the configuration
251
+ typer.echo("Executing migration commands from 'dbos-config.yaml'")
247
252
  try:
248
253
  migrate_commands = (
249
254
  config["database"]["migrate"]
@@ -192,7 +192,7 @@ def _execute_workflow(
192
192
  status["status"] = "ERROR"
193
193
  status["error"] = utils.serialize(error)
194
194
  dbos.sys_db.update_workflow_status(status)
195
- raise error
195
+ raise
196
196
 
197
197
  return output
198
198
 
@@ -217,7 +217,7 @@ def _execute_workflow_wthread(
217
217
  dbos.logger.error(
218
218
  f"Exception encountered in asynchronous workflow: {traceback.format_exc()}"
219
219
  )
220
- raise e
220
+ raise
221
221
 
222
222
 
223
223
  def _execute_workflow_id(dbos: "DBOS", workflow_id: str) -> "WorkflowHandle[Any]":
@@ -493,7 +493,7 @@ def _transaction(
493
493
  )
494
494
  break
495
495
  except DBAPIError as dbapi_error:
496
- if dbapi_error.orig.pgcode == "40001": # type: ignore
496
+ if dbapi_error.orig.sqlstate == "40001": # type: ignore
497
497
  # Retry on serialization failure
498
498
  ctx.get_current_span().add_event(
499
499
  "Transaction Serialization Failure",
@@ -505,13 +505,13 @@ def _transaction(
505
505
  max_retry_wait_seconds,
506
506
  )
507
507
  continue
508
- raise dbapi_error
508
+ raise
509
509
  except Exception as error:
510
510
  # Don't record the error if it was already recorded
511
511
  if not has_recorded_error:
512
512
  txn_output["error"] = utils.serialize(error)
513
513
  dbos.app_db.record_transaction_error(txn_output)
514
- raise error
514
+ raise
515
515
  return output
516
516
 
517
517
  fi = get_or_create_func_info(func)
@@ -277,32 +277,9 @@ class DBOS:
277
277
 
278
278
  # If using FastAPI, set up middleware and lifecycle events
279
279
  if self.fastapi is not None:
280
- from fastapi.requests import Request as FARequest
281
- from fastapi.responses import JSONResponse
282
-
283
- async def dbos_error_handler(
284
- request: FARequest, gexc: Exception
285
- ) -> JSONResponse:
286
- exc: DBOSException = cast(DBOSException, gexc)
287
- status_code = 500
288
- if exc.status_code is not None:
289
- status_code = exc.status_code
290
- return JSONResponse(
291
- status_code=status_code,
292
- content={
293
- "message": str(exc.message),
294
- "dbos_error_code": str(exc.dbos_error_code),
295
- "dbos_error": str(exc.__class__.__name__),
296
- },
297
- )
298
-
299
- self.fastapi.add_exception_handler(DBOSException, dbos_error_handler)
300
-
301
280
  from dbos.fastapi import setup_fastapi_middleware
302
281
 
303
- setup_fastapi_middleware(self.fastapi)
304
- self.fastapi.on_event("startup")(self._launch)
305
- self.fastapi.on_event("shutdown")(self._destroy)
282
+ setup_fastapi_middleware(self.fastapi, _get_dbos_instance())
306
283
 
307
284
  # If using Flask, set up middleware
308
285
  if self.flask is not None:
@@ -105,7 +105,7 @@ def get_dbos_database_url(config_file_path: str = "dbos-config.yaml") -> str:
105
105
  """
106
106
  dbos_config = load_config(config_file_path)
107
107
  db_url = URL.create(
108
- "postgresql",
108
+ "postgresql+psycopg",
109
109
  username=dbos_config["database"]["username"],
110
110
  password=dbos_config["database"]["password"],
111
111
  host=dbos_config["database"]["hostname"],
@@ -1,8 +1,13 @@
1
1
  import uuid
2
- from typing import Any, Callable
2
+ from typing import Any, Callable, cast
3
3
 
4
4
  from fastapi import FastAPI
5
5
  from fastapi import Request as FastAPIRequest
6
+ from fastapi.responses import JSONResponse
7
+ from starlette.types import ASGIApp, Message, Receive, Scope, Send
8
+
9
+ from dbos import DBOS
10
+ from dbos.error import DBOSException
6
11
 
7
12
  from .context import (
8
13
  EnterDBOSHandler,
@@ -35,7 +40,46 @@ def make_request(request: FastAPIRequest) -> Request:
35
40
  )
36
41
 
37
42
 
38
- def setup_fastapi_middleware(app: FastAPI) -> None:
43
+ async def dbos_error_handler(request: FastAPIRequest, gexc: Exception) -> JSONResponse:
44
+ exc: DBOSException = cast(DBOSException, gexc)
45
+ status_code = 500
46
+ if exc.status_code is not None:
47
+ status_code = exc.status_code
48
+ return JSONResponse(
49
+ status_code=status_code,
50
+ content={
51
+ "message": str(exc.message),
52
+ "dbos_error_code": str(exc.dbos_error_code),
53
+ "dbos_error": str(exc.__class__.__name__),
54
+ },
55
+ )
56
+
57
+
58
+ class LifespanMiddleware:
59
+ def __init__(self, app: ASGIApp, dbos: DBOS):
60
+ self.app = app
61
+ self.dbos = dbos
62
+
63
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
64
+ if scope["type"] == "lifespan":
65
+ while True:
66
+ message = await receive()
67
+ if message["type"] == "lifespan.startup":
68
+ self.dbos._launch()
69
+ await send({"type": "lifespan.startup.complete"})
70
+ elif message["type"] == "lifespan.shutdown":
71
+ self.dbos._destroy()
72
+ await send({"type": "lifespan.shutdown.complete"})
73
+ break
74
+ else:
75
+ await self.app(scope, receive, send)
76
+
77
+
78
+ def setup_fastapi_middleware(app: FastAPI, dbos: DBOS) -> None:
79
+
80
+ app.add_middleware(LifespanMiddleware, dbos=dbos)
81
+ app.add_exception_handler(DBOSException, dbos_error_handler)
82
+
39
83
  @app.middleware("http")
40
84
  async def dbos_fastapi_middleware(
41
85
  request: FastAPIRequest, call_next: Callable[..., Any]
@@ -16,7 +16,7 @@ ScheduledWorkflow = Callable[[datetime, datetime], None]
16
16
  def scheduler_loop(
17
17
  func: ScheduledWorkflow, cron: str, stop_event: threading.Event
18
18
  ) -> None:
19
- iter = croniter(cron, datetime.now(timezone.utc))
19
+ iter = croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
20
20
  while not stop_event.is_set():
21
21
  nextExecTime = iter.get_next(datetime)
22
22
  sleepTime = nextExecTime - datetime.now(timezone.utc)
@@ -6,7 +6,7 @@ import time
6
6
  from enum import Enum
7
7
  from typing import Any, Dict, List, Literal, Optional, Sequence, Set, TypedDict, cast
8
8
 
9
- import psycopg2
9
+ import psycopg
10
10
  import sqlalchemy as sa
11
11
  import sqlalchemy.dialects.postgresql as pg
12
12
  from alembic import command
@@ -154,7 +154,7 @@ class SystemDatabase:
154
154
 
155
155
  # If the system database does not already exist, create it
156
156
  postgres_db_url = sa.URL.create(
157
- "postgresql",
157
+ "postgresql+psycopg",
158
158
  username=config["database"]["username"],
159
159
  password=config["database"]["password"],
160
160
  host=config["database"]["hostname"],
@@ -172,7 +172,7 @@ class SystemDatabase:
172
172
  engine.dispose()
173
173
 
174
174
  system_db_url = sa.URL.create(
175
- "postgresql",
175
+ "postgresql+psycopg",
176
176
  username=config["database"]["username"],
177
177
  password=config["database"]["password"],
178
178
  host=config["database"]["hostname"],
@@ -196,7 +196,7 @@ class SystemDatabase:
196
196
  )
197
197
  command.upgrade(alembic_cfg, "head")
198
198
 
199
- self.notification_conn: Optional[psycopg2.extensions.connection] = None
199
+ self.notification_conn: Optional[psycopg.connection.Connection] = None
200
200
  self.notifications_map: Dict[str, threading.Condition] = {}
201
201
  self.workflow_events_map: Dict[str, threading.Condition] = {}
202
202
 
@@ -565,11 +565,9 @@ class SystemDatabase:
565
565
  with self.engine.begin() as c:
566
566
  c.execute(sql)
567
567
  except DBAPIError as dbapi_error:
568
- if dbapi_error.orig.pgcode == "23505": # type: ignore
568
+ if dbapi_error.orig.sqlstate == "23505": # type: ignore
569
569
  raise DBOSWorkflowConflictIDError(result["workflow_uuid"])
570
- raise dbapi_error
571
- except Exception as e:
572
- raise e
570
+ raise
573
571
 
574
572
  def check_operation_execution(
575
573
  self, workflow_uuid: str, function_id: int, conn: Optional[sa.Connection] = None
@@ -623,11 +621,9 @@ class SystemDatabase:
623
621
  )
624
622
  except DBAPIError as dbapi_error:
625
623
  # Foreign key violation
626
- if dbapi_error.orig.pgcode == "23503": # type: ignore
624
+ if dbapi_error.orig.sqlstate == "23503": # type: ignore
627
625
  raise DBOSNonExistentWorkflowError(destination_uuid)
628
- raise dbapi_error
629
- except Exception as e:
630
- raise e
626
+ raise
631
627
  output: OperationResultInternal = {
632
628
  "workflow_uuid": workflow_uuid,
633
629
  "function_id": function_id,
@@ -729,69 +725,59 @@ class SystemDatabase:
729
725
  return message
730
726
 
731
727
  def _notification_listener(self) -> None:
732
- notification_cursor: Optional[psycopg2.extensions.cursor] = None
733
728
  while self._run_background_processes:
734
729
  try:
735
- # Listen to notifications
736
- self.notification_conn = psycopg2.connect(
737
- self.engine.url.render_as_string(hide_password=False)
730
+ # since we're using the psycopg connection directly, we need a url without the "+pycopg" suffix
731
+ url = sa.URL.create(
732
+ "postgresql", **self.engine.url.translate_connect_args()
738
733
  )
739
- self.notification_conn.set_isolation_level(
740
- psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
734
+ # Listen to notifications
735
+ self.notification_conn = psycopg.connect(
736
+ url.render_as_string(hide_password=False), autocommit=True
741
737
  )
742
- notification_cursor = self.notification_conn.cursor()
743
738
 
744
- notification_cursor.execute("LISTEN dbos_notifications_channel")
745
- notification_cursor.execute("LISTEN dbos_workflow_events_channel")
739
+ self.notification_conn.execute("LISTEN dbos_notifications_channel")
740
+ self.notification_conn.execute("LISTEN dbos_workflow_events_channel")
741
+
746
742
  while self._run_background_processes:
747
- if select.select([self.notification_conn], [], [], 60) == (
748
- [],
749
- [],
750
- [],
751
- ):
752
- continue
753
- else:
754
- self.notification_conn.poll()
755
- while self.notification_conn.notifies:
756
- notify = self.notification_conn.notifies.pop(0)
757
- channel = notify.channel
758
- dbos_logger.debug(
759
- f"Received notification on channel: {channel}, payload: {notify.payload}"
760
- )
761
- if channel == "dbos_notifications_channel":
762
- if (
763
- notify.payload
764
- and notify.payload in self.notifications_map
765
- ):
766
- condition = self.notifications_map[notify.payload]
767
- condition.acquire()
768
- condition.notify_all()
769
- condition.release()
770
- dbos_logger.debug(
771
- f"Signaled notifications condition for {notify.payload}"
772
- )
773
- elif channel == "dbos_workflow_events_channel":
774
- if (
775
- notify.payload
776
- and notify.payload in self.workflow_events_map
777
- ):
778
- condition = self.workflow_events_map[notify.payload]
779
- condition.acquire()
780
- condition.notify_all()
781
- condition.release()
782
- dbos_logger.debug(
783
- f"Signaled workflow_events condition for {notify.payload}"
784
- )
785
- else:
786
- dbos_logger.error(f"Unknown channel: {channel}")
743
+ gen = self.notification_conn.notifies(timeout=60)
744
+ for notify in gen:
745
+ channel = notify.channel
746
+ dbos_logger.debug(
747
+ f"Received notification on channel: {channel}, payload: {notify.payload}"
748
+ )
749
+ if channel == "dbos_notifications_channel":
750
+ if (
751
+ notify.payload
752
+ and notify.payload in self.notifications_map
753
+ ):
754
+ condition = self.notifications_map[notify.payload]
755
+ condition.acquire()
756
+ condition.notify_all()
757
+ condition.release()
758
+ dbos_logger.debug(
759
+ f"Signaled notifications condition for {notify.payload}"
760
+ )
761
+ elif channel == "dbos_workflow_events_channel":
762
+ if (
763
+ notify.payload
764
+ and notify.payload in self.workflow_events_map
765
+ ):
766
+ condition = self.workflow_events_map[notify.payload]
767
+ condition.acquire()
768
+ condition.notify_all()
769
+ condition.release()
770
+ dbos_logger.debug(
771
+ f"Signaled workflow_events condition for {notify.payload}"
772
+ )
773
+ else:
774
+ dbos_logger.error(f"Unknown channel: {channel}")
787
775
  except Exception as e:
788
776
  if self._run_background_processes:
789
777
  dbos_logger.error(f"Notification listener error: {e}")
790
778
  time.sleep(1)
791
779
  # Then the loop will try to reconnect and restart the listener
792
780
  finally:
793
- if notification_cursor is not None:
794
- notification_cursor.close()
795
781
  if self.notification_conn is not None:
796
782
  self.notification_conn.close()
797
783
 
@@ -848,11 +834,9 @@ class SystemDatabase:
848
834
  )
849
835
  )
850
836
  except DBAPIError as dbapi_error:
851
- if dbapi_error.orig.pgcode == "23505": # type: ignore
837
+ if dbapi_error.orig.sqlstate == "23505": # type: ignore
852
838
  raise DBOSDuplicateWorkflowEventError(workflow_uuid, key)
853
- raise dbapi_error
854
- except Exception as e:
855
- raise e
839
+ raise
856
840
  output: OperationResultInternal = {
857
841
  "workflow_uuid": workflow_uuid,
858
842
  "function_id": function_id,
@@ -15,7 +15,7 @@ database:
15
15
  password: ${PGPASSWORD}
16
16
  app_db_name: ${db_name}
17
17
  migrate:
18
- - alembic upgrade head
18
+ - ${migration_command}
19
19
  telemetry:
20
20
  logs:
21
21
  logLevel: INFO
@@ -9,7 +9,6 @@ dependencies = [
9
9
  "pyyaml>=6.0.2",
10
10
  "jsonschema>=4.23.0",
11
11
  "alembic>=1.13.2",
12
- "psycopg2-binary>=2.9.9",
13
12
  "typing-extensions>=4.12.2; python_version < \"3.10\"",
14
13
  "typer>=0.12.3",
15
14
  "jsonpickle>=3.2.2",
@@ -20,10 +19,11 @@ dependencies = [
20
19
  "fastapi[standard]>=0.112.1",
21
20
  "psutil>=6.0.0",
22
21
  "tomlkit>=0.13.2",
22
+ "psycopg>=3.2.1",
23
23
  ]
24
24
  requires-python = ">=3.9"
25
25
  readme = "README.md"
26
- version = "0.7.0a0"
26
+ version = "0.7.0a5"
27
27
 
28
28
  [project.license]
29
29
  text = "MIT"
@@ -58,7 +58,6 @@ dev = [
58
58
  "requests>=2.32.3",
59
59
  "types-requests>=2.32.0.20240712",
60
60
  "httpx>=0.27.0",
61
- "types-psycopg2>=2.9.21.20240417",
62
61
  "pytz>=2024.1",
63
62
  "GitPython>=3.1.43",
64
63
  "confluent-kafka>=2.5.3",
@@ -48,7 +48,7 @@ def config() -> ConfigFile:
48
48
  def postgres_db_engine() -> sa.Engine:
49
49
  cfg = default_config()
50
50
  postgres_db_url = sa.URL.create(
51
- "postgresql",
51
+ "postgresql+psycopg",
52
52
  username=cfg["database"]["username"],
53
53
  password=cfg["database"]["password"],
54
54
  host=cfg["database"]["hostname"],
@@ -9,14 +9,14 @@ from dbos import DBOS
9
9
  def test_scheduled_workflow(dbos: DBOS) -> None:
10
10
  wf_counter: int = 0
11
11
 
12
- @DBOS.scheduled("* * * * * *")
12
+ @DBOS.scheduled("*/2 * * * * *")
13
13
  @DBOS.workflow()
14
14
  def test_workflow(scheduled: datetime, actual: datetime) -> None:
15
15
  nonlocal wf_counter
16
16
  wf_counter += 1
17
17
 
18
- time.sleep(2)
19
- assert wf_counter >= 1 and wf_counter <= 3
18
+ time.sleep(4)
19
+ assert wf_counter > 1 and wf_counter <= 3
20
20
 
21
21
 
22
22
  def test_scheduled_workflow_exception(dbos: DBOS) -> None:
@@ -4,7 +4,8 @@ import uuid
4
4
 
5
5
  import pytest
6
6
  import sqlalchemy as sa
7
- from sqlalchemy.exc import DBAPIError
7
+ from psycopg.errors import SerializationFailure
8
+ from sqlalchemy.exc import OperationalError
8
9
 
9
10
  # Public API
10
11
  from dbos import DBOS, GetWorkflowsInput, SetWorkflowID
@@ -18,10 +19,9 @@ def test_transaction_errors(dbos: DBOS) -> None:
18
19
  nonlocal retry_counter
19
20
  if retry_counter < max_retry:
20
21
  retry_counter += 1
21
- base_err = BaseException()
22
- base_err.pgcode = "40001" # type: ignore
23
- err = DBAPIError("Serialization test error", {}, base_err)
24
- raise err
22
+ raise OperationalError(
23
+ "Serialization test error", {}, SerializationFailure()
24
+ )
25
25
  return max_retry
26
26
 
27
27
  @DBOS.transaction()
@@ -36,7 +36,7 @@ def test_transaction_errors(dbos: DBOS) -> None:
36
36
 
37
37
  with pytest.raises(Exception) as exc_info:
38
38
  test_noretry_transaction()
39
- assert exc_info.value.orig.pgcode == "42601" # type: ignore
39
+ assert exc_info.value.orig.sqlstate == "42601" # type: ignore
40
40
  assert retry_counter == 11
41
41
 
42
42
 
@@ -101,7 +101,7 @@ def test_buffer_flush_errors(dbos: DBOS) -> None:
101
101
  # Crash the system database connection and make sure the buffer flush works on time.
102
102
  backup_engine = dbos.sys_db.engine
103
103
  dbos.sys_db.engine = sa.create_engine(
104
- "postgresql+psycopg2://fake:database@localhost/fake_db"
104
+ "postgresql+psycopg://fake:database@localhost/fake_db"
105
105
  )
106
106
 
107
107
  res = test_transaction("bob")
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