aiohttp-asgi-connector 0.1.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.
- aiohttp_asgi_connector-0.1.0/LICENSE +28 -0
- aiohttp_asgi_connector-0.1.0/PKG-INFO +65 -0
- aiohttp_asgi_connector-0.1.0/README.md +55 -0
- aiohttp_asgi_connector-0.1.0/aiohttp_asgi_connector/__init__.py +3 -0
- aiohttp_asgi_connector-0.1.0/aiohttp_asgi_connector/connector.py +59 -0
- aiohttp_asgi_connector-0.1.0/aiohttp_asgi_connector/py.typed +0 -0
- aiohttp_asgi_connector-0.1.0/aiohttp_asgi_connector/transport.py +175 -0
- aiohttp_asgi_connector-0.1.0/pyproject.toml +55 -0
- aiohttp_asgi_connector-0.1.0/tests/conftest.py +41 -0
- aiohttp_asgi_connector-0.1.0/tests/test_connector.py +13 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Elias Gabriel
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: aiohttp-asgi-connector
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AIOHTTP Connector for running ASGI applications
|
|
5
|
+
Author-Email: thearchitector <me@eliasfgabriel.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Requires-Dist: aiohttp>=3
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# aiohttp-asgi
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
AIOHTTP Connector for running ASGI applications.
|
|
18
|
+
|
|
19
|
+
This library intends to increase the parity between AIOHTTP and HTTPX, specifically with HTTPX's `AsyncClient`. It is primarily intended to be used in test suite scenarios, or other situations where one would want to interface with an ASGI application directly instead of through a web server.
|
|
20
|
+
|
|
21
|
+
Supports Python 3.8+ and AIOHTTP 3+.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
$ pdm add aiohttp-asgi-connector
|
|
27
|
+
# or
|
|
28
|
+
$ python -m pip install --user aiohttp-asgi-connector
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
This library replaces the entire connection stack and pool underlying HTTP transport. AIOHTTP exposes custom connectors via the `connector` argument supplied when creating a `ClientSession` instance.
|
|
34
|
+
|
|
35
|
+
To use the `ASGIApplicationConnector`:
|
|
36
|
+
|
|
37
|
+
```py
|
|
38
|
+
import asyncio
|
|
39
|
+
from typing import Annotated # or from typing_extensions
|
|
40
|
+
|
|
41
|
+
from aiohttp_asgi_connector import ASGIApplicationConnector
|
|
42
|
+
from aiohttp import ClientSession
|
|
43
|
+
from fastapi import FastAPI, Body
|
|
44
|
+
|
|
45
|
+
app = FastAPI()
|
|
46
|
+
|
|
47
|
+
@app.post("/ping")
|
|
48
|
+
async def pong(message: Annotated[str, Body(embed=True)]):
|
|
49
|
+
return {"broadcast": f"Application says '{message}'!"}
|
|
50
|
+
|
|
51
|
+
async def main():
|
|
52
|
+
connector = ASGIApplicationConnector(app)
|
|
53
|
+
async with ClientSession(base_url="http://localhost", connector=connector) as session:
|
|
54
|
+
async with session.post("/ping", json={"message": "hello"}) as resp:
|
|
55
|
+
print(await resp.json())
|
|
56
|
+
# ==> {'broadcast': "Application says 'hello'!"}
|
|
57
|
+
|
|
58
|
+
asyncio.run(main())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This library does not handle ASGI lifespan events. If you want to run those events, use this library in conjunction with something like [asgi-lifespan](https://pypi.org/project/asgi-lifespan/).
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
This software is licensed under the [BSD 3-Clause License](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# aiohttp-asgi
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
AIOHTTP Connector for running ASGI applications.
|
|
8
|
+
|
|
9
|
+
This library intends to increase the parity between AIOHTTP and HTTPX, specifically with HTTPX's `AsyncClient`. It is primarily intended to be used in test suite scenarios, or other situations where one would want to interface with an ASGI application directly instead of through a web server.
|
|
10
|
+
|
|
11
|
+
Supports Python 3.8+ and AIOHTTP 3+.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
$ pdm add aiohttp-asgi-connector
|
|
17
|
+
# or
|
|
18
|
+
$ python -m pip install --user aiohttp-asgi-connector
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
This library replaces the entire connection stack and pool underlying HTTP transport. AIOHTTP exposes custom connectors via the `connector` argument supplied when creating a `ClientSession` instance.
|
|
24
|
+
|
|
25
|
+
To use the `ASGIApplicationConnector`:
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
import asyncio
|
|
29
|
+
from typing import Annotated # or from typing_extensions
|
|
30
|
+
|
|
31
|
+
from aiohttp_asgi_connector import ASGIApplicationConnector
|
|
32
|
+
from aiohttp import ClientSession
|
|
33
|
+
from fastapi import FastAPI, Body
|
|
34
|
+
|
|
35
|
+
app = FastAPI()
|
|
36
|
+
|
|
37
|
+
@app.post("/ping")
|
|
38
|
+
async def pong(message: Annotated[str, Body(embed=True)]):
|
|
39
|
+
return {"broadcast": f"Application says '{message}'!"}
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
connector = ASGIApplicationConnector(app)
|
|
43
|
+
async with ClientSession(base_url="http://localhost", connector=connector) as session:
|
|
44
|
+
async with session.post("/ping", json={"message": "hello"}) as resp:
|
|
45
|
+
print(await resp.json())
|
|
46
|
+
# ==> {'broadcast': "Application says 'hello'!"}
|
|
47
|
+
|
|
48
|
+
asyncio.run(main())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This library does not handle ASGI lifespan events. If you want to run those events, use this library in conjunction with something like [asgi-lifespan](https://pypi.org/project/asgi-lifespan/).
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
This software is licensed under the [BSD 3-Clause License](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from aiohttp import BaseConnector
|
|
4
|
+
|
|
5
|
+
from .transport import ASGITransport
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
8
|
+
from asyncio import AbstractEventLoop
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from aiohttp import ClientRequest, ClientTimeout
|
|
12
|
+
from aiohttp.client_proto import ResponseHandler
|
|
13
|
+
from aiohttp.client_reqrep import ConnectionKey
|
|
14
|
+
from aiohttp.tracing import Trace
|
|
15
|
+
|
|
16
|
+
from .transport import Application
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ASGIApplicationConnector(BaseConnector):
|
|
20
|
+
"""
|
|
21
|
+
A Connector that replaces the underlying connection transport with one that
|
|
22
|
+
intercepts and runs the provided ASGI application.
|
|
23
|
+
|
|
24
|
+
Since requests are handled by the ASGI application directly, there is no concept of
|
|
25
|
+
connection pooling with this connector; every request is processed immediately.
|
|
26
|
+
|
|
27
|
+
@param 'root_path' [""]: alters the root path of the constructed ASGI request
|
|
28
|
+
scope.
|
|
29
|
+
@param 'raise_app_exceptions' [True]: will cause the transport to propagate any
|
|
30
|
+
exceptions produced and not handled by the ASGI application.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
application: "Application",
|
|
36
|
+
root_path: str = "",
|
|
37
|
+
raise_app_exceptions: bool = True,
|
|
38
|
+
loop: "Optional[AbstractEventLoop]" = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(loop=loop)
|
|
41
|
+
self.app = application
|
|
42
|
+
self.root_path = root_path
|
|
43
|
+
self.raise_app_exceptions = raise_app_exceptions
|
|
44
|
+
|
|
45
|
+
async def _create_connection(
|
|
46
|
+
self, req: "ClientRequest", traces: "List[Trace]", timeout: "ClientTimeout"
|
|
47
|
+
) -> "ResponseHandler":
|
|
48
|
+
protocol = self._factory()
|
|
49
|
+
transport = ASGITransport(
|
|
50
|
+
protocol, self.app, req, self.root_path, self.raise_app_exceptions
|
|
51
|
+
)
|
|
52
|
+
protocol.connection_made(transport)
|
|
53
|
+
return protocol
|
|
54
|
+
|
|
55
|
+
def _available_connections(self, key: "ConnectionKey") -> int:
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
def _get(self, key: "ConnectionKey") -> None:
|
|
59
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from asyncio import Transport
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from aiohttp import Payload
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
8
|
+
from asyncio import Task
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
Awaitable,
|
|
12
|
+
Callable,
|
|
13
|
+
Dict,
|
|
14
|
+
Iterator,
|
|
15
|
+
List,
|
|
16
|
+
MutableMapping,
|
|
17
|
+
Optional,
|
|
18
|
+
Tuple,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from aiohttp import ClientRequest
|
|
22
|
+
from aiohttp.client_proto import ResponseHandler
|
|
23
|
+
|
|
24
|
+
Application = Callable[
|
|
25
|
+
[
|
|
26
|
+
Dict[str, Any],
|
|
27
|
+
Callable[[], Awaitable[Dict[str, Any]]],
|
|
28
|
+
Callable[[MutableMapping[str, Any]], Awaitable[None]],
|
|
29
|
+
],
|
|
30
|
+
Awaitable[None],
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
STATUS_CODE_TO_REASON: "Dict[int, str]" = {
|
|
34
|
+
200: "OK",
|
|
35
|
+
201: "Created",
|
|
36
|
+
202: "Accepted",
|
|
37
|
+
204: "No Content",
|
|
38
|
+
301: "Moved Permanently",
|
|
39
|
+
302: "Found",
|
|
40
|
+
400: "Bad Request",
|
|
41
|
+
401: "Unauthorized",
|
|
42
|
+
403: "Forbidden",
|
|
43
|
+
404: "Not Found",
|
|
44
|
+
500: "Internal Server Error",
|
|
45
|
+
502: "Bad Gateway",
|
|
46
|
+
503: "Service Unavailable",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ASGITransport(Transport):
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
protocol: "ResponseHandler",
|
|
54
|
+
app: "Application",
|
|
55
|
+
request: "ClientRequest",
|
|
56
|
+
root_path: str,
|
|
57
|
+
raise_app_exceptions: bool,
|
|
58
|
+
):
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.protocol = protocol
|
|
61
|
+
self.app = app
|
|
62
|
+
self.root_path = root_path
|
|
63
|
+
self.raise_app_exceptions = raise_app_exceptions
|
|
64
|
+
|
|
65
|
+
self.request = request
|
|
66
|
+
self.request_size = -1
|
|
67
|
+
self.request_handler: "Optional[Task[None]]" = None
|
|
68
|
+
|
|
69
|
+
self._closing = False
|
|
70
|
+
|
|
71
|
+
async def _handle_request(self) -> None:
|
|
72
|
+
scope: "Dict[str, Any]" = {
|
|
73
|
+
"type": "http",
|
|
74
|
+
"asgi": {"version": "3.0"},
|
|
75
|
+
"http_version": "1.1",
|
|
76
|
+
"method": self.request.method,
|
|
77
|
+
"headers": [(k.lower(), v) for (k, v) in self.request.headers.items()],
|
|
78
|
+
"scheme": self.request.url.scheme,
|
|
79
|
+
"path": self.request.url.path,
|
|
80
|
+
"raw_path": self.request.url.raw_path.split("?")[0],
|
|
81
|
+
"query_string": self.request.url.query,
|
|
82
|
+
"server": (self.request.url.host, self.request.url.port),
|
|
83
|
+
"client": ("127.0.0.1", 123),
|
|
84
|
+
"root_path": self.root_path,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
payload = BytesIO()
|
|
88
|
+
|
|
89
|
+
if isinstance(self.request.body, Payload):
|
|
90
|
+
|
|
91
|
+
class FalseWriter:
|
|
92
|
+
async def write(self, chunk: bytes) -> None:
|
|
93
|
+
payload.write(chunk)
|
|
94
|
+
|
|
95
|
+
await self.request.body.write(FalseWriter()) # type: ignore
|
|
96
|
+
elif isinstance(self.request.body, tuple):
|
|
97
|
+
for chunk in self.request.body:
|
|
98
|
+
payload.write(chunk)
|
|
99
|
+
else:
|
|
100
|
+
payload.write(self.request.body)
|
|
101
|
+
|
|
102
|
+
request_body_chunks: "Iterator[bytes]" = iter([payload.getvalue()])
|
|
103
|
+
status_code: "Optional[int]" = None
|
|
104
|
+
response_headers: "Optional[List[Tuple[bytes, bytes]]]" = None
|
|
105
|
+
response_body: bytearray = bytearray()
|
|
106
|
+
|
|
107
|
+
async def receive() -> "Dict[str, Any]":
|
|
108
|
+
try:
|
|
109
|
+
body = next(request_body_chunks)
|
|
110
|
+
except StopIteration:
|
|
111
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
112
|
+
return {"type": "http.request", "body": body, "more_body": True}
|
|
113
|
+
|
|
114
|
+
async def send(message: "MutableMapping[str, Any]") -> None:
|
|
115
|
+
nonlocal status_code, response_headers
|
|
116
|
+
|
|
117
|
+
if message["type"] == "http.response.start":
|
|
118
|
+
status_code = message["status"]
|
|
119
|
+
response_headers = message.get("headers", [])
|
|
120
|
+
elif message["type"] == "http.response.body":
|
|
121
|
+
body = message.get("body", b"")
|
|
122
|
+
if body and self.request.method != "HEAD":
|
|
123
|
+
response_body.extend(body)
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
await self.app(scope, receive, send)
|
|
127
|
+
except Exception:
|
|
128
|
+
if self.raise_app_exceptions:
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
if status_code is None:
|
|
132
|
+
status_code = 500
|
|
133
|
+
if response_headers is None:
|
|
134
|
+
response_headers = []
|
|
135
|
+
|
|
136
|
+
response_payload = self._encode_response(
|
|
137
|
+
status_code, response_headers, response_body
|
|
138
|
+
)
|
|
139
|
+
self.protocol.data_received(response_payload)
|
|
140
|
+
|
|
141
|
+
def _encode_response(
|
|
142
|
+
self, status: int, headers: "List[Tuple[bytes, bytes]]", body: bytearray
|
|
143
|
+
) -> bytes:
|
|
144
|
+
status_line = (
|
|
145
|
+
f"HTTP/1.1 {status} {STATUS_CODE_TO_REASON.get(status, 'Unknown')}"
|
|
146
|
+
)
|
|
147
|
+
header_line = "\r\n".join(
|
|
148
|
+
f"{name.decode()}: {value.decode()}" for name, value in headers
|
|
149
|
+
)
|
|
150
|
+
response = f"{status_line}\r\n{header_line}\r\n\r\n{body.decode()}"
|
|
151
|
+
return response.encode()
|
|
152
|
+
|
|
153
|
+
def write(self, data: bytes) -> None:
|
|
154
|
+
if self.request_size == -1:
|
|
155
|
+
self.request_size = 0
|
|
156
|
+
if size := getattr(self.request.body, "size", None):
|
|
157
|
+
self.max_request_size = size
|
|
158
|
+
else:
|
|
159
|
+
self.max_request_size = len(self.request.body)
|
|
160
|
+
else:
|
|
161
|
+
self.request_size += len(data)
|
|
162
|
+
|
|
163
|
+
if self.request_size == self.max_request_size:
|
|
164
|
+
# we've hit EOF. schedule the request for processing. we have to save this
|
|
165
|
+
# to a task since the event loop only holds weak refs and we don't want to
|
|
166
|
+
# GC in the middle of an execution
|
|
167
|
+
task = self.protocol._loop.create_task(self._handle_request())
|
|
168
|
+
self.request_handler = task
|
|
169
|
+
|
|
170
|
+
def close(self) -> None:
|
|
171
|
+
self._closing = True
|
|
172
|
+
del self.request_handler
|
|
173
|
+
|
|
174
|
+
def is_closing(self) -> bool:
|
|
175
|
+
return self._closing
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "aiohttp-asgi-connector"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "AIOHTTP Connector for running ASGI applications"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "thearchitector", email = "me@eliasfgabriel.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"aiohttp>=3",
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
|
|
14
|
+
[project.license]
|
|
15
|
+
text = "BSD-3-Clause"
|
|
16
|
+
|
|
17
|
+
[tool.pdm.dev-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"fastapi-slim>=0.111.0",
|
|
20
|
+
"pytest>=8.2.2",
|
|
21
|
+
"pytest-asyncio>=0.23.7",
|
|
22
|
+
"mypy>=1.10.1",
|
|
23
|
+
"typing-extensions>=4.12.2",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
addopts = "-ra -vv"
|
|
28
|
+
testpaths = [
|
|
29
|
+
"tests",
|
|
30
|
+
]
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
|
|
33
|
+
[tool.mypy]
|
|
34
|
+
strict = true
|
|
35
|
+
|
|
36
|
+
[tool.pyright]
|
|
37
|
+
ignore = [
|
|
38
|
+
"tests",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py310"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
extend-select = [
|
|
46
|
+
"B",
|
|
47
|
+
"I",
|
|
48
|
+
"ASYNC",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = [
|
|
53
|
+
"pdm-backend",
|
|
54
|
+
]
|
|
55
|
+
build-backend = "pdm.backend"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from aiohttp import ClientSession
|
|
3
|
+
from fastapi import Body, FastAPI
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from pytest_asyncio import is_async_test
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
from aiohttp_asgi_connector import ASGIApplicationConnector
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def pytest_collection_modifyitems(items):
|
|
12
|
+
pytest_asyncio_tests = (item for item in items if is_async_test(item))
|
|
13
|
+
session_scope_marker = pytest.mark.asyncio(scope="session")
|
|
14
|
+
for async_test in pytest_asyncio_tests:
|
|
15
|
+
async_test.add_marker(session_scope_marker, append=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
app = FastAPI(default_response_class=JSONResponse)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.get("/ping")
|
|
22
|
+
async def pong():
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.post("/post_ping")
|
|
27
|
+
async def post_ping(message: Annotated[str, Body(embed=True)]):
|
|
28
|
+
return {"broadcast": message}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(scope="session")
|
|
32
|
+
async def asgi_connector():
|
|
33
|
+
return ASGIApplicationConnector(app)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture(scope="session")
|
|
37
|
+
async def session(asgi_connector):
|
|
38
|
+
async with ClientSession(
|
|
39
|
+
base_url="http://localhost", connector=asgi_connector
|
|
40
|
+
) as session:
|
|
41
|
+
yield session
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
async def test_bad_method(session):
|
|
2
|
+
async with session.post("/ping") as resp:
|
|
3
|
+
assert (await resp.json()) == {"detail": "Method Not Allowed"}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def test_simple_get(session):
|
|
7
|
+
async with session.get("/ping") as resp:
|
|
8
|
+
assert (await resp.json()) is True
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def test_simple_post(session):
|
|
12
|
+
async with session.post("/post_ping", json={"message": "hello world"}) as resp:
|
|
13
|
+
assert (await resp.json()) == {"broadcast": "hello world"}
|