dex-python-sdk 1.26.0__tar.gz → 1.28.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.
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/PKG-INFO +103 -1
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/README.md +102 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/__init__.py +4 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_async_worker_dispatcher.py +14 -3
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_invocation_context.py +10 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_pull_worker.py +22 -4
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_value_mapper.py +11 -3
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_worker_dispatcher.py +12 -6
- dex_python_sdk-1.28.0/dex/codec.py +849 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/context.py +45 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/flow.py +99 -19
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/flow_options.py +14 -5
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/step.py +121 -21
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/pyproject.toml +1 -1
- dex_python_sdk-1.26.0/dex/codec.py +0 -427
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/LICENSE +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_async_value_hydrator.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_credential_transport.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_native.pyi +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_server_protocol.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_utils.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/_value_hydrator.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/async_client.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/async_worker.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/attribute.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/attribute_match.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/blob_cache.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/channel.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/client.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/client_options.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/condition.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/credential.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/dexpb/dex_pb2.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/dexpb/dex_pb2.pyi +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/flow_config.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/flow_info.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/flow_result.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/pull_registration.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/py.typed +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/rpc_invoke_options.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/step_execution.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/stream.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/subflow.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/timer.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/wait.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/wait_options.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/worker.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/dex/worker_options.py +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/README.md +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache-python/README.md +0 -0
- {dex_python_sdk-1.26.0 → dex_python_sdk-1.28.0}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dex-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.28.0
|
|
4
4
|
Requires-Dist: grpcio>=1.83.0
|
|
5
5
|
Requires-Dist: grpcio-status>=1.83.0
|
|
6
6
|
Requires-Dist: protobuf>=7.35.1
|
|
@@ -394,6 +394,7 @@ registry = dex.Registry((flow,))
|
|
|
394
394
|
Registry derives codecs from declared Python types and handler annotations.
|
|
395
395
|
Built-in primitive types and dataclasses need no codec arguments. Register an
|
|
396
396
|
explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
397
|
+
|
|
397
398
|
`PersistenceSchema.of(...)` accepts attributes, channels, and streams together and
|
|
398
399
|
partitions them by definition type.
|
|
399
400
|
|
|
@@ -465,6 +466,56 @@ pip install dex-python-sdk==0.1.0
|
|
|
465
466
|
|
|
466
467
|
See [samples](../examples/python) for use case examples.
|
|
467
468
|
|
|
469
|
+
### JSON dataclasses
|
|
470
|
+
|
|
471
|
+
Automatic JSON codecs support parameterized generic dataclasses such as
|
|
472
|
+
`Result[Receipt]`, including nested generics, typed containers, `Optional[T]`, and
|
|
473
|
+
`T | None`. Decoding restores nested dataclass instances. Missing keys use the
|
|
474
|
+
field's `default` or `default_factory`; a missing required key raises `ValueError`
|
|
475
|
+
with the dataclass and wire field name. Unknown keys are ignored.
|
|
476
|
+
|
|
477
|
+
Field metadata controls the wire representation:
|
|
478
|
+
|
|
479
|
+
- `field(metadata={"dex_json_name": "callId"})` uses `callId` instead of the
|
|
480
|
+
Python field name for encoding and decoding. Wire names must be non-empty and
|
|
481
|
+
unique within a dataclass.
|
|
482
|
+
- `field(default=None, metadata={"dex_json_omit_default": True})` omits a field
|
|
483
|
+
only when it equals its declared default. This also supports `default_factory`;
|
|
484
|
+
the factory is called on each encode to obtain the comparison value, so use a
|
|
485
|
+
deterministic factory without side effects. Without this opt-in, default
|
|
486
|
+
values, empty containers, and `None` remain present in JSON.
|
|
487
|
+
- `field(metadata={"dex_json_format": "rfc3339"})` formats a datetime field like
|
|
488
|
+
Go's `encoding/json`: UTC uses `Z` and trailing fractional zeroes are removed.
|
|
489
|
+
This requires a datetime or optional datetime field with an aware value and a
|
|
490
|
+
whole-minute timezone offset. Ordinary datetime fields keep their existing
|
|
491
|
+
`isoformat()` encoding. All datetime fields accept Go RFC3339 strings on
|
|
492
|
+
decode, including `Z`, numeric offsets, and fractional seconds. Python retains
|
|
493
|
+
microsecond precision; additional fractional digits are truncated.
|
|
494
|
+
|
|
495
|
+
Metadata options can be combined on one field. Existing dataclasses retain their
|
|
496
|
+
field names and encoded values unless metadata opts into a change.
|
|
497
|
+
|
|
498
|
+
Unions select by JSON kind: null, boolean, number, string, array, or object.
|
|
499
|
+
A union declared directly, such as a Step input of `Receipt | None`, needs
|
|
500
|
+
alternatives with different kinds. `Receipt | None`, `int | str`, and
|
|
501
|
+
`int | bool` are supported. `int | float`, `str | datetime`, two dataclasses,
|
|
502
|
+
or two list types are ambiguous and raise `TypeError` when the codec is
|
|
503
|
+
resolved. Use an explicit `JsonCodec` with a discriminator for these shapes.
|
|
504
|
+
Union values always use JSON on the wire, including primitive alternatives.
|
|
505
|
+
|
|
506
|
+
Unions inside dataclass fields and containers keep the decoding of earlier
|
|
507
|
+
releases and never raise when the codec is resolved. Such a union decodes a
|
|
508
|
+
value into a dataclass, Enum, or datetime alternative, or a container of those,
|
|
509
|
+
when the value's JSON kind matches only that alternative. It returns any other
|
|
510
|
+
value unchanged, without a type check. Fields typed by a type variable and
|
|
511
|
+
`Mapping` or `Sequence` types follow the same rule: `Result[Receipt]` decodes a
|
|
512
|
+
`Receipt`, but `Result[int]` returns the JSON value of its type-variable field
|
|
513
|
+
unchanged. A `float` accepts a JSON integer and keeps it as an `int`.
|
|
514
|
+
|
|
515
|
+
See `help(CodecRegistry)` for a complete generic dataclass example and
|
|
516
|
+
[`tests/fixtures/json_codec`](tests/fixtures/json_codec) for a reproducible
|
|
517
|
+
Go-to-Python wire fixture.
|
|
518
|
+
|
|
468
519
|
## Requirements
|
|
469
520
|
|
|
470
521
|
- Python 3.11+
|
|
@@ -484,6 +535,37 @@ Applications implement two generic interfaces from [`dex`](dex/):
|
|
|
484
535
|
With `AsyncWorker` and `Registry(..., allow_async_handlers=True)`, Step
|
|
485
536
|
coroutines use `AsyncContext`; async RPCs keep `Context`.
|
|
486
537
|
|
|
538
|
+
Steps are registered by `get_step_type()`, which defaults to the class name.
|
|
539
|
+
Multiple instances of one class may return different Step types in the same Flow.
|
|
540
|
+
A duplicate Step type fails registration. `go_to`, `StepMovement.of`,
|
|
541
|
+
`StepOptions.on_execute_failure_proceed_to`,
|
|
542
|
+
`FlowTimeoutHandlerOptions.on_failure_proceed_to`, and decision or RPC
|
|
543
|
+
cancellation selections accept a registered instance, `StepRef("StepType")`, or
|
|
544
|
+
a class. A class must have exactly one registered instance; otherwise select an
|
|
545
|
+
instance or use `StepRef`. Instances match by identity and must belong to the
|
|
546
|
+
containing Flow, so a new instance of a registered class is rejected.
|
|
547
|
+
|
|
548
|
+
Declared execute-failure targets are validated when the Registry is constructed.
|
|
549
|
+
The Registry also rejects an execute-failure target whose input type differs from
|
|
550
|
+
the failing Step's input type. It skips this check when either type is `Any`,
|
|
551
|
+
`object`, or an unresolved TypeVar. Timeout-handler failure targets are validated
|
|
552
|
+
when Flow start options are mapped. Handler-created movements and cancellation
|
|
553
|
+
selections, including StepRef targets, are validated when the Worker applies the
|
|
554
|
+
returned decision; unknown Step types and ambiguous classes raise
|
|
555
|
+
`FlowDefinitionError` with the Flow and target details. StepRef contains only a
|
|
556
|
+
Step type and cannot execute or be registered in a StepList. Its input is checked
|
|
557
|
+
by the destination codec at runtime.
|
|
558
|
+
|
|
559
|
+
For a generic Step whose input annotation is a TypeVar, override
|
|
560
|
+
`get_input_type()` to return a concrete type supported by `CodecRegistry`. The
|
|
561
|
+
override takes precedence over both `execute` and `wait_for` input annotations;
|
|
562
|
+
Context and return annotations are still required. The default returns `None`,
|
|
563
|
+
which preserves annotation inference; return `type(None)` for a Step that accepts
|
|
564
|
+
no input. The Registry resolves the type through its codec registrations, so
|
|
565
|
+
custom codecs work too. See the runnable
|
|
566
|
+
[generic Step instances test](tests/integ/step_instances_flow.py), which registers
|
|
567
|
+
integer and string instances and routes to both by instance and StepRef.
|
|
568
|
+
|
|
487
569
|
`StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
|
|
488
570
|
two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
489
571
|
|
|
@@ -501,6 +583,26 @@ meet the server minimum, which defaults to ten seconds. Asynchronous durability
|
|
|
501
583
|
first allows at most three local attempts in seven seconds. The local phase ignores
|
|
502
584
|
method timeouts and heartbeat frames before falling back to a regular activity.
|
|
503
585
|
|
|
586
|
+
### Flow run start time
|
|
587
|
+
|
|
588
|
+
`Context.flow_started_at` returns the current Flow run's server start time as a
|
|
589
|
+
timezone-aware UTC `datetime` with one-second precision. It stays the same across
|
|
590
|
+
Steps and retries in that run. It is available in `wait_for`, `execute`, Flow
|
|
591
|
+
timeout handlers, and RPC handlers for both Worker and AsyncWorker. Compare it
|
|
592
|
+
with `datetime.now(timezone.utc)` to measure the run's age. It matches
|
|
593
|
+
`Client.describe_flow(flow_id).started_at` to the second.
|
|
594
|
+
|
|
595
|
+
### Step attempt deadline
|
|
596
|
+
|
|
597
|
+
`Context.deadline` is the absolute deadline supplied by the Dex Server for the
|
|
598
|
+
current `wait_for`, `execute`, or Flow timeout handler attempt. It is a
|
|
599
|
+
timezone-aware UTC `datetime` with microsecond precision, not a duration.
|
|
600
|
+
Subtract `datetime.now(timezone.utc)` and call `total_seconds()` to budget
|
|
601
|
+
remaining work after checking for `None`. A shorter remaining
|
|
602
|
+
`RetryPolicy.total_duration` limits this deadline; each retry receives its own
|
|
603
|
+
effective deadline. RPC handlers and tasks without a supplied deadline return
|
|
604
|
+
`None`.
|
|
605
|
+
|
|
504
606
|
### Step progress and heartbeat recovery
|
|
505
607
|
|
|
506
608
|
A synchronous handler yields every heartbeat and Stream write. The generator return
|
|
@@ -378,6 +378,7 @@ registry = dex.Registry((flow,))
|
|
|
378
378
|
Registry derives codecs from declared Python types and handler annotations.
|
|
379
379
|
Built-in primitive types and dataclasses need no codec arguments. Register an
|
|
380
380
|
explicit codec only for a custom encoding or a type Registry cannot derive.
|
|
381
|
+
|
|
381
382
|
`PersistenceSchema.of(...)` accepts attributes, channels, and streams together and
|
|
382
383
|
partitions them by definition type.
|
|
383
384
|
|
|
@@ -449,6 +450,56 @@ pip install dex-python-sdk==0.1.0
|
|
|
449
450
|
|
|
450
451
|
See [samples](../examples/python) for use case examples.
|
|
451
452
|
|
|
453
|
+
### JSON dataclasses
|
|
454
|
+
|
|
455
|
+
Automatic JSON codecs support parameterized generic dataclasses such as
|
|
456
|
+
`Result[Receipt]`, including nested generics, typed containers, `Optional[T]`, and
|
|
457
|
+
`T | None`. Decoding restores nested dataclass instances. Missing keys use the
|
|
458
|
+
field's `default` or `default_factory`; a missing required key raises `ValueError`
|
|
459
|
+
with the dataclass and wire field name. Unknown keys are ignored.
|
|
460
|
+
|
|
461
|
+
Field metadata controls the wire representation:
|
|
462
|
+
|
|
463
|
+
- `field(metadata={"dex_json_name": "callId"})` uses `callId` instead of the
|
|
464
|
+
Python field name for encoding and decoding. Wire names must be non-empty and
|
|
465
|
+
unique within a dataclass.
|
|
466
|
+
- `field(default=None, metadata={"dex_json_omit_default": True})` omits a field
|
|
467
|
+
only when it equals its declared default. This also supports `default_factory`;
|
|
468
|
+
the factory is called on each encode to obtain the comparison value, so use a
|
|
469
|
+
deterministic factory without side effects. Without this opt-in, default
|
|
470
|
+
values, empty containers, and `None` remain present in JSON.
|
|
471
|
+
- `field(metadata={"dex_json_format": "rfc3339"})` formats a datetime field like
|
|
472
|
+
Go's `encoding/json`: UTC uses `Z` and trailing fractional zeroes are removed.
|
|
473
|
+
This requires a datetime or optional datetime field with an aware value and a
|
|
474
|
+
whole-minute timezone offset. Ordinary datetime fields keep their existing
|
|
475
|
+
`isoformat()` encoding. All datetime fields accept Go RFC3339 strings on
|
|
476
|
+
decode, including `Z`, numeric offsets, and fractional seconds. Python retains
|
|
477
|
+
microsecond precision; additional fractional digits are truncated.
|
|
478
|
+
|
|
479
|
+
Metadata options can be combined on one field. Existing dataclasses retain their
|
|
480
|
+
field names and encoded values unless metadata opts into a change.
|
|
481
|
+
|
|
482
|
+
Unions select by JSON kind: null, boolean, number, string, array, or object.
|
|
483
|
+
A union declared directly, such as a Step input of `Receipt | None`, needs
|
|
484
|
+
alternatives with different kinds. `Receipt | None`, `int | str`, and
|
|
485
|
+
`int | bool` are supported. `int | float`, `str | datetime`, two dataclasses,
|
|
486
|
+
or two list types are ambiguous and raise `TypeError` when the codec is
|
|
487
|
+
resolved. Use an explicit `JsonCodec` with a discriminator for these shapes.
|
|
488
|
+
Union values always use JSON on the wire, including primitive alternatives.
|
|
489
|
+
|
|
490
|
+
Unions inside dataclass fields and containers keep the decoding of earlier
|
|
491
|
+
releases and never raise when the codec is resolved. Such a union decodes a
|
|
492
|
+
value into a dataclass, Enum, or datetime alternative, or a container of those,
|
|
493
|
+
when the value's JSON kind matches only that alternative. It returns any other
|
|
494
|
+
value unchanged, without a type check. Fields typed by a type variable and
|
|
495
|
+
`Mapping` or `Sequence` types follow the same rule: `Result[Receipt]` decodes a
|
|
496
|
+
`Receipt`, but `Result[int]` returns the JSON value of its type-variable field
|
|
497
|
+
unchanged. A `float` accepts a JSON integer and keeps it as an `int`.
|
|
498
|
+
|
|
499
|
+
See `help(CodecRegistry)` for a complete generic dataclass example and
|
|
500
|
+
[`tests/fixtures/json_codec`](tests/fixtures/json_codec) for a reproducible
|
|
501
|
+
Go-to-Python wire fixture.
|
|
502
|
+
|
|
452
503
|
## Requirements
|
|
453
504
|
|
|
454
505
|
- Python 3.11+
|
|
@@ -468,6 +519,37 @@ Applications implement two generic interfaces from [`dex`](dex/):
|
|
|
468
519
|
With `AsyncWorker` and `Registry(..., allow_async_handlers=True)`, Step
|
|
469
520
|
coroutines use `AsyncContext`; async RPCs keep `Context`.
|
|
470
521
|
|
|
522
|
+
Steps are registered by `get_step_type()`, which defaults to the class name.
|
|
523
|
+
Multiple instances of one class may return different Step types in the same Flow.
|
|
524
|
+
A duplicate Step type fails registration. `go_to`, `StepMovement.of`,
|
|
525
|
+
`StepOptions.on_execute_failure_proceed_to`,
|
|
526
|
+
`FlowTimeoutHandlerOptions.on_failure_proceed_to`, and decision or RPC
|
|
527
|
+
cancellation selections accept a registered instance, `StepRef("StepType")`, or
|
|
528
|
+
a class. A class must have exactly one registered instance; otherwise select an
|
|
529
|
+
instance or use `StepRef`. Instances match by identity and must belong to the
|
|
530
|
+
containing Flow, so a new instance of a registered class is rejected.
|
|
531
|
+
|
|
532
|
+
Declared execute-failure targets are validated when the Registry is constructed.
|
|
533
|
+
The Registry also rejects an execute-failure target whose input type differs from
|
|
534
|
+
the failing Step's input type. It skips this check when either type is `Any`,
|
|
535
|
+
`object`, or an unresolved TypeVar. Timeout-handler failure targets are validated
|
|
536
|
+
when Flow start options are mapped. Handler-created movements and cancellation
|
|
537
|
+
selections, including StepRef targets, are validated when the Worker applies the
|
|
538
|
+
returned decision; unknown Step types and ambiguous classes raise
|
|
539
|
+
`FlowDefinitionError` with the Flow and target details. StepRef contains only a
|
|
540
|
+
Step type and cannot execute or be registered in a StepList. Its input is checked
|
|
541
|
+
by the destination codec at runtime.
|
|
542
|
+
|
|
543
|
+
For a generic Step whose input annotation is a TypeVar, override
|
|
544
|
+
`get_input_type()` to return a concrete type supported by `CodecRegistry`. The
|
|
545
|
+
override takes precedence over both `execute` and `wait_for` input annotations;
|
|
546
|
+
Context and return annotations are still required. The default returns `None`,
|
|
547
|
+
which preserves annotation inference; return `type(None)` for a Step that accepts
|
|
548
|
+
no input. The Registry resolves the type through its codec registrations, so
|
|
549
|
+
custom codecs work too. See the runnable
|
|
550
|
+
[generic Step instances test](tests/integ/step_instances_flow.py), which registers
|
|
551
|
+
integer and string instances and routes to both by instance and StepRef.
|
|
552
|
+
|
|
471
553
|
`StepOptions.wait_for_method_timeout` and `execute_method_timeout` bound the
|
|
472
554
|
two handler calls. Timer and channel conditions determine how long a Step waits.
|
|
473
555
|
|
|
@@ -485,6 +567,26 @@ meet the server minimum, which defaults to ten seconds. Asynchronous durability
|
|
|
485
567
|
first allows at most three local attempts in seven seconds. The local phase ignores
|
|
486
568
|
method timeouts and heartbeat frames before falling back to a regular activity.
|
|
487
569
|
|
|
570
|
+
### Flow run start time
|
|
571
|
+
|
|
572
|
+
`Context.flow_started_at` returns the current Flow run's server start time as a
|
|
573
|
+
timezone-aware UTC `datetime` with one-second precision. It stays the same across
|
|
574
|
+
Steps and retries in that run. It is available in `wait_for`, `execute`, Flow
|
|
575
|
+
timeout handlers, and RPC handlers for both Worker and AsyncWorker. Compare it
|
|
576
|
+
with `datetime.now(timezone.utc)` to measure the run's age. It matches
|
|
577
|
+
`Client.describe_flow(flow_id).started_at` to the second.
|
|
578
|
+
|
|
579
|
+
### Step attempt deadline
|
|
580
|
+
|
|
581
|
+
`Context.deadline` is the absolute deadline supplied by the Dex Server for the
|
|
582
|
+
current `wait_for`, `execute`, or Flow timeout handler attempt. It is a
|
|
583
|
+
timezone-aware UTC `datetime` with microsecond precision, not a duration.
|
|
584
|
+
Subtract `datetime.now(timezone.utc)` and call `total_seconds()` to budget
|
|
585
|
+
remaining work after checking for `None`. A shorter remaining
|
|
586
|
+
`RetryPolicy.total_duration` limits this deadline; each retry receives its own
|
|
587
|
+
effective deadline. RPC handlers and tasks without a supplied deadline return
|
|
588
|
+
`None`.
|
|
589
|
+
|
|
488
590
|
### Step progress and heartbeat recovery
|
|
489
591
|
|
|
490
592
|
A synchronous handler yields every heartbeat and Stream write. The generator return
|
|
@@ -101,6 +101,8 @@ from dex.step import (
|
|
|
101
101
|
StepMovement,
|
|
102
102
|
StepOptions,
|
|
103
103
|
StepOutput,
|
|
104
|
+
StepRef,
|
|
105
|
+
StepTarget,
|
|
104
106
|
WaitForFailurePolicy,
|
|
105
107
|
dead_end,
|
|
106
108
|
force_complete,
|
|
@@ -210,6 +212,8 @@ __all__ = [
|
|
|
210
212
|
"StepDurability",
|
|
211
213
|
"StepMovement",
|
|
212
214
|
"StepOutput",
|
|
215
|
+
"StepRef",
|
|
216
|
+
"StepTarget",
|
|
213
217
|
"StepOptions",
|
|
214
218
|
"Stream",
|
|
215
219
|
"StreamMessage",
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import asyncio
|
|
12
12
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
13
13
|
from contextlib import suppress
|
|
14
|
+
from datetime import datetime
|
|
14
15
|
from inspect import isawaitable, isgenerator
|
|
15
16
|
from typing import Any, Callable
|
|
16
17
|
|
|
@@ -80,10 +81,11 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
80
81
|
self,
|
|
81
82
|
original: pb.InvokeWaitForMethodRequest,
|
|
82
83
|
is_active: Callable[[], bool] | None = None,
|
|
84
|
+
deadline: datetime | None = None,
|
|
83
85
|
) -> AsyncGenerator[pb.InvokeWaitForMethodOutput, None]:
|
|
84
86
|
emitter = _AsyncStepOutputEmitter()
|
|
85
87
|
handler = asyncio.create_task(
|
|
86
|
-
self._invoke_wait_for_result(original, emitter, is_active)
|
|
88
|
+
self._invoke_wait_for_result(original, emitter, is_active, deadline)
|
|
87
89
|
)
|
|
88
90
|
try:
|
|
89
91
|
async for output in self._drain_outputs(emitter, handler):
|
|
@@ -97,6 +99,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
97
99
|
original: pb.InvokeWaitForMethodRequest,
|
|
98
100
|
emitter: _AsyncStepOutputEmitter,
|
|
99
101
|
is_active: Callable[[], bool] | None,
|
|
102
|
+
deadline: datetime | None,
|
|
100
103
|
) -> pb.InvokeWaitForMethodResponse:
|
|
101
104
|
request = await self._async_hydrator.wait_for_request(original)
|
|
102
105
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -113,6 +116,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
113
116
|
loaded_channel_names=request.loaded_channel_names,
|
|
114
117
|
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
115
118
|
is_active=is_active,
|
|
119
|
+
deadline=deadline,
|
|
116
120
|
output_emitter=emitter,
|
|
117
121
|
)
|
|
118
122
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
@@ -164,11 +168,14 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
164
168
|
self,
|
|
165
169
|
original: pb.InvokeExecuteMethodRequest,
|
|
166
170
|
is_active: Callable[[], bool] | None = None,
|
|
171
|
+
deadline: datetime | None = None,
|
|
167
172
|
) -> AsyncGenerator[pb.InvokeExecuteMethodOutput, None]:
|
|
168
173
|
if original.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
|
|
169
174
|
emitter = _AsyncStepOutputEmitter()
|
|
170
175
|
handler = asyncio.create_task(
|
|
171
|
-
self._invoke_timeout_handler_async(
|
|
176
|
+
self._invoke_timeout_handler_async(
|
|
177
|
+
original, emitter, is_active, deadline
|
|
178
|
+
)
|
|
172
179
|
)
|
|
173
180
|
try:
|
|
174
181
|
async for output in self._drain_outputs(emitter, handler):
|
|
@@ -179,7 +186,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
179
186
|
return
|
|
180
187
|
emitter = _AsyncStepOutputEmitter()
|
|
181
188
|
handler = asyncio.create_task(
|
|
182
|
-
self._invoke_execute_result(original, emitter, is_active)
|
|
189
|
+
self._invoke_execute_result(original, emitter, is_active, deadline)
|
|
183
190
|
)
|
|
184
191
|
try:
|
|
185
192
|
async for output in self._drain_outputs(emitter, handler):
|
|
@@ -193,6 +200,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
193
200
|
original: pb.InvokeExecuteMethodRequest,
|
|
194
201
|
emitter: _AsyncStepOutputEmitter,
|
|
195
202
|
is_active: Callable[[], bool] | None,
|
|
203
|
+
deadline: datetime | None,
|
|
196
204
|
) -> pb.InvokeExecuteMethodResponse:
|
|
197
205
|
request = await self._async_hydrator.execute_request(original)
|
|
198
206
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -214,6 +222,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
214
222
|
loaded_channel_names=request.loaded_channel_names,
|
|
215
223
|
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
216
224
|
is_active=is_active,
|
|
225
|
+
deadline=deadline,
|
|
217
226
|
output_emitter=emitter,
|
|
218
227
|
)
|
|
219
228
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
@@ -264,6 +273,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
264
273
|
original: pb.InvokeExecuteMethodRequest,
|
|
265
274
|
emitter: _AsyncStepOutputEmitter,
|
|
266
275
|
is_active: Callable[[], bool] | None,
|
|
276
|
+
deadline: datetime | None,
|
|
267
277
|
) -> pb.InvokeExecuteMethodResponse:
|
|
268
278
|
request = await self._async_hydrator.execute_request(original)
|
|
269
279
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -295,6 +305,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
295
305
|
loaded_channel_names=request.loaded_channel_names,
|
|
296
306
|
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
297
307
|
is_active=is_active,
|
|
308
|
+
deadline=deadline,
|
|
298
309
|
output_emitter=emitter,
|
|
299
310
|
)
|
|
300
311
|
response: pb.InvokeExecuteMethodResponse | None = None
|
|
@@ -83,7 +83,9 @@ class InvocationContext:
|
|
|
83
83
|
loaded_channel_map_instances: Sequence[str] = (),
|
|
84
84
|
is_active: Callable[[], bool] | None = None,
|
|
85
85
|
output_emitter: _StepOutputEmitter | None = None,
|
|
86
|
+
deadline: datetime | None = None,
|
|
86
87
|
) -> None:
|
|
88
|
+
self._deadline = None if method is InvocationMethod.RPC else deadline
|
|
87
89
|
self._method = method
|
|
88
90
|
self._flow = flow
|
|
89
91
|
self._metadata = metadata
|
|
@@ -115,6 +117,10 @@ class InvocationContext:
|
|
|
115
117
|
def run_id(self) -> str:
|
|
116
118
|
return self._metadata.run_id
|
|
117
119
|
|
|
120
|
+
@property
|
|
121
|
+
def flow_started_at(self) -> datetime:
|
|
122
|
+
return datetime.fromtimestamp(self._metadata.flow_started_timestamp, UTC)
|
|
123
|
+
|
|
118
124
|
@property
|
|
119
125
|
def step_execution_id(self) -> str:
|
|
120
126
|
return self._metadata.step_execution_id
|
|
@@ -136,6 +142,10 @@ class InvocationContext:
|
|
|
136
142
|
def first_attempt_at(self) -> datetime:
|
|
137
143
|
return datetime.fromtimestamp(self._metadata.first_attempt_timestamp, UTC)
|
|
138
144
|
|
|
145
|
+
@property
|
|
146
|
+
def deadline(self) -> datetime | None:
|
|
147
|
+
return self._deadline
|
|
148
|
+
|
|
139
149
|
@property
|
|
140
150
|
def attempt(self) -> int:
|
|
141
151
|
return self._metadata.attempt
|
|
@@ -705,16 +705,25 @@ class PullStream:
|
|
|
705
705
|
await self._send_error(task.task_id, failure, status)
|
|
706
706
|
|
|
707
707
|
async def _execute_async(self, task: pb.PollWorkerTask, kind: str | None) -> None:
|
|
708
|
+
deadline = (
|
|
709
|
+
task.deadline.ToDatetime(tzinfo=timezone.utc)
|
|
710
|
+
if task.HasField("deadline")
|
|
711
|
+
else None
|
|
712
|
+
)
|
|
708
713
|
if kind == "wait_for":
|
|
709
714
|
async for output in self.runtime._dispatcher.invoke_wait_for(
|
|
710
|
-
task.wait_for,
|
|
715
|
+
task.wait_for,
|
|
716
|
+
self._active.is_set,
|
|
717
|
+
deadline,
|
|
711
718
|
):
|
|
712
719
|
await self._send_frame(
|
|
713
720
|
pb.PollWorkerTaskFrame(task_id=task.task_id, wait_for=output)
|
|
714
721
|
)
|
|
715
722
|
elif kind == "execute":
|
|
716
723
|
async for output in self.runtime._dispatcher.invoke_execute(
|
|
717
|
-
task.execute,
|
|
724
|
+
task.execute,
|
|
725
|
+
self._active.is_set,
|
|
726
|
+
deadline,
|
|
718
727
|
):
|
|
719
728
|
await self._send_frame(
|
|
720
729
|
pb.PollWorkerTaskFrame(task_id=task.task_id, execute=output)
|
|
@@ -730,9 +739,16 @@ class PullStream:
|
|
|
730
739
|
raise ValueError(f"unsupported Worker task request {kind}")
|
|
731
740
|
|
|
732
741
|
def _execute_sync(self, task: pb.PollWorkerTask, kind: str | None) -> None:
|
|
742
|
+
deadline = (
|
|
743
|
+
task.deadline.ToDatetime(tzinfo=timezone.utc)
|
|
744
|
+
if task.HasField("deadline")
|
|
745
|
+
else None
|
|
746
|
+
)
|
|
733
747
|
if kind == "wait_for":
|
|
734
748
|
outputs = self.runtime._dispatcher.invoke_wait_for(
|
|
735
|
-
task.wait_for,
|
|
749
|
+
task.wait_for,
|
|
750
|
+
self._active.is_set,
|
|
751
|
+
deadline,
|
|
736
752
|
)
|
|
737
753
|
for output in outputs:
|
|
738
754
|
self._send_from_thread(
|
|
@@ -740,7 +756,9 @@ class PullStream:
|
|
|
740
756
|
)
|
|
741
757
|
elif kind == "execute":
|
|
742
758
|
outputs = self.runtime._dispatcher.invoke_execute(
|
|
743
|
-
task.execute,
|
|
759
|
+
task.execute,
|
|
760
|
+
self._active.is_set,
|
|
761
|
+
deadline,
|
|
744
762
|
)
|
|
745
763
|
for output in outputs:
|
|
746
764
|
self._send_from_thread(
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
-
from dataclasses import
|
|
12
|
+
from dataclasses import is_dataclass
|
|
13
13
|
from datetime import datetime
|
|
14
14
|
from enum import Enum
|
|
15
15
|
from typing import Any, cast
|
|
@@ -17,7 +17,14 @@ from typing import Any, cast
|
|
|
17
17
|
from google.protobuf import struct_pb2
|
|
18
18
|
|
|
19
19
|
from dex.attribute import AttributeIndex, IndexType
|
|
20
|
-
from dex.codec import
|
|
20
|
+
from dex.codec import (
|
|
21
|
+
Codec,
|
|
22
|
+
CodecRegistry,
|
|
23
|
+
Value,
|
|
24
|
+
WireKind,
|
|
25
|
+
_encode_json_value,
|
|
26
|
+
_validate_json_value_metadata,
|
|
27
|
+
)
|
|
21
28
|
from dex.dexpb import dex_pb2 as pb
|
|
22
29
|
from dex.runtime_errors import ValueMappingError
|
|
23
30
|
|
|
@@ -87,6 +94,7 @@ class ValueMapper:
|
|
|
87
94
|
codec = self._codecs.resolve(value_type)
|
|
88
95
|
except TypeError:
|
|
89
96
|
if isinstance(value, (list, tuple, dict)):
|
|
97
|
+
_validate_json_value_metadata(value, set())
|
|
90
98
|
return pb.Value(
|
|
91
99
|
obj_value=pb.EncodedObject(
|
|
92
100
|
encoding=_JSON_ENCODING,
|
|
@@ -213,7 +221,7 @@ class ValueMapper:
|
|
|
213
221
|
def _encode_json_collection(value: object) -> bytes:
|
|
214
222
|
def default(item: object) -> object:
|
|
215
223
|
if is_dataclass(item) and not isinstance(item, type):
|
|
216
|
-
return
|
|
224
|
+
return _encode_json_value(item)
|
|
217
225
|
if isinstance(item, datetime):
|
|
218
226
|
return item.isoformat()
|
|
219
227
|
if isinstance(item, Enum):
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from datetime import timedelta
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
12
|
from inspect import isawaitable, isgenerator
|
|
13
13
|
from typing import Any, Callable, Generator, cast
|
|
14
14
|
|
|
@@ -40,12 +40,12 @@ from dex.runtime_errors import InvalidStepResultError, ValueMappingError
|
|
|
40
40
|
from dex.step import (
|
|
41
41
|
DecisionKind,
|
|
42
42
|
RetryPolicy,
|
|
43
|
-
Step,
|
|
44
43
|
StepDecision,
|
|
45
44
|
StepDurability,
|
|
46
45
|
StepMovement,
|
|
47
46
|
StepOptions,
|
|
48
47
|
StepOutput,
|
|
48
|
+
StepTarget,
|
|
49
49
|
WaitForFailurePolicy,
|
|
50
50
|
_HeartbeatStepOutput,
|
|
51
51
|
_StreamStepOutput,
|
|
@@ -74,6 +74,7 @@ class WorkerDispatcher:
|
|
|
74
74
|
self,
|
|
75
75
|
original: pb.InvokeWaitForMethodRequest,
|
|
76
76
|
is_active: Callable[[], bool] | None = None,
|
|
77
|
+
deadline: datetime | None = None,
|
|
77
78
|
) -> Generator[pb.InvokeWaitForMethodOutput, None, None]:
|
|
78
79
|
request = self._hydrator.wait_for_request(original)
|
|
79
80
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -90,6 +91,7 @@ class WorkerDispatcher:
|
|
|
90
91
|
loaded_channel_names=request.loaded_channel_names,
|
|
91
92
|
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
92
93
|
is_active=is_active,
|
|
94
|
+
deadline=deadline,
|
|
93
95
|
)
|
|
94
96
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
95
97
|
wait = step.step.wait_for(context, input)
|
|
@@ -125,11 +127,12 @@ class WorkerDispatcher:
|
|
|
125
127
|
self,
|
|
126
128
|
original: pb.InvokeExecuteMethodRequest,
|
|
127
129
|
is_active: Callable[[], bool] | None = None,
|
|
130
|
+
deadline: datetime | None = None,
|
|
128
131
|
) -> Generator[pb.InvokeExecuteMethodOutput, None, None]:
|
|
129
132
|
request = self._hydrator.execute_request(original)
|
|
130
133
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
131
134
|
if request.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
|
|
132
|
-
response = yield from self._invoke_timeout_handler(request, flow)
|
|
135
|
+
response = yield from self._invoke_timeout_handler(request, flow, deadline)
|
|
133
136
|
yield pb.InvokeExecuteMethodOutput(result=response)
|
|
134
137
|
return
|
|
135
138
|
step = flow.step(request.step_type)
|
|
@@ -150,6 +153,7 @@ class WorkerDispatcher:
|
|
|
150
153
|
loaded_channel_names=request.loaded_channel_names,
|
|
151
154
|
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
152
155
|
is_active=is_active,
|
|
156
|
+
deadline=deadline,
|
|
153
157
|
)
|
|
154
158
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
155
159
|
decision = step.step.execute(context, input)
|
|
@@ -184,6 +188,7 @@ class WorkerDispatcher:
|
|
|
184
188
|
self,
|
|
185
189
|
request: pb.InvokeExecuteMethodRequest,
|
|
186
190
|
flow: _RegisteredFlow,
|
|
191
|
+
deadline: datetime | None,
|
|
187
192
|
) -> Generator[pb.InvokeExecuteMethodOutput, None, pb.InvokeExecuteMethodResponse]:
|
|
188
193
|
if request.HasField("step_input"):
|
|
189
194
|
raise InvalidStepResultError(
|
|
@@ -212,6 +217,7 @@ class WorkerDispatcher:
|
|
|
212
217
|
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
213
218
|
loaded_channel_names=request.loaded_channel_names,
|
|
214
219
|
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
220
|
+
deadline=deadline,
|
|
215
221
|
)
|
|
216
222
|
decision = flow.flow.handle_timeout(context)
|
|
217
223
|
if isgenerator(decision):
|
|
@@ -840,7 +846,7 @@ class WorkerDispatcher:
|
|
|
840
846
|
def _map_cancellation_steps(
|
|
841
847
|
self,
|
|
842
848
|
flow: _RegisteredFlow,
|
|
843
|
-
steps: tuple[
|
|
849
|
+
steps: tuple[StepTarget[Any], ...],
|
|
844
850
|
) -> list[str]:
|
|
845
851
|
step_types: list[str] = []
|
|
846
852
|
seen: set[str] = set()
|
|
@@ -896,9 +902,9 @@ class WorkerDispatcher:
|
|
|
896
902
|
@staticmethod
|
|
897
903
|
def _registered_movement_target(
|
|
898
904
|
flow: _RegisteredFlow,
|
|
899
|
-
step:
|
|
905
|
+
step: StepTarget[Any],
|
|
900
906
|
) -> _RegisteredStep:
|
|
901
|
-
return flow.
|
|
907
|
+
return flow.step_target(step)
|
|
902
908
|
|
|
903
909
|
@staticmethod
|
|
904
910
|
def _require_persistence_identity(
|