dbos 1.6.0a3__tar.gz → 1.6.0a4__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.
- {dbos-1.6.0a3 → dbos-1.6.0a4}/PKG-INFO +1 -1
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_core.py +41 -27
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_dbos.py +17 -4
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_kafka.py +2 -1
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_registrations.py +5 -3
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_roles.py +3 -2
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_scheduler.py +11 -8
- {dbos-1.6.0a3 → dbos-1.6.0a4}/pyproject.toml +1 -1
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_classdecorators.py +1 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_dbos.py +56 -4
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_failures.py +1 -1
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_fastapi_roles.py +3 -3
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_scheduler.py +15 -1
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_spans.py +21 -15
- {dbos-1.6.0a3 → dbos-1.6.0a4}/LICENSE +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/README.md +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/__init__.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/__main__.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_admin_server.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_app_db.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_classproperty.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_client.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_conductor/conductor.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_conductor/protocol.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_context.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_croniter.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_dbos_config.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_debug.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_docker_pg_helper.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_error.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_event_loop.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_fastapi.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_flask.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_kafka_message.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_logger.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/env.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/script.py.mako +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/27ac6900c6ad_add_queue_dedup.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/50f3227f0b4b_fix_job_queue.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/5c361fc04708_added_system_tables.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/66478e1b95e5_consolidate_queues.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/83f3732ae8e7_workflow_timeout.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/933e86bdac6a_add_queue_priority.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/a3b18ad34abe_added_triggers.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/d76646551a6b_job_queue_limiter.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/d76646551a6c_workflow_queue.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/d994145b47b6_consolidate_inputs.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/eab0cc1d9a14_job_queue.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/f4b9b32ba814_functionname_childid_op_outputs.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_outcome.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_queue.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_recovery.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_schemas/__init__.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_schemas/application_database.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_schemas/system_database.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_serialization.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_sys_db.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/README.md +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/__package/__init__.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/__package/main.py.dbos +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/__package/schema.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/alembic.ini +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/dbos-config.yaml.dbos +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/migrations/env.py.dbos +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/migrations/script.py.mako +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/migrations/versions/2024_07_31_180642_init.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_templates/dbos-db-starter/start_postgres_docker.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_tracer.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_utils.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_workflow_commands.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/cli/_github_init.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/cli/_template_init.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/cli/cli.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/dbos-config.schema.json +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/py.typed +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/__init__.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/atexit_no_ctor.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/atexit_no_launch.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/classdefs.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/client_collateral.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/client_worker.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/conftest.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/dupname_classdefs1.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/dupname_classdefsa.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/more_classdefs.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/queuedworkflow.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_admin_server.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_async.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_cli.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_client.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_concurrency.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_config.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_croniter.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_debug.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_docker_secrets.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_fastapi.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_flask.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_kafka.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_outcome.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_package.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_queue.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_schema_migration.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_singleton.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_sqlalchemy.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_workflow_introspection.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/tests/test_workflow_management.py +0 -0
- {dbos-1.6.0a3 → dbos-1.6.0a4}/version/__init__.py +0 -0
@@ -392,7 +392,7 @@ def _execute_workflow_wthread(
|
|
392
392
|
**kwargs: Any,
|
393
393
|
) -> R:
|
394
394
|
attributes: TracedAttributes = {
|
395
|
-
"name": func
|
395
|
+
"name": get_dbos_func_name(func),
|
396
396
|
"operationType": OperationType.WORKFLOW.value,
|
397
397
|
}
|
398
398
|
with DBOSContextSwap(ctx):
|
@@ -425,7 +425,7 @@ async def _execute_workflow_async(
|
|
425
425
|
**kwargs: Any,
|
426
426
|
) -> R:
|
427
427
|
attributes: TracedAttributes = {
|
428
|
-
"name": func
|
428
|
+
"name": get_dbos_func_name(func),
|
429
429
|
"operationType": OperationType.WORKFLOW.value,
|
430
430
|
}
|
431
431
|
with DBOSContextSwap(ctx):
|
@@ -532,7 +532,8 @@ def start_workflow(
|
|
532
532
|
fi = get_func_info(func)
|
533
533
|
if fi is None:
|
534
534
|
raise DBOSWorkflowFunctionNotFoundError(
|
535
|
-
"<NONE>",
|
535
|
+
"<NONE>",
|
536
|
+
f"start_workflow: function {func.__name__} is not registered",
|
536
537
|
)
|
537
538
|
|
538
539
|
func = cast("Workflow[P, R]", func.__orig_func) # type: ignore
|
@@ -627,7 +628,8 @@ async def start_workflow_async(
|
|
627
628
|
fi = get_func_info(func)
|
628
629
|
if fi is None:
|
629
630
|
raise DBOSWorkflowFunctionNotFoundError(
|
630
|
-
"<NONE>",
|
631
|
+
"<NONE>",
|
632
|
+
f"start_workflow: function {func.__name__} is not registered",
|
631
633
|
)
|
632
634
|
|
633
635
|
func = cast("Workflow[P, R]", func.__orig_func) # type: ignore
|
@@ -731,13 +733,13 @@ def workflow_wrapper(
|
|
731
733
|
assert fi is not None
|
732
734
|
if dbosreg.dbos is None:
|
733
735
|
raise DBOSException(
|
734
|
-
f"Function {func
|
736
|
+
f"Function {get_dbos_func_name(func)} invoked before DBOS initialized"
|
735
737
|
)
|
736
738
|
dbos = dbosreg.dbos
|
737
739
|
|
738
740
|
rr: Optional[str] = check_required_roles(func, fi)
|
739
741
|
attributes: TracedAttributes = {
|
740
|
-
"name": func
|
742
|
+
"name": get_dbos_func_name(func),
|
741
743
|
"operationType": OperationType.WORKFLOW.value,
|
742
744
|
}
|
743
745
|
inputs: WorkflowInputs = {
|
@@ -837,27 +839,30 @@ def workflow_wrapper(
|
|
837
839
|
|
838
840
|
|
839
841
|
def decorate_workflow(
|
840
|
-
reg: "DBOSRegistry", max_recovery_attempts: Optional[int]
|
842
|
+
reg: "DBOSRegistry", name: Optional[str], max_recovery_attempts: Optional[int]
|
841
843
|
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
842
844
|
def _workflow_decorator(func: Callable[P, R]) -> Callable[P, R]:
|
843
845
|
wrapped_func = workflow_wrapper(reg, func, max_recovery_attempts)
|
844
|
-
|
846
|
+
func_name = name if name is not None else func.__qualname__
|
847
|
+
set_dbos_func_name(func, func_name)
|
848
|
+
set_dbos_func_name(wrapped_func, func_name)
|
849
|
+
reg.register_wf_function(func_name, wrapped_func, "workflow")
|
845
850
|
return wrapped_func
|
846
851
|
|
847
852
|
return _workflow_decorator
|
848
853
|
|
849
854
|
|
850
855
|
def decorate_transaction(
|
851
|
-
dbosreg: "DBOSRegistry", isolation_level: "IsolationLevel"
|
856
|
+
dbosreg: "DBOSRegistry", name: Optional[str], isolation_level: "IsolationLevel"
|
852
857
|
) -> Callable[[F], F]:
|
853
858
|
def decorator(func: F) -> F:
|
854
859
|
|
855
|
-
transaction_name = func.__qualname__
|
860
|
+
transaction_name = name if name is not None else func.__qualname__
|
856
861
|
|
857
862
|
def invoke_tx(*args: Any, **kwargs: Any) -> Any:
|
858
863
|
if dbosreg.dbos is None:
|
859
864
|
raise DBOSException(
|
860
|
-
f"Function {
|
865
|
+
f"Function {transaction_name} invoked before DBOS initialized"
|
861
866
|
)
|
862
867
|
|
863
868
|
dbos = dbosreg.dbos
|
@@ -865,12 +870,12 @@ def decorate_transaction(
|
|
865
870
|
status = dbos._sys_db.get_workflow_status(ctx.workflow_id)
|
866
871
|
if status and status["status"] == WorkflowStatusString.CANCELLED.value:
|
867
872
|
raise DBOSWorkflowCancelledError(
|
868
|
-
f"Workflow {ctx.workflow_id} is cancelled. Aborting transaction {
|
873
|
+
f"Workflow {ctx.workflow_id} is cancelled. Aborting transaction {transaction_name}."
|
869
874
|
)
|
870
875
|
|
871
876
|
with dbos._app_db.sessionmaker() as session:
|
872
877
|
attributes: TracedAttributes = {
|
873
|
-
"name":
|
878
|
+
"name": transaction_name,
|
874
879
|
"operationType": OperationType.TRANSACTION.value,
|
875
880
|
}
|
876
881
|
with EnterDBOSTransaction(session, attributes=attributes):
|
@@ -971,7 +976,7 @@ def decorate_transaction(
|
|
971
976
|
raise
|
972
977
|
except InvalidRequestError as invalid_request_error:
|
973
978
|
dbos.logger.error(
|
974
|
-
f"InvalidRequestError in transaction {
|
979
|
+
f"InvalidRequestError in transaction {transaction_name} \033[1m Hint: Do not call commit() or rollback() within a DBOS transaction.\033[0m"
|
975
980
|
)
|
976
981
|
txn_error = invalid_request_error
|
977
982
|
raise
|
@@ -991,7 +996,7 @@ def decorate_transaction(
|
|
991
996
|
|
992
997
|
if inspect.iscoroutinefunction(func):
|
993
998
|
raise DBOSException(
|
994
|
-
f"Function {
|
999
|
+
f"Function {transaction_name} is a coroutine function, but DBOS.transaction does not support coroutine functions"
|
995
1000
|
)
|
996
1001
|
|
997
1002
|
fi = get_or_create_func_info(func)
|
@@ -1010,15 +1015,19 @@ def decorate_transaction(
|
|
1010
1015
|
with DBOSAssumeRole(rr):
|
1011
1016
|
return invoke_tx(*args, **kwargs)
|
1012
1017
|
else:
|
1013
|
-
tempwf = dbosreg.workflow_info_map.get("<temp>." +
|
1018
|
+
tempwf = dbosreg.workflow_info_map.get("<temp>." + transaction_name)
|
1014
1019
|
assert tempwf
|
1015
1020
|
return tempwf(*args, **kwargs)
|
1016
1021
|
|
1022
|
+
set_dbos_func_name(func, transaction_name)
|
1023
|
+
set_dbos_func_name(wrapper, transaction_name)
|
1024
|
+
|
1017
1025
|
def temp_wf(*args: Any, **kwargs: Any) -> Any:
|
1018
1026
|
return wrapper(*args, **kwargs)
|
1019
1027
|
|
1020
1028
|
wrapped_wf = workflow_wrapper(dbosreg, temp_wf)
|
1021
|
-
set_dbos_func_name(temp_wf, "<temp>." +
|
1029
|
+
set_dbos_func_name(temp_wf, "<temp>." + transaction_name)
|
1030
|
+
set_dbos_func_name(wrapped_wf, "<temp>." + transaction_name)
|
1022
1031
|
set_temp_workflow_type(temp_wf, "transaction")
|
1023
1032
|
dbosreg.register_wf_function(
|
1024
1033
|
get_dbos_func_name(temp_wf), wrapped_wf, "transaction"
|
@@ -1035,24 +1044,25 @@ def decorate_transaction(
|
|
1035
1044
|
def decorate_step(
|
1036
1045
|
dbosreg: "DBOSRegistry",
|
1037
1046
|
*,
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1047
|
+
name: Optional[str],
|
1048
|
+
retries_allowed: bool,
|
1049
|
+
interval_seconds: float,
|
1050
|
+
max_attempts: int,
|
1051
|
+
backoff_rate: float,
|
1042
1052
|
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
1043
1053
|
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
1044
1054
|
|
1045
|
-
step_name = func.__qualname__
|
1055
|
+
step_name = name if name is not None else func.__qualname__
|
1046
1056
|
|
1047
1057
|
def invoke_step(*args: Any, **kwargs: Any) -> Any:
|
1048
1058
|
if dbosreg.dbos is None:
|
1049
1059
|
raise DBOSException(
|
1050
|
-
f"Function {
|
1060
|
+
f"Function {step_name} invoked before DBOS initialized"
|
1051
1061
|
)
|
1052
1062
|
dbos = dbosreg.dbos
|
1053
1063
|
|
1054
1064
|
attributes: TracedAttributes = {
|
1055
|
-
"name":
|
1065
|
+
"name": step_name,
|
1056
1066
|
"operationType": OperationType.STEP.value,
|
1057
1067
|
}
|
1058
1068
|
|
@@ -1131,7 +1141,7 @@ def decorate_step(
|
|
1131
1141
|
stepOutcome = stepOutcome.retry(
|
1132
1142
|
max_attempts,
|
1133
1143
|
on_exception,
|
1134
|
-
lambda i, e: DBOSMaxStepRetriesExceeded(
|
1144
|
+
lambda i, e: DBOSMaxStepRetriesExceeded(step_name, i, e),
|
1135
1145
|
)
|
1136
1146
|
|
1137
1147
|
outcome = (
|
@@ -1160,7 +1170,7 @@ def decorate_step(
|
|
1160
1170
|
with DBOSAssumeRole(rr):
|
1161
1171
|
return invoke_step(*args, **kwargs)
|
1162
1172
|
else:
|
1163
|
-
tempwf = dbosreg.workflow_info_map.get("<temp>." +
|
1173
|
+
tempwf = dbosreg.workflow_info_map.get("<temp>." + step_name)
|
1164
1174
|
assert tempwf
|
1165
1175
|
return tempwf(*args, **kwargs)
|
1166
1176
|
|
@@ -1168,6 +1178,9 @@ def decorate_step(
|
|
1168
1178
|
_mark_coroutine(wrapper) if inspect.iscoroutinefunction(func) else wrapper # type: ignore
|
1169
1179
|
)
|
1170
1180
|
|
1181
|
+
set_dbos_func_name(func, step_name)
|
1182
|
+
set_dbos_func_name(wrapper, step_name)
|
1183
|
+
|
1171
1184
|
def temp_wf_sync(*args: Any, **kwargs: Any) -> Any:
|
1172
1185
|
return wrapper(*args, **kwargs)
|
1173
1186
|
|
@@ -1181,7 +1194,8 @@ def decorate_step(
|
|
1181
1194
|
|
1182
1195
|
temp_wf = temp_wf_async if inspect.iscoroutinefunction(func) else temp_wf_sync
|
1183
1196
|
wrapped_wf = workflow_wrapper(dbosreg, temp_wf)
|
1184
|
-
set_dbos_func_name(temp_wf, "<temp>." +
|
1197
|
+
set_dbos_func_name(temp_wf, "<temp>." + step_name)
|
1198
|
+
set_dbos_func_name(wrapped_wf, "<temp>." + step_name)
|
1185
1199
|
set_temp_workflow_type(temp_wf, "step")
|
1186
1200
|
dbosreg.register_wf_function(get_dbos_func_name(temp_wf), wrapped_wf, "step")
|
1187
1201
|
wrapper.__orig_func = temp_wf # type: ignore
|
@@ -360,6 +360,7 @@ class DBOS:
|
|
360
360
|
|
361
361
|
temp_send_wf = workflow_wrapper(self._registry, send_temp_workflow)
|
362
362
|
set_dbos_func_name(send_temp_workflow, TEMP_SEND_WF_NAME)
|
363
|
+
set_dbos_func_name(temp_send_wf, TEMP_SEND_WF_NAME)
|
363
364
|
set_temp_workflow_type(send_temp_workflow, "send")
|
364
365
|
self._registry.register_wf_function(TEMP_SEND_WF_NAME, temp_send_wf, "send")
|
365
366
|
|
@@ -589,14 +590,22 @@ class DBOS:
|
|
589
590
|
# Decorators for DBOS functionality
|
590
591
|
@classmethod
|
591
592
|
def workflow(
|
592
|
-
cls,
|
593
|
+
cls,
|
594
|
+
*,
|
595
|
+
name: Optional[str] = None,
|
596
|
+
max_recovery_attempts: Optional[int] = DEFAULT_MAX_RECOVERY_ATTEMPTS,
|
593
597
|
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
594
598
|
"""Decorate a function for use as a DBOS workflow."""
|
595
|
-
return decorate_workflow(
|
599
|
+
return decorate_workflow(
|
600
|
+
_get_or_create_dbos_registry(), name, max_recovery_attempts
|
601
|
+
)
|
596
602
|
|
597
603
|
@classmethod
|
598
604
|
def transaction(
|
599
|
-
cls,
|
605
|
+
cls,
|
606
|
+
isolation_level: IsolationLevel = "SERIALIZABLE",
|
607
|
+
*,
|
608
|
+
name: Optional[str] = None,
|
600
609
|
) -> Callable[[F], F]:
|
601
610
|
"""
|
602
611
|
Decorate a function for use as a DBOS transaction.
|
@@ -605,12 +614,15 @@ class DBOS:
|
|
605
614
|
isolation_level(IsolationLevel): Transaction isolation level
|
606
615
|
|
607
616
|
"""
|
608
|
-
return decorate_transaction(
|
617
|
+
return decorate_transaction(
|
618
|
+
_get_or_create_dbos_registry(), name, isolation_level
|
619
|
+
)
|
609
620
|
|
610
621
|
@classmethod
|
611
622
|
def step(
|
612
623
|
cls,
|
613
624
|
*,
|
625
|
+
name: Optional[str] = None,
|
614
626
|
retries_allowed: bool = False,
|
615
627
|
interval_seconds: float = 1.0,
|
616
628
|
max_attempts: int = 3,
|
@@ -629,6 +641,7 @@ class DBOS:
|
|
629
641
|
|
630
642
|
return decorate_step(
|
631
643
|
_get_or_create_dbos_registry(),
|
644
|
+
name=name,
|
632
645
|
retries_allowed=retries_allowed,
|
633
646
|
interval_seconds=interval_seconds,
|
634
647
|
max_attempts=max_attempts,
|
@@ -13,6 +13,7 @@ from ._context import SetWorkflowID
|
|
13
13
|
from ._error import DBOSInitializationError
|
14
14
|
from ._kafka_message import KafkaMessage
|
15
15
|
from ._logger import dbos_logger
|
16
|
+
from ._registrations import get_dbos_func_name
|
16
17
|
|
17
18
|
_KafkaConsumerWorkflow = Callable[[KafkaMessage], None]
|
18
19
|
|
@@ -44,7 +45,7 @@ def _kafka_consumer_loop(
|
|
44
45
|
config["auto.offset.reset"] = "earliest"
|
45
46
|
|
46
47
|
if config.get("group.id") is None:
|
47
|
-
config["group.id"] = safe_group_name(func
|
48
|
+
config["group.id"] = safe_group_name(get_dbos_func_name(func), topics)
|
48
49
|
dbos_logger.warning(
|
49
50
|
f"Consumer group ID not found. Using generated group.id {config['group.id']}"
|
50
51
|
)
|
@@ -4,15 +4,17 @@ from enum import Enum
|
|
4
4
|
from types import FunctionType
|
5
5
|
from typing import Any, Callable, List, Literal, Optional, Tuple, Type, cast
|
6
6
|
|
7
|
+
from dbos._error import DBOSWorkflowFunctionNotFoundError
|
8
|
+
|
7
9
|
DEFAULT_MAX_RECOVERY_ATTEMPTS = 100
|
8
10
|
|
9
11
|
|
10
12
|
def get_dbos_func_name(f: Any) -> str:
|
11
13
|
if hasattr(f, "dbos_function_name"):
|
12
14
|
return str(getattr(f, "dbos_function_name"))
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
raise DBOSWorkflowFunctionNotFoundError(
|
16
|
+
"<NONE>", f"function {f.__name__} is not registered"
|
17
|
+
)
|
16
18
|
|
17
19
|
|
18
20
|
def set_dbos_func_name(f: Any, name: str) -> None:
|
@@ -10,6 +10,7 @@ from ._context import DBOSAssumeRole, get_local_dbos_context
|
|
10
10
|
from ._registrations import (
|
11
11
|
DBOSFuncInfo,
|
12
12
|
get_class_info_for_func,
|
13
|
+
get_dbos_func_name,
|
13
14
|
get_or_create_class_info,
|
14
15
|
get_or_create_func_info,
|
15
16
|
)
|
@@ -36,7 +37,7 @@ def check_required_roles(
|
|
36
37
|
ctx = get_local_dbos_context()
|
37
38
|
if ctx is None or ctx.authenticated_roles is None:
|
38
39
|
raise DBOSNotAuthorizedError(
|
39
|
-
f"Function {func
|
40
|
+
f"Function {get_dbos_func_name(func)} requires a role, but was called in a context without authentication information"
|
40
41
|
)
|
41
42
|
|
42
43
|
for r in required_roles:
|
@@ -44,7 +45,7 @@ def check_required_roles(
|
|
44
45
|
return r
|
45
46
|
|
46
47
|
raise DBOSNotAuthorizedError(
|
47
|
-
f"Function {func
|
48
|
+
f"Function {get_dbos_func_name(func)} has required roles, but user is not authenticated for any of them"
|
48
49
|
)
|
49
50
|
|
50
51
|
|
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
|
11
11
|
|
12
12
|
from ._context import SetWorkflowID
|
13
13
|
from ._croniter import croniter # type: ignore
|
14
|
+
from ._registrations import get_dbos_func_name
|
14
15
|
|
15
16
|
ScheduledWorkflow = Callable[[datetime, datetime], None]
|
16
17
|
|
@@ -24,20 +25,22 @@ def scheduler_loop(
|
|
24
25
|
iter = croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
|
25
26
|
except Exception as e:
|
26
27
|
dbos_logger.error(
|
27
|
-
f'Cannot run scheduled function {func
|
28
|
+
f'Cannot run scheduled function {get_dbos_func_name(func)}. Invalid crontab "{cron}"'
|
28
29
|
)
|
29
30
|
while not stop_event.is_set():
|
30
31
|
nextExecTime = iter.get_next(datetime)
|
31
32
|
sleepTime = nextExecTime - datetime.now(timezone.utc)
|
32
33
|
if stop_event.wait(timeout=sleepTime.total_seconds()):
|
33
34
|
return
|
34
|
-
|
35
|
-
|
35
|
+
try:
|
36
|
+
with SetWorkflowID(
|
37
|
+
f"sched-{get_dbos_func_name(func)}-{nextExecTime.isoformat()}"
|
38
|
+
):
|
36
39
|
scheduler_queue.enqueue(func, nextExecTime, datetime.now(timezone.utc))
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
40
|
+
except Exception:
|
41
|
+
dbos_logger.warning(
|
42
|
+
f"Exception encountered in scheduler thread: {traceback.format_exc()})"
|
43
|
+
)
|
41
44
|
|
42
45
|
|
43
46
|
def scheduled(
|
@@ -48,7 +51,7 @@ def scheduled(
|
|
48
51
|
croniter(cron, datetime.now(timezone.utc), second_at_beginning=True)
|
49
52
|
except Exception as e:
|
50
53
|
raise ValueError(
|
51
|
-
f'Invalid crontab "{cron}" for scheduled function function {func
|
54
|
+
f'Invalid crontab "{cron}" for scheduled function function {get_dbos_func_name(func)}.'
|
52
55
|
)
|
53
56
|
|
54
57
|
global scheduler_queue
|
@@ -15,6 +15,7 @@ from tests.conftest import queue_entries_are_cleaned_up
|
|
15
15
|
|
16
16
|
def test_required_roles(dbos: DBOS) -> None:
|
17
17
|
@DBOS.required_roles(["user"])
|
18
|
+
@DBOS.workflow(name="tfunc")
|
18
19
|
def tfunc(var: str) -> str:
|
19
20
|
assert assert_current_dbos_context().assumed_role == "user"
|
20
21
|
return var
|
@@ -15,6 +15,7 @@ import sqlalchemy as sa
|
|
15
15
|
from dbos import (
|
16
16
|
DBOS,
|
17
17
|
DBOSConfig,
|
18
|
+
Queue,
|
18
19
|
SetWorkflowID,
|
19
20
|
SetWorkflowTimeout,
|
20
21
|
WorkflowHandle,
|
@@ -1190,10 +1191,13 @@ def test_debug_logging(
|
|
1190
1191
|
result1 = test_workflow()
|
1191
1192
|
|
1192
1193
|
assert result1 == "Step: Hello, Transaction: World"
|
1193
|
-
assert
|
1194
|
+
assert (
|
1195
|
+
"Running step" in caplog.text
|
1196
|
+
and f"name: {step_function.__qualname__}" in caplog.text
|
1197
|
+
)
|
1194
1198
|
assert (
|
1195
1199
|
"Running transaction" in caplog.text
|
1196
|
-
and "name: transaction_function" in caplog.text
|
1200
|
+
and f"name: {transaction_function.__qualname__}" in caplog.text
|
1197
1201
|
)
|
1198
1202
|
assert "Running sleep" in caplog.text
|
1199
1203
|
assert "Running set_event" in caplog.text
|
@@ -1233,10 +1237,13 @@ def test_debug_logging(
|
|
1233
1237
|
result3 = test_workflow()
|
1234
1238
|
|
1235
1239
|
assert result3 == result1
|
1236
|
-
assert
|
1240
|
+
assert (
|
1241
|
+
"Replaying step" in caplog.text
|
1242
|
+
and f"name: {step_function.__qualname__}" in caplog.text
|
1243
|
+
)
|
1237
1244
|
assert (
|
1238
1245
|
"Replaying transaction" in caplog.text
|
1239
|
-
and "name: transaction_function" in caplog.text
|
1246
|
+
and f"name: {transaction_function.__qualname__}" in caplog.text in caplog.text
|
1240
1247
|
)
|
1241
1248
|
assert "Replaying sleep" in caplog.text
|
1242
1249
|
assert "Replaying set_event" in caplog.text
|
@@ -1556,3 +1563,48 @@ def test_workflow_timeout(dbos: DBOS) -> None:
|
|
1556
1563
|
assert assert_current_dbos_context().workflow_timeout_ms is None
|
1557
1564
|
assert assert_current_dbos_context().workflow_timeout_ms == 1000
|
1558
1565
|
assert get_local_dbos_context() is None
|
1566
|
+
|
1567
|
+
|
1568
|
+
def test_custom_names(dbos: DBOS) -> None:
|
1569
|
+
workflow_name = "workflow_name"
|
1570
|
+
step_name = "step_name"
|
1571
|
+
txn_name = "txn_name"
|
1572
|
+
queue = Queue("test-queue")
|
1573
|
+
|
1574
|
+
@DBOS.workflow(name=workflow_name)
|
1575
|
+
def workflow() -> str:
|
1576
|
+
return DBOS.workflow_id
|
1577
|
+
|
1578
|
+
handle = queue.enqueue(workflow)
|
1579
|
+
assert handle.get_status().name == workflow_name
|
1580
|
+
assert handle.get_result() == handle.workflow_id
|
1581
|
+
|
1582
|
+
@DBOS.step(name=step_name)
|
1583
|
+
def step() -> str:
|
1584
|
+
return DBOS.workflow_id
|
1585
|
+
|
1586
|
+
handle = queue.enqueue(step)
|
1587
|
+
assert handle.get_status().name == f"<temp>.{step_name}"
|
1588
|
+
assert handle.get_result() == handle.workflow_id
|
1589
|
+
|
1590
|
+
@DBOS.transaction(name=txn_name)
|
1591
|
+
def txn() -> str:
|
1592
|
+
return DBOS.workflow_id
|
1593
|
+
|
1594
|
+
handle = queue.enqueue(txn)
|
1595
|
+
assert handle.get_status().name == f"<temp>.{txn_name}"
|
1596
|
+
assert handle.get_result() == handle.workflow_id
|
1597
|
+
|
1598
|
+
# Verify we can declare another workflow with the same function name
|
1599
|
+
# but a different custom name
|
1600
|
+
|
1601
|
+
another_workflow = "another_workflow"
|
1602
|
+
|
1603
|
+
@DBOS.workflow(name=another_workflow)
|
1604
|
+
def workflow(x: int) -> int:
|
1605
|
+
return x
|
1606
|
+
|
1607
|
+
value = 5
|
1608
|
+
handle = DBOS.start_workflow(workflow, value) # type: ignore
|
1609
|
+
assert handle.get_status().name == another_workflow
|
1610
|
+
assert handle.get_result() == value # type: ignore
|
@@ -325,7 +325,7 @@ def test_step_retries(dbos: DBOS) -> None:
|
|
325
325
|
def enqueue_failing_step() -> None:
|
326
326
|
queue.enqueue(failing_step).get_result()
|
327
327
|
|
328
|
-
error_message = f"Step {failing_step.
|
328
|
+
error_message = f"Step {failing_step.__qualname__} has exceeded its maximum of {max_attempts} retries"
|
329
329
|
|
330
330
|
# Test calling the step directly
|
331
331
|
with pytest.raises(DBOSMaxStepRetriesExceeded) as excinfo:
|
@@ -123,7 +123,7 @@ def test_simple_endpoint(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
|
123
123
|
assert span.attributes is not None
|
124
124
|
|
125
125
|
span = spans[0]
|
126
|
-
assert span.name ==
|
126
|
+
assert span.name == test_user_endpoint.__qualname__
|
127
127
|
assert span.parent is not None
|
128
128
|
assert span.parent.span_id == spans[1].context.span_id
|
129
129
|
assert span.attributes is not None
|
@@ -171,7 +171,7 @@ def test_simple_endpoint(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
|
171
171
|
assert response.status_code == 403
|
172
172
|
assert (
|
173
173
|
response.text
|
174
|
-
== '{"message":"Function test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
|
174
|
+
== '{"message":"Function test_simple_endpoint.<locals>.test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
|
175
175
|
)
|
176
176
|
|
177
177
|
|
@@ -293,7 +293,7 @@ def test_jwt_endpoint(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
|
293
293
|
assert response.status_code == 403
|
294
294
|
assert (
|
295
295
|
response.text
|
296
|
-
== '{"message":"Function test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
|
296
|
+
== '{"message":"Function test_jwt_endpoint.<locals>.test_admin_endpoint has required roles, but user is not authenticated for any of them","dbos_error_code":"8","dbos_error":"DBOSNotAuthorizedError"}'
|
297
297
|
)
|
298
298
|
|
299
299
|
|
@@ -7,6 +7,7 @@ from sqlalchemy.engine import Engine
|
|
7
7
|
|
8
8
|
# Public API
|
9
9
|
from dbos import DBOS
|
10
|
+
from dbos._error import DBOSWorkflowFunctionNotFoundError
|
10
11
|
|
11
12
|
|
12
13
|
def simulate_db_restart(engine: Engine, downtime: float) -> None:
|
@@ -155,6 +156,19 @@ def test_scheduled_transaction(dbos: DBOS) -> None:
|
|
155
156
|
assert txn_counter > 2 and txn_counter <= 4
|
156
157
|
|
157
158
|
|
159
|
+
def test_scheduled_step(dbos: DBOS) -> None:
|
160
|
+
step_counter: int = 0
|
161
|
+
|
162
|
+
@DBOS.scheduled("* * * * * *")
|
163
|
+
@DBOS.step()
|
164
|
+
def test_step(scheduled: datetime, actual: datetime) -> None:
|
165
|
+
nonlocal step_counter
|
166
|
+
step_counter += 1
|
167
|
+
|
168
|
+
time.sleep(4)
|
169
|
+
assert step_counter > 2 and step_counter <= 4
|
170
|
+
|
171
|
+
|
158
172
|
def test_scheduled_workflow_exception(dbos: DBOS) -> None:
|
159
173
|
wf_counter: int = 0
|
160
174
|
|
@@ -240,5 +254,5 @@ def my_function():
|
|
240
254
|
pass
|
241
255
|
"""
|
242
256
|
# Use exec to run the code and catch the expected exception
|
243
|
-
with pytest.raises(
|
257
|
+
with pytest.raises(DBOSWorkflowFunctionNotFoundError) as excinfo:
|
244
258
|
exec(code)
|
@@ -41,7 +41,7 @@ def test_spans(config: DBOSConfig) -> None:
|
|
41
41
|
|
42
42
|
spans = exporter.get_finished_spans()
|
43
43
|
|
44
|
-
assert len(spans) ==
|
44
|
+
assert len(spans) == 5
|
45
45
|
|
46
46
|
for span in spans:
|
47
47
|
assert span.attributes is not None
|
@@ -50,15 +50,17 @@ def test_spans(config: DBOSConfig) -> None:
|
|
50
50
|
assert span.context is not None
|
51
51
|
assert span.attributes["foo"] == "bar"
|
52
52
|
|
53
|
-
assert spans[0].name == test_step.
|
53
|
+
assert spans[0].name == test_step.__qualname__
|
54
54
|
assert spans[1].name == "a new span"
|
55
|
-
assert spans[2].name == test_workflow.
|
56
|
-
assert spans[3].name == test_step.
|
55
|
+
assert spans[2].name == test_workflow.__qualname__
|
56
|
+
assert spans[3].name == test_step.__qualname__
|
57
|
+
assert spans[4].name == f"<temp>.{test_step.__qualname__}"
|
57
58
|
|
58
59
|
assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
|
59
60
|
assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
|
60
61
|
assert spans[2].parent == None
|
61
|
-
assert spans[3].parent ==
|
62
|
+
assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
|
63
|
+
assert spans[4].parent == None
|
62
64
|
|
63
65
|
|
64
66
|
@pytest.mark.asyncio
|
@@ -87,7 +89,7 @@ async def test_spans_async(dbos: DBOS) -> None:
|
|
87
89
|
|
88
90
|
spans = exporter.get_finished_spans()
|
89
91
|
|
90
|
-
assert len(spans) ==
|
92
|
+
assert len(spans) == 5
|
91
93
|
|
92
94
|
for span in spans:
|
93
95
|
assert span.attributes is not None
|
@@ -95,15 +97,17 @@ async def test_spans_async(dbos: DBOS) -> None:
|
|
95
97
|
assert span.attributes["executorID"] == GlobalParams.executor_id
|
96
98
|
assert span.context is not None
|
97
99
|
|
98
|
-
assert spans[0].name == test_step.
|
100
|
+
assert spans[0].name == test_step.__qualname__
|
99
101
|
assert spans[1].name == "a new span"
|
100
|
-
assert spans[2].name == test_workflow.
|
101
|
-
assert spans[3].name == test_step.
|
102
|
+
assert spans[2].name == test_workflow.__qualname__
|
103
|
+
assert spans[3].name == test_step.__qualname__
|
104
|
+
assert spans[4].name == f"<temp>.{test_step.__qualname__}"
|
102
105
|
|
103
106
|
assert spans[0].parent.span_id == spans[2].context.span_id # type: ignore
|
104
107
|
assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
|
105
108
|
assert spans[2].parent == None
|
106
|
-
assert spans[3].parent ==
|
109
|
+
assert spans[3].parent.span_id == spans[4].context.span_id # type: ignore
|
110
|
+
assert spans[4].parent == None
|
107
111
|
|
108
112
|
|
109
113
|
def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
@@ -127,15 +131,17 @@ def test_temp_wf_fastapi(dbos_fastapi: Tuple[DBOS, FastAPI]) -> None:
|
|
127
131
|
|
128
132
|
spans = exporter.get_finished_spans()
|
129
133
|
|
130
|
-
assert len(spans) ==
|
134
|
+
assert len(spans) == 3
|
131
135
|
|
132
136
|
for span in spans:
|
133
137
|
assert span.attributes is not None
|
134
138
|
assert span.attributes["applicationVersion"] == GlobalParams.app_version
|
135
139
|
assert span.context is not None
|
136
140
|
|
137
|
-
assert spans[0].name == test_step_endpoint.
|
138
|
-
assert spans[1].name == "
|
141
|
+
assert spans[0].name == test_step_endpoint.__qualname__
|
142
|
+
assert spans[1].name == f"<temp>.{test_step_endpoint.__qualname__}"
|
143
|
+
assert spans[2].name == "/step"
|
139
144
|
|
140
|
-
assert spans[0].parent.span_id == spans[1].context.span_id # type:ignore
|
141
|
-
assert spans[1].parent ==
|
145
|
+
assert spans[0].parent.span_id == spans[1].context.span_id # type: ignore
|
146
|
+
assert spans[1].parent.span_id == spans[2].context.span_id # type: ignore
|
147
|
+
assert spans[2].parent == None
|
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
|
{dbos-1.6.0a3 → dbos-1.6.0a4}/dbos/_migrations/versions/04ca4f231047_workflow_queues_executor_id.py
RENAMED
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
|
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
|