asyncfast 0.5.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.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.3
2
+ Name: asyncfast
3
+ Version: 0.5.0
4
+ Summary: Add your description here
5
+ Author: jack.burridge
6
+ Author-email: jack.burridge <jack.burridge@mail.com>
7
+ Requires-Dist: pydantic>=2.11.7
8
+ Requires-Dist: types-acgi
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
File without changes
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "asyncfast"
3
+ version = "0.5.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "jack.burridge", email = "jack.burridge@mail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "pydantic>=2.11.7",
12
+ "types-acgi",
13
+ ]
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.8.4,<0.9.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.sources]
20
+ types-acgi = { workspace = true }
21
+
22
+ [dependency-groups]
23
+ dev = [
24
+ "pytest>=8.4.1",
25
+ "pytest-asyncio>=1.1.0",
26
+ ]
@@ -0,0 +1,124 @@
1
+ import inspect
2
+ from functools import partial
3
+ from inspect import Signature
4
+ from typing import Any
5
+ from typing import Awaitable
6
+ from typing import Callable
7
+ from typing import Generator
8
+ from typing import Iterable
9
+ from typing import Iterator
10
+ from typing import List
11
+ from typing import Mapping
12
+ from typing import Tuple
13
+ from typing import TypeVar
14
+
15
+ from pydantic import BaseModel
16
+ from pydantic import TypeAdapter
17
+ from types_acgi import ACGIReceiveCallable
18
+ from types_acgi import ACGISendCallable
19
+ from types_acgi import MessageScope
20
+ from types_acgi import Scope
21
+ from typing_extensions import Annotated
22
+ from typing_extensions import get_args
23
+ from typing_extensions import get_origin
24
+
25
+ DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any])
26
+
27
+
28
+ class AsyncFast:
29
+ def __init__(self) -> None:
30
+ self._channels: List[Channel] = []
31
+
32
+ def channel(self, name: str) -> Callable[[DecoratedCallable], DecoratedCallable]:
33
+ return partial(self._add_channel, name)
34
+
35
+ def _add_channel(self, name: str, function: DecoratedCallable) -> DecoratedCallable:
36
+ self._channels.append(Channel(name, function))
37
+ return function
38
+
39
+ async def __call__(
40
+ self, scope: Scope, receive: ACGIReceiveCallable, send: ACGISendCallable
41
+ ) -> None:
42
+ if scope["type"] == "lifespan":
43
+ while True:
44
+ message = await receive()
45
+ if message["type"] == "lifespan.startup":
46
+ await send({"type": "lifespan.startup.complete"})
47
+ elif message["type"] == "lifespan.shutdown":
48
+ await send({"type": "lifespan.shutdown.complete"})
49
+ return
50
+ elif scope["type"] == "message":
51
+ address = scope["address"]
52
+ for channel in self._channels:
53
+ if channel.name == address:
54
+ await channel(scope, receive, send)
55
+ break
56
+
57
+
58
+ class Channel:
59
+
60
+ def __init__(self, name: str, handler: Callable[..., Awaitable[None]]) -> None:
61
+ self.name = name
62
+ self._handler = handler
63
+
64
+ async def __call__(
65
+ self, scope: MessageScope, receive: ACGIReceiveCallable, send: ACGISendCallable
66
+ ) -> None:
67
+ signature = inspect.signature(self._handler)
68
+ arguments = dict(_generate_arguments(scope, signature))
69
+ if inspect.isasyncgenfunction(self._handler):
70
+ async for message in self._handler(**arguments):
71
+ await send(
72
+ {
73
+ "type": "message.send",
74
+ "address": message.address,
75
+ "headers": message.headers,
76
+ "payload": message.payload,
77
+ }
78
+ )
79
+ else:
80
+ await self._handler(**arguments)
81
+
82
+
83
+ def _generate_arguments(
84
+ scope: MessageScope, signature: Signature
85
+ ) -> Generator[Tuple[str, Any], None, None]:
86
+ headers = Headers(scope["headers"])
87
+ for name, parameter in signature.parameters.items():
88
+ annotation = parameter.annotation
89
+ if issubclass(annotation, BaseModel):
90
+ yield name, annotation.model_validate_json(scope["payload"])
91
+ if get_origin(annotation) is Annotated:
92
+ annotated_args = get_args(annotation)
93
+ if isinstance(annotated_args[1], Header):
94
+ alias = name.replace("_", "-")
95
+ header = headers.get(alias, parameter.default)
96
+ value = TypeAdapter(annotated_args[0]).validate_python(
97
+ header, from_attributes=True
98
+ )
99
+ yield name, value
100
+
101
+
102
+ class Header:
103
+ pass
104
+
105
+
106
+ class Headers(Mapping[str, str]):
107
+
108
+ def __init__(self, raw_list: Iterable[Tuple[bytes, bytes]]) -> None:
109
+ self.raw_list = list(raw_list)
110
+
111
+ def __getitem__(self, key: str, /) -> str:
112
+ for header_key, header_value in self.raw_list:
113
+ if header_key.decode().lower() == key.lower():
114
+ return header_value.decode()
115
+ raise KeyError(key)
116
+
117
+ def __len__(self) -> int:
118
+ return len(self.raw_list)
119
+
120
+ def __iter__(self) -> Iterator[str]:
121
+ return iter(self.keys())
122
+
123
+ def keys(self) -> list[str]: # type: ignore[override]
124
+ return [key.decode() for key, _ in self.raw_list]
File without changes