haiway 0.24.0__tar.gz → 0.24.2__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.
Files changed (75) hide show
  1. haiway-0.24.2/.claude/settings.json +15 -0
  2. {haiway-0.24.0 → haiway-0.24.2}/PKG-INFO +1 -1
  3. {haiway-0.24.0 → haiway-0.24.2}/junit/test-results.xml +1 -1
  4. {haiway-0.24.0 → haiway-0.24.2}/pyproject.toml +1 -1
  5. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/__init__.py +4 -0
  6. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/__init__.py +7 -1
  7. haiway-0.24.2/src/haiway/helpers/concurrent.py +394 -0
  8. haiway-0.24.2/tests/test_execute_concurrently.py +198 -0
  9. {haiway-0.24.0 → haiway-0.24.2}/tests/test_process_concurrently.py +0 -2
  10. haiway-0.24.2/tests/test_stream_concurrently.py +281 -0
  11. {haiway-0.24.0 → haiway-0.24.2}/uv.lock +159 -159
  12. haiway-0.24.0/src/haiway/helpers/concurrent.py +0 -85
  13. {haiway-0.24.0 → haiway-0.24.2}/.github/workflows/ci.yml +0 -0
  14. {haiway-0.24.0 → haiway-0.24.2}/.github/workflows/publish.yml +0 -0
  15. {haiway-0.24.0 → haiway-0.24.2}/.gitignore +0 -0
  16. {haiway-0.24.0 → haiway-0.24.2}/CLAUDE.md +0 -0
  17. {haiway-0.24.0 → haiway-0.24.2}/LICENSE +0 -0
  18. {haiway-0.24.0 → haiway-0.24.2}/Makefile +0 -0
  19. {haiway-0.24.0 → haiway-0.24.2}/README.md +0 -0
  20. {haiway-0.24.0 → haiway-0.24.2}/config/pre-push +0 -0
  21. {haiway-0.24.0 → haiway-0.24.2}/guidelines/functionalities.md +0 -0
  22. {haiway-0.24.0 → haiway-0.24.2}/guidelines/llms.txt +0 -0
  23. {haiway-0.24.0 → haiway-0.24.2}/guidelines/packages.md +0 -0
  24. {haiway-0.24.0 → haiway-0.24.2}/guidelines/state.md +0 -0
  25. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/__init__.py +0 -0
  26. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/access.py +0 -0
  27. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/disposables.py +0 -0
  28. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/identifier.py +0 -0
  29. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/observability.py +0 -0
  30. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/state.py +0 -0
  31. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/tasks.py +0 -0
  32. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/context/types.py +0 -0
  33. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/asynchrony.py +0 -0
  34. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/caching.py +0 -0
  35. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/files.py +0 -0
  36. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/observability.py +0 -0
  37. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/retries.py +0 -0
  38. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/throttling.py +0 -0
  39. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/timeouting.py +0 -0
  40. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/helpers/tracing.py +0 -0
  41. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/opentelemetry/__init__.py +0 -0
  42. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/opentelemetry/observability.py +0 -0
  43. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/py.typed +0 -0
  44. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/state/__init__.py +0 -0
  45. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/state/attributes.py +0 -0
  46. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/state/path.py +0 -0
  47. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/state/requirement.py +0 -0
  48. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/state/structure.py +0 -0
  49. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/state/validation.py +0 -0
  50. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/types/__init__.py +0 -0
  51. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/types/default.py +0 -0
  52. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/types/missing.py +0 -0
  53. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/__init__.py +0 -0
  54. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/always.py +0 -0
  55. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/collections.py +0 -0
  56. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/env.py +0 -0
  57. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/formatting.py +0 -0
  58. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/logs.py +0 -0
  59. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/mimic.py +0 -0
  60. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/noop.py +0 -0
  61. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/queue.py +0 -0
  62. {haiway-0.24.0 → haiway-0.24.2}/src/haiway/utils/stream.py +0 -0
  63. {haiway-0.24.0 → haiway-0.24.2}/tests/__init__.py +0 -0
  64. {haiway-0.24.0 → haiway-0.24.2}/tests/test_async_queue.py +0 -0
  65. {haiway-0.24.0 → haiway-0.24.2}/tests/test_async_stream.py +0 -0
  66. {haiway-0.24.0 → haiway-0.24.2}/tests/test_attribute_path.py +0 -0
  67. {haiway-0.24.0 → haiway-0.24.2}/tests/test_attribute_requirement.py +0 -0
  68. {haiway-0.24.0 → haiway-0.24.2}/tests/test_auto_retry.py +0 -0
  69. {haiway-0.24.0 → haiway-0.24.2}/tests/test_cache.py +0 -0
  70. {haiway-0.24.0 → haiway-0.24.2}/tests/test_context.py +0 -0
  71. {haiway-0.24.0 → haiway-0.24.2}/tests/test_disposables.py +0 -0
  72. {haiway-0.24.0 → haiway-0.24.2}/tests/test_state.py +0 -0
  73. {haiway-0.24.0 → haiway-0.24.2}/tests/test_state_validation.py +0 -0
  74. {haiway-0.24.0 → haiway-0.24.2}/tests/test_streaming.py +0 -0
  75. {haiway-0.24.0 → haiway-0.24.2}/tests/test_timeout.py +0 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "source .venv/bin/activate && make format"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.24.0
3
+ Version: 0.24.2
4
4
  Summary: Framework for dependency injection and state management within structured concurrency model.
5
5
  Project-URL: Homepage, https://miquido.com
6
6
  Project-URL: Repository, https://github.com/miquido/haiway.git
@@ -1 +1 @@
1
- <?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="159" time="1.980" timestamp="2025-07-04T08:00:18.241083+00:00" hostname="pkrvmbietmlfzoi"><testcase classname="tests.test_async_queue" name="test_fails_when_stream_fails" time="0.001" /><testcase classname="tests.test_async_queue" name="test_cancels_when_iteration_cancels" time="0.001" /><testcase classname="tests.test_async_queue" name="test_ends_when_stream_ends" time="0.001" /><testcase classname="tests.test_async_queue" name="test_buffers_values_when_not_reading" time="0.001" /><testcase classname="tests.test_async_queue" name="test_delivers_buffer_when_streaming_fails" time="0.001" /><testcase classname="tests.test_async_queue" name="test_delivers_updates_when_sending" time="0.001" /><testcase classname="tests.test_async_queue" name="test_fails_when_sending_to_finished" time="0.001" /><testcase classname="tests.test_async_queue" name="test_ignores_when_finishing_when_finished" time="0.001" /><testcase classname="tests.test_async_stream" name="test_fails_when_stream_fails" time="0.001" /><testcase classname="tests.test_async_stream" name="test_cancels_when_iteration_cancels" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ends_when_stream_ends" time="0.001" /><testcase classname="tests.test_async_stream" name="test_finishes_without_buffer" time="0.001" /><testcase classname="tests.test_async_stream" name="test_fails_without_buffer" time="0.001" /><testcase classname="tests.test_async_stream" name="test_delivers_updates_when_sending" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ignores_when_sending_to_finished" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ignores_when_sending_to_failed" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ignores_when_finishing_when_finished" time="0.001" /><testcase classname="tests.test_async_stream" name="test_delivers_all_when_sending_async" time="0.001" /><testcase classname="tests.test_attribute_path" name="test_id_path_points_to_self" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_attribute_path_points_to_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_nested_attribute_path_points_to_nested_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_recursive_attribute_path_points_to_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_list_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_tuple_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_mixed_tuple_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_dict_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_id_path_set_updates_self" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_attribute_path_set_updates_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_nested_attribute_path_set_updates_nested_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_recursive_attribute_set_updates_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_list_item_path_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_tuple_item_path_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_mixed_tuple_item_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_dict_item_path_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_equal_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_not_equal_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_contains_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_contains_any_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_contained_in_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_logical_and_or_requirements" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_filter" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_immutability" time="0.000" /><testcase classname="tests.test_auto_retry" name="test_returns_value_without_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_retries_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_logs_issue_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_fails_with_exceeding_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_fails_with_cancellation" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_retries_with_selected_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_fails_with_not_selected_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_returns_value_without_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_retries_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_with_exceeding_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_with_cancellation" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_when_cancelled" time="0.021" /><testcase classname="tests.test_auto_retry" name="test_async_uses_delay_with_errors" time="0.101" /><testcase classname="tests.test_auto_retry" name="test_async_uses_computed_delay_with_errors" time="0.107" /><testcase classname="tests.test_auto_retry" name="test_async_logs_issue_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_retries_with_selected_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_with_not_selected_errors" time="0.001" /><testcase classname="tests.test_cache" name="test_returns_cache_value_with_same_argument" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_fresh_value_with_different_argument" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_fresh_value_with_limit_exceed" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_same_value_with_repeating_argument" time="0.000" /><testcase classname="tests.test_cache" name="test_fails_with_error" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_fresh_value_with_expiration_time_exceed" time="0.021" /><testcase classname="tests.test_cache" name="test_async_returns_cache_value_with_same_argument" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_fresh_value_with_different_argument" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_fresh_value_with_limit_exceed" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_same_value_with_repeating_argument" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_fresh_value_with_expiration_time_exceed" time="0.021" /><testcase classname="tests.test_cache" name="test_async_cancel_waiting_does_not_cancel_task" time="0.502" /><testcase classname="tests.test_cache" name="test_async_expiration_does_not_cancel_task" time="0.021" /><testcase classname="tests.test_cache" name="test_async_fails_with_error" time="0.001" /><testcase classname="tests.test_context" name="test_state_is_available_according_to_context" time="0.001" /><testcase classname="tests.test_context" name="test_state_update_updates_local_context" time="0.001" /><testcase classname="tests.test_context" name="test_exceptions_are_propagated" time="0.001" /><testcase classname="tests.test_disposables" name="test_empty_initialization" time="0.000" /><testcase classname="tests.test_disposables" name="test_single_disposable_initialization" time="0.000" /><testcase classname="tests.test_disposables" name="test_multiple_disposables_initialization" time="0.000" /><testcase classname="tests.test_disposables" name="test_cannot_set_attributes" time="0.001" /><testcase classname="tests.test_disposables" name="test_cannot_delete_attributes" time="0.000" /><testcase classname="tests.test_disposables" name="test_empty_disposables_is_falsy" time="0.000" /><testcase classname="tests.test_disposables" name="test_non_empty_disposables_is_truthy" time="0.000" /><testcase classname="tests.test_disposables" name="test_setup_with_no_disposables" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_disposable_returning_none" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_disposable_returning_single_state" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_disposable_returning_multiple_states" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_multiple_disposables_mixed_returns" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_sets_loop_correctly" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_no_disposables" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_successful_cleanup" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_exception_context" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_multiple_exceptions_creates_group" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_single_exception_is_risen" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_resets_loop_even_on_exception" time="0.001" /><testcase classname="tests.test_disposables" name="test_same_loop_cleanup" time="0.001" /><testcase classname="tests.test_disposables" name="test_with_real_async_context_managers" time="0.001" /><testcase classname="tests.test_disposables" name="test_nested_disposables_usage" time="0.001" /><testcase classname="tests.test_disposables" name="test_exception_during_setup_phase" time="0.001" /><testcase classname="tests.test_disposables" name="test_assertion_on_doubleprepare" time="0.001" /><testcase classname="tests.test_disposables" name="test_assertion_on_dispose_withoutprepare" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_processes_all_elements" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_processes_elements_concurrently" time="0.302" /><testcase classname="tests.test_process_concurrently" name="test_handles_empty_source" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_propagates_handler_exceptions" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_ignores_handler_exceptions_when_configured" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_handles_source_exception" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_cancels_running_tasks_on_cancellation" time="0.101" /><testcase classname="tests.test_process_concurrently" name="test_respects_concurrency_limit" time="0.202" /><testcase classname="tests.test_process_concurrently" name="test_processes_elements_from_queue" time="0.052" /><testcase classname="tests.test_state" name="test_basic_initializes_with_arguments" time="0.001" /><testcase classname="tests.test_state" name="test_basic_initializes_with_defaults" time="0.001" /><testcase classname="tests.test_state" name="test_basic_equals_checks_properties" time="0.000" /><testcase classname="tests.test_state" name="test_basic_initializes_with_arguments_and_defaults" time="0.000" /><testcase classname="tests.test_state" name="test_parametrized_initializes_with_proper_parameters" time="0.000" /><testcase classname="tests.test_state" name="test_nested_initializes_with_proper_arguments" time="0.001" /><testcase classname="tests.test_state" name="test_dict_skips_missing_properties" time="0.000" /><testcase classname="tests.test_state" name="test_initialization_allows_missing_properties" time="0.001" /><testcase classname="tests.test_state" name="test_generic_subtypes_validation" time="0.001" /><testcase classname="tests.test_state" name="test_copying_leaves_same_object" time="0.000" /><testcase classname="tests.test_state" name="test_hash_consistency_with_missing_values" time="0.000" /><testcase classname="tests.test_state" name="test_hash_with_unhashable_attributes" time="0.001" /><testcase classname="tests.test_state" name="test_hash_with_dict_key_order_independence" time="0.000" /><testcase classname="tests.test_state" name="test_hash_with_custom_objects" time="0.000" /><testcase classname="tests.test_state" name="test_hash_performance_with_many_attributes" time="0.001" /><testcase classname="tests.test_state" name="test_hash_with_nested_unhashable_collections" time="0.000" /><testcase classname="tests.test_state" name="test_hash_stability_across_instances" time="0.000" /><testcase classname="tests.test_state" name="test_hash_excludes_missing_values" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_basic_types" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_none_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_missing_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_literal_type" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_enum_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_sequence_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_set_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_mapping_type" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_tuple_type" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_union_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_callable_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_typed_dict" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_state_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_complex_types" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_recursive_state" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_generic_state" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validation_error_messages" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validation_any_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_with_defaults" time="0.000" /><testcase classname="tests.test_state_validation" name="test_attribute_validator_direct_usage" time="0.000" /><testcase classname="tests.test_state_validation" name="test_unsupported_type_annotation" time="0.000" /><testcase classname="tests.test_streaming" name="test_fails_when_generator_fails" time="0.001" /><testcase classname="tests.test_streaming" name="test_cancels_when_iteration_cancels" time="0.001" /><testcase classname="tests.test_streaming" name="test_ends_when_generator_ends" time="0.001" /><testcase classname="tests.test_streaming" name="test_delivers_updates_when_generating" time="0.001" /><testcase classname="tests.test_streaming" name="test_streaming_context_variables_access_is_preserved" time="0.001" /><testcase classname="tests.test_streaming" name="test_nested_streaming_streams_correctly" time="0.001" /><testcase classname="tests.test_timeout" name="test_returns_result_when_returning_value" time="0.001" /><testcase classname="tests.test_timeout" name="test_raises_with_error" time="0.001" /><testcase classname="tests.test_timeout" name="test_raises_with_cancel" time="0.011" /><testcase classname="tests.test_timeout" name="test_raises_with_timeout" time="0.011" /></testsuite></testsuites>
1
+ <?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="180" time="3.770" timestamp="2025-07-07T16:20:23.505267+00:00" hostname="pkrvmbietmlfzoi"><testcase classname="tests.test_async_queue" name="test_fails_when_stream_fails" time="0.001" /><testcase classname="tests.test_async_queue" name="test_cancels_when_iteration_cancels" time="0.001" /><testcase classname="tests.test_async_queue" name="test_ends_when_stream_ends" time="0.001" /><testcase classname="tests.test_async_queue" name="test_buffers_values_when_not_reading" time="0.001" /><testcase classname="tests.test_async_queue" name="test_delivers_buffer_when_streaming_fails" time="0.001" /><testcase classname="tests.test_async_queue" name="test_delivers_updates_when_sending" time="0.001" /><testcase classname="tests.test_async_queue" name="test_fails_when_sending_to_finished" time="0.001" /><testcase classname="tests.test_async_queue" name="test_ignores_when_finishing_when_finished" time="0.001" /><testcase classname="tests.test_async_stream" name="test_fails_when_stream_fails" time="0.001" /><testcase classname="tests.test_async_stream" name="test_cancels_when_iteration_cancels" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ends_when_stream_ends" time="0.001" /><testcase classname="tests.test_async_stream" name="test_finishes_without_buffer" time="0.001" /><testcase classname="tests.test_async_stream" name="test_fails_without_buffer" time="0.001" /><testcase classname="tests.test_async_stream" name="test_delivers_updates_when_sending" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ignores_when_sending_to_finished" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ignores_when_sending_to_failed" time="0.001" /><testcase classname="tests.test_async_stream" name="test_ignores_when_finishing_when_finished" time="0.001" /><testcase classname="tests.test_async_stream" name="test_delivers_all_when_sending_async" time="0.001" /><testcase classname="tests.test_attribute_path" name="test_id_path_points_to_self" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_attribute_path_points_to_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_nested_attribute_path_points_to_nested_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_recursive_attribute_path_points_to_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_list_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_tuple_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_mixed_tuple_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_dict_item_path_points_to_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_id_path_set_updates_self" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_attribute_path_set_updates_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_nested_attribute_path_set_updates_nested_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_recursive_attribute_set_updates_attribute" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_list_item_path_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_tuple_item_path_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_mixed_tuple_item_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_path" name="test_dict_item_path_set_updates_item" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_equal_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_not_equal_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_contains_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_contains_any_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_contained_in_requirement" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_logical_and_or_requirements" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_filter" time="0.000" /><testcase classname="tests.test_attribute_requirement" name="test_immutability" time="0.000" /><testcase classname="tests.test_auto_retry" name="test_returns_value_without_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_retries_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_logs_issue_with_errors" time="0.005" /><testcase classname="tests.test_auto_retry" name="test_fails_with_exceeding_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_fails_with_cancellation" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_retries_with_selected_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_fails_with_not_selected_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_returns_value_without_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_retries_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_with_exceeding_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_with_cancellation" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_when_cancelled" time="0.021" /><testcase classname="tests.test_auto_retry" name="test_async_uses_delay_with_errors" time="0.102" /><testcase classname="tests.test_auto_retry" name="test_async_uses_computed_delay_with_errors" time="0.107" /><testcase classname="tests.test_auto_retry" name="test_async_logs_issue_with_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_retries_with_selected_errors" time="0.001" /><testcase classname="tests.test_auto_retry" name="test_async_fails_with_not_selected_errors" time="0.001" /><testcase classname="tests.test_cache" name="test_returns_cache_value_with_same_argument" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_fresh_value_with_different_argument" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_fresh_value_with_limit_exceed" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_same_value_with_repeating_argument" time="0.000" /><testcase classname="tests.test_cache" name="test_fails_with_error" time="0.000" /><testcase classname="tests.test_cache" name="test_returns_fresh_value_with_expiration_time_exceed" time="0.021" /><testcase classname="tests.test_cache" name="test_async_returns_cache_value_with_same_argument" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_fresh_value_with_different_argument" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_fresh_value_with_limit_exceed" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_same_value_with_repeating_argument" time="0.001" /><testcase classname="tests.test_cache" name="test_async_returns_fresh_value_with_expiration_time_exceed" time="0.021" /><testcase classname="tests.test_cache" name="test_async_cancel_waiting_does_not_cancel_task" time="0.502" /><testcase classname="tests.test_cache" name="test_async_expiration_does_not_cancel_task" time="0.021" /><testcase classname="tests.test_cache" name="test_async_fails_with_error" time="0.001" /><testcase classname="tests.test_context" name="test_state_is_available_according_to_context" time="0.001" /><testcase classname="tests.test_context" name="test_state_update_updates_local_context" time="0.001" /><testcase classname="tests.test_context" name="test_exceptions_are_propagated" time="0.001" /><testcase classname="tests.test_disposables" name="test_empty_initialization" time="0.000" /><testcase classname="tests.test_disposables" name="test_single_disposable_initialization" time="0.000" /><testcase classname="tests.test_disposables" name="test_multiple_disposables_initialization" time="0.000" /><testcase classname="tests.test_disposables" name="test_cannot_set_attributes" time="0.000" /><testcase classname="tests.test_disposables" name="test_cannot_delete_attributes" time="0.000" /><testcase classname="tests.test_disposables" name="test_empty_disposables_is_falsy" time="0.000" /><testcase classname="tests.test_disposables" name="test_non_empty_disposables_is_truthy" time="0.000" /><testcase classname="tests.test_disposables" name="test_setup_with_no_disposables" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_disposable_returning_none" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_disposable_returning_single_state" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_disposable_returning_multiple_states" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_with_multiple_disposables_mixed_returns" time="0.001" /><testcase classname="tests.test_disposables" name="test_setup_sets_loop_correctly" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_no_disposables" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_successful_cleanup" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_exception_context" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_multiple_exceptions_creates_group" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_with_single_exception_is_risen" time="0.001" /><testcase classname="tests.test_disposables" name="test_dispose_resets_loop_even_on_exception" time="0.001" /><testcase classname="tests.test_disposables" name="test_same_loop_cleanup" time="0.001" /><testcase classname="tests.test_disposables" name="test_with_real_async_context_managers" time="0.001" /><testcase classname="tests.test_disposables" name="test_nested_disposables_usage" time="0.001" /><testcase classname="tests.test_disposables" name="test_exception_during_setup_phase" time="0.001" /><testcase classname="tests.test_disposables" name="test_assertion_on_doubleprepare" time="0.001" /><testcase classname="tests.test_disposables" name="test_assertion_on_dispose_withoutprepare" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_processes_all_elements" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_preserves_order" time="0.302" /><testcase classname="tests.test_execute_concurrently" name="test_handles_empty_collection" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_propagates_handler_exceptions" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_returns_exceptions_when_configured" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_cancels_running_tasks_on_cancellation" time="0.101" /><testcase classname="tests.test_execute_concurrently" name="test_respects_concurrency_limit" time="0.202" /><testcase classname="tests.test_execute_concurrently" name="test_works_with_different_types" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_handles_mixed_success_and_failure" time="0.002" /><testcase classname="tests.test_execute_concurrently" name="test_works_with_sets_and_tuples" time="0.001" /><testcase classname="tests.test_execute_concurrently" name="test_exception_details_preserved" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_processes_all_elements" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_processes_elements_concurrently" time="0.302" /><testcase classname="tests.test_process_concurrently" name="test_handles_empty_source" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_propagates_handler_exceptions" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_ignores_handler_exceptions_when_configured" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_handles_source_exception" time="0.001" /><testcase classname="tests.test_process_concurrently" name="test_cancels_running_tasks_on_cancellation" time="0.101" /><testcase classname="tests.test_process_concurrently" name="test_respects_concurrency_limit" time="0.202" /><testcase classname="tests.test_process_concurrently" name="test_processes_elements_from_queue" time="0.052" /><testcase classname="tests.test_state" name="test_basic_initializes_with_arguments" time="0.002" /><testcase classname="tests.test_state" name="test_basic_initializes_with_defaults" time="0.001" /><testcase classname="tests.test_state" name="test_basic_equals_checks_properties" time="0.000" /><testcase classname="tests.test_state" name="test_basic_initializes_with_arguments_and_defaults" time="0.000" /><testcase classname="tests.test_state" name="test_parametrized_initializes_with_proper_parameters" time="0.000" /><testcase classname="tests.test_state" name="test_nested_initializes_with_proper_arguments" time="0.001" /><testcase classname="tests.test_state" name="test_dict_skips_missing_properties" time="0.000" /><testcase classname="tests.test_state" name="test_initialization_allows_missing_properties" time="0.000" /><testcase classname="tests.test_state" name="test_generic_subtypes_validation" time="0.002" /><testcase classname="tests.test_state" name="test_copying_leaves_same_object" time="0.000" /><testcase classname="tests.test_state" name="test_hash_consistency_with_missing_values" time="0.000" /><testcase classname="tests.test_state" name="test_hash_with_unhashable_attributes" time="0.001" /><testcase classname="tests.test_state" name="test_hash_with_dict_key_order_independence" time="0.000" /><testcase classname="tests.test_state" name="test_hash_with_custom_objects" time="0.000" /><testcase classname="tests.test_state" name="test_hash_performance_with_many_attributes" time="0.001" /><testcase classname="tests.test_state" name="test_hash_with_nested_unhashable_collections" time="0.000" /><testcase classname="tests.test_state" name="test_hash_stability_across_instances" time="0.000" /><testcase classname="tests.test_state" name="test_hash_excludes_missing_values" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_basic_types" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_none_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_missing_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_literal_type" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_enum_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_sequence_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_set_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_mapping_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_tuple_type" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_union_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_callable_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_typed_dict" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_state_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_complex_types" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_recursive_state" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_generic_state" time="0.002" /><testcase classname="tests.test_state_validation" name="test_validation_error_messages" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validation_any_type" time="0.000" /><testcase classname="tests.test_state_validation" name="test_validator_with_defaults" time="0.000" /><testcase classname="tests.test_state_validation" name="test_attribute_validator_direct_usage" time="0.000" /><testcase classname="tests.test_state_validation" name="test_unsupported_type_annotation" time="0.000" /><testcase classname="tests.test_stream_concurrently" name="test_merges_two_streams" time="0.001" /><testcase classname="tests.test_stream_concurrently" name="test_interleaves_based_on_timing" time="0.152" /><testcase classname="tests.test_stream_concurrently" name="test_handles_empty_iterators" time="0.001" /><testcase classname="tests.test_stream_concurrently" name="test_handles_different_lengths" time="0.001" /><testcase classname="tests.test_stream_concurrently" name="test_propagates_exceptions_from_source_a" time="0.001" /><testcase classname="tests.test_stream_concurrently" name="test_propagates_exceptions_from_source_b" time="0.017" /><testcase classname="tests.test_stream_concurrently" name="test_cancellation_cancels_both_sources" time="0.202" /><testcase classname="tests.test_stream_concurrently" name="test_works_with_different_types" time="0.001" /><testcase classname="tests.test_stream_concurrently" name="test_immediate_yield" time="0.601" /><testcase classname="tests.test_stream_concurrently" name="test_concurrent_execution" time="0.201" /><testcase classname="tests.test_streaming" name="test_fails_when_generator_fails" time="0.001" /><testcase classname="tests.test_streaming" name="test_cancels_when_iteration_cancels" time="0.001" /><testcase classname="tests.test_streaming" name="test_ends_when_generator_ends" time="0.001" /><testcase classname="tests.test_streaming" name="test_delivers_updates_when_generating" time="0.001" /><testcase classname="tests.test_streaming" name="test_streaming_context_variables_access_is_preserved" time="0.001" /><testcase classname="tests.test_streaming" name="test_nested_streaming_streams_correctly" time="0.001" /><testcase classname="tests.test_timeout" name="test_returns_result_when_returning_value" time="0.001" /><testcase classname="tests.test_timeout" name="test_raises_with_error" time="0.001" /><testcase classname="tests.test_timeout" name="test_raises_with_cancel" time="0.011" /><testcase classname="tests.test_timeout" name="test_raises_with_timeout" time="0.011" /></testsuite></testsuites>
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "haiway"
7
7
  description = "Framework for dependency injection and state management within structured concurrency model."
8
- version = "0.24.0"
8
+ version = "0.24.2"
9
9
  readme = "README.md"
10
10
  maintainers = [
11
11
  { name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" },
@@ -18,8 +18,10 @@ from haiway.helpers import (
18
18
  LoggerObservability,
19
19
  asynchronous,
20
20
  cache,
21
+ execute_concurrently,
21
22
  process_concurrently,
22
23
  retry,
24
+ stream_concurrently,
23
25
  throttle,
24
26
  timeout,
25
27
  traced,
@@ -90,6 +92,7 @@ __all__ = (
90
92
  "asynchronous",
91
93
  "cache",
92
94
  "ctx",
95
+ "execute_concurrently",
93
96
  "getenv",
94
97
  "getenv_base64",
95
98
  "getenv_bool",
@@ -103,6 +106,7 @@ __all__ = (
103
106
  "process_concurrently",
104
107
  "retry",
105
108
  "setup_logging",
109
+ "stream_concurrently",
106
110
  "throttle",
107
111
  "timeout",
108
112
  "traced",
@@ -1,6 +1,10 @@
1
1
  from haiway.helpers.asynchrony import asynchronous
2
2
  from haiway.helpers.caching import CacheMakeKey, CacheRead, CacheWrite, cache
3
- from haiway.helpers.concurrent import process_concurrently
3
+ from haiway.helpers.concurrent import (
4
+ execute_concurrently,
5
+ process_concurrently,
6
+ stream_concurrently,
7
+ )
4
8
  from haiway.helpers.files import File, FileAccess
5
9
  from haiway.helpers.observability import LoggerObservability
6
10
  from haiway.helpers.retries import retry
@@ -17,8 +21,10 @@ __all__ = (
17
21
  "LoggerObservability",
18
22
  "asynchronous",
19
23
  "cache",
24
+ "execute_concurrently",
20
25
  "process_concurrently",
21
26
  "retry",
27
+ "stream_concurrently",
22
28
  "throttle",
23
29
  "timeout",
24
30
  "traced",
@@ -0,0 +1,394 @@
1
+ from asyncio import ALL_COMPLETED, FIRST_COMPLETED, CancelledError, Task, wait
2
+ from collections.abc import (
3
+ AsyncIterable,
4
+ AsyncIterator,
5
+ Callable,
6
+ Collection,
7
+ Coroutine,
8
+ MutableSequence,
9
+ Sequence,
10
+ )
11
+ from typing import Any, Literal, overload
12
+
13
+ from haiway.context import ctx
14
+
15
+ __all__ = (
16
+ "execute_concurrently",
17
+ "process_concurrently",
18
+ "stream_concurrently",
19
+ )
20
+
21
+
22
+ async def process_concurrently[Element]( # noqa: C901, PLR0912
23
+ source: AsyncIterator[Element],
24
+ /,
25
+ handler: Callable[[Element], Coroutine[Any, Any, None]],
26
+ *,
27
+ concurrent_tasks: int = 2,
28
+ ignore_exceptions: bool = False,
29
+ ) -> None:
30
+ """Process elements from an async iterator concurrently.
31
+
32
+ Consumes elements from an async iterator and processes them using the provided
33
+ handler function. Processing happens concurrently with a configurable maximum
34
+ number of concurrent tasks. Elements are processed as they become available,
35
+ maintaining the specified concurrency limit.
36
+
37
+ The function continues until the source iterator is exhausted. If the function
38
+ is cancelled, all running tasks are also cancelled. When ignore_exceptions is
39
+ False, the first exception encountered will stop processing and propagate.
40
+
41
+ Parameters
42
+ ----------
43
+ source : AsyncIterator[Element]
44
+ An async iterator providing elements to process. Elements are consumed
45
+ one at a time as processing slots become available.
46
+ handler : Callable[[Element], Coroutine[Any, Any, None]]
47
+ A coroutine function that processes each element. The handler should
48
+ not return a value (returns None).
49
+ concurrent_tasks : int, default=2
50
+ Maximum number of concurrent tasks. Must be greater than 0. Higher
51
+ values allow more parallelism but consume more resources.
52
+ ignore_exceptions : bool, default=False
53
+ If True, exceptions from handler tasks will be logged but not propagated,
54
+ allowing processing to continue. If False, the first exception stops
55
+ all processing.
56
+
57
+ Raises
58
+ ------
59
+ CancelledError
60
+ If the function is cancelled, propagated after cancelling all running tasks.
61
+ Exception
62
+ Any exception raised by handler tasks when ignore_exceptions is False.
63
+
64
+ Examples
65
+ --------
66
+ >>> async def process_item(item: str) -> None:
67
+ ... await some_async_operation(item)
68
+ ...
69
+ >>> async def items() -> AsyncIterator[str]:
70
+ ... for i in range(10):
71
+ ... yield f"item_{i}"
72
+ ...
73
+ >>> await process_concurrently(
74
+ ... items(),
75
+ ... process_item,
76
+ ... concurrent_tasks=5
77
+ ... )
78
+
79
+ """
80
+ assert concurrent_tasks > 0 # nosec: B101
81
+ running: set[Task[None]] = set()
82
+ try:
83
+ while True:
84
+ element: Element = await anext(source)
85
+ running.add(ctx.spawn(handler, element))
86
+ if len(running) < concurrent_tasks:
87
+ continue # keep spawning tasks
88
+
89
+ completed, running = await wait(
90
+ running,
91
+ return_when=FIRST_COMPLETED,
92
+ )
93
+
94
+ for task in completed:
95
+ if exc := task.exception():
96
+ if not ignore_exceptions:
97
+ raise exc
98
+
99
+ ctx.log_error(
100
+ f"Concurrent processing error - {type(exc)}: {exc}",
101
+ exception=exc,
102
+ )
103
+
104
+ except CancelledError as exc:
105
+ # Cancel all running tasks
106
+ for task in running:
107
+ task.cancel()
108
+
109
+ raise exc
110
+
111
+ except StopAsyncIteration:
112
+ pass # just stop and proceed to finally
113
+
114
+ finally:
115
+ if running:
116
+ completed, _ = await wait(
117
+ running,
118
+ return_when=ALL_COMPLETED,
119
+ )
120
+ for task in completed:
121
+ if exc := task.exception():
122
+ if not ignore_exceptions:
123
+ raise exc
124
+
125
+ ctx.log_error(
126
+ f"Concurrent processing error - {type(exc)}: {exc}",
127
+ exception=exc,
128
+ )
129
+
130
+
131
+ @overload
132
+ async def execute_concurrently[Element, Result](
133
+ source: Collection[Element],
134
+ /,
135
+ handler: Callable[[Element], Coroutine[Any, Any, Result]],
136
+ *,
137
+ concurrent_tasks: int = 2,
138
+ ) -> Sequence[Result]: ...
139
+
140
+
141
+ @overload
142
+ async def execute_concurrently[Element, Result](
143
+ source: Collection[Element],
144
+ /,
145
+ handler: Callable[[Element], Coroutine[Any, Any, Result]],
146
+ *,
147
+ concurrent_tasks: int = 2,
148
+ return_exceptions: Literal[True],
149
+ ) -> Sequence[Result | BaseException]: ...
150
+
151
+
152
+ async def execute_concurrently[Element, Result]( # noqa: C901
153
+ source: Collection[Element],
154
+ /,
155
+ handler: Callable[[Element], Coroutine[Any, Any, Result]],
156
+ *,
157
+ concurrent_tasks: int = 2,
158
+ return_exceptions: bool = False,
159
+ ) -> Sequence[Result | BaseException] | Sequence[Result]:
160
+ """Execute handler for each element from a collection concurrently.
161
+
162
+ Processes all elements from a collection using the provided handler function,
163
+ executing multiple handlers concurrently up to the specified limit. Results
164
+ are collected and returned in the same order as the input elements.
165
+
166
+ Unlike `process_concurrently`, this function:
167
+ - Works with collections (known size) rather than async iterators
168
+ - Returns results from each handler invocation
169
+ - Preserves the order of results to match input order
170
+
171
+ The function ensures all tasks complete before returning. If cancelled,
172
+ all running tasks are cancelled before propagating the cancellation.
173
+
174
+ Parameters
175
+ ----------
176
+ source : Collection[Element]
177
+ A collection of elements to process. The collection size determines
178
+ the result sequence length.
179
+ handler : Callable[[Element], Coroutine[Any, Any, Result]]
180
+ A coroutine function that processes each element and returns a result.
181
+ concurrent_tasks : int, default=2
182
+ Maximum number of concurrent tasks. Must be greater than 0. Higher
183
+ values allow more parallelism but consume more resources.
184
+ return_exceptions : bool, default=False
185
+ If True, exceptions from handler tasks are included in the results
186
+ as BaseException instances. If False, the first exception stops
187
+ processing and is raised.
188
+
189
+ Returns
190
+ -------
191
+ Sequence[Result] or Sequence[Result | BaseException]
192
+ Results from each handler invocation, in the same order as input elements.
193
+ If return_exceptions is True, failed tasks return BaseException instances.
194
+
195
+ Raises
196
+ ------
197
+ CancelledError
198
+ If the function is cancelled, propagated after cancelling all running tasks.
199
+ Exception
200
+ Any exception raised by handler tasks when return_exceptions is False.
201
+
202
+ Examples
203
+ --------
204
+ >>> async def fetch_data(url: str) -> dict:
205
+ ... return await http_client.get(url)
206
+ ...
207
+ >>> urls = ["http://api.example.com/1", "http://api.example.com/2"]
208
+ >>> results = await execute_concurrently(
209
+ ... urls,
210
+ ... fetch_data,
211
+ ... concurrent_tasks=10
212
+ ... )
213
+ >>> # results[0] corresponds to urls[0], results[1] to urls[1], etc.
214
+
215
+ >>> # With exception handling
216
+ >>> results = await execute_concurrently(
217
+ ... urls,
218
+ ... fetch_data,
219
+ ... concurrent_tasks=10,
220
+ ... return_exceptions=True
221
+ ... )
222
+ >>> for url, result in zip(urls, results):
223
+ ... if isinstance(result, BaseException):
224
+ ... print(f"Failed to fetch {url}: {result}")
225
+ ... else:
226
+ ... print(f"Got data from {url}")
227
+
228
+ """
229
+ assert concurrent_tasks > 0 # nosec: B101
230
+ running: set[Task[Result]] = set()
231
+ results: MutableSequence[Task[Result]] = []
232
+ try:
233
+ for element in source:
234
+ task: Task[Result] = ctx.spawn(handler, element)
235
+ results.append(task)
236
+ running.add(task)
237
+ if len(running) < concurrent_tasks:
238
+ continue # keep spawning tasks
239
+
240
+ completed, running = await wait(
241
+ running,
242
+ return_when=FIRST_COMPLETED,
243
+ )
244
+
245
+ for task in completed:
246
+ if exc := task.exception():
247
+ if not return_exceptions:
248
+ raise exc
249
+
250
+ ctx.log_error(
251
+ f"Concurrent execution error - {type(exc)}: {exc}",
252
+ exception=exc,
253
+ )
254
+
255
+ except CancelledError as exc:
256
+ # Cancel all running tasks
257
+ for task in running:
258
+ task.cancel()
259
+
260
+ raise exc
261
+
262
+ finally:
263
+ if running:
264
+ completed, _ = await wait(
265
+ running,
266
+ return_when=ALL_COMPLETED,
267
+ )
268
+ for task in completed:
269
+ if exc := task.exception():
270
+ if not return_exceptions:
271
+ raise exc
272
+
273
+ ctx.log_error(
274
+ f"Concurrent execution error - {type(exc)}: {exc}",
275
+ exception=exc,
276
+ )
277
+
278
+ return [result.exception() or result.result() for result in results]
279
+
280
+
281
+ async def stream_concurrently[ElementA, ElementB]( # noqa: C901
282
+ source_a: AsyncIterator[ElementA],
283
+ source_b: AsyncIterator[ElementB],
284
+ /,
285
+ ) -> AsyncIterable[ElementA | ElementB]:
286
+ """Merge streams from two async iterators processed concurrently.
287
+
288
+ Concurrently consumes elements from two async iterators and yields them
289
+ as they become available. Elements from both sources are interleaved based
290
+ on which iterator produces them first. The function continues until both
291
+ iterators are exhausted.
292
+
293
+ This is useful for combining multiple async data sources into a single
294
+ stream while maintaining concurrency. Each iterator is polled independently,
295
+ and whichever has data available first will have its element yielded.
296
+
297
+ Parameters
298
+ ----------
299
+ source_a : AsyncIterator[ElementA]
300
+ First async iterator to consume from.
301
+ source_b : AsyncIterator[ElementB]
302
+ Second async iterator to consume from.
303
+
304
+ Yields
305
+ ------
306
+ ElementA | ElementB
307
+ Elements from either source as they become available. The order
308
+ depends on which iterator produces elements first.
309
+
310
+ Raises
311
+ ------
312
+ CancelledError
313
+ If the async generator is cancelled, both source tasks are cancelled
314
+ before propagating the cancellation.
315
+ Exception
316
+ Any exception raised by either source iterator.
317
+
318
+ Examples
319
+ --------
320
+ >>> async def numbers() -> AsyncIterator[int]:
321
+ ... for i in range(5):
322
+ ... await asyncio.sleep(0.1)
323
+ ... yield i
324
+ ...
325
+ >>> async def letters() -> AsyncIterator[str]:
326
+ ... for c in "abcde":
327
+ ... await asyncio.sleep(0.15)
328
+ ... yield c
329
+ ...
330
+ >>> async for item in stream_concurrently(numbers(), letters()):
331
+ ... print(item) # Prints interleaved numbers and letters
332
+
333
+ Notes
334
+ -----
335
+ The function maintains exactly one pending task per iterator at all times,
336
+ ensuring efficient resource usage while maximizing throughput from both
337
+ sources.
338
+
339
+ """
340
+
341
+ async def next_a() -> ElementA:
342
+ return await anext(source_a)
343
+
344
+ async def next_b() -> ElementB:
345
+ return await anext(source_b)
346
+
347
+ task_a: Task[ElementA] = ctx.spawn(next_a)
348
+ task_b: Task[ElementB] = ctx.spawn(next_b)
349
+
350
+ try:
351
+ while not ( # Continue until both iterators are exhausted
352
+ task_a.done()
353
+ and task_b.done()
354
+ and isinstance(task_a.exception(), StopAsyncIteration)
355
+ and isinstance(task_b.exception(), StopAsyncIteration)
356
+ ):
357
+ # Wait for at least one task to complete
358
+ done, _ = await wait(
359
+ {task_a, task_b},
360
+ return_when=FIRST_COMPLETED,
361
+ )
362
+
363
+ # Process completed tasks
364
+ for task in done:
365
+ if task is task_a:
366
+ exc: BaseException | None = task.exception()
367
+ if exc is None:
368
+ yield task.result()
369
+ task_a = ctx.spawn(next_a)
370
+
371
+ elif not isinstance(exc, StopAsyncIteration):
372
+ raise exc
373
+ # If StopAsyncIteration, don't respawn task_a
374
+
375
+ elif task is task_b:
376
+ exc: BaseException | None = task.exception()
377
+ if exc is None:
378
+ yield task.result()
379
+ task_b = ctx.spawn(next_b)
380
+
381
+ elif not isinstance(exc, StopAsyncIteration):
382
+ raise exc
383
+ # If StopAsyncIteration, don't respawn task_b
384
+
385
+ except CancelledError as exc:
386
+ # Cancel all running tasks
387
+ task_a.cancel()
388
+ task_b.cancel()
389
+ raise exc
390
+
391
+ finally:
392
+ # Ensure cleanup of any remaining tasks
393
+ for task in (task_a, task_b):
394
+ task.cancel()