omserv 0.0.0.dev7__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 (44) hide show
  1. omserv/__about__.py +28 -0
  2. omserv/__init__.py +0 -0
  3. omserv/apps/__init__.py +0 -0
  4. omserv/apps/base.py +23 -0
  5. omserv/apps/inject.py +89 -0
  6. omserv/apps/markers.py +41 -0
  7. omserv/apps/routes.py +139 -0
  8. omserv/apps/sessions.py +57 -0
  9. omserv/apps/templates.py +90 -0
  10. omserv/dbs.py +24 -0
  11. omserv/node/__init__.py +0 -0
  12. omserv/node/models.py +53 -0
  13. omserv/node/registry.py +124 -0
  14. omserv/node/sql.py +131 -0
  15. omserv/secrets.py +12 -0
  16. omserv/server/__init__.py +18 -0
  17. omserv/server/config.py +51 -0
  18. omserv/server/debug.py +14 -0
  19. omserv/server/events.py +83 -0
  20. omserv/server/headers.py +36 -0
  21. omserv/server/lifespans.py +132 -0
  22. omserv/server/multiprocess.py +157 -0
  23. omserv/server/protocols/__init__.py +1 -0
  24. omserv/server/protocols/h11.py +334 -0
  25. omserv/server/protocols/h2.py +407 -0
  26. omserv/server/protocols/protocols.py +91 -0
  27. omserv/server/protocols/types.py +18 -0
  28. omserv/server/resources/__init__.py +8 -0
  29. omserv/server/sockets.py +111 -0
  30. omserv/server/ssl.py +47 -0
  31. omserv/server/streams/__init__.py +0 -0
  32. omserv/server/streams/httpstream.py +237 -0
  33. omserv/server/streams/utils.py +53 -0
  34. omserv/server/streams/wsstream.py +447 -0
  35. omserv/server/taskspawner.py +111 -0
  36. omserv/server/tcpserver.py +173 -0
  37. omserv/server/types.py +94 -0
  38. omserv/server/workercontext.py +52 -0
  39. omserv/server/workers.py +193 -0
  40. omserv-0.0.0.dev7.dist-info/LICENSE +21 -0
  41. omserv-0.0.0.dev7.dist-info/METADATA +21 -0
  42. omserv-0.0.0.dev7.dist-info/RECORD +44 -0
  43. omserv-0.0.0.dev7.dist-info/WHEEL +5 -0
  44. omserv-0.0.0.dev7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,237 @@
1
+ import enum
2
+ import logging
3
+ import time
4
+ import typing as ta
5
+ import urllib.parse
6
+
7
+ from ..config import Config
8
+ from ..events import Body
9
+ from ..events import EndBody
10
+ from ..events import InformationalResponse
11
+ from ..events import ProtocolEvent
12
+ from ..events import Request
13
+ from ..events import Response
14
+ from ..events import StreamClosed
15
+ from ..taskspawner import TaskSpawner
16
+ from ..types import AppWrapper
17
+ from ..types import AsgiSendEvent
18
+ from ..types import HttpResponseStartEvent
19
+ from ..types import HttpScope
20
+ from ..types import UnexpectedMessageError
21
+ from ..workercontext import WorkerContext
22
+ from .utils import build_and_validate_headers
23
+ from .utils import log_access
24
+ from .utils import suppress_body
25
+ from .utils import valid_server_name
26
+
27
+
28
+ log = logging.getLogger(__name__)
29
+
30
+
31
+ PUSH_VERSIONS = {'2', '3'}
32
+ EARLY_HINTS_VERSIONS = {'2', '3'}
33
+
34
+
35
+ class AsgiHttpState(enum.Enum):
36
+ # The Asgi Spec is clear that a response should not start till the framework has sent at least one body message
37
+ # hence why this state tracking is required.
38
+ REQUEST = enum.auto()
39
+ RESPONSE = enum.auto()
40
+ CLOSED = enum.auto()
41
+
42
+
43
+ class HttpStream:
44
+ def __init__(
45
+ self,
46
+ app: AppWrapper,
47
+ config: Config,
48
+ context: WorkerContext,
49
+ task_spawner: TaskSpawner,
50
+ client: tuple[str, int] | None,
51
+ server: tuple[str, int] | None,
52
+ send: ta.Callable[[ProtocolEvent], ta.Awaitable[None]],
53
+ stream_id: int,
54
+ ) -> None:
55
+ super().__init__()
56
+ self.app = app
57
+ self.client = client
58
+ self.closed = False
59
+ self.config = config
60
+ self.context = context
61
+ self.response: HttpResponseStartEvent
62
+ self.scope: HttpScope
63
+ self.send = send
64
+ self.scheme = 'http'
65
+ self.server = server
66
+ self.start_time: float
67
+ self.state = AsgiHttpState.REQUEST
68
+ self.stream_id = stream_id
69
+ self.task_spawner = task_spawner
70
+
71
+ @property
72
+ def idle(self) -> bool:
73
+ return False
74
+
75
+ async def handle(self, event: ProtocolEvent) -> None:
76
+ if self.closed:
77
+ return
78
+
79
+ elif isinstance(event, Request):
80
+ self.start_time = time.time()
81
+
82
+ path, _, query_string = event.raw_path.partition(b'?')
83
+
84
+ self.scope = {
85
+ 'type': 'http',
86
+ 'http_version': event.http_version,
87
+ 'asgi': {'spec_version': '2.1', 'version': '3.0'},
88
+ 'method': event.method,
89
+ 'scheme': self.scheme,
90
+ 'path': urllib.parse.unquote(path.decode('ascii')),
91
+ 'raw_path': path,
92
+ 'query_string': query_string,
93
+ 'headers': event.headers,
94
+ 'client': self.client,
95
+ 'server': self.server,
96
+ 'extensions': {},
97
+ }
98
+
99
+ if event.http_version in PUSH_VERSIONS:
100
+ self.scope['extensions']['http.response.push'] = {}
101
+
102
+ if event.http_version in EARLY_HINTS_VERSIONS:
103
+ self.scope['extensions']['http.response.early_hint'] = {}
104
+
105
+ if valid_server_name(self.config, event):
106
+ self.app_put = await self.task_spawner.spawn_app(self.app, self.config, self.scope, self.app_send)
107
+
108
+ else:
109
+ await self._send_error_response(404)
110
+ self.closed = True
111
+
112
+ elif isinstance(event, Body):
113
+ await self.app_put({'type': 'http.request', 'body': bytes(event.data), 'more_body': True})
114
+
115
+ elif isinstance(event, EndBody):
116
+ await self.app_put({'type': 'http.request', 'body': b'', 'more_body': False})
117
+
118
+ elif isinstance(event, StreamClosed):
119
+ self.closed = True
120
+
121
+ if self.state != AsgiHttpState.CLOSED:
122
+ log_access(self.config, self.scope, None, time.time() - self.start_time)
123
+
124
+ if self.app_put is not None:
125
+ await self.app_put({'type': 'http.disconnect'})
126
+
127
+ async def app_send(self, message: AsgiSendEvent | None) -> None:
128
+ if message is None: # Asgi App has finished sending messages
129
+ if not self.closed:
130
+ # Cleanup if required
131
+ if self.state == AsgiHttpState.REQUEST:
132
+ await self._send_error_response(500)
133
+
134
+ await self.send(StreamClosed(stream_id=self.stream_id))
135
+
136
+ elif message['type'] == 'http.response.start' and self.state == AsgiHttpState.REQUEST:
137
+ self.response = message
138
+
139
+ elif (
140
+ message['type'] == 'http.response.push' and
141
+ self.scope['http_version'] in PUSH_VERSIONS
142
+ ):
143
+ if not isinstance(message['path'], str):
144
+ raise TypeError(f'{message["path"]} should be a str')
145
+
146
+ headers = [(b':scheme', self.scope['scheme'].encode())]
147
+ for name, value in self.scope['headers']:
148
+ if name == b'host':
149
+ headers.append((b':authority', value))
150
+
151
+ headers.extend(build_and_validate_headers(message['headers']))
152
+
153
+ await self.send(Request(
154
+ stream_id=self.stream_id,
155
+ headers=headers,
156
+ http_version=self.scope['http_version'],
157
+ method='GET',
158
+ raw_path=message['path'].encode(),
159
+ ))
160
+
161
+ elif (
162
+ message['type'] == 'http.response.early_hint'
163
+ and self.scope['http_version'] in EARLY_HINTS_VERSIONS
164
+ and self.state == AsgiHttpState.REQUEST
165
+ ):
166
+ headers = [(b'link', bytes(link).strip()) for link in message['links']]
167
+
168
+ await self.send(
169
+ InformationalResponse(
170
+ stream_id=self.stream_id,
171
+ headers=headers,
172
+ status_code=103,
173
+ ),
174
+ )
175
+
176
+ elif message['type'] == 'http.response.body' and self.state in {
177
+ AsgiHttpState.REQUEST,
178
+ AsgiHttpState.RESPONSE,
179
+ }:
180
+ if self.state == AsgiHttpState.REQUEST:
181
+ headers = build_and_validate_headers(self.response.get('headers', []))
182
+
183
+ await self.send(Response(
184
+ stream_id=self.stream_id,
185
+ headers=headers,
186
+ status_code=int(self.response['status']),
187
+ ))
188
+
189
+ self.state = AsgiHttpState.RESPONSE
190
+
191
+ if (
192
+ not suppress_body(self.scope['method'], int(self.response['status']))
193
+ and message.get('body', b'') != b''
194
+ ):
195
+ await self.send(Body(stream_id=self.stream_id, data=bytes(message.get('body', b''))))
196
+
197
+ if not message.get('more_body', False):
198
+ if self.state != AsgiHttpState.CLOSED:
199
+ self.state = AsgiHttpState.CLOSED
200
+
201
+ log_access(
202
+ self.config,
203
+ self.scope,
204
+ self.response, # type: ignore # FIXME # noqa
205
+ time.time() - self.start_time,
206
+ )
207
+
208
+ await self.send(EndBody(stream_id=self.stream_id))
209
+
210
+ await self.send(StreamClosed(stream_id=self.stream_id))
211
+
212
+ else:
213
+ raise UnexpectedMessageError(self.state, message['type'])
214
+
215
+ async def _send_error_response(self, status_code: int) -> None:
216
+ await self.send(Response(
217
+ stream_id=self.stream_id,
218
+ headers=[
219
+ (b'content-length', b'0'),
220
+ (b'connection', b'close'),
221
+ ],
222
+ status_code=status_code,
223
+ ))
224
+
225
+ await self.send(EndBody(stream_id=self.stream_id))
226
+
227
+ self.state = AsgiHttpState.CLOSED
228
+
229
+ log_access(
230
+ self.config,
231
+ self.scope,
232
+ {
233
+ 'status': status_code,
234
+ 'headers': [],
235
+ },
236
+ time.time() - self.start_time,
237
+ )
@@ -0,0 +1,53 @@
1
+ import logging
2
+ import typing as ta
3
+
4
+ from ..config import Config
5
+ from ..events import Request
6
+ from ..types import Scope
7
+
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ def valid_server_name(config: Config, request: Request) -> bool:
13
+ if not config.server_names:
14
+ return True
15
+
16
+ host = ''
17
+ for name, value in request.headers:
18
+ if name.lower() == b'host':
19
+ host = value.decode()
20
+ break
21
+ return host in config.server_names
22
+
23
+
24
+ def build_and_validate_headers(headers: ta.Iterable[tuple[bytes, bytes]]) -> list[tuple[bytes, bytes]]:
25
+ # Validates that the header name and value are bytes
26
+ validated_headers: list[tuple[bytes, bytes]] = []
27
+ for name, value in headers:
28
+ if name[0] == b':'[0]:
29
+ raise ValueError('Pseudo headers are not valid')
30
+ validated_headers.append((bytes(name).strip(), bytes(value).strip()))
31
+ return validated_headers
32
+
33
+
34
+ def suppress_body(method: str, status_code: int) -> bool:
35
+ return method == 'HEAD' or 100 <= status_code < 200 or status_code in {204, 304}
36
+
37
+
38
+ class ResponseSummary(ta.TypedDict):
39
+ status: int
40
+ headers: ta.Iterable[tuple[bytes, bytes]]
41
+
42
+
43
+ def log_access(
44
+ config: Config,
45
+ request: 'Scope',
46
+ response: ta.Optional['ResponseSummary'],
47
+ request_time: float,
48
+ ) -> None:
49
+ # if self.access_logger is not None:
50
+ # self.access_logger.info(
51
+ # self.access_log_format, self.atoms(request, response, request_time)
52
+ # )
53
+ log.info('access: %r %r', request, response)