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,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