dbos 0.5.0a11__py3-none-any.whl → 0.6.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.
Potentially problematic release.
This version of dbos might be problematic. Click here for more details.
- dbos/__init__.py +7 -9
- dbos/admin_sever.py +2 -2
- dbos/cli.py +19 -6
- dbos/context.py +18 -2
- dbos/core.py +17 -1
- dbos/dbos.py +91 -12
- dbos/dbos_config.py +1 -0
- dbos/error.py +2 -0
- dbos/fastapi.py +1 -3
- dbos/flask.py +79 -0
- dbos/kafka.py +94 -0
- dbos/kafka_message.py +15 -0
- dbos/request.py +2 -0
- dbos/roles.py +1 -1
- dbos/system_database.py +21 -14
- dbos/templates/hello/__package/main.py +15 -22
- dbos/templates/hello/__package/schema.py +2 -2
- dbos/templates/hello/migrations/versions/2024_07_31_180642_init.py +2 -2
- dbos/tracer.py +9 -1
- dbos-0.6.0.dist-info/METADATA +132 -0
- {dbos-0.5.0a11.dist-info → dbos-0.6.0.dist-info}/RECORD +24 -21
- dbos-0.5.0a11.dist-info/METADATA +0 -78
- {dbos-0.5.0a11.dist-info → dbos-0.6.0.dist-info}/WHEEL +0 -0
- {dbos-0.5.0a11.dist-info → dbos-0.6.0.dist-info}/entry_points.txt +0 -0
- {dbos-0.5.0a11.dist-info → dbos-0.6.0.dist-info}/licenses/LICENSE +0 -0
dbos/__init__.py
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
from . import error as error
|
|
2
|
-
from .context import
|
|
3
|
-
from .dbos import DBOS
|
|
4
|
-
from .
|
|
5
|
-
from .
|
|
6
|
-
from .
|
|
7
|
-
from .dbos_config import ConfigFile as ConfigFile
|
|
8
|
-
from .dbos_config import get_dbos_database_url, load_config
|
|
9
|
-
from .system_database import GetWorkflowsInput as GetWorkflowsInput
|
|
10
|
-
from .system_database import WorkflowStatusString as WorkflowStatusString
|
|
2
|
+
from .context import DBOSContextEnsure, SetWorkflowID
|
|
3
|
+
from .dbos import DBOS, DBOSConfiguredInstance, WorkflowHandle, WorkflowStatus
|
|
4
|
+
from .dbos_config import ConfigFile, get_dbos_database_url, load_config
|
|
5
|
+
from .kafka_message import KafkaMessage
|
|
6
|
+
from .system_database import GetWorkflowsInput, WorkflowStatusString
|
|
11
7
|
|
|
12
8
|
__all__ = [
|
|
13
9
|
"ConfigFile",
|
|
14
10
|
"DBOS",
|
|
15
11
|
"DBOSConfiguredInstance",
|
|
12
|
+
"DBOSContextEnsure",
|
|
16
13
|
"GetWorkflowsInput",
|
|
14
|
+
"KafkaMessage",
|
|
17
15
|
"SetWorkflowID",
|
|
18
16
|
"WorkflowHandle",
|
|
19
17
|
"WorkflowStatus",
|
dbos/admin_sever.py
CHANGED
|
@@ -28,11 +28,11 @@ class AdminServer:
|
|
|
28
28
|
self.server_thread = threading.Thread(target=self.server.serve_forever)
|
|
29
29
|
self.server_thread.daemon = True
|
|
30
30
|
|
|
31
|
-
dbos_logger.
|
|
31
|
+
dbos_logger.debug("Starting DBOS admin server on port %d", self.port)
|
|
32
32
|
self.server_thread.start()
|
|
33
33
|
|
|
34
34
|
def stop(self) -> None:
|
|
35
|
-
dbos_logger.
|
|
35
|
+
dbos_logger.debug("Stopping DBOS admin server")
|
|
36
36
|
self.server.shutdown()
|
|
37
37
|
self.server.server_close()
|
|
38
38
|
self.server_thread.join()
|
dbos/cli.py
CHANGED
|
@@ -119,7 +119,7 @@ def copy_template_dir(src_dir: str, dst_dir: str, ctx: dict[str, str]) -> None:
|
|
|
119
119
|
shutil.copy(src, dst)
|
|
120
120
|
|
|
121
121
|
|
|
122
|
-
def copy_template(src_dir: str, project_name: str) -> None:
|
|
122
|
+
def copy_template(src_dir: str, project_name: str, config_mode: bool) -> None:
|
|
123
123
|
|
|
124
124
|
dst_dir = path.abspath(".")
|
|
125
125
|
|
|
@@ -131,10 +131,17 @@ def copy_template(src_dir: str, project_name: str) -> None:
|
|
|
131
131
|
"db_name": db_name,
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
if config_mode:
|
|
135
|
+
copy_dbos_template(
|
|
136
|
+
os.path.join(src_dir, "dbos-config.yaml.dbos"),
|
|
137
|
+
os.path.join(dst_dir, "dbos-config.yaml"),
|
|
138
|
+
ctx,
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
copy_template_dir(src_dir, dst_dir, ctx)
|
|
142
|
+
copy_template_dir(
|
|
143
|
+
path.join(src_dir, "__package"), path.join(dst_dir, package_name), ctx
|
|
144
|
+
)
|
|
138
145
|
|
|
139
146
|
|
|
140
147
|
def get_project_name() -> typing.Union[str, None]:
|
|
@@ -173,6 +180,10 @@ def init(
|
|
|
173
180
|
typing.Optional[str],
|
|
174
181
|
typer.Option("--template", "-t", help="Specify template to use"),
|
|
175
182
|
] = None,
|
|
183
|
+
config: Annotated[
|
|
184
|
+
bool,
|
|
185
|
+
typer.Option("--config", "-c", help="Only add dbos-config.yaml"),
|
|
186
|
+
] = False,
|
|
176
187
|
) -> None:
|
|
177
188
|
try:
|
|
178
189
|
if project_name is None:
|
|
@@ -199,7 +210,9 @@ def init(
|
|
|
199
210
|
if template not in templates:
|
|
200
211
|
raise Exception(f"template {template} not found in {templates_dir}")
|
|
201
212
|
|
|
202
|
-
copy_template(
|
|
213
|
+
copy_template(
|
|
214
|
+
path.join(templates_dir, template), project_name, config_mode=config
|
|
215
|
+
)
|
|
203
216
|
except Exception as e:
|
|
204
217
|
print(f"[red]{e}[/red]")
|
|
205
218
|
|
dbos/context.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import uuid
|
|
5
6
|
from contextvars import ContextVar
|
|
@@ -39,6 +40,9 @@ class TracedAttributes(TypedDict, total=False):
|
|
|
39
40
|
applicationID: Optional[str]
|
|
40
41
|
applicationVersion: Optional[str]
|
|
41
42
|
executorID: Optional[str]
|
|
43
|
+
authenticatedUser: Optional[str]
|
|
44
|
+
authenticatedUserRoles: Optional[str]
|
|
45
|
+
authenticatedUserAssumedRole: Optional[str]
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
class DBOSContext:
|
|
@@ -159,6 +163,13 @@ class DBOSContext:
|
|
|
159
163
|
attributes["operationUUID"] = (
|
|
160
164
|
self.workflow_id if len(self.workflow_id) > 0 else None
|
|
161
165
|
)
|
|
166
|
+
attributes["authenticatedUser"] = self.authenticated_user
|
|
167
|
+
attributes["authenticatedUserRoles"] = (
|
|
168
|
+
json.dumps(self.authenticated_roles)
|
|
169
|
+
if self.authenticated_roles is not None
|
|
170
|
+
else ""
|
|
171
|
+
)
|
|
172
|
+
attributes["authenticatedUserAssumedRole"] = self.assumed_role
|
|
162
173
|
span = dbos_tracer.start_span(
|
|
163
174
|
attributes, parent=self.spans[-1] if len(self.spans) > 0 else None
|
|
164
175
|
)
|
|
@@ -178,6 +189,11 @@ class DBOSContext:
|
|
|
178
189
|
) -> None:
|
|
179
190
|
self.authenticated_user = user
|
|
180
191
|
self.authenticated_roles = roles
|
|
192
|
+
if user is not None and len(self.spans) > 0:
|
|
193
|
+
self.spans[-1].set_attribute("authenticatedUser", user)
|
|
194
|
+
self.spans[-1].set_attribute(
|
|
195
|
+
"authenticatedUserRoles", json.dumps(roles) if roles is not None else ""
|
|
196
|
+
)
|
|
181
197
|
|
|
182
198
|
|
|
183
199
|
##############################################################
|
|
@@ -217,13 +233,13 @@ class DBOSContextEnsure:
|
|
|
217
233
|
def __init__(self) -> None:
|
|
218
234
|
self.created_ctx = False
|
|
219
235
|
|
|
220
|
-
def __enter__(self) ->
|
|
236
|
+
def __enter__(self) -> DBOSContext:
|
|
221
237
|
# Code to create a basic context
|
|
222
238
|
ctx = get_local_dbos_context()
|
|
223
239
|
if ctx is None:
|
|
224
240
|
self.created_ctx = True
|
|
225
241
|
set_local_dbos_context(DBOSContext())
|
|
226
|
-
return
|
|
242
|
+
return assert_current_dbos_context()
|
|
227
243
|
|
|
228
244
|
def __exit__(
|
|
229
245
|
self,
|
dbos/core.py
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import sys
|
|
2
3
|
import time
|
|
3
4
|
import traceback
|
|
4
5
|
from concurrent.futures import Future
|
|
5
6
|
from functools import wraps
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import (
|
|
8
|
+
TYPE_CHECKING,
|
|
9
|
+
Any,
|
|
10
|
+
Callable,
|
|
11
|
+
Generic,
|
|
12
|
+
List,
|
|
13
|
+
Optional,
|
|
14
|
+
Tuple,
|
|
15
|
+
TypeVar,
|
|
16
|
+
cast,
|
|
17
|
+
)
|
|
7
18
|
|
|
8
19
|
from dbos.application_database import ApplicationDatabase, TransactionResultInternal
|
|
9
20
|
|
|
@@ -134,6 +145,11 @@ def _init_workflow(
|
|
|
134
145
|
"executor_id": ctx.executor_id,
|
|
135
146
|
"request": (utils.serialize(ctx.request) if ctx.request is not None else None),
|
|
136
147
|
"recovery_attempts": None,
|
|
148
|
+
"authenticated_user": ctx.authenticated_user,
|
|
149
|
+
"authenticated_roles": (
|
|
150
|
+
json.dumps(ctx.authenticated_roles) if ctx.authenticated_roles else None
|
|
151
|
+
),
|
|
152
|
+
"assumed_role": ctx.assumed_role,
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
# If we have a class name, the first arg is the instance and do not serialize
|
dbos/dbos.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import atexit
|
|
4
|
+
import json
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
6
7
|
import threading
|
|
@@ -19,6 +20,7 @@ from typing import (
|
|
|
19
20
|
Tuple,
|
|
20
21
|
Type,
|
|
21
22
|
TypeVar,
|
|
23
|
+
cast,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
from opentelemetry.trace import Span
|
|
@@ -52,10 +54,14 @@ from .tracer import dbos_tracer
|
|
|
52
54
|
|
|
53
55
|
if TYPE_CHECKING:
|
|
54
56
|
from fastapi import FastAPI
|
|
57
|
+
from dbos.kafka import KafkaConsumerWorkflow
|
|
55
58
|
from .request import Request
|
|
59
|
+
from flask import Flask
|
|
56
60
|
|
|
57
61
|
from sqlalchemy.orm import Session
|
|
58
62
|
|
|
63
|
+
from dbos.request import Request
|
|
64
|
+
|
|
59
65
|
if sys.version_info < (3, 10):
|
|
60
66
|
from typing_extensions import ParamSpec, TypeAlias
|
|
61
67
|
else:
|
|
@@ -205,6 +211,7 @@ class DBOS:
|
|
|
205
211
|
*,
|
|
206
212
|
config: Optional[ConfigFile] = None,
|
|
207
213
|
fastapi: Optional["FastAPI"] = None,
|
|
214
|
+
flask: Optional["Flask"] = None,
|
|
208
215
|
) -> DBOS:
|
|
209
216
|
global _dbos_global_instance
|
|
210
217
|
global _dbos_global_registry
|
|
@@ -219,7 +226,7 @@ class DBOS:
|
|
|
219
226
|
)
|
|
220
227
|
config = _dbos_global_registry.config
|
|
221
228
|
_dbos_global_instance = super().__new__(cls)
|
|
222
|
-
_dbos_global_instance.__init__(fastapi=fastapi, config=config) # type: ignore
|
|
229
|
+
_dbos_global_instance.__init__(fastapi=fastapi, config=config, flask=flask) # type: ignore
|
|
223
230
|
else:
|
|
224
231
|
if (config is not None and _dbos_global_instance.config is not config) or (
|
|
225
232
|
_dbos_global_instance.fastapi is not fastapi
|
|
@@ -243,6 +250,7 @@ class DBOS:
|
|
|
243
250
|
*,
|
|
244
251
|
config: Optional[ConfigFile] = None,
|
|
245
252
|
fastapi: Optional["FastAPI"] = None,
|
|
253
|
+
flask: Optional["Flask"] = None,
|
|
246
254
|
) -> None:
|
|
247
255
|
if hasattr(self, "_initialized") and self._initialized:
|
|
248
256
|
return
|
|
@@ -264,14 +272,44 @@ class DBOS:
|
|
|
264
272
|
self._admin_server: Optional[AdminServer] = None
|
|
265
273
|
self.stop_events: List[threading.Event] = []
|
|
266
274
|
self.fastapi: Optional["FastAPI"] = fastapi
|
|
275
|
+
self.flask: Optional["Flask"] = flask
|
|
267
276
|
self._executor: Optional[ThreadPoolExecutor] = None
|
|
277
|
+
|
|
278
|
+
# If using FastAPI, set up middleware and lifecycle events
|
|
268
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
|
+
|
|
269
301
|
from dbos.fastapi import setup_fastapi_middleware
|
|
270
302
|
|
|
271
303
|
setup_fastapi_middleware(self.fastapi)
|
|
272
304
|
self.fastapi.on_event("startup")(self._launch)
|
|
273
305
|
self.fastapi.on_event("shutdown")(self._destroy)
|
|
274
306
|
|
|
307
|
+
# If using Flask, set up middleware
|
|
308
|
+
if self.flask is not None:
|
|
309
|
+
from dbos.flask import setup_flask_middleware
|
|
310
|
+
|
|
311
|
+
setup_flask_middleware(self.flask)
|
|
312
|
+
|
|
275
313
|
# Register send_stub as a workflow
|
|
276
314
|
def send_temp_workflow(
|
|
277
315
|
destination_id: str, message: Any, topic: Optional[str]
|
|
@@ -471,6 +509,20 @@ class DBOS:
|
|
|
471
509
|
|
|
472
510
|
return scheduled(_get_or_create_dbos_registry(), cron)
|
|
473
511
|
|
|
512
|
+
@classmethod
|
|
513
|
+
def kafka_consumer(
|
|
514
|
+
cls, config: dict[str, Any], topics: list[str]
|
|
515
|
+
) -> Callable[[KafkaConsumerWorkflow], KafkaConsumerWorkflow]:
|
|
516
|
+
"""Decorate a function to be used as a Kafka consumer."""
|
|
517
|
+
try:
|
|
518
|
+
from dbos.kafka import kafka_consumer
|
|
519
|
+
|
|
520
|
+
return kafka_consumer(_get_or_create_dbos_registry(), config, topics)
|
|
521
|
+
except ModuleNotFoundError as e:
|
|
522
|
+
raise DBOSException(
|
|
523
|
+
f"{e.name} dependency not found. Please install {e.name} via your package manager."
|
|
524
|
+
) from e
|
|
525
|
+
|
|
474
526
|
@classmethod
|
|
475
527
|
def start_workflow(
|
|
476
528
|
cls,
|
|
@@ -502,9 +554,13 @@ class DBOS:
|
|
|
502
554
|
recovery_attempts=stat["recovery_attempts"],
|
|
503
555
|
class_name=stat["class_name"],
|
|
504
556
|
config_name=stat["config_name"],
|
|
505
|
-
authenticated_user=
|
|
506
|
-
assumed_role=
|
|
507
|
-
|
|
557
|
+
authenticated_user=stat["authenticated_user"],
|
|
558
|
+
assumed_role=stat["assumed_role"],
|
|
559
|
+
authenticated_roles=(
|
|
560
|
+
json.loads(stat["authenticated_roles"])
|
|
561
|
+
if stat["authenticated_roles"] is not None
|
|
562
|
+
else None
|
|
563
|
+
),
|
|
508
564
|
)
|
|
509
565
|
|
|
510
566
|
@classmethod
|
|
@@ -663,6 +719,33 @@ class DBOS:
|
|
|
663
719
|
ctx = assert_current_dbos_context()
|
|
664
720
|
return ctx.request
|
|
665
721
|
|
|
722
|
+
@classproperty
|
|
723
|
+
def authenticated_user(cls) -> Optional[str]:
|
|
724
|
+
"""Return the current authenticated user, if any, associated with the current context."""
|
|
725
|
+
ctx = assert_current_dbos_context()
|
|
726
|
+
return ctx.authenticated_user
|
|
727
|
+
|
|
728
|
+
@classproperty
|
|
729
|
+
def authenticated_roles(cls) -> Optional[List[str]]:
|
|
730
|
+
"""Return the roles granted to the current authenticated user, if any, associated with the current context."""
|
|
731
|
+
ctx = assert_current_dbos_context()
|
|
732
|
+
return ctx.authenticated_roles
|
|
733
|
+
|
|
734
|
+
@classproperty
|
|
735
|
+
def assumed_role(cls) -> Optional[str]:
|
|
736
|
+
"""Return the role currently assumed by the authenticated user, if any, associated with the current context."""
|
|
737
|
+
ctx = assert_current_dbos_context()
|
|
738
|
+
return ctx.assumed_role
|
|
739
|
+
|
|
740
|
+
@classmethod
|
|
741
|
+
def set_authentication(
|
|
742
|
+
cls, authenticated_user: Optional[str], authenticated_roles: Optional[List[str]]
|
|
743
|
+
) -> None:
|
|
744
|
+
"""Set the current authenticated user and granted roles into the current context."""
|
|
745
|
+
ctx = assert_current_dbos_context()
|
|
746
|
+
ctx.authenticated_user = authenticated_user
|
|
747
|
+
ctx.authenticated_roles = authenticated_roles
|
|
748
|
+
|
|
666
749
|
|
|
667
750
|
@dataclass
|
|
668
751
|
class WorkflowStatus:
|
|
@@ -679,7 +762,7 @@ class WorkflowStatus:
|
|
|
679
762
|
config_name(str): For instance member functions, the name of the class instance for the execution
|
|
680
763
|
authenticated_user(str): The user who invoked the workflow
|
|
681
764
|
assumed_role(str): The access role used by the user to allow access to the workflow function
|
|
682
|
-
|
|
765
|
+
authenticated_roles(List[str]): List of all access roles available to the authenticated user
|
|
683
766
|
recovery_attempts(int): Number of times the workflow has been restarted (usually by recovery)
|
|
684
767
|
|
|
685
768
|
"""
|
|
@@ -691,7 +774,7 @@ class WorkflowStatus:
|
|
|
691
774
|
config_name: Optional[str]
|
|
692
775
|
authenticated_user: Optional[str]
|
|
693
776
|
assumed_role: Optional[str]
|
|
694
|
-
|
|
777
|
+
authenticated_roles: Optional[List[str]]
|
|
695
778
|
recovery_attempts: Optional[int]
|
|
696
779
|
|
|
697
780
|
|
|
@@ -740,13 +823,9 @@ class DBOSConfiguredInstance:
|
|
|
740
823
|
|
|
741
824
|
"""
|
|
742
825
|
|
|
743
|
-
def __init__(self, config_name: str
|
|
826
|
+
def __init__(self, config_name: str) -> None:
|
|
744
827
|
self.config_name = config_name
|
|
745
|
-
|
|
746
|
-
assert isinstance(dbos, DBOS)
|
|
747
|
-
dbos._registry.register_instance(self)
|
|
748
|
-
else:
|
|
749
|
-
DBOS.register_instance(self)
|
|
828
|
+
DBOS.register_instance(self)
|
|
750
829
|
|
|
751
830
|
|
|
752
831
|
# Apps that import DBOS probably don't exit. If they do, let's see if
|
dbos/dbos_config.py
CHANGED
dbos/error.py
CHANGED
|
@@ -17,6 +17,7 @@ class DBOSException(Exception):
|
|
|
17
17
|
def __init__(self, message: str, dbos_error_code: Optional[int] = None):
|
|
18
18
|
self.message = message
|
|
19
19
|
self.dbos_error_code = dbos_error_code
|
|
20
|
+
self.status_code: Optional[int] = None
|
|
20
21
|
super().__init__(self.message)
|
|
21
22
|
|
|
22
23
|
def __str__(self) -> str:
|
|
@@ -104,6 +105,7 @@ class DBOSNotAuthorizedError(DBOSException):
|
|
|
104
105
|
msg,
|
|
105
106
|
dbos_error_code=DBOSErrorCode.NotAuthorized.value,
|
|
106
107
|
)
|
|
108
|
+
self.status_code = 403
|
|
107
109
|
|
|
108
110
|
|
|
109
111
|
class DBOSMaxStepRetriesExceeded(DBOSException):
|
dbos/fastapi.py
CHANGED
|
@@ -11,9 +11,7 @@ from .context import (
|
|
|
11
11
|
TracedAttributes,
|
|
12
12
|
assert_current_dbos_context,
|
|
13
13
|
)
|
|
14
|
-
from .request import Address, Request
|
|
15
|
-
|
|
16
|
-
request_id_header = "x-request-id"
|
|
14
|
+
from .request import Address, Request, request_id_header
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
def get_or_generate_request_id(request: FastAPIRequest) -> str:
|
dbos/flask.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Any
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from flask import Flask, request
|
|
6
|
+
from werkzeug.wrappers import Request as WRequest
|
|
7
|
+
|
|
8
|
+
from dbos.context import (
|
|
9
|
+
EnterDBOSHandler,
|
|
10
|
+
OperationType,
|
|
11
|
+
SetWorkflowID,
|
|
12
|
+
TracedAttributes,
|
|
13
|
+
assert_current_dbos_context,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .request import Address, Request, request_id_header
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FlaskMiddleware:
|
|
20
|
+
def __init__(self, app: Any) -> None:
|
|
21
|
+
self.app = app
|
|
22
|
+
|
|
23
|
+
def __call__(self, environ: Any, start_response: Any) -> Any:
|
|
24
|
+
request = WRequest(environ)
|
|
25
|
+
attributes: TracedAttributes = {
|
|
26
|
+
"name": urlparse(request.url).path,
|
|
27
|
+
"requestID": get_or_generate_request_id(request),
|
|
28
|
+
"requestIP": (
|
|
29
|
+
request.remote_addr if request.remote_addr is not None else None
|
|
30
|
+
),
|
|
31
|
+
"requestURL": request.url,
|
|
32
|
+
"requestMethod": request.method,
|
|
33
|
+
"operationType": OperationType.HANDLER.value,
|
|
34
|
+
}
|
|
35
|
+
with EnterDBOSHandler(attributes):
|
|
36
|
+
ctx = assert_current_dbos_context()
|
|
37
|
+
ctx.request = make_request(request)
|
|
38
|
+
workflow_id = request.headers.get("dbos-idempotency-key", "")
|
|
39
|
+
with SetWorkflowID(workflow_id):
|
|
40
|
+
response = self.app(environ, start_response)
|
|
41
|
+
return response
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_or_generate_request_id(request: WRequest) -> str:
|
|
45
|
+
request_id = request.headers.get(request_id_header, None)
|
|
46
|
+
if request_id is not None:
|
|
47
|
+
return request_id
|
|
48
|
+
else:
|
|
49
|
+
return str(uuid.uuid4())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_request(request: WRequest) -> Request:
|
|
53
|
+
parsed_url = urlparse(request.url)
|
|
54
|
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
55
|
+
|
|
56
|
+
client = None
|
|
57
|
+
if request.remote_addr:
|
|
58
|
+
hostname = request.remote_addr
|
|
59
|
+
port = request.environ.get("REMOTE_PORT")
|
|
60
|
+
if port:
|
|
61
|
+
client = Address(hostname=hostname, port=int(port))
|
|
62
|
+
else:
|
|
63
|
+
# If port is not available, use 0 as a placeholder
|
|
64
|
+
client = Address(hostname=hostname, port=0)
|
|
65
|
+
|
|
66
|
+
return Request(
|
|
67
|
+
headers=dict(request.headers),
|
|
68
|
+
path_params={},
|
|
69
|
+
query_params=dict(request.args),
|
|
70
|
+
url=request.url,
|
|
71
|
+
base_url=base_url,
|
|
72
|
+
client=client,
|
|
73
|
+
cookies=dict(request.cookies),
|
|
74
|
+
method=request.method,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def setup_flask_middleware(app: Flask) -> None:
|
|
79
|
+
app.wsgi_app = FlaskMiddleware(app.wsgi_app) # type: ignore
|
dbos/kafka.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import traceback
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn, Optional, Union
|
|
5
|
+
|
|
6
|
+
from confluent_kafka import Consumer, KafkaError, KafkaException
|
|
7
|
+
from confluent_kafka import Message as CTypeMessage
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from dbos.dbos import _DBOSRegistry
|
|
11
|
+
|
|
12
|
+
from .context import SetWorkflowID
|
|
13
|
+
from .kafka_message import KafkaMessage
|
|
14
|
+
from .logger import dbos_logger
|
|
15
|
+
|
|
16
|
+
KafkaConsumerWorkflow = Callable[[KafkaMessage], None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _kafka_consumer_loop(
|
|
20
|
+
func: KafkaConsumerWorkflow,
|
|
21
|
+
config: dict[str, Any],
|
|
22
|
+
topics: list[str],
|
|
23
|
+
stop_event: threading.Event,
|
|
24
|
+
) -> None:
|
|
25
|
+
|
|
26
|
+
def on_error(err: KafkaError) -> NoReturn:
|
|
27
|
+
raise KafkaException(err)
|
|
28
|
+
|
|
29
|
+
config["error_cb"] = on_error
|
|
30
|
+
if "auto.offset.reset" not in config:
|
|
31
|
+
config["auto.offset.reset"] = "earliest"
|
|
32
|
+
|
|
33
|
+
consumer = Consumer(config)
|
|
34
|
+
try:
|
|
35
|
+
consumer.subscribe(topics)
|
|
36
|
+
while not stop_event.is_set():
|
|
37
|
+
cmsg = consumer.poll(1.0)
|
|
38
|
+
|
|
39
|
+
if stop_event.is_set():
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if cmsg is None:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
err = cmsg.error()
|
|
46
|
+
if err is not None:
|
|
47
|
+
dbos_logger.error(
|
|
48
|
+
f"Kafka error {err.code()} ({err.name()}): {err.str()}"
|
|
49
|
+
)
|
|
50
|
+
# fatal errors require an updated consumer instance
|
|
51
|
+
if err.code() == KafkaError._FATAL or err.fatal():
|
|
52
|
+
original_consumer = consumer
|
|
53
|
+
try:
|
|
54
|
+
consumer = Consumer(config)
|
|
55
|
+
consumer.subscribe(topics)
|
|
56
|
+
finally:
|
|
57
|
+
original_consumer.close()
|
|
58
|
+
else:
|
|
59
|
+
msg = KafkaMessage(
|
|
60
|
+
headers=cmsg.headers(),
|
|
61
|
+
key=cmsg.key(),
|
|
62
|
+
latency=cmsg.latency(),
|
|
63
|
+
leader_epoch=cmsg.leader_epoch(),
|
|
64
|
+
offset=cmsg.offset(),
|
|
65
|
+
partition=cmsg.partition(),
|
|
66
|
+
timestamp=cmsg.timestamp(),
|
|
67
|
+
topic=cmsg.topic(),
|
|
68
|
+
value=cmsg.value(),
|
|
69
|
+
)
|
|
70
|
+
with SetWorkflowID(
|
|
71
|
+
f"kafka-unique-id-{msg.topic}-{msg.partition}-{msg.offset}"
|
|
72
|
+
):
|
|
73
|
+
try:
|
|
74
|
+
func(msg)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
dbos_logger.error(
|
|
77
|
+
f"Exception encountered in Kafka consumer: {traceback.format_exc()}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
finally:
|
|
81
|
+
consumer.close()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def kafka_consumer(
|
|
85
|
+
dbosreg: "_DBOSRegistry", config: dict[str, Any], topics: list[str]
|
|
86
|
+
) -> Callable[[KafkaConsumerWorkflow], KafkaConsumerWorkflow]:
|
|
87
|
+
def decorator(func: KafkaConsumerWorkflow) -> KafkaConsumerWorkflow:
|
|
88
|
+
stop_event = threading.Event()
|
|
89
|
+
dbosreg.register_poller(
|
|
90
|
+
stop_event, _kafka_consumer_loop, func, config, topics, stop_event
|
|
91
|
+
)
|
|
92
|
+
return func
|
|
93
|
+
|
|
94
|
+
return decorator
|
dbos/kafka_message.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class KafkaMessage:
|
|
7
|
+
headers: Optional[list[tuple[str, Union[str, bytes]]]]
|
|
8
|
+
key: Optional[Union[str, bytes]]
|
|
9
|
+
latency: Optional[float]
|
|
10
|
+
leader_epoch: Optional[int]
|
|
11
|
+
offset: Optional[int]
|
|
12
|
+
partition: Optional[int]
|
|
13
|
+
timestamp: tuple[int, int]
|
|
14
|
+
topic: Optional[str]
|
|
15
|
+
value: Optional[Union[str, bytes]]
|
dbos/request.py
CHANGED
dbos/roles.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, TypeVar,
|
|
|
4
4
|
from dbos.error import DBOSNotAuthorizedError
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
|
-
from dbos.dbos import
|
|
7
|
+
from dbos.dbos import _DBOSRegistry
|
|
8
8
|
|
|
9
9
|
from dbos.context import DBOSAssumeRole, get_local_dbos_context
|
|
10
10
|
from dbos.registrations import (
|
dbos/system_database.py
CHANGED
|
@@ -4,18 +4,7 @@ import select
|
|
|
4
4
|
import threading
|
|
5
5
|
import time
|
|
6
6
|
from enum import Enum
|
|
7
|
-
from typing import
|
|
8
|
-
TYPE_CHECKING,
|
|
9
|
-
Any,
|
|
10
|
-
Dict,
|
|
11
|
-
List,
|
|
12
|
-
Literal,
|
|
13
|
-
Optional,
|
|
14
|
-
Sequence,
|
|
15
|
-
Set,
|
|
16
|
-
TypedDict,
|
|
17
|
-
cast,
|
|
18
|
-
)
|
|
7
|
+
from typing import Any, Dict, List, Literal, Optional, Sequence, Set, TypedDict, cast
|
|
19
8
|
|
|
20
9
|
import psycopg2
|
|
21
10
|
import sqlalchemy as sa
|
|
@@ -69,6 +58,9 @@ class WorkflowStatusInternal(TypedDict):
|
|
|
69
58
|
app_id: Optional[str]
|
|
70
59
|
request: Optional[str] # JSON (jsonpickle)
|
|
71
60
|
recovery_attempts: Optional[int]
|
|
61
|
+
authenticated_user: Optional[str]
|
|
62
|
+
assumed_role: Optional[str]
|
|
63
|
+
authenticated_roles: Optional[str] # JSON list of roles.
|
|
72
64
|
|
|
73
65
|
|
|
74
66
|
class RecordedResult(TypedDict):
|
|
@@ -136,7 +128,7 @@ class WorkflowInformation(TypedDict, total=False):
|
|
|
136
128
|
authenticated_user: str # The user who ran the workflow. Empty string if not set.
|
|
137
129
|
assumed_role: str
|
|
138
130
|
# The role used to run this workflow. Empty string if authorization is not required.
|
|
139
|
-
|
|
131
|
+
authenticated_roles: List[str]
|
|
140
132
|
# All roles the authenticated user has, if any.
|
|
141
133
|
input: Optional[WorkflowInputs]
|
|
142
134
|
output: Optional[str]
|
|
@@ -230,7 +222,7 @@ class SystemDatabase:
|
|
|
230
222
|
def wait_for_buffer_flush(self) -> None:
|
|
231
223
|
# Wait until the buffers are flushed.
|
|
232
224
|
while self._is_flushing_status_buffer or not self._is_buffers_empty:
|
|
233
|
-
dbos_logger.
|
|
225
|
+
dbos_logger.debug("Waiting for system buffers to be exported")
|
|
234
226
|
time.sleep(1)
|
|
235
227
|
|
|
236
228
|
def update_workflow_status(
|
|
@@ -252,6 +244,9 @@ class SystemDatabase:
|
|
|
252
244
|
application_version=status["app_version"],
|
|
253
245
|
application_id=status["app_id"],
|
|
254
246
|
request=status["request"],
|
|
247
|
+
authenticated_user=status["authenticated_user"],
|
|
248
|
+
authenticated_roles=status["authenticated_roles"],
|
|
249
|
+
assumed_role=status["assumed_role"],
|
|
255
250
|
)
|
|
256
251
|
if replace:
|
|
257
252
|
cmd = cmd.on_conflict_do_update(
|
|
@@ -322,6 +317,9 @@ class SystemDatabase:
|
|
|
322
317
|
SystemSchema.workflow_status.c.recovery_attempts,
|
|
323
318
|
SystemSchema.workflow_status.c.config_name,
|
|
324
319
|
SystemSchema.workflow_status.c.class_name,
|
|
320
|
+
SystemSchema.workflow_status.c.authenticated_user,
|
|
321
|
+
SystemSchema.workflow_status.c.authenticated_roles,
|
|
322
|
+
SystemSchema.workflow_status.c.assumed_role,
|
|
325
323
|
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid)
|
|
326
324
|
).fetchone()
|
|
327
325
|
if row is None:
|
|
@@ -339,6 +337,9 @@ class SystemDatabase:
|
|
|
339
337
|
"executor_id": None,
|
|
340
338
|
"request": row[2],
|
|
341
339
|
"recovery_attempts": row[3],
|
|
340
|
+
"authenticated_user": row[6],
|
|
341
|
+
"authenticated_roles": row[7],
|
|
342
|
+
"assumed_role": row[8],
|
|
342
343
|
}
|
|
343
344
|
return status
|
|
344
345
|
|
|
@@ -375,6 +376,9 @@ class SystemDatabase:
|
|
|
375
376
|
SystemSchema.workflow_status.c.error,
|
|
376
377
|
SystemSchema.workflow_status.c.config_name,
|
|
377
378
|
SystemSchema.workflow_status.c.class_name,
|
|
379
|
+
SystemSchema.workflow_status.c.authenticated_user,
|
|
380
|
+
SystemSchema.workflow_status.c.authenticated_roles,
|
|
381
|
+
SystemSchema.workflow_status.c.assumed_role,
|
|
378
382
|
).where(SystemSchema.workflow_status.c.workflow_uuid == workflow_uuid)
|
|
379
383
|
).fetchone()
|
|
380
384
|
if row is None:
|
|
@@ -392,6 +396,9 @@ class SystemDatabase:
|
|
|
392
396
|
"executor_id": None,
|
|
393
397
|
"request": row[2],
|
|
394
398
|
"recovery_attempts": None,
|
|
399
|
+
"authenticated_user": row[7],
|
|
400
|
+
"authenticated_roles": row[8],
|
|
401
|
+
"assumed_role": row[9],
|
|
395
402
|
}
|
|
396
403
|
return status
|
|
397
404
|
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
# This is a sample app built with DBOS and FastAPI.
|
|
4
4
|
# It displays greetings to visitors and keeps track of how
|
|
5
|
-
# many times
|
|
5
|
+
# many times visitors have been greeted.
|
|
6
6
|
|
|
7
7
|
# First, let's do imports, create a FastAPI app, and initialize DBOS.
|
|
8
8
|
|
|
9
9
|
from fastapi import FastAPI
|
|
10
10
|
from fastapi.responses import HTMLResponse
|
|
11
|
-
from sqlalchemy.dialects.postgresql import insert
|
|
12
11
|
|
|
13
12
|
from dbos import DBOS
|
|
14
13
|
|
|
@@ -19,33 +18,26 @@ DBOS(fastapi=app)
|
|
|
19
18
|
|
|
20
19
|
# Next, let's write a function that greets visitors.
|
|
21
20
|
# To make it more interesting, we'll keep track of how
|
|
22
|
-
# many times
|
|
21
|
+
# many times visitors have been greeted and store
|
|
23
22
|
# the count in the database.
|
|
24
23
|
|
|
25
|
-
# We
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
24
|
+
# We implement the database operations using SQLAlchemy
|
|
25
|
+
# and serve the function from a FastAPI endpoint.
|
|
26
|
+
# We annotate it with @DBOS.transaction() to access
|
|
27
|
+
# an automatically-configured database client.
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
@app.get("/greeting/{name}")
|
|
32
31
|
@DBOS.transaction()
|
|
33
32
|
def example_transaction(name: str) -> str:
|
|
34
|
-
query = (
|
|
35
|
-
insert(dbos_hello)
|
|
36
|
-
.values(name="dbos", greet_count=1)
|
|
37
|
-
.on_conflict_do_update(
|
|
38
|
-
index_elements=["name"], set_={"greet_count": dbos_hello.c.greet_count + 1}
|
|
39
|
-
)
|
|
40
|
-
.returning(dbos_hello.c.greet_count)
|
|
41
|
-
)
|
|
33
|
+
query = dbos_hello.insert().values(name=name).returning(dbos_hello.c.greet_count)
|
|
42
34
|
greet_count = DBOS.sql_session.execute(query).scalar_one()
|
|
43
35
|
greeting = f"Greetings, {name}! You have been greeted {greet_count} times."
|
|
44
36
|
DBOS.logger.info(greeting)
|
|
45
37
|
return greeting
|
|
46
38
|
|
|
47
39
|
|
|
48
|
-
# Finally, let's use FastAPI to serve
|
|
40
|
+
# Finally, let's use FastAPI to serve an HTML + CSS readme
|
|
49
41
|
# from the root path.
|
|
50
42
|
|
|
51
43
|
|
|
@@ -74,13 +66,14 @@ def readme() -> HTMLResponse:
|
|
|
74
66
|
return HTMLResponse(readme)
|
|
75
67
|
|
|
76
68
|
|
|
77
|
-
# To run this app locally:
|
|
78
|
-
# - Make sure you have a Postgres database to connect
|
|
79
|
-
# - "dbos migrate" to set up your database tables
|
|
80
|
-
# - "dbos start" to start the app
|
|
81
|
-
# - Visit localhost:8000 to see your app!
|
|
82
|
-
|
|
83
69
|
# To deploy this app to DBOS Cloud:
|
|
84
70
|
# - "npm i -g @dbos-inc/dbos-cloud@latest" to install the Cloud CLI (requires Node)
|
|
85
71
|
# - "dbos-cloud app deploy" to deploy your app
|
|
86
72
|
# - Deploy outputs a URL--visit it to see your app!
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# To run this app locally:
|
|
76
|
+
# - Make sure you have a Postgres database to connect to
|
|
77
|
+
# - "dbos migrate" to set up your database tables
|
|
78
|
+
# - "dbos start" to start the app
|
|
79
|
+
# - Visit localhost:8000 to see your app!
|
|
@@ -5,6 +5,6 @@ metadata = MetaData()
|
|
|
5
5
|
dbos_hello = Table(
|
|
6
6
|
"dbos_hello",
|
|
7
7
|
metadata,
|
|
8
|
-
Column("
|
|
9
|
-
Column("
|
|
8
|
+
Column("greet_count", Integer, primary_key=True, autoincrement=True),
|
|
9
|
+
Column("name", String, nullable=False),
|
|
10
10
|
)
|
|
@@ -22,9 +22,9 @@ def upgrade() -> None:
|
|
|
22
22
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
23
23
|
op.create_table(
|
|
24
24
|
"dbos_hello",
|
|
25
|
+
sa.Column("greet_count", sa.Integer(), autoincrement=True, nullable=False),
|
|
25
26
|
sa.Column("name", sa.String(), nullable=False),
|
|
26
|
-
sa.
|
|
27
|
-
sa.PrimaryKeyConstraint("name"),
|
|
27
|
+
sa.PrimaryKeyConstraint("greet_count"),
|
|
28
28
|
)
|
|
29
29
|
# ### end Alembic commands ###
|
|
30
30
|
|
dbos/tracer.py
CHANGED
|
@@ -19,6 +19,7 @@ class DBOSTracer:
|
|
|
19
19
|
self.app_id = os.environ.get("DBOS__APPID", None)
|
|
20
20
|
self.app_version = os.environ.get("DBOS__APPVERSION", None)
|
|
21
21
|
self.executor_id = os.environ.get("DBOS__VMID", "local")
|
|
22
|
+
self.provider: Optional[TracerProvider] = None
|
|
22
23
|
|
|
23
24
|
def config(self, config: ConfigFile) -> None:
|
|
24
25
|
if not isinstance(trace.get_tracer_provider(), TracerProvider):
|
|
@@ -36,10 +37,17 @@ class DBOSTracer:
|
|
|
36
37
|
provider.add_span_processor(processor)
|
|
37
38
|
trace.set_tracer_provider(provider)
|
|
38
39
|
|
|
40
|
+
def set_provider(self, provider: Optional[TracerProvider]) -> None:
|
|
41
|
+
self.provider = provider
|
|
42
|
+
|
|
39
43
|
def start_span(
|
|
40
44
|
self, attributes: "TracedAttributes", parent: Optional[Span] = None
|
|
41
45
|
) -> Span:
|
|
42
|
-
tracer =
|
|
46
|
+
tracer = (
|
|
47
|
+
self.provider.get_tracer("dbos-tracer")
|
|
48
|
+
if self.provider is not None
|
|
49
|
+
else trace.get_tracer("dbos-tracer")
|
|
50
|
+
)
|
|
43
51
|
context = trace.set_span_in_context(parent) if parent else None
|
|
44
52
|
span: Span = tracer.start_span(name=attributes["name"], context=context)
|
|
45
53
|
attributes["applicationID"] = self.app_id
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dbos
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Ultra-lightweight durable execution in Python
|
|
5
|
+
Author-Email: "DBOS, Inc." <contact@dbos.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
9
|
+
Requires-Dist: jsonschema>=4.23.0
|
|
10
|
+
Requires-Dist: alembic>=1.13.2
|
|
11
|
+
Requires-Dist: psycopg2-binary>=2.9.9
|
|
12
|
+
Requires-Dist: typing-extensions>=4.12.2; python_version < "3.10"
|
|
13
|
+
Requires-Dist: typer>=0.12.3
|
|
14
|
+
Requires-Dist: jsonpickle>=3.2.2
|
|
15
|
+
Requires-Dist: opentelemetry-api>=1.26.0
|
|
16
|
+
Requires-Dist: opentelemetry-sdk>=1.26.0
|
|
17
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.26.0
|
|
18
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
19
|
+
Requires-Dist: fastapi[standard]>=0.112.1
|
|
20
|
+
Requires-Dist: psutil>=6.0.0
|
|
21
|
+
Requires-Dist: tomlkit>=0.13.2
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
<div align="center">
|
|
26
|
+
|
|
27
|
+
# DBOS Transact: Ultra-Lightweight Durable Execution
|
|
28
|
+
|
|
29
|
+
#### [Documentation](https://docs.dbos.dev/) • [Examples](https://docs.dbos.dev/examples) • [Github](https://github.com/dbos-inc) • [Discord](https://discord.com/invite/jsmC6pXGgX)
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
DBOS Transact is a Python library providing **ultra-lightweight durable execution**.
|
|
35
|
+
For example:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
@DBOS.step()
|
|
39
|
+
def step_one():
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
@DBOS.step()
|
|
43
|
+
def step_two():
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
@DBOS.workflow()
|
|
47
|
+
def workflow()
|
|
48
|
+
step_one()
|
|
49
|
+
step_two()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Durable execution means your program is **resilient to any failure**.
|
|
53
|
+
If it is ever interrupted or crashes, all your workflows will automatically resume from the last completed step.
|
|
54
|
+
If you want to see durable execution in action, check out [this demo app](https://demo-widget-store.cloud.dbos.dev/) (source code [here](https://github.com/dbos-inc/dbos-demo-apps/tree/main/python/widget-store)).
|
|
55
|
+
No matter how many times you try to crash it, it always resumes from exactly where it left off!
|
|
56
|
+
|
|
57
|
+
Under the hood, DBOS Transact works by storing your program's execution state (which workflows are currently executing and which steps they've completed) in a Postgres database.
|
|
58
|
+
So all you need to use it is a Postgres database to connect to—there's no need for a "workflow server."
|
|
59
|
+
This approach is also incredibly fast, for example [25x faster than AWS Step Functions](https://www.dbos.dev/blog/dbos-vs-aws-step-functions-benchmark).
|
|
60
|
+
|
|
61
|
+
Some more cool features include:
|
|
62
|
+
|
|
63
|
+
- Scheduled jobs—run your workflows exactly-once per time interval.
|
|
64
|
+
- Exactly-once event processing—use workflows to process incoming events (for example, from a Kafka topic) exactly-once.
|
|
65
|
+
- Observability—all workflows automatically emit [OpenTelemetry](https://opentelemetry.io/) traces.
|
|
66
|
+
|
|
67
|
+
## Getting Started
|
|
68
|
+
|
|
69
|
+
Install and configure with:
|
|
70
|
+
|
|
71
|
+
```shell
|
|
72
|
+
pip install dbos
|
|
73
|
+
dbos init --config
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then, try it out with this simple program (requires Postgres):
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fastapi import FastAPI
|
|
80
|
+
from dbos import DBOS
|
|
81
|
+
|
|
82
|
+
app = FastAPI()
|
|
83
|
+
DBOS(fastapi=app)
|
|
84
|
+
|
|
85
|
+
@DBOS.step()
|
|
86
|
+
def step_one():
|
|
87
|
+
print("Step one completed!")
|
|
88
|
+
|
|
89
|
+
@DBOS.step()
|
|
90
|
+
def step_two():
|
|
91
|
+
print("Step two completed!")
|
|
92
|
+
|
|
93
|
+
@DBOS.workflow()
|
|
94
|
+
def workflow():
|
|
95
|
+
step_one()
|
|
96
|
+
for _ in range(5):
|
|
97
|
+
print("Press Control + \ to stop the app...")
|
|
98
|
+
DBOS.sleep(1)
|
|
99
|
+
step_two()
|
|
100
|
+
|
|
101
|
+
@app.get("/")
|
|
102
|
+
def endpoint():
|
|
103
|
+
workflow()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Save the program into `main.py`, edit `dbos-config.yaml` to configure your Postgres connection settings, and start it with `fastapi run`.
|
|
107
|
+
Visit `localhost:8000` in your browser to start the workflow.
|
|
108
|
+
When prompted, press `Control + \` to force quit your application.
|
|
109
|
+
It should crash midway through the workflow, having completed step one but not step two.
|
|
110
|
+
Then, restart your app with `fastapi run`.
|
|
111
|
+
It should resume the workflow from where it left off, completing step two without re-executing step one.
|
|
112
|
+
|
|
113
|
+
To learn how to build more complex workflows, see our [programming guide](https://docs.dbos.dev/python/programming-guide) or [examples](https://docs.dbos.dev/examples).
|
|
114
|
+
|
|
115
|
+
## Documentation
|
|
116
|
+
|
|
117
|
+
[https://docs.dbos.dev](https://docs.dbos.dev)
|
|
118
|
+
|
|
119
|
+
## Examples
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
- [**AI-Powered Slackbot**](https://docs.dbos.dev/python/examples/rag-slackbot) — A Slackbot that answers questions about previous Slack conversations, using DBOS to durably orchestrate its RAG pipeline.
|
|
123
|
+
- [**Widget Store**](https://docs.dbos.dev/python/examples/widget-store) — An online storefront that uses DBOS durable workflows to be resilient to any failure.
|
|
124
|
+
- [**Earthquake Tracker**](https://docs.dbos.dev/python/examples/earthquake-tracker) — A real-time earthquake dashboard that uses DBOS to stream data from the USGS into Postgres, then visualizes it with Streamlit.
|
|
125
|
+
|
|
126
|
+
More examples [here](https://docs.dbos.dev/examples)!
|
|
127
|
+
|
|
128
|
+
## Community
|
|
129
|
+
|
|
130
|
+
If you're interested in building with us, please star our repository and join our community on [Discord](https://discord.gg/fMwQjeW5zg)!
|
|
131
|
+
If you see a bug or have a feature request, don't hesitate to open an issue here on GitHub.
|
|
132
|
+
If you're interested in contributing, check out our [contributions guide](./CONTRIBUTING.md).
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
dbos-0.
|
|
2
|
-
dbos-0.
|
|
3
|
-
dbos-0.
|
|
4
|
-
dbos-0.
|
|
5
|
-
dbos/__init__.py,sha256=
|
|
6
|
-
dbos/admin_sever.py,sha256=
|
|
1
|
+
dbos-0.6.0.dist-info/METADATA,sha256=KmnHNM81muzumN3rCi5ehIHATQepqFl0DyWlKrlV5xY,4998
|
|
2
|
+
dbos-0.6.0.dist-info/WHEEL,sha256=rSwsxJWe3vzyR5HCwjWXQruDgschpei4h_giTm0dJVE,90
|
|
3
|
+
dbos-0.6.0.dist-info/entry_points.txt,sha256=3PmOPbM4FYxEmggRRdJw0oAsiBzKR8U0yx7bmwUmMOM,39
|
|
4
|
+
dbos-0.6.0.dist-info/licenses/LICENSE,sha256=VGZit_a5-kdw9WT6fY5jxAWVwGQzgLFyPWrcVVUhVNU,1067
|
|
5
|
+
dbos/__init__.py,sha256=heuB3bqRXlVdfea9sKHBIVKSqFP6UuwhQecQfV4hyas,642
|
|
6
|
+
dbos/admin_sever.py,sha256=Qg5T3YRrbPW05PR_99yAaxgo1ugQrAp_uTeTqSfjm_k,3397
|
|
7
7
|
dbos/application_database.py,sha256=1K3kE96BgGi_QWOd2heXluyNTwFAwlUVuAR6JKKUqf0,5659
|
|
8
|
-
dbos/cli.py,sha256=
|
|
9
|
-
dbos/context.py,sha256=
|
|
10
|
-
dbos/core.py,sha256=
|
|
8
|
+
dbos/cli.py,sha256=YARlQiWHUwFni-fEOr0k5P_-pqPS4xkywj_B0oTMXn0,8318
|
|
9
|
+
dbos/context.py,sha256=NVMGyvAa2RIiBVspvDz-8MBk_BQyGyYdPdorgO-GSng,16407
|
|
10
|
+
dbos/core.py,sha256=jeqO8DABPAUrFlJXOfRfFDSnA8BGwiPnMa1JNbGuYLs,28584
|
|
11
11
|
dbos/dbos-config.schema.json,sha256=azpfmoDZg7WfSy3kvIsk9iEiKB_-VZt03VEOoXJAkqE,5331
|
|
12
|
-
dbos/dbos.py,sha256=
|
|
13
|
-
dbos/dbos_config.py,sha256=
|
|
12
|
+
dbos/dbos.py,sha256=wzL51K7bY3J8grHXi0k0F0N09vFVBYWKwI0DGxyN4MY,29730
|
|
13
|
+
dbos/dbos_config.py,sha256=ih_TD_1zTKhPKxk8TPdEIp3ihu82R06SGKg-s4rHxws,5344
|
|
14
14
|
dbos/decorators.py,sha256=lbPefsLK6Cya4cb7TrOcLglOpGT3pc6qjZdsQKlfZLg,629
|
|
15
|
-
dbos/error.py,sha256=
|
|
16
|
-
dbos/fastapi.py,sha256=
|
|
15
|
+
dbos/error.py,sha256=DDhB0VHmoZE_CP51ICdFMZSL2gmVS3Dm0aPNWncci94,3876
|
|
16
|
+
dbos/fastapi.py,sha256=s7LnwwYVpJm_QZZwBW5um8NV2Q2Qx85uVZqGcKlSZAo,1881
|
|
17
|
+
dbos/flask.py,sha256=azr4geMEGuuTBCyxIZmgDmmP-6s_pTIF-lGyp9Q4IB8,2430
|
|
18
|
+
dbos/kafka.py,sha256=FtngQHBu2TKfyDF7GFsKJAawFQJiOFxgKEUlNNxrdrw,3055
|
|
19
|
+
dbos/kafka_message.py,sha256=NYvOXNG3Qn7bghn1pv3fg4Pbs86ILZGcK4IB-MLUNu0,409
|
|
17
20
|
dbos/logger.py,sha256=D-aFSZUCHBP34J1IZ5YNkTrJW-rDiH3py_v9jLU4Yrk,3565
|
|
18
21
|
dbos/migrations/env.py,sha256=38SIGVbmn_VV2x2u1aHLcPOoWgZ84eCymf3g_NljmbU,1626
|
|
19
22
|
dbos/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
@@ -22,25 +25,25 @@ dbos/migrations/versions/a3b18ad34abe_added_triggers.py,sha256=Rv0ZsZYZ_WdgGEULY
|
|
|
22
25
|
dbos/py.typed,sha256=QfzXT1Ktfk3Rj84akygc7_42z0lRpCq0Ilh8OXI6Zas,44
|
|
23
26
|
dbos/recovery.py,sha256=xfwQFWNuD8DXg5HD5_-3tG7Neo9j-x1lrqiwtn5FSh8,2015
|
|
24
27
|
dbos/registrations.py,sha256=gMI-u05tv5bpvyddQGtoUgCsqARx51aOY7p0JXPafQo,6539
|
|
25
|
-
dbos/request.py,sha256
|
|
26
|
-
dbos/roles.py,sha256=
|
|
28
|
+
dbos/request.py,sha256=-FIwtknayvRl6OjvqO4V2GySVzSdP1Ft3cc9ZBS-PLY,928
|
|
29
|
+
dbos/roles.py,sha256=7Lh7uwUq1dpa6TXCOHre4mPTd5qmXzK_QPkvYR52DXg,2285
|
|
27
30
|
dbos/scheduler/croniter.py,sha256=hbhgfsHBqclUS8VeLnJ9PSE9Z54z6mi4nnrr1aUXn0k,47561
|
|
28
31
|
dbos/scheduler/scheduler.py,sha256=uO4_9jmWW2rLv1ODL3lc1cE_37ZaVTgnvmFx_FAlN50,1472
|
|
29
32
|
dbos/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
33
|
dbos/schemas/application_database.py,sha256=q_Wr2XbiZNBYFkOtu7uKavo1T_cSOBblxKGHThYGGsY,962
|
|
31
34
|
dbos/schemas/system_database.py,sha256=5V3vqnEzry0Hn7ZbVS9Gs_dJKia8uX8p7mGC82Ru8rk,4303
|
|
32
|
-
dbos/system_database.py,sha256=
|
|
35
|
+
dbos/system_database.py,sha256=BZ8yE79FH9NZhEt8AAYNXy0-wMcKLBAq8VPdZbfidR4,40564
|
|
33
36
|
dbos/templates/hello/README.md,sha256=GhxhBj42wjTt1fWEtwNriHbJuKb66Vzu89G4pxNHw2g,930
|
|
34
37
|
dbos/templates/hello/__package/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
-
dbos/templates/hello/__package/main.py,sha256=
|
|
36
|
-
dbos/templates/hello/__package/schema.py,sha256=
|
|
38
|
+
dbos/templates/hello/__package/main.py,sha256=eI0SS9Nwj-fldtiuSzIlIG6dC91GXXwdRsoHxv6S_WI,2719
|
|
39
|
+
dbos/templates/hello/__package/schema.py,sha256=7Z27JGC8yy7Z44cbVXIREYxtUhU4JVkLCp5Q7UahVQ0,260
|
|
37
40
|
dbos/templates/hello/alembic.ini,sha256=VKBn4Gy8mMuCdY7Hip1jmo3wEUJ1VG1aW7EqY0_n-as,3695
|
|
38
41
|
dbos/templates/hello/dbos-config.yaml.dbos,sha256=8wxCf_MIEFNWqMXj0nAHUwg1U3YaKz4xcUN6g51WkDE,603
|
|
39
42
|
dbos/templates/hello/migrations/env.py.dbos,sha256=CsiFOea3ZIsahqkfYtioha0ewmlGR78Ng0wOB2t5LQg,2208
|
|
40
43
|
dbos/templates/hello/migrations/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
|
|
41
|
-
dbos/templates/hello/migrations/versions/2024_07_31_180642_init.py,sha256=
|
|
44
|
+
dbos/templates/hello/migrations/versions/2024_07_31_180642_init.py,sha256=U5thFWGqNN4QLrNXT7wUUqftIFDNE5eSdqD8JNW1mec,942
|
|
42
45
|
dbos/templates/hello/start_postgres_docker.py,sha256=lQVLlYO5YkhGPEgPqwGc7Y8uDKse9HsWv5fynJEFJHM,1681
|
|
43
|
-
dbos/tracer.py,sha256=
|
|
46
|
+
dbos/tracer.py,sha256=GaXDhdKKF_IQp5SAMipGXiDVwteRKjNbrXyYCH1mor0,2520
|
|
44
47
|
dbos/utils.py,sha256=hWj9iWDrby2cVEhb0pG-IdnrxLqP64NhkaWUXiLc8bA,402
|
|
45
48
|
version/__init__.py,sha256=L4sNxecRuqdtSFdpUGX3TtBi9KL3k7YsZVIvv-fv9-A,1678
|
|
46
|
-
dbos-0.
|
|
49
|
+
dbos-0.6.0.dist-info/RECORD,,
|
dbos-0.5.0a11.dist-info/METADATA
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: dbos
|
|
3
|
-
Version: 0.5.0a11
|
|
4
|
-
Summary: A Python framework for backends that scale
|
|
5
|
-
Author-Email: "DBOS, Inc." <contact@dbos.dev>
|
|
6
|
-
License: MIT
|
|
7
|
-
Requires-Python: >=3.9
|
|
8
|
-
Requires-Dist: pyyaml>=6.0.2
|
|
9
|
-
Requires-Dist: jsonschema>=4.23.0
|
|
10
|
-
Requires-Dist: alembic>=1.13.2
|
|
11
|
-
Requires-Dist: psycopg2-binary>=2.9.9
|
|
12
|
-
Requires-Dist: typing-extensions>=4.12.2; python_version < "3.10"
|
|
13
|
-
Requires-Dist: typer>=0.12.3
|
|
14
|
-
Requires-Dist: jsonpickle>=3.2.2
|
|
15
|
-
Requires-Dist: opentelemetry-api>=1.26.0
|
|
16
|
-
Requires-Dist: opentelemetry-sdk>=1.26.0
|
|
17
|
-
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.26.0
|
|
18
|
-
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
19
|
-
Requires-Dist: fastapi[standard]>=0.112.1
|
|
20
|
-
Requires-Dist: psutil>=6.0.0
|
|
21
|
-
Requires-Dist: tomlkit>=0.13.2
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
|
|
24
|
-
# DBOS Transact Python
|
|
25
|
-
|
|
26
|
-
**DBOS Python is under construction! 🚧🚧🚧 Check back regularly for updates, release coming in mid-September!**
|
|
27
|
-
|
|
28
|
-
DBOS Transact is a **Python library** for building durable and scalable applications.
|
|
29
|
-
|
|
30
|
-
You want to use DBOS Transact in your application because you need:
|
|
31
|
-
|
|
32
|
-
- **Resilience to any failure**. If your app is interrupted for any reason, it automatically resumes from where it left off. Reliable message delivery is built in. Idempotency is built in.
|
|
33
|
-
- **Reliable event processing**. Need to consume Kafka events exactly-once? Just add one line of code to your app. Need to run a task exactly once per hour, day, or month? Just one more line of code.
|
|
34
|
-
- **Built-in observability**. Automatically emit [OpenTelemetry](https://opentelemetry.io/)-compatible logs and traces from any application. Query your app's history from the command line or with SQL.
|
|
35
|
-
- **Blazing-fast, developer-friendly serverless**. Develop your project locally and run it anywhere. When you're ready, [deploy it for free to DBOS Cloud](https://docs.dbos.dev/getting-started/quickstart#deploying-to-dbos-cloud) and we'll host it for you, [25x faster](https://www.dbos.dev/blog/dbos-vs-aws-step-functions-benchmark) and [15x cheaper](https://www.dbos.dev/blog/dbos-vs-lambda-cost) than AWS Lambda.
|
|
36
|
-
|
|
37
|
-
## Getting Started
|
|
38
|
-
|
|
39
|
-
To try out the latest pre-release version, install with:
|
|
40
|
-
|
|
41
|
-
```shell
|
|
42
|
-
pip install --pre dbos
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Documentation
|
|
46
|
-
|
|
47
|
-
Coming soon! 🚧
|
|
48
|
-
|
|
49
|
-
But we have some cool demo apps for you to check out: [https://github.com/dbos-inc/dbos-demo-apps/tree/main/python](https://github.com/dbos-inc/dbos-demo-apps/tree/main/python)
|
|
50
|
-
|
|
51
|
-
## Main Features
|
|
52
|
-
|
|
53
|
-
Here are some of the core features of DBOS Transact:
|
|
54
|
-
|
|
55
|
-
| Feature | Description
|
|
56
|
-
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
-
| [Transactions](https://www.dbos.dev/dbos-transact-python) | Easily and safely query your application database using [SQLAlchemy](https://www.sqlalchemy.org/) or raw SQL.
|
|
58
|
-
| [Workflows](https://www.dbos.dev/dbos-transact-python) | Reliable workflow orchestration—resume your program after any failure.
|
|
59
|
-
| [Idempotency](https://www.dbos.dev/dbos-transact-python) | Automatically make any request idempotent, so your requests happen exactly once.
|
|
60
|
-
| [Authentication and Authorization](https://www.dbos.dev/dbos-transact-python) | Secure your HTTP endpoints so only authorized users can access them.
|
|
61
|
-
| [Kafka Integration](https://www.dbos.dev/dbos-transact-python) | Consume Kafka messages exactly-once with transactions or workflows.
|
|
62
|
-
| [Scheduled Workflows](https://www.dbos.dev/dbos-transact-python) | Schedule your workflows to run exactly-once per time interval with cron-like syntax.
|
|
63
|
-
| [Self-Hosting](https://www.dbos.dev/dbos-transact-python) | Host your applications anywhere, as long as they have a Postgres database to connect to.
|
|
64
|
-
|
|
65
|
-
And DBOS Cloud:
|
|
66
|
-
|
|
67
|
-
| Feature | Description
|
|
68
|
-
| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
-
| [Serverless App Deployment](https://docs.dbos.dev/cloud-tutorials/application-management) | Deploy apps to DBOS Cloud in minutes.
|
|
70
|
-
| [Interactive Time Travel](https://docs.dbos.dev/cloud-tutorials/interactive-timetravel) | Query your application database as of any past point in time.
|
|
71
|
-
| [Cloud Database Management](https://docs.dbos.dev/cloud-tutorials/database-management) | Provision cloud Postgres instances for your applications. Alternatively, [bring your own database](https://docs.dbos.dev/cloud-tutorials/byod-management).
|
|
72
|
-
| [Built-in Observability](https://docs.dbos.dev/cloud-tutorials/monitoring-dashboard) | Built-in log capture, request tracing, and dashboards.
|
|
73
|
-
|
|
74
|
-
## Community
|
|
75
|
-
|
|
76
|
-
If you're interested in building with us, please star our repository and join our community on [Discord](https://discord.gg/fMwQjeW5zg)!
|
|
77
|
-
If you see a bug or have a feature request, don't hesitate to open an issue here on GitHub.
|
|
78
|
-
If you're interested in contributing, check out our [contributions guide](./CONTRIBUTING.md).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|