haiway 0.13.0__tar.gz → 0.14.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 (105) hide show
  1. {haiway-0.13.0 → haiway-0.14.0}/PKG-INFO +1 -1
  2. haiway-0.14.0/junit/test-results.xml +1 -0
  3. {haiway-0.13.0 → haiway-0.14.0}/pyproject.toml +1 -1
  4. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/__init__.py +2 -0
  5. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/access.py +32 -50
  6. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/state.py +1 -1
  7. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/tasks.py +1 -1
  8. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/__init__.py +4 -1
  9. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/asynchrony.py +18 -16
  10. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/caching.py +12 -9
  11. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/metrics.py +46 -24
  12. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/retries.py +3 -3
  13. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/throttling.py +16 -16
  14. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/timeouted.py +7 -6
  15. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/__init__.py +2 -0
  16. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/always.py +2 -2
  17. haiway-0.14.0/src/haiway/utils/stream.py +97 -0
  18. haiway-0.14.0/tests/test_async_stream.py +138 -0
  19. {haiway-0.13.0 → haiway-0.14.0}/tests/test_streaming.py +5 -11
  20. {haiway-0.13.0 → haiway-0.14.0}/uv.lock +1 -1
  21. haiway-0.13.0/junit/test-results.xml +0 -1
  22. {haiway-0.13.0 → haiway-0.14.0}/.github/workflows/ci.yml +0 -0
  23. {haiway-0.13.0 → haiway-0.14.0}/.github/workflows/publish.yml +0 -0
  24. {haiway-0.13.0 → haiway-0.14.0}/.gitignore +0 -0
  25. {haiway-0.13.0 → haiway-0.14.0}/LICENSE +0 -0
  26. {haiway-0.13.0 → haiway-0.14.0}/Makefile +0 -0
  27. {haiway-0.13.0 → haiway-0.14.0}/README.md +0 -0
  28. {haiway-0.13.0 → haiway-0.14.0}/config/pre-push +0 -0
  29. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/.dockerignore +0 -0
  30. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/Dockerfile +0 -0
  31. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/Makefile +0 -0
  32. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/README.md +0 -0
  33. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/config/.env.example +0 -0
  34. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/config/unit.json +0 -0
  35. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/docker-compose.yml +0 -0
  36. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/pyproject.toml +0 -0
  37. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/features/__int__.py +0 -0
  38. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/features/todos/__init__.py +0 -0
  39. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/features/todos/config.py +0 -0
  40. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/features/todos/state.py +0 -0
  41. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/features/todos/types.py +0 -0
  42. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/features/todos/user_tasks.py +0 -0
  43. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/integrations/__init__.py +0 -0
  44. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/__init__.py +0 -0
  45. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/client.py +0 -0
  46. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/config.py +0 -0
  47. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/state.py +0 -0
  48. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/types.py +0 -0
  49. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/migrations/__init__.py +0 -0
  50. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/migrations/__main__.py +0 -0
  51. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/__init__.py +0 -0
  52. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/execution.py +0 -0
  53. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/migration_0.py +0 -0
  54. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/types.py +0 -0
  55. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/__init__.py +0 -0
  56. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/__main__.py +0 -0
  57. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/application.py +0 -0
  58. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/config.py +0 -0
  59. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/middlewares/__init__.py +0 -0
  60. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/middlewares/context.py +0 -0
  61. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/routes/__init__.py +0 -0
  62. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/routes/technical.py +0 -0
  63. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/server/routes/todos.py +0 -0
  64. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/solutions/__init__.py +0 -0
  65. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/__init__.py +0 -0
  66. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/config.py +0 -0
  67. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/postgres.py +0 -0
  68. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/state.py +0 -0
  69. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/types.py +0 -0
  70. {haiway-0.13.0 → haiway-0.14.0}/examples/fastAPI/uv.lock +0 -0
  71. {haiway-0.13.0 → haiway-0.14.0}/guidelines/functionalities.md +0 -0
  72. {haiway-0.13.0 → haiway-0.14.0}/guidelines/packages.md +0 -0
  73. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/__init__.py +0 -0
  74. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/disposables.py +0 -0
  75. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/identifier.py +0 -0
  76. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/logging.py +0 -0
  77. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/metrics.py +0 -0
  78. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/context/types.py +0 -0
  79. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/helpers/tracing.py +0 -0
  80. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/py.typed +0 -0
  81. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/state/__init__.py +0 -0
  82. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/state/attributes.py +0 -0
  83. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/state/path.py +0 -0
  84. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/state/requirement.py +0 -0
  85. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/state/structure.py +0 -0
  86. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/state/validation.py +0 -0
  87. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/types/__init__.py +0 -0
  88. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/types/default.py +0 -0
  89. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/types/frozen.py +0 -0
  90. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/types/missing.py +0 -0
  91. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/collections.py +0 -0
  92. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/env.py +0 -0
  93. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/freezing.py +0 -0
  94. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/logs.py +0 -0
  95. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/mimic.py +0 -0
  96. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/noop.py +0 -0
  97. {haiway-0.13.0 → haiway-0.14.0}/src/haiway/utils/queue.py +0 -0
  98. {haiway-0.13.0 → haiway-0.14.0}/tests/__init__.py +0 -0
  99. {haiway-0.13.0 → haiway-0.14.0}/tests/test_async_queue.py +0 -0
  100. {haiway-0.13.0 → haiway-0.14.0}/tests/test_attribute_path.py +0 -0
  101. {haiway-0.13.0 → haiway-0.14.0}/tests/test_auto_retry.py +0 -0
  102. {haiway-0.13.0 → haiway-0.14.0}/tests/test_cache.py +0 -0
  103. {haiway-0.13.0 → haiway-0.14.0}/tests/test_context.py +0 -0
  104. {haiway-0.13.0 → haiway-0.14.0}/tests/test_state.py +0 -0
  105. {haiway-0.13.0 → haiway-0.14.0}/tests/test_timeout.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.13.0
3
+ Version: 0.14.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
@@ -0,0 +1 @@
1
+ <?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="88" time="1.127" timestamp="2025-04-01T10:53:25.573207" hostname="fv-az1786-487"><testcase classname="tests.test_async_queue" name="test_fails_when_stream_fails" time="0.002" /><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.001" /><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_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.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.001" /><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_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.001" /><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.002" /><testcase classname="tests.test_state" name="test_copying_leaves_same_object" time="0.001" /><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.13.0"
8
+ version = "0.14.0"
9
9
  readme = "README.md"
10
10
  maintainers = [
11
11
  { name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" },
@@ -40,6 +40,7 @@ from haiway.types import (
40
40
  )
41
41
  from haiway.utils import (
42
42
  AsyncQueue,
43
+ AsyncStream,
43
44
  always,
44
45
  as_dict,
45
46
  as_list,
@@ -63,6 +64,7 @@ __all__ = [
63
64
  "MISSING",
64
65
  "ArgumentsTrace",
65
66
  "AsyncQueue",
67
+ "AsyncStream",
66
68
  "AttributePath",
67
69
  "AttributeRequirement",
68
70
  "Default",
@@ -11,7 +11,6 @@ from collections.abc import (
11
11
  Coroutine,
12
12
  Iterable,
13
13
  )
14
- from contextvars import Context, copy_context
15
14
  from logging import Logger
16
15
  from types import TracebackType
17
16
  from typing import Any, final, overload
@@ -24,6 +23,7 @@ from haiway.context.state import StateContext
24
23
  from haiway.context.tasks import TaskGroupContext
25
24
  from haiway.state import State
26
25
  from haiway.utils import mimic_function
26
+ from haiway.utils.stream import AsyncStream
27
27
 
28
28
  __all__ = [
29
29
  "ctx",
@@ -37,7 +37,6 @@ class ScopeContext:
37
37
  "_identifier",
38
38
  "_logger_context",
39
39
  "_metrics_context",
40
- "_state",
41
40
  "_state_context",
42
41
  "_task_group_context",
43
42
  )
@@ -67,13 +66,12 @@ class ScopeContext:
67
66
  )
68
67
  # postponing task group creation to include only when needed
69
68
  self._task_group_context: TaskGroupContext
70
- # postponing state creation to include disposables state when prepared
69
+ # prepare state context to capture current state
71
70
  self._state_context: StateContext
72
- self._state: tuple[State, ...]
73
71
  object.__setattr__(
74
72
  self,
75
- "_state",
76
- state,
73
+ "_state_context",
74
+ StateContext.updated(state),
77
75
  )
78
76
  self._disposables: Disposables | None
79
77
  object.__setattr__(
@@ -115,12 +113,6 @@ class ScopeContext:
115
113
  assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
116
114
  self._identifier.__enter__()
117
115
  self._logger_context.__enter__()
118
- # lazily initialize state
119
- object.__setattr__(
120
- self,
121
- "_state_context",
122
- StateContext.updated(self._state),
123
- )
124
116
  self._state_context.__enter__()
125
117
  self._metrics_context.__enter__()
126
118
 
@@ -169,24 +161,17 @@ class ScopeContext:
169
161
 
170
162
  # lazily initialize state to include disposables results
171
163
  if self._disposables is not None:
164
+ assert self._state_context._token is None # nosec: B101
172
165
  object.__setattr__(
173
166
  self,
174
167
  "_state_context",
175
- StateContext.updated(
176
- (
177
- *self._state,
178
- *await self._disposables.__aenter__(),
179
- )
168
+ StateContext(
169
+ state=self._state_context._state.updated(
170
+ await self._disposables.__aenter__(),
171
+ ),
180
172
  ),
181
173
  )
182
174
 
183
- else:
184
- object.__setattr__(
185
- self,
186
- "_state_context",
187
- StateContext.updated(self._state),
188
- )
189
-
190
175
  self._state_context.__enter__()
191
176
  self._metrics_context.__enter__()
192
177
 
@@ -238,8 +223,8 @@ class ScopeContext:
238
223
  @overload
239
224
  def __call__[Result, **Arguments](
240
225
  self,
241
- function: Callable[Arguments, Coroutine[None, None, Result]],
242
- ) -> Callable[Arguments, Coroutine[None, None, Result]]: ...
226
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
227
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]]: ...
243
228
 
244
229
  @overload
245
230
  def __call__[Result, **Arguments](
@@ -249,8 +234,8 @@ class ScopeContext:
249
234
 
250
235
  def __call__[Result, **Arguments](
251
236
  self,
252
- function: Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result],
253
- ) -> Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result]:
237
+ function: Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result],
238
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result]:
254
239
  if iscoroutinefunction(function):
255
240
 
256
241
  async def async_context(
@@ -372,7 +357,7 @@ class ctx:
372
357
 
373
358
  @staticmethod
374
359
  def spawn[Result, **Arguments](
375
- function: Callable[Arguments, Coroutine[None, None, Result]],
360
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
376
361
  /,
377
362
  *args: Arguments.args,
378
363
  **kwargs: Arguments.kwargs,
@@ -383,7 +368,7 @@ class ctx:
383
368
 
384
369
  Parameters
385
370
  ----------
386
- function: Callable[Arguments, Coroutine[None, None, Result]]
371
+ function: Callable[Arguments, Coroutine[Any, Any, Result]]
387
372
  function to be called within the task group
388
373
 
389
374
  *args: Arguments.args
@@ -401,12 +386,12 @@ class ctx:
401
386
  return TaskGroupContext.run(function, *args, **kwargs)
402
387
 
403
388
  @staticmethod
404
- def stream[Result, **Arguments](
405
- source: Callable[Arguments, AsyncGenerator[Result, None]],
389
+ def stream[Element, **Arguments](
390
+ source: Callable[Arguments, AsyncGenerator[Element, None]],
406
391
  /,
407
392
  *args: Arguments.args,
408
393
  **kwargs: Arguments.kwargs,
409
- ) -> AsyncIterator[Result]:
394
+ ) -> AsyncIterator[Element]:
410
395
  """
411
396
  Stream results produced by a generator within the proper context state.
412
397
 
@@ -427,25 +412,22 @@ class ctx:
427
412
  iterator for accessing generated results
428
413
  """
429
414
 
430
- # prepare context snapshot
431
- context_snapshot: Context = copy_context()
432
-
433
- # prepare nested context
434
- streaming_context: ScopeContext = ctx.scope(
435
- getattr(
436
- source,
437
- "__name__",
438
- "streaming",
439
- )
440
- )
415
+ output_stream = AsyncStream[Element]()
441
416
 
442
- async def generator() -> AsyncGenerator[Result, None]:
443
- async with streaming_context:
417
+ @ctx.scope("stream")
418
+ async def stream() -> None:
419
+ try:
444
420
  async for result in source(*args, **kwargs):
445
- yield result
421
+ await output_stream.send(result)
422
+
423
+ except BaseException as exc:
424
+ output_stream.finish(exception=exc)
425
+
426
+ else:
427
+ output_stream.finish()
446
428
 
447
- # finally return it as an iterator
448
- return context_snapshot.run(generator)
429
+ TaskGroupContext.run(stream)
430
+ return output_stream
449
431
 
450
432
  @staticmethod
451
433
  def check_cancellation() -> None:
@@ -488,7 +470,7 @@ class ctx:
488
470
  StateType
489
471
  resolved state instance
490
472
  """
491
- return StateContext.current(
473
+ return StateContext.state(
492
474
  state,
493
475
  default=default,
494
476
  )
@@ -92,7 +92,7 @@ class StateContext:
92
92
  _context = ContextVar[ScopeState]("StateContext")
93
93
 
94
94
  @classmethod
95
- def current[StateType: State](
95
+ def state[StateType: State](
96
96
  cls,
97
97
  state: type[StateType],
98
98
  /,
@@ -16,7 +16,7 @@ class TaskGroupContext:
16
16
  @classmethod
17
17
  def run[Result, **Arguments](
18
18
  cls,
19
- function: Callable[Arguments, Coroutine[None, None, Result]],
19
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
20
20
  /,
21
21
  *args: Arguments.args,
22
22
  **kwargs: Arguments.kwargs,
@@ -1,5 +1,5 @@
1
1
  from haiway.helpers.asynchrony import asynchronous, wrap_async
2
- from haiway.helpers.caching import cache
2
+ from haiway.helpers.caching import CacheMakeKey, CacheRead, CacheWrite, cache
3
3
  from haiway.helpers.metrics import MetricsHolder, MetricsLogger
4
4
  from haiway.helpers.retries import retry
5
5
  from haiway.helpers.throttling import throttle
@@ -8,6 +8,9 @@ from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
8
8
 
9
9
  __all__ = [
10
10
  "ArgumentsTrace",
11
+ "CacheMakeKey",
12
+ "CacheRead",
13
+ "CacheWrite",
11
14
  "MetricsHolder",
12
15
  "MetricsLogger",
13
16
  "ResultTrace",
@@ -14,9 +14,9 @@ __all__ = [
14
14
 
15
15
 
16
16
  def wrap_async[**Args, Result](
17
- function: Callable[Args, Coroutine[None, None, Result]] | Callable[Args, Result],
17
+ function: Callable[Args, Coroutine[Any, Any, Result]] | Callable[Args, Result],
18
18
  /,
19
- ) -> Callable[Args, Coroutine[None, None, Result]]:
19
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
20
20
  if iscoroutinefunction(function):
21
21
  return function
22
22
 
@@ -30,10 +30,12 @@ def wrap_async[**Args, Result](
30
30
 
31
31
 
32
32
  @overload
33
- def asynchronous[**Args, Result]() -> Callable[
34
- [Callable[Args, Result]],
35
- Callable[Args, Coroutine[None, None, Result]],
36
- ]: ...
33
+ def asynchronous[**Args, Result]() -> (
34
+ Callable[
35
+ [Callable[Args, Result]],
36
+ Callable[Args, Coroutine[Any, Any, Result]],
37
+ ]
38
+ ): ...
37
39
 
38
40
 
39
41
  @overload
@@ -43,7 +45,7 @@ def asynchronous[**Args, Result](
43
45
  executor: Executor,
44
46
  ) -> Callable[
45
47
  [Callable[Args, Result]],
46
- Callable[Args, Coroutine[None, None, Result]],
48
+ Callable[Args, Coroutine[Any, Any, Result]],
47
49
  ]: ...
48
50
 
49
51
 
@@ -51,7 +53,7 @@ def asynchronous[**Args, Result](
51
53
  def asynchronous[**Args, Result](
52
54
  function: Callable[Args, Result],
53
55
  /,
54
- ) -> Callable[Args, Coroutine[None, None, Result]]: ...
56
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]: ...
55
57
 
56
58
 
57
59
  def asynchronous[**Args, Result](
@@ -62,9 +64,9 @@ def asynchronous[**Args, Result](
62
64
  ) -> (
63
65
  Callable[
64
66
  [Callable[Args, Result]],
65
- Callable[Args, Coroutine[None, None, Result]],
67
+ Callable[Args, Coroutine[Any, Any, Result]],
66
68
  ]
67
- | Callable[Args, Coroutine[None, None, Result]]
69
+ | Callable[Args, Coroutine[Any, Any, Result]]
68
70
  ):
69
71
  """\
70
72
  Wrapper for a sync function to convert it to an async function. \
@@ -90,7 +92,7 @@ def asynchronous[**Args, Result](
90
92
 
91
93
  def wrap(
92
94
  wrapped: Callable[Args, Result],
93
- ) -> Callable[Args, Coroutine[None, None, Result]]:
95
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
94
96
  assert not iscoroutinefunction(wrapped), "Cannot wrap async function in executor" # nosec: B101
95
97
 
96
98
  return _ExecutorWrapper(
@@ -152,7 +154,7 @@ class _ExecutorWrapper[**Args, Result]:
152
154
  instance: object,
153
155
  owner: type | None = None,
154
156
  /,
155
- ) -> Callable[Args, Coroutine[None, None, Result]]:
157
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
156
158
  if owner is None:
157
159
  return self
158
160
 
@@ -180,8 +182,8 @@ class _ExecutorWrapper[**Args, Result]:
180
182
  def _mimic_async[**Args, Result](
181
183
  function: Callable[Args, Result],
182
184
  /,
183
- within: Callable[..., Coroutine[None, None, Result]],
184
- ) -> Callable[Args, Coroutine[None, None, Result]]:
185
+ within: Callable[..., Coroutine[Any, Any, Result]],
186
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
185
187
  try:
186
188
  annotations: Any = getattr( # noqa: B009
187
189
  function,
@@ -192,7 +194,7 @@ def _mimic_async[**Args, Result](
192
194
  "__annotations__",
193
195
  {
194
196
  **annotations,
195
- "return": Coroutine[None, None, annotations.get("return", Any)],
197
+ "return": Coroutine[Any, Any, annotations.get("return", Any)],
196
198
  },
197
199
  )
198
200
 
@@ -234,6 +236,6 @@ def _mimic_async[**Args, Result](
234
236
  )
235
237
 
236
238
  return cast(
237
- Callable[Args, Coroutine[None, None, Result]],
239
+ Callable[Args, Coroutine[Any, Any, Result]],
238
240
  within,
239
241
  )
@@ -3,12 +3,15 @@ from collections import OrderedDict
3
3
  from collections.abc import Callable, Coroutine, Hashable
4
4
  from functools import _make_key # pyright: ignore[reportPrivateUsage]
5
5
  from time import monotonic
6
- from typing import NamedTuple, Protocol, cast, overload
6
+ from typing import Any, NamedTuple, Protocol, cast, overload
7
7
 
8
8
  from haiway.context.access import ctx
9
9
  from haiway.utils.mimic import mimic_function
10
10
 
11
11
  __all__ = [
12
+ "CacheMakeKey",
13
+ "CacheRead",
14
+ "CacheWrite",
12
15
  "cache",
13
16
  ]
14
17
 
@@ -59,7 +62,7 @@ def cache[**Args, Result, Key](
59
62
  read: CacheRead[Key, Result],
60
63
  write: CacheWrite[Key, Result],
61
64
  ) -> Callable[
62
- [Callable[Args, Coroutine[None, None, Result]]], Callable[Args, Coroutine[None, None, Result]]
65
+ [Callable[Args, Coroutine[Any, Any, Result]]], Callable[Args, Coroutine[Any, Any, Result]]
63
66
  ]: ...
64
67
 
65
68
 
@@ -73,8 +76,8 @@ def cache[**Args, Result, Key]( # noqa: PLR0913
73
76
  write: CacheWrite[Key, Result] | None = None,
74
77
  ) -> (
75
78
  Callable[
76
- [Callable[Args, Coroutine[None, None, Result]]],
77
- Callable[Args, Coroutine[None, None, Result]],
79
+ [Callable[Args, Coroutine[Any, Any, Result]]],
80
+ Callable[Args, Coroutine[Any, Any, Result]],
78
81
  ]
79
82
  | Callable[[Callable[Args, Result]], Callable[Args, Result]]
80
83
  | Callable[Args, Result]
@@ -317,13 +320,13 @@ class _AsyncCache[**Args, Result]:
317
320
 
318
321
  def __init__(
319
322
  self,
320
- function: Callable[Args, Coroutine[None, None, Result]],
323
+ function: Callable[Args, Coroutine[Any, Any, Result]],
321
324
  /,
322
325
  limit: int,
323
326
  expiration: float | None,
324
327
  make_key: CacheMakeKey[Args, Hashable],
325
328
  ) -> None:
326
- self._function: Callable[Args, Coroutine[None, None, Result]] = function
329
+ self._function: Callable[Args, Coroutine[Any, Any, Result]] = function
327
330
  self._cached: OrderedDict[Hashable, _CacheEntry[Result]] = OrderedDict()
328
331
  self._limit: int = limit
329
332
  self._make_key: CacheMakeKey[Args, Hashable] = make_key
@@ -348,7 +351,7 @@ class _AsyncCache[**Args, Result]:
348
351
  instance: object | None,
349
352
  owner: type | None = None,
350
353
  /,
351
- ) -> Callable[Args, Coroutine[None, None, Result]]:
354
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
352
355
  assert owner is None and instance is None, "cache does not work for classes" # nosec: B101
353
356
  return self
354
357
 
@@ -405,13 +408,13 @@ class _CustomCache[**Args, Result, Key]:
405
408
 
406
409
  def __init__(
407
410
  self,
408
- function: Callable[Args, Coroutine[None, None, Result]],
411
+ function: Callable[Args, Coroutine[Any, Any, Result]],
409
412
  /,
410
413
  make_key: CacheMakeKey[Args, Key],
411
414
  read: CacheRead[Key, Result],
412
415
  write: CacheWrite[Key, Result],
413
416
  ) -> None:
414
- self._function: Callable[Args, Coroutine[None, None, Result]] = function
417
+ self._function: Callable[Args, Coroutine[Any, Any, Result]] = function
415
418
  self._make_key: CacheMakeKey[Args, Key] = make_key
416
419
  self._read: CacheRead[Key, Result] = read
417
420
  self._write: CacheWrite[Key, Result] = write
@@ -15,6 +15,7 @@ __all_ = [
15
15
 
16
16
  class MetricsScopeStore:
17
17
  __slots__ = (
18
+ "allow_exit",
18
19
  "entered",
19
20
  "exited",
20
21
  "identifier",
@@ -31,6 +32,7 @@ class MetricsScopeStore:
31
32
  self.entered: float = monotonic()
32
33
  self.metrics: dict[type[State], State] = {}
33
34
  self.exited: float | None = None
35
+ self.allow_exit: bool = False
34
36
  self.nested: list[MetricsScopeStore] = []
35
37
 
36
38
  @property
@@ -115,7 +117,7 @@ class MetricsHolder:
115
117
 
116
118
  def __init__(self) -> None:
117
119
  self.root_scope: ScopeIdentifier | None = None
118
- self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
120
+ self.scopes: dict[str, MetricsScopeStore] = {}
119
121
 
120
122
  def record(
121
123
  self,
@@ -124,10 +126,10 @@ class MetricsHolder:
124
126
  metric: State,
125
127
  ) -> None:
126
128
  assert self.root_scope is not None # nosec: B101
127
- assert scope in self.scopes # nosec: B101
129
+ assert scope.scope_id in self.scopes # nosec: B101
128
130
 
129
131
  metric_type: type[State] = type(metric)
130
- metrics: dict[type[State], State] = self.scopes[scope].metrics
132
+ metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
131
133
  if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
132
134
  metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
133
135
 
@@ -142,29 +144,29 @@ class MetricsHolder:
142
144
  merged: bool,
143
145
  ) -> Metric | None:
144
146
  assert self.root_scope is not None # nosec: B101
145
- assert scope in self.scopes # nosec: B101
147
+ assert scope.scope_id in self.scopes # nosec: B101
146
148
 
147
149
  if merged:
148
- return self.scopes[scope].merged(metric)
150
+ return self.scopes[scope.scope_id].merged(metric)
149
151
 
150
152
  else:
151
- return cast(Metric | None, self.scopes[scope].metrics.get(metric))
153
+ return cast(Metric | None, self.scopes[scope.scope_id].metrics.get(metric))
152
154
 
153
155
  def enter_scope[Metric: State](
154
156
  self,
155
157
  scope: ScopeIdentifier,
156
158
  /,
157
159
  ) -> None:
158
- assert scope not in self.scopes # nosec: B101
160
+ assert scope.scope_id not in self.scopes # nosec: B101
159
161
  scope_metrics = MetricsScopeStore(scope)
160
- self.scopes[scope] = scope_metrics
162
+ self.scopes[scope.scope_id] = scope_metrics
161
163
 
162
164
  if self.root_scope is None:
163
165
  self.root_scope = scope
164
166
 
165
167
  else:
166
168
  for key in self.scopes.keys():
167
- if key.scope_id == scope.parent_id:
169
+ if key == scope.parent_id:
168
170
  self.scopes[key].nested.append(scope_metrics)
169
171
  return
170
172
 
@@ -177,8 +179,18 @@ class MetricsHolder:
177
179
  scope: ScopeIdentifier,
178
180
  /,
179
181
  ) -> None:
180
- assert scope in self.scopes # nosec: B101
181
- self.scopes[scope].exited = monotonic()
182
+ assert self.root_scope is not None # nosec: B101
183
+ assert scope.scope_id in self.scopes # nosec: B101
184
+
185
+ self.scopes[scope.scope_id].allow_exit = True
186
+
187
+ if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
188
+ return # not completed yet
189
+
190
+ self.scopes[scope.scope_id].exited = monotonic()
191
+
192
+ if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
193
+ self.exit_scope(self.scopes[scope.parent_id].identifier)
182
194
 
183
195
 
184
196
  @final
@@ -213,7 +225,7 @@ class MetricsLogger:
213
225
  redact_content: bool,
214
226
  ) -> None:
215
227
  self.root_scope: ScopeIdentifier | None = None
216
- self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {}
228
+ self.scopes: dict[str, MetricsScopeStore] = {}
217
229
  self.items_limit: int | None = items_limit
218
230
  self.redact_content: bool = redact_content
219
231
 
@@ -224,10 +236,10 @@ class MetricsLogger:
224
236
  metric: State,
225
237
  ) -> None:
226
238
  assert self.root_scope is not None # nosec: B101
227
- assert scope in self.scopes # nosec: B101
239
+ assert scope.scope_id in self.scopes # nosec: B101
228
240
 
229
241
  metric_type: type[State] = type(metric)
230
- metrics: dict[type[State], State] = self.scopes[scope].metrics
242
+ metrics: dict[type[State], State] = self.scopes[scope.scope_id].metrics
231
243
  if (current := metrics.get(metric_type)) and hasattr(current, "__add__"):
232
244
  metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
233
245
 
@@ -248,29 +260,29 @@ class MetricsLogger:
248
260
  merged: bool,
249
261
  ) -> Metric | None:
250
262
  assert self.root_scope is not None # nosec: B101
251
- assert scope in self.scopes # nosec: B101
263
+ assert scope.scope_id in self.scopes # nosec: B101
252
264
 
253
265
  if merged:
254
- return self.scopes[scope].merged(metric)
266
+ return self.scopes[scope.scope_id].merged(metric)
255
267
 
256
268
  else:
257
- return cast(Metric | None, self.scopes[scope].metrics.get(metric))
269
+ return cast(Metric | None, self.scopes[scope.scope_id].metrics.get(metric))
258
270
 
259
271
  def enter_scope[Metric: State](
260
272
  self,
261
273
  scope: ScopeIdentifier,
262
274
  /,
263
275
  ) -> None:
264
- assert scope not in self.scopes # nosec: B101
276
+ assert scope.scope_id not in self.scopes # nosec: B101
265
277
  scope_metrics = MetricsScopeStore(scope)
266
- self.scopes[scope] = scope_metrics
278
+ self.scopes[scope.scope_id] = scope_metrics
267
279
 
268
280
  if self.root_scope is None:
269
281
  self.root_scope = scope
270
282
 
271
283
  else:
272
284
  for key in self.scopes.keys():
273
- if key.scope_id == scope.parent_id:
285
+ if key == scope.parent_id:
274
286
  self.scopes[key].nested.append(scope_metrics)
275
287
  return
276
288
 
@@ -283,12 +295,22 @@ class MetricsLogger:
283
295
  scope: ScopeIdentifier,
284
296
  /,
285
297
  ) -> None:
286
- assert scope in self.scopes # nosec: B101
287
- self.scopes[scope].exited = monotonic()
298
+ assert self.root_scope is not None # nosec: B101
299
+ assert scope.scope_id in self.scopes # nosec: B101
300
+
301
+ self.scopes[scope.scope_id].allow_exit = True
302
+
303
+ if not all(nested.exited for nested in self.scopes[scope.scope_id].nested):
304
+ return # not completed yet
305
+
306
+ self.scopes[scope.scope_id].exited = monotonic()
307
+
308
+ if scope != self.root_scope and self.scopes[scope.parent_id].allow_exit:
309
+ self.exit_scope(self.scopes[scope.parent_id].identifier)
288
310
 
289
- if scope == self.root_scope and self.scopes[scope].finished:
311
+ elif scope == self.root_scope and self.scopes[self.root_scope.scope_id].finished:
290
312
  if log := _tree_log(
291
- self.scopes[scope],
313
+ self.scopes[scope.scope_id],
292
314
  list_items_limit=self.items_limit,
293
315
  redact_content=self.redact_content,
294
316
  ):
@@ -1,7 +1,7 @@
1
1
  from asyncio import CancelledError, iscoroutinefunction, sleep
2
2
  from collections.abc import Callable, Coroutine
3
3
  from time import sleep as sleep_sync
4
- from typing import cast, overload
4
+ from typing import Any, cast, overload
5
5
 
6
6
  from haiway.context import ctx
7
7
  from haiway.utils import mimic_function
@@ -178,12 +178,12 @@ def _wrap_sync[**Args, Result](
178
178
 
179
179
 
180
180
  def _wrap_async[**Args, Result](
181
- function: Callable[Args, Coroutine[None, None, Result]],
181
+ function: Callable[Args, Coroutine[Any, Any, Result]],
182
182
  *,
183
183
  limit: int,
184
184
  delay: Callable[[int, Exception], float] | float | None,
185
185
  catching: set[type[Exception]] | tuple[type[Exception], ...],
186
- ) -> Callable[Args, Coroutine[None, None, Result]]:
186
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
187
187
  assert limit > 0, "Limit has to be greater than zero" # nosec: B101
188
188
 
189
189
  @mimic_function(function)