haiway 0.13.1__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.1 → haiway-0.14.0}/PKG-INFO +1 -1
  2. haiway-0.14.0/junit/test-results.xml +1 -0
  3. {haiway-0.13.1 → haiway-0.14.0}/pyproject.toml +1 -1
  4. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/__init__.py +2 -0
  5. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/access.py +26 -44
  6. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/state.py +1 -1
  7. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/metrics.py +46 -24
  8. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/__init__.py +2 -0
  9. haiway-0.14.0/src/haiway/utils/stream.py +97 -0
  10. haiway-0.14.0/tests/test_async_stream.py +138 -0
  11. {haiway-0.13.1 → haiway-0.14.0}/tests/test_streaming.py +5 -11
  12. {haiway-0.13.1 → haiway-0.14.0}/uv.lock +1 -1
  13. haiway-0.13.1/junit/test-results.xml +0 -1
  14. {haiway-0.13.1 → haiway-0.14.0}/.github/workflows/ci.yml +0 -0
  15. {haiway-0.13.1 → haiway-0.14.0}/.github/workflows/publish.yml +0 -0
  16. {haiway-0.13.1 → haiway-0.14.0}/.gitignore +0 -0
  17. {haiway-0.13.1 → haiway-0.14.0}/LICENSE +0 -0
  18. {haiway-0.13.1 → haiway-0.14.0}/Makefile +0 -0
  19. {haiway-0.13.1 → haiway-0.14.0}/README.md +0 -0
  20. {haiway-0.13.1 → haiway-0.14.0}/config/pre-push +0 -0
  21. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/.dockerignore +0 -0
  22. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/Dockerfile +0 -0
  23. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/Makefile +0 -0
  24. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/README.md +0 -0
  25. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/config/.env.example +0 -0
  26. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/config/unit.json +0 -0
  27. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/docker-compose.yml +0 -0
  28. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/pyproject.toml +0 -0
  29. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/features/__int__.py +0 -0
  30. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/features/todos/__init__.py +0 -0
  31. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/features/todos/config.py +0 -0
  32. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/features/todos/state.py +0 -0
  33. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/features/todos/types.py +0 -0
  34. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/features/todos/user_tasks.py +0 -0
  35. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/integrations/__init__.py +0 -0
  36. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/__init__.py +0 -0
  37. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/client.py +0 -0
  38. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/config.py +0 -0
  39. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/state.py +0 -0
  40. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/integrations/postgres/types.py +0 -0
  41. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/migrations/__init__.py +0 -0
  42. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/migrations/__main__.py +0 -0
  43. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/__init__.py +0 -0
  44. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/execution.py +0 -0
  45. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/migration_0.py +0 -0
  46. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/migrations/postgres/types.py +0 -0
  47. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/__init__.py +0 -0
  48. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/__main__.py +0 -0
  49. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/application.py +0 -0
  50. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/config.py +0 -0
  51. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/middlewares/__init__.py +0 -0
  52. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/middlewares/context.py +0 -0
  53. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/routes/__init__.py +0 -0
  54. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/routes/technical.py +0 -0
  55. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/server/routes/todos.py +0 -0
  56. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/solutions/__init__.py +0 -0
  57. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/__init__.py +0 -0
  58. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/config.py +0 -0
  59. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/postgres.py +0 -0
  60. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/state.py +0 -0
  61. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/src/solutions/user_tasks/types.py +0 -0
  62. {haiway-0.13.1 → haiway-0.14.0}/examples/fastAPI/uv.lock +0 -0
  63. {haiway-0.13.1 → haiway-0.14.0}/guidelines/functionalities.md +0 -0
  64. {haiway-0.13.1 → haiway-0.14.0}/guidelines/packages.md +0 -0
  65. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/__init__.py +0 -0
  66. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/disposables.py +0 -0
  67. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/identifier.py +0 -0
  68. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/logging.py +0 -0
  69. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/metrics.py +0 -0
  70. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/tasks.py +0 -0
  71. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/context/types.py +0 -0
  72. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/__init__.py +0 -0
  73. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/asynchrony.py +0 -0
  74. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/caching.py +0 -0
  75. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/retries.py +0 -0
  76. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/throttling.py +0 -0
  77. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/timeouted.py +0 -0
  78. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/helpers/tracing.py +0 -0
  79. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/py.typed +0 -0
  80. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/state/__init__.py +0 -0
  81. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/state/attributes.py +0 -0
  82. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/state/path.py +0 -0
  83. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/state/requirement.py +0 -0
  84. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/state/structure.py +0 -0
  85. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/state/validation.py +0 -0
  86. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/types/__init__.py +0 -0
  87. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/types/default.py +0 -0
  88. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/types/frozen.py +0 -0
  89. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/types/missing.py +0 -0
  90. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/always.py +0 -0
  91. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/collections.py +0 -0
  92. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/env.py +0 -0
  93. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/freezing.py +0 -0
  94. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/logs.py +0 -0
  95. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/mimic.py +0 -0
  96. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/noop.py +0 -0
  97. {haiway-0.13.1 → haiway-0.14.0}/src/haiway/utils/queue.py +0 -0
  98. {haiway-0.13.1 → haiway-0.14.0}/tests/__init__.py +0 -0
  99. {haiway-0.13.1 → haiway-0.14.0}/tests/test_async_queue.py +0 -0
  100. {haiway-0.13.1 → haiway-0.14.0}/tests/test_attribute_path.py +0 -0
  101. {haiway-0.13.1 → haiway-0.14.0}/tests/test_auto_retry.py +0 -0
  102. {haiway-0.13.1 → haiway-0.14.0}/tests/test_cache.py +0 -0
  103. {haiway-0.13.1 → haiway-0.14.0}/tests/test_context.py +0 -0
  104. {haiway-0.13.1 → haiway-0.14.0}/tests/test_state.py +0 -0
  105. {haiway-0.13.1 → 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.1
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.1"
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
 
@@ -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
  /,
@@ -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",
@@ -0,0 +1,97 @@
1
+ from asyncio import (
2
+ AbstractEventLoop,
3
+ CancelledError,
4
+ Future,
5
+ get_running_loop,
6
+ )
7
+ from collections.abc import AsyncIterator
8
+
9
+ __all__ = [
10
+ "AsyncStream",
11
+ ]
12
+
13
+
14
+ class AsyncStream[Element](AsyncIterator[Element]):
15
+ def __init__(
16
+ self,
17
+ loop: AbstractEventLoop | None = None,
18
+ ) -> None:
19
+ self._loop: AbstractEventLoop = loop or get_running_loop()
20
+ self._ready: Future[None] = self._loop.create_future()
21
+ self._waiting: Future[Element] | None = None
22
+ self._finish_reason: BaseException | None = None
23
+
24
+ @property
25
+ def finished(self) -> bool:
26
+ return self._finish_reason is not None
27
+
28
+ async def send(
29
+ self,
30
+ element: Element,
31
+ /,
32
+ ) -> None:
33
+ if self._finish_reason is not None:
34
+ return # already finished
35
+
36
+ # wait for readiness
37
+ await self._ready
38
+ # we could finish while waiting
39
+ if self._finish_reason is not None:
40
+ return # already finished
41
+
42
+ assert self._waiting is not None and not self._waiting.done() # nosec: B101
43
+ # send the element
44
+ self._waiting.set_result(element)
45
+ # and create new readiness future afterwards
46
+ self._ready = self._loop.create_future()
47
+
48
+ def finish(
49
+ self,
50
+ exception: BaseException | None = None,
51
+ ) -> None:
52
+ if self.finished:
53
+ return # already finished, ignore
54
+
55
+ self._finish_reason = exception or StopAsyncIteration()
56
+
57
+ if not self._ready.done():
58
+ if get_running_loop() is not self._loop:
59
+ self._loop.call_soon_threadsafe(
60
+ self._ready.set_result,
61
+ None,
62
+ )
63
+
64
+ else:
65
+ self._ready.set_result(None)
66
+
67
+ if self._waiting is not None and not self._waiting.done():
68
+ if get_running_loop() is not self._loop:
69
+ self._loop.call_soon_threadsafe(
70
+ self._waiting.set_exception,
71
+ self._finish_reason,
72
+ )
73
+
74
+ else:
75
+ self._waiting.set_exception(self._finish_reason)
76
+
77
+ def cancel(self) -> None:
78
+ self.finish(exception=CancelledError())
79
+
80
+ async def __anext__(self) -> Element:
81
+ assert self._waiting is None, "AsyncStream can't be reused" # nosec: B101
82
+
83
+ if self._finish_reason:
84
+ raise self._finish_reason
85
+
86
+ try:
87
+ assert not self._ready.done() # nosec: B101
88
+ # create new waiting future
89
+ self._waiting = self._loop.create_future()
90
+ # and notify readiness
91
+ self._ready.set_result(None)
92
+ # and wait for the result
93
+ return await self._waiting
94
+
95
+ finally:
96
+ # cleanup waiting future
97
+ self._waiting = None
@@ -0,0 +1,138 @@
1
+ from asyncio import CancelledError, Task
2
+
3
+ import pytest
4
+ from pytest import raises
5
+
6
+ from haiway import AsyncStream, ctx
7
+
8
+
9
+ class FakeException(Exception):
10
+ pass
11
+
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_fails_when_stream_fails():
15
+ stream: AsyncStream[int] = AsyncStream()
16
+ Task(stream.send(0))
17
+ elements: int = 0
18
+ with raises(FakeException):
19
+ async for _ in stream:
20
+ elements += 1
21
+ stream.finish(exception=FakeException())
22
+
23
+ assert elements == 1
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_cancels_when_iteration_cancels():
28
+ stream: AsyncStream[int] = AsyncStream()
29
+ elements: int = 0
30
+ with raises(CancelledError):
31
+ ctx.cancel()
32
+ async for _ in stream:
33
+ elements += 1
34
+
35
+ assert elements == 0
36
+
37
+
38
+ @pytest.mark.asyncio
39
+ async def test_ends_when_stream_ends():
40
+ stream: AsyncStream[int] = AsyncStream()
41
+ stream.finish()
42
+ elements: int = 0
43
+ async for _ in stream:
44
+ elements += 1
45
+
46
+ assert elements == 0
47
+
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_finishes_without_buffer():
51
+ stream: AsyncStream[int] = AsyncStream()
52
+ Task(stream.send(0))
53
+ Task(stream.send(1))
54
+ Task(stream.send(2))
55
+ Task(stream.send(3))
56
+ stream.finish()
57
+ elements: int = 0
58
+
59
+ async for _ in stream:
60
+ elements += 1
61
+
62
+ assert elements == 0
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_fails_without_buffer():
67
+ stream: AsyncStream[int] = AsyncStream()
68
+ Task(stream.send(0))
69
+ Task(stream.send(1))
70
+ Task(stream.send(2))
71
+ Task(stream.send(3))
72
+ stream.finish(exception=FakeException())
73
+ elements: int = 0
74
+
75
+ with raises(FakeException):
76
+ async for _ in stream:
77
+ elements += 1
78
+
79
+ assert elements == 0
80
+
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_delivers_updates_when_sending():
84
+ stream: AsyncStream[int] = AsyncStream()
85
+ Task(stream.send(0))
86
+
87
+ elements: list[int] = []
88
+
89
+ async for element in stream:
90
+ elements.append(element)
91
+ if len(elements) < 10:
92
+ Task(stream.send(element + 1))
93
+ else:
94
+ stream.finish()
95
+
96
+ assert elements == list(range(0, 10))
97
+
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_ignores_when_sending_to_finished():
101
+ stream: AsyncStream[int] = AsyncStream()
102
+ stream.finish()
103
+
104
+ await stream.send(42)
105
+
106
+
107
+ @pytest.mark.asyncio
108
+ async def test_ignores_when_sending_to_failed():
109
+ stream: AsyncStream[int] = AsyncStream()
110
+ stream.finish(exception=FakeException())
111
+
112
+ await stream.send(42)
113
+
114
+
115
+ @pytest.mark.asyncio
116
+ async def test_ignores_when_finishing_when_finished():
117
+ stream: AsyncStream[int] = AsyncStream()
118
+ stream.finish()
119
+ stream.finish() # should not raise
120
+
121
+
122
+ @pytest.mark.asyncio
123
+ async def test_delivers_all_when_sending_async():
124
+ stream: AsyncStream[int] = AsyncStream()
125
+
126
+ async def sender() -> None:
127
+ await stream.send(0)
128
+ await stream.send(1)
129
+ await stream.send(2)
130
+ stream.finish()
131
+
132
+ ctx.spawn(sender)
133
+ elements: list[int] = []
134
+
135
+ async for element in stream:
136
+ elements.append(element)
137
+
138
+ assert elements == [0, 1, 2]
@@ -1,5 +1,5 @@
1
1
  from asyncio import CancelledError, sleep
2
- from collections.abc import AsyncGenerator, AsyncIterator
2
+ from collections.abc import AsyncGenerator
3
3
 
4
4
  from pytest import mark, raises
5
5
 
@@ -77,14 +77,11 @@ async def test_streaming_context_variables_access_is_preserved():
77
77
  with ctx.scope("nested", ctx.state(TestState).updated(value=value)):
78
78
  yield ctx.state(TestState)
79
79
 
80
- stream: AsyncIterator[TestState]
81
80
  async with ctx.scope("test", TestState(value=42)):
82
81
  elements: list[TestState] = []
83
82
 
84
- stream = ctx.stream(generator, 10)
85
-
86
- async for element in stream:
87
- elements.append(element)
83
+ async for element in ctx.stream(generator, 10):
84
+ elements.append(element)
88
85
 
89
86
  assert elements == [
90
87
  TestState(value=42),
@@ -109,14 +106,11 @@ async def test_nested_streaming_streams_correctly():
109
106
  async for item in ctx.stream(inner, value):
110
107
  yield item
111
108
 
112
- stream: AsyncIterator[TestState]
113
109
  async with ctx.scope("test", TestState(value=42)):
114
110
  elements: list[TestState] = []
115
111
 
116
- stream = ctx.stream(outer, 10)
117
-
118
- async for element in stream:
119
- elements.append(element)
112
+ async for element in ctx.stream(outer, 10):
113
+ elements.append(element)
120
114
 
121
115
  assert elements == [
122
116
  TestState(value=42),
@@ -67,7 +67,7 @@ wheels = [
67
67
 
68
68
  [[package]]
69
69
  name = "haiway"
70
- version = "0.13.1"
70
+ version = "0.14.0"
71
71
  source = { editable = "." }
72
72
 
73
73
  [package.optional-dependencies]
@@ -1 +0,0 @@
1
- <?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="78" time="1.098" timestamp="2025-03-25T13:23:02.644198" hostname="fv-az1307-33"><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_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.001" /><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.001" /><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.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.002" /><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>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes