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.
Files changed (104) hide show
  1. DefEssential/Adapter/Implements/Gunicorn/Server.py +152 -0
  2. DefEssential/Adapter/Implements/Gunicorn/Transport.py +171 -0
  3. DefEssential/Adapter/Implements/Gunicorn/__init__.py +5 -0
  4. DefEssential/Adapter/Implements/Python/Server.py +273 -0
  5. DefEssential/Adapter/Implements/Python/__init__.py +5 -0
  6. DefEssential/Adapter/Implements/Uvicorn/HTTP.py +256 -0
  7. DefEssential/Adapter/Implements/Uvicorn/Portal.py +65 -0
  8. DefEssential/Adapter/Implements/Uvicorn/Server.py +137 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +222 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +109 -0
  13. DefEssential/Adapter/Server.py +54 -0
  14. DefEssential/Adapter/WSGI.py +219 -0
  15. DefEssential/Adapter/__init__.py +5 -0
  16. DefEssential/Application.py +657 -0
  17. DefEssential/Body.py +50 -0
  18. DefEssential/Content.py +18 -0
  19. DefEssential/Credentials.py +245 -0
  20. DefEssential/CrossOriginResourceSharing.py +107 -0
  21. DefEssential/Descriptor/DataObject.py +409 -0
  22. DefEssential/Descriptor/Types.py +248 -0
  23. DefEssential/Descriptor/Value.py +188 -0
  24. DefEssential/Descriptor/__init__.py +172 -0
  25. DefEssential/Error.py +66 -0
  26. DefEssential/Extension.py +191 -0
  27. DefEssential/Extensions/File.py +95 -0
  28. DefEssential/Extensions/FileSystem.py +195 -0
  29. DefEssential/Extensions/Metrics.py +511 -0
  30. DefEssential/Extensions/Ping.py +93 -0
  31. DefEssential/Extensions/Specifications.py +342 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +110 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +117 -0
  36. DefEssential/Extensions/Utils/Routes.py +42 -0
  37. DefEssential/Extensions/Utils/__init__.py +43 -0
  38. DefEssential/Extensions/__init__.py +21 -0
  39. DefEssential/IO/Readers/ChunkedStream.py +106 -0
  40. DefEssential/IO/Readers/ContentReader.py +36 -0
  41. DefEssential/IO/Readers/ContentReaders.py +241 -0
  42. DefEssential/IO/Readers/__init__.py +25 -0
  43. DefEssential/IO/RequestReader.py +125 -0
  44. DefEssential/IO/ResponseWriter.py +242 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +288 -0
  46. DefEssential/IO/WebSocket/WebSocketError.py +17 -0
  47. DefEssential/IO/WebSocket/WebSocketFrame.py +68 -0
  48. DefEssential/IO/WebSocket/WebSocketFrameReader.py +98 -0
  49. DefEssential/IO/WebSocket/WebSocketHandshake.py +94 -0
  50. DefEssential/IO/WebSocket/__init__.py +13 -0
  51. DefEssential/IO/Writers/ChunkedStreamWriter.py +19 -0
  52. DefEssential/IO/Writers/ServerSentEvents.py +41 -0
  53. DefEssential/IO/Writers/ServerSentEventsWriter.py +67 -0
  54. DefEssential/IO/Writers/StreamWriter.py +57 -0
  55. DefEssential/IO/Writers/__init__.py +11 -0
  56. DefEssential/IO/__init__.py +7 -0
  57. DefEssential/Request.py +110 -0
  58. DefEssential/RequestRunner.py +261 -0
  59. DefEssential/Response.py +89 -0
  60. DefEssential/ResponseSpecification.py +23 -0
  61. DefEssential/Responses/ResponseFile.py +108 -0
  62. DefEssential/Responses/__init__.py +63 -0
  63. DefEssential/Route.py +55 -0
  64. DefEssential/Router.py +48 -0
  65. DefEssential/Runners/RunExtension.py +116 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +198 -0
  69. DefEssential/Runners/__init__.py +5 -0
  70. DefEssential/Startup.py +104 -0
  71. DefEssential/Utils/FileSystemObject/Configuration.py +15 -0
  72. DefEssential/Utils/FileSystemObject/Connection.py +57 -0
  73. DefEssential/Utils/FileSystemObject/File.py +41 -0
  74. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +15 -0
  75. DefEssential/Utils/FileSystemObject/Implements/FileSystem/Connection.py +115 -0
  76. DefEssential/Utils/FileSystemObject/Implements/FileSystem/File.py +77 -0
  77. DefEssential/Utils/FileSystemObject/Implements/FileSystem/__init__.py +11 -0
  78. DefEssential/Utils/FileSystemObject/Implements/__init__.py +3 -0
  79. DefEssential/Utils/FileSystemObject/__init__.py +11 -0
  80. DefEssential/Utils/Loader.py +81 -0
  81. DefEssential/Utils/__init__.py +3 -0
  82. DefEssential/Validator/Pattern.py +36 -0
  83. DefEssential/Validator/Patterns/Array.py +30 -0
  84. DefEssential/Validator/Patterns/Common.py +92 -0
  85. DefEssential/Validator/Patterns/Compare.py +180 -0
  86. DefEssential/Validator/Patterns/Condition.py +60 -0
  87. DefEssential/Validator/Patterns/DataObject.py +85 -0
  88. DefEssential/Validator/Patterns/DateTime.py +111 -0
  89. DefEssential/Validator/Patterns/Number.py +43 -0
  90. DefEssential/Validator/Patterns/Object.py +150 -0
  91. DefEssential/Validator/Patterns/Size.py +110 -0
  92. DefEssential/Validator/Patterns/String.py +85 -0
  93. DefEssential/Validator/Patterns/Type.py +350 -0
  94. DefEssential/Validator/Patterns/__init__.py +137 -0
  95. DefEssential/Validator/Validate.py +29 -0
  96. DefEssential/Validator/Validator.py +30 -0
  97. DefEssential/Validator/__init__.py +11 -0
  98. DefEssential/Validators.py +255 -0
  99. DefEssential/__init__.py +83 -0
  100. defessential-0.1.8.dist-info/METADATA +86 -0
  101. defessential-0.1.8.dist-info/RECORD +104 -0
  102. defessential-0.1.8.dist-info/WHEEL +6 -0
  103. defessential-0.1.8.dist-info/licenses/LICENSE +674 -0
  104. 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 ()
@@ -0,0 +1,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )