DefEssential 0.1.8__25-py3-none-any.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.
- DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
- DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
- DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/Python/Server.py +273 -0
- DefEssential/Adapter/Implements/Python/__init__.py +5 -0
- DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
- DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
- DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
- DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
- DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
- DefEssential/Adapter/Implements/__init__.py +3 -0
- DefEssential/Adapter/RequestHandler.py +109 -0
- DefEssential/Adapter/Server.py +54 -0
- DefEssential/Adapter/WSGI.py +219 -0
- DefEssential/Adapter/__init__.py +5 -0
- DefEssential/Application.py +657 -0
- DefEssential/Body.py +50 -0
- DefEssential/Content.py +18 -0
- DefEssential/Credentials.py +245 -0
- DefEssential/CrossOriginResourceSharing.py +107 -0
- DefEssential/Descriptor/DataObject.py +409 -0
- DefEssential/Descriptor/Types.py +248 -0
- DefEssential/Descriptor/Value.py +188 -0
- DefEssential/Descriptor/__init__.py +172 -0
- DefEssential/Error.py +66 -0
- DefEssential/Extension.py +191 -0
- DefEssential/Extensions/File.py +95 -0
- DefEssential/Extensions/FileSystem.py +195 -0
- DefEssential/Extensions/Metrics.py +511 -0
- DefEssential/Extensions/Ping.py +93 -0
- DefEssential/Extensions/Specifications.py +342 -0
- DefEssential/Extensions/Status.html +207 -0
- DefEssential/Extensions/Status.py +110 -0
- DefEssential/Extensions/Swagger.html +77 -0
- DefEssential/Extensions/Swagger.py +117 -0
- DefEssential/Extensions/Utils/Routes.py +42 -0
- DefEssential/Extensions/Utils/__init__.py +43 -0
- DefEssential/Extensions/__init__.py +21 -0
- DefEssential/IO/Readers/ChunkedStream.py +106 -0
- DefEssential/IO/Readers/ContentReader.py +36 -0
- DefEssential/IO/Readers/ContentReaders.py +241 -0
- DefEssential/IO/Readers/__init__.py +25 -0
- DefEssential/IO/RequestReader.py +125 -0
- DefEssential/IO/ResponseWriter.py +242 -0
- DefEssential/IO/WebSocket/WebSocket.py +288 -0
- DefEssential/IO/WebSocket/WebSocketError.py +17 -0
- DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
- DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
- DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
- DefEssential/IO/WebSocket/__init__.py +13 -0
- DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
- DefEssential/IO/Writers/ServerSentEvents.py +41 -0
- DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
- DefEssential/IO/Writers/StreamWriter.py +57 -0
- DefEssential/IO/Writers/__init__.py +11 -0
- DefEssential/IO/__init__.py +7 -0
- DefEssential/Request.py +110 -0
- DefEssential/RequestRunner.py +261 -0
- DefEssential/Response.py +89 -0
- DefEssential/ResponseSpecification.py +23 -0
- DefEssential/Responses/ResponseFile.py +108 -0
- DefEssential/Responses/__init__.py +63 -0
- DefEssential/Route.py +55 -0
- DefEssential/Router.py +48 -0
- DefEssential/Runners/RunExtension.py +116 -0
- DefEssential/Runners/RunFactory.py +26 -0
- DefEssential/Runners/RunOptions.py +77 -0
- DefEssential/Runners/RunRequest.py +198 -0
- DefEssential/Runners/__init__.py +5 -0
- DefEssential/Startup.py +104 -0
- DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Connection.py +57 -0
- DefEssential/Utils/FileSystemObject/File.py +41 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
- DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
- DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
- DefEssential/Utils/FileSystemObject/__init__.py +11 -0
- DefEssential/Utils/Loader.py +81 -0
- DefEssential/Utils/__init__.py +3 -0
- DefEssential/Validator/Pattern.py +36 -0
- DefEssential/Validator/Patterns/Array.py +30 -0
- DefEssential/Validator/Patterns/Common.py +92 -0
- DefEssential/Validator/Patterns/Compare.py +180 -0
- DefEssential/Validator/Patterns/Condition.py +60 -0
- DefEssential/Validator/Patterns/DataObject.py +85 -0
- DefEssential/Validator/Patterns/DateTime.py +111 -0
- DefEssential/Validator/Patterns/Number.py +43 -0
- DefEssential/Validator/Patterns/Object.py +150 -0
- DefEssential/Validator/Patterns/Size.py +110 -0
- DefEssential/Validator/Patterns/String.py +85 -0
- DefEssential/Validator/Patterns/Type.py +350 -0
- DefEssential/Validator/Patterns/__init__.py +137 -0
- DefEssential/Validator/Validate.py +29 -0
- DefEssential/Validator/Validator.py +30 -0
- DefEssential/Validator/__init__.py +11 -0
- DefEssential/Validators.py +255 -0
- DefEssential/__init__.py +83 -0
- defessential-0.1.8.dist-info/METADATA +86 -0
- defessential-0.1.8.dist-info/RECORD +104 -0
- defessential-0.1.8.dist-info/WHEEL +6 -0
- defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Mapping,
|
|
3
|
+
Optional,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from ...Response import Response
|
|
7
|
+
from ..ResponseWriter import ResponseWriter
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
'StreamWriter',
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StreamWriter:
|
|
15
|
+
"""Start a response and write bytes directly to the stream."""
|
|
16
|
+
|
|
17
|
+
chunked = False
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
writer: ResponseWriter,
|
|
22
|
+
):
|
|
23
|
+
self.writer = writer
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
def begin(
|
|
27
|
+
self,
|
|
28
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
29
|
+
format: Optional[str] = None,
|
|
30
|
+
charset: Optional[str] = None,
|
|
31
|
+
status: int = 200,
|
|
32
|
+
) -> None:
|
|
33
|
+
self.writer.begin(
|
|
34
|
+
Response(
|
|
35
|
+
status=status,
|
|
36
|
+
headers=headers,
|
|
37
|
+
format=format,
|
|
38
|
+
charset=charset,
|
|
39
|
+
),
|
|
40
|
+
chunked=self.chunked,
|
|
41
|
+
)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def write(
|
|
45
|
+
self,
|
|
46
|
+
data: bytes,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.writer.write(data)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
def flush(self) -> None:
|
|
52
|
+
self.writer.flush()
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
def end(self) -> None:
|
|
56
|
+
self.writer.end()
|
|
57
|
+
return
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .ChunkedStreamWriter import ChunkedStreamWriter
|
|
2
|
+
from .ServerSentEvents import ServerSentEvents
|
|
3
|
+
from .ServerSentEventsWriter import ServerSentEventsWriter
|
|
4
|
+
from .StreamWriter import StreamWriter
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
'ChunkedStreamWriter',
|
|
8
|
+
'ServerSentEvents',
|
|
9
|
+
'ServerSentEventsWriter',
|
|
10
|
+
'StreamWriter',
|
|
11
|
+
)
|
DefEssential/Request.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from email.message import Message
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
from re import fullmatch
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Mapping,
|
|
7
|
+
Optional,
|
|
8
|
+
)
|
|
9
|
+
from urllib.parse import (
|
|
10
|
+
parse_qs,
|
|
11
|
+
urlsplit,
|
|
12
|
+
)
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from .IO.RequestReader import RequestReader
|
|
16
|
+
from .IO.ResponseWriter import ResponseWriter
|
|
17
|
+
from .Error import ClientError
|
|
18
|
+
|
|
19
|
+
__all__ = (
|
|
20
|
+
'Request',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Request:
|
|
25
|
+
"""Represent an HTTP request independently of server environment dictionaries."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
address: str,
|
|
30
|
+
port: int,
|
|
31
|
+
method: str,
|
|
32
|
+
uri: str,
|
|
33
|
+
parameters: Optional[Mapping[str, str]] = None,
|
|
34
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
35
|
+
reader: Optional[RequestReader] = None,
|
|
36
|
+
writer: Optional[ResponseWriter] = None,
|
|
37
|
+
):
|
|
38
|
+
self.id = uuid4().hex
|
|
39
|
+
self.address = address
|
|
40
|
+
self.port = port
|
|
41
|
+
self.method = method
|
|
42
|
+
self.uri = uri
|
|
43
|
+
parsed = urlsplit(uri)
|
|
44
|
+
self.path = parsed.path
|
|
45
|
+
self.querystring = parsed.query
|
|
46
|
+
self.params = dict(parameters) if parameters is not None else {}
|
|
47
|
+
self.args = {
|
|
48
|
+
key: values[0] if len(values) == 1 else values
|
|
49
|
+
for key, values in parse_qs(
|
|
50
|
+
parsed.query,
|
|
51
|
+
keep_blank_values=True,
|
|
52
|
+
).items()
|
|
53
|
+
}
|
|
54
|
+
self.props = Message()
|
|
55
|
+
for key, value in headers.items() if headers is not None else ():
|
|
56
|
+
self.props[key] = value
|
|
57
|
+
self.reader = reader if reader is not None else RequestReader(BytesIO())
|
|
58
|
+
self.response_writer = writer
|
|
59
|
+
self.body: Any = None
|
|
60
|
+
self.session: Any = None
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def writer(self) -> ResponseWriter:
|
|
65
|
+
"""Return the response writer provided by the execution layer."""
|
|
66
|
+
if self.response_writer is None:
|
|
67
|
+
raise RuntimeError('No response writer is attached to this request')
|
|
68
|
+
return self.response_writer
|
|
69
|
+
|
|
70
|
+
def header(
|
|
71
|
+
self,
|
|
72
|
+
key: str,
|
|
73
|
+
value: Optional[str] = None,
|
|
74
|
+
) -> Optional[str]:
|
|
75
|
+
if value is not None:
|
|
76
|
+
del self.props[key]
|
|
77
|
+
self.props[key] = value
|
|
78
|
+
return None
|
|
79
|
+
return self.props.get(key)
|
|
80
|
+
|
|
81
|
+
def headers(self) -> list:
|
|
82
|
+
return list(self.props.items())
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def parameters(self) -> dict:
|
|
86
|
+
return self.params
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def qs(self) -> dict:
|
|
90
|
+
return self.args
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def size(self) -> Optional[int]:
|
|
94
|
+
value = self.header('Content-Length')
|
|
95
|
+
return int(value) if value is not None else None
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def format(self) -> Optional[str]:
|
|
99
|
+
value = self.header('Content-Type')
|
|
100
|
+
if value is None:
|
|
101
|
+
return None
|
|
102
|
+
format = value.split(';', 1)[0].strip().lower()
|
|
103
|
+
token = r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+"
|
|
104
|
+
if fullmatch(token + '/' + token, format) is None:
|
|
105
|
+
raise ClientError('Invalid Content-Type')
|
|
106
|
+
return format
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def charset(self) -> Optional[str]:
|
|
110
|
+
return self.props.get_content_charset()
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
ABCMeta,
|
|
4
|
+
abstractmethod,
|
|
5
|
+
)
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from re import fullmatch
|
|
8
|
+
from typing import (
|
|
9
|
+
Mapping,
|
|
10
|
+
Optional,
|
|
11
|
+
Sequence,
|
|
12
|
+
Type,
|
|
13
|
+
TypeVar,
|
|
14
|
+
Union,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .Body import Body
|
|
18
|
+
from .Request import Request
|
|
19
|
+
from .Response import Response
|
|
20
|
+
from .ResponseSpecification import ResponseSpecification
|
|
21
|
+
from .Validators import (
|
|
22
|
+
Auth,
|
|
23
|
+
Headers,
|
|
24
|
+
Origin,
|
|
25
|
+
Parameters,
|
|
26
|
+
QueryString,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = (
|
|
30
|
+
'RequestRunnerRequest',
|
|
31
|
+
'RequestRunnerRequestResponse',
|
|
32
|
+
'RequestRunnerRequestError',
|
|
33
|
+
'RequestProperties',
|
|
34
|
+
'RequestRunner',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RequestRunnerRequest(ABC):
|
|
39
|
+
"""Transform a prepared request immediately before the handler runs."""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def __call__(
|
|
43
|
+
self,
|
|
44
|
+
request: Request,
|
|
45
|
+
) -> Request:
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RequestRunnerRequestResponse(ABC):
|
|
50
|
+
"""Transform a returned response or a direct-output response before transmission."""
|
|
51
|
+
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def __call__(
|
|
54
|
+
self,
|
|
55
|
+
request: Request,
|
|
56
|
+
response: Response,
|
|
57
|
+
) -> Response:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RequestRunnerRequestError(ABC):
|
|
62
|
+
"""Observe execution errors from validation through response processing."""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def __call__(
|
|
66
|
+
self,
|
|
67
|
+
request: Request,
|
|
68
|
+
error: Exception,
|
|
69
|
+
) -> None:
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
Hook = TypeVar('Hook')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def request_runner_hooks(
|
|
77
|
+
value: Union[Hook, Sequence[Hook], None],
|
|
78
|
+
contract: Type[Hook],
|
|
79
|
+
name: str,
|
|
80
|
+
) -> tuple[Hook, ...]:
|
|
81
|
+
if value is None:
|
|
82
|
+
return ()
|
|
83
|
+
if isinstance(value, contract):
|
|
84
|
+
return (value,)
|
|
85
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
86
|
+
result = tuple(value)
|
|
87
|
+
if all(isinstance(hook, contract) for hook in result):
|
|
88
|
+
return result
|
|
89
|
+
raise TypeError(name + ' must be a ' + contract.__name__ + ' instance or a sequence of them')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def require_contract(
|
|
93
|
+
value: object,
|
|
94
|
+
contract: Type,
|
|
95
|
+
name: str,
|
|
96
|
+
*,
|
|
97
|
+
multiple: bool = False,
|
|
98
|
+
) -> None:
|
|
99
|
+
if value is None:
|
|
100
|
+
return
|
|
101
|
+
values = (value,) if isinstance(value, contract) else value
|
|
102
|
+
if multiple:
|
|
103
|
+
if not isinstance(values, Sequence) or isinstance(values, (str, bytes)):
|
|
104
|
+
raise TypeError(name + ' must contain ' + contract.__name__ + ' instances')
|
|
105
|
+
if any(not isinstance(item, contract) for item in values):
|
|
106
|
+
raise TypeError(name + ' must contain ' + contract.__name__ + ' instances')
|
|
107
|
+
return
|
|
108
|
+
if not isinstance(value, contract):
|
|
109
|
+
raise TypeError(name + ' must be a ' + contract.__name__ + ' instance')
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RequestRunnerMeta(ABCMeta):
|
|
114
|
+
def __new__(
|
|
115
|
+
cls,
|
|
116
|
+
class_name,
|
|
117
|
+
bases,
|
|
118
|
+
namespace,
|
|
119
|
+
**kwargs,
|
|
120
|
+
):
|
|
121
|
+
if 'name' in kwargs:
|
|
122
|
+
if 'request_runner_name' in kwargs:
|
|
123
|
+
raise TypeError('Specify only one request runner name')
|
|
124
|
+
kwargs['request_runner_name'] = kwargs.pop('name')
|
|
125
|
+
return super().__new__(
|
|
126
|
+
cls,
|
|
127
|
+
class_name,
|
|
128
|
+
bases,
|
|
129
|
+
namespace,
|
|
130
|
+
**kwargs,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class RequestProperties:
|
|
136
|
+
"""Store request handler settings for registration with an application."""
|
|
137
|
+
|
|
138
|
+
method: Optional[str]
|
|
139
|
+
url: Optional[str]
|
|
140
|
+
origin: Union[Origin, Sequence[Origin], None]
|
|
141
|
+
auth: Optional[Auth]
|
|
142
|
+
parameters: Optional[Parameters]
|
|
143
|
+
qs: Optional[QueryString]
|
|
144
|
+
headers: Optional[Headers]
|
|
145
|
+
body: Optional[Body]
|
|
146
|
+
response: Union[
|
|
147
|
+
ResponseSpecification,
|
|
148
|
+
Mapping[str, Union[str, Mapping[str, object]]],
|
|
149
|
+
Sequence[ResponseSpecification],
|
|
150
|
+
None,
|
|
151
|
+
]
|
|
152
|
+
summary: Optional[str]
|
|
153
|
+
description: Optional[str]
|
|
154
|
+
tags: Union[str, Sequence[str], None]
|
|
155
|
+
on_request: tuple[RequestRunnerRequest, ...]
|
|
156
|
+
on_request_response: tuple[RequestRunnerRequestResponse, ...]
|
|
157
|
+
on_request_error: tuple[RequestRunnerRequestError, ...]
|
|
158
|
+
name: Optional[str] = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class RequestRunner(
|
|
162
|
+
ABC,
|
|
163
|
+
metaclass=RequestRunnerMeta,
|
|
164
|
+
):
|
|
165
|
+
"""Configure a handler through class declarations and instantiate it per request."""
|
|
166
|
+
|
|
167
|
+
properties: RequestProperties
|
|
168
|
+
|
|
169
|
+
def __init_subclass__(
|
|
170
|
+
cls,
|
|
171
|
+
*,
|
|
172
|
+
method: Optional[str] = None,
|
|
173
|
+
url: Optional[str] = None,
|
|
174
|
+
request_runner_name: Optional[str] = None,
|
|
175
|
+
origin: Union[Origin, Sequence[Origin], None] = None,
|
|
176
|
+
auth: Optional[Auth] = None,
|
|
177
|
+
parameters: Optional[Parameters] = None,
|
|
178
|
+
qs: Optional[QueryString] = None,
|
|
179
|
+
headers: Optional[Headers] = None,
|
|
180
|
+
body: Optional[Body] = None,
|
|
181
|
+
response: Union[
|
|
182
|
+
ResponseSpecification,
|
|
183
|
+
Mapping[str, Union[str, Mapping[str, object]]],
|
|
184
|
+
Sequence[ResponseSpecification],
|
|
185
|
+
None,
|
|
186
|
+
] = None,
|
|
187
|
+
summary: Optional[str] = None,
|
|
188
|
+
description: Optional[str] = None,
|
|
189
|
+
tags: Union[str, Sequence[str], None] = None,
|
|
190
|
+
on_request: Union[
|
|
191
|
+
RequestRunnerRequest,
|
|
192
|
+
Sequence[RequestRunnerRequest],
|
|
193
|
+
None,
|
|
194
|
+
] = None,
|
|
195
|
+
on_request_response: Union[
|
|
196
|
+
RequestRunnerRequestResponse,
|
|
197
|
+
Sequence[RequestRunnerRequestResponse],
|
|
198
|
+
None,
|
|
199
|
+
] = None,
|
|
200
|
+
on_request_error: Union[
|
|
201
|
+
RequestRunnerRequestError,
|
|
202
|
+
Sequence[RequestRunnerRequestError],
|
|
203
|
+
None,
|
|
204
|
+
] = None,
|
|
205
|
+
):
|
|
206
|
+
super().__init_subclass__()
|
|
207
|
+
if request_runner_name is not None and (
|
|
208
|
+
not isinstance(request_runner_name, str) or not request_runner_name.strip()
|
|
209
|
+
):
|
|
210
|
+
raise ValueError('name must be a non-empty string')
|
|
211
|
+
if (method is None) != (url is None):
|
|
212
|
+
raise TypeError('method and url must be specified together')
|
|
213
|
+
if method is not None and fullmatch(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+", method) is None:
|
|
214
|
+
raise ValueError('Invalid HTTP method token')
|
|
215
|
+
if url is not None and (not url.startswith('/') or '?' in url or '#' in url):
|
|
216
|
+
raise ValueError('url must be a path starting with /')
|
|
217
|
+
require_contract(origin, Origin, 'origin', multiple=True)
|
|
218
|
+
require_contract(auth, Auth, 'auth')
|
|
219
|
+
require_contract(parameters, Parameters, 'parameters')
|
|
220
|
+
require_contract(qs, QueryString, 'qs')
|
|
221
|
+
require_contract(headers, Headers, 'headers')
|
|
222
|
+
require_contract(body, Body, 'body')
|
|
223
|
+
cls.properties = RequestProperties(
|
|
224
|
+
method=method,
|
|
225
|
+
url=url,
|
|
226
|
+
name=request_runner_name,
|
|
227
|
+
origin=origin,
|
|
228
|
+
auth=auth,
|
|
229
|
+
parameters=parameters,
|
|
230
|
+
qs=qs,
|
|
231
|
+
headers=headers,
|
|
232
|
+
body=body,
|
|
233
|
+
response=response,
|
|
234
|
+
summary=summary,
|
|
235
|
+
description=description,
|
|
236
|
+
tags=tags,
|
|
237
|
+
on_request=request_runner_hooks(
|
|
238
|
+
on_request,
|
|
239
|
+
RequestRunnerRequest,
|
|
240
|
+
'on_request',
|
|
241
|
+
),
|
|
242
|
+
on_request_response=request_runner_hooks(
|
|
243
|
+
on_request_response,
|
|
244
|
+
RequestRunnerRequestResponse,
|
|
245
|
+
'on_request_response',
|
|
246
|
+
),
|
|
247
|
+
on_request_error=request_runner_hooks(
|
|
248
|
+
on_request_error,
|
|
249
|
+
RequestRunnerRequestError,
|
|
250
|
+
'on_request_error',
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
@abstractmethod
|
|
256
|
+
def run(
|
|
257
|
+
self,
|
|
258
|
+
request: Request,
|
|
259
|
+
) -> Optional[Response]:
|
|
260
|
+
"""Return a Response, or write through request.writer and return None."""
|
|
261
|
+
raise NotImplementedError
|
DefEssential/Response.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from email.message import Message
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
from logging import getLogger
|
|
4
|
+
from typing import (
|
|
5
|
+
Iterable,
|
|
6
|
+
Mapping,
|
|
7
|
+
Optional,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'Response',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = getLogger('DefEssential.RequestRunner')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def close_bodies(
|
|
19
|
+
*bodies: object,
|
|
20
|
+
preserve_error: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Close every repeated body once without letting cleanup errors hide the original failure."""
|
|
23
|
+
closed: list[object] = []
|
|
24
|
+
error: Optional[Exception] = None
|
|
25
|
+
for body in bodies:
|
|
26
|
+
if any(body is previous for previous in closed):
|
|
27
|
+
continue
|
|
28
|
+
closed.append(body)
|
|
29
|
+
try:
|
|
30
|
+
close = getattr(body, 'close', None)
|
|
31
|
+
if close is not None:
|
|
32
|
+
close()
|
|
33
|
+
except Exception as caught:
|
|
34
|
+
if preserve_error or error is not None:
|
|
35
|
+
logger.exception('Response body cleanup failed')
|
|
36
|
+
else:
|
|
37
|
+
error = caught
|
|
38
|
+
if error is not None:
|
|
39
|
+
raise error
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Response:
|
|
44
|
+
"""Represent an HTTP response with a byte body or sequential output."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
status: int = 200,
|
|
49
|
+
message: Optional[str] = None,
|
|
50
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
51
|
+
body: Union[bytes, Iterable[bytes]] = b'',
|
|
52
|
+
format: Optional[str] = None,
|
|
53
|
+
charset: Optional[str] = None,
|
|
54
|
+
):
|
|
55
|
+
self.status = status
|
|
56
|
+
self.message = HTTPStatus(status).phrase if message is None else message
|
|
57
|
+
self.props = Message()
|
|
58
|
+
for key, value in headers.items() if headers is not None else ():
|
|
59
|
+
self.header(
|
|
60
|
+
key,
|
|
61
|
+
value,
|
|
62
|
+
)
|
|
63
|
+
if format is not None:
|
|
64
|
+
self.header(
|
|
65
|
+
'Content-Type',
|
|
66
|
+
format + ('; charset=' + charset if charset is not None else ''),
|
|
67
|
+
)
|
|
68
|
+
self.body = body
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
def header(
|
|
72
|
+
self,
|
|
73
|
+
key: str,
|
|
74
|
+
value: Optional[str] = None,
|
|
75
|
+
) -> Optional[str]:
|
|
76
|
+
if value is None:
|
|
77
|
+
return self.props.get(key)
|
|
78
|
+
if '\r' in key or '\n' in key or '\r' in value or '\n' in value:
|
|
79
|
+
raise ValueError('Response headers must not contain line breaks')
|
|
80
|
+
del self.props[key]
|
|
81
|
+
self.props[key] = value
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def headers(self) -> list:
|
|
85
|
+
return list(self.props.items())
|
|
86
|
+
|
|
87
|
+
def close(self) -> None:
|
|
88
|
+
close_bodies(self.body)
|
|
89
|
+
return
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import (
|
|
3
|
+
Mapping,
|
|
4
|
+
Optional,
|
|
5
|
+
Sequence,
|
|
6
|
+
Union,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from .Content import Content
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'ResponseSpecification',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ResponseSpecification:
|
|
18
|
+
"""Declare an OpenAPI response independently of the actual Response."""
|
|
19
|
+
|
|
20
|
+
status: int
|
|
21
|
+
description: str
|
|
22
|
+
content: Union[Content, Sequence[Content], None] = None
|
|
23
|
+
headers: Optional[Mapping[str, object]] = None
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from mimetypes import guess_type
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import (
|
|
4
|
+
Iterator,
|
|
5
|
+
Optional,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from ..Error import ClientError
|
|
9
|
+
from ..Response import Response
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
'FileBody',
|
|
13
|
+
'ResponseFile',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def file_error(error: OSError) -> ClientError:
|
|
18
|
+
if isinstance(
|
|
19
|
+
error,
|
|
20
|
+
(
|
|
21
|
+
FileNotFoundError,
|
|
22
|
+
NotADirectoryError,
|
|
23
|
+
IsADirectoryError,
|
|
24
|
+
),
|
|
25
|
+
):
|
|
26
|
+
return ClientError(
|
|
27
|
+
'File not found',
|
|
28
|
+
status=404,
|
|
29
|
+
)
|
|
30
|
+
if isinstance(error, PermissionError):
|
|
31
|
+
return ClientError(
|
|
32
|
+
'File access denied',
|
|
33
|
+
status=403,
|
|
34
|
+
)
|
|
35
|
+
raise error
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FileBody:
|
|
39
|
+
"""Read an open file in fixed-size chunks and close it when the response ends."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
file,
|
|
44
|
+
):
|
|
45
|
+
self.file = file
|
|
46
|
+
self.closed = False
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
def __iter__(self) -> Iterator[bytes]:
|
|
50
|
+
try:
|
|
51
|
+
while True:
|
|
52
|
+
data = self.file.read(65536)
|
|
53
|
+
if not data:
|
|
54
|
+
break
|
|
55
|
+
yield data
|
|
56
|
+
finally:
|
|
57
|
+
self.close()
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
def close(self) -> None:
|
|
61
|
+
if not self.closed:
|
|
62
|
+
self.closed = True
|
|
63
|
+
self.file.close()
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ResponseFile(Response):
|
|
68
|
+
"""Stream a local file or an open FileSystemObject file as a response."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
file,
|
|
73
|
+
format: Optional[str] = None,
|
|
74
|
+
encoding: Optional[str] = None,
|
|
75
|
+
):
|
|
76
|
+
if isinstance(
|
|
77
|
+
file,
|
|
78
|
+
(
|
|
79
|
+
str,
|
|
80
|
+
Path,
|
|
81
|
+
),
|
|
82
|
+
):
|
|
83
|
+
path = Path(file)
|
|
84
|
+
try:
|
|
85
|
+
file = path.open('rb')
|
|
86
|
+
except (
|
|
87
|
+
FileNotFoundError,
|
|
88
|
+
NotADirectoryError,
|
|
89
|
+
IsADirectoryError,
|
|
90
|
+
PermissionError,
|
|
91
|
+
) as error:
|
|
92
|
+
raise file_error(error) from error
|
|
93
|
+
mime, compression = guess_type(str(path))
|
|
94
|
+
if format is None:
|
|
95
|
+
format = mime
|
|
96
|
+
if encoding is None:
|
|
97
|
+
encoding = compression
|
|
98
|
+
body = FileBody(file)
|
|
99
|
+
try:
|
|
100
|
+
super().__init__(
|
|
101
|
+
body=body,
|
|
102
|
+
format=format if format is not None else 'application/octet-stream',
|
|
103
|
+
headers={'Content-Encoding': encoding} if encoding is not None else None,
|
|
104
|
+
)
|
|
105
|
+
except Exception:
|
|
106
|
+
body.close()
|
|
107
|
+
raise
|
|
108
|
+
return
|