durable-workflow 2.14.0__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.0/src/durable_workflow.egg-info → durable_workflow-2.15.0}/PKG-INFO +1 -1
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/pyproject.toml +3 -3
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/__init__.py +8 -0
- durable_workflow-2.15.0/src/durable_workflow/_condition_fingerprint.py +229 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_typed_values.py +67 -8
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/activity.py +4 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/client.py +286 -3
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/errors.py +7 -0
- {durable_workflow-2.14.0 → 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.0 → durable_workflow-2.15.0}/src/durable_workflow/worker.py +23 -1
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/workflow.py +242 -74
- {durable_workflow-2.14.0 → durable_workflow-2.15.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/SOURCES.txt +21 -0
- durable_workflow-2.15.0/tests/test_activity_identity.py +79 -0
- {durable_workflow-2.14.0 → 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.14.0 → durable_workflow-2.15.0}/tests/test_client_timeouts.py +5 -0
- durable_workflow-2.15.0/tests/test_condition_fingerprint.py +282 -0
- durable_workflow-2.15.0/tests/test_condition_occurrence_id.py +245 -0
- {durable_workflow-2.14.0 → 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.0 → 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.0 → durable_workflow-2.15.0}/tests/test_redrive_successor_evidence.py +266 -5
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_clock.py +89 -21
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_regression_corpus.py +24 -0
- durable_workflow-2.15.0/tests/test_run_activities.py +161 -0
- {durable_workflow-2.14.0 → 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.0 → durable_workflow-2.15.0}/tests/test_wait_condition.py +65 -1
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker.py +19 -7
- 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.0/tests/test_worker_observation.py +0 -311
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/LICENSE +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/README.md +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/setup.cfg +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/server_compatibility.json +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/worker_status.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_empty_callback_failure.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_generator_lifecycle.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_bundle_pruned.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_publish_workflow_isolation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_search_attribute_replay_identity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_typed_values.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_status.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.14.0 → 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",
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Stable identity for ``wait_condition`` predicates (#258).
|
|
2
|
+
|
|
3
|
+
The Server records the fingerprint a worker sends with ``open_condition_wait``
|
|
4
|
+
and replay compares it, so the value must be the same on every Python version
|
|
5
|
+
and in every process. Version 2 hashes the predicate's module, qualified name
|
|
6
|
+
and a canonical form of its source AST. It never hashes bytecode, constant
|
|
7
|
+
reprs, memory addresses, file paths or line numbers.
|
|
8
|
+
|
|
9
|
+
Version 1 (every release through 2.14.0) hashed bytecode and ``repr`` of code
|
|
10
|
+
constants, which change across Python versions and, for nested functions,
|
|
11
|
+
across processes. Recorded v1 values cannot be recomputed portably, so replay
|
|
12
|
+
treats a v1 value that differs from this process's v1 as unverifiable rather
|
|
13
|
+
than as a changed predicate (see :func:`fingerprint_verdict`).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import hashlib
|
|
20
|
+
import inspect
|
|
21
|
+
import linecache
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
V2_PREFIX = "dwpy2:sha256:"
|
|
27
|
+
_LEGACY = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
28
|
+
# Fields that carry no predicate meaning or differ between Python versions.
|
|
29
|
+
_SKIPPED_FIELDS = frozenset({"ctx", "kind", "type_comment", "type_params", "decorator_list", "returns"})
|
|
30
|
+
_parsed: dict[tuple[str, int], ast.Module | None] = {}
|
|
31
|
+
_nodes: dict[Any, str | None] = {}
|
|
32
|
+
# Python 3.12 inlines list, dict and set comprehensions (PEP 709), which drops
|
|
33
|
+
# their segment from a nested function's __qualname__.
|
|
34
|
+
_INLINED_COMPREHENSION = re.compile(r"\.<(?:listcomp|dictcomp|setcomp)>")
|
|
35
|
+
|
|
36
|
+
Verdict = Literal["match", "mismatch", "unverifiable"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def legacy_fingerprint(predicate: Callable[[], bool]) -> str:
|
|
40
|
+
"""The v1 fingerprint, kept only to recognise histories recorded by 2.14.0 and earlier."""
|
|
41
|
+
h = hashlib.sha256()
|
|
42
|
+
h.update(b"durable-workflow-python.wait-condition.v1\0")
|
|
43
|
+
h.update(f"{getattr(predicate, '__module__', '')}\0".encode())
|
|
44
|
+
h.update(f"{getattr(predicate, '__qualname__', '')}\0".encode())
|
|
45
|
+
original = getattr(predicate, "__typed_workflow_method__", None)
|
|
46
|
+
code = getattr(original if original is not None else predicate, "__code__", None)
|
|
47
|
+
if code is None:
|
|
48
|
+
h.update(repr(predicate).encode())
|
|
49
|
+
else:
|
|
50
|
+
h.update(repr((
|
|
51
|
+
code.co_argcount, code.co_posonlyargcount, code.co_kwonlyargcount, code.co_code,
|
|
52
|
+
code.co_consts, code.co_names, code.co_varnames, code.co_freevars,
|
|
53
|
+
)).encode())
|
|
54
|
+
return f"sha256:{h.hexdigest()}"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def is_legacy(value: Any) -> bool:
|
|
58
|
+
return isinstance(value, str) and _LEGACY.match(value) is not None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def fingerprint(predicate: Callable[[], bool]) -> str | None:
|
|
62
|
+
"""Return the v2 fingerprint, or None when the predicate's source is unavailable.
|
|
63
|
+
|
|
64
|
+
Without a fingerprint the wait is identified by its key and call order,
|
|
65
|
+
as in sdk-php.
|
|
66
|
+
"""
|
|
67
|
+
function = _authored_function(predicate)
|
|
68
|
+
if function is None:
|
|
69
|
+
return None
|
|
70
|
+
code = function.__code__
|
|
71
|
+
if code not in _nodes:
|
|
72
|
+
_nodes[code] = _predicate_node(function)
|
|
73
|
+
node = _nodes[code]
|
|
74
|
+
if node is None:
|
|
75
|
+
return None
|
|
76
|
+
qualname = _INLINED_COMPREHENSION.sub("", getattr(function, "__qualname__", ""))
|
|
77
|
+
h = hashlib.sha256()
|
|
78
|
+
h.update(b"durable-workflow-python.wait-condition.v2\0")
|
|
79
|
+
h.update(f"{getattr(function, '__module__', '')}\0".encode())
|
|
80
|
+
h.update(f"{qualname}\0".encode())
|
|
81
|
+
h.update(node.encode())
|
|
82
|
+
return f"{V2_PREFIX}{h.hexdigest()}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def fingerprint_verdict(recorded: Any, authored: str | None, predicate: Callable[[], bool]) -> Verdict:
|
|
86
|
+
"""Compare a recorded fingerprint with the authored predicate.
|
|
87
|
+
|
|
88
|
+
- No recorded value: nothing to compare.
|
|
89
|
+
- An identity supplied explicitly in another format (not v2, not None)
|
|
90
|
+
is compared exactly, as before.
|
|
91
|
+
- A v2 value is compared exactly; without source it cannot be checked.
|
|
92
|
+
- A v1 value equal to this process's v1 matches. Any other v1 value is
|
|
93
|
+
unverifiable, because v1 differs across Python versions and processes.
|
|
94
|
+
- Any other string is compared exactly, as before.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(recorded, str):
|
|
97
|
+
return "match"
|
|
98
|
+
if authored is not None and not authored.startswith(V2_PREFIX):
|
|
99
|
+
return "match" if recorded == authored else "mismatch"
|
|
100
|
+
if recorded.startswith(V2_PREFIX):
|
|
101
|
+
if authored is None:
|
|
102
|
+
return "unverifiable"
|
|
103
|
+
return "match" if recorded == authored else "mismatch"
|
|
104
|
+
if is_legacy(recorded):
|
|
105
|
+
return "match" if recorded == legacy_fingerprint(predicate) else "unverifiable"
|
|
106
|
+
return "match" if recorded == authored else "mismatch"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _authored_function(predicate: Any) -> Any:
|
|
110
|
+
target = getattr(predicate, "__typed_workflow_method__", None) or predicate
|
|
111
|
+
if inspect.ismethod(target):
|
|
112
|
+
target = target.__func__
|
|
113
|
+
target = getattr(target, "__typed_workflow_method__", None) or target
|
|
114
|
+
return target if inspect.isfunction(target) else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _module_tree(function: Any) -> ast.Module | None:
|
|
118
|
+
try:
|
|
119
|
+
filename = inspect.getsourcefile(function) or inspect.getfile(function)
|
|
120
|
+
except (OSError, TypeError):
|
|
121
|
+
return None
|
|
122
|
+
lines = linecache.getlines(filename, function.__globals__)
|
|
123
|
+
if not lines:
|
|
124
|
+
return None
|
|
125
|
+
source = "".join(lines)
|
|
126
|
+
key = (filename, hash(source))
|
|
127
|
+
if key not in _parsed:
|
|
128
|
+
try:
|
|
129
|
+
_parsed[key] = ast.parse(source)
|
|
130
|
+
except (SyntaxError, ValueError):
|
|
131
|
+
_parsed[key] = None
|
|
132
|
+
return _parsed[key]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _predicate_node(function: Any) -> str | None:
|
|
136
|
+
tree = _module_tree(function)
|
|
137
|
+
if tree is None:
|
|
138
|
+
return None
|
|
139
|
+
code = function.__code__
|
|
140
|
+
first_line = code.co_firstlineno
|
|
141
|
+
if function.__name__ == "<lambda>":
|
|
142
|
+
lambdas = [node for node in ast.walk(tree) if isinstance(node, ast.Lambda) and node.lineno == first_line]
|
|
143
|
+
lambdas.sort(key=lambda node: node.col_offset)
|
|
144
|
+
# PEP 709 moves comprehension instructions into the enclosing lambda.
|
|
145
|
+
# Candidates indistinguishable before 3.12 can become distinct after
|
|
146
|
+
# inlining, so keep the combined AST on every version in this case.
|
|
147
|
+
has_inlined_comprehension = any(
|
|
148
|
+
isinstance(child, ast.ListComp | ast.DictComp | ast.SetComp)
|
|
149
|
+
for node in lambdas for child in ast.walk(node)
|
|
150
|
+
)
|
|
151
|
+
if len(lambdas) > 1 and not has_inlined_comprehension:
|
|
152
|
+
# Names and constants are stable across versions; bytecode only
|
|
153
|
+
# breaks a tie, and is compared within this interpreter.
|
|
154
|
+
matching = [node for node in lambdas if _compiles_to(node, code, compare_bytecode=False)]
|
|
155
|
+
if len(matching) > 1:
|
|
156
|
+
matching = [node for node in matching if _compiles_to(node, code, compare_bytecode=True)]
|
|
157
|
+
if len(matching) == 1:
|
|
158
|
+
lambdas = matching
|
|
159
|
+
if not lambdas:
|
|
160
|
+
return None
|
|
161
|
+
# Lambdas that start on the same line and compile identically share
|
|
162
|
+
# one fingerprint covering all of them.
|
|
163
|
+
return "|".join(_canonical(node) for node in lambdas)
|
|
164
|
+
definitions = [
|
|
165
|
+
node for node in ast.walk(tree)
|
|
166
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == function.__name__
|
|
167
|
+
and min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]) == first_line
|
|
168
|
+
]
|
|
169
|
+
if len(definitions) != 1:
|
|
170
|
+
return None
|
|
171
|
+
return _canonical(definitions[0])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _compiles_to(node: ast.Lambda, code: Any, *, compare_bytecode: bool) -> bool:
|
|
175
|
+
"""Whether *node* compiles to *code* on this interpreter.
|
|
176
|
+
|
|
177
|
+
The candidate is compiled inside a function that binds the live code's
|
|
178
|
+
free variables, so names resolve the same way. Only the choice of node
|
|
179
|
+
uses bytecode, which is valid within one interpreter; the fingerprint
|
|
180
|
+
hashes the node's AST, so it stays portable.
|
|
181
|
+
"""
|
|
182
|
+
free = list(code.co_freevars)
|
|
183
|
+
lines = ["def __dw_outer__():"]
|
|
184
|
+
if free:
|
|
185
|
+
lines.append(f" {' = '.join(free)} = None")
|
|
186
|
+
lines.append(f" return ({ast.unparse(node)})")
|
|
187
|
+
try:
|
|
188
|
+
outer = compile("\n".join(lines), "<condition-fingerprint>", "exec")
|
|
189
|
+
except SyntaxError:
|
|
190
|
+
return False
|
|
191
|
+
function = next((const for const in outer.co_consts if hasattr(const, "co_code")), None)
|
|
192
|
+
candidate = next(
|
|
193
|
+
(const for const in getattr(function, "co_consts", ()) if getattr(const, "co_name", None) == "<lambda>"),
|
|
194
|
+
None,
|
|
195
|
+
)
|
|
196
|
+
return candidate is not None and (
|
|
197
|
+
(not compare_bytecode or candidate.co_code == code.co_code)
|
|
198
|
+
and candidate.co_argcount == code.co_argcount
|
|
199
|
+
and candidate.co_names == code.co_names
|
|
200
|
+
and candidate.co_varnames == code.co_varnames
|
|
201
|
+
and candidate.co_freevars == code.co_freevars
|
|
202
|
+
and _plain_constants(candidate) == _plain_constants(code)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _plain_constants(code: Any) -> list[Any]:
|
|
207
|
+
"""Constants other than nested code and the qualnames Python 3.10 stores for them."""
|
|
208
|
+
return [
|
|
209
|
+
const for const in code.co_consts
|
|
210
|
+
if not hasattr(const, "co_code") and not (isinstance(const, str) and "<locals>" in const)
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _canonical(value: Any) -> str:
|
|
215
|
+
if isinstance(value, ast.AST):
|
|
216
|
+
parts = [type(value).__name__]
|
|
217
|
+
for name in value._fields:
|
|
218
|
+
if name in _SKIPPED_FIELDS:
|
|
219
|
+
continue
|
|
220
|
+
item = getattr(value, name, None)
|
|
221
|
+
if item is None or item == []:
|
|
222
|
+
continue
|
|
223
|
+
parts.append(f"{name}={_canonical(item)}")
|
|
224
|
+
return "(" + " ".join(parts) + ")"
|
|
225
|
+
if isinstance(value, list):
|
|
226
|
+
return "[" + ",".join(_canonical(item) for item in value) + "]"
|
|
227
|
+
if value is None or value is Ellipsis or isinstance(value, str | bytes | int | float | complex | bool):
|
|
228
|
+
return f"{type(value).__name__}:{value!r}"
|
|
229
|
+
return type(value).__name__
|
|
@@ -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:
|