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.
asyncfast-0.5.0/PKG-INFO
ADDED
|
@@ -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
|