haiway 0.12.1__py3-none-any.whl → 0.13.1__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/access.py CHANGED
@@ -238,8 +238,8 @@ class ScopeContext:
238
238
  @overload
239
239
  def __call__[Result, **Arguments](
240
240
  self,
241
- function: Callable[Arguments, Coroutine[None, None, Result]],
242
- ) -> Callable[Arguments, Coroutine[None, None, Result]]: ...
241
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
242
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]]: ...
243
243
 
244
244
  @overload
245
245
  def __call__[Result, **Arguments](
@@ -249,8 +249,8 @@ class ScopeContext:
249
249
 
250
250
  def __call__[Result, **Arguments](
251
251
  self,
252
- function: Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result],
253
- ) -> Callable[Arguments, Coroutine[None, None, Result]] | Callable[Arguments, Result]:
252
+ function: Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result],
253
+ ) -> Callable[Arguments, Coroutine[Any, Any, Result]] | Callable[Arguments, Result]:
254
254
  if iscoroutinefunction(function):
255
255
 
256
256
  async def async_context(
@@ -372,7 +372,7 @@ class ctx:
372
372
 
373
373
  @staticmethod
374
374
  def spawn[Result, **Arguments](
375
- function: Callable[Arguments, Coroutine[None, None, Result]],
375
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
376
376
  /,
377
377
  *args: Arguments.args,
378
378
  **kwargs: Arguments.kwargs,
@@ -383,7 +383,7 @@ class ctx:
383
383
 
384
384
  Parameters
385
385
  ----------
386
- function: Callable[Arguments, Coroutine[None, None, Result]]
386
+ function: Callable[Arguments, Coroutine[Any, Any, Result]]
387
387
  function to be called within the task group
388
388
 
389
389
  *args: Arguments.args
haiway/context/state.py CHANGED
@@ -91,11 +91,6 @@ class ScopeState:
91
91
  class StateContext:
92
92
  _context = ContextVar[ScopeState]("StateContext")
93
93
 
94
- __slots__ = (
95
- "_state",
96
- "_token",
97
- )
98
-
99
94
  @classmethod
100
95
  def current[StateType: State](
101
96
  cls,
@@ -122,6 +117,11 @@ class StateContext:
122
117
  except LookupError: # create root scope when missing
123
118
  return cls(state=ScopeState(state))
124
119
 
120
+ __slots__ = (
121
+ "_state",
122
+ "_token",
123
+ )
124
+
125
125
  def __init__(
126
126
  self,
127
127
  state: ScopeState,
haiway/context/tasks.py CHANGED
@@ -16,7 +16,7 @@ class TaskGroupContext:
16
16
  @classmethod
17
17
  def run[Result, **Arguments](
18
18
  cls,
19
- function: Callable[Arguments, Coroutine[None, None, Result]],
19
+ function: Callable[Arguments, Coroutine[Any, Any, Result]],
20
20
  /,
21
21
  *args: Arguments.args,
22
22
  **kwargs: Arguments.kwargs,
@@ -1,5 +1,5 @@
1
1
  from haiway.helpers.asynchrony import asynchronous, wrap_async
2
- from haiway.helpers.caching import cache
2
+ from haiway.helpers.caching import CacheMakeKey, CacheRead, CacheWrite, cache
3
3
  from haiway.helpers.metrics import MetricsHolder, MetricsLogger
4
4
  from haiway.helpers.retries import retry
5
5
  from haiway.helpers.throttling import throttle
@@ -8,6 +8,9 @@ from haiway.helpers.tracing import ArgumentsTrace, ResultTrace, traced
8
8
 
9
9
  __all__ = [
10
10
  "ArgumentsTrace",
11
+ "CacheMakeKey",
12
+ "CacheRead",
13
+ "CacheWrite",
11
14
  "MetricsHolder",
12
15
  "MetricsLogger",
13
16
  "ResultTrace",
@@ -14,9 +14,9 @@ __all__ = [
14
14
 
15
15
 
16
16
  def wrap_async[**Args, Result](
17
- function: Callable[Args, Coroutine[None, None, Result]] | Callable[Args, Result],
17
+ function: Callable[Args, Coroutine[Any, Any, Result]] | Callable[Args, Result],
18
18
  /,
19
- ) -> Callable[Args, Coroutine[None, None, Result]]:
19
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
20
20
  if iscoroutinefunction(function):
21
21
  return function
22
22
 
@@ -30,10 +30,12 @@ def wrap_async[**Args, Result](
30
30
 
31
31
 
32
32
  @overload
33
- def asynchronous[**Args, Result]() -> Callable[
34
- [Callable[Args, Result]],
35
- Callable[Args, Coroutine[None, None, Result]],
36
- ]: ...
33
+ def asynchronous[**Args, Result]() -> (
34
+ Callable[
35
+ [Callable[Args, Result]],
36
+ Callable[Args, Coroutine[Any, Any, Result]],
37
+ ]
38
+ ): ...
37
39
 
38
40
 
39
41
  @overload
@@ -43,7 +45,7 @@ def asynchronous[**Args, Result](
43
45
  executor: Executor,
44
46
  ) -> Callable[
45
47
  [Callable[Args, Result]],
46
- Callable[Args, Coroutine[None, None, Result]],
48
+ Callable[Args, Coroutine[Any, Any, Result]],
47
49
  ]: ...
48
50
 
49
51
 
@@ -51,7 +53,7 @@ def asynchronous[**Args, Result](
51
53
  def asynchronous[**Args, Result](
52
54
  function: Callable[Args, Result],
53
55
  /,
54
- ) -> Callable[Args, Coroutine[None, None, Result]]: ...
56
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]: ...
55
57
 
56
58
 
57
59
  def asynchronous[**Args, Result](
@@ -62,9 +64,9 @@ def asynchronous[**Args, Result](
62
64
  ) -> (
63
65
  Callable[
64
66
  [Callable[Args, Result]],
65
- Callable[Args, Coroutine[None, None, Result]],
67
+ Callable[Args, Coroutine[Any, Any, Result]],
66
68
  ]
67
- | Callable[Args, Coroutine[None, None, Result]]
69
+ | Callable[Args, Coroutine[Any, Any, Result]]
68
70
  ):
69
71
  """\
70
72
  Wrapper for a sync function to convert it to an async function. \
@@ -90,7 +92,7 @@ def asynchronous[**Args, Result](
90
92
 
91
93
  def wrap(
92
94
  wrapped: Callable[Args, Result],
93
- ) -> Callable[Args, Coroutine[None, None, Result]]:
95
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
94
96
  assert not iscoroutinefunction(wrapped), "Cannot wrap async function in executor" # nosec: B101
95
97
 
96
98
  return _ExecutorWrapper(
@@ -152,7 +154,7 @@ class _ExecutorWrapper[**Args, Result]:
152
154
  instance: object,
153
155
  owner: type | None = None,
154
156
  /,
155
- ) -> Callable[Args, Coroutine[None, None, Result]]:
157
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
156
158
  if owner is None:
157
159
  return self
158
160
 
@@ -180,8 +182,8 @@ class _ExecutorWrapper[**Args, Result]:
180
182
  def _mimic_async[**Args, Result](
181
183
  function: Callable[Args, Result],
182
184
  /,
183
- within: Callable[..., Coroutine[None, None, Result]],
184
- ) -> Callable[Args, Coroutine[None, None, Result]]:
185
+ within: Callable[..., Coroutine[Any, Any, Result]],
186
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
185
187
  try:
186
188
  annotations: Any = getattr( # noqa: B009
187
189
  function,
@@ -192,7 +194,7 @@ def _mimic_async[**Args, Result](
192
194
  "__annotations__",
193
195
  {
194
196
  **annotations,
195
- "return": Coroutine[None, None, annotations.get("return", Any)],
197
+ "return": Coroutine[Any, Any, annotations.get("return", Any)],
196
198
  },
197
199
  )
198
200
 
@@ -234,6 +236,6 @@ def _mimic_async[**Args, Result](
234
236
  )
235
237
 
236
238
  return cast(
237
- Callable[Args, Coroutine[None, None, Result]],
239
+ Callable[Args, Coroutine[Any, Any, Result]],
238
240
  within,
239
241
  )
haiway/helpers/caching.py CHANGED
@@ -1,18 +1,44 @@
1
- from asyncio import AbstractEventLoop, Task, get_running_loop, iscoroutinefunction, shield
1
+ from asyncio import iscoroutinefunction
2
2
  from collections import OrderedDict
3
3
  from collections.abc import Callable, Coroutine, Hashable
4
- from functools import _make_key, partial # pyright: ignore[reportPrivateUsage]
4
+ from functools import _make_key # pyright: ignore[reportPrivateUsage]
5
5
  from time import monotonic
6
- from typing import NamedTuple, cast, overload
7
- from weakref import ref
6
+ from typing import Any, NamedTuple, Protocol, cast, overload
8
7
 
8
+ from haiway.context.access import ctx
9
9
  from haiway.utils.mimic import mimic_function
10
10
 
11
11
  __all__ = [
12
+ "CacheMakeKey",
13
+ "CacheRead",
14
+ "CacheWrite",
12
15
  "cache",
13
16
  ]
14
17
 
15
18
 
19
+ class CacheMakeKey[**Args, Key](Protocol):
20
+ def __call__(
21
+ self,
22
+ *args: Args.args,
23
+ **kwargs: Args.kwargs,
24
+ ) -> Key: ...
25
+
26
+
27
+ class CacheRead[Key, Value](Protocol):
28
+ async def __call__(
29
+ self,
30
+ key: Key,
31
+ ) -> Value | None: ...
32
+
33
+
34
+ class CacheWrite[Key, Value](Protocol):
35
+ async def __call__(
36
+ self,
37
+ key: Key,
38
+ value: Value,
39
+ ) -> None: ...
40
+
41
+
16
42
  @overload
17
43
  def cache[**Args, Result](
18
44
  function: Callable[Args, Result],
@@ -21,58 +47,156 @@ def cache[**Args, Result](
21
47
 
22
48
 
23
49
  @overload
24
- def cache[**Args, Result](
50
+ def cache[**Args, Result, Key: Hashable](
25
51
  *,
26
- limit: int = 1,
52
+ limit: int | None = None,
27
53
  expiration: float | None = None,
54
+ make_key: CacheMakeKey[Args, Key] | None = None,
28
55
  ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]: ...
29
56
 
30
57
 
31
- def cache[**Args, Result](
58
+ @overload
59
+ def cache[**Args, Result, Key](
60
+ *,
61
+ make_key: CacheMakeKey[Args, Key],
62
+ read: CacheRead[Key, Result],
63
+ write: CacheWrite[Key, Result],
64
+ ) -> Callable[
65
+ [Callable[Args, Coroutine[Any, Any, Result]]], Callable[Args, Coroutine[Any, Any, Result]]
66
+ ]: ...
67
+
68
+
69
+ def cache[**Args, Result, Key]( # noqa: PLR0913
32
70
  function: Callable[Args, Result] | None = None,
33
71
  *,
34
- limit: int = 1,
72
+ limit: int | None = None,
35
73
  expiration: float | None = None,
36
- ) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
37
- """\
38
- Simple lru function result cache with optional expire time. \
39
- Works for both sync and async functions. \
40
- It is not allowed to be used on class methods. \
41
- This wrapper is not thread safe.
74
+ make_key: CacheMakeKey[Args, Key] | None = None,
75
+ read: CacheRead[Key, Result] | None = None,
76
+ write: CacheWrite[Key, Result] | None = None,
77
+ ) -> (
78
+ Callable[
79
+ [Callable[Args, Coroutine[Any, Any, Result]]],
80
+ Callable[Args, Coroutine[Any, Any, Result]],
81
+ ]
82
+ | Callable[[Callable[Args, Result]], Callable[Args, Result]]
83
+ | Callable[Args, Result]
84
+ ):
85
+ """
86
+ Memoize the result of a function using a configurable cache.
42
87
 
43
88
  Parameters
44
89
  ----------
45
- function: Callable[_Args, _Result]
46
- function to wrap in cache, either sync or async
47
- limit: int
48
- limit of cache entries to keep, default is 1
49
- expiration: float | None
50
- entries expiration time in seconds, default is None (not expiring)
90
+ function : Callable[Args, Result] | None
91
+ The function to be memoized.
92
+ When used as a simple decorator (i.e., `@cache`), this is the decorated function.
93
+ Should be omitted when cache is called with configuration arguments.
94
+ limit : int | None
95
+ The maximum number of entries to keep in the cache.
96
+ Defaults to 1 if not specified.
97
+ Ignored when using custom cache implementations (read/write).
98
+ expiration : float | None
99
+ Time in seconds after which a cache entry expires and will be recomputed.
100
+ Defaults to None, meaning entries don't expire based on time.
101
+ Ignored when using custom cache implementations (read/write).
102
+ make_key : CacheMakeKey[Args, Key] | None
103
+ Function to generate a cache key from function arguments.
104
+ If None, uses a default implementation that handles most cases.
105
+ Required when using custom cache implementations (read/write).
106
+ read : CacheRead[Key, Result] | None
107
+ Custom asynchronous function to read values from cache.
108
+ Must be provided together with `write` and `make_key`.
109
+ Only available for async functions.
110
+ write : CacheWrite[Key, Result] | None
111
+ Custom asynchronous function to write values to cache.
112
+ Must be provided together with `read` and `make_key`.
113
+ Only available for async functions.
51
114
 
52
115
  Returns
53
116
  -------
54
- Callable[[Callable[_Args, _Result]], Callable[_Args, _Result]] | Callable[_Args, _Result]
55
- provided function wrapped in cache
117
+ Callable
118
+ If `function` is provided as a positional argument, returns the memoized function.
119
+ Otherwise returns a decorator that can be applied to a function to memoize it
120
+ with the given configuration.
121
+
122
+ Notes
123
+ -----
124
+ This decorator supports both synchronous and asynchronous functions.
125
+ The default implementation uses a simple in-memory LRU cache.
126
+ For asynchronous functions, you can provide custom cache implementations
127
+ via the `read` and `write` parameters.
128
+
129
+ The default cache is not thread-safe and should not be used in multi-threaded
130
+ applications without external synchronization.
131
+
132
+ Examples
133
+ --------
134
+ Simple usage as a decorator:
135
+
136
+ >>> @cache
137
+ ... def my_function(x: int) -> int:
138
+ ... print("Function called")
139
+ ... return x * 2
140
+ >>> my_function(5)
141
+ Function called
142
+ 10
143
+ >>> my_function(5) # Cache hit, function body not executed
144
+ 10
145
+
146
+ With configuration parameters:
147
+
148
+ >>> @cache(limit=10, expiration=60.0)
149
+ ... def my_function(x: int) -> int:
150
+ ... return x * 2
151
+
152
+ With custom cache for async functions:
153
+
154
+ >>> @cache(make_key=custom_key_maker, read=redis_read, write=redis_write)
155
+ ... async def fetch_data(user_id: str) -> dict:
156
+ ... return await api_call(user_id)
56
157
  """
57
158
 
58
159
  def _wrap(function: Callable[Args, Result]) -> Callable[Args, Result]:
59
160
  if iscoroutinefunction(function):
60
- return cast(
61
- Callable[Args, Result],
62
- _AsyncCache(
63
- function,
64
- limit=limit,
65
- expiration=expiration,
66
- ),
67
- )
161
+ if read is not None and write is not None and make_key is not None:
162
+ assert limit is None and expiration is None # nosec: B101
163
+ return cast(
164
+ Callable[Args, Result],
165
+ _CustomCache(
166
+ function,
167
+ make_key=make_key,
168
+ read=read,
169
+ write=write,
170
+ ),
171
+ )
172
+
173
+ else:
174
+ assert read is None and write is None # nosec: B101
175
+ return cast(
176
+ Callable[Args, Result],
177
+ _AsyncCache(
178
+ function,
179
+ limit=limit if limit is not None else 1,
180
+ expiration=expiration,
181
+ make_key=cast(
182
+ CacheMakeKey[Args, Hashable],
183
+ make_key if make_key is not None else _default_make_key,
184
+ ),
185
+ ),
186
+ )
68
187
 
69
188
  else:
189
+ assert read is None and write is None, "Custom sync cache is not supported" # nosec: B101
70
190
  return cast(
71
191
  Callable[Args, Result],
72
192
  _SyncCache(
73
193
  function,
74
- limit=limit,
194
+ limit=limit if limit is not None else 1,
75
195
  expiration=expiration,
196
+ make_key=cast(
197
+ CacheMakeKey[Args, Hashable],
198
+ make_key if make_key is not None else _default_make_key,
199
+ ),
76
200
  ),
77
201
  )
78
202
 
@@ -101,6 +225,7 @@ class _SyncCache[**Args, Result]:
101
225
  "_cached",
102
226
  "_function",
103
227
  "_limit",
228
+ "_make_key",
104
229
  "_next_expire_time",
105
230
  )
106
231
 
@@ -110,10 +235,13 @@ class _SyncCache[**Args, Result]:
110
235
  /,
111
236
  limit: int,
112
237
  expiration: float | None,
238
+ make_key: CacheMakeKey[Args, Hashable],
113
239
  ) -> None:
114
240
  self._function: Callable[Args, Result] = function
115
241
  self._cached: OrderedDict[Hashable, _CacheEntry[Result]] = OrderedDict()
116
242
  self._limit: int = limit
243
+ self._make_key: CacheMakeKey[Args, Hashable] = make_key
244
+
117
245
  if expiration := expiration:
118
246
 
119
247
  def next_expire_time() -> float | None:
@@ -135,27 +263,17 @@ class _SyncCache[**Args, Result]:
135
263
  owner: type | None = None,
136
264
  /,
137
265
  ) -> Callable[Args, Result]:
138
- if owner is None or instance is None:
139
- return self
140
-
141
- else:
142
- return mimic_function(
143
- self._function,
144
- within=partial(
145
- self.__method_call__,
146
- instance,
147
- ),
148
- )
266
+ assert owner is None and instance is None, "cache does not work for classes" # nosec: B101
267
+ return self
149
268
 
150
269
  def __call__(
151
270
  self,
152
271
  *args: Args.args,
153
272
  **kwargs: Args.kwargs,
154
273
  ) -> Result:
155
- key: Hashable = _make_key(
156
- args=args,
157
- kwds=kwargs,
158
- typed=True,
274
+ key: Hashable = self._make_key(
275
+ *args,
276
+ **kwargs,
159
277
  )
160
278
 
161
279
  match self._cached.get(key):
@@ -164,7 +282,6 @@ class _SyncCache[**Args, Result]:
164
282
 
165
283
  case entry:
166
284
  if (expire := entry[1]) and expire < monotonic():
167
- # if still running let it complete if able
168
285
  del self._cached[key] # continue the same way as if empty
169
286
 
170
287
  else:
@@ -178,47 +295,11 @@ class _SyncCache[**Args, Result]:
178
295
  )
179
296
 
180
297
  if len(self._cached) > self._limit:
181
- # if still running let it complete if able
298
+ # keep the size limit
182
299
  self._cached.popitem(last=False)
183
300
 
184
301
  return result
185
302
 
186
- def __method_call__(
187
- self,
188
- __method_self: object,
189
- *args: Args.args,
190
- **kwargs: Args.kwargs,
191
- ) -> Result:
192
- key: Hashable = _make_key(
193
- args=(ref(__method_self), *args),
194
- kwds=kwargs,
195
- typed=True,
196
- )
197
-
198
- match self._cached.get(key):
199
- case None:
200
- pass
201
-
202
- case entry:
203
- if (expire := entry[1]) and expire < monotonic():
204
- # if still running let it complete if able
205
- del self._cached[key] # continue the same way as if empty
206
-
207
- else:
208
- self._cached.move_to_end(key)
209
- return entry[0]
210
-
211
- result: Result = self._function(__method_self, *args, **kwargs) # pyright: ignore[reportUnknownVariableType, reportCallIssue]
212
- self._cached[key] = _CacheEntry(
213
- value=result, # pyright: ignore[reportUnknownArgumentType]
214
- expire=self._next_expire_time(),
215
- )
216
- if len(self._cached) > self._limit:
217
- # if still running let it complete if able
218
- self._cached.popitem(last=False)
219
-
220
- return result # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType]
221
-
222
303
 
223
304
  class _AsyncCache[**Args, Result]:
224
305
  __slots__ = (
@@ -233,19 +314,23 @@ class _AsyncCache[**Args, Result]:
233
314
  "_cached",
234
315
  "_function",
235
316
  "_limit",
317
+ "_make_key",
236
318
  "_next_expire_time",
237
319
  )
238
320
 
239
321
  def __init__(
240
322
  self,
241
- function: Callable[Args, Coroutine[None, None, Result]],
323
+ function: Callable[Args, Coroutine[Any, Any, Result]],
242
324
  /,
243
325
  limit: int,
244
326
  expiration: float | None,
327
+ make_key: CacheMakeKey[Args, Hashable],
245
328
  ) -> None:
246
- self._function: Callable[Args, Coroutine[None, None, Result]] = function
247
- self._cached: OrderedDict[Hashable, _CacheEntry[Task[Result]]] = OrderedDict()
329
+ self._function: Callable[Args, Coroutine[Any, Any, Result]] = function
330
+ self._cached: OrderedDict[Hashable, _CacheEntry[Result]] = OrderedDict()
248
331
  self._limit: int = limit
332
+ self._make_key: CacheMakeKey[Args, Hashable] = make_key
333
+
249
334
  if expiration := expiration:
250
335
 
251
336
  def next_expire_time() -> float | None:
@@ -266,29 +351,18 @@ class _AsyncCache[**Args, Result]:
266
351
  instance: object | None,
267
352
  owner: type | None = None,
268
353
  /,
269
- ) -> Callable[Args, Coroutine[None, None, Result]]:
270
- if owner is None or instance is None:
271
- return self
272
-
273
- else:
274
- return mimic_function(
275
- self._function,
276
- within=partial(
277
- self.__method_call__,
278
- instance,
279
- ),
280
- )
354
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
355
+ assert owner is None and instance is None, "cache does not work for classes" # nosec: B101
356
+ return self
281
357
 
282
358
  async def __call__(
283
359
  self,
284
360
  *args: Args.args,
285
361
  **kwargs: Args.kwargs,
286
362
  ) -> Result:
287
- loop: AbstractEventLoop = get_running_loop()
288
- key: Hashable = _make_key(
289
- args=args,
290
- kwds=kwargs,
291
- typed=True,
363
+ key: Hashable = self._make_key(
364
+ *args,
365
+ **kwargs,
292
366
  )
293
367
 
294
368
  match self._cached.get(key):
@@ -297,60 +371,88 @@ class _AsyncCache[**Args, Result]:
297
371
 
298
372
  case entry:
299
373
  if (expire := entry[1]) and expire < monotonic():
300
- # if still running let it complete if able
301
374
  del self._cached[key] # continue the same way as if empty
302
375
 
303
376
  else:
304
377
  self._cached.move_to_end(key)
305
- return await shield(entry[0])
378
+ return entry[0]
306
379
 
307
- task: Task[Result] = loop.create_task(self._function(*args, **kwargs)) # pyright: ignore[reportCallIssue]
380
+ result: Result = await self._function(*args, **kwargs)
308
381
  self._cached[key] = _CacheEntry(
309
- value=task,
382
+ value=result,
310
383
  expire=self._next_expire_time(),
311
384
  )
312
385
  if len(self._cached) > self._limit:
313
- # if still running let it complete if able
386
+ # keep the size limit
314
387
  self._cached.popitem(last=False)
315
388
 
316
- return await shield(task)
389
+ return result
390
+
391
+
392
+ class _CustomCache[**Args, Result, Key]:
393
+ __slots__ = (
394
+ "__annotations__",
395
+ "__defaults__",
396
+ "__doc__",
397
+ "__globals__",
398
+ "__kwdefaults__",
399
+ "__name__",
400
+ "__qualname__",
401
+ "__wrapped__",
402
+ "_expiration",
403
+ "_function",
404
+ "_make_key",
405
+ "_read",
406
+ "_write",
407
+ )
408
+
409
+ def __init__(
410
+ self,
411
+ function: Callable[Args, Coroutine[Any, Any, Result]],
412
+ /,
413
+ make_key: CacheMakeKey[Args, Key],
414
+ read: CacheRead[Key, Result],
415
+ write: CacheWrite[Key, Result],
416
+ ) -> None:
417
+ self._function: Callable[Args, Coroutine[Any, Any, Result]] = function
418
+ self._make_key: CacheMakeKey[Args, Key] = make_key
419
+ self._read: CacheRead[Key, Result] = read
420
+ self._write: CacheWrite[Key, Result] = write
317
421
 
318
- async def __method_call__(
422
+ # mimic function attributes if able
423
+ mimic_function(function, within=self)
424
+
425
+ async def __call__(
319
426
  self,
320
- __method_self: object,
321
427
  *args: Args.args,
322
428
  **kwargs: Args.kwargs,
323
429
  ) -> Result:
324
- loop: AbstractEventLoop = get_running_loop()
325
- key: Hashable = _make_key(
326
- args=(ref(__method_self), *args),
327
- kwds=kwargs,
328
- typed=True,
430
+ key: Key = self._make_key(
431
+ *args,
432
+ **kwargs,
329
433
  )
330
434
 
331
- match self._cached.get(key):
435
+ match await self._read(key):
332
436
  case None:
333
- pass
334
-
335
- case entry:
336
- if (expire := entry[1]) and expire < monotonic():
337
- # if still running let it complete if able
338
- del self._cached[key] # continue the same way as if empty
437
+ result: Result = await self._function(*args, **kwargs)
438
+ ctx.spawn( # write the value asnychronously
439
+ self._write,
440
+ key=key,
441
+ value=result,
442
+ )
339
443
 
340
- else:
341
- self._cached.move_to_end(key)
342
- return await shield(entry[0])
444
+ return result
343
445
 
344
- task: Task[Result] = loop.create_task(
345
- self._function(__method_self, *args, **kwargs), # pyright: ignore[reportCallIssue, reportUnknownArgumentType]
346
- )
347
- self._cached[key] = _CacheEntry(
348
- value=task,
349
- expire=self._next_expire_time(),
350
- )
446
+ case entry:
447
+ return entry
351
448
 
352
- if len(self._cached) > self._limit:
353
- # if still running let it complete if able
354
- self._cached.popitem(last=False)
355
449
 
356
- return await shield(task)
450
+ def _default_make_key[**Args](
451
+ *args: Args.args,
452
+ **kwargs: Args.kwargs,
453
+ ) -> Hashable:
454
+ return _make_key(
455
+ args=args,
456
+ kwds=kwargs,
457
+ typed=True,
458
+ )
haiway/helpers/retries.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from asyncio import CancelledError, iscoroutinefunction, sleep
2
2
  from collections.abc import Callable, Coroutine
3
3
  from time import sleep as sleep_sync
4
- from typing import cast, overload
4
+ from typing import Any, cast, overload
5
5
 
6
6
  from haiway.context import ctx
7
7
  from haiway.utils import mimic_function
@@ -178,12 +178,12 @@ def _wrap_sync[**Args, Result](
178
178
 
179
179
 
180
180
  def _wrap_async[**Args, Result](
181
- function: Callable[Args, Coroutine[None, None, Result]],
181
+ function: Callable[Args, Coroutine[Any, Any, Result]],
182
182
  *,
183
183
  limit: int,
184
184
  delay: Callable[[int, Exception], float] | float | None,
185
185
  catching: set[type[Exception]] | tuple[type[Exception], ...],
186
- ) -> Callable[Args, Coroutine[None, None, Result]]:
186
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
187
187
  assert limit > 0, "Limit has to be greater than zero" # nosec: B101
188
188
 
189
189
  @mimic_function(function)
@@ -7,7 +7,7 @@ from collections import deque
7
7
  from collections.abc import Callable, Coroutine
8
8
  from datetime import timedelta
9
9
  from time import monotonic
10
- from typing import cast, overload
10
+ from typing import Any, cast, overload
11
11
 
12
12
  from haiway.utils.mimic import mimic_function
13
13
 
@@ -18,9 +18,9 @@ __all__ = [
18
18
 
19
19
  @overload
20
20
  def throttle[**Args, Result](
21
- function: Callable[Args, Coroutine[None, None, Result]],
21
+ function: Callable[Args, Coroutine[Any, Any, Result]],
22
22
  /,
23
- ) -> Callable[Args, Coroutine[None, None, Result]]: ...
23
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]: ...
24
24
 
25
25
 
26
26
  @overload
@@ -29,21 +29,21 @@ def throttle[**Args, Result](
29
29
  limit: int = 1,
30
30
  period: timedelta | float = 1,
31
31
  ) -> Callable[
32
- [Callable[Args, Coroutine[None, None, Result]]], Callable[Args, Coroutine[None, None, Result]]
32
+ [Callable[Args, Coroutine[Any, Any, Result]]], Callable[Args, Coroutine[Any, Any, Result]]
33
33
  ]: ...
34
34
 
35
35
 
36
36
  def throttle[**Args, Result](
37
- function: Callable[Args, Coroutine[None, None, Result]] | None = None,
37
+ function: Callable[Args, Coroutine[Any, Any, Result]] | None = None,
38
38
  *,
39
39
  limit: int = 1,
40
40
  period: timedelta | float = 1,
41
41
  ) -> (
42
42
  Callable[
43
- [Callable[Args, Coroutine[None, None, Result]]],
44
- Callable[Args, Coroutine[None, None, Result]],
43
+ [Callable[Args, Coroutine[Any, Any, Result]]],
44
+ Callable[Args, Coroutine[Any, Any, Result]],
45
45
  ]
46
- | Callable[Args, Coroutine[None, None, Result]]
46
+ | Callable[Args, Coroutine[Any, Any, Result]]
47
47
  ):
48
48
  """\
49
49
  Throttle for function calls with custom limit and period time. \
@@ -53,7 +53,7 @@ def throttle[**Args, Result](
53
53
 
54
54
  Parameters
55
55
  ----------
56
- function: Callable[Args, Coroutine[None, None, Result]]
56
+ function: Callable[Args, Coroutine[Any, Any, Result]]
57
57
  function to wrap in throttle
58
58
  limit: int
59
59
  limit of executions in given period, if no period was specified
@@ -63,17 +63,17 @@ def throttle[**Args, Result](
63
63
 
64
64
  Returns
65
65
  -------
66
- Callable[[Callable[Args, Coroutine[None, None, Result]]], Callable[Args, Coroutine[None, None, Result]]] \
67
- | Callable[Args, Coroutine[None, None, Result]]
66
+ Callable[[Callable[Args, Coroutine[Any, Any, Result]]], Callable[Args, Coroutine[Any, Any, Result]]] \
67
+ | Callable[Args, Coroutine[Any, Any, Result]]
68
68
  provided function wrapped in throttle
69
69
  """ # noqa: E501
70
70
 
71
71
  def _wrap(
72
- function: Callable[Args, Coroutine[None, None, Result]],
73
- ) -> Callable[Args, Coroutine[None, None, Result]]:
72
+ function: Callable[Args, Coroutine[Any, Any, Result]],
73
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
74
74
  assert iscoroutinefunction(function) # nosec: B101
75
75
  return cast(
76
- Callable[Args, Coroutine[None, None, Result]],
76
+ Callable[Args, Coroutine[Any, Any, Result]],
77
77
  _AsyncThrottle(
78
78
  function,
79
79
  limit=limit,
@@ -107,12 +107,12 @@ class _AsyncThrottle[**Args, Result]:
107
107
 
108
108
  def __init__(
109
109
  self,
110
- function: Callable[Args, Coroutine[None, None, Result]],
110
+ function: Callable[Args, Coroutine[Any, Any, Result]],
111
111
  /,
112
112
  limit: int,
113
113
  period: timedelta | float,
114
114
  ) -> None:
115
- self._function: Callable[Args, Coroutine[None, None, Result]] = function
115
+ self._function: Callable[Args, Coroutine[Any, Any, Result]] = function
116
116
  self._entries: deque[float] = deque()
117
117
  self._lock: Lock = Lock()
118
118
  self._limit: int = limit
@@ -1,5 +1,6 @@
1
1
  from asyncio import AbstractEventLoop, Future, Task, TimerHandle, get_running_loop
2
2
  from collections.abc import Callable, Coroutine
3
+ from typing import Any
3
4
 
4
5
  from haiway.utils.mimic import mimic_function
5
6
 
@@ -12,8 +13,8 @@ def timeout[**Args, Result](
12
13
  timeout: float,
13
14
  /,
14
15
  ) -> Callable[
15
- [Callable[Args, Coroutine[None, None, Result]]],
16
- Callable[Args, Coroutine[None, None, Result]],
16
+ [Callable[Args, Coroutine[Any, Any, Result]]],
17
+ Callable[Args, Coroutine[Any, Any, Result]],
17
18
  ]:
18
19
  """\
19
20
  Timeout wrapper for a function call. \
@@ -34,8 +35,8 @@ def timeout[**Args, Result](
34
35
  """
35
36
 
36
37
  def _wrap(
37
- function: Callable[Args, Coroutine[None, None, Result]],
38
- ) -> Callable[Args, Coroutine[None, None, Result]]:
38
+ function: Callable[Args, Coroutine[Any, Any, Result]],
39
+ ) -> Callable[Args, Coroutine[Any, Any, Result]]:
39
40
  return _AsyncTimeout(
40
41
  function,
41
42
  timeout=timeout,
@@ -60,11 +61,11 @@ class _AsyncTimeout[**Args, Result]:
60
61
 
61
62
  def __init__(
62
63
  self,
63
- function: Callable[Args, Coroutine[None, None, Result]],
64
+ function: Callable[Args, Coroutine[Any, Any, Result]],
64
65
  /,
65
66
  timeout: float,
66
67
  ) -> None:
67
- self._function: Callable[Args, Coroutine[None, None, Result]] = function
68
+ self._function: Callable[Args, Coroutine[Any, Any, Result]] = function
68
69
  self._timeout: float = timeout
69
70
 
70
71
  # mimic function attributes if able
haiway/utils/always.py CHANGED
@@ -37,7 +37,7 @@ def always[Value](
37
37
  def async_always[Value](
38
38
  value: Value,
39
39
  /,
40
- ) -> Callable[..., Coroutine[None, None, Value]]:
40
+ ) -> Callable[..., Coroutine[Any, Any, Value]]:
41
41
  """
42
42
  Factory method creating async functions returning always the same value.
43
43
 
@@ -48,7 +48,7 @@ def async_always[Value](
48
48
 
49
49
  Returns
50
50
  -------
51
- Callable[..., Coroutine[None, None, Value]]
51
+ Callable[..., Coroutine[Any, Any, Value]]
52
52
  async function ignoring arguments and always returning the provided value.
53
53
  """
54
54
 
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haiway
3
- Version: 0.12.1
3
+ Version: 0.13.1
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
7
7
  Maintainer-email: Kacper Kaliński <kacper.kalinski@miquido.com>
8
8
  License: MIT License
9
9
 
10
- Copyright (c) 2024 Miquido
10
+ Copyright (c) 2024-2025 Miquido
11
11
 
12
12
  Permission is hereby granted, free of charge, to any person obtaining a copy
13
13
  of this software and associated documentation files (the "Software"), to deal
@@ -40,12 +40,12 @@ Requires-Dist: pyright~=1.1; extra == 'dev'
40
40
  Requires-Dist: pytest-asyncio~=0.23; extra == 'dev'
41
41
  Requires-Dist: pytest-cov~=4.1; extra == 'dev'
42
42
  Requires-Dist: pytest~=7.4; extra == 'dev'
43
- Requires-Dist: ruff~=0.9; extra == 'dev'
43
+ Requires-Dist: ruff~=0.11; extra == 'dev'
44
44
  Description-Content-Type: text/markdown
45
45
 
46
46
  # 🚗 haiway 🚕 🚚 🚙
47
47
 
48
- haiway is a framework helping to build better project codebase by leveraging concepts of structured concurrency and functional programming.
48
+ haiway is a framework designed to facilitate the development of applications using the functional programming paradigm combined with structured concurrency concepts. Unlike traditional object-oriented frameworks, haiway emphasizes immutability, pure functions, and context-based state management, enabling developers to build scalable and maintainable applications. By leveraging context managers combined with context vars, haiway ensures safe state propagation in concurrent environments and simplifies dependency injection through function implementation propagation.
49
49
 
50
50
  ## 🖥️ Install
51
51
 
@@ -65,7 +65,7 @@ We welcome any feedback and suggestions! Feel free to open an issue or pull requ
65
65
 
66
66
  MIT License
67
67
 
68
- Copyright (c) 2024 Miquido
68
+ Copyright (c) 2024-2025 Miquido
69
69
 
70
70
  Permission is hereby granted, free of charge, to any person obtaining a copy
71
71
  of this software and associated documentation files (the "Software"), to deal
@@ -1,21 +1,21 @@
1
1
  haiway/__init__.py,sha256=ONC4Hk0GaPzhQ3oYmgh6Z4kJdXQiyJ8ZQcM_hCUz-IY,2045
2
2
  haiway/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  haiway/context/__init__.py,sha256=feqd0eJnGQwh4B8BZXpS0fQRE-DqoFCFOHipF1jOY8A,762
4
- haiway/context/access.py,sha256=3dloiRk4v3ljIJ8LkjW0Tj4vx6G_mDvjEy0lKLU3Wug,18963
4
+ haiway/context/access.py,sha256=CXGe-qkKeG5352_Bo7D4UusnEEQ_SYmRG94RyK5fG8Q,18951
5
5
  haiway/context/disposables.py,sha256=vcsh8jRaJ8Q1ob7oh5LsrSPw9f5AMTcaD_p_Gb7tXAI,2588
6
6
  haiway/context/identifier.py,sha256=lz-FuspOtsaEsfb7QPrEVWYfbcMJgd3A6BGG3kLbaV0,3914
7
7
  haiway/context/logging.py,sha256=F3dr6MLjodg3MX5WTInxn3r3JuihG-giBzumI0GGUQw,5590
8
8
  haiway/context/metrics.py,sha256=N20XQtC8au_e_3iWrsZdej78YBEIWF44fdtWcZBWono,5223
9
- haiway/context/state.py,sha256=E3Z49XeE_EsXfYcPGtd-5_YLD5idK1WMMLZQ02of6cg,4554
10
- haiway/context/tasks.py,sha256=J1BFQJis_15SIXbFclppxL-AOIThg2KS4SX8Hg_-YRY,2828
9
+ haiway/context/state.py,sha256=qskYoNwN5Ad0OgnyhL-PyGzTZltwVVdE9CEqWWn4lm8,4554
10
+ haiway/context/tasks.py,sha256=MKfsa-921cIpQ_BKskwokjR27suCHkHZa3O9kOE8UOg,2826
11
11
  haiway/context/types.py,sha256=VvJA7wAPZ3ISpgyThVguioYUXqhHf0XkPfRd0M1ERiQ,142
12
- haiway/helpers/__init__.py,sha256=8XRJWNhidWuBKqRZ1Hyc2xqt7DeWLcoOs2V-oexl8VY,579
13
- haiway/helpers/asynchrony.py,sha256=-yJsttRhKw0GgnzMc0FqIigS5UJl_G0YgkK12V5IzJg,6292
14
- haiway/helpers/caching.py,sha256=mz8IMKs6KWWVY62PosPbNQ9sGstC6xCWAAWLYZT2oSg,10132
12
+ haiway/helpers/__init__.py,sha256=ZKDlL3twDqXyI1a9FDgRy3m1-Dfycvke6BJ4C3CndEk,671
13
+ haiway/helpers/asynchrony.py,sha256=YHLK5Hjc-5UWlQRypC11yHeEQyeAtHqrMoBTBfqQBvQ,6286
14
+ haiway/helpers/caching.py,sha256=EU5usTHGDzf0SO3bMW4hHB9oZlLlE7BxO_2ckbjYBw8,13274
15
15
  haiway/helpers/metrics.py,sha256=lCSvat3IrkmytFdqTvsqkVqYcVOK_bByfwYAe0hJIWg,13614
16
- haiway/helpers/retries.py,sha256=gIkyUlqJLDYaxIZd3qzeqGFY9y5Gp8dgZLlZ6hs8hoc,7538
17
- haiway/helpers/throttling.py,sha256=RfQn8GGPqTuPWzA1CJvZvb1s00vryVpo-eqz5EY9vD4,4150
18
- haiway/helpers/timeouted.py,sha256=T2I2fThDuP_wDq_3QnZdEaUgAD16uRacbSF1fRnzAaE,3326
16
+ haiway/helpers/retries.py,sha256=3m1SsJW_YY_HPufX9LEzcd_MEyRRFNXvSExLeEti8W8,7539
17
+ haiway/helpers/throttling.py,sha256=r9HnUuo4nX36Pf-oMFHUJk-ZCDeXQ__JTDHlkSltRhA,4121
18
+ haiway/helpers/timeouted.py,sha256=DthIm4ytKhmiIKf-pcO_vrO1X-ImZh-sLNCWcLY9gfw,3337
19
19
  haiway/helpers/tracing.py,sha256=8Gpcc_DguuHAdaxM4rGP0mB-S-8E7DKt7ZGym9f6x6Q,4018
20
20
  haiway/state/__init__.py,sha256=emTuwGFn7HyjyTJ_ass69J5jQIA7_WHO4teZz_dR05Y,355
21
21
  haiway/state/attributes.py,sha256=3chvq3ENoIX688RSYiqZnOCpxbzt-kQ2Wl8Fc3vVyMo,23311
@@ -28,7 +28,7 @@ haiway/types/default.py,sha256=h38-zFkbn_UPEiw1SdDF5rkObVmD9UJpmyhOgS1gQ9U,2208
28
28
  haiway/types/frozen.py,sha256=CZhFCXnWAKEhuWSfILxA8smfdpMd5Ku694ycfLh98R8,76
29
29
  haiway/types/missing.py,sha256=rDnyA2wxPkTbJl0L-zbo0owp7IJ04xkCIp6xD6wh8NI,1712
30
30
  haiway/utils/__init__.py,sha256=YBq9hYhrHFB-4d_M53A620-2KEz5SMU31GDBW6gXFnQ,804
31
- haiway/utils/always.py,sha256=2abp8Lm9rQkrfS3rm1Iqhb-IcWyVfH1BULab3KMxgOw,1234
31
+ haiway/utils/always.py,sha256=u1tssiErzm0Q3ASc3CV1rLhcMQ54MjpMlC_bRJMQhK4,1230
32
32
  haiway/utils/collections.py,sha256=IzD-XSEyngKyzLTNG9sr7QjXIneoAzi3oRsDmbRHtzU,3276
33
33
  haiway/utils/env.py,sha256=-hI4CgLkzdyueuECVjm-TfR3lQjE2bDsc72w7vNC4nQ,5339
34
34
  haiway/utils/freezing.py,sha256=K34ZIMzbkpgkHKH-KF73plEbXExsajNRkRTYp9nJEf4,620
@@ -36,7 +36,7 @@ haiway/utils/logs.py,sha256=oDsc1ZdqKDjlTlctLbDcp9iX98Acr-1tdw-Pyg3DElo,1577
36
36
  haiway/utils/mimic.py,sha256=BkVjTVP2TxxC8GChPGyDV6UXVwJmiRiSWeOYZNZFHxs,1828
37
37
  haiway/utils/noop.py,sha256=qgbZlOKWY6_23Zs43OLukK2HagIQKRyR04zrFVm5rWI,344
38
38
  haiway/utils/queue.py,sha256=Tk1bXvuNbEgapeC3-h_PYBASqVjhEoL8mUvtJnM29xI,4000
39
- haiway-0.12.1.dist-info/METADATA,sha256=9FgAnUCIw62Vr7u8MOmDbriyyURb-5aOQVSjv1qwMs0,3857
40
- haiway-0.12.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
- haiway-0.12.1.dist-info/licenses/LICENSE,sha256=GehQEW_I1pkmxkkj3NEa7rCTQKYBn7vTPabpDYJlRuo,1063
42
- haiway-0.12.1.dist-info/RECORD,,
39
+ haiway-0.13.1.dist-info/METADATA,sha256=9YaEi2sxzl_QL76iqwkhmKrWJh_0EQsoaucu9Yf0eXo,4299
40
+ haiway-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
41
+ haiway-0.13.1.dist-info/licenses/LICENSE,sha256=3phcpHVNBP8jsi77gOO0E7rgKeDeu99Pi7DSnK9YHoQ,1069
42
+ haiway-0.13.1.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Miquido
3
+ Copyright (c) 2024-2025 Miquido
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
21
+ SOFTWARE.