haiway 0.13.1__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {haiway-0.13.1 → haiway-0.15.0}/PKG-INFO +1 -1
  2. haiway-0.15.0/junit/test-results.xml +1 -0
  3. {haiway-0.13.1 → haiway-0.15.0}/pyproject.toml +4 -1
  4. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/__init__.py +2 -0
  5. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/access.py +59 -61
  6. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/state.py +43 -3
  7. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/tasks.py +2 -1
  8. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/asynchrony.py +4 -6
  9. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/caching.py +1 -1
  10. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/metrics.py +46 -24
  11. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/__init__.py +2 -0
  12. haiway-0.15.0/src/haiway/utils/stream.py +97 -0
  13. haiway-0.15.0/tests/test_async_stream.py +138 -0
  14. {haiway-0.13.1 → haiway-0.15.0}/tests/test_streaming.py +5 -11
  15. {haiway-0.13.1 → haiway-0.15.0}/uv.lock +46 -46
  16. haiway-0.13.1/junit/test-results.xml +0 -1
  17. {haiway-0.13.1 → haiway-0.15.0}/.github/workflows/ci.yml +0 -0
  18. {haiway-0.13.1 → haiway-0.15.0}/.github/workflows/publish.yml +0 -0
  19. {haiway-0.13.1 → haiway-0.15.0}/.gitignore +0 -0
  20. {haiway-0.13.1 → haiway-0.15.0}/LICENSE +0 -0
  21. {haiway-0.13.1 → haiway-0.15.0}/Makefile +0 -0
  22. {haiway-0.13.1 → haiway-0.15.0}/README.md +0 -0
  23. {haiway-0.13.1 → haiway-0.15.0}/config/pre-push +0 -0
  24. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/.dockerignore +0 -0
  25. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/Dockerfile +0 -0
  26. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/Makefile +0 -0
  27. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/README.md +0 -0
  28. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/config/.env.example +0 -0
  29. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/config/unit.json +0 -0
  30. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/docker-compose.yml +0 -0
  31. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/pyproject.toml +0 -0
  32. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/features/__int__.py +0 -0
  33. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/features/todos/__init__.py +0 -0
  34. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/features/todos/config.py +0 -0
  35. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/features/todos/state.py +0 -0
  36. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/features/todos/types.py +0 -0
  37. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/features/todos/user_tasks.py +0 -0
  38. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/integrations/__init__.py +0 -0
  39. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/integrations/postgres/__init__.py +0 -0
  40. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/integrations/postgres/client.py +0 -0
  41. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/integrations/postgres/config.py +0 -0
  42. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/integrations/postgres/state.py +0 -0
  43. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/integrations/postgres/types.py +0 -0
  44. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/migrations/__init__.py +0 -0
  45. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/migrations/__main__.py +0 -0
  46. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/migrations/postgres/__init__.py +0 -0
  47. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/migrations/postgres/execution.py +0 -0
  48. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/migrations/postgres/migration_0.py +0 -0
  49. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/migrations/postgres/types.py +0 -0
  50. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/__init__.py +0 -0
  51. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/__main__.py +0 -0
  52. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/application.py +0 -0
  53. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/config.py +0 -0
  54. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/middlewares/__init__.py +0 -0
  55. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/middlewares/context.py +0 -0
  56. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/routes/__init__.py +0 -0
  57. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/routes/technical.py +0 -0
  58. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/server/routes/todos.py +0 -0
  59. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/solutions/__init__.py +0 -0
  60. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/solutions/user_tasks/__init__.py +0 -0
  61. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/solutions/user_tasks/config.py +0 -0
  62. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/solutions/user_tasks/postgres.py +0 -0
  63. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/solutions/user_tasks/state.py +0 -0
  64. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/src/solutions/user_tasks/types.py +0 -0
  65. {haiway-0.13.1 → haiway-0.15.0}/examples/fastAPI/uv.lock +0 -0
  66. {haiway-0.13.1 → haiway-0.15.0}/guidelines/functionalities.md +0 -0
  67. {haiway-0.13.1 → haiway-0.15.0}/guidelines/packages.md +0 -0
  68. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/__init__.py +0 -0
  69. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/disposables.py +0 -0
  70. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/identifier.py +0 -0
  71. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/logging.py +0 -0
  72. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/metrics.py +0 -0
  73. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/context/types.py +0 -0
  74. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/__init__.py +0 -0
  75. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/retries.py +0 -0
  76. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/throttling.py +0 -0
  77. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/timeouted.py +0 -0
  78. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/helpers/tracing.py +0 -0
  79. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/py.typed +0 -0
  80. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/state/__init__.py +0 -0
  81. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/state/attributes.py +0 -0
  82. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/state/path.py +0 -0
  83. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/state/requirement.py +0 -0
  84. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/state/structure.py +0 -0
  85. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/state/validation.py +0 -0
  86. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/types/__init__.py +0 -0
  87. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/types/default.py +0 -0
  88. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/types/frozen.py +0 -0
  89. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/types/missing.py +0 -0
  90. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/always.py +0 -0
  91. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/collections.py +0 -0
  92. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/env.py +0 -0
  93. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/freezing.py +0 -0
  94. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/logs.py +0 -0
  95. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/mimic.py +0 -0
  96. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/noop.py +0 -0
  97. {haiway-0.13.1 → haiway-0.15.0}/src/haiway/utils/queue.py +0 -0
  98. {haiway-0.13.1 → haiway-0.15.0}/tests/__init__.py +0 -0
  99. {haiway-0.13.1 → haiway-0.15.0}/tests/test_async_queue.py +0 -0
  100. {haiway-0.13.1 → haiway-0.15.0}/tests/test_attribute_path.py +0 -0
  101. {haiway-0.13.1 → haiway-0.15.0}/tests/test_auto_retry.py +0 -0
  102. {haiway-0.13.1 → haiway-0.15.0}/tests/test_cache.py +0 -0
  103. {haiway-0.13.1 → haiway-0.15.0}/tests/test_context.py +0 -0
  104. {haiway-0.13.1 → haiway-0.15.0}/tests/test_state.py +0 -0
  105. {haiway-0.13.1 → haiway-0.15.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.1
3
+ Version: 0.15.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.118" timestamp="2025-04-02T07:36:48.794161" hostname="fv-az1784-745"><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.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.001" /><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.012" /></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.1"
8
+ version = "0.15.0"
9
9
  readme = "README.md"
10
10
  maintainers = [
11
11
  { name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" },
@@ -44,6 +44,9 @@ extend-exclude = [".venv", ".git", ".cache"]
44
44
  lint.select = ["E", "F", "A", "I", "B", "PL", "W", "C", "RUF", "UP"]
45
45
  lint.ignore = ["A005"]
46
46
 
47
+ [tool.ruff.lint.pylint]
48
+ max-args = 12
49
+
47
50
  [tool.ruff.lint.per-file-ignores]
48
51
  "__init__.py" = ["F401", "E402"]
49
52
  "./tests/*.py" = ["PLR2004"]
@@ -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",
@@ -1,6 +1,7 @@
1
1
  from asyncio import (
2
2
  CancelledError,
3
3
  Task,
4
+ TaskGroup,
4
5
  current_task,
5
6
  iscoroutinefunction,
6
7
  )
@@ -11,7 +12,6 @@ from collections.abc import (
11
12
  Coroutine,
12
13
  Iterable,
13
14
  )
14
- from contextvars import Context, copy_context
15
15
  from logging import Logger
16
16
  from types import TracebackType
17
17
  from typing import Any, final, overload
@@ -24,6 +24,7 @@ from haiway.context.state import StateContext
24
24
  from haiway.context.tasks import TaskGroupContext
25
25
  from haiway.state import State
26
26
  from haiway.utils import mimic_function
27
+ from haiway.utils.stream import AsyncStream
27
28
 
28
29
  __all__ = [
29
30
  "ctx",
@@ -37,7 +38,6 @@ class ScopeContext:
37
38
  "_identifier",
38
39
  "_logger_context",
39
40
  "_metrics_context",
40
- "_state",
41
41
  "_state_context",
42
42
  "_task_group_context",
43
43
  )
@@ -46,6 +46,7 @@ class ScopeContext:
46
46
  self,
47
47
  label: str,
48
48
  logger: Logger | None,
49
+ task_group: TaskGroup | None,
49
50
  state: tuple[State, ...],
50
51
  disposables: Disposables | None,
51
52
  metrics: MetricsHandler | None,
@@ -65,15 +66,22 @@ class ScopeContext:
65
66
  logger=logger,
66
67
  ),
67
68
  )
68
- # postponing task group creation to include only when needed
69
- self._task_group_context: TaskGroupContext
70
- # postponing state creation to include disposables state when prepared
69
+ self._task_group_context: TaskGroupContext | None
70
+ object.__setattr__(
71
+ self,
72
+ "_task_group_context",
73
+ TaskGroupContext(
74
+ task_group=task_group,
75
+ )
76
+ if task_group is not None or self._identifier.is_root
77
+ else None,
78
+ )
79
+ # prepare state context to capture current state
71
80
  self._state_context: StateContext
72
- self._state: tuple[State, ...]
73
81
  object.__setattr__(
74
82
  self,
75
- "_state",
76
- state,
83
+ "_state_context",
84
+ StateContext.updated(state),
77
85
  )
78
86
  self._disposables: Disposables | None
79
87
  object.__setattr__(
@@ -112,15 +120,12 @@ class ScopeContext:
112
120
  )
113
121
 
114
122
  def __enter__(self) -> str:
123
+ assert ( # nosec: B101
124
+ self._task_group_context is None or self._identifier.is_root
125
+ ), "Can't enter synchronous context with task group"
115
126
  assert self._disposables is None, "Can't enter synchronous context with disposables" # nosec: B101
116
127
  self._identifier.__enter__()
117
128
  self._logger_context.__enter__()
118
- # lazily initialize state
119
- object.__setattr__(
120
- self,
121
- "_state_context",
122
- StateContext.updated(self._state),
123
- )
124
129
  self._state_context.__enter__()
125
130
  self._metrics_context.__enter__()
126
131
 
@@ -159,34 +164,23 @@ class ScopeContext:
159
164
  async def __aenter__(self) -> str:
160
165
  self._identifier.__enter__()
161
166
  self._logger_context.__enter__()
162
- # lazily initialize group when needed
163
- object.__setattr__(
164
- self,
165
- "_task_group_context",
166
- TaskGroupContext(),
167
- )
168
- await self._task_group_context.__aenter__()
167
+
168
+ if task_group := self._task_group_context:
169
+ await task_group.__aenter__()
169
170
 
170
171
  # lazily initialize state to include disposables results
171
- if self._disposables is not None:
172
+ if disposables := self._disposables:
173
+ assert self._state_context._token is None # nosec: B101
172
174
  object.__setattr__(
173
175
  self,
174
176
  "_state_context",
175
- StateContext.updated(
176
- (
177
- *self._state,
178
- *await self._disposables.__aenter__(),
179
- )
177
+ StateContext(
178
+ state=self._state_context._state.updated(
179
+ await disposables.__aenter__(),
180
+ ),
180
181
  ),
181
182
  )
182
183
 
183
- else:
184
- object.__setattr__(
185
- self,
186
- "_state_context",
187
- StateContext.updated(self._state),
188
- )
189
-
190
184
  self._state_context.__enter__()
191
185
  self._metrics_context.__enter__()
192
186
 
@@ -198,18 +192,19 @@ class ScopeContext:
198
192
  exc_val: BaseException | None,
199
193
  exc_tb: TracebackType | None,
200
194
  ) -> None:
201
- if self._disposables is not None:
202
- await self._disposables.__aexit__(
195
+ if disposables := self._disposables:
196
+ await disposables.__aexit__(
203
197
  exc_type=exc_type,
204
198
  exc_val=exc_val,
205
199
  exc_tb=exc_tb,
206
200
  )
207
201
 
208
- await self._task_group_context.__aexit__(
209
- exc_type=exc_type,
210
- exc_val=exc_val,
211
- exc_tb=exc_tb,
212
- )
202
+ if task_group := self._task_group_context:
203
+ await task_group.__aexit__(
204
+ exc_type=exc_type,
205
+ exc_val=exc_val,
206
+ exc_tb=exc_tb,
207
+ )
213
208
 
214
209
  self._metrics_context.__exit__(
215
210
  exc_type=exc_type,
@@ -293,6 +288,7 @@ class ctx:
293
288
  *state: State,
294
289
  disposables: Disposables | Iterable[Disposable] | None = None,
295
290
  logger: Logger | None = None,
291
+ task_group: TaskGroup | None = None,
296
292
  metrics: MetricsHandler | None = None,
297
293
  ) -> ScopeContext:
298
294
  """
@@ -317,6 +313,10 @@ class ctx:
317
313
  logger used within the scope context, when not provided current logger will be used\
318
314
  if any, otherwise the logger with the scope name will be requested.
319
315
 
316
+ task_group: TaskGroup | None
317
+ task group used for spawning and joining tasks within the context. Root scope will
318
+ always have task group created even when not set.
319
+
320
320
  metrics_store: MetricsStore | None = None
321
321
  metrics storage solution responsible for recording and storing metrics.\
322
322
  Metrics recroding will be ignored if storage is not provided.
@@ -343,6 +343,7 @@ class ctx:
343
343
  return ScopeContext(
344
344
  label=label,
345
345
  logger=logger,
346
+ task_group=task_group,
346
347
  state=state,
347
348
  disposables=resolved_disposables,
348
349
  metrics=metrics,
@@ -401,12 +402,12 @@ class ctx:
401
402
  return TaskGroupContext.run(function, *args, **kwargs)
402
403
 
403
404
  @staticmethod
404
- def stream[Result, **Arguments](
405
- source: Callable[Arguments, AsyncGenerator[Result, None]],
405
+ def stream[Element, **Arguments](
406
+ source: Callable[Arguments, AsyncGenerator[Element, None]],
406
407
  /,
407
408
  *args: Arguments.args,
408
409
  **kwargs: Arguments.kwargs,
409
- ) -> AsyncIterator[Result]:
410
+ ) -> AsyncIterator[Element]:
410
411
  """
411
412
  Stream results produced by a generator within the proper context state.
412
413
 
@@ -427,25 +428,22 @@ class ctx:
427
428
  iterator for accessing generated results
428
429
  """
429
430
 
430
- # prepare context snapshot
431
- context_snapshot: Context = copy_context()
431
+ output_stream = AsyncStream[Element]()
432
432
 
433
- # prepare nested context
434
- streaming_context: ScopeContext = ctx.scope(
435
- getattr(
436
- source,
437
- "__name__",
438
- "streaming",
439
- )
440
- )
441
-
442
- async def generator() -> AsyncGenerator[Result, None]:
443
- async with streaming_context:
433
+ @ctx.scope("stream")
434
+ async def stream() -> None:
435
+ try:
444
436
  async for result in source(*args, **kwargs):
445
- yield result
437
+ await output_stream.send(result)
438
+
439
+ except BaseException as exc:
440
+ output_stream.finish(exception=exc)
441
+
442
+ else:
443
+ output_stream.finish()
446
444
 
447
- # finally return it as an iterator
448
- return context_snapshot.run(generator)
445
+ TaskGroupContext.run(stream)
446
+ return output_stream
449
447
 
450
448
  @staticmethod
451
449
  def check_cancellation() -> None:
@@ -488,7 +486,7 @@ class ctx:
488
486
  StateType
489
487
  resolved state instance
490
488
  """
491
- return StateContext.current(
489
+ return StateContext.state(
492
490
  state,
493
491
  default=default,
494
492
  )
@@ -1,10 +1,12 @@
1
- from collections.abc import Iterable, MutableMapping
1
+ from asyncio import iscoroutinefunction
2
+ from collections.abc import Callable, Coroutine, Iterable, MutableMapping
2
3
  from contextvars import ContextVar, Token
3
4
  from types import TracebackType
4
- from typing import Any, Self, cast, final
5
+ from typing import Any, Self, cast, final, overload
5
6
 
6
7
  from haiway.context.types import MissingContext, MissingState
7
8
  from haiway.state import State
9
+ from haiway.utils.mimic import mimic_function
8
10
 
9
11
  __all__ = [
10
12
  "ScopeState",
@@ -92,7 +94,7 @@ class StateContext:
92
94
  _context = ContextVar[ScopeState]("StateContext")
93
95
 
94
96
  @classmethod
95
- def current[StateType: State](
97
+ def state[StateType: State](
96
98
  cls,
97
99
  state: type[StateType],
98
100
  /,
@@ -179,3 +181,41 @@ class StateContext:
179
181
  "_token",
180
182
  None,
181
183
  )
184
+
185
+ @overload
186
+ def __call__[Result, **Arguments](
187
+ self,
188
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
189
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]]: ...
190
+
191
+ @overload
192
+ def __call__[Result, **Arguments](
193
+ self,
194
+ function: Callable[Arguments, Result],
195
+ ) -> Callable[Arguments, Result]: ...
196
+
197
+ def __call__[Result, **Arguments](
198
+ self,
199
+ function: Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result],
200
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result]:
201
+ if iscoroutinefunction(function):
202
+
203
+ async def async_context(
204
+ *args: Arguments.args,
205
+ **kwargs: Arguments.kwargs,
206
+ ) -> Result:
207
+ with self:
208
+ return await function(*args, **kwargs)
209
+
210
+ return mimic_function(function, within=async_context)
211
+
212
+ else:
213
+
214
+ def sync_context(
215
+ *args: Arguments.args,
216
+ **kwargs: Arguments.kwargs,
217
+ ) -> Result:
218
+ with self:
219
+ return function(*args, **kwargs) # pyright: ignore[reportReturnType]
220
+
221
+ return mimic_function(function, within=sync_context) # pyright: ignore[reportReturnType]
@@ -40,12 +40,13 @@ class TaskGroupContext:
40
40
 
41
41
  def __init__(
42
42
  self,
43
+ task_group: TaskGroup | None = None,
43
44
  ) -> None:
44
45
  self._group: TaskGroup
45
46
  object.__setattr__(
46
47
  self,
47
48
  "_group",
48
- TaskGroup(),
49
+ task_group if task_group is not None else TaskGroup(),
49
50
  )
50
51
  self._token: Token[TaskGroup] | None
51
52
  object.__setattr__(
@@ -30,12 +30,10 @@ def wrap_async[**Args, Result](
30
30
 
31
31
 
32
32
  @overload
33
- def asynchronous[**Args, Result]() -> (
34
- Callable[
35
- [Callable[Args, Result]],
36
- Callable[Args, Coroutine[Any, Any, Result]],
37
- ]
38
- ): ...
33
+ def asynchronous[**Args, Result]() -> Callable[
34
+ [Callable[Args, Result]],
35
+ Callable[Args, Coroutine[Any, Any, Result]],
36
+ ]: ...
39
37
 
40
38
 
41
39
  @overload
@@ -66,7 +66,7 @@ def cache[**Args, Result, Key](
66
66
  ]: ...
67
67
 
68
68
 
69
- def cache[**Args, Result, Key]( # noqa: PLR0913
69
+ def cache[**Args, Result, Key](
70
70
  function: Callable[Args, Result] | None = None,
71
71
  *,
72
72
  limit: int | None = None,
@@ -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
  ):
@@ -13,9 +13,11 @@ from haiway.utils.logs import setup_logging
13
13
  from haiway.utils.mimic import mimic_function
14
14
  from haiway.utils.noop import async_noop, noop
15
15
  from haiway.utils.queue import AsyncQueue
16
+ from haiway.utils.stream import AsyncStream
16
17
 
17
18
  __all__ = [
18
19
  "AsyncQueue",
20
+ "AsyncStream",
19
21
  "always",
20
22
  "as_dict",
21
23
  "as_list",