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,109 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from time import monotonic
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from ..Application import Application
|
|
6
|
+
from ..Error import Error
|
|
7
|
+
from ..IO.RequestReader import ClientDisconnected
|
|
8
|
+
from ..IO.ResponseWriter import ResponseWriter
|
|
9
|
+
from ..Request import Request
|
|
10
|
+
from ..Response import Response
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'handle_request',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
LOGGER = getLogger('DefEssential.Server')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def handle_request(
|
|
20
|
+
application: Application,
|
|
21
|
+
create_request: Callable[[], Request],
|
|
22
|
+
writer: ResponseWriter,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Handle error responses and resource cleanup independently of the transport layer."""
|
|
25
|
+
logger = LOGGER
|
|
26
|
+
started = monotonic()
|
|
27
|
+
request = None
|
|
28
|
+
response = None
|
|
29
|
+
outcome = 'failed'
|
|
30
|
+
try:
|
|
31
|
+
try:
|
|
32
|
+
request = create_request()
|
|
33
|
+
logger.debug(
|
|
34
|
+
'HTTP request received: request_id=%s method=%s',
|
|
35
|
+
request.id,
|
|
36
|
+
ascii(request.method[:32]),
|
|
37
|
+
)
|
|
38
|
+
response = application(request)
|
|
39
|
+
except ClientDisconnected:
|
|
40
|
+
raise
|
|
41
|
+
except Error as error:
|
|
42
|
+
if writer.started:
|
|
43
|
+
raise
|
|
44
|
+
logger.debug(
|
|
45
|
+
'HTTP error response selected: request_id=%s status=%s',
|
|
46
|
+
request.id if request is not None else None,
|
|
47
|
+
error.status,
|
|
48
|
+
)
|
|
49
|
+
response = Response(
|
|
50
|
+
status=error.status,
|
|
51
|
+
headers=error.headers,
|
|
52
|
+
body=error.reason.encode('utf-8'),
|
|
53
|
+
format='text/plain',
|
|
54
|
+
charset='utf-8',
|
|
55
|
+
)
|
|
56
|
+
except Exception:
|
|
57
|
+
if writer.started:
|
|
58
|
+
raise
|
|
59
|
+
logger.exception(
|
|
60
|
+
'Unexpected error while handling request: request_id=%s',
|
|
61
|
+
request.id if request is not None else None,
|
|
62
|
+
)
|
|
63
|
+
response = Response(
|
|
64
|
+
status=500,
|
|
65
|
+
body=b'Internal Server Error',
|
|
66
|
+
)
|
|
67
|
+
if response is not None:
|
|
68
|
+
logger.debug('Response transmission starting: request_id=%s status=%s', request.id if request is not None else None, response.status)
|
|
69
|
+
writer.response(
|
|
70
|
+
response,
|
|
71
|
+
filtered=True,
|
|
72
|
+
)
|
|
73
|
+
elif not writer.ended:
|
|
74
|
+
raise RuntimeError('Response was neither returned nor completed successfully')
|
|
75
|
+
outcome = 'completed'
|
|
76
|
+
except ConnectionError:
|
|
77
|
+
outcome = 'disconnected'
|
|
78
|
+
logger.warning(
|
|
79
|
+
'Client disconnected: request_id=%s',
|
|
80
|
+
request.id if request is not None else None,
|
|
81
|
+
)
|
|
82
|
+
raise
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.exception(
|
|
85
|
+
'Response transmission failed: closing connection without another error response request_id=%s',
|
|
86
|
+
request.id if request is not None else None,
|
|
87
|
+
)
|
|
88
|
+
raise
|
|
89
|
+
finally:
|
|
90
|
+
duration = monotonic() - started
|
|
91
|
+
logger.info(
|
|
92
|
+
'HTTP transmission result: request_id=%s method=%s status=%s outcome=%s duration=%.6f',
|
|
93
|
+
request.id if request is not None else None,
|
|
94
|
+
ascii(request.method[:32]) if request is not None else None,
|
|
95
|
+
writer.status if writer.started else None,
|
|
96
|
+
outcome,
|
|
97
|
+
duration,
|
|
98
|
+
)
|
|
99
|
+
if response is not None:
|
|
100
|
+
try:
|
|
101
|
+
logger.debug('Response body cleaning up: request_id=%s', request.id if request is not None else None)
|
|
102
|
+
response.close()
|
|
103
|
+
logger.debug('Response body cleaned up: request_id=%s', request.id if request is not None else None)
|
|
104
|
+
except Exception:
|
|
105
|
+
logger.exception(
|
|
106
|
+
'Response body cleanup failed: request_id=%s',
|
|
107
|
+
request.id if request is not None else None,
|
|
108
|
+
)
|
|
109
|
+
return
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
abstractmethod,
|
|
4
|
+
)
|
|
5
|
+
from types import TracebackType
|
|
6
|
+
from typing import (
|
|
7
|
+
Optional,
|
|
8
|
+
TypeVar,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ..Application import Application
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
'Server',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ServerType = TypeVar(
|
|
19
|
+
'ServerType',
|
|
20
|
+
bound='Server',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Server(ABC):
|
|
25
|
+
"""Define the server adapter contract for running an application at an address."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
host: str,
|
|
30
|
+
port: int,
|
|
31
|
+
application: Application,
|
|
32
|
+
):
|
|
33
|
+
self.host = host
|
|
34
|
+
self.port = port
|
|
35
|
+
self.application = application
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def __enter__(self: ServerType) -> ServerType:
|
|
39
|
+
"""Return this instance without starting the server."""
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def __exit__(
|
|
43
|
+
self,
|
|
44
|
+
exception_type: Optional[type[BaseException]],
|
|
45
|
+
exception: Optional[BaseException],
|
|
46
|
+
traceback: Optional[TracebackType],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Propagate exceptions; run() is responsible for cleaning up active resources."""
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def run(self) -> None:
|
|
53
|
+
"""Serve until shutdown and manage the application lifecycle."""
|
|
54
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Callable,
|
|
4
|
+
Iterable,
|
|
5
|
+
Mapping,
|
|
6
|
+
Optional,
|
|
7
|
+
Protocol,
|
|
8
|
+
)
|
|
9
|
+
from urllib.parse import quote_from_bytes
|
|
10
|
+
from wsgiref.util import is_hop_by_hop
|
|
11
|
+
|
|
12
|
+
from ..Application import Application
|
|
13
|
+
from ..Error import Error
|
|
14
|
+
from ..IO.RequestReader import RequestReader
|
|
15
|
+
from ..IO.ResponseWriter import ResponseWriter
|
|
16
|
+
from ..Request import Request
|
|
17
|
+
from ..Response import Response
|
|
18
|
+
from .RequestHandler import handle_request
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
'StartResponse',
|
|
22
|
+
'WSGITransport',
|
|
23
|
+
'WSGIResponseWriter',
|
|
24
|
+
'WSGIApplication',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StartResponse(Protocol):
|
|
29
|
+
def __call__(
|
|
30
|
+
self,
|
|
31
|
+
status: str,
|
|
32
|
+
headers: list[tuple[str, str]],
|
|
33
|
+
/,
|
|
34
|
+
) -> Callable[[bytes], object]:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WSGITransport:
|
|
39
|
+
"""Delegate HTTP framing to the WSGI server and forward bytes immediately."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
start_response: StartResponse,
|
|
44
|
+
):
|
|
45
|
+
self.start_response = start_response
|
|
46
|
+
self.output: Optional[Callable[[bytes], object]] = None
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
def begin(
|
|
50
|
+
self,
|
|
51
|
+
response: Response,
|
|
52
|
+
chunked: bool,
|
|
53
|
+
) -> None:
|
|
54
|
+
headers = [
|
|
55
|
+
(
|
|
56
|
+
key,
|
|
57
|
+
value,
|
|
58
|
+
) for key, value in response.headers()
|
|
59
|
+
if not is_hop_by_hop(key)
|
|
60
|
+
and not (key.lower() == 'content-length' and response.status in (
|
|
61
|
+
204,
|
|
62
|
+
205,
|
|
63
|
+
304,
|
|
64
|
+
))
|
|
65
|
+
]
|
|
66
|
+
if response.status == 205:
|
|
67
|
+
headers.append((
|
|
68
|
+
'Content-Length',
|
|
69
|
+
'0',
|
|
70
|
+
))
|
|
71
|
+
self.output = self.start_response(
|
|
72
|
+
f'{response.status} {response.message}',
|
|
73
|
+
headers,
|
|
74
|
+
)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def write(
|
|
78
|
+
self,
|
|
79
|
+
data: bytes,
|
|
80
|
+
/,
|
|
81
|
+
) -> int:
|
|
82
|
+
if self.output is None:
|
|
83
|
+
raise RuntimeError('WSGI response must be started before writing')
|
|
84
|
+
self.output(data)
|
|
85
|
+
return len(data)
|
|
86
|
+
|
|
87
|
+
def flush(self) -> None:
|
|
88
|
+
# Calling WSGI write() forwards the bytes to the server.
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class WSGIResponseWriter(ResponseWriter):
|
|
93
|
+
"""Preserve the direct-output API without applying chunk framing twice."""
|
|
94
|
+
|
|
95
|
+
def start(
|
|
96
|
+
self,
|
|
97
|
+
response: Response,
|
|
98
|
+
chunked: bool,
|
|
99
|
+
) -> None:
|
|
100
|
+
super().start(
|
|
101
|
+
response,
|
|
102
|
+
False,
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
def upgrade(
|
|
107
|
+
self,
|
|
108
|
+
response: Response,
|
|
109
|
+
) -> None:
|
|
110
|
+
raise Error(
|
|
111
|
+
501,
|
|
112
|
+
'WSGI adapters do not support protocol upgrades',
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class WSGIApplication:
|
|
117
|
+
"""Adapt DefEssential requests to the WSGI calls used by Gunicorn and a2wsgi."""
|
|
118
|
+
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
application: Application,
|
|
122
|
+
):
|
|
123
|
+
self.application = application
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
def request_reader(
|
|
127
|
+
self,
|
|
128
|
+
environ: Mapping[str, Any],
|
|
129
|
+
) -> RequestReader:
|
|
130
|
+
length = environ.get('CONTENT_LENGTH', '')
|
|
131
|
+
transfer = environ.get('HTTP_TRANSFER_ENCODING', '')
|
|
132
|
+
if length and (not length.isascii() or not length.isdigit()):
|
|
133
|
+
raise Error(
|
|
134
|
+
400,
|
|
135
|
+
'Invalid Content-Length',
|
|
136
|
+
)
|
|
137
|
+
if transfer and (transfer.lower() != 'chunked' or length):
|
|
138
|
+
raise Error(
|
|
139
|
+
400,
|
|
140
|
+
'Ambiguous or unsupported request body framing',
|
|
141
|
+
)
|
|
142
|
+
return RequestReader(
|
|
143
|
+
environ['wsgi.input'],
|
|
144
|
+
size=int(length) if length else None,
|
|
145
|
+
chunked=transfer.lower() == 'chunked',
|
|
146
|
+
decoded=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def response_writer(
|
|
150
|
+
self,
|
|
151
|
+
environ: Mapping[str, Any],
|
|
152
|
+
start_response: StartResponse,
|
|
153
|
+
) -> ResponseWriter:
|
|
154
|
+
transport = WSGITransport(start_response)
|
|
155
|
+
return WSGIResponseWriter(
|
|
156
|
+
transport.begin,
|
|
157
|
+
transport,
|
|
158
|
+
method=environ['REQUEST_METHOD'],
|
|
159
|
+
version=environ['SERVER_PROTOCOL'],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def __call__(
|
|
163
|
+
self,
|
|
164
|
+
environ: Mapping[str, Any],
|
|
165
|
+
start_response: StartResponse,
|
|
166
|
+
) -> Iterable[bytes]:
|
|
167
|
+
writer = self.response_writer(
|
|
168
|
+
environ,
|
|
169
|
+
start_response,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def create_request() -> Request:
|
|
173
|
+
reader = self.request_reader(environ)
|
|
174
|
+
headers = {
|
|
175
|
+
key[5:].replace('_', '-'): value
|
|
176
|
+
for key, value in environ.items()
|
|
177
|
+
if key.startswith('HTTP_')
|
|
178
|
+
}
|
|
179
|
+
for source, target in (
|
|
180
|
+
(
|
|
181
|
+
'CONTENT_LENGTH',
|
|
182
|
+
'Content-Length',
|
|
183
|
+
),
|
|
184
|
+
(
|
|
185
|
+
'CONTENT_TYPE',
|
|
186
|
+
'Content-Type',
|
|
187
|
+
),
|
|
188
|
+
):
|
|
189
|
+
if environ.get(source):
|
|
190
|
+
headers[target] = environ[source]
|
|
191
|
+
uri = environ.get('RAW_URI') or environ.get('REQUEST_URI')
|
|
192
|
+
if uri is None:
|
|
193
|
+
raw_path = environ.get('asgi.scope', {}).get('raw_path')
|
|
194
|
+
if raw_path is not None:
|
|
195
|
+
uri = raw_path.decode('latin-1')
|
|
196
|
+
else:
|
|
197
|
+
path = environ.get('SCRIPT_NAME', '') + environ['PATH_INFO']
|
|
198
|
+
uri = quote_from_bytes(
|
|
199
|
+
path.encode('latin-1'),
|
|
200
|
+
safe="/:@!$&'()*+,;=-._~",
|
|
201
|
+
)
|
|
202
|
+
if environ.get('QUERY_STRING'):
|
|
203
|
+
uri += '?' + environ['QUERY_STRING']
|
|
204
|
+
return Request(
|
|
205
|
+
address=environ.get('REMOTE_ADDR', ''),
|
|
206
|
+
port=int(environ.get('REMOTE_PORT', 0)),
|
|
207
|
+
method=environ['REQUEST_METHOD'],
|
|
208
|
+
uri=uri,
|
|
209
|
+
headers=headers,
|
|
210
|
+
reader=reader,
|
|
211
|
+
writer=writer,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
handle_request(
|
|
215
|
+
self.application,
|
|
216
|
+
create_request,
|
|
217
|
+
writer,
|
|
218
|
+
)
|
|
219
|
+
return ()
|