haiway 0.25.1__py3-none-any.whl → 0.26.0__py3-none-any.whl

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.
haiway/context/presets.py CHANGED
@@ -1,32 +1,32 @@
1
+ from asyncio import gather
1
2
  from collections.abc import Collection, Iterable, Mapping
2
3
  from contextvars import ContextVar, Token
3
4
  from types import TracebackType
4
5
  from typing import ClassVar, Protocol, Self, cast
5
6
 
6
7
  from haiway.context.disposables import Disposable, Disposables
8
+ from haiway.context.state import StateContext
7
9
  from haiway.state import Immutable, State
8
- from haiway.types.default import Default
9
10
 
10
11
  __all__ = (
11
- "ContextPresets",
12
- "ContextPresetsRegistry",
13
- "ContextPresetsRegistryContext",
12
+ "ContextPreset",
13
+ "ContextPresetRegistryContext",
14
14
  )
15
15
 
16
16
 
17
- class ContextPresetsStatePreparing(Protocol):
17
+ class ContextPresetStatePreparing(Protocol):
18
18
  async def __call__(self) -> Iterable[State] | State: ...
19
19
 
20
20
 
21
- class ContextPresetsDisposablesPreparing(Protocol):
21
+ class ContextPresetDisposablesPreparing(Protocol):
22
22
  async def __call__(self) -> Iterable[Disposable] | Disposable: ...
23
23
 
24
24
 
25
- class ContextPresets(Immutable):
25
+ class ContextPreset(Immutable):
26
26
  """
27
27
  A configuration preset for context scopes.
28
28
 
29
- ContextPresets allow you to define reusable combinations of state and disposables
29
+ ContextPreset allows you to define reusable combinations of state and disposables
30
30
  that can be applied to scopes by name. This provides a convenient way to manage
31
31
  complex application configurations and resource setups.
32
32
 
@@ -43,15 +43,15 @@ class ContextPresets(Immutable):
43
43
  Basic preset with static state:
44
44
 
45
45
  >>> from haiway import State
46
- >>> from haiway.context import ContextPresets
46
+ >>> from haiway.context import ContextPreset
47
47
  >>>
48
48
  >>> class DatabaseConfig(State):
49
49
  ... connection_string: str
50
50
  ... pool_size: int = 10
51
51
  >>>
52
- >>> db_preset = ContextPresets(
52
+ >>> db_preset = ContextPreset(
53
53
  ... name="database",
54
- ... _state=[DatabaseConfig(connection_string="postgresql://localhost/app")]
54
+ ... state=[DatabaseConfig(connection_string="postgresql://localhost/app")]
55
55
  ... )
56
56
 
57
57
  Preset with dynamic state factory:
@@ -60,9 +60,9 @@ class ContextPresets(Immutable):
60
60
  ... # Load configuration from environment or config file
61
61
  ... return DatabaseConfig(connection_string=os.getenv("DB_URL"))
62
62
  >>>
63
- >>> dynamic_preset = ContextPresets(
63
+ >>> dynamic_preset = ContextPreset(
64
64
  ... name="dynamic_db",
65
- ... _state=[load_config]
65
+ ... state=[load_config]
66
66
  ... )
67
67
 
68
68
  Preset with disposables:
@@ -80,10 +80,10 @@ class ContextPresets(Immutable):
80
80
  >>> async def connection_factory():
81
81
  ... return database_connection()
82
82
  >>>
83
- >>> db_preset = ContextPresets(
83
+ >>> db_preset = ContextPreset(
84
84
  ... name="database",
85
- ... _state=[DatabaseConfig(connection_string="...")],
86
- ... _disposables=[connection_factory]
85
+ ... state=[DatabaseConfig(connection_string="...")],
86
+ ... disposables=[connection_factory]
87
87
  ... )
88
88
 
89
89
  Using presets:
@@ -97,8 +97,31 @@ class ContextPresets(Immutable):
97
97
  """
98
98
 
99
99
  name: str
100
- _state: Collection[ContextPresetsStatePreparing | State] = Default(())
101
- _disposables: Collection[ContextPresetsDisposablesPreparing] = Default(())
100
+ _state: Collection[ContextPresetStatePreparing | State]
101
+ _disposables: Collection[ContextPresetDisposablesPreparing]
102
+
103
+ def __init__(
104
+ self,
105
+ name: str,
106
+ *,
107
+ state: Collection[ContextPresetStatePreparing | State] = (),
108
+ disposables: Collection[ContextPresetDisposablesPreparing] = (),
109
+ ) -> None:
110
+ object.__setattr__(
111
+ self,
112
+ "name",
113
+ name,
114
+ )
115
+ object.__setattr__(
116
+ self,
117
+ "_state",
118
+ state,
119
+ )
120
+ object.__setattr__(
121
+ self,
122
+ "_disposables",
123
+ disposables,
124
+ )
102
125
 
103
126
  def extended(
104
127
  self,
@@ -114,38 +137,38 @@ class ContextPresets(Immutable):
114
137
  Parameters
115
138
  ----------
116
139
  other : Self
117
- Another ContextPresets instance to merge with this one.
140
+ Another ContextPreset instance to merge with this one.
118
141
 
119
142
  Returns
120
143
  -------
121
144
  Self
122
- A new ContextPresets instance with combined state and disposables.
145
+ A new ContextPreset instance with combined state and disposables.
123
146
  """
124
147
  return self.__class__(
125
148
  name=self.name,
126
- _state=(*self._state, *other._state),
127
- _disposables=(*self._disposables, *other._disposables),
149
+ state=(*self._state, *other._state),
150
+ disposables=(*self._disposables, *other._disposables),
128
151
  )
129
152
 
130
153
  def with_state(
131
154
  self,
132
- *state: ContextPresetsStatePreparing | State,
155
+ *state: ContextPresetStatePreparing | State,
133
156
  ) -> Self:
134
157
  """
135
158
  Create a new preset with additional state.
136
159
 
137
- Returns a new ContextPresets instance with the provided state objects
160
+ Returns a new ContextPreset instance with the provided state objects
138
161
  or state factories added to the existing state collection.
139
162
 
140
163
  Parameters
141
164
  ----------
142
- *state : ContextPresetsStatePreparing | State
165
+ *state : ContextPresetStatePreparing | State
143
166
  Additional state objects or state factory functions to include.
144
167
 
145
168
  Returns
146
169
  -------
147
170
  Self
148
- A new ContextPresets instance with the additional state, or the
171
+ A new ContextPreset instance with the additional state, or the
149
172
  same instance if no state was provided.
150
173
  """
151
174
  if not state:
@@ -153,29 +176,29 @@ class ContextPresets(Immutable):
153
176
 
154
177
  return self.__class__(
155
178
  name=self.name,
156
- _state=(*self._state, *state),
157
- _disposables=self._disposables,
179
+ state=(*self._state, *state),
180
+ disposables=self._disposables,
158
181
  )
159
182
 
160
183
  def with_disposable(
161
184
  self,
162
- *disposable: ContextPresetsDisposablesPreparing,
185
+ *disposable: ContextPresetDisposablesPreparing,
163
186
  ) -> Self:
164
187
  """
165
188
  Create a new preset with additional disposables.
166
189
 
167
- Returns a new ContextPresets instance with the provided disposable
190
+ Returns a new ContextPreset instance with the provided disposable
168
191
  factory functions added to the existing disposables collection.
169
192
 
170
193
  Parameters
171
194
  ----------
172
- *disposable : ContextPresetsDisposablesPreparing
195
+ *disposable : ContextPresetDisposablesPreparing
173
196
  Additional disposable factory functions to include.
174
197
 
175
198
  Returns
176
199
  -------
177
200
  Self
178
- A new ContextPresets instance with the additional disposables, or the
201
+ A new ContextPreset instance with the additional disposables, or the
179
202
  same instance if no disposables were provided.
180
203
  """
181
204
  if not disposable:
@@ -183,8 +206,8 @@ class ContextPresets(Immutable):
183
206
 
184
207
  return self.__class__(
185
208
  name=self.name,
186
- _state=self._state,
187
- _disposables=(*self._disposables, *disposable),
209
+ state=self._state,
210
+ disposables=(*self._disposables, *disposable),
188
211
  )
189
212
 
190
213
  async def prepare(self) -> Disposables:
@@ -210,37 +233,48 @@ class ContextPresets(Immutable):
210
233
  This method is called automatically when using presets with ctx.scope(),
211
234
  so you typically don't need to call it directly.
212
235
  """
213
- # Collect states directly
214
- collected_states: list[State] = []
236
+ collected_state: Collection[State] = await self._collect_state()
237
+
238
+ collected_disposables: Collection[Disposable]
239
+ if collected_state:
240
+ # use available state immediately when preparing disposables
241
+ with StateContext.updated(collected_state):
242
+ collected_disposables = (
243
+ DisposableState(_state=collected_state),
244
+ *await self._collect_disposables(),
245
+ )
246
+
247
+ else:
248
+ collected_disposables = await self._collect_disposables()
249
+
250
+ return Disposables(*collected_disposables)
251
+
252
+ async def _collect_state(self) -> Collection[State]:
253
+ collected_state: list[State] = []
215
254
  for state in self._state:
216
255
  if isinstance(state, State):
217
- collected_states.append(state)
256
+ collected_state.append(state)
257
+
218
258
  else:
219
259
  resolved_state: Iterable[State] | State = await state()
220
260
  if isinstance(resolved_state, State):
221
- collected_states.append(resolved_state)
261
+ collected_state.append(resolved_state)
222
262
 
223
263
  else:
224
- collected_states.extend(resolved_state)
225
-
226
- collected_disposables: list[Disposable]
227
- if collected_states:
228
- collected_disposables = [DisposableState(_state=collected_states)]
264
+ collected_state.extend(resolved_state)
229
265
 
230
- else:
231
- collected_disposables = []
266
+ return collected_state
232
267
 
233
- for disposable in self._disposables:
234
- resolved_disposable: Iterable[Disposable] | Disposable = await disposable()
235
- if hasattr(resolved_disposable, "__aenter__") and hasattr(
236
- resolved_disposable, "__aexit__"
237
- ):
238
- collected_disposables.append(cast(Disposable, resolved_disposable))
268
+ async def _collect_disposables(self) -> Collection[Disposable]:
269
+ collected_disposables: list[Disposable] = []
270
+ for disposable in await gather(*(factory() for factory in self._disposables)):
271
+ if hasattr(disposable, "__aenter__") and hasattr(disposable, "__aexit__"):
272
+ collected_disposables.append(cast(Disposable, disposable))
239
273
 
240
274
  else:
241
- collected_disposables.extend(cast(Iterable[Disposable], resolved_disposable))
275
+ collected_disposables.extend(cast(Iterable[Disposable], disposable))
242
276
 
243
- return Disposables(*collected_disposables)
277
+ return collected_disposables
244
278
 
245
279
 
246
280
  class DisposableState(Immutable):
@@ -258,55 +292,32 @@ class DisposableState(Immutable):
258
292
  pass
259
293
 
260
294
 
261
- class ContextPresetsRegistry(Immutable):
262
- _presets: Mapping[str, ContextPresets]
263
-
264
- def __init__(
265
- self,
266
- presets: Collection[ContextPresets],
267
- ) -> None:
268
- object.__setattr__(
269
- self,
270
- "_presets",
271
- {preset.name: preset for preset in presets},
272
- )
273
-
274
- def select(
275
- self,
276
- name: str,
277
- /,
278
- ) -> ContextPresets | None:
279
- return self._presets.get(name)
280
-
281
-
282
- class ContextPresetsRegistryContext(Immutable):
283
- _context: ClassVar[ContextVar[ContextPresetsRegistry]] = ContextVar[ContextPresetsRegistry](
284
- "ContextPresetsRegistryContext"
285
- )
295
+ class ContextPresetRegistryContext(Immutable):
296
+ _context: ClassVar[ContextVar[Self]] = ContextVar[Self]("ContextPresetRegistryContext")
286
297
 
287
298
  @classmethod
288
299
  def select(
289
300
  cls,
290
301
  name: str,
291
302
  /,
292
- ) -> ContextPresets | None:
303
+ ) -> ContextPreset | None:
293
304
  try:
294
- return cls._context.get().select(name)
305
+ return cls._context.get().preset(name)
295
306
 
296
307
  except LookupError:
297
308
  return None # no presets
298
309
 
299
- _registry: ContextPresetsRegistry
300
- _token: Token[ContextPresetsRegistry] | None = None
310
+ _registry: Mapping[str, ContextPreset]
311
+ _token: Token[Self] | None = None
301
312
 
302
313
  def __init__(
303
314
  self,
304
- registry: ContextPresetsRegistry,
315
+ presets: Iterable[ContextPreset],
305
316
  ) -> None:
306
317
  object.__setattr__(
307
318
  self,
308
319
  "_registry",
309
- registry,
320
+ {preset.name: preset for preset in presets},
310
321
  )
311
322
  object.__setattr__(
312
323
  self,
@@ -314,12 +325,19 @@ class ContextPresetsRegistryContext(Immutable):
314
325
  None,
315
326
  )
316
327
 
328
+ def preset(
329
+ self,
330
+ name: str,
331
+ /,
332
+ ) -> ContextPreset | None:
333
+ return self._registry.get(name)
334
+
317
335
  def __enter__(self) -> None:
318
336
  assert self._token is None, "Context reentrance is not allowed" # nosec: B101
319
337
  object.__setattr__(
320
338
  self,
321
339
  "_token",
322
- ContextPresetsRegistryContext._context.set(self._registry),
340
+ ContextPresetRegistryContext._context.set(self),
323
341
  )
324
342
 
325
343
  def __exit__(
@@ -329,7 +347,7 @@ class ContextPresetsRegistryContext(Immutable):
329
347
  exc_tb: TracebackType | None,
330
348
  ) -> None:
331
349
  assert self._token is not None, "Unbalanced context enter/exit" # nosec: B101
332
- ContextPresetsRegistryContext._context.reset(self._token)
350
+ ContextPresetRegistryContext._context.reset(self._token) # pyright: ignore[reportArgumentType]
333
351
  object.__setattr__(
334
352
  self,
335
353
  "_token",
haiway/context/state.py CHANGED
@@ -1,13 +1,11 @@
1
- from asyncio import iscoroutinefunction
2
- from collections.abc import Callable, Collection, Coroutine, Iterable, MutableMapping
1
+ from collections.abc import Collection, Iterable, MutableMapping
3
2
  from contextvars import ContextVar, Token
4
3
  from threading import Lock
5
4
  from types import TracebackType
6
- from typing import Any, ClassVar, Self, cast, overload
5
+ from typing import ClassVar, Self, cast
7
6
 
8
7
  from haiway.context.types import MissingContext, MissingState
9
8
  from haiway.state import Immutable, State
10
- from haiway.utils.mimic import mimic_function
11
9
 
12
10
  __all__ = (
13
11
  "ScopeState",
@@ -356,41 +354,3 @@ class StateContext(Immutable):
356
354
  "_token",
357
355
  None,
358
356
  )
359
-
360
- @overload
361
- def __call__[Result, **Arguments](
362
- self,
363
- function: Callable[Arguments, Coroutine[Any, Any, Result]],
364
- ) -> Callable[Arguments, Coroutine[Any, Any, Result]]: ...
365
-
366
- @overload
367
- def __call__[Result, **Arguments](
368
- self,
369
- function: Callable[Arguments, Result],
370
- ) -> Callable[Arguments, Result]: ...
371
-
372
- def __call__[Result, **Arguments](
373
- self,
374
- function: Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result],
375
- ) -> Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result]:
376
- if iscoroutinefunction(function):
377
-
378
- async def async_context(
379
- *args: Arguments.args,
380
- **kwargs: Arguments.kwargs,
381
- ) -> Result:
382
- with self:
383
- return await function(*args, **kwargs)
384
-
385
- return mimic_function(function, within=async_context)
386
-
387
- else:
388
-
389
- def sync_context(
390
- *args: Arguments.args,
391
- **kwargs: Arguments.kwargs,
392
- ) -> Result:
393
- with self:
394
- return function(*args, **kwargs) # pyright: ignore[reportReturnType]
395
-
396
- return mimic_function(function, within=sync_context) # pyright: ignore[reportReturnType]
@@ -10,7 +10,6 @@ from haiway.helpers.observability import LoggerObservability
10
10
  from haiway.helpers.retries import retry
11
11
  from haiway.helpers.throttling import throttle
12
12
  from haiway.helpers.timeouting import timeout
13
- from haiway.helpers.tracing import traced
14
13
 
15
14
  __all__ = (
16
15
  "CacheMakeKey",
@@ -27,5 +26,4 @@ __all__ = (
27
26
  "stream_concurrently",
28
27
  "throttle",
29
28
  "timeout",
30
- "traced",
31
29
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.25.1
3
+ Version: 0.26.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
@@ -1,15 +1,15 @@
1
- haiway/__init__.py,sha256=czgQKokK8NkHBv80HrUV5T1LIvMF301u7ZnIAneq-rg,2123
1
+ haiway/__init__.py,sha256=tJpU6TzK-o-Pt8joGrJah5eC08STVoRzvhXeLn3oTKo,2095
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- haiway/context/__init__.py,sha256=5_4gad1PbSs1Swf9IyeOk6gDEDie8uTqXwm75FMyT2Q,1298
4
- haiway/context/access.py,sha256=aYEn28Zb64IbsE2MmNmLUsg4CxZfBuJX8GG65UakkAk,33029
3
+ haiway/context/__init__.py,sha256=DKcf1AHGEqLWF8Kki30YKQ07GjonUjpAA8I51P4AxAg,1296
4
+ haiway/context/access.py,sha256=g8WdrKNvg6sO3X7Km_9X9syafxS0Shm_EP-JKqRLyxI,31367
5
5
  haiway/context/disposables.py,sha256=7Jo-5qzS3UQvZUf4yOqUgfnueMg8I65jwHDp-4g6w54,7998
6
6
  haiway/context/identifier.py,sha256=ps7YM1ZnUrj66SPVyxqMhTRMaYOMNSb82J3FfMRVHm4,4690
7
7
  haiway/context/observability.py,sha256=rZoZT7g4ZM5_OKIFV0uA0rJodHxondw8X2bMSf_W6_s,20244
8
- haiway/context/presets.py,sha256=ho6tb87uDQ8lsmqSvbhEP6saVxZHgekzXGf1kklYahM,10181
9
- haiway/context/state.py,sha256=5-wVAWBOwoBXyXtLo6qv5dc-phBvXFbolBYohyC4puI,11354
8
+ haiway/context/presets.py,sha256=NVRv-PzuzonxdzIEJmt8SRMRO0lUgKb-jR-2ixUtkzI,10760
9
+ haiway/context/state.py,sha256=1oeON23_vaX7IgyckPcA5jWMXije93TNuSh0l9dQqNE,9920
10
10
  haiway/context/tasks.py,sha256=0LdoxkQW0op4-QhAA-IDQO0PQr6Q3Vp4mO5ssEFbclU,4930
11
11
  haiway/context/types.py,sha256=LoW8238TTdbUgmxyHDi0LVc8M8ZwTHLWKkAPttTsTeg,746
12
- haiway/helpers/__init__.py,sha256=J1WQdI2jD_zDP4azn9Me6hVvaBtz8kh9kTN-jDgDA5U,836
12
+ haiway/helpers/__init__.py,sha256=gyKM1mWyuQwSx_2ajpI0UF1nA8l5D7wrzZOt9XUkWJA,780
13
13
  haiway/helpers/asynchrony.py,sha256=Ddj8UdXhVczAbAC-rLpyhWa4RJ_W2Eolo45Veorq7_4,5362
14
14
  haiway/helpers/caching.py,sha256=BqgcUGQSAmXsuLi5V8EwlZzuGyutHOn1V4k7BHsGKeg,14347
15
15
  haiway/helpers/concurrent.py,sha256=fU6je62XvbySylZKDgzC_AGKPC7Kimmd5SmkVpySBUo,13115
@@ -18,7 +18,6 @@ haiway/helpers/observability.py,sha256=R4md41g7iTslzvtRaY5W9pgXqmuzJuGByjFb6vsO4
18
18
  haiway/helpers/retries.py,sha256=OH__I9e-PUFxcSwuQLIzJ9F1MwXgbz1Ur4jEjJiOmjQ,8974
19
19
  haiway/helpers/throttling.py,sha256=KBWUSHdKVMC5_nRMmmoPNwfp-3AcerQ6OczJa9gNLM0,5796
20
20
  haiway/helpers/timeouting.py,sha256=GQ8-btb36f0Jq7TnorAPYXyKScNmf0nxHXCYxqGl-o8,3949
21
- haiway/helpers/tracing.py,sha256=NHipA5UlngwFcAaKhXg1jTuJ-ti6AqSNxE7u7-92vWo,5409
22
21
  haiway/opentelemetry/__init__.py,sha256=TV-1C14mDAtcHhFZ29ActFQdrGH6x5KuGV9w-JlKYJg,91
23
22
  haiway/opentelemetry/observability.py,sha256=uFgSuvwOgW7IbffROY6Kc4ZRJGoQV6rEWqIQltU_Iho,27365
24
23
  haiway/state/__init__.py,sha256=mtYgg2TojOBNjFsfoRjYkfZPDhKV5sPJXxDGFBvB8-0,417
@@ -41,7 +40,7 @@ haiway/utils/mimic.py,sha256=xaZiUKp096QFfdSw7cNIKEWt2UIS7vf880KF54gny38,1831
41
40
  haiway/utils/noop.py,sha256=U8ocfoCgt-pY0owJDPtrRrj53cabeIXH9qCKWMQnoRk,1336
42
41
  haiway/utils/queue.py,sha256=6v2u3pA6A44IuCCTOjmCt3yLyOcm7PCRnrIGo25j-1o,6402
43
42
  haiway/utils/stream.py,sha256=lXaeveTY0-AYG5xVzcQYaiC6SUD5fUtHoMXiQcrQAAM,5723
44
- haiway-0.25.1.dist-info/METADATA,sha256=ggU_T40b25Eqe886YvYy6VydW4iZRAzJ1U57u7QpUbU,4919
45
- haiway-0.25.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
46
- haiway-0.25.1.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
47
- haiway-0.25.1.dist-info/RECORD,,
43
+ haiway-0.26.0.dist-info/METADATA,sha256=IWQSIN2nqYPQETpjzyJYJ6dv2L--xrP-RG9J9qoQXoc,4919
44
+ haiway-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ haiway-0.26.0.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
46
+ haiway-0.26.0.dist-info/RECORD,,
haiway/helpers/tracing.py DELETED
@@ -1,185 +0,0 @@
1
- from asyncio import iscoroutinefunction
2
- from collections.abc import Callable, Coroutine
3
- from typing import Any, cast, overload
4
-
5
- from haiway.context import ctx
6
- from haiway.context.observability import ObservabilityLevel
7
- from haiway.types import MISSING
8
- from haiway.utils import mimic_function
9
- from haiway.utils.formatting import format_str
10
-
11
- __all__ = ("traced",)
12
-
13
-
14
- @overload
15
- def traced[**Args, Result](
16
- function: Callable[Args, Result],
17
- /,
18
- ) -> Callable[Args, Result]: ...
19
-
20
-
21
- @overload
22
- def traced[**Args, Result](
23
- *,
24
- level: ObservabilityLevel = ObservabilityLevel.DEBUG,
25
- label: str,
26
- ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]: ...
27
-
28
-
29
- def traced[**Args, Result](
30
- function: Callable[Args, Result] | None = None,
31
- /,
32
- *,
33
- level: ObservabilityLevel = ObservabilityLevel.DEBUG,
34
- label: str | None = None,
35
- ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
36
- """
37
- Decorator that adds tracing to functions, recording inputs, outputs, and exceptions.
38
-
39
- Automatically records function arguments, return values, and any exceptions
40
- within the current observability context. The recorded data can be used for
41
- debugging, performance analysis, and understanding program execution flow.
42
-
43
- In non-debug builds (when __debug__ is False), this decorator has no effect
44
- and returns the original function to avoid performance impact in production.
45
-
46
- Parameters
47
- ----------
48
- function: Callable[Args, Result] | None
49
- The function to be traced
50
- level: ObservabilityLevel
51
- The observability level at which to record trace information (default: DEBUG)
52
- label: str | None
53
- Custom label for the trace; defaults to the function name if not provided
54
-
55
- Returns
56
- -------
57
- Callable
58
- A decorated function that performs the same operation as the original
59
- but with added tracing
60
-
61
- Notes
62
- -----
63
- Works with both synchronous and asynchronous functions. For asynchronous
64
- functions, properly awaits the result before recording it.
65
- """
66
-
67
- def wrap(
68
- wrapped: Callable[Args, Result],
69
- ) -> Callable[Args, Result]:
70
- if __debug__:
71
- if iscoroutinefunction(wrapped):
72
- return cast(
73
- Callable[Args, Result],
74
- _traced_async(
75
- wrapped,
76
- label=label or wrapped.__name__,
77
- level=level,
78
- ),
79
- )
80
-
81
- else:
82
- return _traced_sync(
83
- wrapped,
84
- label=label or wrapped.__name__,
85
- level=level,
86
- )
87
-
88
- else: # do not trace on non debug runs
89
- return wrapped
90
-
91
- if function := function:
92
- return wrap(wrapped=function)
93
-
94
- else:
95
- return wrap
96
-
97
-
98
- def _traced_sync[**Args, Result](
99
- function: Callable[Args, Result],
100
- /,
101
- label: str,
102
- level: ObservabilityLevel,
103
- ) -> Callable[Args, Result]:
104
- def traced(
105
- *args: Args.args,
106
- **kwargs: Args.kwargs,
107
- ) -> Result:
108
- with ctx.scope(label):
109
- ctx.record(
110
- level,
111
- attributes={
112
- f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
113
- },
114
- )
115
- ctx.record(
116
- level,
117
- attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING},
118
- )
119
-
120
- try:
121
- result: Result = function(*args, **kwargs)
122
- ctx.record(
123
- level,
124
- event="result",
125
- attributes={"value": format_str(result)},
126
- )
127
- return result
128
-
129
- except BaseException as exc:
130
- ctx.record(
131
- level,
132
- event="result",
133
- attributes={"error": f"{type(exc)}: {exc}"},
134
- )
135
- raise exc
136
-
137
- return mimic_function(
138
- function,
139
- within=traced,
140
- )
141
-
142
-
143
- def _traced_async[**Args, Result](
144
- function: Callable[Args, Coroutine[Any, Any, Result]],
145
- /,
146
- label: str,
147
- level: ObservabilityLevel,
148
- ) -> Callable[Args, Coroutine[Any, Any, Result]]:
149
- async def traced(
150
- *args: Args.args,
151
- **kwargs: Args.kwargs,
152
- ) -> Result:
153
- with ctx.scope(label):
154
- ctx.record(
155
- level,
156
- attributes={
157
- f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
158
- },
159
- )
160
- ctx.record(
161
- level,
162
- attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING},
163
- )
164
-
165
- try:
166
- result: Result = await function(*args, **kwargs)
167
- ctx.record(
168
- level,
169
- event="result",
170
- attributes={"value": format_str(result)},
171
- )
172
- return result
173
-
174
- except BaseException as exc:
175
- ctx.record(
176
- level,
177
- event="result",
178
- attributes={"error": f"{type(exc)}: {exc}"},
179
- )
180
- raise exc
181
-
182
- return mimic_function(
183
- function,
184
- within=traced,
185
- )