DefEssential 0.1.6__19-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 +134 -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 +245 -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 +102 -0
  9. DefEssential/Adapter/Implements/Uvicorn/WebSocket.py +220 -0
  10. DefEssential/Adapter/Implements/Uvicorn/__init__.py +5 -0
  11. DefEssential/Adapter/Implements/__init__.py +3 -0
  12. DefEssential/Adapter/RequestHandler.py +70 -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 +579 -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 +184 -0
  27. DefEssential/Extensions/File.py +89 -0
  28. DefEssential/Extensions/FileSystem.py +184 -0
  29. DefEssential/Extensions/Metrics.py +500 -0
  30. DefEssential/Extensions/Ping.py +89 -0
  31. DefEssential/Extensions/Specifications.py +328 -0
  32. DefEssential/Extensions/Status.html +207 -0
  33. DefEssential/Extensions/Status.py +104 -0
  34. DefEssential/Extensions/Swagger.html +77 -0
  35. DefEssential/Extensions/Swagger.py +111 -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 +241 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +282 -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 +87 -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 +101 -0
  66. DefEssential/Runners/RunFactory.py +26 -0
  67. DefEssential/Runners/RunOptions.py +77 -0
  68. DefEssential/Runners/RunRequest.py +121 -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.6.dist-info/METADATA +41 -0
  101. defessential-0.1.6.dist-info/RECORD +104 -0
  102. defessential-0.1.6.dist-info/WHEEL +6 -0
  103. defessential-0.1.6.dist-info/licenses/LICENSE +674 -0
  104. defessential-0.1.6.dist-info/top_level.txt +1 -0
@@ -0,0 +1,134 @@
1
+ from os import name
2
+ from typing import (
3
+ Any,
4
+ Union,
5
+ )
6
+
7
+ from ....Application import Application
8
+ from ....Startup import startup
9
+ from ...Server import Server as ServerAdapter
10
+ from .Transport import (
11
+ Body,
12
+ GunicornApplication as RequestApplication,
13
+ )
14
+
15
+ __all__ = (
16
+ 'Server',
17
+ )
18
+
19
+
20
+ class Server(ServerAdapter):
21
+ """Gunicorn의 워커별로 애플리케이션을 시작하고 종료한다."""
22
+
23
+ def __init__(
24
+ self,
25
+ host: str,
26
+ port: int,
27
+ application: Application,
28
+ *,
29
+ workers: int = 1,
30
+ worker_class: Union[str, type] = 'sync',
31
+ **options: Any,
32
+ ):
33
+ super().__init__(
34
+ host,
35
+ port,
36
+ application,
37
+ )
38
+ if workers < 1:
39
+ raise ValueError('workers must be at least 1')
40
+ if 'bind' in options:
41
+ raise ValueError('Use host and port instead of bind')
42
+ self.workers = workers
43
+ self.worker_class = worker_class
44
+ self.options = options
45
+ return
46
+
47
+ def run(self) -> None:
48
+ if name == 'nt':
49
+ raise RuntimeError('Gunicorn requires a POSIX platform; use Linux or WSL')
50
+ # Gunicorn은 선택적 의존성이며 Windows에서는 가져올 수 없다.
51
+ from gunicorn.app.base import BaseApplication
52
+
53
+ application = self.application
54
+ options = dict(self.options)
55
+ when_ready = options.pop('when_ready', None)
56
+ post_worker_init = options.pop('post_worker_init', None)
57
+ worker_exit = options.pop('worker_exit', None)
58
+ pre_request = options.pop('pre_request', None)
59
+ address = f'[{self.host}]' if ':' in self.host else self.host
60
+ options.update(
61
+ bind=f'{address}:{self.port}',
62
+ workers=self.workers,
63
+ worker_class=self.worker_class,
64
+ )
65
+
66
+ def ready(server: Any) -> None:
67
+ if when_ready is not None:
68
+ when_ready(server)
69
+ startup(
70
+ application,
71
+ 'Gunicorn',
72
+ self.host,
73
+ self.port,
74
+ )
75
+ return
76
+
77
+ def start(worker: Any) -> None:
78
+ application.start()
79
+ if post_worker_init is not None:
80
+ post_worker_init(worker)
81
+ return
82
+
83
+ def prepare(
84
+ worker: Any,
85
+ request: Any,
86
+ ) -> None:
87
+ if pre_request is not None:
88
+ pre_request(
89
+ worker,
90
+ request,
91
+ )
92
+ request.body = Body(request)
93
+ if any(
94
+ key == 'UPGRADE' and value.lower() == 'websocket'
95
+ for key, value in request.headers
96
+ ):
97
+ request.force_close()
98
+ return
99
+
100
+ def stop(
101
+ server: Any,
102
+ worker: Any,
103
+ ) -> None:
104
+ try:
105
+ application.stop()
106
+ finally:
107
+ if worker_exit is not None:
108
+ worker_exit(
109
+ server,
110
+ worker,
111
+ )
112
+ return
113
+
114
+ options.update(
115
+ when_ready=ready,
116
+ post_worker_init=start,
117
+ worker_exit=stop,
118
+ pre_request=prepare,
119
+ )
120
+
121
+ class GunicornApplication(BaseApplication):
122
+ def load_config(self) -> None:
123
+ for key, value in options.items():
124
+ self.cfg.set(
125
+ key,
126
+ value,
127
+ )
128
+ return
129
+
130
+ def load(self) -> RequestApplication:
131
+ return RequestApplication(application)
132
+
133
+ GunicornApplication().run()
134
+ 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
+ """Gunicorn 파서의 선행 입력과 본문 상태를 프레임워크 및 워커가 공유한다."""
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
+ """워커가 남은 본문을 버릴 때도 애플리케이션이 읽은 위치를 유지한다."""
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
+ """HTTP는 Gunicorn에 맡기고 101 전환 뒤에는 원시 소켓을 사용한다."""
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
+ """sync·gthread·gevent에서 같은 입출력 및 WebSocket API를 제공한다."""
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,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )
@@ -0,0 +1,245 @@
1
+ from http.server import (
2
+ BaseHTTPRequestHandler,
3
+ ThreadingHTTPServer,
4
+ )
5
+ from logging import getLogger
6
+ from typing import Optional
7
+
8
+ from ....Application import Application
9
+ from ....Error import Error
10
+ from ....Request import Request
11
+ from ....IO.RequestReader import RequestReader
12
+ from ....Response import Response
13
+ from ....IO.ResponseWriter import ResponseWriter
14
+ from ....Startup import startup
15
+ from ...Server import Server as ServerAdapter
16
+ from ...RequestHandler import handle_request
17
+
18
+ __all__ = (
19
+ 'Server',
20
+ 'ServerRequestHandler',
21
+ )
22
+
23
+
24
+ class Server(
25
+ ThreadingHTTPServer,
26
+ ServerAdapter,
27
+ ):
28
+ """외부 의존성 없이 자체 HTTP 전송 계층으로 실행하는 기본 서버."""
29
+
30
+ daemon_threads = True
31
+ app: Application
32
+
33
+ def __init__(
34
+ self,
35
+ host: str,
36
+ port: int,
37
+ application: Application,
38
+ *,
39
+ poll_interval: float = 0.5,
40
+ ):
41
+ ServerAdapter.__init__(
42
+ self,
43
+ host,
44
+ port,
45
+ application,
46
+ )
47
+ self.app = application
48
+ self.poll_interval = poll_interval
49
+ ThreadingHTTPServer.__init__(
50
+ self,
51
+ (
52
+ host,
53
+ port,
54
+ ),
55
+ ServerRequestHandler,
56
+ )
57
+ return
58
+
59
+ def run(self) -> None:
60
+ """종료 시 수신 소켓까지 닫고 확장 기능의 종료를 보장한다."""
61
+ with self:
62
+ self.serve_forever(self.poll_interval)
63
+ return
64
+
65
+ def serve_forever(
66
+ self,
67
+ poll_interval: float = 0.5,
68
+ ) -> None:
69
+ host = self.server_address[0]
70
+ if not isinstance(host, str):
71
+ raise TypeError('HTTP listen address must be a string')
72
+ startup(
73
+ self.application,
74
+ 'Python',
75
+ host,
76
+ self.server_port,
77
+ )
78
+ self.app.start()
79
+ try:
80
+ super().serve_forever(poll_interval=poll_interval)
81
+ except KeyboardInterrupt:
82
+ getLogger('DefEssential.Server').info('Shutdown requested by keyboard interrupt')
83
+ finally:
84
+ self.app.stop()
85
+ return
86
+
87
+
88
+ class ServerRequestHandler(BaseHTTPRequestHandler):
89
+ server: Server
90
+ protocol_version = 'HTTP/1.1'
91
+ server_version = 'DefEssential'
92
+ logger = getLogger('DefEssential.Server')
93
+
94
+ def request_reader(self) -> RequestReader:
95
+ lengths = self.headers.get_all('Content-Length', [])
96
+ transfers = self.headers.get_all('Transfer-Encoding', [])
97
+ if len(self.headers.get_all('Content-Type', [])) > 1:
98
+ raise Error(
99
+ 400,
100
+ 'Duplicate Content-Type',
101
+ )
102
+ if len(lengths) > 1 or len(transfers) > 1 or (lengths and transfers):
103
+ raise Error(
104
+ 400,
105
+ 'Ambiguous request body framing',
106
+ )
107
+ size: Optional[int] = None
108
+ if lengths:
109
+ if not lengths[0].isascii() or not lengths[0].isdigit():
110
+ raise Error(
111
+ 400,
112
+ 'Invalid Content-Length',
113
+ )
114
+ size = int(lengths[0])
115
+ if transfers and transfers[0].lower() != 'chunked':
116
+ raise Error(
117
+ 400,
118
+ 'Unsupported Transfer-Encoding',
119
+ )
120
+ return RequestReader(
121
+ self.rfile,
122
+ size=size,
123
+ chunked=bool(transfers),
124
+ )
125
+
126
+ def handle_one_request(self) -> None:
127
+ self.close_connection = True
128
+ try:
129
+ self.raw_requestline = self.rfile.readline(65537)
130
+ if len(self.raw_requestline) > 65536:
131
+ self.requestline = ''
132
+ self.request_version = ''
133
+ self.command = ''
134
+ self.send_error(414)
135
+ return
136
+ if not self.raw_requestline:
137
+ return
138
+ if not self.parse_request():
139
+ return
140
+ except ConnectionError as error:
141
+ self.logger.warning('Client disconnected: %s', error)
142
+ return
143
+ self.close_connection = True
144
+ writer = ResponseWriter(
145
+ self.begin_response,
146
+ self.wfile,
147
+ method=self.command,
148
+ version=self.request_version,
149
+ )
150
+
151
+ def create_request() -> Request:
152
+ return Request(
153
+ address=self.client_address[0],
154
+ port=self.client_address[1],
155
+ method=self.command,
156
+ uri=self.path,
157
+ headers=dict(self.headers.items()),
158
+ reader=self.request_reader(),
159
+ writer=writer,
160
+ )
161
+
162
+ try:
163
+ handle_request(
164
+ self.server.app,
165
+ create_request,
166
+ writer,
167
+ )
168
+ except Exception:
169
+ # 공통 처리기가 기록한 전송 오류는 연결 종료로 마무리한다.
170
+ self.close_connection = True
171
+ return
172
+
173
+ def begin_response(
174
+ self,
175
+ response: Response,
176
+ chunked: bool,
177
+ ) -> None:
178
+ length = response.header('Content-Length')
179
+ self.response_status = response.status
180
+ self.send_response(
181
+ response.status,
182
+ response.message,
183
+ )
184
+ no_body = (
185
+ self.command == 'HEAD'
186
+ or response.status in (
187
+ 204,
188
+ 205,
189
+ 304,
190
+ )
191
+ or response.status < 200
192
+ )
193
+ upgrade = response.status == 101
194
+ excluded = (
195
+ 'content-length',
196
+ 'transfer-encoding',
197
+ ) if upgrade else (
198
+ 'connection',
199
+ 'content-length',
200
+ 'transfer-encoding',
201
+ )
202
+ for key, value in response.headers():
203
+ if key.lower() not in excluded:
204
+ self.send_header(
205
+ key,
206
+ value,
207
+ )
208
+ if chunked and not no_body:
209
+ self.send_header(
210
+ 'Transfer-Encoding',
211
+ 'chunked',
212
+ )
213
+ elif length is not None and response.status not in (
214
+ 204,
215
+ 205,
216
+ 304,
217
+ ):
218
+ self.send_header(
219
+ 'Content-Length',
220
+ length,
221
+ )
222
+ if response.status == 205:
223
+ self.send_header(
224
+ 'Content-Length',
225
+ '0',
226
+ )
227
+ if not upgrade:
228
+ self.send_header(
229
+ 'Connection',
230
+ 'close',
231
+ )
232
+ self.end_headers()
233
+ return
234
+
235
+ def log_message(
236
+ self,
237
+ format: str,
238
+ *args: object,
239
+ ) -> None:
240
+ self.logger.info(
241
+ '%s - %s',
242
+ self.address_string(),
243
+ format % args,
244
+ )
245
+ return
@@ -0,0 +1,5 @@
1
+ from .Server import Server
2
+
3
+ __all__ = (
4
+ 'Server',
5
+ )