haiway 0.21.4__tar.gz → 0.22.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.
Files changed (117) hide show
  1. {haiway-0.21.4 → haiway-0.22.0}/PKG-INFO +2 -2
  2. haiway-0.22.0/junit/test-results.xml +1 -0
  3. {haiway-0.21.4 → haiway-0.22.0}/pyproject.toml +2 -2
  4. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/__init__.py +4 -0
  5. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/access.py +113 -6
  6. haiway-0.22.0/src/haiway/context/disposables.py +357 -0
  7. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/state.py +86 -12
  8. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/__init__.py +3 -0
  9. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/asynchrony.py +4 -6
  10. haiway-0.22.0/src/haiway/helpers/files.py +421 -0
  11. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/state/structure.py +8 -0
  12. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/types/missing.py +3 -0
  13. haiway-0.22.0/tests/test_disposables.py +362 -0
  14. haiway-0.22.0/uv.lock +491 -0
  15. haiway-0.21.4/junit/test-results.xml +0 -1
  16. haiway-0.21.4/src/haiway/context/disposables.py +0 -164
  17. haiway-0.21.4/uv.lock +0 -540
  18. {haiway-0.21.4 → haiway-0.22.0}/.github/workflows/ci.yml +0 -0
  19. {haiway-0.21.4 → haiway-0.22.0}/.github/workflows/publish.yml +0 -0
  20. {haiway-0.21.4 → haiway-0.22.0}/.gitignore +0 -0
  21. {haiway-0.21.4 → haiway-0.22.0}/CLAUDE.md +0 -0
  22. {haiway-0.21.4 → haiway-0.22.0}/LICENSE +0 -0
  23. {haiway-0.21.4 → haiway-0.22.0}/Makefile +0 -0
  24. {haiway-0.21.4 → haiway-0.22.0}/README.md +0 -0
  25. {haiway-0.21.4 → haiway-0.22.0}/config/pre-push +0 -0
  26. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/.dockerignore +0 -0
  27. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/Dockerfile +0 -0
  28. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/Makefile +0 -0
  29. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/README.md +0 -0
  30. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/config/.env.example +0 -0
  31. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/config/unit.json +0 -0
  32. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/docker-compose.yml +0 -0
  33. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/pyproject.toml +0 -0
  34. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/features/__int__.py +0 -0
  35. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/features/todos/__init__.py +0 -0
  36. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/features/todos/config.py +0 -0
  37. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/features/todos/state.py +0 -0
  38. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/features/todos/types.py +0 -0
  39. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/features/todos/user_tasks.py +0 -0
  40. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/integrations/__init__.py +0 -0
  41. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/integrations/postgres/__init__.py +0 -0
  42. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/integrations/postgres/client.py +0 -0
  43. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/integrations/postgres/config.py +0 -0
  44. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/integrations/postgres/state.py +0 -0
  45. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/integrations/postgres/types.py +0 -0
  46. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/migrations/__init__.py +0 -0
  47. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/migrations/__main__.py +0 -0
  48. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/migrations/postgres/__init__.py +0 -0
  49. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/migrations/postgres/execution.py +0 -0
  50. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/migrations/postgres/migration_0.py +0 -0
  51. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/migrations/postgres/types.py +0 -0
  52. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/__init__.py +0 -0
  53. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/__main__.py +0 -0
  54. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/application.py +0 -0
  55. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/config.py +0 -0
  56. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/middlewares/__init__.py +0 -0
  57. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/middlewares/context.py +0 -0
  58. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/routes/__init__.py +0 -0
  59. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/routes/technical.py +0 -0
  60. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/server/routes/todos.py +0 -0
  61. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/solutions/__init__.py +0 -0
  62. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/solutions/user_tasks/__init__.py +0 -0
  63. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/solutions/user_tasks/config.py +0 -0
  64. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/solutions/user_tasks/postgres.py +0 -0
  65. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/solutions/user_tasks/state.py +0 -0
  66. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/src/solutions/user_tasks/types.py +0 -0
  67. {haiway-0.21.4 → haiway-0.22.0}/examples/fastAPI/uv.lock +0 -0
  68. {haiway-0.21.4 → haiway-0.22.0}/guidelines/functionalities.md +0 -0
  69. {haiway-0.21.4 → haiway-0.22.0}/guidelines/llms.txt +0 -0
  70. {haiway-0.21.4 → haiway-0.22.0}/guidelines/packages.md +0 -0
  71. {haiway-0.21.4 → haiway-0.22.0}/guidelines/state.md +0 -0
  72. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/__init__.py +0 -0
  73. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/identifier.py +0 -0
  74. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/observability.py +0 -0
  75. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/tasks.py +0 -0
  76. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/context/types.py +0 -0
  77. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/caching.py +0 -0
  78. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/concurrent.py +0 -0
  79. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/observability.py +0 -0
  80. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/retries.py +0 -0
  81. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/throttling.py +0 -0
  82. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/timeouted.py +0 -0
  83. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/helpers/tracing.py +0 -0
  84. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/opentelemetry/__init__.py +0 -0
  85. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/opentelemetry/observability.py +0 -0
  86. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/py.typed +0 -0
  87. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/state/__init__.py +0 -0
  88. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/state/attributes.py +0 -0
  89. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/state/path.py +0 -0
  90. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/state/requirement.py +0 -0
  91. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/state/validation.py +0 -0
  92. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/types/__init__.py +0 -0
  93. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/types/default.py +0 -0
  94. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/__init__.py +0 -0
  95. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/always.py +0 -0
  96. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/collections.py +0 -0
  97. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/env.py +0 -0
  98. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/formatting.py +0 -0
  99. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/freezing.py +0 -0
  100. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/logs.py +0 -0
  101. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/mimic.py +0 -0
  102. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/noop.py +0 -0
  103. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/queue.py +0 -0
  104. {haiway-0.21.4 → haiway-0.22.0}/src/haiway/utils/stream.py +0 -0
  105. {haiway-0.21.4 → haiway-0.22.0}/tests/__init__.py +0 -0
  106. {haiway-0.21.4 → haiway-0.22.0}/tests/test_async_queue.py +0 -0
  107. {haiway-0.21.4 → haiway-0.22.0}/tests/test_async_stream.py +0 -0
  108. {haiway-0.21.4 → haiway-0.22.0}/tests/test_attribute_path.py +0 -0
  109. {haiway-0.21.4 → haiway-0.22.0}/tests/test_attribute_requirement.py +0 -0
  110. {haiway-0.21.4 → haiway-0.22.0}/tests/test_auto_retry.py +0 -0
  111. {haiway-0.21.4 → haiway-0.22.0}/tests/test_cache.py +0 -0
  112. {haiway-0.21.4 → haiway-0.22.0}/tests/test_context.py +0 -0
  113. {haiway-0.21.4 → haiway-0.22.0}/tests/test_process_concurrently.py +0 -0
  114. {haiway-0.21.4 → haiway-0.22.0}/tests/test_state.py +0 -0
  115. {haiway-0.21.4 → haiway-0.22.0}/tests/test_state_validation.py +0 -0
  116. {haiway-0.21.4 → haiway-0.22.0}/tests/test_streaming.py +0 -0
  117. {haiway-0.21.4 → haiway-0.22.0}/tests/test_timeout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.21.4
3
+ Version: 0.22.0
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
@@ -40,7 +40,7 @@ Requires-Dist: pyright~=1.1; extra == 'dev'
40
40
  Requires-Dist: pytest-asyncio~=0.26; extra == 'dev'
41
41
  Requires-Dist: pytest-cov~=6.1; extra == 'dev'
42
42
  Requires-Dist: pytest~=8.3; extra == 'dev'
43
- Requires-Dist: ruff~=0.11; extra == 'dev'
43
+ Requires-Dist: ruff~=0.12; extra == 'dev'
44
44
  Provides-Extra: opentelemetry
45
45
  Requires-Dist: opentelemetry-api~=1.33; extra == 'opentelemetry'
46
46
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc~=1.33; extra == 'opentelemetry'
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="151" time="1.947" timestamp="2025-06-18T09:04:02.496497+00:00" hostname="pkrvmxyh4eaekms"><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.002" /><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.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_validation" name="test_validator_basic_types" time="0.001" /><testcase classname="tests.test_state_validation" name="test_validator_none_type" time="0.001" /><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.000" /><testcase classname="tests.test_state_validation" name="test_validator_union_type" time="0.001" /><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.001" /><testcase classname="tests.test_state_validation" name="test_validation_error_messages" time="0.001" /><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.012" /><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.21.4"
8
+ version = "0.22.0"
9
9
  readme = "README.md"
10
10
  maintainers = [
11
11
  { name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" },
@@ -38,7 +38,7 @@ dev = [
38
38
  "pytest~=8.3",
39
39
  "pytest-asyncio~=0.26",
40
40
  "pytest-cov~=6.1",
41
- "ruff~=0.11",
41
+ "ruff~=0.12",
42
42
  ]
43
43
 
44
44
  [tool.ruff]
@@ -13,6 +13,8 @@ from haiway.context import (
13
13
  ctx,
14
14
  )
15
15
  from haiway.helpers import (
16
+ File,
17
+ FileAccess,
16
18
  LoggerObservability,
17
19
  asynchronous,
18
20
  cache,
@@ -63,6 +65,8 @@ __all__ = (
63
65
  "DefaultValue",
64
66
  "Disposable",
65
67
  "Disposables",
68
+ "File",
69
+ "FileAccess",
66
70
  "LoggerObservability",
67
71
  "Missing",
68
72
  "MissingContext",
@@ -169,8 +169,8 @@ class ScopeContext:
169
169
  StateContext(
170
170
  state=ScopeState(
171
171
  (
172
- *await disposables.__aenter__(),
173
172
  *self._state_context._state._state.values(),
173
+ *await disposables.prepare(),
174
174
  )
175
175
  ),
176
176
  ),
@@ -187,7 +187,7 @@ class ScopeContext:
187
187
  exc_tb: TracebackType | None,
188
188
  ) -> None:
189
189
  if disposables := self._disposables:
190
- await disposables.__aexit__(
190
+ await disposables.dispose(
191
191
  exc_type=exc_type,
192
192
  exc_val=exc_val,
193
193
  exc_tb=exc_tb,
@@ -382,6 +382,48 @@ class ctx:
382
382
 
383
383
  return StateContext.updated(element for element in state if element is not None)
384
384
 
385
+ @staticmethod
386
+ def disposables(
387
+ *disposables: Disposable | None,
388
+ ) -> Disposables:
389
+ """
390
+ Create a container for managing multiple disposable resources.
391
+
392
+ Disposables are async context managers that can provide state objects and
393
+ require proper cleanup. This method creates a Disposables container that
394
+ manages multiple disposable resources as a single unit, handling their
395
+ lifecycle and state propagation.
396
+
397
+ Parameters
398
+ ----------
399
+ *disposables: Disposable | None
400
+ Variable number of disposable resources to be managed together.
401
+ None values are filtered out automatically.
402
+
403
+ Returns
404
+ -------
405
+ Disposables
406
+ A container that manages the lifecycle of all provided disposables
407
+ and propagates their state to the context when used with ctx.scope()
408
+
409
+ Examples
410
+ --------
411
+ Using disposables with database connections:
412
+
413
+ >>> from haiway import ctx
414
+ >>> async def main():
415
+ ...
416
+ ... async with ctx.scope(
417
+ ... "database_work",
418
+ ... disposables=(database_connection(),)
419
+ ... ):
420
+ ... # ConnectionState is now available in context
421
+ ... conn_state = ctx.state(ConnectionState)
422
+ ... await conn_state.connection.execute("SELECT 1")
423
+ """
424
+
425
+ return Disposables(*(disposable for disposable in disposables if disposable is not None))
426
+
385
427
  @staticmethod
386
428
  def spawn[Result, **Arguments](
387
429
  function: Callable[Arguments, Coroutine[Any, Any, Result]],
@@ -493,6 +535,29 @@ class ctx:
493
535
  else:
494
536
  raise RuntimeError("Attempting to cancel context out of asyncio task")
495
537
 
538
+ @staticmethod
539
+ def check_state[StateType: State](
540
+ state: type[StateType],
541
+ /,
542
+ ) -> bool:
543
+ """
544
+ Check if state object is available in the current context.
545
+
546
+ Verifies if state object of the specified type is available the current context.
547
+ Instantiates requested state if needed and possible.
548
+
549
+ Parameters
550
+ ----------
551
+ state: type[StateType]
552
+ The type of state to check
553
+
554
+ Returns
555
+ -------
556
+ bool
557
+ True if state is available, otherwise False.
558
+ """
559
+ return StateContext.check_state(state)
560
+
496
561
  @staticmethod
497
562
  def state[StateType: State](
498
563
  state: type[StateType],
@@ -500,18 +565,60 @@ class ctx:
500
565
  default: StateType | None = None,
501
566
  ) -> StateType:
502
567
  """
503
- Access current scope context state by its type. If there is no matching state defined\
504
- default value will be created if able, an exception will raise otherwise.
568
+ Access state from the current scope context by its type.
569
+
570
+ Retrieves state objects that have been propagated within the current execution context.
571
+ State objects are automatically made available through context scopes and disposables.
572
+ If no matching state is found, creates a default instance if possible.
505
573
 
506
574
  Parameters
507
575
  ----------
508
576
  state: type[StateType]
509
- type of requested state
577
+ The State class type to retrieve from the current context
578
+ default: StateType | None, default=None
579
+ Optional default instance to return if state is not found in context.
580
+ If None and no state is found, a new instance will be created if possible.
510
581
 
511
582
  Returns
512
583
  -------
513
584
  StateType
514
- resolved state instance
585
+ The state instance from the current context or a default/new instance
586
+
587
+ Raises
588
+ ------
589
+ RuntimeError
590
+ If called outside of any scope context
591
+ TypeError
592
+ If no state is found and no default can be created
593
+
594
+ Examples
595
+ --------
596
+ Accessing configuration state:
597
+
598
+ >>> from haiway import ctx, State
599
+ >>>
600
+ >>> class ApiConfig(State):
601
+ ... base_url: str = "https://api.example.com"
602
+ ... timeout: int = 30
603
+ >>>
604
+ >>> async def fetch_data():
605
+ ... config = ctx.state(ApiConfig)
606
+ ... # Use config.base_url and config.timeout
607
+ >>>
608
+ >>> async with ctx.scope("api", ApiConfig(base_url="https://custom.api.com")):
609
+ ... await fetch_data() # Uses custom config
610
+
611
+ Accessing state with default:
612
+
613
+ >>> cache_config = ctx.state(CacheConfig, default=CacheConfig(ttl=3600))
614
+
615
+ Within service classes:
616
+
617
+ >>> class UserService(State):
618
+ ... @classmethod
619
+ ... async def get_user(cls, user_id: str) -> User:
620
+ ... config = ctx.state(DatabaseConfig)
621
+ ... # Use config to connect to database
515
622
  """
516
623
  return StateContext.state(
517
624
  state,
@@ -0,0 +1,357 @@
1
+ from asyncio import (
2
+ AbstractEventLoop,
3
+ gather,
4
+ get_running_loop,
5
+ iscoroutinefunction,
6
+ run_coroutine_threadsafe,
7
+ wrap_future,
8
+ )
9
+ from collections.abc import Callable, Coroutine, Iterable
10
+ from contextlib import AbstractAsyncContextManager
11
+ from itertools import chain
12
+ from types import TracebackType
13
+ from typing import Any, final
14
+
15
+ from haiway.context.state import ScopeState, StateContext
16
+ from haiway.state import State
17
+ from haiway.utils.mimic import mimic_function
18
+
19
+ __all__ = (
20
+ "Disposable",
21
+ "Disposables",
22
+ )
23
+
24
+ type Disposable = AbstractAsyncContextManager[Iterable[State] | State | None]
25
+ """
26
+ A type alias for asynchronous context managers that provide disposable resources.
27
+
28
+ Represents an asynchronous resource that needs proper cleanup when no longer needed.
29
+ When entered, it may return State instances that will be automatically propagated
30
+ to the current context. The resource is guaranteed to be properly disposed of
31
+ when the context exits, even if exceptions occur.
32
+
33
+ Type Details
34
+ ------------
35
+ - Must be an async context manager (implements __aenter__ and __aexit__)
36
+ - Can return None, a single State instance, or multiple State instances
37
+ - State instances are automatically added to the context scope
38
+ - Cleanup is handled automatically when the context exits
39
+
40
+ Examples
41
+ --------
42
+ Creating a disposable database connection:
43
+
44
+ >>> import contextlib
45
+ >>> from haiway import State
46
+ >>>
47
+ >>> class DatabaseState(State):
48
+ ... connection: DatabaseConnection
49
+ ...
50
+ >>> @contextlib.asynccontextmanager
51
+ >>> async def database_disposable():
52
+ ... connection = await create_database_connection()
53
+ ... try:
54
+ ... yield DatabaseState(connection=connection)
55
+ ... finally:
56
+ ... await connection.close()
57
+ """
58
+
59
+
60
+ @final
61
+ class Disposables:
62
+ """
63
+ A container for multiple Disposable resources that manages their lifecycle.
64
+
65
+ This class provides a way to handle multiple disposable resources as a single unit,
66
+ entering all of them in parallel when the container is entered and exiting all of
67
+ them when the container is exited. Any states returned by the disposables are
68
+ collected and automatically propagated to the context.
69
+
70
+ Key Features
71
+ ------------
72
+ - Parallel setup and cleanup of all contained disposables
73
+ - Automatic state collection and context propagation
74
+ - Thread-safe cross-event-loop disposal
75
+ - Exception handling with BaseExceptionGroup for multiple failures
76
+ - Immutable after initialization
77
+
78
+ The class is designed to work seamlessly with ctx.scope() and ensures proper
79
+ resource cleanup even when exceptions occur during setup or teardown.
80
+
81
+ Examples
82
+ --------
83
+ Creating and using multiple disposables:
84
+
85
+ >>> from haiway import ctx
86
+ >>> async def main():
87
+ ... disposables = Disposables(
88
+ ... database_disposable(),
89
+ ... cache_disposable()
90
+ ... )
91
+ ...
92
+ ... async with ctx.scope("app", disposables=disposables):
93
+ ... # Both DatabaseState and CacheState are available
94
+ ... db = ctx.state(DatabaseState)
95
+ ... cache = ctx.state(CacheState)
96
+
97
+ Direct context manager usage:
98
+
99
+ >>> async def process_data():
100
+ ... disposables = Disposables(
101
+ ... create_temp_file_disposable(),
102
+ ... create_network_connection_disposable()
103
+ ... )
104
+ ...
105
+ ... async with disposables:
106
+ ... # Resources are set up in parallel
107
+ ... temp_file = ctx.state(TempFileState)
108
+ ... network = ctx.state(NetworkState)
109
+ ...
110
+ ... # Process data using both resources
111
+ ...
112
+ ... # All resources cleaned up automatically
113
+ """
114
+
115
+ __slots__ = (
116
+ "_disposables",
117
+ "_loop",
118
+ "_state_context",
119
+ )
120
+
121
+ def __init__(
122
+ self,
123
+ *disposables: Disposable,
124
+ ) -> None:
125
+ """
126
+ Initialize a collection of disposable resources.
127
+
128
+ Parameters
129
+ ----------
130
+ *disposables: Disposable
131
+ Variable number of disposable resources to be managed together.
132
+ """
133
+ self._disposables: tuple[Disposable, ...]
134
+ object.__setattr__(
135
+ self,
136
+ "_disposables",
137
+ disposables,
138
+ )
139
+ self._state_context: StateContext | None
140
+ object.__setattr__(
141
+ self,
142
+ "_state_context",
143
+ None,
144
+ )
145
+ self._loop: AbstractEventLoop | None
146
+ object.__setattr__(
147
+ self,
148
+ "_loop",
149
+ None,
150
+ )
151
+
152
+ def __setattr__(
153
+ self,
154
+ name: str,
155
+ value: Any,
156
+ ) -> Any:
157
+ raise AttributeError(
158
+ f"Can't modify immutable {self.__class__.__qualname__},"
159
+ f" attribute - '{name}' cannot be modified"
160
+ )
161
+
162
+ def __delattr__(
163
+ self,
164
+ name: str,
165
+ ) -> None:
166
+ raise AttributeError(
167
+ f"Can't modify immutable {self.__class__.__qualname__},"
168
+ f" attribute - '{name}' cannot be deleted"
169
+ )
170
+
171
+ def __bool__(self) -> bool:
172
+ """
173
+ Check if this container has any disposables.
174
+
175
+ Returns
176
+ -------
177
+ bool
178
+ True if there are disposables, False otherwise.
179
+ """
180
+ return len(self._disposables) > 0
181
+
182
+ async def _setup(
183
+ self,
184
+ disposable: Disposable,
185
+ /,
186
+ ) -> Iterable[State]:
187
+ match await disposable.__aenter__():
188
+ case None:
189
+ return ()
190
+
191
+ case State() as single:
192
+ return (single,)
193
+
194
+ case multiple:
195
+ return multiple
196
+
197
+ async def prepare(self) -> Iterable[State]:
198
+ assert self._loop is None # nosec: B101
199
+ object.__setattr__(
200
+ self,
201
+ "_loop",
202
+ get_running_loop(),
203
+ )
204
+ return [
205
+ *chain.from_iterable(
206
+ await gather(
207
+ *[self._setup(disposable) for disposable in self._disposables],
208
+ )
209
+ )
210
+ ]
211
+
212
+ async def __aenter__(self) -> None:
213
+ """
214
+ Enter all contained disposables asynchronously.
215
+
216
+ Enters all disposables in parallel and collects any State objects they return updating
217
+ current state context.
218
+ """
219
+
220
+ assert self._state_context is None, "Context reentrance is not allowed" # nosec: B101
221
+ state_context = StateContext(state=ScopeState(await self.prepare()))
222
+ state_context.__enter__()
223
+ object.__setattr__(
224
+ self,
225
+ "_state_context",
226
+ state_context,
227
+ )
228
+
229
+ async def _cleanup(
230
+ self,
231
+ /,
232
+ exc_type: type[BaseException] | None,
233
+ exc_val: BaseException | None,
234
+ exc_tb: TracebackType | None,
235
+ ) -> list[bool | BaseException | None]:
236
+ return await gather(
237
+ *[
238
+ disposable.__aexit__(
239
+ exc_type,
240
+ exc_val,
241
+ exc_tb,
242
+ )
243
+ for disposable in self._disposables
244
+ ],
245
+ return_exceptions=True,
246
+ )
247
+
248
+ async def dispose(
249
+ self,
250
+ /,
251
+ exc_type: type[BaseException] | None = None,
252
+ exc_val: BaseException | None = None,
253
+ exc_tb: TracebackType | None = None,
254
+ ) -> None:
255
+ assert self._loop is not None # nosec: B101
256
+ results: list[bool | BaseException | None]
257
+
258
+ try:
259
+ current_loop: AbstractEventLoop = get_running_loop()
260
+ if self._loop != current_loop:
261
+ results = await wrap_future(
262
+ run_coroutine_threadsafe(
263
+ self._cleanup(
264
+ exc_type,
265
+ exc_val,
266
+ exc_tb,
267
+ ),
268
+ loop=self._loop,
269
+ )
270
+ )
271
+
272
+ else:
273
+ results = await self._cleanup(
274
+ exc_type,
275
+ exc_val,
276
+ exc_tb,
277
+ )
278
+
279
+ finally:
280
+ object.__setattr__(
281
+ self,
282
+ "_loop",
283
+ None,
284
+ )
285
+
286
+ exceptions: list[BaseException] = [exc for exc in results if isinstance(exc, BaseException)]
287
+
288
+ match len(exceptions):
289
+ case 0:
290
+ return
291
+
292
+ case 1:
293
+ raise exceptions[0]
294
+
295
+ case _:
296
+ raise BaseExceptionGroup("Disposables cleanup errors", exceptions)
297
+
298
+ async def __aexit__(
299
+ self,
300
+ exc_type: type[BaseException] | None,
301
+ exc_val: BaseException | None,
302
+ exc_tb: TracebackType | None,
303
+ ) -> None:
304
+ """
305
+ Exit all contained disposables asynchronously.
306
+
307
+ Properly disposes of all resources by calling their __aexit__ methods in parallel.
308
+ If multiple disposables raise exceptions, they are collected into a BaseExceptionGroup.
309
+
310
+ Parameters
311
+ ----------
312
+ exc_type: type[BaseException] | None
313
+ The type of exception that caused the context to be exited
314
+ exc_val: BaseException | None
315
+ The exception that caused the context to be exited
316
+ exc_tb: TracebackType | None
317
+ The traceback for the exception that caused the context to be exited
318
+
319
+ Raises
320
+ ------
321
+ BaseExceptionGroup
322
+ If multiple disposables raise exceptions during exit
323
+ """
324
+ assert self._state_context is not None, "Unbalanced context enter/exit" # nosec: B101
325
+ try:
326
+ self._state_context.__exit__(
327
+ exc_type,
328
+ exc_val,
329
+ exc_tb,
330
+ )
331
+ object.__setattr__(
332
+ self,
333
+ "_state_context",
334
+ None,
335
+ )
336
+
337
+ finally:
338
+ await self.dispose(
339
+ exc_type,
340
+ exc_val,
341
+ exc_tb,
342
+ )
343
+
344
+ def __call__[Result, **Arguments](
345
+ self,
346
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
347
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]]:
348
+ assert iscoroutinefunction(function) # nosec: B101
349
+
350
+ async def async_context(
351
+ *args: Arguments.args,
352
+ **kwargs: Arguments.kwargs,
353
+ ) -> Result:
354
+ async with self:
355
+ return await function(*args, **kwargs)
356
+
357
+ return mimic_function(function, within=async_context)