python-cq 0.26.0__tar.gz → 0.27.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 (37) hide show
  1. {python_cq-0.26.0 → python_cq-0.27.0}/PKG-INFO +4 -2
  2. {python_cq-0.26.0 → python_cq-0.27.0}/cq/__init__.py +0 -30
  3. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/middleware.py +3 -5
  4. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/command_pipeline.py +8 -1
  5. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/dispatchers/bus.py +19 -6
  6. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/handler.py +80 -55
  7. python_cq-0.27.0/cq/default.py +37 -0
  8. {python_cq-0.26.0 → python_cq-0.27.0}/docs/index.md +2 -1
  9. {python_cq-0.26.0 → python_cq-0.27.0}/pyproject.toml +2 -1
  10. {python_cq-0.26.0 → python_cq-0.27.0}/.gitignore +0 -0
  11. {python_cq-0.26.0 → python_cq-0.27.0}/LICENSE +0 -0
  12. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/__init__.py +0 -0
  13. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/common/__init__.py +0 -0
  14. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/common/typing.py +0 -0
  15. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/message.py +0 -0
  16. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/queuing/__init__.py +0 -0
  17. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/queuing/pump.py +0 -0
  18. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/queuing/queues/__init__.py +0 -0
  19. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/queuing/queues/abc.py +0 -0
  20. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/queuing/queues/memory.py +0 -0
  21. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/related_events.py +0 -0
  22. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/__init__.py +0 -0
  23. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/di.py +0 -0
  24. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/dispatchers/__init__.py +0 -0
  25. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/dispatchers/abc.py +0 -0
  26. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/dispatchers/functions.py +0 -0
  27. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/dispatchers/lazy.py +0 -0
  28. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/dispatchers/pipe.py +0 -0
  29. {python_cq-0.26.0 → python_cq-0.27.0}/cq/_core/routing/router.py +0 -0
  30. {python_cq-0.26.0 → python_cq-0.27.0}/cq/exceptions.py +0 -0
  31. {python_cq-0.26.0 → python_cq-0.27.0}/cq/ext/__init__.py +0 -0
  32. {python_cq-0.26.0 → python_cq-0.27.0}/cq/ext/injection.py +0 -0
  33. {python_cq-0.26.0 → python_cq-0.27.0}/cq/middlewares/__init__.py +0 -0
  34. {python_cq-0.26.0 → python_cq-0.27.0}/cq/middlewares/contextlib.py +0 -0
  35. {python_cq-0.26.0 → python_cq-0.27.0}/cq/middlewares/exc.py +0 -0
  36. {python_cq-0.26.0 → python_cq-0.27.0}/cq/middlewares/retry.py +0 -0
  37. {python_cq-0.26.0 → python_cq-0.27.0}/cq/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-cq
3
- Version: 0.26.0
3
+ Version: 0.27.0
4
4
  Summary: CQRS library for async Python projects.
5
5
  Project-URL: Documentation, https://python-cq.remimd.dev
6
6
  Project-URL: Repository, https://github.com/100nm/python-cq
@@ -26,6 +26,7 @@ Classifier: Typing :: Typed
26
26
  Requires-Python: <3.16,>=3.12
27
27
  Requires-Dist: anyio
28
28
  Requires-Dist: type-analyzer
29
+ Requires-Dist: typing-extensions; python_version < '3.13'
29
30
  Provides-Extra: injection
30
31
  Requires-Dist: python-injection[async]; extra == 'injection'
31
32
  Description-Content-Type: text/markdown
@@ -84,7 +85,8 @@ pip install python-cq
84
85
 
85
86
  ```python
86
87
  import asyncio
87
- from cq import CommandBus, command_handler
88
+ from cq import CommandBus
89
+ from cq.default import command_handler
88
90
  from dataclasses import dataclass
89
91
  from injection import inject
90
92
 
@@ -50,37 +50,7 @@ __all__ = (
50
50
  "Queue",
51
51
  "RelatedEvents",
52
52
  "Router",
53
- "__router__",
54
- "command_handler",
55
53
  "dispatch_sequentially",
56
- "event_handler",
57
- "new_command_bus",
58
- "new_event_bus",
59
- "new_query_bus",
60
- "query_handler",
61
54
  "resolve_handler_source",
62
55
  "through_middlewares",
63
56
  )
64
-
65
-
66
- try:
67
- from .ext.injection import InjectionAdapter
68
-
69
- except ImportError: # pragma: no cover
70
- __router__ = Router()
71
-
72
- else:
73
- __router__ = Router(InjectionAdapter())
74
- del InjectionAdapter
75
-
76
- __router__.register_defaults()
77
-
78
- command_handler = __router__.command_handler
79
- event_handler = __router__.event_handler
80
- query_handler = __router__.query_handler
81
-
82
- new_command_bus = __router__.new_command_bus
83
- new_event_bus = __router__.new_event_bus
84
- new_query_bus = __router__.new_query_bus
85
-
86
- ContextCommandPipeline._set_default_di(__router__.di)
@@ -4,7 +4,7 @@ from functools import wraps
4
4
  from inspect import isasyncgenfunction
5
5
  from typing import Any, Concatenate, Self, TypeGuard
6
6
 
7
- from cq._core.routing.handler import HandleFunction, HandlerType
7
+ from cq._core.routing.handler import HandlerEntry, HandlerType
8
8
  from cq.exceptions import MiddlewareError
9
9
 
10
10
  type MiddlewareResult[T] = AsyncGenerator[None, T]
@@ -86,15 +86,13 @@ class _BoundMiddleware[**P, T]:
86
86
 
87
87
 
88
88
  def resolve_handler_source[**P, T](
89
- call_next: Callable[P, Awaitable[T]]
90
- | _BoundMiddleware[P, T]
91
- | HandleFunction[P, T],
89
+ call_next: Callable[P, Awaitable[T]] | _BoundMiddleware[P, T] | HandlerEntry[P, T],
92
90
  /,
93
91
  ) -> HandlerType[P, T] | Any:
94
92
  while isinstance(call_next, _BoundMiddleware):
95
93
  call_next = call_next.call_next
96
94
 
97
- if isinstance(call_next, HandleFunction):
95
+ if isinstance(call_next, HandlerEntry):
98
96
  return call_next.source
99
97
 
100
98
  return call_next
@@ -21,7 +21,14 @@ class ContextCommandPipeline[C: Command](ContextPipeline[C]):
21
21
  __default_di: ClassVar[DIAdapter]
22
22
 
23
23
  def __init__(self, di: DIAdapter | None = None, /) -> None:
24
- di = di or self.__default_di
24
+ try:
25
+ di = di or self.__default_di
26
+ except AttributeError as exc:
27
+ raise RuntimeError(
28
+ "No DI adapter: import `cq.default` to use the default router, "
29
+ "or build the pipeline with `router.command_pipeline()`."
30
+ ) from exc
31
+
25
32
  super().__init__(LazyDispatcher(CommandBus, di))
26
33
  self.__query_dispatcher = LazyDispatcher(QueryBus, di)
27
34
 
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  from abc import ABC, abstractmethod
2
3
  from collections.abc import Awaitable, Callable, Iterator
3
4
  from typing import Any, Concatenate, Protocol, Self, runtime_checkable
@@ -8,13 +9,17 @@ from anyio.abc import TaskGroup
8
9
  from cq._core.middleware import Middleware
9
10
  from cq._core.routing.dispatchers.abc import BaseDispatcher, Dispatcher
10
11
  from cq._core.routing.handler import (
11
- HandleFunction,
12
- HandlerFactory,
12
+ HandlerEntry,
13
13
  HandlerRegistry,
14
14
  MultipleHandlerRegistry,
15
15
  SingleHandlerRegistry,
16
16
  )
17
17
 
18
+ if sys.version_info >= (3, 13): # pragma: no cover
19
+ from warnings import deprecated
20
+ else: # pragma: no cover
21
+ from typing_extensions import deprecated
22
+
18
23
  type Listener[T] = Callable[[T], Awaitable[Any]]
19
24
 
20
25
 
@@ -30,11 +35,16 @@ class Bus[I, O](Dispatcher[I, O], Protocol):
30
35
  def add_middlewares(self, *middlewares: Middleware[Concatenate[I, ...], O]) -> Self:
31
36
  raise NotImplementedError
32
37
 
38
+ @deprecated(
39
+ "`Bus.subscribe` is intended for tests, register handlers with the router "
40
+ "decorators (`command_handler`, `event_handler`, `query_handler`) instead.",
41
+ category=None,
42
+ )
33
43
  @abstractmethod
34
44
  def subscribe(
35
45
  self,
36
46
  message_type: type[I],
37
- factory: HandlerFactory[Concatenate[I, ...], O],
47
+ function: Callable[Concatenate[I, ...], Awaitable[O]],
38
48
  fail_silently: bool = ...,
39
49
  ) -> Self:
40
50
  raise NotImplementedError
@@ -58,16 +68,19 @@ class BaseBus[I, O](BaseDispatcher[I, O], Bus[I, O], ABC):
58
68
  def subscribe(
59
69
  self,
60
70
  message_type: type[I],
61
- factory: HandlerFactory[Concatenate[I, ...], O],
71
+ function: Callable[Concatenate[I, ...], Awaitable[O]],
62
72
  fail_silently: bool = False,
63
73
  ) -> Self:
64
- self.__registry.subscribe(message_type, factory, fail_silently=fail_silently)
74
+ self.__registry.subscribe(
75
+ message_type,
76
+ HandlerEntry.from_function(function, fail_silently),
77
+ )
65
78
  return self
66
79
 
67
80
  def _handlers_from(
68
81
  self,
69
82
  message_type: type[I],
70
- ) -> Iterator[HandleFunction[Concatenate[I, ...], O]]:
83
+ ) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
71
84
  return self.__registry.handlers_from(message_type)
72
85
 
73
86
  def _trigger_listeners(self, message: I, /, task_group: TaskGroup) -> None:
@@ -3,7 +3,7 @@ from collections import defaultdict
3
3
  from collections.abc import Awaitable, Callable, Iterator, KeysView
4
4
  from dataclasses import dataclass, field
5
5
  from functools import partial
6
- from inspect import Parameter, isclass, unwrap
6
+ from inspect import Parameter, isclass, isfunction, unwrap
7
7
  from inspect import signature as inspect_signature
8
8
  from typing import (
9
9
  TYPE_CHECKING,
@@ -34,23 +34,43 @@ class Handler[**P, T](Protocol):
34
34
 
35
35
 
36
36
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
37
- class HandleFunction[**P, T]:
38
- factory: HandlerFactory[P, T]
37
+ class HandlerEntry[**P, T]:
38
+ function: Callable[P, Awaitable[T]]
39
39
  source: HandlerType[P, T] | Any
40
40
  fail_silently: bool
41
41
 
42
42
  async def __call__(self, /, *args: P.args, **kwargs: P.kwargs) -> T:
43
- handler = await self.factory()
44
- return await handler.handle(*args, **kwargs)
43
+ return await self.function(*args, **kwargs)
45
44
 
46
45
  @classmethod
47
- def create(
46
+ def from_factory(
48
47
  cls,
49
48
  factory: HandlerFactory[P, T],
49
+ /,
50
50
  source: HandlerType[P, T] | None = None,
51
51
  fail_silently: bool = False,
52
52
  ) -> Self:
53
- return cls(factory, source or unwrap(factory), fail_silently)
53
+ function = partial(cls.__handle_with, factory)
54
+ return cls(function, source or unwrap(factory), fail_silently)
55
+
56
+ @classmethod
57
+ def from_function(
58
+ cls,
59
+ function: Callable[P, Awaitable[T]],
60
+ /,
61
+ fail_silently: bool = False,
62
+ ) -> Self:
63
+ return cls(function, function, fail_silently)
64
+
65
+ @staticmethod
66
+ async def __handle_with(
67
+ factory: HandlerFactory[P, T],
68
+ /,
69
+ *args: P.args,
70
+ **kwargs: P.kwargs,
71
+ ) -> T:
72
+ handler = await factory()
73
+ return await handler.handle(*args, **kwargs)
54
74
 
55
75
 
56
76
  @runtime_checkable
@@ -66,23 +86,21 @@ class HandlerRegistry[I, O](Protocol):
66
86
  def handlers_from(
67
87
  self,
68
88
  message_type: type[I],
69
- ) -> Iterator[HandleFunction[Concatenate[I, ...], O]]:
89
+ ) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
70
90
  raise NotImplementedError
71
91
 
72
92
  @abstractmethod
73
93
  def subscribe(
74
94
  self,
75
95
  message_type: type[I],
76
- handler_factory: HandlerFactory[Concatenate[I, ...], O],
77
- handler_type: HandlerType[Concatenate[I, ...], O] | None = ...,
78
- fail_silently: bool = ...,
96
+ entry: HandlerEntry[Concatenate[I, ...], O],
79
97
  ) -> Self:
80
98
  raise NotImplementedError
81
99
 
82
100
 
83
101
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
84
102
  class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
85
- __values: dict[type[I], list[HandleFunction[Concatenate[I, ...], O]]] = field(
103
+ __values: dict[type[I], list[HandlerEntry[Concatenate[I, ...], O]]] = field(
86
104
  default_factory=partial(defaultdict, list),
87
105
  init=False,
88
106
  )
@@ -94,28 +112,24 @@ class MultipleHandlerRegistry[I, O](HandlerRegistry[I, O]):
94
112
  def handlers_from(
95
113
  self,
96
114
  message_type: type[I],
97
- ) -> Iterator[HandleFunction[Concatenate[I, ...], O]]:
115
+ ) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
98
116
  for key_type in _iter_key_types(message_type):
99
117
  yield from self.__values.get(key_type, ())
100
118
 
101
119
  def subscribe(
102
120
  self,
103
121
  message_type: type[I],
104
- handler_factory: HandlerFactory[Concatenate[I, ...], O],
105
- handler_type: HandlerType[Concatenate[I, ...], O] | None = None,
106
- fail_silently: bool = False,
122
+ entry: HandlerEntry[Concatenate[I, ...], O],
107
123
  ) -> Self:
108
- function = HandleFunction.create(handler_factory, handler_type, fail_silently)
109
-
110
124
  for key_type in _build_key_types(message_type):
111
- self.__values[key_type].append(function)
125
+ self.__values[key_type].append(entry)
112
126
 
113
127
  return self
114
128
 
115
129
 
116
130
  @dataclass(repr=False, eq=False, frozen=True, slots=True)
117
131
  class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
118
- __values: dict[type[I], HandleFunction[Concatenate[I, ...], O]] = field(
132
+ __values: dict[type[I], HandlerEntry[Concatenate[I, ...], O]] = field(
119
133
  default_factory=dict,
120
134
  init=False,
121
135
  )
@@ -127,21 +141,18 @@ class SingleHandlerRegistry[I, O](HandlerRegistry[I, O]):
127
141
  def handlers_from(
128
142
  self,
129
143
  message_type: type[I],
130
- ) -> Iterator[HandleFunction[Concatenate[I, ...], O]]:
144
+ ) -> Iterator[HandlerEntry[Concatenate[I, ...], O]]:
131
145
  for key_type in _iter_key_types(message_type):
132
- function = self.__values.get(key_type, None)
133
- if function is not None:
134
- yield function
146
+ entry = self.__values.get(key_type, None)
147
+ if entry is not None:
148
+ yield entry
135
149
 
136
150
  def subscribe(
137
151
  self,
138
152
  message_type: type[I],
139
- handler_factory: HandlerFactory[Concatenate[I, ...], O],
140
- handler_type: HandlerType[Concatenate[I, ...], O] | None = None,
141
- fail_silently: bool = False,
153
+ entry: HandlerEntry[Concatenate[I, ...], O],
142
154
  ) -> Self:
143
- function = HandleFunction.create(handler_factory, handler_type, fail_silently)
144
- entries = {key_type: function for key_type in _build_key_types(message_type)}
155
+ entries = {key_type: entry for key_type in _build_key_types(message_type)}
145
156
 
146
157
  for key_type in entries:
147
158
  if key_type in self.__values:
@@ -163,7 +174,7 @@ class HandlerDecorator[I, O]:
163
174
  @overload
164
175
  def __call__(
165
176
  self,
166
- message_or_handler_type: type[I],
177
+ message_type: type[I],
167
178
  /,
168
179
  *,
169
180
  fail_silently: bool = ...,
@@ -172,7 +183,7 @@ class HandlerDecorator[I, O]:
172
183
  @overload
173
184
  def __call__[T](
174
185
  self,
175
- message_or_handler_type: T,
186
+ handler: T,
176
187
  /,
177
188
  *,
178
189
  fail_silently: bool = ...,
@@ -181,7 +192,7 @@ class HandlerDecorator[I, O]:
181
192
  @overload
182
193
  def __call__(
183
194
  self,
184
- message_or_handler_type: None = ...,
195
+ nothing: None = ...,
185
196
  /,
186
197
  *,
187
198
  fail_silently: bool = ...,
@@ -189,40 +200,55 @@ class HandlerDecorator[I, O]:
189
200
 
190
201
  def __call__[T](
191
202
  self,
192
- message_or_handler_type: type[I] | T | None = None,
203
+ message_or_handler: type[I] | T | None = None,
193
204
  /,
194
205
  *,
195
206
  fail_silently: bool = False,
196
207
  ) -> Any:
197
- if (
198
- message_or_handler_type is not None
199
- and isclass(message_or_handler_type)
200
- and issubclass(message_or_handler_type, Handler)
201
- ):
202
- return self.__decorator(
203
- message_or_handler_type, # type: ignore[arg-type]
204
- fail_silently=fail_silently,
205
- )
208
+ if self.__try_subscribe(message_or_handler, None, fail_silently):
209
+ return message_or_handler
206
210
 
207
211
  return partial(
208
212
  self.__decorator,
209
- message_type=message_or_handler_type, # type: ignore[arg-type]
213
+ message_type=message_or_handler, # type: ignore[arg-type]
210
214
  fail_silently=fail_silently,
211
215
  )
212
216
 
213
- def __decorator(
217
+ def __decorator[T](
214
218
  self,
215
- wrapped: HandlerType[Concatenate[I, ...], O],
219
+ wrapped: T,
216
220
  /,
217
221
  *,
218
- message_type: type[I] | None = None,
219
- fail_silently: bool = False,
220
- ) -> HandlerType[Concatenate[I, ...], O]:
221
- factory = self.di.wire(wrapped)
222
- message_type = message_type or _resolve_message_type(wrapped)
223
- self.registry.subscribe(message_type, factory, wrapped, fail_silently)
222
+ message_type: type[I] | None,
223
+ fail_silently: bool,
224
+ ) -> T:
225
+ if not self.__try_subscribe(wrapped, message_type, fail_silently):
226
+ raise TypeError(f"`{wrapped}` isn't a handler.")
227
+
224
228
  return wrapped
225
229
 
230
+ def __try_subscribe(
231
+ self,
232
+ handler: Any,
233
+ message_type: type[I] | None,
234
+ fail_silently: bool,
235
+ ) -> bool:
236
+ if isfunction(handler):
237
+ entry = HandlerEntry.from_function(handler, fail_silently)
238
+ callee = handler
239
+
240
+ elif isclass(handler) and issubclass(handler, Handler):
241
+ factory = self.di.wire(handler)
242
+ entry = HandlerEntry.from_factory(factory, handler, fail_silently)
243
+ callee = handler.handle.__get__(NotImplemented, handler)
244
+
245
+ else:
246
+ return False
247
+
248
+ message_type = message_type or _resolve_message_type(callee)
249
+ self.registry.subscribe(message_type, entry)
250
+ return True
251
+
226
252
 
227
253
  def _build_key_types(message_type: Any) -> tuple[Any, ...]:
228
254
  config = MatchingTypesConfig(ignore_none=True)
@@ -239,10 +265,9 @@ def _iter_key_types(message_type: Any) -> Iterator[Any]:
239
265
 
240
266
 
241
267
  def _resolve_message_type[I, O](
242
- handler_type: HandlerType[Concatenate[I, ...], O],
268
+ handler: Callable[Concatenate[I, ...], Awaitable[O]],
243
269
  ) -> type[I]:
244
- fake_method = handler_type.handle.__get__(NotImplemented, handler_type)
245
- signature = inspect_signature(fake_method, eval_str=True)
270
+ signature = inspect_signature(handler, eval_str=True)
246
271
 
247
272
  for parameter in signature.parameters.values():
248
273
  message_type = parameter.annotation
@@ -253,6 +278,6 @@ def _resolve_message_type[I, O](
253
278
  return message_type
254
279
 
255
280
  raise TypeError(
256
- f"Unable to resolve message type for handler `{handler_type}`, "
281
+ f"Unable to resolve message type for handler `{handler}`, "
257
282
  "`handle` method must have a type annotation for its first parameter."
258
283
  )
@@ -0,0 +1,37 @@
1
+ from contextlib import suppress
2
+
3
+ from ._core.routing.command_pipeline import ContextCommandPipeline
4
+ from ._core.routing.di import DIAdapter
5
+ from ._core.routing.router import Router
6
+
7
+ __all__ = (
8
+ "command_handler",
9
+ "event_handler",
10
+ "new_command_bus",
11
+ "new_event_bus",
12
+ "new_query_bus",
13
+ "query_handler",
14
+ "router",
15
+ )
16
+
17
+
18
+ def __detect_di_adapter() -> DIAdapter | None:
19
+ with suppress(ImportError):
20
+ from .ext.injection import InjectionAdapter
21
+
22
+ return InjectionAdapter()
23
+
24
+ return None # pragma: no cover
25
+
26
+
27
+ router = Router(__detect_di_adapter()).register_defaults()
28
+
29
+ command_handler = router.command_handler
30
+ event_handler = router.event_handler
31
+ query_handler = router.query_handler
32
+
33
+ new_command_bus = router.new_command_bus
34
+ new_event_bus = router.new_event_bus
35
+ new_query_bus = router.new_query_bus
36
+
37
+ ContextCommandPipeline._set_default_di(router.di)
@@ -52,7 +52,8 @@ pip install python-cq
52
52
 
53
53
  ```python
54
54
  import asyncio
55
- from cq import CommandBus, command_handler
55
+ from cq import CommandBus
56
+ from cq.default import command_handler
56
57
  from dataclasses import dataclass
57
58
  from injection import inject
58
59
 
@@ -20,7 +20,7 @@ test = [
20
20
 
21
21
  [project]
22
22
  name = "python-cq"
23
- version = "0.26.0"
23
+ version = "0.27.0"
24
24
  description = "CQRS library for async Python projects."
25
25
  license = "MIT"
26
26
  license-files = ["LICENSE"]
@@ -48,6 +48,7 @@ classifiers = [
48
48
  dependencies = [
49
49
  "anyio",
50
50
  "type-analyzer",
51
+ "typing-extensions; python_version < '3.13'",
51
52
  ]
52
53
 
53
54
  [project.optional-dependencies]
File without changes
File without changes
File without changes
File without changes