DefEssential 0.1.8__27-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,110 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import (
|
|
3
|
+
Optional,
|
|
4
|
+
Sequence,
|
|
5
|
+
Union,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from ..Application import Application
|
|
9
|
+
from ..Extension import (
|
|
10
|
+
Extension,
|
|
11
|
+
ExtensionCleanup,
|
|
12
|
+
ExtensionRequest,
|
|
13
|
+
ExtensionRequestError,
|
|
14
|
+
ExtensionRequestResponse,
|
|
15
|
+
ExtensionStartup,
|
|
16
|
+
)
|
|
17
|
+
from ..Request import Request
|
|
18
|
+
from ..Response import Response
|
|
19
|
+
from .Utils import page
|
|
20
|
+
from .Utils.Routes import (
|
|
21
|
+
http_routes,
|
|
22
|
+
validate_path,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
'Status',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Status(Extension):
|
|
31
|
+
"""Provide a status page using an explicitly configured same-origin metrics path."""
|
|
32
|
+
|
|
33
|
+
logger = getLogger('DefEssential.Extensions.Status')
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
path: str,
|
|
38
|
+
*,
|
|
39
|
+
metrics_uri: str,
|
|
40
|
+
title: str = 'DefEssential',
|
|
41
|
+
description: str = '',
|
|
42
|
+
on_startup: Optional[Union[
|
|
43
|
+
ExtensionStartup,
|
|
44
|
+
Sequence[ExtensionStartup],
|
|
45
|
+
]] = None,
|
|
46
|
+
on_cleanup: Optional[Union[
|
|
47
|
+
ExtensionCleanup,
|
|
48
|
+
Sequence[ExtensionCleanup],
|
|
49
|
+
]] = None,
|
|
50
|
+
on_request: Optional[Union[
|
|
51
|
+
ExtensionRequest,
|
|
52
|
+
Sequence[ExtensionRequest],
|
|
53
|
+
]] = None,
|
|
54
|
+
on_request_response: Optional[Union[
|
|
55
|
+
ExtensionRequestResponse,
|
|
56
|
+
Sequence[ExtensionRequestResponse],
|
|
57
|
+
]] = None,
|
|
58
|
+
on_request_error: Optional[Union[
|
|
59
|
+
ExtensionRequestError,
|
|
60
|
+
Sequence[ExtensionRequestError],
|
|
61
|
+
]] = None,
|
|
62
|
+
):
|
|
63
|
+
super().__init__(
|
|
64
|
+
on_startup=on_startup,
|
|
65
|
+
on_cleanup=on_cleanup,
|
|
66
|
+
on_request=on_request,
|
|
67
|
+
on_request_response=on_request_response,
|
|
68
|
+
on_request_error=on_request_error,
|
|
69
|
+
)
|
|
70
|
+
validate_path(path)
|
|
71
|
+
self.path = path
|
|
72
|
+
if (
|
|
73
|
+
not isinstance(metrics_uri, str)
|
|
74
|
+
or not metrics_uri.startswith('/')
|
|
75
|
+
or metrics_uri.startswith('//')
|
|
76
|
+
or any(character.isspace() or ord(character) < 32 or character == '\\' for character in metrics_uri)
|
|
77
|
+
):
|
|
78
|
+
raise ValueError('Status metrics_uri must be a same-origin absolute path')
|
|
79
|
+
if not isinstance(title, str) or not title or not isinstance(description, str):
|
|
80
|
+
raise ValueError('Status title must be a non-empty string and description must be a string')
|
|
81
|
+
self.title = title
|
|
82
|
+
self.description = description
|
|
83
|
+
self.metrics_uri = metrics_uri
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return '{}(uri={}, metrics={})'.format(type(self).__name__, self.path, self.metrics_uri)
|
|
88
|
+
|
|
89
|
+
def startup(
|
|
90
|
+
self,
|
|
91
|
+
application: Application,
|
|
92
|
+
) -> None:
|
|
93
|
+
for route in http_routes(self, self.path):
|
|
94
|
+
application.add_extension_route(self, route)
|
|
95
|
+
super().startup(application)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
def __call__(
|
|
99
|
+
self,
|
|
100
|
+
request: Request,
|
|
101
|
+
) -> Response:
|
|
102
|
+
self.logger.debug('Status page preparing: request_id=%s', request.id)
|
|
103
|
+
response = page(
|
|
104
|
+
'Status',
|
|
105
|
+
self.title,
|
|
106
|
+
self.description,
|
|
107
|
+
parameters={'metrics_uri': self.metrics_uri},
|
|
108
|
+
)
|
|
109
|
+
self.logger.debug('Status page prepared: request_id=%s', request.id)
|
|
110
|
+
return response
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{{title}} Swagger UI</title>
|
|
7
|
+
<meta name="description" content="{{description}}">
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--ui-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", "Apple SD Gothic Neo", sans-serif;
|
|
11
|
+
--code-font: Consolas, "SFMono-Regular", Menlo, "Malgun Gothic", "Apple SD Gothic Neo", monospace;
|
|
12
|
+
}
|
|
13
|
+
body { margin: 0; background: #fafafa; font-family: var(--ui-font); }
|
|
14
|
+
.swagger-ui, .swagger-ui * { font-family: var(--ui-font) !important; }
|
|
15
|
+
.swagger-ui code, .swagger-ui code *, .swagger-ui pre, .swagger-ui pre *,
|
|
16
|
+
.swagger-ui textarea, .swagger-ui .opblock-summary-path, .swagger-ui .opblock-summary-path *,
|
|
17
|
+
.swagger-ui .opblock-summary-path__deprecated, .swagger-ui .opblock-summary-path__deprecated * {
|
|
18
|
+
font-family: var(--code-font) !important;
|
|
19
|
+
}
|
|
20
|
+
#loading, #error, noscript { display: block; margin: 16px 20px; font-size: 14px; line-height: 1.6; }
|
|
21
|
+
#error { color: #b00020; white-space: pre-wrap; }
|
|
22
|
+
#error a { margin-left: 8px; }
|
|
23
|
+
[hidden] { display: none !important; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<p id="loading" role="status">Swagger UI를 불러오는 중입니다.</p>
|
|
28
|
+
<div id="error" role="alert" hidden><span id="error-message"></span><a href="{{spec_url}}">OpenAPI 문서</a></div>
|
|
29
|
+
<noscript>Swagger UI를 사용하려면 JavaScript를 활성화하세요. <a href="{{spec_url}}">OpenAPI 문서</a></noscript>
|
|
30
|
+
<div id="swagger-ui" data-spec-url="{{spec_url}}"></div>
|
|
31
|
+
<script>
|
|
32
|
+
'use strict';
|
|
33
|
+
const error = document.getElementById('error');
|
|
34
|
+
function fail(message) {
|
|
35
|
+
document.getElementById('loading').hidden = true;
|
|
36
|
+
error.hidden = false;
|
|
37
|
+
document.getElementById('error-message').textContent = message;
|
|
38
|
+
}
|
|
39
|
+
const stylesheet = document.createElement('link');
|
|
40
|
+
stylesheet.rel = 'stylesheet';
|
|
41
|
+
stylesheet.crossOrigin = 'anonymous';
|
|
42
|
+
stylesheet.href = 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.0/swagger-ui.css';
|
|
43
|
+
stylesheet.addEventListener('error', () => fail('Failed to load Swagger UI styles. Check your CDN connection.'));
|
|
44
|
+
document.head.append(stylesheet);
|
|
45
|
+
</script>
|
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.0/swagger-ui-bundle.js" crossorigin="anonymous"></script>
|
|
47
|
+
<script>
|
|
48
|
+
if (typeof SwaggerUIBundle !== 'function') {
|
|
49
|
+
fail('Failed to load Swagger UI. Check your CDN connection.');
|
|
50
|
+
} else {
|
|
51
|
+
try {
|
|
52
|
+
window.ui = SwaggerUIBundle({
|
|
53
|
+
url: document.getElementById('swagger-ui').dataset.specUrl,
|
|
54
|
+
dom_id: '#swagger-ui',
|
|
55
|
+
deepLinking: true,
|
|
56
|
+
validatorUrl: null,
|
|
57
|
+
persistAuthorization: false,
|
|
58
|
+
queryConfigEnabled: false,
|
|
59
|
+
withCredentials: false,
|
|
60
|
+
requestInterceptor(request) {
|
|
61
|
+
const url = new URL(request.url, location.origin);
|
|
62
|
+
if (url.origin !== location.origin || url.username || url.password) {
|
|
63
|
+
throw new Error('Only same-origin API and document requests are allowed.');
|
|
64
|
+
}
|
|
65
|
+
request.credentials = 'same-origin';
|
|
66
|
+
request.redirect = 'error';
|
|
67
|
+
return request;
|
|
68
|
+
},
|
|
69
|
+
onComplete() { document.getElementById('loading').hidden = true; }
|
|
70
|
+
});
|
|
71
|
+
} catch (failure) {
|
|
72
|
+
fail('Failed to initialize Swagger UI: ' + failure.message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import (
|
|
3
|
+
Optional,
|
|
4
|
+
Sequence,
|
|
5
|
+
Union,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from ..Application import Application
|
|
9
|
+
from ..Extension import (
|
|
10
|
+
Extension,
|
|
11
|
+
ExtensionCleanup,
|
|
12
|
+
ExtensionRequest,
|
|
13
|
+
ExtensionRequestError,
|
|
14
|
+
ExtensionRequestResponse,
|
|
15
|
+
ExtensionStartup,
|
|
16
|
+
)
|
|
17
|
+
from ..Request import Request
|
|
18
|
+
from ..Response import Response
|
|
19
|
+
from .Utils import page
|
|
20
|
+
from .Utils.Routes import (
|
|
21
|
+
http_routes,
|
|
22
|
+
validate_path,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
'Swagger',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Swagger(Extension):
|
|
31
|
+
"""Connect Swagger UI to an explicitly configured same-origin specification path."""
|
|
32
|
+
|
|
33
|
+
logger = getLogger('DefEssential.Extensions.Swagger')
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
path: str,
|
|
38
|
+
*,
|
|
39
|
+
spec_uri: str,
|
|
40
|
+
title: str = 'DefEssential',
|
|
41
|
+
description: str = '',
|
|
42
|
+
on_startup: Optional[Union[
|
|
43
|
+
ExtensionStartup,
|
|
44
|
+
Sequence[ExtensionStartup],
|
|
45
|
+
]] = None,
|
|
46
|
+
on_cleanup: Optional[Union[
|
|
47
|
+
ExtensionCleanup,
|
|
48
|
+
Sequence[ExtensionCleanup],
|
|
49
|
+
]] = None,
|
|
50
|
+
on_request: Optional[Union[
|
|
51
|
+
ExtensionRequest,
|
|
52
|
+
Sequence[ExtensionRequest],
|
|
53
|
+
]] = None,
|
|
54
|
+
on_request_response: Optional[Union[
|
|
55
|
+
ExtensionRequestResponse,
|
|
56
|
+
Sequence[ExtensionRequestResponse],
|
|
57
|
+
]] = None,
|
|
58
|
+
on_request_error: Optional[Union[
|
|
59
|
+
ExtensionRequestError,
|
|
60
|
+
Sequence[ExtensionRequestError],
|
|
61
|
+
]] = None,
|
|
62
|
+
):
|
|
63
|
+
super().__init__(
|
|
64
|
+
on_startup=on_startup,
|
|
65
|
+
on_cleanup=on_cleanup,
|
|
66
|
+
on_request=on_request,
|
|
67
|
+
on_request_response=on_request_response,
|
|
68
|
+
on_request_error=on_request_error,
|
|
69
|
+
)
|
|
70
|
+
validate_path(path)
|
|
71
|
+
self.path = path
|
|
72
|
+
if (
|
|
73
|
+
not isinstance(spec_uri, str)
|
|
74
|
+
or not spec_uri.startswith('/')
|
|
75
|
+
or spec_uri.startswith('//')
|
|
76
|
+
or any(character.isspace() or ord(character) < 32 or character == '\\' for character in spec_uri)
|
|
77
|
+
):
|
|
78
|
+
raise ValueError('Swagger spec_uri must be a same-origin absolute path')
|
|
79
|
+
if not isinstance(title, str) or not title or not isinstance(description, str):
|
|
80
|
+
raise ValueError('Swagger title must be a non-empty string and description must be a string')
|
|
81
|
+
self.title = title
|
|
82
|
+
self.description = description
|
|
83
|
+
self.spec_uri = spec_uri
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
def __str__(self) -> str:
|
|
87
|
+
return '{}(uri={}, spec={})'.format(type(self).__name__, self.path, self.spec_uri)
|
|
88
|
+
|
|
89
|
+
def startup(
|
|
90
|
+
self,
|
|
91
|
+
application: Application,
|
|
92
|
+
) -> None:
|
|
93
|
+
for route in http_routes(self, self.path):
|
|
94
|
+
application.add_extension_route(self, route)
|
|
95
|
+
super().startup(application)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
def __call__(
|
|
99
|
+
self,
|
|
100
|
+
request: Request,
|
|
101
|
+
) -> Response:
|
|
102
|
+
self.logger.debug('Swagger page preparing: request_id=%s', request.id)
|
|
103
|
+
response = page(
|
|
104
|
+
'Swagger',
|
|
105
|
+
self.title,
|
|
106
|
+
self.description,
|
|
107
|
+
parameters={'spec_url': self.spec_uri},
|
|
108
|
+
policy=(
|
|
109
|
+
"default-src 'none'; "
|
|
110
|
+
"script-src 'unsafe-inline' https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.0/; "
|
|
111
|
+
"style-src 'unsafe-inline' https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.0/; "
|
|
112
|
+
"img-src 'self' data: blob:; font-src 'self' data:; "
|
|
113
|
+
"connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
self.logger.debug('Swagger page prepared: request_id=%s', request.id)
|
|
117
|
+
return response
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from ...Extension import Extension
|
|
2
|
+
from ...Route import Route
|
|
3
|
+
from ...Runners.RunExtension import RunExtensionFactory
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
'validate_path',
|
|
7
|
+
'http_routes',
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_path(path: str) -> None:
|
|
12
|
+
if not isinstance(path, str) or not path.startswith('/') or any(
|
|
13
|
+
character in path for character in (
|
|
14
|
+
'?',
|
|
15
|
+
'#',
|
|
16
|
+
':',
|
|
17
|
+
)
|
|
18
|
+
):
|
|
19
|
+
raise ValueError('Extensions require an absolute HTTP path without parameters')
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def http_routes(
|
|
24
|
+
extension: Extension,
|
|
25
|
+
path: str,
|
|
26
|
+
) -> tuple[Route, ...]:
|
|
27
|
+
"""Create GET and HEAD request handlers for an extension."""
|
|
28
|
+
return tuple(
|
|
29
|
+
Route(
|
|
30
|
+
method,
|
|
31
|
+
path,
|
|
32
|
+
RunExtensionFactory(
|
|
33
|
+
extension,
|
|
34
|
+
method,
|
|
35
|
+
path,
|
|
36
|
+
)(),
|
|
37
|
+
)
|
|
38
|
+
for method in (
|
|
39
|
+
'GET',
|
|
40
|
+
'HEAD',
|
|
41
|
+
)
|
|
42
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from html import escape
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
from re import sub
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ...Response import Response
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
'page',
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def page(
|
|
14
|
+
name: str,
|
|
15
|
+
title: str,
|
|
16
|
+
description: str,
|
|
17
|
+
*,
|
|
18
|
+
parameters: Optional[dict[str, str]] = None,
|
|
19
|
+
policy: Optional[str] = None,
|
|
20
|
+
) -> Response:
|
|
21
|
+
values = {
|
|
22
|
+
'title': escape(title),
|
|
23
|
+
'description': escape(description),
|
|
24
|
+
}
|
|
25
|
+
if parameters is not None:
|
|
26
|
+
values.update({key: escape(value) for key, value in parameters.items()})
|
|
27
|
+
text = files(__name__.rpartition('.')[0]).joinpath(name + '.html').read_text(encoding='utf-8')
|
|
28
|
+
text = sub(
|
|
29
|
+
r'\{\{(' + '|'.join(values) + r')\}\}',
|
|
30
|
+
lambda match: values[match[1]],
|
|
31
|
+
text,
|
|
32
|
+
)
|
|
33
|
+
return Response(
|
|
34
|
+
body=text.encode('utf-8'),
|
|
35
|
+
format='text/html',
|
|
36
|
+
charset='utf-8',
|
|
37
|
+
headers={
|
|
38
|
+
'Content-Security-Policy': policy if policy is not None else "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
|
|
39
|
+
'X-Content-Type-Options': 'nosniff',
|
|
40
|
+
'Referrer-Policy': 'no-referrer',
|
|
41
|
+
'Cache-Control': 'no-store',
|
|
42
|
+
},
|
|
43
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .Status import Status
|
|
2
|
+
from .Ping import Ping
|
|
3
|
+
from .Metrics import (
|
|
4
|
+
Metrics,
|
|
5
|
+
MetricsCollector,
|
|
6
|
+
)
|
|
7
|
+
from .Specifications import Specifications
|
|
8
|
+
from .Swagger import Swagger
|
|
9
|
+
from .File import File
|
|
10
|
+
from .FileSystem import FileSystem
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'Status',
|
|
14
|
+
'Ping',
|
|
15
|
+
'Metrics',
|
|
16
|
+
'Specifications',
|
|
17
|
+
'Swagger',
|
|
18
|
+
'File',
|
|
19
|
+
'FileSystem',
|
|
20
|
+
'MetricsCollector',
|
|
21
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from ...Error import ClientError
|
|
2
|
+
from ..RequestReader import RequestReader
|
|
3
|
+
|
|
4
|
+
__all__ = (
|
|
5
|
+
'ChunkedStream',
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChunkedStream:
|
|
10
|
+
"""Decode HTTP/1.1 chunked transfer coding from raw request input."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
reader: RequestReader,
|
|
15
|
+
):
|
|
16
|
+
if not reader.chunked:
|
|
17
|
+
raise ValueError('ChunkedStream requires a chunked request body')
|
|
18
|
+
self.reader = reader
|
|
19
|
+
self.remaining = 0
|
|
20
|
+
self.finished = reader.finished
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
def next_chunk(self) -> None:
|
|
24
|
+
line = self.reader.line()
|
|
25
|
+
size = line.split(b';', 1)[0]
|
|
26
|
+
if not size or any(byte not in b'0123456789abcdefABCDEF' for byte in size):
|
|
27
|
+
raise ClientError('Invalid chunk size')
|
|
28
|
+
self.remaining = int(size, 16)
|
|
29
|
+
if self.remaining == 0:
|
|
30
|
+
while True:
|
|
31
|
+
trailer = self.reader.line()
|
|
32
|
+
if not trailer:
|
|
33
|
+
break
|
|
34
|
+
name, separator, _ = trailer.partition(b':')
|
|
35
|
+
if not separator or name.lower() in (
|
|
36
|
+
b'content-length',
|
|
37
|
+
b'transfer-encoding',
|
|
38
|
+
b'host',
|
|
39
|
+
):
|
|
40
|
+
raise ClientError('Invalid chunked trailer')
|
|
41
|
+
self.finished = True
|
|
42
|
+
self.reader.finished = True
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def take(
|
|
46
|
+
self,
|
|
47
|
+
size: int,
|
|
48
|
+
) -> bytes:
|
|
49
|
+
data = self.reader.exact(size)
|
|
50
|
+
self.remaining -= size
|
|
51
|
+
if self.remaining == 0 and self.reader.exact(2) != b'\r\n':
|
|
52
|
+
raise ClientError('Invalid chunk delimiter')
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
def read(
|
|
56
|
+
self,
|
|
57
|
+
size: int = -1,
|
|
58
|
+
) -> bytes:
|
|
59
|
+
if size < -1:
|
|
60
|
+
raise ValueError('Read size must be at least -1')
|
|
61
|
+
if self.reader.decoded:
|
|
62
|
+
data = self.reader.read(size)
|
|
63
|
+
self.finished = self.reader.finished
|
|
64
|
+
return data
|
|
65
|
+
parts = []
|
|
66
|
+
while not self.finished and (size == -1 or size > 0):
|
|
67
|
+
if self.remaining == 0:
|
|
68
|
+
self.next_chunk()
|
|
69
|
+
if self.finished:
|
|
70
|
+
break
|
|
71
|
+
part = self.take(self.remaining if size == -1 else min(size, self.remaining))
|
|
72
|
+
parts.append(part)
|
|
73
|
+
if size != -1:
|
|
74
|
+
size -= len(part)
|
|
75
|
+
return b''.join(parts)
|
|
76
|
+
|
|
77
|
+
def chunk(
|
|
78
|
+
self,
|
|
79
|
+
size: int = -1,
|
|
80
|
+
) -> bytes:
|
|
81
|
+
"""Read up to size bytes without crossing the current chunk boundary."""
|
|
82
|
+
if size < -1:
|
|
83
|
+
raise ValueError('Read size must be at least -1')
|
|
84
|
+
if self.reader.decoded:
|
|
85
|
+
return self.read(size)
|
|
86
|
+
if size == 0 or self.finished:
|
|
87
|
+
return b''
|
|
88
|
+
if self.remaining == 0:
|
|
89
|
+
self.next_chunk()
|
|
90
|
+
if self.finished:
|
|
91
|
+
return b''
|
|
92
|
+
return self.take(self.remaining if size == -1 else min(size, self.remaining))
|
|
93
|
+
|
|
94
|
+
def readline(
|
|
95
|
+
self,
|
|
96
|
+
size: int = -1,
|
|
97
|
+
) -> bytes:
|
|
98
|
+
parts = []
|
|
99
|
+
while size == -1 or len(parts) < size:
|
|
100
|
+
byte = self.read(1)
|
|
101
|
+
if not byte:
|
|
102
|
+
break
|
|
103
|
+
parts.append(byte)
|
|
104
|
+
if byte == b'\n':
|
|
105
|
+
break
|
|
106
|
+
return b''.join(parts)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
abstractmethod,
|
|
4
|
+
)
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ...Request import Request
|
|
8
|
+
from ..RequestReader import RequestReader
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
'ContentReader',
|
|
12
|
+
'TypeReader',
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContentReader(ABC):
|
|
17
|
+
"""Define body parsing and streaming input selection."""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def __call__(
|
|
21
|
+
self,
|
|
22
|
+
request: Request,
|
|
23
|
+
reader: RequestReader,
|
|
24
|
+
) -> Any:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TypeReader(ABC):
|
|
29
|
+
"""Define conversion of individual JSON leaf values."""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def __call__(
|
|
33
|
+
self,
|
|
34
|
+
value: Any,
|
|
35
|
+
) -> Any:
|
|
36
|
+
raise NotImplementedError
|