DefEssential 0.1.7__21-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 +510 -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 +241 -0
- DefEssential/IO/WebSocket/WebSocket.py +286 -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.7.dist-info/METADATA +41 -0
- defessential-0.1.7.dist-info/RECORD +104 -0
- defessential-0.1.7.dist-info/WHEEL +6 -0
- defessential-0.1.7.dist-info/licenses/LICENSE +674 -0
- defessential-0.1.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
Iterator,
|
|
9
|
+
KeysView,
|
|
10
|
+
ItemsView,
|
|
11
|
+
ValuesView,
|
|
12
|
+
Optional,
|
|
13
|
+
Dict,
|
|
14
|
+
Union,
|
|
15
|
+
Iterable,
|
|
16
|
+
Tuple,
|
|
17
|
+
)
|
|
18
|
+
from abc import (
|
|
19
|
+
ABCMeta,
|
|
20
|
+
ABC,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = (
|
|
24
|
+
'Type',
|
|
25
|
+
'Value',
|
|
26
|
+
'Condition',
|
|
27
|
+
'Schema',
|
|
28
|
+
'OneOf',
|
|
29
|
+
'AllOf',
|
|
30
|
+
'TypeMapper',
|
|
31
|
+
'TypeEncoder',
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Descriptor(Mapping[str, Any]):
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
**kwargs: Any,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.__properties__: Dict[str, Any] = kwargs
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
def __getitem__(
|
|
44
|
+
self,
|
|
45
|
+
key: str,
|
|
46
|
+
) -> Any:
|
|
47
|
+
return self.__properties__.__getitem__(key)
|
|
48
|
+
|
|
49
|
+
def __setitem__(
|
|
50
|
+
self,
|
|
51
|
+
key: str,
|
|
52
|
+
value: Any,
|
|
53
|
+
) -> None:
|
|
54
|
+
return self.__properties__.__setitem__(key, value)
|
|
55
|
+
|
|
56
|
+
def __delitem__(
|
|
57
|
+
self,
|
|
58
|
+
key: str,
|
|
59
|
+
) -> None:
|
|
60
|
+
return self.__properties__.__delitem__(key)
|
|
61
|
+
|
|
62
|
+
def __iter__(self) -> Iterator[str]:
|
|
63
|
+
return self.__properties__.__iter__()
|
|
64
|
+
|
|
65
|
+
def __len__(self) -> int:
|
|
66
|
+
return self.__properties__.__len__()
|
|
67
|
+
|
|
68
|
+
def __contains__(
|
|
69
|
+
self,
|
|
70
|
+
key: object,
|
|
71
|
+
) -> bool:
|
|
72
|
+
return self.__properties__.__contains__(key)
|
|
73
|
+
|
|
74
|
+
def keys(self) -> KeysView[str]:
|
|
75
|
+
return self.__properties__.keys()
|
|
76
|
+
|
|
77
|
+
def items(self) -> ItemsView[str, Any]:
|
|
78
|
+
return self.__properties__.items()
|
|
79
|
+
|
|
80
|
+
def values(self) -> ValuesView[Any]:
|
|
81
|
+
return self.__properties__.values()
|
|
82
|
+
|
|
83
|
+
def __eq__(
|
|
84
|
+
self,
|
|
85
|
+
other: object,
|
|
86
|
+
) -> bool:
|
|
87
|
+
return self.__properties__.__eq__(other)
|
|
88
|
+
|
|
89
|
+
def __ne__(
|
|
90
|
+
self,
|
|
91
|
+
value: object,
|
|
92
|
+
) -> bool:
|
|
93
|
+
return self.__properties__.__ne__(value)
|
|
94
|
+
|
|
95
|
+
def get(
|
|
96
|
+
self,
|
|
97
|
+
key: str,
|
|
98
|
+
default: Any = None,
|
|
99
|
+
) -> Any:
|
|
100
|
+
return self.__properties__.get(key, default)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Type(str, Enum):
|
|
104
|
+
Boolean = 'boolean'
|
|
105
|
+
Integer = 'integer'
|
|
106
|
+
Number = 'number'
|
|
107
|
+
String = 'string'
|
|
108
|
+
Array = 'array'
|
|
109
|
+
Object = 'object'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Value(Descriptor):
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
type: Type,
|
|
116
|
+
description: Optional[str] = None,
|
|
117
|
+
format: Optional[str] = None,
|
|
118
|
+
min: Any = None,
|
|
119
|
+
max: Any = None,
|
|
120
|
+
default: Any = None,
|
|
121
|
+
enum: Optional[Iterable[Any]] = None,
|
|
122
|
+
required: bool = True,
|
|
123
|
+
) -> None:
|
|
124
|
+
super().__init__(type=type)
|
|
125
|
+
if description: self['description'] = description
|
|
126
|
+
if format: self['format'] = format
|
|
127
|
+
if min is not None: self['minimum'] = min
|
|
128
|
+
if max is not None: self['maximum'] = max
|
|
129
|
+
if not required: self['default'] = default
|
|
130
|
+
if enum: self['enum'] = enum
|
|
131
|
+
self.required = required
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Condition(ABC):
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
*args: Union[Descriptor, 'Condition', 'Schema'],
|
|
139
|
+
) -> None:
|
|
140
|
+
self.args = args
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Schema(object):
|
|
145
|
+
"""Wrap a Descriptor node, condition, or JSON Schema mapping with a name."""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
name: str,
|
|
150
|
+
format: Optional[Union[Mapping[str, Any], Condition, 'Schema']] = None,
|
|
151
|
+
) -> None:
|
|
152
|
+
self.name = name
|
|
153
|
+
self.format = format
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class OneOf(Condition):
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
*args: Union[Descriptor, Condition, Schema],
|
|
161
|
+
discriminator: Optional[str] = None,
|
|
162
|
+
required: bool = True,
|
|
163
|
+
) -> None:
|
|
164
|
+
super().__init__(*args)
|
|
165
|
+
self.discriminator = discriminator
|
|
166
|
+
self.required = required
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class AllOf(Condition): pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class TypeMapper(metaclass=ABCMeta):
|
|
174
|
+
"""Convert a user-defined type to a Value subclass and constructor arguments."""
|
|
175
|
+
|
|
176
|
+
def __call__(
|
|
177
|
+
self,
|
|
178
|
+
o: type,
|
|
179
|
+
) -> Tuple[type[Value], Dict[str, Any]]:
|
|
180
|
+
raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class TypeEncoder(metaclass=ABCMeta):
|
|
184
|
+
def __call__(
|
|
185
|
+
self,
|
|
186
|
+
o: Any,
|
|
187
|
+
) -> Any:
|
|
188
|
+
raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from collections.abc import (
|
|
2
|
+
Mapping,
|
|
3
|
+
Sequence,
|
|
4
|
+
)
|
|
5
|
+
from dataclasses import is_dataclass
|
|
6
|
+
from json import (
|
|
7
|
+
dumps,
|
|
8
|
+
loads,
|
|
9
|
+
)
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
Optional,
|
|
13
|
+
Type,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .Value import (
|
|
17
|
+
Value,
|
|
18
|
+
Condition,
|
|
19
|
+
Schema,
|
|
20
|
+
OneOf,
|
|
21
|
+
AllOf,
|
|
22
|
+
TypeMapper,
|
|
23
|
+
TypeEncoder,
|
|
24
|
+
)
|
|
25
|
+
from .Types import (
|
|
26
|
+
Boolean,
|
|
27
|
+
IntegerFormat,
|
|
28
|
+
Integer,
|
|
29
|
+
NumberFormat,
|
|
30
|
+
Number,
|
|
31
|
+
StringFormat,
|
|
32
|
+
String,
|
|
33
|
+
Binary,
|
|
34
|
+
BinaryBase64Encoded,
|
|
35
|
+
Array,
|
|
36
|
+
Object,
|
|
37
|
+
Properties,
|
|
38
|
+
)
|
|
39
|
+
from .DataObject import (
|
|
40
|
+
DataObjectToSchema,
|
|
41
|
+
DataObjectToObject,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = (
|
|
45
|
+
'Value',
|
|
46
|
+
'Condition',
|
|
47
|
+
'Schema',
|
|
48
|
+
'OneOf',
|
|
49
|
+
'AllOf',
|
|
50
|
+
'TypeMapper',
|
|
51
|
+
'TypeEncoder',
|
|
52
|
+
'Boolean',
|
|
53
|
+
'IntegerFormat',
|
|
54
|
+
'Integer',
|
|
55
|
+
'NumberFormat',
|
|
56
|
+
'Number',
|
|
57
|
+
'StringFormat',
|
|
58
|
+
'String',
|
|
59
|
+
'Binary',
|
|
60
|
+
'BinaryBase64Encoded',
|
|
61
|
+
'Array',
|
|
62
|
+
'Object',
|
|
63
|
+
'Properties',
|
|
64
|
+
'ToSchema',
|
|
65
|
+
'DumpSchema',
|
|
66
|
+
'ToObject',
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def schema_value(
|
|
71
|
+
value: Any,
|
|
72
|
+
) -> Any:
|
|
73
|
+
if isinstance(
|
|
74
|
+
value,
|
|
75
|
+
Schema,
|
|
76
|
+
):
|
|
77
|
+
return schema_value(value.format)
|
|
78
|
+
if isinstance(
|
|
79
|
+
value,
|
|
80
|
+
Condition,
|
|
81
|
+
):
|
|
82
|
+
if isinstance(
|
|
83
|
+
value,
|
|
84
|
+
OneOf,
|
|
85
|
+
):
|
|
86
|
+
key = 'oneOf'
|
|
87
|
+
elif isinstance(
|
|
88
|
+
value,
|
|
89
|
+
AllOf,
|
|
90
|
+
):
|
|
91
|
+
key = 'allOf'
|
|
92
|
+
else:
|
|
93
|
+
raise TypeError('Unsupported schema condition: ' + type(value).__name__)
|
|
94
|
+
return {key: [schema_value(arg) for arg in value.args]}
|
|
95
|
+
if isinstance(
|
|
96
|
+
value,
|
|
97
|
+
Mapping,
|
|
98
|
+
):
|
|
99
|
+
return {
|
|
100
|
+
key: schema_value(item) for key, item in value.items()
|
|
101
|
+
}
|
|
102
|
+
if isinstance(
|
|
103
|
+
value,
|
|
104
|
+
Sequence,
|
|
105
|
+
) and not isinstance(
|
|
106
|
+
value,
|
|
107
|
+
(
|
|
108
|
+
str,
|
|
109
|
+
bytes,
|
|
110
|
+
bytearray,
|
|
111
|
+
),
|
|
112
|
+
):
|
|
113
|
+
return [schema_value(item) for item in value]
|
|
114
|
+
if value is None or isinstance(
|
|
115
|
+
value,
|
|
116
|
+
(
|
|
117
|
+
str,
|
|
118
|
+
int,
|
|
119
|
+
float,
|
|
120
|
+
bool,
|
|
121
|
+
),
|
|
122
|
+
):
|
|
123
|
+
return value
|
|
124
|
+
raise TypeError('Unsupported schema value: ' + type(value).__name__)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def DumpSchema(
|
|
128
|
+
o: Schema,
|
|
129
|
+
) -> dict:
|
|
130
|
+
if not isinstance(
|
|
131
|
+
o,
|
|
132
|
+
Schema,
|
|
133
|
+
):
|
|
134
|
+
raise TypeError('A Schema is required')
|
|
135
|
+
result = schema_value(o)
|
|
136
|
+
if not isinstance(
|
|
137
|
+
result,
|
|
138
|
+
dict,
|
|
139
|
+
):
|
|
140
|
+
raise TypeError('A schema must describe an object, value or condition')
|
|
141
|
+
return loads(dumps(
|
|
142
|
+
result,
|
|
143
|
+
allow_nan=False,
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def ToSchema(
|
|
148
|
+
o: Type[object],
|
|
149
|
+
description: Optional[str] = None,
|
|
150
|
+
typedef: Optional[TypeMapper] = None,
|
|
151
|
+
) -> Schema:
|
|
152
|
+
if not isinstance(
|
|
153
|
+
o,
|
|
154
|
+
type,
|
|
155
|
+
) or not is_dataclass(o):
|
|
156
|
+
raise TypeError('A dataclass type is required')
|
|
157
|
+
return DataObjectToSchema(mapper=typedef)(
|
|
158
|
+
o,
|
|
159
|
+
description,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def ToObject(
|
|
164
|
+
o: object,
|
|
165
|
+
typeenc: Optional[TypeEncoder] = None,
|
|
166
|
+
) -> dict:
|
|
167
|
+
if isinstance(
|
|
168
|
+
o,
|
|
169
|
+
type,
|
|
170
|
+
) or not is_dataclass(o):
|
|
171
|
+
raise TypeError('A dataclass instance is required')
|
|
172
|
+
return DataObjectToObject(encoder=typeenc)(o)
|
DefEssential/Error.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import (
|
|
3
|
+
Mapping,
|
|
4
|
+
Optional,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
'Error',
|
|
9
|
+
'ClientError',
|
|
10
|
+
'ServerError',
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Error(Exception):
|
|
15
|
+
"""Represent an error to be returned as an HTTP response."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
status: int,
|
|
20
|
+
reason: str,
|
|
21
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
22
|
+
):
|
|
23
|
+
super().__init__(reason)
|
|
24
|
+
self.status = status
|
|
25
|
+
self.message = HTTPStatus(status).phrase
|
|
26
|
+
self.reason = reason
|
|
27
|
+
self.headers = dict(headers) if headers is not None else {}
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClientError(Error):
|
|
32
|
+
"""Represent an invalid client request."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
reason: str,
|
|
37
|
+
status: int = 400,
|
|
38
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
39
|
+
):
|
|
40
|
+
if not 400 <= status < 500:
|
|
41
|
+
raise ValueError('ClientError status code must be in the 4xx range')
|
|
42
|
+
super().__init__(
|
|
43
|
+
status,
|
|
44
|
+
reason,
|
|
45
|
+
headers,
|
|
46
|
+
)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ServerError(Error):
|
|
51
|
+
"""Represent a server-side processing failure."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
reason: str,
|
|
56
|
+
status: int = 500,
|
|
57
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
58
|
+
):
|
|
59
|
+
if not 500 <= status < 600:
|
|
60
|
+
raise ValueError('ServerError status code must be in the 5xx range')
|
|
61
|
+
super().__init__(
|
|
62
|
+
status,
|
|
63
|
+
reason,
|
|
64
|
+
headers,
|
|
65
|
+
)
|
|
66
|
+
return
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from abc import (
|
|
2
|
+
ABC,
|
|
3
|
+
abstractmethod,
|
|
4
|
+
)
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import (
|
|
7
|
+
Optional,
|
|
8
|
+
Sequence,
|
|
9
|
+
Type,
|
|
10
|
+
TypeVar,
|
|
11
|
+
TYPE_CHECKING,
|
|
12
|
+
Union,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .Request import Request
|
|
16
|
+
from .Response import Response
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .Application import Application
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
'Extension',
|
|
23
|
+
'ExtensionStartup',
|
|
24
|
+
'ExtensionCleanup',
|
|
25
|
+
'ExtensionRequest',
|
|
26
|
+
'ExtensionRequestResponse',
|
|
27
|
+
'ExtensionRequestError',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ExtensionStartup(ABC):
|
|
32
|
+
"""확장 시작 훅의 호출 계약."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def __call__(self) -> None:
|
|
36
|
+
raise NotImplementedError
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ExtensionCleanup(ABC):
|
|
40
|
+
"""확장 정리 훅의 호출 계약."""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def __call__(self) -> None:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExtensionRequest(ABC):
|
|
48
|
+
"""확장 요청 진입 훅의 호출 계약."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def __call__(
|
|
52
|
+
self,
|
|
53
|
+
request: Request,
|
|
54
|
+
) -> Request:
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ExtensionRequestResponse(ABC):
|
|
59
|
+
"""확장 응답 반환 훅의 호출 계약."""
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def __call__(
|
|
63
|
+
self,
|
|
64
|
+
request: Request,
|
|
65
|
+
response: Optional[Response],
|
|
66
|
+
) -> Optional[Response]:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ExtensionRequestError(ABC):
|
|
71
|
+
"""확장 오류 관찰 훅의 호출 계약."""
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def __call__(
|
|
75
|
+
self,
|
|
76
|
+
request: Request,
|
|
77
|
+
error: Exception,
|
|
78
|
+
) -> None:
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Hook = TypeVar('Hook')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def extension_hooks(
|
|
86
|
+
value: object,
|
|
87
|
+
contract: Type[Hook],
|
|
88
|
+
name: str,
|
|
89
|
+
) -> tuple[Hook, ...]:
|
|
90
|
+
if value is None:
|
|
91
|
+
return ()
|
|
92
|
+
if isinstance(value, contract):
|
|
93
|
+
return (value,)
|
|
94
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
|
95
|
+
result = tuple(value)
|
|
96
|
+
if all(isinstance(hook, contract) for hook in result):
|
|
97
|
+
return result
|
|
98
|
+
raise TypeError(name + ' must be a ' + contract.__name__ + ' instance or a sequence of them')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Extension(ABC):
|
|
102
|
+
"""인스턴스별 사용자 훅과 시작·요청·정리 계약을 제공한다."""
|
|
103
|
+
|
|
104
|
+
logger = getLogger('DefEssential.Extension')
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
on_startup: Union[
|
|
110
|
+
ExtensionStartup,
|
|
111
|
+
Sequence[ExtensionStartup],
|
|
112
|
+
None,
|
|
113
|
+
] = None,
|
|
114
|
+
on_cleanup: Union[
|
|
115
|
+
ExtensionCleanup,
|
|
116
|
+
Sequence[ExtensionCleanup],
|
|
117
|
+
None,
|
|
118
|
+
] = None,
|
|
119
|
+
on_request: Union[
|
|
120
|
+
ExtensionRequest,
|
|
121
|
+
Sequence[ExtensionRequest],
|
|
122
|
+
None,
|
|
123
|
+
] = None,
|
|
124
|
+
on_request_response: Union[
|
|
125
|
+
ExtensionRequestResponse,
|
|
126
|
+
Sequence[ExtensionRequestResponse],
|
|
127
|
+
None,
|
|
128
|
+
] = None,
|
|
129
|
+
on_request_error: Union[
|
|
130
|
+
ExtensionRequestError,
|
|
131
|
+
Sequence[ExtensionRequestError],
|
|
132
|
+
None,
|
|
133
|
+
] = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
self.startup_hooks = extension_hooks(
|
|
136
|
+
on_startup,
|
|
137
|
+
ExtensionStartup,
|
|
138
|
+
'on_startup',
|
|
139
|
+
)
|
|
140
|
+
self.cleanup_hooks = extension_hooks(
|
|
141
|
+
on_cleanup,
|
|
142
|
+
ExtensionCleanup,
|
|
143
|
+
'on_cleanup',
|
|
144
|
+
)
|
|
145
|
+
self.request_hooks = extension_hooks(
|
|
146
|
+
on_request,
|
|
147
|
+
ExtensionRequest,
|
|
148
|
+
'on_request',
|
|
149
|
+
)
|
|
150
|
+
self.request_response_hooks = extension_hooks(
|
|
151
|
+
on_request_response,
|
|
152
|
+
ExtensionRequestResponse,
|
|
153
|
+
'on_request_response',
|
|
154
|
+
)
|
|
155
|
+
self.request_error_hooks = extension_hooks(
|
|
156
|
+
on_request_error,
|
|
157
|
+
ExtensionRequestError,
|
|
158
|
+
'on_request_error',
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
def __str__(self) -> str:
|
|
163
|
+
return type(self).__name__
|
|
164
|
+
|
|
165
|
+
def startup(
|
|
166
|
+
self,
|
|
167
|
+
application: 'Application',
|
|
168
|
+
) -> None:
|
|
169
|
+
"""구현체 준비 후 호출하여 사용자 시작 훅을 실행한다."""
|
|
170
|
+
for index, hook in enumerate(self.startup_hooks):
|
|
171
|
+
self.logger.debug('Extension startup hook starting: index=%s hook=%s', index, type(hook).__qualname__)
|
|
172
|
+
if hook() is not None:
|
|
173
|
+
raise TypeError('ExtensionStartup hooks must return None')
|
|
174
|
+
self.logger.debug('Extension startup hook completed: index=%s', index)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def cleanup(self) -> None:
|
|
178
|
+
"""사용자 정리 훅을 실행한다. 구현체는 별도로 자원 회수를 보장한다."""
|
|
179
|
+
for index, hook in enumerate(self.cleanup_hooks):
|
|
180
|
+
self.logger.debug('Extension cleanup hook starting: index=%s hook=%s', index, type(hook).__qualname__)
|
|
181
|
+
if hook() is not None:
|
|
182
|
+
raise TypeError('ExtensionCleanup hooks must return None')
|
|
183
|
+
self.logger.debug('Extension cleanup hook completed: index=%s', index)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
@abstractmethod
|
|
187
|
+
def __call__(
|
|
188
|
+
self,
|
|
189
|
+
request: Request,
|
|
190
|
+
) -> Optional[Response]:
|
|
191
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import (
|
|
4
|
+
Optional,
|
|
5
|
+
Sequence,
|
|
6
|
+
Union,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ..Application import Application
|
|
10
|
+
from ..Extension import (
|
|
11
|
+
Extension,
|
|
12
|
+
ExtensionCleanup,
|
|
13
|
+
ExtensionRequest,
|
|
14
|
+
ExtensionRequestError,
|
|
15
|
+
ExtensionRequestResponse,
|
|
16
|
+
ExtensionStartup,
|
|
17
|
+
)
|
|
18
|
+
from ..Request import Request
|
|
19
|
+
from ..Responses import ResponseFile
|
|
20
|
+
from .Utils.Routes import (
|
|
21
|
+
http_routes,
|
|
22
|
+
validate_path,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
'File',
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class File(Extension):
|
|
31
|
+
"""고정 HTTP 경로에 파일과 인스턴스별 사용자 훅을 연결한다."""
|
|
32
|
+
|
|
33
|
+
logger = getLogger('DefEssential.Extensions.File')
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
path: str,
|
|
38
|
+
file: Union[
|
|
39
|
+
str,
|
|
40
|
+
Path,
|
|
41
|
+
],
|
|
42
|
+
*,
|
|
43
|
+
on_startup: Optional[Union[
|
|
44
|
+
ExtensionStartup,
|
|
45
|
+
Sequence[ExtensionStartup],
|
|
46
|
+
]] = None,
|
|
47
|
+
on_cleanup: Optional[Union[
|
|
48
|
+
ExtensionCleanup,
|
|
49
|
+
Sequence[ExtensionCleanup],
|
|
50
|
+
]] = None,
|
|
51
|
+
on_request: Optional[Union[
|
|
52
|
+
ExtensionRequest,
|
|
53
|
+
Sequence[ExtensionRequest],
|
|
54
|
+
]] = None,
|
|
55
|
+
on_request_response: Optional[Union[
|
|
56
|
+
ExtensionRequestResponse,
|
|
57
|
+
Sequence[ExtensionRequestResponse],
|
|
58
|
+
]] = None,
|
|
59
|
+
on_request_error: Optional[Union[
|
|
60
|
+
ExtensionRequestError,
|
|
61
|
+
Sequence[ExtensionRequestError],
|
|
62
|
+
]] = None,
|
|
63
|
+
):
|
|
64
|
+
super().__init__(
|
|
65
|
+
on_startup=on_startup,
|
|
66
|
+
on_cleanup=on_cleanup,
|
|
67
|
+
on_request=on_request,
|
|
68
|
+
on_request_response=on_request_response,
|
|
69
|
+
on_request_error=on_request_error,
|
|
70
|
+
)
|
|
71
|
+
validate_path(path)
|
|
72
|
+
self.path = path
|
|
73
|
+
self.file = Path(file).resolve()
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return '{}(uri={}, path={})'.format(type(self).__name__, self.path, self.file)
|
|
78
|
+
|
|
79
|
+
def startup(
|
|
80
|
+
self,
|
|
81
|
+
application: Application,
|
|
82
|
+
) -> None:
|
|
83
|
+
for route in http_routes(self, self.path):
|
|
84
|
+
application.add_extension_route(self, route)
|
|
85
|
+
super().startup(application)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def __call__(
|
|
89
|
+
self,
|
|
90
|
+
request: Request,
|
|
91
|
+
) -> ResponseFile:
|
|
92
|
+
self.logger.debug('File response preparing: request_id=%s', request.id)
|
|
93
|
+
response = ResponseFile(self.file)
|
|
94
|
+
self.logger.debug('File response prepared: request_id=%s', request.id)
|
|
95
|
+
return response
|