DefEssential 0.1.8__23-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 +82 -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,152 @@
|
|
|
1
|
+
from os import name
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Union,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from ....Application import Application
|
|
9
|
+
from ....Startup import startup
|
|
10
|
+
from ...Server import Server as ServerAdapter
|
|
11
|
+
from .Transport import (
|
|
12
|
+
Body,
|
|
13
|
+
GunicornApplication as RequestApplication,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
'Server',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Server(ServerAdapter):
|
|
22
|
+
"""Start and stop the application in each Gunicorn worker."""
|
|
23
|
+
|
|
24
|
+
logger = getLogger('DefEssential.Server')
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
host: str,
|
|
29
|
+
port: int,
|
|
30
|
+
application: Application,
|
|
31
|
+
*,
|
|
32
|
+
workers: int = 1,
|
|
33
|
+
worker_class: Union[str, type] = 'sync',
|
|
34
|
+
**options: Any,
|
|
35
|
+
):
|
|
36
|
+
super().__init__(
|
|
37
|
+
host,
|
|
38
|
+
port,
|
|
39
|
+
application,
|
|
40
|
+
)
|
|
41
|
+
if workers < 1:
|
|
42
|
+
raise ValueError('workers must be at least 1')
|
|
43
|
+
if 'bind' in options:
|
|
44
|
+
raise ValueError('Use host and port instead of bind')
|
|
45
|
+
self.workers = workers
|
|
46
|
+
self.worker_class = worker_class
|
|
47
|
+
self.options = options
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
def run(self) -> None:
|
|
51
|
+
if name == 'nt':
|
|
52
|
+
raise RuntimeError('Gunicorn requires a POSIX platform; use Linux or WSL')
|
|
53
|
+
# Gunicorn is an optional dependency and cannot be imported on Windows.
|
|
54
|
+
from gunicorn.app.base import BaseApplication
|
|
55
|
+
|
|
56
|
+
application = self.application
|
|
57
|
+
options = dict(self.options)
|
|
58
|
+
when_ready = options.pop('when_ready', None)
|
|
59
|
+
post_worker_init = options.pop('post_worker_init', None)
|
|
60
|
+
worker_exit = options.pop('worker_exit', None)
|
|
61
|
+
pre_request = options.pop('pre_request', None)
|
|
62
|
+
address = f'[{self.host}]' if ':' in self.host else self.host
|
|
63
|
+
options.update(
|
|
64
|
+
bind=f'{address}:{self.port}',
|
|
65
|
+
workers=self.workers,
|
|
66
|
+
worker_class=self.worker_class,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def ready(server: Any) -> None:
|
|
70
|
+
if when_ready is not None:
|
|
71
|
+
when_ready(server)
|
|
72
|
+
startup(
|
|
73
|
+
application,
|
|
74
|
+
'Gunicorn',
|
|
75
|
+
self.host,
|
|
76
|
+
self.port,
|
|
77
|
+
)
|
|
78
|
+
for listener in server.LISTENERS:
|
|
79
|
+
self.logger.info(
|
|
80
|
+
'Server ready: adapter=Gunicorn address=%s',
|
|
81
|
+
listener.getsockname(),
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
def start(worker: Any) -> None:
|
|
86
|
+
application.start()
|
|
87
|
+
if post_worker_init is not None:
|
|
88
|
+
post_worker_init(worker)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
def prepare(
|
|
92
|
+
worker: Any,
|
|
93
|
+
request: Any,
|
|
94
|
+
) -> None:
|
|
95
|
+
if pre_request is not None:
|
|
96
|
+
pre_request(
|
|
97
|
+
worker,
|
|
98
|
+
request,
|
|
99
|
+
)
|
|
100
|
+
request.body = Body(request)
|
|
101
|
+
if any(
|
|
102
|
+
key == 'UPGRADE' and value.lower() == 'websocket'
|
|
103
|
+
for key, value in request.headers
|
|
104
|
+
):
|
|
105
|
+
request.force_close()
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
def stop(
|
|
109
|
+
server: Any,
|
|
110
|
+
worker: Any,
|
|
111
|
+
) -> None:
|
|
112
|
+
try:
|
|
113
|
+
application.stop()
|
|
114
|
+
finally:
|
|
115
|
+
if worker_exit is not None:
|
|
116
|
+
worker_exit(
|
|
117
|
+
server,
|
|
118
|
+
worker,
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
options.update(
|
|
123
|
+
when_ready=ready,
|
|
124
|
+
post_worker_init=start,
|
|
125
|
+
worker_exit=stop,
|
|
126
|
+
pre_request=prepare,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
class GunicornApplication(BaseApplication):
|
|
130
|
+
def load_config(self) -> None:
|
|
131
|
+
for key, value in options.items():
|
|
132
|
+
self.cfg.set(
|
|
133
|
+
key,
|
|
134
|
+
value,
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
def load(self) -> RequestApplication:
|
|
139
|
+
return RequestApplication(application)
|
|
140
|
+
|
|
141
|
+
self.logger.info('Server starting: adapter=Gunicorn host=%s port=%s', self.host, self.port)
|
|
142
|
+
try:
|
|
143
|
+
GunicornApplication().run()
|
|
144
|
+
except SystemExit as error:
|
|
145
|
+
if error.code:
|
|
146
|
+
self.logger.error('Server exited unsuccessfully: adapter=Gunicorn code=%s', error.code)
|
|
147
|
+
raise
|
|
148
|
+
except Exception:
|
|
149
|
+
self.logger.exception('Server execution failed: adapter=Gunicorn')
|
|
150
|
+
raise
|
|
151
|
+
self.logger.info('Server stopped: adapter=Gunicorn')
|
|
152
|
+
return
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Mapping,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from ....IO.Readers.ChunkedStream import ChunkedStream
|
|
7
|
+
from ....IO.RequestReader import RequestReader
|
|
8
|
+
from ....IO.ResponseWriter import ResponseWriter
|
|
9
|
+
from ....Response import Response
|
|
10
|
+
from ...WSGI import (
|
|
11
|
+
StartResponse,
|
|
12
|
+
WSGIApplication,
|
|
13
|
+
WSGIResponseWriter,
|
|
14
|
+
WSGITransport,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
'Input',
|
|
19
|
+
'Body',
|
|
20
|
+
'Transport',
|
|
21
|
+
'Writer',
|
|
22
|
+
'GunicornApplication',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Input:
|
|
27
|
+
"""Share the Gunicorn parser's buffered input and body state with the framework and worker."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
request: Any,
|
|
32
|
+
):
|
|
33
|
+
self.unreader = request.unreader
|
|
34
|
+
headers = dict(request.headers)
|
|
35
|
+
length = headers.get('CONTENT-LENGTH')
|
|
36
|
+
self.request_reader = RequestReader(
|
|
37
|
+
self,
|
|
38
|
+
size=int(length) if length is not None else None,
|
|
39
|
+
chunked=headers.get('TRANSFER-ENCODING', '').lower() == 'chunked',
|
|
40
|
+
)
|
|
41
|
+
self.body = ChunkedStream(self.request_reader) if self.request_reader.chunked else self.request_reader
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def read(
|
|
45
|
+
self,
|
|
46
|
+
size: int = -1,
|
|
47
|
+
/,
|
|
48
|
+
) -> bytes:
|
|
49
|
+
return self.unreader.read(size)
|
|
50
|
+
|
|
51
|
+
def readline(
|
|
52
|
+
self,
|
|
53
|
+
size: int = -1,
|
|
54
|
+
/,
|
|
55
|
+
) -> bytes:
|
|
56
|
+
parts = []
|
|
57
|
+
while size < 0 or len(parts) < size:
|
|
58
|
+
data = self.unreader.read(1)
|
|
59
|
+
if not data:
|
|
60
|
+
break
|
|
61
|
+
parts.append(data)
|
|
62
|
+
if data == b'\n':
|
|
63
|
+
break
|
|
64
|
+
return b''.join(parts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Body:
|
|
68
|
+
"""Preserve the application's read position when the worker discards the remaining body."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
request: Any,
|
|
73
|
+
):
|
|
74
|
+
self.input = Input(request)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
def read(
|
|
78
|
+
self,
|
|
79
|
+
size: int = -1,
|
|
80
|
+
) -> bytes:
|
|
81
|
+
return self.input.body.read(size)
|
|
82
|
+
|
|
83
|
+
def readline(
|
|
84
|
+
self,
|
|
85
|
+
size: int = -1,
|
|
86
|
+
) -> bytes:
|
|
87
|
+
return self.input.body.readline(size)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Transport(WSGITransport):
|
|
91
|
+
"""Delegate HTTP to Gunicorn and use the raw socket after a 101 upgrade."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
start_response: StartResponse,
|
|
96
|
+
socket: Any,
|
|
97
|
+
):
|
|
98
|
+
super().__init__(start_response)
|
|
99
|
+
self.socket = socket
|
|
100
|
+
self.upgraded = False
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
def begin(
|
|
104
|
+
self,
|
|
105
|
+
response: Response,
|
|
106
|
+
chunked: bool,
|
|
107
|
+
) -> None:
|
|
108
|
+
if response.status != 101:
|
|
109
|
+
super().begin(
|
|
110
|
+
response,
|
|
111
|
+
chunked,
|
|
112
|
+
)
|
|
113
|
+
return
|
|
114
|
+
output = self.start_response(
|
|
115
|
+
f'{response.status} {response.message}',
|
|
116
|
+
response.headers(),
|
|
117
|
+
)
|
|
118
|
+
output(b'')
|
|
119
|
+
self.upgraded = True
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def write(
|
|
123
|
+
self,
|
|
124
|
+
data: bytes,
|
|
125
|
+
/,
|
|
126
|
+
) -> int:
|
|
127
|
+
if self.upgraded:
|
|
128
|
+
self.socket.sendall(data)
|
|
129
|
+
return len(data)
|
|
130
|
+
return super().write(data)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Writer(WSGIResponseWriter):
|
|
134
|
+
def upgrade(
|
|
135
|
+
self,
|
|
136
|
+
response: Response,
|
|
137
|
+
) -> None:
|
|
138
|
+
ResponseWriter.upgrade(
|
|
139
|
+
self,
|
|
140
|
+
response,
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class GunicornApplication(WSGIApplication):
|
|
146
|
+
"""Provide the same I/O and WebSocket API for sync, gthread, and gevent workers."""
|
|
147
|
+
|
|
148
|
+
def request_reader(
|
|
149
|
+
self,
|
|
150
|
+
environ: Mapping[str, Any],
|
|
151
|
+
) -> RequestReader:
|
|
152
|
+
body = environ['wsgi.input']
|
|
153
|
+
if not isinstance(body, Body):
|
|
154
|
+
raise TypeError('Gunicorn input hook was not installed')
|
|
155
|
+
return body.input.request_reader
|
|
156
|
+
|
|
157
|
+
def response_writer(
|
|
158
|
+
self,
|
|
159
|
+
environ: Mapping[str, Any],
|
|
160
|
+
start_response: StartResponse,
|
|
161
|
+
) -> ResponseWriter:
|
|
162
|
+
transport = Transport(
|
|
163
|
+
start_response,
|
|
164
|
+
environ['gunicorn.socket'],
|
|
165
|
+
)
|
|
166
|
+
return Writer(
|
|
167
|
+
transport.begin,
|
|
168
|
+
transport,
|
|
169
|
+
method=environ['REQUEST_METHOD'],
|
|
170
|
+
version=environ['SERVER_PROTOCOL'],
|
|
171
|
+
)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from http.server import (
|
|
2
|
+
BaseHTTPRequestHandler,
|
|
3
|
+
ThreadingHTTPServer,
|
|
4
|
+
)
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import (
|
|
7
|
+
Optional,
|
|
8
|
+
Union,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from ....Application import Application
|
|
12
|
+
from ....Error import Error
|
|
13
|
+
from ....Request import Request
|
|
14
|
+
from ....IO.RequestReader import RequestReader
|
|
15
|
+
from ....Response import Response
|
|
16
|
+
from ....IO.ResponseWriter import ResponseWriter
|
|
17
|
+
from ....Startup import startup
|
|
18
|
+
from ...Server import Server as ServerAdapter
|
|
19
|
+
from ...RequestHandler import handle_request
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
'Server',
|
|
23
|
+
'ServerRequestHandler',
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Server(
|
|
28
|
+
ThreadingHTTPServer,
|
|
29
|
+
ServerAdapter,
|
|
30
|
+
):
|
|
31
|
+
"""Run the built-in HTTP transport without external dependencies."""
|
|
32
|
+
|
|
33
|
+
daemon_threads = True
|
|
34
|
+
app: Application
|
|
35
|
+
logger = getLogger('DefEssential.Server')
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
host: str,
|
|
40
|
+
port: int,
|
|
41
|
+
application: Application,
|
|
42
|
+
*,
|
|
43
|
+
poll_interval: float = 0.5,
|
|
44
|
+
):
|
|
45
|
+
ServerAdapter.__init__(
|
|
46
|
+
self,
|
|
47
|
+
host,
|
|
48
|
+
port,
|
|
49
|
+
application,
|
|
50
|
+
)
|
|
51
|
+
self.app = application
|
|
52
|
+
self.poll_interval = poll_interval
|
|
53
|
+
try:
|
|
54
|
+
ThreadingHTTPServer.__init__(
|
|
55
|
+
self,
|
|
56
|
+
(
|
|
57
|
+
host,
|
|
58
|
+
port,
|
|
59
|
+
),
|
|
60
|
+
ServerRequestHandler,
|
|
61
|
+
)
|
|
62
|
+
except Exception:
|
|
63
|
+
self.logger.exception('Server bind failed: adapter=Python host=%s port=%s', host, port)
|
|
64
|
+
raise
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
def run(self) -> None:
|
|
68
|
+
"""Close the listening socket and ensure extensions shut down when serving stops."""
|
|
69
|
+
with self:
|
|
70
|
+
self.serve_forever(self.poll_interval)
|
|
71
|
+
self.logger.info('Server stopped: adapter=Python')
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
def serve_forever(
|
|
75
|
+
self,
|
|
76
|
+
poll_interval: float = 0.5,
|
|
77
|
+
) -> None:
|
|
78
|
+
host = self.server_address[0]
|
|
79
|
+
if not isinstance(host, str):
|
|
80
|
+
raise TypeError('HTTP listen address must be a string')
|
|
81
|
+
self.logger.info('Server starting: adapter=Python host=%s port=%s', host, self.server_port)
|
|
82
|
+
startup(
|
|
83
|
+
self.application,
|
|
84
|
+
'Python',
|
|
85
|
+
host,
|
|
86
|
+
self.server_port,
|
|
87
|
+
)
|
|
88
|
+
self.app.start()
|
|
89
|
+
self.logger.info('Server ready: adapter=Python host=%s port=%s', host, self.server_port)
|
|
90
|
+
try:
|
|
91
|
+
super().serve_forever(poll_interval=poll_interval)
|
|
92
|
+
except KeyboardInterrupt:
|
|
93
|
+
self.logger.info('Shutdown requested by keyboard interrupt: adapter=Python')
|
|
94
|
+
except Exception:
|
|
95
|
+
self.logger.exception('Server execution failed: adapter=Python')
|
|
96
|
+
raise
|
|
97
|
+
finally:
|
|
98
|
+
self.logger.info('Server stopping: adapter=Python')
|
|
99
|
+
self.app.stop()
|
|
100
|
+
self.logger.info('Server serving stopped: adapter=Python')
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ServerRequestHandler(BaseHTTPRequestHandler):
|
|
105
|
+
server: Server
|
|
106
|
+
protocol_version = 'HTTP/1.1'
|
|
107
|
+
server_version = 'DefEssential'
|
|
108
|
+
logger = getLogger('DefEssential.Server')
|
|
109
|
+
|
|
110
|
+
def request_reader(self) -> RequestReader:
|
|
111
|
+
lengths = self.headers.get_all('Content-Length', [])
|
|
112
|
+
transfers = self.headers.get_all('Transfer-Encoding', [])
|
|
113
|
+
if len(self.headers.get_all('Content-Type', [])) > 1:
|
|
114
|
+
raise Error(
|
|
115
|
+
400,
|
|
116
|
+
'Duplicate Content-Type',
|
|
117
|
+
)
|
|
118
|
+
if len(lengths) > 1 or len(transfers) > 1 or (lengths and transfers):
|
|
119
|
+
raise Error(
|
|
120
|
+
400,
|
|
121
|
+
'Ambiguous request body framing',
|
|
122
|
+
)
|
|
123
|
+
size: Optional[int] = None
|
|
124
|
+
if lengths:
|
|
125
|
+
if not lengths[0].isascii() or not lengths[0].isdigit():
|
|
126
|
+
raise Error(
|
|
127
|
+
400,
|
|
128
|
+
'Invalid Content-Length',
|
|
129
|
+
)
|
|
130
|
+
size = int(lengths[0])
|
|
131
|
+
if transfers and transfers[0].lower() != 'chunked':
|
|
132
|
+
raise Error(
|
|
133
|
+
400,
|
|
134
|
+
'Unsupported Transfer-Encoding',
|
|
135
|
+
)
|
|
136
|
+
return RequestReader(
|
|
137
|
+
self.rfile,
|
|
138
|
+
size=size,
|
|
139
|
+
chunked=bool(transfers),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def handle_one_request(self) -> None:
|
|
143
|
+
self.close_connection = True
|
|
144
|
+
try:
|
|
145
|
+
self.raw_requestline = self.rfile.readline(65537)
|
|
146
|
+
if len(self.raw_requestline) > 65536:
|
|
147
|
+
self.requestline = ''
|
|
148
|
+
self.request_version = ''
|
|
149
|
+
self.command = ''
|
|
150
|
+
self.send_error(414)
|
|
151
|
+
return
|
|
152
|
+
if not self.raw_requestline:
|
|
153
|
+
return
|
|
154
|
+
if not self.parse_request():
|
|
155
|
+
return
|
|
156
|
+
except ConnectionError:
|
|
157
|
+
self.logger.warning('Client disconnected before request parsing: adapter=Python')
|
|
158
|
+
return
|
|
159
|
+
self.close_connection = True
|
|
160
|
+
writer = ResponseWriter(
|
|
161
|
+
self.begin_response,
|
|
162
|
+
self.wfile,
|
|
163
|
+
method=self.command,
|
|
164
|
+
version=self.request_version,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def create_request() -> Request:
|
|
168
|
+
return Request(
|
|
169
|
+
address=self.client_address[0],
|
|
170
|
+
port=self.client_address[1],
|
|
171
|
+
method=self.command,
|
|
172
|
+
uri=self.path,
|
|
173
|
+
headers=dict(self.headers.items()),
|
|
174
|
+
reader=self.request_reader(),
|
|
175
|
+
writer=writer,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
handle_request(
|
|
180
|
+
self.server.app,
|
|
181
|
+
create_request,
|
|
182
|
+
writer,
|
|
183
|
+
)
|
|
184
|
+
except Exception:
|
|
185
|
+
# The shared handler logs transport errors; finish by closing the connection.
|
|
186
|
+
self.close_connection = True
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
def begin_response(
|
|
190
|
+
self,
|
|
191
|
+
response: Response,
|
|
192
|
+
chunked: bool,
|
|
193
|
+
) -> None:
|
|
194
|
+
length = response.header('Content-Length')
|
|
195
|
+
self.response_status = response.status
|
|
196
|
+
self.send_response(
|
|
197
|
+
response.status,
|
|
198
|
+
response.message,
|
|
199
|
+
)
|
|
200
|
+
no_body = (
|
|
201
|
+
self.command == 'HEAD'
|
|
202
|
+
or response.status in (
|
|
203
|
+
204,
|
|
204
|
+
205,
|
|
205
|
+
304,
|
|
206
|
+
)
|
|
207
|
+
or response.status < 200
|
|
208
|
+
)
|
|
209
|
+
upgrade = response.status == 101
|
|
210
|
+
excluded = (
|
|
211
|
+
'content-length',
|
|
212
|
+
'transfer-encoding',
|
|
213
|
+
) if upgrade else (
|
|
214
|
+
'connection',
|
|
215
|
+
'content-length',
|
|
216
|
+
'transfer-encoding',
|
|
217
|
+
)
|
|
218
|
+
for key, value in response.headers():
|
|
219
|
+
if key.lower() not in excluded:
|
|
220
|
+
self.send_header(
|
|
221
|
+
key,
|
|
222
|
+
value,
|
|
223
|
+
)
|
|
224
|
+
if chunked and not no_body:
|
|
225
|
+
self.send_header(
|
|
226
|
+
'Transfer-Encoding',
|
|
227
|
+
'chunked',
|
|
228
|
+
)
|
|
229
|
+
elif length is not None and response.status not in (
|
|
230
|
+
204,
|
|
231
|
+
205,
|
|
232
|
+
304,
|
|
233
|
+
):
|
|
234
|
+
self.send_header(
|
|
235
|
+
'Content-Length',
|
|
236
|
+
length,
|
|
237
|
+
)
|
|
238
|
+
if response.status == 205:
|
|
239
|
+
self.send_header(
|
|
240
|
+
'Content-Length',
|
|
241
|
+
'0',
|
|
242
|
+
)
|
|
243
|
+
if not upgrade:
|
|
244
|
+
self.send_header(
|
|
245
|
+
'Connection',
|
|
246
|
+
'close',
|
|
247
|
+
)
|
|
248
|
+
self.end_headers()
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
def log_message(
|
|
252
|
+
self,
|
|
253
|
+
format: str,
|
|
254
|
+
*args: object,
|
|
255
|
+
) -> None:
|
|
256
|
+
self.logger.debug(
|
|
257
|
+
'HTTP parser diagnostic: method=%s',
|
|
258
|
+
ascii(self.command[:32]) if self.command is not None else None,
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
def log_request(
|
|
263
|
+
self,
|
|
264
|
+
code: Union[int, str] = '-',
|
|
265
|
+
size: Union[int, str] = '-',
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Keep shared result logging separate because selecting headers does not complete transmission."""
|
|
268
|
+
self.logger.debug(
|
|
269
|
+
'HTTP response headers selected: status=%s method=%s',
|
|
270
|
+
code,
|
|
271
|
+
ascii(self.command[:32]) if self.command is not None else None,
|
|
272
|
+
)
|
|
273
|
+
return
|