omlish 0.0.0.dev123__py3-none-any.whl → 0.0.0.dev125__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.
@@ -0,0 +1,585 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
3
+ # --------------------------------------------
4
+ #
5
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
6
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
7
+ # documentation.
8
+ #
9
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
10
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
11
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
12
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
13
+ # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
14
+ # alone or in any derivative version prepared by Licensee.
15
+ #
16
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
17
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
18
+ # any such work a brief summary of the changes made to Python.
19
+ #
20
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
21
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
22
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
23
+ # RIGHTS.
24
+ #
25
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
26
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
27
+ # ADVISED OF THE POSSIBILITY THEREOF.
28
+ #
29
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
30
+ #
31
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
32
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
33
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
34
+ #
35
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
36
+ # License Agreement.
37
+ """
38
+ "Test suite" lol:
39
+
40
+ curl -v localhost:8000
41
+ curl -v localhost:8000 -d 'foo'
42
+ curl -v -XFOO localhost:8000 -d 'foo'
43
+ curl -v -XPOST -H 'Expect: 100-Continue' localhost:8000 -d 'foo'
44
+
45
+ curl -v -0 localhost:8000
46
+ curl -v -0 localhost:8000 -d 'foo'
47
+ curl -v -0 -XFOO localhost:8000 -d 'foo'
48
+
49
+ curl -v -XPOST localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
50
+ curl -v -XPOST localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
51
+ curl -v -XFOO localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
52
+ curl -v -XFOO localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
53
+ """
54
+ import abc
55
+ import dataclasses as dc
56
+ import email.utils
57
+ import html
58
+ import http.client
59
+ import http.server
60
+ import io
61
+ import textwrap
62
+ import time
63
+ import typing as ta
64
+
65
+ from ..check import check_isinstance
66
+ from ..check import check_none
67
+ from ..check import check_not_none
68
+ from ..socket import SocketAddress
69
+ from ..socket import SocketHandler
70
+ from .handlers import HttpHandler
71
+ from .handlers import HttpHandlerRequest
72
+ from .handlers import UnsupportedMethodHttpHandlerError
73
+ from .parsing import EmptyParsedHttpResult
74
+ from .parsing import HttpRequestParser
75
+ from .parsing import ParsedHttpRequest
76
+ from .parsing import ParseHttpRequestError
77
+ from .versions import HttpProtocolVersion
78
+ from .versions import HttpProtocolVersions
79
+
80
+
81
+ CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
82
+
83
+
84
+ ##
85
+
86
+
87
+ class CoroHttpServer:
88
+ """
89
+ Adapted from stdlib:
90
+ - https://github.com/python/cpython/blob/4b4e0dbdf49adc91c35a357ad332ab3abd4c31b1/Lib/http/server.py#L146
91
+ """
92
+
93
+ #
94
+
95
+ def __init__(
96
+ self,
97
+ client_address: SocketAddress,
98
+ *,
99
+ handler: HttpHandler,
100
+ parser: HttpRequestParser = HttpRequestParser(),
101
+
102
+ default_content_type: ta.Optional[str] = None,
103
+
104
+ error_message_format: ta.Optional[str] = None,
105
+ error_content_type: ta.Optional[str] = None,
106
+ ) -> None:
107
+ super().__init__()
108
+
109
+ self._client_address = client_address
110
+
111
+ self._handler = handler
112
+ self._parser = parser
113
+
114
+ self._default_content_type = default_content_type or self.DEFAULT_CONTENT_TYPE
115
+
116
+ self._error_message_format = error_message_format or self.DEFAULT_ERROR_MESSAGE
117
+ self._error_content_type = error_content_type or self.DEFAULT_ERROR_CONTENT_TYPE
118
+
119
+ #
120
+
121
+ @property
122
+ def client_address(self) -> SocketAddress:
123
+ return self._client_address
124
+
125
+ @property
126
+ def handler(self) -> HttpHandler:
127
+ return self._handler
128
+
129
+ @property
130
+ def parser(self) -> HttpRequestParser:
131
+ return self._parser
132
+
133
+ #
134
+
135
+ def _format_timestamp(self, timestamp: ta.Optional[float] = None) -> str:
136
+ if timestamp is None:
137
+ timestamp = time.time()
138
+ return email.utils.formatdate(timestamp, usegmt=True)
139
+
140
+ #
141
+
142
+ def _header_encode(self, s: str) -> bytes:
143
+ return s.encode('latin-1', 'strict')
144
+
145
+ class _Header(ta.NamedTuple):
146
+ key: str
147
+ value: str
148
+
149
+ def _format_header_line(self, h: _Header) -> str:
150
+ return f'{h.key}: {h.value}\r\n'
151
+
152
+ def _get_header_close_connection_action(self, h: _Header) -> ta.Optional[bool]:
153
+ if h.key.lower() != 'connection':
154
+ return None
155
+ elif h.value.lower() == 'close':
156
+ return True
157
+ elif h.value.lower() == 'keep-alive':
158
+ return False
159
+ else:
160
+ return None
161
+
162
+ def _make_default_headers(self) -> ta.List[_Header]:
163
+ return [
164
+ self._Header('Date', self._format_timestamp()),
165
+ ]
166
+
167
+ #
168
+
169
+ _STATUS_RESPONSES: ta.Mapping[int, ta.Tuple[str, str]] = {
170
+ v: (v.phrase, v.description)
171
+ for v in http.HTTPStatus.__members__.values()
172
+ }
173
+
174
+ def _format_status_line(
175
+ self,
176
+ version: HttpProtocolVersion,
177
+ code: ta.Union[http.HTTPStatus, int],
178
+ message: ta.Optional[str] = None,
179
+ ) -> str:
180
+ if message is None:
181
+ if code in self._STATUS_RESPONSES:
182
+ message = self._STATUS_RESPONSES[code][0]
183
+ else:
184
+ message = ''
185
+
186
+ return f'{version} {int(code)} {message}\r\n'
187
+
188
+ #
189
+
190
+ @dc.dataclass(frozen=True)
191
+ class _Response:
192
+ version: HttpProtocolVersion
193
+ code: http.HTTPStatus
194
+
195
+ message: ta.Optional[str] = None
196
+ headers: ta.Optional[ta.Sequence['CoroHttpServer._Header']] = None
197
+ data: ta.Optional[bytes] = None
198
+ close_connection: ta.Optional[bool] = False
199
+
200
+ def get_header(self, key: str) -> ta.Optional['CoroHttpServer._Header']:
201
+ for h in self.headers or []:
202
+ if h.key.lower() == key.lower():
203
+ return h
204
+ return None
205
+
206
+ #
207
+
208
+ def _build_response_bytes(self, a: _Response) -> bytes:
209
+ out = io.BytesIO()
210
+
211
+ if a.version >= HttpProtocolVersions.HTTP_1_0:
212
+ out.write(self._header_encode(self._format_status_line(
213
+ a.version,
214
+ a.code,
215
+ a.message,
216
+ )))
217
+
218
+ for h in a.headers or []:
219
+ out.write(self._header_encode(self._format_header_line(h)))
220
+
221
+ out.write(b'\r\n')
222
+
223
+ if a.data is not None:
224
+ out.write(a.data)
225
+
226
+ return out.getvalue()
227
+
228
+ #
229
+
230
+ DEFAULT_CONTENT_TYPE = 'text/plain'
231
+
232
+ def _preprocess_response(self, resp: _Response) -> _Response:
233
+ nh: ta.List[CoroHttpServer._Header] = []
234
+ kw: ta.Dict[str, ta.Any] = {}
235
+
236
+ if resp.get_header('Content-Type') is None:
237
+ nh.append(self._Header('Content-Type', self._default_content_type))
238
+ if resp.data is not None and resp.get_header('Content-Length') is None:
239
+ nh.append(self._Header('Content-Length', str(len(resp.data))))
240
+
241
+ if nh:
242
+ kw.update(headers=[*(resp.headers or []), *nh])
243
+
244
+ if (clh := resp.get_header('Connection')) is not None:
245
+ if self._get_header_close_connection_action(clh):
246
+ kw.update(close_connection=True)
247
+
248
+ if not kw:
249
+ return resp
250
+ return dc.replace(resp, **kw)
251
+
252
+ #
253
+
254
+ @dc.dataclass(frozen=True)
255
+ class Error:
256
+ version: HttpProtocolVersion
257
+ code: http.HTTPStatus
258
+ message: str
259
+ explain: str
260
+
261
+ method: ta.Optional[str] = None
262
+
263
+ def _build_error(
264
+ self,
265
+ code: ta.Union[http.HTTPStatus, int],
266
+ message: ta.Optional[str] = None,
267
+ explain: ta.Optional[str] = None,
268
+ *,
269
+ version: ta.Optional[HttpProtocolVersion] = None,
270
+ method: ta.Optional[str] = None,
271
+ ) -> Error:
272
+ code = http.HTTPStatus(code)
273
+
274
+ try:
275
+ short_msg, long_msg = self._STATUS_RESPONSES[code]
276
+ except KeyError:
277
+ short_msg, long_msg = '???', '???'
278
+ if message is None:
279
+ message = short_msg
280
+ if explain is None:
281
+ explain = long_msg
282
+
283
+ if version is None:
284
+ version = self._parser.server_version
285
+
286
+ return self.Error(
287
+ version=version,
288
+ code=code,
289
+ message=message,
290
+ explain=explain,
291
+
292
+ method=method,
293
+ )
294
+
295
+ #
296
+
297
+ DEFAULT_ERROR_MESSAGE = textwrap.dedent("""\
298
+ <!DOCTYPE HTML>
299
+ <html lang="en">
300
+ <head>
301
+ <meta charset="utf-8">
302
+ <title>Error response</title>
303
+ </head>
304
+ <body>
305
+ <h1>Error response</h1>
306
+ <p>Error code: %(code)d</p>
307
+ <p>Message: %(message)s.</p>
308
+ <p>Error code explanation: %(code)s - %(explain)s.</p>
309
+ </body>
310
+ </html>
311
+ """)
312
+
313
+ DEFAULT_ERROR_CONTENT_TYPE = 'text/html;charset=utf-8'
314
+
315
+ def _build_error_response(self, err: Error) -> _Response:
316
+ headers: ta.List[CoroHttpServer._Header] = [
317
+ *self._make_default_headers(),
318
+ self._Header('Connection', 'close'),
319
+ ]
320
+
321
+ # Message body is omitted for cases described in:
322
+ # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified)
323
+ # - RFC7231: 6.3.6. 205(Reset Content)
324
+ data: ta.Optional[bytes] = None
325
+ if (
326
+ err.code >= http.HTTPStatus.OK and
327
+ err.code not in (
328
+ http.HTTPStatus.NO_CONTENT,
329
+ http.HTTPStatus.RESET_CONTENT,
330
+ http.HTTPStatus.NOT_MODIFIED,
331
+ )
332
+ ):
333
+ # HTML encode to prevent Cross Site Scripting attacks (see bug #1100201)
334
+ content = self._error_message_format.format(
335
+ code=err.code,
336
+ message=html.escape(err.message, quote=False),
337
+ explain=html.escape(err.explain, quote=False),
338
+ )
339
+ body = content.encode('UTF-8', 'replace')
340
+
341
+ headers.extend([
342
+ self._Header('Content-Type', self._error_content_type),
343
+ self._Header('Content-Length', str(len(body))),
344
+ ])
345
+
346
+ if err.method != 'HEAD' and body:
347
+ data = body
348
+
349
+ return self._Response(
350
+ version=err.version,
351
+ code=err.code,
352
+ message=err.message,
353
+ headers=headers,
354
+ data=data,
355
+ close_connection=True,
356
+ )
357
+
358
+ #
359
+
360
+ class Io(abc.ABC): # noqa
361
+ pass
362
+
363
+ #
364
+
365
+ class AnyLogIo(Io):
366
+ pass
367
+
368
+ @dc.dataclass(frozen=True)
369
+ class ParsedRequestLogIo(AnyLogIo):
370
+ request: ParsedHttpRequest
371
+
372
+ @dc.dataclass(frozen=True)
373
+ class ErrorLogIo(AnyLogIo):
374
+ error: 'CoroHttpServer.Error'
375
+
376
+ #
377
+
378
+ class AnyReadIo(Io): # noqa
379
+ pass
380
+
381
+ @dc.dataclass(frozen=True)
382
+ class ReadIo(AnyReadIo):
383
+ sz: int
384
+
385
+ @dc.dataclass(frozen=True)
386
+ class ReadLineIo(AnyReadIo):
387
+ sz: int
388
+
389
+ #
390
+
391
+ @dc.dataclass(frozen=True)
392
+ class WriteIo(Io):
393
+ data: bytes
394
+
395
+ #
396
+
397
+ def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
398
+ while True:
399
+ gen = self.coro_handle_one()
400
+
401
+ o = next(gen)
402
+ i: ta.Optional[bytes]
403
+ while True:
404
+ if isinstance(o, self.AnyLogIo):
405
+ i = None
406
+ yield o
407
+
408
+ elif isinstance(o, self.AnyReadIo):
409
+ i = check_isinstance((yield o), bytes)
410
+
411
+ elif isinstance(o, self._Response):
412
+ i = None
413
+ r = self._preprocess_response(o)
414
+ b = self._build_response_bytes(r)
415
+ check_none((yield self.WriteIo(b)))
416
+
417
+ else:
418
+ raise TypeError(o)
419
+
420
+ try:
421
+ o = gen.send(i)
422
+ except EOFError:
423
+ return
424
+ except StopIteration:
425
+ break
426
+
427
+ def coro_handle_one(self) -> ta.Generator[
428
+ ta.Union[AnyLogIo, AnyReadIo, _Response],
429
+ ta.Optional[bytes],
430
+ None,
431
+ ]:
432
+ # Parse request
433
+
434
+ gen = self._parser.coro_parse()
435
+ sz = next(gen)
436
+ while True:
437
+ try:
438
+ line = check_isinstance((yield self.ReadLineIo(sz)), bytes)
439
+ sz = gen.send(line)
440
+ except StopIteration as e:
441
+ parsed = e.value
442
+ break
443
+
444
+ if isinstance(parsed, EmptyParsedHttpResult):
445
+ raise EOFError # noqa
446
+
447
+ if isinstance(parsed, ParseHttpRequestError):
448
+ err = self._build_error(
449
+ parsed.code,
450
+ *parsed.message,
451
+ version=parsed.version,
452
+ )
453
+ yield self.ErrorLogIo(err)
454
+ yield self._build_error_response(err)
455
+ return
456
+
457
+ parsed = check_isinstance(parsed, ParsedHttpRequest)
458
+
459
+ # Log
460
+
461
+ check_none((yield self.ParsedRequestLogIo(parsed)))
462
+
463
+ # Handle CONTINUE
464
+
465
+ if parsed.expects_continue:
466
+ # https://bugs.python.org/issue1491
467
+ # https://github.com/python/cpython/commit/0f476d49f8d4aa84210392bf13b59afc67b32b31
468
+ yield self._Response(
469
+ version=parsed.version,
470
+ code=http.HTTPStatus.CONTINUE,
471
+ )
472
+
473
+ # Read data
474
+
475
+ request_data: ta.Optional[bytes]
476
+ if (cl := parsed.headers.get('Content-Length')) is not None:
477
+ request_data = check_isinstance((yield self.ReadIo(int(cl))), bytes)
478
+ else:
479
+ request_data = None
480
+
481
+ # Build request
482
+
483
+ handler_request = HttpHandlerRequest(
484
+ client_address=self._client_address,
485
+ method=check_not_none(parsed.method),
486
+ path=parsed.path,
487
+ headers=parsed.headers,
488
+ data=request_data,
489
+ )
490
+
491
+ # Build handler response
492
+
493
+ try:
494
+ handler_response = self._handler(handler_request)
495
+
496
+ except UnsupportedMethodHttpHandlerError:
497
+ err = self._build_error(
498
+ http.HTTPStatus.NOT_IMPLEMENTED,
499
+ f'Unsupported method ({parsed.method!r})',
500
+ version=parsed.version,
501
+ method=parsed.method,
502
+ )
503
+ yield self.ErrorLogIo(err)
504
+ yield self._build_error_response(err)
505
+ return
506
+
507
+ # Build internal response
508
+
509
+ response_headers = handler_response.headers or {}
510
+ response_data = handler_response.data
511
+
512
+ headers: ta.List[CoroHttpServer._Header] = [
513
+ *self._make_default_headers(),
514
+ ]
515
+
516
+ for k, v in response_headers.items():
517
+ headers.append(self._Header(k, v))
518
+
519
+ if handler_response.close_connection and 'Connection' not in headers:
520
+ headers.append(self._Header('Connection', 'close'))
521
+
522
+ yield self._Response(
523
+ version=parsed.version,
524
+ code=http.HTTPStatus(handler_response.status),
525
+ headers=headers,
526
+ data=response_data,
527
+ close_connection=handler_response.close_connection,
528
+ )
529
+
530
+
531
+ ##
532
+
533
+
534
+ class CoroHttpServerSocketHandler(SocketHandler):
535
+ def __init__(
536
+ self,
537
+ client_address: SocketAddress,
538
+ rfile: ta.BinaryIO,
539
+ wfile: ta.BinaryIO,
540
+ *,
541
+ server_factory: CoroHttpServerFactory,
542
+ log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
543
+ ) -> None:
544
+ super().__init__(
545
+ client_address,
546
+ rfile,
547
+ wfile,
548
+ )
549
+
550
+ self._server_factory = server_factory
551
+ self._log_handler = log_handler
552
+
553
+ def handle(self) -> None:
554
+ server = self._server_factory(self._client_address)
555
+
556
+ gen = server.coro_handle()
557
+
558
+ o = next(gen)
559
+ while True:
560
+ if isinstance(o, CoroHttpServer.AnyLogIo):
561
+ i = None
562
+ if self._log_handler is not None:
563
+ self._log_handler(server, o)
564
+
565
+ elif isinstance(o, CoroHttpServer.ReadIo):
566
+ i = self._rfile.read(o.sz)
567
+
568
+ elif isinstance(o, CoroHttpServer.ReadLineIo):
569
+ i = self._rfile.readline(o.sz)
570
+
571
+ elif isinstance(o, CoroHttpServer.WriteIo):
572
+ i = None
573
+ self._wfile.write(o.data)
574
+ self._wfile.flush()
575
+
576
+ else:
577
+ raise TypeError(o)
578
+
579
+ try:
580
+ if i is not None:
581
+ o = gen.send(i)
582
+ else:
583
+ o = next(gen)
584
+ except StopIteration:
585
+ break
@@ -0,0 +1,36 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import http.server
4
+ import typing as ta
5
+
6
+ from ..socket import SocketAddress
7
+ from .parsing import HttpHeaders
8
+
9
+
10
+ HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse']
11
+
12
+
13
+ @dc.dataclass(frozen=True)
14
+ class HttpHandlerRequest:
15
+ client_address: SocketAddress
16
+ method: str
17
+ path: str
18
+ headers: HttpHeaders
19
+ data: ta.Optional[bytes]
20
+
21
+
22
+ @dc.dataclass(frozen=True)
23
+ class HttpHandlerResponse:
24
+ status: ta.Union[http.HTTPStatus, int]
25
+
26
+ headers: ta.Optional[ta.Mapping[str, str]] = None
27
+ data: ta.Optional[bytes] = None
28
+ close_connection: ta.Optional[bool] = None
29
+
30
+
31
+ class HttpHandlerError(Exception):
32
+ pass
33
+
34
+
35
+ class UnsupportedMethodHttpHandlerError(Exception):
36
+ pass