dhttpy 0.1.0__cp313-cp313-win_amd64.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.
- dhttpy/__init__.py +32 -0
- dhttpy/_native.cp313-win_amd64.pyd +0 -0
- dhttpy/authority.py +8 -0
- dhttpy/certificate.py +8 -0
- dhttpy/endpoint.py +515 -0
- dhttpy/raw.py +120 -0
- dhttpy/response.py +258 -0
- dhttpy-0.1.0.dist-info/METADATA +144 -0
- dhttpy-0.1.0.dist-info/RECORD +11 -0
- dhttpy-0.1.0.dist-info/WHEEL +4 -0
- dhttpy-0.1.0.dist-info/sboms/dhttp-api.cyclonedx.json +17582 -0
dhttpy/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Python bindings for DHTTP."""
|
|
2
|
+
|
|
3
|
+
from . import _native
|
|
4
|
+
from .authority import LocalAuthority, RemoteAuthority
|
|
5
|
+
from .certificate import CertificateChainKey, DhttpSubjectKeyIdentifier
|
|
6
|
+
from .endpoint import Endpoint, QueryParams, ServerRequest, Service
|
|
7
|
+
from .response import ClientResponse, Headers, Response, StreamContent, json_response
|
|
8
|
+
|
|
9
|
+
DhttpHome = _native.DhttpHome
|
|
10
|
+
IdentityProfile = _native.IdentityProfile
|
|
11
|
+
Identity = _native.Identity
|
|
12
|
+
ServeHandle = _native.ServeHandle
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CertificateChainKey",
|
|
16
|
+
"ClientResponse",
|
|
17
|
+
"DhttpHome",
|
|
18
|
+
"DhttpSubjectKeyIdentifier",
|
|
19
|
+
"Endpoint",
|
|
20
|
+
"Headers",
|
|
21
|
+
"Identity",
|
|
22
|
+
"IdentityProfile",
|
|
23
|
+
"LocalAuthority",
|
|
24
|
+
"QueryParams",
|
|
25
|
+
"RemoteAuthority",
|
|
26
|
+
"Response",
|
|
27
|
+
"ServeHandle",
|
|
28
|
+
"ServerRequest",
|
|
29
|
+
"Service",
|
|
30
|
+
"StreamContent",
|
|
31
|
+
"json_response",
|
|
32
|
+
]
|
|
Binary file
|
dhttpy/authority.py
ADDED
dhttpy/certificate.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Certificate metadata value classes for Python bindings."""
|
|
2
|
+
|
|
3
|
+
from . import _native
|
|
4
|
+
|
|
5
|
+
CertificateChainKey = _native.CertificateChainKey
|
|
6
|
+
DhttpSubjectKeyIdentifier = _native.DhttpSubjectKeyIdentifier
|
|
7
|
+
|
|
8
|
+
__all__ = ["CertificateChainKey", "DhttpSubjectKeyIdentifier"]
|
dhttpy/endpoint.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""aiohttp-like endpoint wrapper for dhttp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json as _json
|
|
7
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
|
|
8
|
+
from inspect import isawaitable
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import parse_qsl, urlsplit
|
|
11
|
+
|
|
12
|
+
from . import _native, raw
|
|
13
|
+
from .response import (
|
|
14
|
+
ClientResponse,
|
|
15
|
+
HeaderInput,
|
|
16
|
+
Headers,
|
|
17
|
+
Response,
|
|
18
|
+
StreamContent,
|
|
19
|
+
has_body,
|
|
20
|
+
header_fields,
|
|
21
|
+
header_text,
|
|
22
|
+
normalize_headers,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
BodyInput = bytes | bytearray | memoryview | str | Iterable[bytes] | AsyncIterator[bytes] | None
|
|
26
|
+
Handler = Callable[..., Response | bytes | str |
|
|
27
|
+
None | Awaitable[Response | bytes | str | None]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _path_with_query(url: str) -> str:
|
|
31
|
+
parts = urlsplit(url)
|
|
32
|
+
path = parts.path or "/"
|
|
33
|
+
if parts.query:
|
|
34
|
+
path = f"{path}?{parts.query}"
|
|
35
|
+
return path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _authority(url: str) -> str:
|
|
39
|
+
parts = urlsplit(url)
|
|
40
|
+
if not parts.netloc:
|
|
41
|
+
raise ValueError("url must include an authority")
|
|
42
|
+
return parts.netloc
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _request_header_fields(method: str, url: str, headers: HeaderInput) -> list[raw.HeaderField]:
|
|
46
|
+
parts = urlsplit(url)
|
|
47
|
+
if not parts.scheme or not parts.netloc:
|
|
48
|
+
raise ValueError("url must include scheme and authority")
|
|
49
|
+
fields = [
|
|
50
|
+
raw.HeaderField(b":method", method.upper().encode("latin-1")),
|
|
51
|
+
raw.HeaderField(b":scheme", parts.scheme.encode("latin-1")),
|
|
52
|
+
raw.HeaderField(b":authority", parts.netloc.encode("latin-1")),
|
|
53
|
+
raw.HeaderField(b":path", _path_with_query(url).encode("latin-1")),
|
|
54
|
+
]
|
|
55
|
+
fields.extend(raw.HeaderField(name, value) for name, value in header_fields(headers))
|
|
56
|
+
return fields
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _request_body_and_headers(
|
|
60
|
+
headers: HeaderInput,
|
|
61
|
+
data: BodyInput,
|
|
62
|
+
json: Any,
|
|
63
|
+
) -> tuple[HeaderInput, BodyInput]:
|
|
64
|
+
if data is not None and json is not None:
|
|
65
|
+
raise ValueError("only one of data or json may be provided")
|
|
66
|
+
|
|
67
|
+
if json is None:
|
|
68
|
+
return headers, data
|
|
69
|
+
|
|
70
|
+
pairs = normalize_headers(headers)
|
|
71
|
+
if not any(name.lower() == "content-type" for name, _ in pairs):
|
|
72
|
+
pairs.append(("content-type", "application/json"))
|
|
73
|
+
return pairs, _json.dumps(json).encode("utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _endpoint_options(
|
|
77
|
+
options: Any = None,
|
|
78
|
+
*,
|
|
79
|
+
identity: Any = None,
|
|
80
|
+
dns_schemes: Iterable[str] | None = None,
|
|
81
|
+
bind_patterns: Iterable[str] | None = None,
|
|
82
|
+
):
|
|
83
|
+
has_keywords = identity is not None or dns_schemes is not None or bind_patterns is not None
|
|
84
|
+
if options is not None:
|
|
85
|
+
if has_keywords:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
"pass either options or keyword configuration, not both")
|
|
88
|
+
return options
|
|
89
|
+
if not has_keywords:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
options = _native.EndpointOptions()
|
|
93
|
+
if identity is not None:
|
|
94
|
+
options.set_identity(identity)
|
|
95
|
+
for scheme in dns_schemes or ():
|
|
96
|
+
options.add_dns_scheme(scheme)
|
|
97
|
+
for pattern in bind_patterns or ():
|
|
98
|
+
options.add_bind_pattern(pattern)
|
|
99
|
+
return options
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _response_header_fields(response: Response) -> list[raw.HeaderField]:
|
|
103
|
+
fields = [raw.HeaderField(b":status", str(response.status).encode("latin-1"))]
|
|
104
|
+
fields.extend(
|
|
105
|
+
raw.HeaderField(name, value) for name, value in header_fields(response.headers.items())
|
|
106
|
+
)
|
|
107
|
+
return fields
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _field_parts(field: raw.HeaderField | tuple[bytes, bytes]) -> tuple[bytes, bytes]:
|
|
111
|
+
if isinstance(field, raw.HeaderField):
|
|
112
|
+
return field.name, field.value
|
|
113
|
+
return field
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _parse_response_header(fields: list[tuple[bytes, bytes]] | None) -> tuple[int, Headers]:
|
|
117
|
+
if fields is None:
|
|
118
|
+
raise RuntimeError("response header frame is missing")
|
|
119
|
+
status: int | None = None
|
|
120
|
+
headers: list[tuple[str, str]] = []
|
|
121
|
+
for field in fields:
|
|
122
|
+
name_bytes, value_bytes = _field_parts(field)
|
|
123
|
+
name = header_text(name_bytes).lower()
|
|
124
|
+
value = header_text(value_bytes)
|
|
125
|
+
if name == ":status":
|
|
126
|
+
status = int(value)
|
|
127
|
+
elif not name.startswith(":"):
|
|
128
|
+
headers.append((name, value))
|
|
129
|
+
if status is None:
|
|
130
|
+
raise RuntimeError("response status pseudo-header is missing")
|
|
131
|
+
return status, Headers(headers)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_request_header(fields: list[tuple[bytes, bytes]] | None) -> tuple[str, str, Headers]:
|
|
135
|
+
if fields is None:
|
|
136
|
+
raise RuntimeError("request header frame is missing")
|
|
137
|
+
pseudo: dict[str, str] = {}
|
|
138
|
+
headers: list[tuple[str, str]] = []
|
|
139
|
+
for field in fields:
|
|
140
|
+
name_bytes, value_bytes = _field_parts(field)
|
|
141
|
+
name = header_text(name_bytes).lower()
|
|
142
|
+
value = header_text(value_bytes)
|
|
143
|
+
if name.startswith(":"):
|
|
144
|
+
pseudo[name] = value
|
|
145
|
+
else:
|
|
146
|
+
headers.append((name, value))
|
|
147
|
+
method = pseudo.get(":method")
|
|
148
|
+
scheme = pseudo.get(":scheme")
|
|
149
|
+
authority = pseudo.get(":authority")
|
|
150
|
+
path = pseudo.get(":path")
|
|
151
|
+
if method is None or scheme is None or authority is None or path is None:
|
|
152
|
+
raise RuntimeError("request pseudo-headers are missing")
|
|
153
|
+
return method, f"{scheme}://{authority}{path}", Headers(headers)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def _body_chunks(body: BodyInput):
|
|
157
|
+
if body is None:
|
|
158
|
+
return
|
|
159
|
+
if isinstance(body, str):
|
|
160
|
+
yield body.encode()
|
|
161
|
+
return
|
|
162
|
+
if isinstance(body, bytes | bytearray | memoryview):
|
|
163
|
+
yield bytes(body)
|
|
164
|
+
return
|
|
165
|
+
if hasattr(body, "__aiter__"):
|
|
166
|
+
async for chunk in body: # type: ignore[union-attr]
|
|
167
|
+
if chunk:
|
|
168
|
+
yield bytes(chunk)
|
|
169
|
+
return
|
|
170
|
+
for chunk in body: # type: ignore[union-attr]
|
|
171
|
+
if chunk:
|
|
172
|
+
yield bytes(chunk)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def _write_body(write_stream: Any, body: BodyInput) -> None:
|
|
176
|
+
async for chunk in _body_chunks(body):
|
|
177
|
+
await write_stream.write_data(chunk)
|
|
178
|
+
await write_stream.close()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class QueryParams:
|
|
182
|
+
"""Ordered case-sensitive query parameter view preserving duplicate keys."""
|
|
183
|
+
|
|
184
|
+
def __init__(self, pairs: Iterable[tuple[str, str]] = ()):
|
|
185
|
+
self._pairs = [(str(name), str(value)) for name, value in pairs]
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_query_string(cls, query_string: str) -> "QueryParams":
|
|
189
|
+
return cls(parse_qsl(query_string, keep_blank_values=True))
|
|
190
|
+
|
|
191
|
+
def __iter__(self):
|
|
192
|
+
return iter(self._pairs)
|
|
193
|
+
|
|
194
|
+
def __len__(self) -> int:
|
|
195
|
+
return len(self._pairs)
|
|
196
|
+
|
|
197
|
+
def __contains__(self, name: object) -> bool:
|
|
198
|
+
if not isinstance(name, str):
|
|
199
|
+
return False
|
|
200
|
+
return any(param == name for param, _ in self._pairs)
|
|
201
|
+
|
|
202
|
+
def items(self) -> list[tuple[str, str]]:
|
|
203
|
+
return list(self._pairs)
|
|
204
|
+
|
|
205
|
+
def get(self, name: str, default: str | None = None) -> str | None:
|
|
206
|
+
for param, value in self._pairs:
|
|
207
|
+
if param == name:
|
|
208
|
+
return value
|
|
209
|
+
return default
|
|
210
|
+
|
|
211
|
+
def getall(self, name: str) -> list[str]:
|
|
212
|
+
return [value for param, value in self._pairs if param == name]
|
|
213
|
+
|
|
214
|
+
def __getitem__(self, name: str) -> str:
|
|
215
|
+
for param, value in self._pairs:
|
|
216
|
+
if param == name:
|
|
217
|
+
return value
|
|
218
|
+
raise KeyError(name)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ServerRequest:
|
|
222
|
+
def __init__(
|
|
223
|
+
self,
|
|
224
|
+
read_stream: Any,
|
|
225
|
+
method: str,
|
|
226
|
+
url: str,
|
|
227
|
+
headers: Headers,
|
|
228
|
+
*,
|
|
229
|
+
authority: Any = None,
|
|
230
|
+
):
|
|
231
|
+
self._read_stream = read_stream
|
|
232
|
+
self._body: bytes | None = None
|
|
233
|
+
self._released = False
|
|
234
|
+
self._authority = authority
|
|
235
|
+
self.content = StreamContent(read_stream)
|
|
236
|
+
self.method = method.upper()
|
|
237
|
+
self.url = url
|
|
238
|
+
self.headers = headers
|
|
239
|
+
parts = urlsplit(url)
|
|
240
|
+
self.scheme = parts.scheme
|
|
241
|
+
self.host = parts.netloc
|
|
242
|
+
self.path = parts.path or "/"
|
|
243
|
+
self.query_string = parts.query
|
|
244
|
+
self.query = QueryParams.from_query_string(parts.query)
|
|
245
|
+
|
|
246
|
+
def authority(self):
|
|
247
|
+
return self._authority
|
|
248
|
+
|
|
249
|
+
async def read(self) -> bytes:
|
|
250
|
+
if self._body is not None:
|
|
251
|
+
return self._body
|
|
252
|
+
if self._released:
|
|
253
|
+
return b""
|
|
254
|
+
body = await self.content.read()
|
|
255
|
+
self._released = True
|
|
256
|
+
self._body = body
|
|
257
|
+
return self._body
|
|
258
|
+
|
|
259
|
+
async def text(self, encoding: str = "utf-8") -> str:
|
|
260
|
+
return (await self.read()).decode(encoding)
|
|
261
|
+
|
|
262
|
+
async def json(self) -> Any:
|
|
263
|
+
return _json.loads(await self.text())
|
|
264
|
+
|
|
265
|
+
async def release(self) -> None:
|
|
266
|
+
if self._released:
|
|
267
|
+
return
|
|
268
|
+
self._released = True
|
|
269
|
+
try:
|
|
270
|
+
await self._read_stream.stop(0)
|
|
271
|
+
except RuntimeError:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class _RequestContextManager:
|
|
276
|
+
def __init__(self, awaitable: Awaitable[ClientResponse]):
|
|
277
|
+
self._awaitable = awaitable
|
|
278
|
+
self._response: ClientResponse | None = None
|
|
279
|
+
|
|
280
|
+
def __await__(self):
|
|
281
|
+
return self._awaitable.__await__()
|
|
282
|
+
|
|
283
|
+
async def __aenter__(self) -> ClientResponse:
|
|
284
|
+
self._response = await self._awaitable
|
|
285
|
+
return self._response
|
|
286
|
+
|
|
287
|
+
async def __aexit__(self, exc_type, exc, traceback) -> None:
|
|
288
|
+
if self._response is not None:
|
|
289
|
+
await self._response.release()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class Endpoint:
|
|
293
|
+
def __init__(self, inner: Any):
|
|
294
|
+
self._inner = inner
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
async def create(
|
|
298
|
+
cls,
|
|
299
|
+
options: Any = None,
|
|
300
|
+
*,
|
|
301
|
+
identity: Any = None,
|
|
302
|
+
dns_schemes: Iterable[str] | None = None,
|
|
303
|
+
bind_patterns: Iterable[str] | None = None,
|
|
304
|
+
) -> "Endpoint":
|
|
305
|
+
return cls(
|
|
306
|
+
await _native.Endpoint.create(
|
|
307
|
+
_endpoint_options(
|
|
308
|
+
options,
|
|
309
|
+
identity=identity,
|
|
310
|
+
dns_schemes=dns_schemes,
|
|
311
|
+
bind_patterns=bind_patterns,
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
async def load(cls, name: str) -> "Endpoint":
|
|
318
|
+
return cls(await _native.Endpoint.load(name))
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
async def load_from(cls, path: str) -> "Endpoint":
|
|
322
|
+
return cls(await _native.Endpoint.load_from(path))
|
|
323
|
+
|
|
324
|
+
def identity(self):
|
|
325
|
+
return self._inner.identity()
|
|
326
|
+
|
|
327
|
+
def bind_patterns(self) -> list[str]:
|
|
328
|
+
return self._inner.bind_patterns()
|
|
329
|
+
|
|
330
|
+
def request(
|
|
331
|
+
self,
|
|
332
|
+
method: str,
|
|
333
|
+
url: str,
|
|
334
|
+
*,
|
|
335
|
+
headers: HeaderInput = None,
|
|
336
|
+
data: BodyInput = None,
|
|
337
|
+
json: Any = None,
|
|
338
|
+
) -> _RequestContextManager:
|
|
339
|
+
return _RequestContextManager(
|
|
340
|
+
self._request(method, url, headers=headers,
|
|
341
|
+
data=data, json=json)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def get(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
345
|
+
return self.request("GET", url, **kwargs)
|
|
346
|
+
|
|
347
|
+
def post(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
348
|
+
return self.request("POST", url, **kwargs)
|
|
349
|
+
|
|
350
|
+
def put(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
351
|
+
return self.request("PUT", url, **kwargs)
|
|
352
|
+
|
|
353
|
+
def delete(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
354
|
+
return self.request("DELETE", url, **kwargs)
|
|
355
|
+
|
|
356
|
+
def patch(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
357
|
+
return self.request("PATCH", url, **kwargs)
|
|
358
|
+
|
|
359
|
+
def head(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
360
|
+
return self.request("HEAD", url, **kwargs)
|
|
361
|
+
|
|
362
|
+
def options(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
363
|
+
return self.request("OPTIONS", url, **kwargs)
|
|
364
|
+
|
|
365
|
+
def trace(self, url: str, **kwargs: Any) -> _RequestContextManager:
|
|
366
|
+
return self.request("TRACE", url, **kwargs)
|
|
367
|
+
|
|
368
|
+
async def connect(self, authority: str) -> raw.Connection:
|
|
369
|
+
return raw.Connection(await self._inner.connect(authority))
|
|
370
|
+
|
|
371
|
+
async def _request(
|
|
372
|
+
self,
|
|
373
|
+
method: str,
|
|
374
|
+
url: str,
|
|
375
|
+
*,
|
|
376
|
+
headers: HeaderInput,
|
|
377
|
+
data: BodyInput,
|
|
378
|
+
json: Any,
|
|
379
|
+
) -> ClientResponse:
|
|
380
|
+
headers, body = _request_body_and_headers(headers, data, json)
|
|
381
|
+
connection = await self.connect(_authority(url))
|
|
382
|
+
request = await connection.open_request()
|
|
383
|
+
read_stream = request.reader
|
|
384
|
+
write_stream = request.writer
|
|
385
|
+
authority = await request.remote_authority()
|
|
386
|
+
upload_task: asyncio.Task[None] | None = None
|
|
387
|
+
try:
|
|
388
|
+
await write_stream.write_header(_request_header_fields(method, url, headers))
|
|
389
|
+
upload_task = asyncio.create_task(_write_body(write_stream, body))
|
|
390
|
+
await asyncio.sleep(0)
|
|
391
|
+
status, response_headers = _parse_response_header(await read_stream.read_header())
|
|
392
|
+
return ClientResponse(
|
|
393
|
+
read_stream,
|
|
394
|
+
status,
|
|
395
|
+
response_headers.items(),
|
|
396
|
+
method=method.upper(),
|
|
397
|
+
url=url,
|
|
398
|
+
authority=authority,
|
|
399
|
+
upload_task=upload_task,
|
|
400
|
+
)
|
|
401
|
+
except Exception:
|
|
402
|
+
if upload_task is not None:
|
|
403
|
+
upload_task.cancel()
|
|
404
|
+
try:
|
|
405
|
+
await read_stream.stop(0)
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
try:
|
|
409
|
+
await write_stream.reset(0)
|
|
410
|
+
except Exception:
|
|
411
|
+
pass
|
|
412
|
+
raise
|
|
413
|
+
|
|
414
|
+
def listen(self, handler: Callable[[raw.UnresolvedRequest], Any]):
|
|
415
|
+
async def raw_handler(native_request: Any) -> None:
|
|
416
|
+
result = handler(raw.UnresolvedRequest(native_request))
|
|
417
|
+
if isawaitable(result):
|
|
418
|
+
result = await result
|
|
419
|
+
if result is not None:
|
|
420
|
+
raise TypeError(
|
|
421
|
+
"Endpoint.listen(function) receives raw UnresolvedRequest; "
|
|
422
|
+
"use dhttpy.Service for ServerRequest -> Response handlers"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
return self._inner.listen_raw(raw_handler)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class Service:
|
|
429
|
+
def __init__(self):
|
|
430
|
+
self._routes: list[tuple[str | None, str, Handler]] = []
|
|
431
|
+
self._fallback: Handler = lambda _request: Response(status=404)
|
|
432
|
+
|
|
433
|
+
async def __call__(self, raw_request: raw.UnresolvedRequest) -> None:
|
|
434
|
+
reader = raw_request.reader
|
|
435
|
+
writer = raw_request.writer
|
|
436
|
+
request: ServerRequest | None = None
|
|
437
|
+
try:
|
|
438
|
+
method, url, headers = _parse_request_header(await reader.read_header())
|
|
439
|
+
authority = await raw_request.remote_authority()
|
|
440
|
+
request = ServerRequest(reader, method, url, headers, authority=authority)
|
|
441
|
+
handler = self._match(method, request.path)
|
|
442
|
+
result = handler(request)
|
|
443
|
+
if isawaitable(result):
|
|
444
|
+
result = await result
|
|
445
|
+
response = self._response_from_result(result)
|
|
446
|
+
await writer.write_header(_response_header_fields(response))
|
|
447
|
+
body = response.body if has_body(method, response.status) else None
|
|
448
|
+
await _write_body(writer, body)
|
|
449
|
+
except Exception:
|
|
450
|
+
try:
|
|
451
|
+
await writer.reset(0)
|
|
452
|
+
except Exception:
|
|
453
|
+
pass
|
|
454
|
+
raise
|
|
455
|
+
finally:
|
|
456
|
+
if request is not None:
|
|
457
|
+
await request.release()
|
|
458
|
+
else:
|
|
459
|
+
try:
|
|
460
|
+
await reader.stop(0)
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
def _match(self, method: str, path: str) -> Handler:
|
|
465
|
+
method = method.upper()
|
|
466
|
+
for route_method, route_path, handler in self._routes:
|
|
467
|
+
if route_path == path and (route_method is None or route_method == method):
|
|
468
|
+
return handler
|
|
469
|
+
return self._fallback
|
|
470
|
+
|
|
471
|
+
def _response_from_result(self, result: Response | bytes | str | None) -> Response:
|
|
472
|
+
if isinstance(result, Response):
|
|
473
|
+
return result
|
|
474
|
+
if result is None or isinstance(result, bytes | str):
|
|
475
|
+
return Response(result)
|
|
476
|
+
raise TypeError(f"handler returned unsupported response type {type(result).__name__}")
|
|
477
|
+
|
|
478
|
+
def route(self, path: str, handler: Handler) -> "Service":
|
|
479
|
+
self._routes.append((None, path, handler))
|
|
480
|
+
return self
|
|
481
|
+
|
|
482
|
+
def on(self, method: str, path: str, handler: Handler) -> "Service":
|
|
483
|
+
self._routes.append((method.upper(), path, handler))
|
|
484
|
+
return self
|
|
485
|
+
|
|
486
|
+
def fallback(self, handler: Handler) -> "Service":
|
|
487
|
+
self._fallback = handler
|
|
488
|
+
return self
|
|
489
|
+
|
|
490
|
+
def options(self, path: str, handler: Handler) -> "Service":
|
|
491
|
+
return self.on("OPTIONS", path, handler)
|
|
492
|
+
|
|
493
|
+
def get(self, path: str, handler: Handler) -> "Service":
|
|
494
|
+
return self.on("GET", path, handler)
|
|
495
|
+
|
|
496
|
+
def post(self, path: str, handler: Handler) -> "Service":
|
|
497
|
+
return self.on("POST", path, handler)
|
|
498
|
+
|
|
499
|
+
def put(self, path: str, handler: Handler) -> "Service":
|
|
500
|
+
return self.on("PUT", path, handler)
|
|
501
|
+
|
|
502
|
+
def delete(self, path: str, handler: Handler) -> "Service":
|
|
503
|
+
return self.on("DELETE", path, handler)
|
|
504
|
+
|
|
505
|
+
def head(self, path: str, handler: Handler) -> "Service":
|
|
506
|
+
return self.on("HEAD", path, handler)
|
|
507
|
+
|
|
508
|
+
def trace(self, path: str, handler: Handler) -> "Service":
|
|
509
|
+
return self.on("TRACE", path, handler)
|
|
510
|
+
|
|
511
|
+
def connect(self, path: str, handler: Handler) -> "Service":
|
|
512
|
+
return self.on("CONNECT", path, handler)
|
|
513
|
+
|
|
514
|
+
def patch(self, path: str, handler: Handler) -> "Service":
|
|
515
|
+
return self.on("PATCH", path, handler)
|
dhttpy/raw.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Raw DHTTP message primitives for Python bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import _native
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class HeaderField:
|
|
14
|
+
name: bytes
|
|
15
|
+
value: bytes
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: Any, value: Any):
|
|
18
|
+
object.__setattr__(self, "name", _bytes(name))
|
|
19
|
+
object.__setattr__(self, "value", _bytes(value))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _bytes(value: Any) -> bytes:
|
|
23
|
+
if isinstance(value, bytes):
|
|
24
|
+
return value
|
|
25
|
+
if isinstance(value, bytearray | memoryview):
|
|
26
|
+
return bytes(value)
|
|
27
|
+
return bytes(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _field_from_native(field: Any) -> HeaderField:
|
|
31
|
+
if isinstance(field, HeaderField):
|
|
32
|
+
return field
|
|
33
|
+
if hasattr(field, "name") and hasattr(field, "value"):
|
|
34
|
+
return HeaderField(field.name, field.value)
|
|
35
|
+
name, value = field
|
|
36
|
+
return HeaderField(name, value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _field_pair(field: HeaderField | tuple[Any, Any]) -> tuple[bytes, bytes]:
|
|
40
|
+
if isinstance(field, HeaderField):
|
|
41
|
+
return (field.name, field.value)
|
|
42
|
+
name, value = field
|
|
43
|
+
return (_bytes(name), _bytes(value))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MessageReader:
|
|
47
|
+
def __init__(self, inner: Any):
|
|
48
|
+
self._inner = inner
|
|
49
|
+
|
|
50
|
+
async def read_header(self) -> list[HeaderField] | None:
|
|
51
|
+
headers = await self._inner.read_header()
|
|
52
|
+
if headers is None:
|
|
53
|
+
return None
|
|
54
|
+
return [_field_from_native(field) for field in headers]
|
|
55
|
+
|
|
56
|
+
async def read_data(self) -> bytes | None:
|
|
57
|
+
chunk = await self._inner.read_data()
|
|
58
|
+
if chunk is None:
|
|
59
|
+
return None
|
|
60
|
+
return _bytes(chunk)
|
|
61
|
+
|
|
62
|
+
async def stop(self, code: int) -> None:
|
|
63
|
+
await self._inner.stop(code)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MessageWriter:
|
|
67
|
+
def __init__(self, inner: Any):
|
|
68
|
+
self._inner = inner
|
|
69
|
+
|
|
70
|
+
async def write_header(self, headers: Iterable[HeaderField | tuple[Any, Any]]) -> None:
|
|
71
|
+
await self._inner.write_header([_field_pair(field) for field in headers])
|
|
72
|
+
|
|
73
|
+
async def write_data(self, data: Any) -> None:
|
|
74
|
+
await self._inner.write_data(_bytes(data))
|
|
75
|
+
|
|
76
|
+
async def flush(self) -> None:
|
|
77
|
+
await self._inner.flush()
|
|
78
|
+
|
|
79
|
+
async def close(self) -> None:
|
|
80
|
+
await self._inner.close()
|
|
81
|
+
|
|
82
|
+
async def reset(self, code: int) -> None:
|
|
83
|
+
await self._inner.reset(code)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UnresolvedRequest:
|
|
87
|
+
def __init__(self, inner: Any):
|
|
88
|
+
self._inner = inner
|
|
89
|
+
self.stream_id = inner.stream_id
|
|
90
|
+
self.reader = MessageReader(inner.reader)
|
|
91
|
+
self.writer = MessageWriter(inner.writer)
|
|
92
|
+
|
|
93
|
+
async def local_authority(self):
|
|
94
|
+
result = self._inner.local_authority()
|
|
95
|
+
if hasattr(result, "__await__"):
|
|
96
|
+
return await result
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
async def remote_authority(self):
|
|
100
|
+
result = self._inner.remote_authority()
|
|
101
|
+
if hasattr(result, "__await__"):
|
|
102
|
+
return await result
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Connection:
|
|
107
|
+
def __init__(self, inner: Any):
|
|
108
|
+
self._inner = inner
|
|
109
|
+
|
|
110
|
+
async def open_request(self) -> UnresolvedRequest:
|
|
111
|
+
return UnresolvedRequest(await self._inner.open_request())
|
|
112
|
+
|
|
113
|
+
async def local_authority(self):
|
|
114
|
+
return await self._inner.local_authority()
|
|
115
|
+
|
|
116
|
+
async def remote_authority(self):
|
|
117
|
+
return await self._inner.remote_authority()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = ["Connection", "HeaderField", "MessageReader", "MessageWriter", "UnresolvedRequest"]
|