durable-workflow 2.14.1__tar.gz → 2.15.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.
- {durable_workflow-2.14.1/src/durable_workflow.egg-info → durable_workflow-2.15.0}/PKG-INFO +1 -1
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/pyproject.toml +3 -3
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/__init__.py +8 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_typed_values.py +67 -8
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/activity.py +4 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/client.py +286 -3
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/errors.py +7 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/metrics.py +1 -0
- durable_workflow-2.15.0/src/durable_workflow/replay_clock.py +488 -0
- durable_workflow-2.15.0/src/durable_workflow/replay_clock_math.py +23 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/worker.py +23 -1
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/workflow.py +162 -34
- {durable_workflow-2.14.1 → durable_workflow-2.15.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/SOURCES.txt +19 -0
- durable_workflow-2.15.0/tests/test_activity_identity.py +79 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_retry_info.py +2 -1
- durable_workflow-2.15.0/tests/test_business_replay_clock.py +445 -0
- durable_workflow-2.15.0/tests/test_business_replay_clock_child.py +239 -0
- durable_workflow-2.15.0/tests/test_business_replay_clock_parallel.py +456 -0
- durable_workflow-2.15.0/tests/test_business_replay_clock_redrive.py +142 -0
- durable_workflow-2.15.0/tests/test_business_replay_clock_sequence_claims.py +105 -0
- durable_workflow-2.15.0/tests/test_client_iterators.py +207 -0
- durable_workflow-2.15.0/tests/test_condition_occurrence_id.py +245 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_remote_worker.py +2 -1
- durable_workflow-2.15.0/tests/test_docs_release_channel.py +206 -0
- durable_workflow-2.15.0/tests/test_portal_source_split.py +139 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_worker.py +4 -2
- durable_workflow-2.15.0/tests/test_redrive_call_ledger.py +259 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_redrive_successor_evidence.py +266 -5
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_clock.py +89 -21
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_regression_corpus.py +20 -0
- durable_workflow-2.15.0/tests/test_run_activities.py +161 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sdk_metrics_contract.py +145 -1
- durable_workflow-2.15.0/tests/test_typed_workflow_values.py +339 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_wait_condition.py +63 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker.py +5 -2
- durable_workflow-2.15.0/tests/test_worker_observation.py +656 -0
- durable_workflow-2.15.0/tests/test_worker_startup_buffered.py +235 -0
- durable_workflow-2.15.0/tests/test_worker_startup_raw.py +200 -0
- durable_workflow-2.15.0/tests/test_worker_startup_read_budget.py +122 -0
- durable_workflow-2.15.0/tests/test_worker_startup_snapshot.py +235 -0
- durable_workflow-2.14.1/tests/test_worker_observation.py +0 -311
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/LICENSE +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/README.md +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/setup.cfg +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_condition_fingerprint.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/server_compatibility.json +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/worker_status.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_client.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_condition_fingerprint.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_empty_callback_failure.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_generator_lifecycle.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_bundle_pruned.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_publish_workflow_isolation.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_search_attribute_replay_identity.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_typed_values.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_status.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.15.0"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -79,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
79
79
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
80
80
|
|
|
81
81
|
[tool.durable-workflow]
|
|
82
|
-
product-train = "2.
|
|
83
|
-
registry-version = "2.
|
|
82
|
+
product-train = "2.15.0"
|
|
83
|
+
registry-version = "2.15.0"
|
|
84
84
|
supported-server-versions = "2.8.0"
|
|
85
85
|
supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" }
|
|
86
86
|
worker-protocol-version = "1.20"
|
|
@@ -52,6 +52,9 @@ from .client import (
|
|
|
52
52
|
TaskQueueList,
|
|
53
53
|
TaskQueueQueryAdmission,
|
|
54
54
|
TaskQueueTaskAdmission,
|
|
55
|
+
WorkflowActivity,
|
|
56
|
+
WorkflowActivityAttempt,
|
|
57
|
+
WorkflowActivityList,
|
|
55
58
|
WorkflowCommandResult,
|
|
56
59
|
WorkflowExecution,
|
|
57
60
|
WorkflowHandle,
|
|
@@ -90,6 +93,7 @@ from .errors import (
|
|
|
90
93
|
NonRetryableError,
|
|
91
94
|
QueryFailed,
|
|
92
95
|
QueryTaskAuthorityLost,
|
|
96
|
+
ReplayClockUnavailable,
|
|
93
97
|
RuntimeCapabilityUnsupported,
|
|
94
98
|
RuntimeDiscoveryUnavailable,
|
|
95
99
|
SagaCompensationFailed,
|
|
@@ -331,6 +335,9 @@ __all__ = [
|
|
|
331
335
|
"WorkflowEntryArgumentsMismatchError",
|
|
332
336
|
"WorkflowRun",
|
|
333
337
|
"WorkflowRunList",
|
|
338
|
+
"WorkflowActivity",
|
|
339
|
+
"WorkflowActivityAttempt",
|
|
340
|
+
"WorkflowActivityList",
|
|
334
341
|
"WorkflowStreamAppendItem",
|
|
335
342
|
"WorkflowStreamAppendResult",
|
|
336
343
|
"WorkflowStreamDescription",
|
|
@@ -383,6 +390,7 @@ __all__ = [
|
|
|
383
390
|
"NexusOperationResult",
|
|
384
391
|
"NexusServiceCall",
|
|
385
392
|
"NonDeterministicReplayError",
|
|
393
|
+
"ReplayClockUnavailable",
|
|
386
394
|
"DuplicateRegistrationError",
|
|
387
395
|
"InvalidWorkflowDefinition",
|
|
388
396
|
"NoopMetrics",
|
|
@@ -255,22 +255,22 @@ def to_payload_values(values: list[Any]) -> list[Any]:
|
|
|
255
255
|
return [to_payload_value(value) for value in values]
|
|
256
256
|
|
|
257
257
|
|
|
258
|
-
def
|
|
259
|
-
|
|
258
|
+
def _positional_decoder(
|
|
259
|
+
fn: Callable[..., Any], skip: int, where: str,
|
|
260
|
+
) -> Callable[[tuple[Any, ...]], list[Any]] | None:
|
|
261
|
+
"""Build a decoder for *fn*'s positional arguments after the first *skip*.
|
|
260
262
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
the decorator binds that name to the wrapper, so supervised callbacks that
|
|
264
|
-
pickle by reference resolve to it.
|
|
263
|
+
Returns None when no argument needs decoding (none, or only ``Any`` /
|
|
264
|
+
unannotated ones), so the caller can leave *fn* unwrapped.
|
|
265
265
|
"""
|
|
266
266
|
signature = inspect.signature(fn)
|
|
267
267
|
try:
|
|
268
268
|
hints = get_type_hints(fn)
|
|
269
269
|
except (NameError, TypeError) as error:
|
|
270
|
-
raise TypeError(f"
|
|
270
|
+
raise TypeError(f"{where} {fn.__qualname__} has unresolvable annotations: {error}") from None
|
|
271
271
|
positional: list[tuple[str, Any]] = []
|
|
272
272
|
variadic: tuple[str, Any] | None = None
|
|
273
|
-
for parameter in signature.parameters.values():
|
|
273
|
+
for parameter in list(signature.parameters.values())[skip:]:
|
|
274
274
|
annotation = hints.get(parameter.name, Any)
|
|
275
275
|
if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
|
276
276
|
validate_annotation(annotation, parameter.name)
|
|
@@ -279,6 +279,10 @@ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
279
279
|
validate_annotation(annotation, parameter.name)
|
|
280
280
|
variadic = (parameter.name, annotation)
|
|
281
281
|
|
|
282
|
+
annotations = [annotation for _, annotation in positional] + ([variadic[1]] if variadic is not None else [])
|
|
283
|
+
if all(annotation in _PASS_THROUGH for annotation in annotations):
|
|
284
|
+
return None
|
|
285
|
+
|
|
282
286
|
def convert(args: tuple[Any, ...]) -> list[Any]:
|
|
283
287
|
converted: list[Any] = []
|
|
284
288
|
for index, value in enumerate(args):
|
|
@@ -292,6 +296,22 @@ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
292
296
|
converted.append(value)
|
|
293
297
|
return converted
|
|
294
298
|
|
|
299
|
+
return convert
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
303
|
+
"""Wrap an activity so positional arguments decode to its annotations.
|
|
304
|
+
|
|
305
|
+
Annotations are validated here, so an unsupported type fails when the
|
|
306
|
+
activity is declared. The wrapper keeps *fn*'s module and qualified name:
|
|
307
|
+
the decorator binds that name to the wrapper, so supervised callbacks that
|
|
308
|
+
pickle by reference resolve to it.
|
|
309
|
+
"""
|
|
310
|
+
decoder = _positional_decoder(fn, 0, "typed activity")
|
|
311
|
+
|
|
312
|
+
def convert(args: tuple[Any, ...]) -> list[Any]:
|
|
313
|
+
return list(args) if decoder is None else decoder(args)
|
|
314
|
+
|
|
295
315
|
wrapper: Any
|
|
296
316
|
if inspect.iscoroutinefunction(fn):
|
|
297
317
|
|
|
@@ -309,3 +329,42 @@ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
309
329
|
wrapper = typed_sync
|
|
310
330
|
wrapper.__typed_activity__ = fn
|
|
311
331
|
return cast(Callable[..., Any], wrapper)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def typed_method(fn: Callable[..., Any], skip: int) -> Callable[..., Any]:
|
|
335
|
+
"""Wrap a workflow method so arguments after the first *skip* decode.
|
|
336
|
+
|
|
337
|
+
``skip`` is 1 for handlers (``self``) and 2 for ``run`` (``self, ctx``). A
|
|
338
|
+
generator ``run`` stays a generator. Results are returned unchanged; the
|
|
339
|
+
commands that carry them send dataclass and Enum values as neutral values.
|
|
340
|
+
A method with nothing to decode is returned unwrapped.
|
|
341
|
+
"""
|
|
342
|
+
convert = _positional_decoder(fn, skip, "typed workflow method")
|
|
343
|
+
if convert is None:
|
|
344
|
+
# Nothing to decode: keep the original function, so a handler used as a
|
|
345
|
+
# wait_condition predicate keeps its recorded fingerprint.
|
|
346
|
+
return fn
|
|
347
|
+
wrapper: Any
|
|
348
|
+
if inspect.isgeneratorfunction(fn):
|
|
349
|
+
|
|
350
|
+
@functools.wraps(fn)
|
|
351
|
+
def typed_generator(*args: Any, **kwargs: Any) -> Any:
|
|
352
|
+
return (yield from fn(*args[:skip], *convert(args[skip:]), **kwargs))
|
|
353
|
+
|
|
354
|
+
wrapper = typed_generator
|
|
355
|
+
elif inspect.iscoroutinefunction(fn):
|
|
356
|
+
|
|
357
|
+
@functools.wraps(fn)
|
|
358
|
+
async def typed_async(*args: Any, **kwargs: Any) -> Any:
|
|
359
|
+
return await fn(*args[:skip], *convert(args[skip:]), **kwargs)
|
|
360
|
+
|
|
361
|
+
wrapper = typed_async
|
|
362
|
+
else:
|
|
363
|
+
|
|
364
|
+
@functools.wraps(fn)
|
|
365
|
+
def typed_sync(*args: Any, **kwargs: Any) -> Any:
|
|
366
|
+
return fn(*args[:skip], *convert(args[skip:]), **kwargs)
|
|
367
|
+
|
|
368
|
+
wrapper = typed_sync
|
|
369
|
+
wrapper.__typed_workflow_method__ = fn
|
|
370
|
+
return cast(Callable[..., Any], wrapper)
|
|
@@ -82,6 +82,7 @@ class ActivityInfo:
|
|
|
82
82
|
retry_info: ActivityRetryInfo = field(init=False)
|
|
83
83
|
workflow_id: str | None = field(init=False)
|
|
84
84
|
run_id: str | None = field(init=False)
|
|
85
|
+
activity_execution_id: str | None = field(init=False)
|
|
85
86
|
|
|
86
87
|
def __post_init__(self, task_metadata: Mapping[str, Any] | None) -> None:
|
|
87
88
|
task = task_metadata if task_metadata is not None else {}
|
|
@@ -89,6 +90,9 @@ class ActivityInfo:
|
|
|
89
90
|
for name in ("workflow_id", "run_id"):
|
|
90
91
|
value = task.get(name)
|
|
91
92
|
object.__setattr__(self, name, value if isinstance(value, str) and value else None)
|
|
93
|
+
execution_id = task.get("activity_execution_id")
|
|
94
|
+
object.__setattr__(self, "activity_execution_id",
|
|
95
|
+
execution_id if isinstance(execution_id, str) and execution_id.strip() else None)
|
|
92
96
|
|
|
93
97
|
|
|
94
98
|
class ActivityContext:
|
|
@@ -62,6 +62,7 @@ from ._poll_transport import POLL_PATHS as _POLL_PATHS
|
|
|
62
62
|
from ._poll_transport import unwrap_response as _unwrap_poll_response
|
|
63
63
|
from ._poll_transport import warn_early_cutoff as _warn_poll_cutoff
|
|
64
64
|
from ._query_task_authority import QueryTaskAuthority, current_query_authority
|
|
65
|
+
from ._typed_values import decode_payload_value, to_payload_values, validate_annotation
|
|
65
66
|
from .errors import (
|
|
66
67
|
REQUEST_ID_CONFLICT,
|
|
67
68
|
ExternalPayloadIntegrityMismatch,
|
|
@@ -377,6 +378,33 @@ class _RuntimeExternalPayloadTransport:
|
|
|
377
378
|
prepared_completion_context: bool = False
|
|
378
379
|
|
|
379
380
|
|
|
381
|
+
class _PageTokens:
|
|
382
|
+
"""Detect cyclic opaque continuations with constant-space checkpoints."""
|
|
383
|
+
|
|
384
|
+
def __init__(self, token: str | None) -> None:
|
|
385
|
+
self.current = token
|
|
386
|
+
self._checkpoint = token
|
|
387
|
+
self._distance = 0
|
|
388
|
+
self._power = 1
|
|
389
|
+
|
|
390
|
+
def advance(self, token: Any) -> bool:
|
|
391
|
+
if token is None or token == "":
|
|
392
|
+
self.current = None
|
|
393
|
+
return False
|
|
394
|
+
if not isinstance(token, str):
|
|
395
|
+
raise RuntimeError("pagination returned an invalid page token")
|
|
396
|
+
if token == self.current or token == self._checkpoint:
|
|
397
|
+
raise RuntimeError("pagination repeated a page token")
|
|
398
|
+
self.current = token
|
|
399
|
+
self._distance += 1
|
|
400
|
+
# Brent's cycle detector retains one checkpoint, not every old token.
|
|
401
|
+
if self._distance == self._power:
|
|
402
|
+
self._checkpoint = token
|
|
403
|
+
self._power *= 2
|
|
404
|
+
self._distance = 0
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
|
|
380
408
|
@dataclass
|
|
381
409
|
class WorkflowExecution:
|
|
382
410
|
"""Current server view of one workflow execution."""
|
|
@@ -698,6 +726,133 @@ class WorkflowRunList:
|
|
|
698
726
|
runs: list[WorkflowRun]
|
|
699
727
|
|
|
700
728
|
|
|
729
|
+
@dataclass
|
|
730
|
+
class WorkflowActivityAttempt:
|
|
731
|
+
"""Received attempt visibility. Missing metadata stays unavailable."""
|
|
732
|
+
|
|
733
|
+
id: str | None = None
|
|
734
|
+
worker_attempt_id: str | None = None
|
|
735
|
+
attempt_number: int | None = None
|
|
736
|
+
status: str | None = None
|
|
737
|
+
task_id: str | None = None
|
|
738
|
+
lease_owner: str | None = None
|
|
739
|
+
lease_expires_at: str | None = None
|
|
740
|
+
started_at: str | None = None
|
|
741
|
+
last_heartbeat_at: str | None = None
|
|
742
|
+
last_heartbeat_progress: dict[str, Any] | list[Any] | None = None
|
|
743
|
+
closed_at: str | None = None
|
|
744
|
+
can_continue: bool | None = None
|
|
745
|
+
cancel_requested: bool | None = None
|
|
746
|
+
stop_reason: str | None = None
|
|
747
|
+
failure_reason: str | None = None
|
|
748
|
+
raw: dict[str, Any] | None = None
|
|
749
|
+
|
|
750
|
+
@classmethod
|
|
751
|
+
def from_dict(cls, data: dict[str, Any]) -> WorkflowActivityAttempt:
|
|
752
|
+
values = {name: _run_activity_field(data, name, str) for name in (
|
|
753
|
+
"id", "worker_attempt_id", "status", "task_id", "lease_owner", "lease_expires_at", "started_at",
|
|
754
|
+
"last_heartbeat_at", "closed_at", "stop_reason", "failure_reason",
|
|
755
|
+
)}
|
|
756
|
+
return cls(**values, attempt_number=_run_activity_field(data, "attempt_number", int),
|
|
757
|
+
can_continue=_run_activity_field(data, "can_continue", bool),
|
|
758
|
+
cancel_requested=_run_activity_field(data, "cancel_requested", bool),
|
|
759
|
+
last_heartbeat_progress=_run_activity_progress(data), raw=copy.deepcopy(data))
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@dataclass
|
|
763
|
+
class WorkflowActivity:
|
|
764
|
+
"""Activity metadata from one run, retaining Server diagnostic authority."""
|
|
765
|
+
|
|
766
|
+
id: str | None = None
|
|
767
|
+
idempotency_key: str | None = None
|
|
768
|
+
sequence: int | None = None
|
|
769
|
+
activity_type: str | None = None
|
|
770
|
+
activity_class: str | None = None
|
|
771
|
+
execution_mode: str | None = None
|
|
772
|
+
local_activity: bool | None = None
|
|
773
|
+
history_authority: str | None = None
|
|
774
|
+
diagnostic_only: bool | None = None
|
|
775
|
+
history_event_types: list[str] | None = None
|
|
776
|
+
history_unsupported_reason: str | None = None
|
|
777
|
+
reused_from_run_id: str | None = None
|
|
778
|
+
reused_activity_execution_id: str | None = None
|
|
779
|
+
reused_recorded_at: str | None = None
|
|
780
|
+
row_status: str | None = None
|
|
781
|
+
parallel_group_kind: str | None = None
|
|
782
|
+
parallel_group_id: str | None = None
|
|
783
|
+
parallel_group_base_sequence: int | None = None
|
|
784
|
+
parallel_group_size: int | None = None
|
|
785
|
+
parallel_group_index: int | None = None
|
|
786
|
+
parallel_group_path: list[dict[str, Any]] | None = None
|
|
787
|
+
attempt_id: str | None = None
|
|
788
|
+
worker_attempt_id: str | None = None
|
|
789
|
+
status: str | None = None
|
|
790
|
+
attempt_count: int | None = None
|
|
791
|
+
retry_policy: Any = None
|
|
792
|
+
connection: str | None = None
|
|
793
|
+
queue: str | None = None
|
|
794
|
+
last_heartbeat_at: str | None = None
|
|
795
|
+
last_heartbeat_progress: dict[str, Any] | list[Any] | None = None
|
|
796
|
+
created_at: str | None = None
|
|
797
|
+
started_at: str | None = None
|
|
798
|
+
closed_at: str | None = None
|
|
799
|
+
attempts: list[WorkflowActivityAttempt] | None = None
|
|
800
|
+
raw: dict[str, Any] | None = None
|
|
801
|
+
|
|
802
|
+
@classmethod
|
|
803
|
+
def from_dict(cls, data: dict[str, Any]) -> WorkflowActivity:
|
|
804
|
+
values = {name: _run_activity_field(data, name, str) for name in (
|
|
805
|
+
"id", "idempotency_key", "execution_mode", "history_authority", "history_unsupported_reason",
|
|
806
|
+
"reused_from_run_id", "reused_activity_execution_id", "reused_recorded_at", "row_status",
|
|
807
|
+
"parallel_group_kind", "parallel_group_id", "attempt_id", "worker_attempt_id", "status",
|
|
808
|
+
"connection", "queue", "last_heartbeat_at", "created_at", "started_at", "closed_at",
|
|
809
|
+
)}
|
|
810
|
+
for name in ("sequence", "parallel_group_base_sequence", "parallel_group_size", "parallel_group_index",
|
|
811
|
+
"attempt_count"):
|
|
812
|
+
values[name] = _run_activity_field(data, name, int)
|
|
813
|
+
for name in ("local_activity", "diagnostic_only"):
|
|
814
|
+
values[name] = _run_activity_field(data, name, bool)
|
|
815
|
+
attempts = _run_activity_items(data, "attempts", dict)
|
|
816
|
+
return cls(**values, activity_type=_run_activity_field(data, "type", str),
|
|
817
|
+
activity_class=_run_activity_field(data, "class", str),
|
|
818
|
+
history_event_types=_run_activity_items(data, "history_event_types", str),
|
|
819
|
+
parallel_group_path=_run_activity_items(data, "parallel_group_path", dict),
|
|
820
|
+
attempts=[WorkflowActivityAttempt.from_dict(item) for item in attempts]
|
|
821
|
+
if attempts is not None else None,
|
|
822
|
+
last_heartbeat_progress=_run_activity_progress(data),
|
|
823
|
+
retry_policy=copy.deepcopy(data.get("retry_policy")), raw=copy.deepcopy(data))
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@dataclass
|
|
827
|
+
class WorkflowActivityList:
|
|
828
|
+
"""One explicit run's activity visibility, distinct from standalone jobs."""
|
|
829
|
+
|
|
830
|
+
workflow_id: str
|
|
831
|
+
run_id: str
|
|
832
|
+
activities: list[WorkflowActivity]
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def _run_activity_field(data: dict[str, Any], name: str, kind: type[Any]) -> Any:
|
|
836
|
+
value = data.get(name)
|
|
837
|
+
if value is not None and type(value) is not kind:
|
|
838
|
+
raise RuntimeError(f"invalid run activity field: {name}")
|
|
839
|
+
return value
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _run_activity_items(data: dict[str, Any], name: str, kind: type[Any]) -> Any:
|
|
843
|
+
items = _run_activity_field(data, name, list)
|
|
844
|
+
if items is not None and any(type(item) is not kind for item in items):
|
|
845
|
+
raise RuntimeError(f"invalid run activity items: {name}")
|
|
846
|
+
return items
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _run_activity_progress(data: dict[str, Any]) -> Any:
|
|
850
|
+
value = data.get("last_heartbeat_progress")
|
|
851
|
+
if value is not None and not isinstance(value, (dict, list)):
|
|
852
|
+
raise RuntimeError("invalid run activity field: last_heartbeat_progress")
|
|
853
|
+
return copy.deepcopy(value)
|
|
854
|
+
|
|
855
|
+
|
|
701
856
|
@dataclass
|
|
702
857
|
class StandaloneActivityExecution:
|
|
703
858
|
"""Server view of one standalone activity execution.
|
|
@@ -1328,6 +1483,12 @@ class BridgeAdapterOutcome:
|
|
|
1328
1483
|
)
|
|
1329
1484
|
|
|
1330
1485
|
|
|
1486
|
+
# Client payloads that carry caller arguments.
|
|
1487
|
+
_ARGUMENT_PAYLOAD_KINDS = frozenset({
|
|
1488
|
+
"workflow_input", "signal", "update", "query", "activity_input", "schedule_input",
|
|
1489
|
+
})
|
|
1490
|
+
|
|
1491
|
+
|
|
1331
1492
|
class WorkflowHandle:
|
|
1332
1493
|
"""Convenience wrapper for operating on one workflow ID."""
|
|
1333
1494
|
|
|
@@ -1337,9 +1498,11 @@ class WorkflowHandle:
|
|
|
1337
1498
|
self.run_id = run_id
|
|
1338
1499
|
self.workflow_type = workflow_type
|
|
1339
1500
|
|
|
1340
|
-
async def result(self, *, poll_interval: float = 0.5, timeout: float = 30.0) -> Any:
|
|
1501
|
+
async def result(self, *, poll_interval: float = 0.5, timeout: float = 30.0, result_type: Any = None) -> Any:
|
|
1341
1502
|
"""Block until this workflow terminates and return its result. See :meth:`Client.get_result`."""
|
|
1342
|
-
return await self._client.get_result(
|
|
1503
|
+
return await self._client.get_result(
|
|
1504
|
+
self, poll_interval=poll_interval, timeout=timeout, result_type=result_type,
|
|
1505
|
+
)
|
|
1343
1506
|
|
|
1344
1507
|
async def describe(self) -> WorkflowExecution:
|
|
1345
1508
|
"""Return the server's current view of this workflow. See :meth:`Client.describe_workflow`."""
|
|
@@ -1367,6 +1530,17 @@ class WorkflowHandle:
|
|
|
1367
1530
|
raise ValueError("run_id is required to export workflow history from a handle")
|
|
1368
1531
|
return await self._client.export_history(self.workflow_id, self.run_id)
|
|
1369
1532
|
|
|
1533
|
+
async def iter_history(
|
|
1534
|
+
self, *, page_size: int | None = None, next_page_token: str | None = None,
|
|
1535
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
1536
|
+
"""Yield this explicit run's history. See :meth:`Client.iter_history`."""
|
|
1537
|
+
if self.run_id is None:
|
|
1538
|
+
raise ValueError("run_id is required to iterate workflow history from a handle")
|
|
1539
|
+
async for event in self._client.iter_history(
|
|
1540
|
+
self.workflow_id, self.run_id, page_size=page_size, next_page_token=next_page_token,
|
|
1541
|
+
):
|
|
1542
|
+
yield event
|
|
1543
|
+
|
|
1370
1544
|
async def list_runs(self) -> WorkflowRunList:
|
|
1371
1545
|
"""List all runs in this workflow execution chain. See :meth:`Client.list_workflow_runs`."""
|
|
1372
1546
|
return await self._client.list_workflow_runs(self.workflow_id)
|
|
@@ -1378,6 +1552,13 @@ class WorkflowHandle:
|
|
|
1378
1552
|
raise ValueError("run_id is required to describe a workflow run from a handle")
|
|
1379
1553
|
return await self._client.describe_workflow_run(self.workflow_id, selected_run_id)
|
|
1380
1554
|
|
|
1555
|
+
async def get_activities(self, run_id: str | None = None) -> WorkflowActivityList:
|
|
1556
|
+
"""Read activities for an explicit run. See :meth:`Client.get_run_activities`."""
|
|
1557
|
+
selected_run_id = self.run_id if run_id is None else run_id
|
|
1558
|
+
if selected_run_id is None:
|
|
1559
|
+
raise ValueError("run_id is required to fetch workflow activities from a handle")
|
|
1560
|
+
return await self._client.get_run_activities(self.workflow_id, selected_run_id)
|
|
1561
|
+
|
|
1381
1562
|
async def signal(
|
|
1382
1563
|
self,
|
|
1383
1564
|
signal_name: str,
|
|
@@ -1807,6 +1988,11 @@ class Client:
|
|
|
1807
1988
|
external_storage: ExternalStorageDriver | None = None,
|
|
1808
1989
|
external_storage_threshold_bytes: int | None = None,
|
|
1809
1990
|
) -> dict[str, Any]:
|
|
1991
|
+
if kind in _ARGUMENT_PAYLOAD_KINDS and isinstance(value, list):
|
|
1992
|
+
# Dataclass and Enum arguments travel as their neutral values, the
|
|
1993
|
+
# bytes dataclasses.asdict() and .value give. The codec refuses
|
|
1994
|
+
# those values, so no existing caller depends on this conversion.
|
|
1995
|
+
value = to_payload_values(value)
|
|
1810
1996
|
warning_context = self._payload_context(
|
|
1811
1997
|
kind=kind,
|
|
1812
1998
|
workflow_id=workflow_id,
|
|
@@ -3857,6 +4043,39 @@ class Client:
|
|
|
3857
4043
|
next_page_token=data.get("next_page_token"),
|
|
3858
4044
|
)
|
|
3859
4045
|
|
|
4046
|
+
async def iter_workflows(
|
|
4047
|
+
self,
|
|
4048
|
+
*,
|
|
4049
|
+
workflow_type: str | None = None,
|
|
4050
|
+
status: str | None = None,
|
|
4051
|
+
query: str | None = None,
|
|
4052
|
+
page_size: int | None = None,
|
|
4053
|
+
next_page_token: str | None = None,
|
|
4054
|
+
) -> AsyncIterator[WorkflowExecution]:
|
|
4055
|
+
"""Yield workflow list results lazily, retaining no previous pages.
|
|
4056
|
+
|
|
4057
|
+
Filters and namespace stay fixed across pages. An empty page with a
|
|
4058
|
+
continuation is followed; missing/empty continuation ends iteration.
|
|
4059
|
+
Cancellation, malformed/cyclic continuations and HTTP errors propagate,
|
|
4060
|
+
possibly after earlier results have been yielded. This is a traversal
|
|
4061
|
+
of Server pages, not a snapshot of concurrently changing workflows.
|
|
4062
|
+
"""
|
|
4063
|
+
tokens = _PageTokens(next_page_token)
|
|
4064
|
+
namespace = self.namespace
|
|
4065
|
+
while True:
|
|
4066
|
+
if self.namespace != namespace:
|
|
4067
|
+
raise RuntimeError("client namespace changed during pagination")
|
|
4068
|
+
page = await self.list_workflows(
|
|
4069
|
+
workflow_type=workflow_type, status=status, query=query, page_size=page_size,
|
|
4070
|
+
next_page_token=tokens.current,
|
|
4071
|
+
)
|
|
4072
|
+
more = tokens.advance(page.next_page_token)
|
|
4073
|
+
for execution in page.executions:
|
|
4074
|
+
yield execution
|
|
4075
|
+
del page
|
|
4076
|
+
if not more:
|
|
4077
|
+
return
|
|
4078
|
+
|
|
3860
4079
|
async def get_history(
|
|
3861
4080
|
self,
|
|
3862
4081
|
workflow_id: str,
|
|
@@ -3879,6 +4098,39 @@ class Client:
|
|
|
3879
4098
|
"GET", path, context=workflow_id
|
|
3880
4099
|
)
|
|
3881
4100
|
|
|
4101
|
+
async def iter_history(
|
|
4102
|
+
self,
|
|
4103
|
+
workflow_id: str,
|
|
4104
|
+
run_id: str,
|
|
4105
|
+
*,
|
|
4106
|
+
page_size: int | None = None,
|
|
4107
|
+
next_page_token: str | None = None,
|
|
4108
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
4109
|
+
"""Yield run-history events in Server order, fetching one page at a time.
|
|
4110
|
+
|
|
4111
|
+
Workflow/run/namespace identity and page size stay fixed. Stopping the
|
|
4112
|
+
iterator fetches no subsequent page. Empty continued pages are followed;
|
|
4113
|
+
malformed/cyclic continuations and HTTP errors propagate rather than
|
|
4114
|
+
appearing as a complete history. Already yielded events are not undone.
|
|
4115
|
+
"""
|
|
4116
|
+
tokens = _PageTokens(next_page_token)
|
|
4117
|
+
namespace = self.namespace
|
|
4118
|
+
while True:
|
|
4119
|
+
if self.namespace != namespace:
|
|
4120
|
+
raise RuntimeError("client namespace changed during pagination")
|
|
4121
|
+
page = await self.get_history(
|
|
4122
|
+
workflow_id, run_id, page_size=page_size, next_page_token=tokens.current,
|
|
4123
|
+
)
|
|
4124
|
+
if (not isinstance(page, dict) or not isinstance(page.get("events"), list)
|
|
4125
|
+
or any(not isinstance(event, dict) for event in page["events"])):
|
|
4126
|
+
raise RuntimeError("history pagination returned invalid events")
|
|
4127
|
+
more = tokens.advance(page.get("next_page_token"))
|
|
4128
|
+
for event in page["events"]:
|
|
4129
|
+
yield event
|
|
4130
|
+
del page
|
|
4131
|
+
if not more:
|
|
4132
|
+
return
|
|
4133
|
+
|
|
3882
4134
|
async def export_history(self, workflow_id: str, run_id: str) -> Any:
|
|
3883
4135
|
"""Export one workflow run history as a replay bundle."""
|
|
3884
4136
|
return await self._request(
|
|
@@ -3898,6 +4150,25 @@ class Client:
|
|
|
3898
4150
|
runs=runs,
|
|
3899
4151
|
)
|
|
3900
4152
|
|
|
4153
|
+
async def get_run_activities(self, workflow_id: str, run_id: str) -> WorkflowActivityList:
|
|
4154
|
+
"""Read typed activity/attempt metadata for this namespace and exact run.
|
|
4155
|
+
|
|
4156
|
+
Missing optional metadata stays None. Diagnostic/unsupported states and
|
|
4157
|
+
unknown future status strings are preserved; this visibility snapshot
|
|
4158
|
+
does not grant attempt authority. Payloads are not fetched or decoded.
|
|
4159
|
+
"""
|
|
4160
|
+
data = await self._request(
|
|
4161
|
+
"GET", f"/workflows/{quote(workflow_id, safe='._:-')}/runs/{quote(run_id, safe='._:-')}/activities",
|
|
4162
|
+
context=workflow_id,
|
|
4163
|
+
)
|
|
4164
|
+
if (not isinstance(data, dict) or data.get("workflow_id") != workflow_id
|
|
4165
|
+
or data.get("run_id") != run_id):
|
|
4166
|
+
raise RuntimeError("run activity response changed workflow/run identity")
|
|
4167
|
+
items = _run_activity_items(data, "activities", dict)
|
|
4168
|
+
if items is None:
|
|
4169
|
+
raise RuntimeError("run activity response omitted activities")
|
|
4170
|
+
return WorkflowActivityList(workflow_id, run_id, [WorkflowActivity.from_dict(item) for item in items])
|
|
4171
|
+
|
|
3901
4172
|
async def describe_workflow_run(self, workflow_id: str, run_id: str) -> WorkflowRun:
|
|
3902
4173
|
"""Return detailed status, payload, and actionability for one specific workflow run."""
|
|
3903
4174
|
data = await self._request("GET", f"/workflows/{workflow_id}/runs/{run_id}", context=workflow_id)
|
|
@@ -4760,9 +5031,16 @@ class Client:
|
|
|
4760
5031
|
*,
|
|
4761
5032
|
poll_interval: float = 0.5,
|
|
4762
5033
|
timeout: float = 30.0,
|
|
5034
|
+
result_type: Any = None,
|
|
4763
5035
|
) -> Any:
|
|
4764
5036
|
"""Poll a workflow until it reaches a terminal state and return its result.
|
|
4765
5037
|
|
|
5038
|
+
``result_type`` opts in to decoding the result to that annotation
|
|
5039
|
+
(the types :func:`durable_workflow.activity.defn` lists for
|
|
5040
|
+
``typed=True``); a mismatch raises
|
|
5041
|
+
:class:`~durable_workflow.errors.TypedValueError`, which names the
|
|
5042
|
+
field path but never the value. Without it the plain value is returned.
|
|
5043
|
+
|
|
4766
5044
|
Raises :class:`~durable_workflow.errors.WorkflowFailed`,
|
|
4767
5045
|
:class:`~durable_workflow.errors.WorkflowCancelled`, or
|
|
4768
5046
|
:class:`~durable_workflow.errors.WorkflowTerminated` if the workflow
|
|
@@ -4770,6 +5048,8 @@ class Client:
|
|
|
4770
5048
|
raises :class:`~durable_workflow.errors.WorkflowTimedOut`; a caller's
|
|
4771
5049
|
:class:`TimeoutError` means ``timeout`` seconds elapsed while waiting.
|
|
4772
5050
|
"""
|
|
5051
|
+
if result_type is not None:
|
|
5052
|
+
validate_annotation(result_type, "result_type")
|
|
4773
5053
|
deadline = asyncio.get_running_loop().time() + timeout
|
|
4774
5054
|
while True:
|
|
4775
5055
|
desc = await self.describe_workflow(handle.workflow_id)
|
|
@@ -4789,12 +5069,15 @@ class Client:
|
|
|
4789
5069
|
etype = ev.get("event_type")
|
|
4790
5070
|
payload = ev.get("payload") or {}
|
|
4791
5071
|
if etype == "WorkflowCompleted":
|
|
4792
|
-
|
|
5072
|
+
output = serializer.decode_envelope(
|
|
4793
5073
|
payload.get("output"),
|
|
4794
5074
|
codec=payload.get("payload_codec") or desc.payload_codec,
|
|
4795
5075
|
external_storage=self.external_storage,
|
|
4796
5076
|
external_storage_cache=self.external_storage_cache,
|
|
4797
5077
|
)
|
|
5078
|
+
if result_type is None:
|
|
5079
|
+
return output
|
|
5080
|
+
return decode_payload_value(output, result_type, "workflow result")
|
|
4798
5081
|
if etype == "WorkflowFailed":
|
|
4799
5082
|
raise WorkflowFailed(
|
|
4800
5083
|
payload.get("message", "workflow failed"),
|
|
@@ -27,6 +27,13 @@ class DurableWorkflowError(Exception):
|
|
|
27
27
|
"""Base class for every exception raised by the SDK."""
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class ReplayClockUnavailable(DurableWorkflowError):
|
|
31
|
+
"""The current execution has no proved business replay-time boundary."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
super().__init__("business replay time is unavailable at this execution boundary")
|
|
35
|
+
|
|
36
|
+
|
|
30
37
|
class FailureReportTooLarge(DurableWorkflowError, ValueError):
|
|
31
38
|
"""Required failure identity or semantics cannot fit the reporting budget.
|
|
32
39
|
|
|
@@ -26,6 +26,7 @@ WORKER_POLLS = "durable_workflow_worker_polls"
|
|
|
26
26
|
WORKER_POLL_DURATION_SECONDS = "durable_workflow_worker_poll_duration_seconds"
|
|
27
27
|
WORKER_TASKS = "durable_workflow_worker_tasks"
|
|
28
28
|
WORKER_TASK_DURATION_SECONDS = "durable_workflow_worker_task_duration_seconds"
|
|
29
|
+
WORKER_TASK_SCHEDULE_TO_START_SECONDS = "durable_workflow_worker_task_schedule_to_start_seconds"
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class MetricsRecorder(Protocol):
|