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.
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 +510 -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 +241 -0
  45. DefEssential/IO/WebSocket/WebSocket.py +286 -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.7.dist-info/METADATA +41 -0
  101. defessential-0.1.7.dist-info/RECORD +104 -0
  102. defessential-0.1.7.dist-info/WHEEL +6 -0
  103. defessential-0.1.7.dist-info/licenses/LICENSE +674 -0
  104. 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