dbos 0.9.0a1__tar.gz → 0.10.0__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.9.0a1 → dbos-0.10.0}/PKG-INFO +9 -9
  2. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/dbos-config.schema.json +8 -0
  3. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/dbos.py +33 -7
  4. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/dbos_config.py +8 -1
  5. {dbos-0.9.0a1 → dbos-0.10.0}/pyproject.toml +18 -18
  6. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_admin_server.py +36 -0
  7. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_config.py +35 -0
  8. {dbos-0.9.0a1 → dbos-0.10.0}/LICENSE +0 -0
  9. {dbos-0.9.0a1 → dbos-0.10.0}/README.md +0 -0
  10. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/__init__.py +0 -0
  11. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/admin_sever.py +0 -0
  12. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/application_database.py +0 -0
  13. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/cli.py +0 -0
  14. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/context.py +0 -0
  15. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/core.py +0 -0
  16. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/decorators.py +0 -0
  17. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/error.py +0 -0
  18. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/fastapi.py +0 -0
  19. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/flask.py +0 -0
  20. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/kafka.py +0 -0
  21. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/kafka_message.py +0 -0
  22. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/logger.py +0 -0
  23. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/env.py +0 -0
  24. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/script.py.mako +0 -0
  25. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
  26. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/versions/5c361fc04708_added_system_tables.py +0 -0
  27. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
  28. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
  29. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/versions/d76646551a6c_workflow_queue.py +0 -0
  30. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
  31. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/py.typed +0 -0
  32. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/queue.py +0 -0
  33. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/recovery.py +0 -0
  34. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/registrations.py +0 -0
  35. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/request.py +0 -0
  36. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/roles.py +0 -0
  37. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/scheduler/croniter.py +0 -0
  38. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/scheduler/scheduler.py +0 -0
  39. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/schemas/__init__.py +0 -0
  40. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/schemas/application_database.py +0 -0
  41. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/schemas/system_database.py +0 -0
  42. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/system_database.py +0 -0
  43. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/README.md +0 -0
  44. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/__package/__init__.py +0 -0
  45. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/__package/main.py +0 -0
  46. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/__package/schema.py +0 -0
  47. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/alembic.ini +0 -0
  48. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/dbos-config.yaml.dbos +0 -0
  49. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/migrations/env.py.dbos +0 -0
  50. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/migrations/script.py.mako +0 -0
  51. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/migrations/versions/2024_07_31_180642_init.py +0 -0
  52. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/templates/hello/start_postgres_docker.py +0 -0
  53. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/tracer.py +0 -0
  54. {dbos-0.9.0a1 → dbos-0.10.0}/dbos/utils.py +0 -0
  55. {dbos-0.9.0a1 → dbos-0.10.0}/tests/__init__.py +0 -0
  56. {dbos-0.9.0a1 → dbos-0.10.0}/tests/atexit_no_ctor.py +0 -0
  57. {dbos-0.9.0a1 → dbos-0.10.0}/tests/atexit_no_launch.py +0 -0
  58. {dbos-0.9.0a1 → dbos-0.10.0}/tests/classdefs.py +0 -0
  59. {dbos-0.9.0a1 → dbos-0.10.0}/tests/conftest.py +0 -0
  60. {dbos-0.9.0a1 → dbos-0.10.0}/tests/more_classdefs.py +0 -0
  61. {dbos-0.9.0a1 → dbos-0.10.0}/tests/scheduler/test_croniter.py +0 -0
  62. {dbos-0.9.0a1 → dbos-0.10.0}/tests/scheduler/test_scheduler.py +0 -0
  63. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_classdecorators.py +0 -0
  64. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_concurrency.py +0 -0
  65. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_dbos.py +0 -0
  66. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_failures.py +0 -0
  67. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_fastapi.py +0 -0
  68. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_fastapi_roles.py +0 -0
  69. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_flask.py +0 -0
  70. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_kafka.py +0 -0
  71. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_package.py +0 -0
  72. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_queue.py +0 -0
  73. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_schema_migration.py +0 -0
  74. {dbos-0.9.0a1 → dbos-0.10.0}/tests/test_singleton.py +0 -0
  75. {dbos-0.9.0a1 → dbos-0.10.0}/version/__init__.py +0 -0
@@ -1,24 +1,24 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dbos
3
- Version: 0.9.0a1
3
+ Version: 0.10.0
4
4
  Summary: Ultra-lightweight durable execution in Python
5
5
  Author-Email: "DBOS, Inc." <contact@dbos.dev>
6
6
  License: MIT
7
7
  Requires-Python: >=3.9
8
8
  Requires-Dist: pyyaml>=6.0.2
9
9
  Requires-Dist: jsonschema>=4.23.0
10
- Requires-Dist: alembic>=1.13.2
10
+ Requires-Dist: alembic>=1.13.3
11
11
  Requires-Dist: typing-extensions>=4.12.2; python_version < "3.10"
12
- Requires-Dist: typer>=0.12.3
13
- Requires-Dist: jsonpickle>=3.2.2
14
- Requires-Dist: opentelemetry-api>=1.26.0
15
- Requires-Dist: opentelemetry-sdk>=1.26.0
16
- Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.26.0
12
+ Requires-Dist: typer>=0.12.5
13
+ Requires-Dist: jsonpickle>=3.3.0
14
+ Requires-Dist: opentelemetry-api>=1.27.0
15
+ Requires-Dist: opentelemetry-sdk>=1.27.0
16
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27.0
17
17
  Requires-Dist: python-dateutil>=2.9.0.post0
18
- Requires-Dist: fastapi[standard]>=0.112.1
18
+ Requires-Dist: fastapi[standard]>=0.115.2
19
19
  Requires-Dist: psutil>=6.0.0
20
20
  Requires-Dist: tomlkit>=0.13.2
21
- Requires-Dist: psycopg>=3.2.1
21
+ Requires-Dist: psycopg>=3.2.3
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
 
@@ -59,6 +59,10 @@
59
59
  "type": "string",
60
60
  "description": "If using SSL/TLS to securely connect to a database, path to an SSL root certificate file"
61
61
  },
62
+ "local_suffix": {
63
+ "type": "boolean",
64
+ "description": "Whether to suffix app_db_name with '_local'. Set to true when doing local development using a DBOS Cloud database."
65
+ },
62
66
  "app_db_client": {
63
67
  "type": "string",
64
68
  "description": "Specify the database client to use to connect to the application database",
@@ -140,6 +144,10 @@
140
144
  "start": {
141
145
  "type": "array",
142
146
  "description": "Specify commands to run to start your application (Python only)"
147
+ },
148
+ "admin_port": {
149
+ "type": "number",
150
+ "description": "The port number of the admin server (Default: 3001)"
143
151
  }
144
152
  }
145
153
  },
@@ -277,6 +277,7 @@ class DBOS:
277
277
  self.fastapi: Optional["FastAPI"] = fastapi
278
278
  self.flask: Optional["Flask"] = flask
279
279
  self._executor_field: Optional[ThreadPoolExecutor] = None
280
+ self._background_threads: List[threading.Thread] = []
280
281
 
281
282
  # If using FastAPI, set up middleware and lifecycle events
282
283
  if self.fastapi is not None:
@@ -348,27 +349,48 @@ class DBOS:
348
349
  self._executor_field = ThreadPoolExecutor(max_workers=64)
349
350
  self._sys_db_field = SystemDatabase(self.config)
350
351
  self._app_db_field = ApplicationDatabase(self.config)
351
- self._admin_server_field = AdminServer(dbos=self)
352
+ admin_port = self.config["runtimeConfig"].get("admin_port")
353
+ if admin_port is None:
354
+ admin_port = 3001
355
+ self._admin_server_field = AdminServer(dbos=self, port=admin_port)
352
356
 
353
357
  if not os.environ.get("DBOS__VMID"):
354
358
  workflow_ids = self._sys_db.get_pending_workflows("local")
355
359
  self._executor.submit(_startup_recovery_thread, self, workflow_ids)
356
360
 
357
361
  # Listen to notifications
358
- self._executor.submit(self._sys_db._notification_listener)
362
+ notification_listener_thread = threading.Thread(
363
+ target=self._sys_db._notification_listener,
364
+ daemon=True,
365
+ )
366
+ notification_listener_thread.start()
367
+ self._background_threads.append(notification_listener_thread)
359
368
 
360
369
  # Start flush workflow buffers thread
361
- self._executor.submit(self._sys_db.flush_workflow_buffers)
370
+ flush_workflow_buffers_thread = threading.Thread(
371
+ target=self._sys_db.flush_workflow_buffers,
372
+ daemon=True,
373
+ )
374
+ flush_workflow_buffers_thread.start()
375
+ self._background_threads.append(flush_workflow_buffers_thread)
362
376
 
363
377
  # Start the queue thread
364
378
  evt = threading.Event()
365
379
  self.stop_events.append(evt)
366
- self._executor.submit(queue_thread, evt, self)
380
+ bg_queue_thread = threading.Thread(
381
+ target=queue_thread, args=(evt, self), daemon=True
382
+ )
383
+ bg_queue_thread.start()
384
+ self._background_threads.append(bg_queue_thread)
367
385
 
368
386
  # Grab any pollers that were deferred and start them
369
387
  for evt, func, args, kwargs in self._registry.pollers:
370
388
  self.stop_events.append(evt)
371
- self._executor.submit(func, *args, **kwargs)
389
+ poller_thread = threading.Thread(
390
+ target=func, args=args, kwargs=kwargs, daemon=True
391
+ )
392
+ poller_thread.start()
393
+ self._background_threads.append(poller_thread)
372
394
  self._registry.pollers = []
373
395
 
374
396
  dbos_logger.info("DBOS launched")
@@ -400,6 +422,8 @@ class DBOS:
400
422
  if self._executor_field is not None:
401
423
  self._executor_field.shutdown(cancel_futures=True)
402
424
  self._executor_field = None
425
+ for bg_thread in self._background_threads:
426
+ bg_thread.join()
403
427
 
404
428
  @classmethod
405
429
  def register_instance(cls, inst: object) -> None:
@@ -829,7 +853,7 @@ class DBOSConfiguredInstance:
829
853
 
830
854
  # Apps that import DBOS probably don't exit. If they do, let's see if
831
855
  # it looks like startup was abandoned or a call was forgotten...
832
- def log_exit_info() -> None:
856
+ def dbos_exit_hook() -> None:
833
857
  if _dbos_global_registry is None:
834
858
  # Probably used as or for a support module
835
859
  return
@@ -843,7 +867,9 @@ def log_exit_info() -> None:
843
867
  print("DBOS exiting; DBOS exists but launch() was not called")
844
868
  dbos_logger.warning("DBOS exiting; DBOS exists but launch() was not called")
845
869
  return
870
+ # If we get here, we're exiting normally
871
+ _dbos_global_instance.destroy()
846
872
 
847
873
 
848
874
  # Register the exit hook
849
- atexit.register(log_exit_info)
875
+ atexit.register(dbos_exit_hook)
@@ -2,7 +2,7 @@ import json
2
2
  import os
3
3
  import re
4
4
  from importlib import resources
5
- from typing import Any, Dict, List, Optional, TypedDict
5
+ from typing import Any, Dict, List, Optional, TypedDict, cast
6
6
 
7
7
  import yaml
8
8
  from jsonschema import ValidationError, validate
@@ -14,6 +14,7 @@ from dbos.logger import dbos_logger
14
14
 
15
15
  class RuntimeConfig(TypedDict, total=False):
16
16
  start: List[str]
17
+ admin_port: Optional[int]
17
18
 
18
19
 
19
20
  class DatabaseConfig(TypedDict, total=False):
@@ -26,6 +27,7 @@ class DatabaseConfig(TypedDict, total=False):
26
27
  sys_db_name: Optional[str]
27
28
  ssl: Optional[bool]
28
29
  ssl_ca: Optional[str]
30
+ local_suffix: Optional[bool]
29
31
  app_db_client: Optional[str]
30
32
  migrate: Optional[List[str]]
31
33
  rollback: Optional[List[str]]
@@ -163,6 +165,11 @@ def load_config(config_file_path: str = "dbos-config.yaml") -> ConfigFile:
163
165
  if "runtimeConfig" not in data or "start" not in data["runtimeConfig"]:
164
166
  raise DBOSInitializationError(f"dbos-config.yaml must specify a start command")
165
167
 
168
+ data = cast(ConfigFile, data)
169
+
170
+ if "local_suffix" in data["database"] and data["database"]["local_suffix"]:
171
+ data["database"]["app_db_name"] = f"{data['database']['app_db_name']}_local"
172
+
166
173
  # Return data as ConfigFile type
167
174
  return data # type: ignore
168
175
 
@@ -8,22 +8,22 @@ authors = [
8
8
  dependencies = [
9
9
  "pyyaml>=6.0.2",
10
10
  "jsonschema>=4.23.0",
11
- "alembic>=1.13.2",
11
+ "alembic>=1.13.3",
12
12
  "typing-extensions>=4.12.2; python_version < \"3.10\"",
13
- "typer>=0.12.3",
14
- "jsonpickle>=3.2.2",
15
- "opentelemetry-api>=1.26.0",
16
- "opentelemetry-sdk>=1.26.0",
17
- "opentelemetry-exporter-otlp-proto-http>=1.26.0",
13
+ "typer>=0.12.5",
14
+ "jsonpickle>=3.3.0",
15
+ "opentelemetry-api>=1.27.0",
16
+ "opentelemetry-sdk>=1.27.0",
17
+ "opentelemetry-exporter-otlp-proto-http>=1.27.0",
18
18
  "python-dateutil>=2.9.0.post0",
19
- "fastapi[standard]>=0.112.1",
19
+ "fastapi[standard]>=0.115.2",
20
20
  "psutil>=6.0.0",
21
21
  "tomlkit>=0.13.2",
22
- "psycopg>=3.2.1",
22
+ "psycopg>=3.2.3",
23
23
  ]
24
24
  requires-python = ">=3.9"
25
25
  readme = "README.md"
26
- version = "0.9.0a1"
26
+ version = "0.10.0"
27
27
 
28
28
  [project.license]
29
29
  text = "MIT"
@@ -46,26 +46,26 @@ version_format = "version:format_version"
46
46
 
47
47
  [tool.pdm.dev-dependencies]
48
48
  dev = [
49
- "pytest>=8.3.2",
50
- "mypy>=1.11.1",
49
+ "pytest>=8.3.3",
50
+ "mypy>=1.12.0",
51
51
  "pytest-mock>=3.14.0",
52
52
  "types-PyYAML>=6.0.12.20240808",
53
53
  "types-jsonschema>=4.23.0.20240813",
54
- "black>=24.8.0",
55
- "pre-commit>=3.8.0",
54
+ "black>=24.10.0",
55
+ "pre-commit>=4.0.1",
56
56
  "isort>=5.13.2",
57
57
  "types-psutil>=6.0.0.20240621",
58
58
  "requests>=2.32.3",
59
- "types-requests>=2.32.0.20240712",
60
- "httpx>=0.27.0",
61
- "pytz>=2024.1",
59
+ "types-requests>=2.32.0.20240914",
60
+ "httpx>=0.27.2",
61
+ "pytz>=2024.2",
62
62
  "GitPython>=3.1.43",
63
- "confluent-kafka>=2.5.3",
63
+ "confluent-kafka>=2.6.0",
64
64
  "types-confluent-kafka>=1.2.2",
65
65
  "flask>=3.0.3",
66
66
  "pytest-order>=1.3.0",
67
67
  "pyjwt>=2.9.0",
68
- "pdm-backend>=2.3.3",
68
+ "pdm-backend>=2.4.2",
69
69
  ]
70
70
 
71
71
  [tool.black]
@@ -123,3 +123,39 @@ def test_admin_recovery(dbos: DBOS) -> None:
123
123
  time.sleep(1)
124
124
  print(f"Attempt {attempt + 1} failed. Retrying in 1 second...")
125
125
  assert succeeded, "Workflow did not recover"
126
+
127
+
128
+ def test_admin_diff_port(cleanup_test_databases: None) -> None:
129
+ # Initialize singleton
130
+ DBOS.destroy() # In case of other tests leaving it
131
+
132
+ config_string = """name: test-app
133
+ language: python
134
+ database:
135
+ hostname: localhost
136
+ port: 5432
137
+ username: postgres
138
+ password: ${PGPASSWORD}
139
+ app_db_name: dbostestpy
140
+ runtimeConfig:
141
+ start:
142
+ - python3 main.py
143
+ admin_port: 8001
144
+ """
145
+ # Write the config to a text file for the moment
146
+ with open("dbos-config.yaml", "w") as file:
147
+ file.write(config_string)
148
+
149
+ try:
150
+ # Initialize DBOS
151
+ DBOS()
152
+ DBOS.launch()
153
+
154
+ # Test GET /dbos-healthz
155
+ response = requests.get("http://localhost:8001/dbos-healthz", timeout=5)
156
+ assert response.status_code == 200
157
+ assert response.text == "healthy"
158
+ finally:
159
+ # Clean up after the test
160
+ DBOS.destroy()
161
+ os.remove("dbos-config.yaml")
@@ -32,6 +32,7 @@ def test_valid_config(mocker):
32
32
  runtimeConfig:
33
33
  start:
34
34
  - "python3 main.py"
35
+ admin_port: 8001
35
36
  database:
36
37
  hostname: 'some host'
37
38
  port: 1234
@@ -196,3 +197,37 @@ def test_config_no_start(mocker):
196
197
  load_config(mock_filename)
197
198
 
198
199
  assert "start command" in str(exc_info.value)
200
+
201
+
202
+ def test_local_config(mocker):
203
+ mock_config = """
204
+ name: "some app"
205
+ language: "python"
206
+ runtimeConfig:
207
+ start:
208
+ - "python3 main.py"
209
+ admin_port: 8001
210
+ database:
211
+ hostname: 'some host'
212
+ port: 1234
213
+ username: 'some user'
214
+ password: ${PGPASSWORD}
215
+ app_db_name: 'some_db'
216
+ connectionTimeoutMillis: 3000
217
+ local_suffix: true
218
+ """
219
+ os.environ["BARBAR"] = "FOOFOO"
220
+ mocker.patch(
221
+ "builtins.open", side_effect=generate_mock_open(mock_filename, mock_config)
222
+ )
223
+
224
+ configFile = load_config(mock_filename)
225
+ assert configFile["name"] == "some app"
226
+ assert configFile["database"]["local_suffix"] == True
227
+ assert configFile["language"] == "python"
228
+ assert configFile["database"]["hostname"] == "some host"
229
+ assert configFile["database"]["port"] == 1234
230
+ assert configFile["database"]["username"] == "some user"
231
+ assert configFile["database"]["password"] == os.environ["PGPASSWORD"]
232
+ assert configFile["database"]["app_db_name"] == "some_db_local"
233
+ assert configFile["database"]["connectionTimeoutMillis"] == 3000
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
File without changes
File without changes