ominfra 0.0.0.dev123__py3-none-any.whl → 0.0.0.dev125__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,34 @@
4
4
  # @omlish-script
5
5
  # @omlish-amalg-output ../supervisor/main.py
6
6
  # ruff: noqa: N802 UP006 UP007 UP012 UP036
7
+ # Supervisor is licensed under the following license:
8
+ #
9
+ # A copyright notice accompanies this license document that identifies the copyright holders.
10
+ #
11
+ # Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
12
+ # following conditions are met:
13
+ #
14
+ # 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the
15
+ # following disclaimer.
16
+ #
17
+ # 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the
18
+ # following disclaimer in the documentation and/or other materials provided with the distribution.
19
+ #
20
+ # 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without
21
+ # prior written permission from the copyright holders.
22
+ #
23
+ # 4. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed
24
+ # the files and the date of any change.
25
+ #
26
+ # Disclaimer
27
+ #
28
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
29
+ # NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
30
+ # EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
32
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
34
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
7
35
  import abc
8
36
  import base64
9
37
  import collections.abc
@@ -12,13 +40,18 @@ import ctypes as ct
12
40
  import dataclasses as dc
13
41
  import datetime
14
42
  import decimal
43
+ import email.utils
15
44
  import enum
16
45
  import errno
17
46
  import fcntl
18
47
  import fractions
19
48
  import functools
20
49
  import grp
50
+ import html
51
+ import http.client
52
+ import http.server
21
53
  import inspect
54
+ import io
22
55
  import itertools
23
56
  import json
24
57
  import logging
@@ -30,11 +63,13 @@ import resource
30
63
  import select
31
64
  import shlex
32
65
  import signal
66
+ import socket
33
67
  import stat
34
68
  import string
35
69
  import sys
36
70
  import syslog
37
71
  import tempfile
72
+ import textwrap
38
73
  import threading
39
74
  import time
40
75
  import traceback
@@ -49,8 +84,7 @@ import weakref # noqa
49
84
 
50
85
 
51
86
  if sys.version_info < (3, 8):
52
- raise OSError(
53
- f'Requires python (3, 8), got {sys.version_info} from {sys.executable}') # noqa
87
+ raise OSError(f'Requires python (3, 8), got {sys.version_info} from {sys.executable}') # noqa
54
88
 
55
89
 
56
90
  ########################################
@@ -64,6 +98,16 @@ TomlPos = int # ta.TypeAlias
64
98
  # ../../../omlish/lite/cached.py
65
99
  T = ta.TypeVar('T')
66
100
 
101
+ # ../../../omlish/lite/socket.py
102
+ SocketAddress = ta.Any
103
+ SocketHandlerFactory = ta.Callable[[SocketAddress, ta.BinaryIO, ta.BinaryIO], 'SocketHandler']
104
+
105
+ # ../events.py
106
+ EventCallback = ta.Callable[['Event'], None]
107
+
108
+ # ../../../omlish/lite/http/parsing.py
109
+ HttpHeaders = http.client.HTTPMessage # ta.TypeAlias
110
+
67
111
  # ../../../omlish/lite/inject.py
68
112
  InjectorKeyCls = ta.Union[type, ta.NewType]
69
113
  InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
@@ -73,11 +117,11 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
73
117
  # ../../configs.py
74
118
  ConfigMapping = ta.Mapping[str, ta.Any]
75
119
 
76
- # ../context.py
77
- ServerEpoch = ta.NewType('ServerEpoch', int)
120
+ # ../../../omlish/lite/http/handlers.py
121
+ HttpHandler = ta.Callable[['HttpHandlerRequest'], 'HttpHandlerResponse']
78
122
 
79
- # ../process.py
80
- InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
123
+ # ../../../omlish/lite/http/coroserver.py
124
+ CoroHttpServerFactory = ta.Callable[[SocketAddress], 'CoroHttpServer']
81
125
 
82
126
 
83
127
  ########################################
@@ -1254,6 +1298,11 @@ def check_not_isinstance(v: T, spec: ta.Union[type, tuple]) -> T:
1254
1298
  return v
1255
1299
 
1256
1300
 
1301
+ def check_none(v: T) -> None:
1302
+ if v is not None:
1303
+ raise ValueError(v)
1304
+
1305
+
1257
1306
  def check_not_none(v: ta.Optional[T]) -> T:
1258
1307
  if v is None:
1259
1308
  raise ValueError
@@ -1294,6 +1343,25 @@ def check_single(vs: ta.Iterable[T]) -> T:
1294
1343
  return v
1295
1344
 
1296
1345
 
1346
+ ########################################
1347
+ # ../../../omlish/lite/http/versions.py
1348
+
1349
+
1350
+ class HttpProtocolVersion(ta.NamedTuple):
1351
+ major: int
1352
+ minor: int
1353
+
1354
+ def __str__(self) -> str:
1355
+ return f'HTTP/{self.major}.{self.minor}'
1356
+
1357
+
1358
+ class HttpProtocolVersions:
1359
+ HTTP_0_9 = HttpProtocolVersion(0, 9)
1360
+ HTTP_1_0 = HttpProtocolVersion(1, 0)
1361
+ HTTP_1_1 = HttpProtocolVersion(1, 1)
1362
+ HTTP_2_0 = HttpProtocolVersion(2, 0)
1363
+
1364
+
1297
1365
  ########################################
1298
1366
  # ../../../omlish/lite/json.py
1299
1367
 
@@ -1424,6 +1492,88 @@ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
1424
1492
  todo.extend(reversed(cur.__subclasses__()))
1425
1493
 
1426
1494
 
1495
+ ########################################
1496
+ # ../../../omlish/lite/socket.py
1497
+ """
1498
+ TODO:
1499
+ - SocketClientAddress family / tuple pairs
1500
+ + codification of https://docs.python.org/3/library/socket.html#socket-families
1501
+ """
1502
+
1503
+
1504
+ ##
1505
+
1506
+
1507
+ @dc.dataclass(frozen=True)
1508
+ class SocketAddressInfoArgs:
1509
+ host: ta.Optional[str]
1510
+ port: ta.Union[str, int, None]
1511
+ family: socket.AddressFamily = socket.AddressFamily.AF_UNSPEC
1512
+ type: int = 0
1513
+ proto: int = 0
1514
+ flags: socket.AddressInfo = socket.AddressInfo(0)
1515
+
1516
+
1517
+ @dc.dataclass(frozen=True)
1518
+ class SocketAddressInfo:
1519
+ family: socket.AddressFamily
1520
+ type: int
1521
+ proto: int
1522
+ canonname: ta.Optional[str]
1523
+ sockaddr: SocketAddress
1524
+
1525
+
1526
+ def get_best_socket_family(
1527
+ host: ta.Optional[str],
1528
+ port: ta.Union[str, int, None],
1529
+ family: ta.Union[int, socket.AddressFamily] = socket.AddressFamily.AF_UNSPEC,
1530
+ ) -> ta.Tuple[socket.AddressFamily, SocketAddress]:
1531
+ """https://github.com/python/cpython/commit/f289084c83190cc72db4a70c58f007ec62e75247"""
1532
+
1533
+ infos = socket.getaddrinfo(
1534
+ host,
1535
+ port,
1536
+ family,
1537
+ type=socket.SOCK_STREAM,
1538
+ flags=socket.AI_PASSIVE,
1539
+ )
1540
+ ai = SocketAddressInfo(*next(iter(infos)))
1541
+ return ai.family, ai.sockaddr
1542
+
1543
+
1544
+ ##
1545
+
1546
+
1547
+ class SocketHandler(abc.ABC):
1548
+ def __init__(
1549
+ self,
1550
+ client_address: SocketAddress,
1551
+ rfile: ta.BinaryIO,
1552
+ wfile: ta.BinaryIO,
1553
+ ) -> None:
1554
+ super().__init__()
1555
+
1556
+ self._client_address = client_address
1557
+ self._rfile = rfile
1558
+ self._wfile = wfile
1559
+
1560
+ @abc.abstractmethod
1561
+ def handle(self) -> None:
1562
+ raise NotImplementedError
1563
+
1564
+
1565
+ ########################################
1566
+ # ../../../omlish/lite/typing.py
1567
+
1568
+
1569
+ @dc.dataclass(frozen=True)
1570
+ class Func(ta.Generic[T]):
1571
+ fn: ta.Callable[..., T]
1572
+
1573
+ def __call__(self, *args: ta.Any, **kwargs: ta.Any) -> T:
1574
+ return self.fn(*args, **kwargs)
1575
+
1576
+
1427
1577
  ########################################
1428
1578
  # ../events.py
1429
1579
 
@@ -1438,9 +1588,6 @@ class Event(abc.ABC): # noqa
1438
1588
  ##
1439
1589
 
1440
1590
 
1441
- EventCallback = ta.Callable[['Event'], None]
1442
-
1443
-
1444
1591
  class EventCallbacks:
1445
1592
  def __init__(self) -> None:
1446
1593
  super().__init__()
@@ -1910,6 +2057,371 @@ def timeslice(period: int, when: float) -> int:
1910
2057
  return int(when - (when % period))
1911
2058
 
1912
2059
 
2060
+ ########################################
2061
+ # ../../../omlish/lite/http/parsing.py
2062
+
2063
+
2064
+ ##
2065
+
2066
+
2067
+ class ParseHttpRequestResult(abc.ABC): # noqa
2068
+ __slots__ = (
2069
+ 'server_version',
2070
+ 'request_line',
2071
+ 'request_version',
2072
+ 'version',
2073
+ 'headers',
2074
+ 'close_connection',
2075
+ )
2076
+
2077
+ def __init__(
2078
+ self,
2079
+ *,
2080
+ server_version: HttpProtocolVersion,
2081
+ request_line: str,
2082
+ request_version: HttpProtocolVersion,
2083
+ version: HttpProtocolVersion,
2084
+ headers: ta.Optional[HttpHeaders],
2085
+ close_connection: bool,
2086
+ ) -> None:
2087
+ super().__init__()
2088
+
2089
+ self.server_version = server_version
2090
+ self.request_line = request_line
2091
+ self.request_version = request_version
2092
+ self.version = version
2093
+ self.headers = headers
2094
+ self.close_connection = close_connection
2095
+
2096
+ def __repr__(self) -> str:
2097
+ return f'{self.__class__.__name__}({", ".join(f"{a}={getattr(self, a)!r}" for a in self.__slots__)})'
2098
+
2099
+
2100
+ class EmptyParsedHttpResult(ParseHttpRequestResult):
2101
+ pass
2102
+
2103
+
2104
+ class ParseHttpRequestError(ParseHttpRequestResult):
2105
+ __slots__ = (
2106
+ 'code',
2107
+ 'message',
2108
+ *ParseHttpRequestResult.__slots__,
2109
+ )
2110
+
2111
+ def __init__(
2112
+ self,
2113
+ *,
2114
+ code: http.HTTPStatus,
2115
+ message: ta.Union[str, ta.Tuple[str, str]],
2116
+
2117
+ **kwargs: ta.Any,
2118
+ ) -> None:
2119
+ super().__init__(**kwargs)
2120
+
2121
+ self.code = code
2122
+ self.message = message
2123
+
2124
+
2125
+ class ParsedHttpRequest(ParseHttpRequestResult):
2126
+ __slots__ = (
2127
+ 'method',
2128
+ 'path',
2129
+ 'headers',
2130
+ 'expects_continue',
2131
+ *[a for a in ParseHttpRequestResult.__slots__ if a != 'headers'],
2132
+ )
2133
+
2134
+ def __init__(
2135
+ self,
2136
+ *,
2137
+ method: str,
2138
+ path: str,
2139
+ headers: HttpHeaders,
2140
+ expects_continue: bool,
2141
+
2142
+ **kwargs: ta.Any,
2143
+ ) -> None:
2144
+ super().__init__(
2145
+ headers=headers,
2146
+ **kwargs,
2147
+ )
2148
+
2149
+ self.method = method
2150
+ self.path = path
2151
+ self.expects_continue = expects_continue
2152
+
2153
+ headers: HttpHeaders
2154
+
2155
+
2156
+ #
2157
+
2158
+
2159
+ class HttpRequestParser:
2160
+ DEFAULT_SERVER_VERSION = HttpProtocolVersions.HTTP_1_0
2161
+
2162
+ # The default request version. This only affects responses up until the point where the request line is parsed, so
2163
+ # it mainly decides what the client gets back when sending a malformed request line.
2164
+ # Most web servers default to HTTP 0.9, i.e. don't send a status line.
2165
+ DEFAULT_REQUEST_VERSION = HttpProtocolVersions.HTTP_0_9
2166
+
2167
+ #
2168
+
2169
+ DEFAULT_MAX_LINE: int = 0x10000
2170
+ DEFAULT_MAX_HEADERS: int = 100
2171
+
2172
+ #
2173
+
2174
+ def __init__(
2175
+ self,
2176
+ *,
2177
+ server_version: HttpProtocolVersion = DEFAULT_SERVER_VERSION,
2178
+
2179
+ max_line: int = DEFAULT_MAX_LINE,
2180
+ max_headers: int = DEFAULT_MAX_HEADERS,
2181
+ ) -> None:
2182
+ super().__init__()
2183
+
2184
+ if server_version >= HttpProtocolVersions.HTTP_2_0:
2185
+ raise ValueError(f'Unsupported protocol version: {server_version}')
2186
+ self._server_version = server_version
2187
+
2188
+ self._max_line = max_line
2189
+ self._max_headers = max_headers
2190
+
2191
+ #
2192
+
2193
+ @property
2194
+ def server_version(self) -> HttpProtocolVersion:
2195
+ return self._server_version
2196
+
2197
+ #
2198
+
2199
+ def _run_read_line_coro(
2200
+ self,
2201
+ gen: ta.Generator[int, bytes, T],
2202
+ read_line: ta.Callable[[int], bytes],
2203
+ ) -> T:
2204
+ sz = next(gen)
2205
+ while True:
2206
+ try:
2207
+ sz = gen.send(read_line(sz))
2208
+ except StopIteration as e:
2209
+ return e.value
2210
+
2211
+ #
2212
+
2213
+ def parse_request_version(self, version_str: str) -> HttpProtocolVersion:
2214
+ if not version_str.startswith('HTTP/'):
2215
+ raise ValueError(version_str) # noqa
2216
+
2217
+ base_version_number = version_str.split('/', 1)[1]
2218
+ version_number_parts = base_version_number.split('.')
2219
+
2220
+ # RFC 2145 section 3.1 says there can be only one "." and
2221
+ # - major and minor numbers MUST be treated as separate integers;
2222
+ # - HTTP/2.4 is a lower version than HTTP/2.13, which in turn is lower than HTTP/12.3;
2223
+ # - Leading zeros MUST be ignored by recipients.
2224
+ if len(version_number_parts) != 2:
2225
+ raise ValueError(version_number_parts) # noqa
2226
+ if any(not component.isdigit() for component in version_number_parts):
2227
+ raise ValueError('non digit in http version') # noqa
2228
+ if any(len(component) > 10 for component in version_number_parts):
2229
+ raise ValueError('unreasonable length http version') # noqa
2230
+
2231
+ return HttpProtocolVersion(
2232
+ int(version_number_parts[0]),
2233
+ int(version_number_parts[1]),
2234
+ )
2235
+
2236
+ #
2237
+
2238
+ def coro_read_raw_headers(self) -> ta.Generator[int, bytes, ta.List[bytes]]:
2239
+ raw_headers: ta.List[bytes] = []
2240
+ while True:
2241
+ line = yield self._max_line + 1
2242
+ if len(line) > self._max_line:
2243
+ raise http.client.LineTooLong('header line')
2244
+ raw_headers.append(line)
2245
+ if len(raw_headers) > self._max_headers:
2246
+ raise http.client.HTTPException(f'got more than {self._max_headers} headers')
2247
+ if line in (b'\r\n', b'\n', b''):
2248
+ break
2249
+ return raw_headers
2250
+
2251
+ def read_raw_headers(self, read_line: ta.Callable[[int], bytes]) -> ta.List[bytes]:
2252
+ return self._run_read_line_coro(self.coro_read_raw_headers(), read_line)
2253
+
2254
+ def parse_raw_headers(self, raw_headers: ta.Sequence[bytes]) -> HttpHeaders:
2255
+ return http.client.parse_headers(io.BytesIO(b''.join(raw_headers)))
2256
+
2257
+ #
2258
+
2259
+ def coro_parse(self) -> ta.Generator[int, bytes, ParseHttpRequestResult]:
2260
+ raw_request_line = yield self._max_line + 1
2261
+
2262
+ # Common result kwargs
2263
+
2264
+ request_line = '-'
2265
+ request_version = self.DEFAULT_REQUEST_VERSION
2266
+
2267
+ # Set to min(server, request) when it gets that far, but if it fails before that the server authoritatively
2268
+ # responds with its own version.
2269
+ version = self._server_version
2270
+
2271
+ headers: HttpHeaders | None = None
2272
+
2273
+ close_connection = True
2274
+
2275
+ def result_kwargs():
2276
+ return dict(
2277
+ server_version=self._server_version,
2278
+ request_line=request_line,
2279
+ request_version=request_version,
2280
+ version=version,
2281
+ headers=headers,
2282
+ close_connection=close_connection,
2283
+ )
2284
+
2285
+ # Decode line
2286
+
2287
+ if len(raw_request_line) > self._max_line:
2288
+ return ParseHttpRequestError(
2289
+ code=http.HTTPStatus.REQUEST_URI_TOO_LONG,
2290
+ message='Request line too long',
2291
+ **result_kwargs(),
2292
+ )
2293
+
2294
+ if not raw_request_line:
2295
+ return EmptyParsedHttpResult(**result_kwargs())
2296
+
2297
+ request_line = raw_request_line.decode('iso-8859-1').rstrip('\r\n')
2298
+
2299
+ # Split words
2300
+
2301
+ words = request_line.split()
2302
+ if len(words) == 0:
2303
+ return EmptyParsedHttpResult(**result_kwargs())
2304
+
2305
+ # Parse and set version
2306
+
2307
+ if len(words) >= 3: # Enough to determine protocol version
2308
+ version_str = words[-1]
2309
+ try:
2310
+ request_version = self.parse_request_version(version_str)
2311
+
2312
+ except (ValueError, IndexError):
2313
+ return ParseHttpRequestError(
2314
+ code=http.HTTPStatus.BAD_REQUEST,
2315
+ message=f'Bad request version ({version_str!r})',
2316
+ **result_kwargs(),
2317
+ )
2318
+
2319
+ if (
2320
+ request_version < HttpProtocolVersions.HTTP_0_9 or
2321
+ request_version >= HttpProtocolVersions.HTTP_2_0
2322
+ ):
2323
+ return ParseHttpRequestError(
2324
+ code=http.HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
2325
+ message=f'Invalid HTTP version ({version_str})',
2326
+ **result_kwargs(),
2327
+ )
2328
+
2329
+ version = min([self._server_version, request_version])
2330
+
2331
+ if version >= HttpProtocolVersions.HTTP_1_1:
2332
+ close_connection = False
2333
+
2334
+ # Verify word count
2335
+
2336
+ if not 2 <= len(words) <= 3:
2337
+ return ParseHttpRequestError(
2338
+ code=http.HTTPStatus.BAD_REQUEST,
2339
+ message=f'Bad request syntax ({request_line!r})',
2340
+ **result_kwargs(),
2341
+ )
2342
+
2343
+ # Parse method and path
2344
+
2345
+ method, path = words[:2]
2346
+ if len(words) == 2:
2347
+ close_connection = True
2348
+ if method != 'GET':
2349
+ return ParseHttpRequestError(
2350
+ code=http.HTTPStatus.BAD_REQUEST,
2351
+ message=f'Bad HTTP/0.9 request type ({method!r})',
2352
+ **result_kwargs(),
2353
+ )
2354
+
2355
+ # gh-87389: The purpose of replacing '//' with '/' is to protect against open redirect attacks possibly
2356
+ # triggered if the path starts with '//' because http clients treat //path as an absolute URI without scheme
2357
+ # (similar to http://path) rather than a path.
2358
+ if path.startswith('//'):
2359
+ path = '/' + path.lstrip('/') # Reduce to a single /
2360
+
2361
+ # Parse headers
2362
+
2363
+ try:
2364
+ raw_gen = self.coro_read_raw_headers()
2365
+ raw_sz = next(raw_gen)
2366
+ while True:
2367
+ buf = yield raw_sz
2368
+ try:
2369
+ raw_sz = raw_gen.send(buf)
2370
+ except StopIteration as e:
2371
+ raw_headers = e.value
2372
+ break
2373
+
2374
+ headers = self.parse_raw_headers(raw_headers)
2375
+
2376
+ except http.client.LineTooLong as err:
2377
+ return ParseHttpRequestError(
2378
+ code=http.HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
2379
+ message=('Line too long', str(err)),
2380
+ **result_kwargs(),
2381
+ )
2382
+
2383
+ except http.client.HTTPException as err:
2384
+ return ParseHttpRequestError(
2385
+ code=http.HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
2386
+ message=('Too many headers', str(err)),
2387
+ **result_kwargs(),
2388
+ )
2389
+
2390
+ # Check for connection directive
2391
+
2392
+ conn_type = headers.get('Connection', '')
2393
+ if conn_type.lower() == 'close':
2394
+ close_connection = True
2395
+ elif (
2396
+ conn_type.lower() == 'keep-alive' and
2397
+ version >= HttpProtocolVersions.HTTP_1_1
2398
+ ):
2399
+ close_connection = False
2400
+
2401
+ # Check for expect directive
2402
+
2403
+ expect = headers.get('Expect', '')
2404
+ if (
2405
+ expect.lower() == '100-continue' and
2406
+ version >= HttpProtocolVersions.HTTP_1_1
2407
+ ):
2408
+ expects_continue = True
2409
+ else:
2410
+ expects_continue = False
2411
+
2412
+ # Return
2413
+
2414
+ return ParsedHttpRequest(
2415
+ method=method,
2416
+ path=path,
2417
+ expects_continue=expects_continue,
2418
+ **result_kwargs(),
2419
+ )
2420
+
2421
+ def parse(self, read_line: ta.Callable[[int], bytes]) -> ParseHttpRequestResult:
2422
+ return self._run_read_line_coro(self.coro_parse(), read_line)
2423
+
2424
+
1913
2425
  ########################################
1914
2426
  # ../../../omlish/lite/inject.py
1915
2427
 
@@ -1919,12 +2431,28 @@ def timeslice(period: int, when: float) -> int:
1919
2431
 
1920
2432
 
1921
2433
  @dc.dataclass(frozen=True)
1922
- class InjectorKey:
1923
- cls: InjectorKeyCls
2434
+ class InjectorKey(ta.Generic[T]):
2435
+ # Before PEP-560 typing.Generic was a metaclass with a __new__ that takes a 'cls' arg, so instantiating a dataclass
2436
+ # with kwargs (such as through dc.replace) causes `TypeError: __new__() got multiple values for argument 'cls'`.
2437
+ # See:
2438
+ # - https://github.com/python/cpython/commit/d911e40e788fb679723d78b6ea11cabf46caed5a
2439
+ # - https://gist.github.com/wrmsr/4468b86efe9f373b6b114bfe85b98fd3
2440
+ cls_: InjectorKeyCls
2441
+
1924
2442
  tag: ta.Any = None
1925
2443
  array: bool = False
1926
2444
 
1927
2445
 
2446
+ def is_valid_injector_key_cls(cls: ta.Any) -> bool:
2447
+ return isinstance(cls, type) or is_new_type(cls)
2448
+
2449
+
2450
+ def check_valid_injector_key_cls(cls: T) -> T:
2451
+ if not is_valid_injector_key_cls(cls):
2452
+ raise TypeError(cls)
2453
+ return cls
2454
+
2455
+
1928
2456
  ##
1929
2457
 
1930
2458
 
@@ -1968,6 +2496,12 @@ class Injector(abc.ABC):
1968
2496
  def inject(self, obj: ta.Any) -> ta.Any:
1969
2497
  raise NotImplementedError
1970
2498
 
2499
+ def __getitem__(
2500
+ self,
2501
+ target: ta.Union[InjectorKey[T], ta.Type[T]],
2502
+ ) -> T:
2503
+ return self.provide(target)
2504
+
1971
2505
 
1972
2506
  ###
1973
2507
  # exceptions
@@ -2000,7 +2534,7 @@ def as_injector_key(o: ta.Any) -> InjectorKey:
2000
2534
  raise TypeError(o)
2001
2535
  if isinstance(o, InjectorKey):
2002
2536
  return o
2003
- if isinstance(o, type) or is_new_type(o):
2537
+ if is_valid_injector_key_cls(o):
2004
2538
  return InjectorKey(o)
2005
2539
  raise TypeError(o)
2006
2540
 
@@ -2025,14 +2559,14 @@ class FnInjectorProvider(InjectorProvider):
2025
2559
 
2026
2560
  @dc.dataclass(frozen=True)
2027
2561
  class CtorInjectorProvider(InjectorProvider):
2028
- cls: type
2562
+ cls_: type
2029
2563
 
2030
2564
  def __post_init__(self) -> None:
2031
- check_isinstance(self.cls, type)
2565
+ check_isinstance(self.cls_, type)
2032
2566
 
2033
2567
  def provider_fn(self) -> InjectorProviderFn:
2034
2568
  def pfn(i: Injector) -> ta.Any:
2035
- return i.inject(self.cls)
2569
+ return i.inject(self.cls_)
2036
2570
 
2037
2571
  return pfn
2038
2572
 
@@ -2181,19 +2715,42 @@ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey,
2181
2715
  # inspection
2182
2716
 
2183
2717
 
2184
- _INJECTION_SIGNATURE_CACHE: ta.MutableMapping[ta.Any, inspect.Signature] = weakref.WeakKeyDictionary()
2718
+ # inspect.signature(eval_str=True) was added in 3.10 and we have to support 3.8, so we have to get_type_hints to eval
2719
+ # str annotations *in addition to* getting the signature for parameter information.
2720
+ class _InjectionInspection(ta.NamedTuple):
2721
+ signature: inspect.Signature
2722
+ type_hints: ta.Mapping[str, ta.Any]
2723
+
2724
+
2725
+ _INJECTION_INSPECTION_CACHE: ta.MutableMapping[ta.Any, _InjectionInspection] = weakref.WeakKeyDictionary()
2726
+
2185
2727
 
2728
+ def _do_injection_inspect(obj: ta.Any) -> _InjectionInspection:
2729
+ uw = obj
2730
+ while True:
2731
+ if isinstance(uw, functools.partial):
2732
+ uw = uw.func
2733
+ else:
2734
+ if (uw2 := inspect.unwrap(uw)) is uw:
2735
+ break
2736
+ uw = uw2
2737
+
2738
+ return _InjectionInspection(
2739
+ inspect.signature(obj),
2740
+ ta.get_type_hints(uw),
2741
+ )
2186
2742
 
2187
- def _injection_signature(obj: ta.Any) -> inspect.Signature:
2743
+
2744
+ def _injection_inspect(obj: ta.Any) -> _InjectionInspection:
2188
2745
  try:
2189
- return _INJECTION_SIGNATURE_CACHE[obj]
2746
+ return _INJECTION_INSPECTION_CACHE[obj]
2190
2747
  except TypeError:
2191
- return inspect.signature(obj)
2748
+ return _do_injection_inspect(obj)
2192
2749
  except KeyError:
2193
2750
  pass
2194
- sig = inspect.signature(obj)
2195
- _INJECTION_SIGNATURE_CACHE[obj] = sig
2196
- return sig
2751
+ insp = _do_injection_inspect(obj)
2752
+ _INJECTION_INSPECTION_CACHE[obj] = insp
2753
+ return insp
2197
2754
 
2198
2755
 
2199
2756
  class InjectionKwarg(ta.NamedTuple):
@@ -2214,20 +2771,20 @@ def build_injection_kwargs_target(
2214
2771
  skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
2215
2772
  raw_optional: bool = False,
2216
2773
  ) -> InjectionKwargsTarget:
2217
- sig = _injection_signature(obj)
2774
+ insp = _injection_inspect(obj)
2218
2775
 
2219
2776
  seen: ta.Set[InjectorKey] = set(map(as_injector_key, skip_kwargs)) if skip_kwargs is not None else set()
2220
2777
  kws: ta.List[InjectionKwarg] = []
2221
- for p in list(sig.parameters.values())[skip_args:]:
2778
+ for p in list(insp.signature.parameters.values())[skip_args:]:
2222
2779
  if p.annotation is inspect.Signature.empty:
2223
2780
  if p.default is not inspect.Parameter.empty:
2224
2781
  raise KeyError(f'{obj}, {p.name}')
2225
2782
  continue
2226
2783
 
2227
2784
  if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
2228
- raise TypeError(sig)
2785
+ raise TypeError(insp)
2229
2786
 
2230
- ann = p.annotation
2787
+ ann = insp.type_hints.get(p.name, p.annotation)
2231
2788
  if (
2232
2789
  not raw_optional and
2233
2790
  is_optional_alias(ann)
@@ -2253,38 +2810,98 @@ def build_injection_kwargs_target(
2253
2810
 
2254
2811
 
2255
2812
  ###
2256
- # binder
2813
+ # injector
2257
2814
 
2258
2815
 
2259
- class InjectorBinder:
2260
- def __new__(cls, *args, **kwargs): # noqa
2261
- raise TypeError
2816
+ _INJECTOR_INJECTOR_KEY: InjectorKey[Injector] = InjectorKey(Injector)
2262
2817
 
2263
- _FN_TYPES: ta.Tuple[type, ...] = (
2264
- types.FunctionType,
2265
- types.MethodType,
2266
2818
 
2267
- classmethod,
2268
- staticmethod,
2819
+ class _Injector(Injector):
2820
+ def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
2821
+ super().__init__()
2269
2822
 
2270
- functools.partial,
2271
- functools.partialmethod,
2272
- )
2823
+ self._bs = check_isinstance(bs, InjectorBindings)
2824
+ self._p: ta.Optional[Injector] = check_isinstance(p, (Injector, type(None)))
2273
2825
 
2274
- @classmethod
2275
- def _is_fn(cls, obj: ta.Any) -> bool:
2276
- return isinstance(obj, cls._FN_TYPES)
2826
+ self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
2277
2827
 
2278
- @classmethod
2279
- def bind_as_fn(cls, icls: ta.Type[T]) -> ta.Type[T]:
2280
- check_isinstance(icls, type)
2281
- if icls not in cls._FN_TYPES:
2282
- cls._FN_TYPES = (*cls._FN_TYPES, icls)
2283
- return icls
2828
+ if _INJECTOR_INJECTOR_KEY in self._pfm:
2829
+ raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
2284
2830
 
2285
- _BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
2286
- InjectorProvider,
2287
- )
2831
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
2832
+ key = as_injector_key(key)
2833
+
2834
+ if key == _INJECTOR_INJECTOR_KEY:
2835
+ return Maybe.just(self)
2836
+
2837
+ fn = self._pfm.get(key)
2838
+ if fn is not None:
2839
+ return Maybe.just(fn(self))
2840
+
2841
+ if self._p is not None:
2842
+ pv = self._p.try_provide(key)
2843
+ if pv is not None:
2844
+ return Maybe.empty()
2845
+
2846
+ return Maybe.empty()
2847
+
2848
+ def provide(self, key: ta.Any) -> ta.Any:
2849
+ v = self.try_provide(key)
2850
+ if v.present:
2851
+ return v.must()
2852
+ raise UnboundInjectorKeyError(key)
2853
+
2854
+ def provide_kwargs(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
2855
+ kt = build_injection_kwargs_target(obj)
2856
+ ret: ta.Dict[str, ta.Any] = {}
2857
+ for kw in kt.kwargs:
2858
+ if kw.has_default:
2859
+ if not (mv := self.try_provide(kw.key)).present:
2860
+ continue
2861
+ v = mv.must()
2862
+ else:
2863
+ v = self.provide(kw.key)
2864
+ ret[kw.name] = v
2865
+ return ret
2866
+
2867
+ def inject(self, obj: ta.Any) -> ta.Any:
2868
+ kws = self.provide_kwargs(obj)
2869
+ return obj(**kws)
2870
+
2871
+
2872
+ ###
2873
+ # binder
2874
+
2875
+
2876
+ class InjectorBinder:
2877
+ def __new__(cls, *args, **kwargs): # noqa
2878
+ raise TypeError
2879
+
2880
+ _FN_TYPES: ta.Tuple[type, ...] = (
2881
+ types.FunctionType,
2882
+ types.MethodType,
2883
+
2884
+ classmethod,
2885
+ staticmethod,
2886
+
2887
+ functools.partial,
2888
+ functools.partialmethod,
2889
+ )
2890
+
2891
+ @classmethod
2892
+ def _is_fn(cls, obj: ta.Any) -> bool:
2893
+ return isinstance(obj, cls._FN_TYPES)
2894
+
2895
+ @classmethod
2896
+ def bind_as_fn(cls, icls: ta.Type[T]) -> ta.Type[T]:
2897
+ check_isinstance(icls, type)
2898
+ if icls not in cls._FN_TYPES:
2899
+ cls._FN_TYPES = (*cls._FN_TYPES, icls)
2900
+ return icls
2901
+
2902
+ _BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
2903
+ InjectorProvider,
2904
+ )
2288
2905
 
2289
2906
  @classmethod
2290
2907
  def bind(
@@ -2301,7 +2918,7 @@ class InjectorBinder:
2301
2918
  to_key: ta.Any = None,
2302
2919
 
2303
2920
  singleton: bool = False,
2304
- ) -> InjectorBinding:
2921
+ ) -> InjectorBindingOrBindings:
2305
2922
  if obj is None or obj is inspect.Parameter.empty:
2306
2923
  raise TypeError(obj)
2307
2924
  if isinstance(obj, cls._BANNED_BIND_TYPES):
@@ -2331,9 +2948,9 @@ class InjectorBinder:
2331
2948
  elif cls._is_fn(obj) and not has_to:
2332
2949
  to_fn = obj
2333
2950
  if key is None:
2334
- sig = _injection_signature(obj)
2335
- ty = check_isinstance(sig.return_annotation, type)
2336
- key = InjectorKey(ty)
2951
+ insp = _injection_inspect(obj)
2952
+ key_cls: ta.Any = check_valid_injector_key_cls(check_not_none(insp.type_hints.get('return')))
2953
+ key = InjectorKey(key_cls)
2337
2954
  else:
2338
2955
  if to_const is not None:
2339
2956
  raise TypeError('Cannot bind instance with to_const')
@@ -2384,67 +3001,21 @@ class InjectorBinder:
2384
3001
 
2385
3002
 
2386
3003
  ###
2387
- # injector
2388
-
2389
-
2390
- _INJECTOR_INJECTOR_KEY = InjectorKey(Injector)
2391
-
2392
-
2393
- class _Injector(Injector):
2394
- def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
2395
- super().__init__()
2396
-
2397
- self._bs = check_isinstance(bs, InjectorBindings)
2398
- self._p: ta.Optional[Injector] = check_isinstance(p, (Injector, type(None)))
2399
-
2400
- self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
2401
-
2402
- if _INJECTOR_INJECTOR_KEY in self._pfm:
2403
- raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
2404
-
2405
- def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
2406
- key = as_injector_key(key)
2407
-
2408
- if key == _INJECTOR_INJECTOR_KEY:
2409
- return Maybe.just(self)
2410
-
2411
- fn = self._pfm.get(key)
2412
- if fn is not None:
2413
- return Maybe.just(fn(self))
3004
+ # injection helpers
2414
3005
 
2415
- if self._p is not None:
2416
- pv = self._p.try_provide(key)
2417
- if pv is not None:
2418
- return Maybe.empty()
2419
3006
 
2420
- return Maybe.empty()
3007
+ def make_injector_factory(
3008
+ factory_cls: ta.Any,
3009
+ factory_fn: ta.Callable[..., T],
3010
+ ) -> ta.Callable[..., Func[T]]:
3011
+ def outer(injector: Injector) -> factory_cls:
3012
+ def inner(*args, **kwargs):
3013
+ return injector.inject(functools.partial(factory_fn, *args, **kwargs))
3014
+ return Func(inner)
3015
+ return outer
2421
3016
 
2422
- def provide(self, key: ta.Any) -> ta.Any:
2423
- v = self.try_provide(key)
2424
- if v.present:
2425
- return v.must()
2426
- raise UnboundInjectorKeyError(key)
2427
3017
 
2428
- def provide_kwargs(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
2429
- kt = build_injection_kwargs_target(obj)
2430
- ret: ta.Dict[str, ta.Any] = {}
2431
- for kw in kt.kwargs:
2432
- if kw.has_default:
2433
- if not (mv := self.try_provide(kw.key)).present:
2434
- continue
2435
- v = mv.must()
2436
- else:
2437
- v = self.provide(kw.key)
2438
- ret[kw.name] = v
2439
- return ret
2440
-
2441
- def inject(self, obj: ta.Any) -> ta.Any:
2442
- kws = self.provide_kwargs(obj)
2443
- return obj(**kws)
2444
-
2445
-
2446
- ###
2447
- # injection helpers
3018
+ ##
2448
3019
 
2449
3020
 
2450
3021
  class Injection:
@@ -2475,6 +3046,12 @@ class Injection:
2475
3046
  def override(cls, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
2476
3047
  return injector_override(p, *args)
2477
3048
 
3049
+ # injector
3050
+
3051
+ @classmethod
3052
+ def create_injector(cls, *args: InjectorBindingOrBindings, p: ta.Optional[Injector] = None) -> Injector:
3053
+ return _Injector(as_injector_bindings(*args), p)
3054
+
2478
3055
  # binder
2479
3056
 
2480
3057
  @classmethod
@@ -2492,7 +3069,7 @@ class Injection:
2492
3069
  to_key: ta.Any = None,
2493
3070
 
2494
3071
  singleton: bool = False,
2495
- ) -> InjectorBinding:
3072
+ ) -> InjectorBindingOrBindings:
2496
3073
  return InjectorBinder.bind(
2497
3074
  obj,
2498
3075
 
@@ -2508,11 +3085,15 @@ class Injection:
2508
3085
  singleton=singleton,
2509
3086
  )
2510
3087
 
2511
- # injector
3088
+ # helpers
2512
3089
 
2513
3090
  @classmethod
2514
- def create_injector(cls, *args: InjectorBindingOrBindings, p: ta.Optional[Injector] = None) -> Injector:
2515
- return _Injector(as_injector_bindings(*args), p)
3091
+ def bind_factory(
3092
+ cls,
3093
+ factory_cls: ta.Any,
3094
+ factory_fn: ta.Callable[..., T],
3095
+ ) -> InjectorBindingOrBindings:
3096
+ return cls.bind(make_injector_factory(factory_cls, factory_fn))
2516
3097
 
2517
3098
 
2518
3099
  inj = Injection
@@ -2542,7 +3123,6 @@ sd_iovec._fields_ = [
2542
3123
  def sd_libsystemd() -> ta.Any:
2543
3124
  lib = ct.CDLL('libsystemd.so.0')
2544
3125
 
2545
- lib.sd_journal_sendv = lib['sd_journal_sendv'] # type: ignore
2546
3126
  lib.sd_journal_sendv.restype = ct.c_int
2547
3127
  lib.sd_journal_sendv.argtypes = [ct.POINTER(sd_iovec), ct.c_int]
2548
3128
 
@@ -3582,6 +4162,36 @@ def get_poller_impl() -> ta.Type[Poller]:
3582
4162
  return SelectPoller
3583
4163
 
3584
4164
 
4165
+ ########################################
4166
+ # ../../../omlish/lite/http/handlers.py
4167
+
4168
+
4169
+ @dc.dataclass(frozen=True)
4170
+ class HttpHandlerRequest:
4171
+ client_address: SocketAddress
4172
+ method: str
4173
+ path: str
4174
+ headers: HttpHeaders
4175
+ data: ta.Optional[bytes]
4176
+
4177
+
4178
+ @dc.dataclass(frozen=True)
4179
+ class HttpHandlerResponse:
4180
+ status: ta.Union[http.HTTPStatus, int]
4181
+
4182
+ headers: ta.Optional[ta.Mapping[str, str]] = None
4183
+ data: ta.Optional[bytes] = None
4184
+ close_connection: ta.Optional[bool] = None
4185
+
4186
+
4187
+ class HttpHandlerError(Exception):
4188
+ pass
4189
+
4190
+
4191
+ class UnsupportedMethodHttpHandlerError(Exception):
4192
+ pass
4193
+
4194
+
3585
4195
  ########################################
3586
4196
  # ../configs.py
3587
4197
 
@@ -3703,11 +4313,571 @@ def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.An
3703
4313
  return out
3704
4314
 
3705
4315
 
4316
+ ########################################
4317
+ # ../../../omlish/lite/http/coroserver.py
4318
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
4319
+ # --------------------------------------------
4320
+ #
4321
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
4322
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
4323
+ # documentation.
4324
+ #
4325
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
4326
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
4327
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
4328
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
4329
+ # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; All Rights Reserved" are retained in Python
4330
+ # alone or in any derivative version prepared by Licensee.
4331
+ #
4332
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
4333
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
4334
+ # any such work a brief summary of the changes made to Python.
4335
+ #
4336
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
4337
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
4338
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
4339
+ # RIGHTS.
4340
+ #
4341
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
4342
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
4343
+ # ADVISED OF THE POSSIBILITY THEREOF.
4344
+ #
4345
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
4346
+ #
4347
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
4348
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
4349
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
4350
+ #
4351
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
4352
+ # License Agreement.
4353
+ """
4354
+ "Test suite" lol:
4355
+
4356
+ curl -v localhost:8000
4357
+ curl -v localhost:8000 -d 'foo'
4358
+ curl -v -XFOO localhost:8000 -d 'foo'
4359
+ curl -v -XPOST -H 'Expect: 100-Continue' localhost:8000 -d 'foo'
4360
+
4361
+ curl -v -0 localhost:8000
4362
+ curl -v -0 localhost:8000 -d 'foo'
4363
+ curl -v -0 -XFOO localhost:8000 -d 'foo'
4364
+
4365
+ curl -v -XPOST localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
4366
+ curl -v -XPOST localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
4367
+ curl -v -XFOO localhost:8000 -d 'foo' --next -XPOST localhost:8000 -d 'bar'
4368
+ curl -v -XFOO localhost:8000 -d 'foo' --next -XFOO localhost:8000 -d 'bar'
4369
+ """
4370
+
4371
+
4372
+ ##
4373
+
4374
+
4375
+ class CoroHttpServer:
4376
+ """
4377
+ Adapted from stdlib:
4378
+ - https://github.com/python/cpython/blob/4b4e0dbdf49adc91c35a357ad332ab3abd4c31b1/Lib/http/server.py#L146
4379
+ """
4380
+
4381
+ #
4382
+
4383
+ def __init__(
4384
+ self,
4385
+ client_address: SocketAddress,
4386
+ *,
4387
+ handler: HttpHandler,
4388
+ parser: HttpRequestParser = HttpRequestParser(),
4389
+
4390
+ default_content_type: ta.Optional[str] = None,
4391
+
4392
+ error_message_format: ta.Optional[str] = None,
4393
+ error_content_type: ta.Optional[str] = None,
4394
+ ) -> None:
4395
+ super().__init__()
4396
+
4397
+ self._client_address = client_address
4398
+
4399
+ self._handler = handler
4400
+ self._parser = parser
4401
+
4402
+ self._default_content_type = default_content_type or self.DEFAULT_CONTENT_TYPE
4403
+
4404
+ self._error_message_format = error_message_format or self.DEFAULT_ERROR_MESSAGE
4405
+ self._error_content_type = error_content_type or self.DEFAULT_ERROR_CONTENT_TYPE
4406
+
4407
+ #
4408
+
4409
+ @property
4410
+ def client_address(self) -> SocketAddress:
4411
+ return self._client_address
4412
+
4413
+ @property
4414
+ def handler(self) -> HttpHandler:
4415
+ return self._handler
4416
+
4417
+ @property
4418
+ def parser(self) -> HttpRequestParser:
4419
+ return self._parser
4420
+
4421
+ #
4422
+
4423
+ def _format_timestamp(self, timestamp: ta.Optional[float] = None) -> str:
4424
+ if timestamp is None:
4425
+ timestamp = time.time()
4426
+ return email.utils.formatdate(timestamp, usegmt=True)
4427
+
4428
+ #
4429
+
4430
+ def _header_encode(self, s: str) -> bytes:
4431
+ return s.encode('latin-1', 'strict')
4432
+
4433
+ class _Header(ta.NamedTuple):
4434
+ key: str
4435
+ value: str
4436
+
4437
+ def _format_header_line(self, h: _Header) -> str:
4438
+ return f'{h.key}: {h.value}\r\n'
4439
+
4440
+ def _get_header_close_connection_action(self, h: _Header) -> ta.Optional[bool]:
4441
+ if h.key.lower() != 'connection':
4442
+ return None
4443
+ elif h.value.lower() == 'close':
4444
+ return True
4445
+ elif h.value.lower() == 'keep-alive':
4446
+ return False
4447
+ else:
4448
+ return None
4449
+
4450
+ def _make_default_headers(self) -> ta.List[_Header]:
4451
+ return [
4452
+ self._Header('Date', self._format_timestamp()),
4453
+ ]
4454
+
4455
+ #
4456
+
4457
+ _STATUS_RESPONSES: ta.Mapping[int, ta.Tuple[str, str]] = {
4458
+ v: (v.phrase, v.description)
4459
+ for v in http.HTTPStatus.__members__.values()
4460
+ }
4461
+
4462
+ def _format_status_line(
4463
+ self,
4464
+ version: HttpProtocolVersion,
4465
+ code: ta.Union[http.HTTPStatus, int],
4466
+ message: ta.Optional[str] = None,
4467
+ ) -> str:
4468
+ if message is None:
4469
+ if code in self._STATUS_RESPONSES:
4470
+ message = self._STATUS_RESPONSES[code][0]
4471
+ else:
4472
+ message = ''
4473
+
4474
+ return f'{version} {int(code)} {message}\r\n'
4475
+
4476
+ #
4477
+
4478
+ @dc.dataclass(frozen=True)
4479
+ class _Response:
4480
+ version: HttpProtocolVersion
4481
+ code: http.HTTPStatus
4482
+
4483
+ message: ta.Optional[str] = None
4484
+ headers: ta.Optional[ta.Sequence['CoroHttpServer._Header']] = None
4485
+ data: ta.Optional[bytes] = None
4486
+ close_connection: ta.Optional[bool] = False
4487
+
4488
+ def get_header(self, key: str) -> ta.Optional['CoroHttpServer._Header']:
4489
+ for h in self.headers or []:
4490
+ if h.key.lower() == key.lower():
4491
+ return h
4492
+ return None
4493
+
4494
+ #
4495
+
4496
+ def _build_response_bytes(self, a: _Response) -> bytes:
4497
+ out = io.BytesIO()
4498
+
4499
+ if a.version >= HttpProtocolVersions.HTTP_1_0:
4500
+ out.write(self._header_encode(self._format_status_line(
4501
+ a.version,
4502
+ a.code,
4503
+ a.message,
4504
+ )))
4505
+
4506
+ for h in a.headers or []:
4507
+ out.write(self._header_encode(self._format_header_line(h)))
4508
+
4509
+ out.write(b'\r\n')
4510
+
4511
+ if a.data is not None:
4512
+ out.write(a.data)
4513
+
4514
+ return out.getvalue()
4515
+
4516
+ #
4517
+
4518
+ DEFAULT_CONTENT_TYPE = 'text/plain'
4519
+
4520
+ def _preprocess_response(self, resp: _Response) -> _Response:
4521
+ nh: ta.List[CoroHttpServer._Header] = []
4522
+ kw: ta.Dict[str, ta.Any] = {}
4523
+
4524
+ if resp.get_header('Content-Type') is None:
4525
+ nh.append(self._Header('Content-Type', self._default_content_type))
4526
+ if resp.data is not None and resp.get_header('Content-Length') is None:
4527
+ nh.append(self._Header('Content-Length', str(len(resp.data))))
4528
+
4529
+ if nh:
4530
+ kw.update(headers=[*(resp.headers or []), *nh])
4531
+
4532
+ if (clh := resp.get_header('Connection')) is not None:
4533
+ if self._get_header_close_connection_action(clh):
4534
+ kw.update(close_connection=True)
4535
+
4536
+ if not kw:
4537
+ return resp
4538
+ return dc.replace(resp, **kw)
4539
+
4540
+ #
4541
+
4542
+ @dc.dataclass(frozen=True)
4543
+ class Error:
4544
+ version: HttpProtocolVersion
4545
+ code: http.HTTPStatus
4546
+ message: str
4547
+ explain: str
4548
+
4549
+ method: ta.Optional[str] = None
4550
+
4551
+ def _build_error(
4552
+ self,
4553
+ code: ta.Union[http.HTTPStatus, int],
4554
+ message: ta.Optional[str] = None,
4555
+ explain: ta.Optional[str] = None,
4556
+ *,
4557
+ version: ta.Optional[HttpProtocolVersion] = None,
4558
+ method: ta.Optional[str] = None,
4559
+ ) -> Error:
4560
+ code = http.HTTPStatus(code)
4561
+
4562
+ try:
4563
+ short_msg, long_msg = self._STATUS_RESPONSES[code]
4564
+ except KeyError:
4565
+ short_msg, long_msg = '???', '???'
4566
+ if message is None:
4567
+ message = short_msg
4568
+ if explain is None:
4569
+ explain = long_msg
4570
+
4571
+ if version is None:
4572
+ version = self._parser.server_version
4573
+
4574
+ return self.Error(
4575
+ version=version,
4576
+ code=code,
4577
+ message=message,
4578
+ explain=explain,
4579
+
4580
+ method=method,
4581
+ )
4582
+
4583
+ #
4584
+
4585
+ DEFAULT_ERROR_MESSAGE = textwrap.dedent("""\
4586
+ <!DOCTYPE HTML>
4587
+ <html lang="en">
4588
+ <head>
4589
+ <meta charset="utf-8">
4590
+ <title>Error response</title>
4591
+ </head>
4592
+ <body>
4593
+ <h1>Error response</h1>
4594
+ <p>Error code: %(code)d</p>
4595
+ <p>Message: %(message)s.</p>
4596
+ <p>Error code explanation: %(code)s - %(explain)s.</p>
4597
+ </body>
4598
+ </html>
4599
+ """)
4600
+
4601
+ DEFAULT_ERROR_CONTENT_TYPE = 'text/html;charset=utf-8'
4602
+
4603
+ def _build_error_response(self, err: Error) -> _Response:
4604
+ headers: ta.List[CoroHttpServer._Header] = [
4605
+ *self._make_default_headers(),
4606
+ self._Header('Connection', 'close'),
4607
+ ]
4608
+
4609
+ # Message body is omitted for cases described in:
4610
+ # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified)
4611
+ # - RFC7231: 6.3.6. 205(Reset Content)
4612
+ data: ta.Optional[bytes] = None
4613
+ if (
4614
+ err.code >= http.HTTPStatus.OK and
4615
+ err.code not in (
4616
+ http.HTTPStatus.NO_CONTENT,
4617
+ http.HTTPStatus.RESET_CONTENT,
4618
+ http.HTTPStatus.NOT_MODIFIED,
4619
+ )
4620
+ ):
4621
+ # HTML encode to prevent Cross Site Scripting attacks (see bug #1100201)
4622
+ content = self._error_message_format.format(
4623
+ code=err.code,
4624
+ message=html.escape(err.message, quote=False),
4625
+ explain=html.escape(err.explain, quote=False),
4626
+ )
4627
+ body = content.encode('UTF-8', 'replace')
4628
+
4629
+ headers.extend([
4630
+ self._Header('Content-Type', self._error_content_type),
4631
+ self._Header('Content-Length', str(len(body))),
4632
+ ])
4633
+
4634
+ if err.method != 'HEAD' and body:
4635
+ data = body
4636
+
4637
+ return self._Response(
4638
+ version=err.version,
4639
+ code=err.code,
4640
+ message=err.message,
4641
+ headers=headers,
4642
+ data=data,
4643
+ close_connection=True,
4644
+ )
4645
+
4646
+ #
4647
+
4648
+ class Io(abc.ABC): # noqa
4649
+ pass
4650
+
4651
+ #
4652
+
4653
+ class AnyLogIo(Io):
4654
+ pass
4655
+
4656
+ @dc.dataclass(frozen=True)
4657
+ class ParsedRequestLogIo(AnyLogIo):
4658
+ request: ParsedHttpRequest
4659
+
4660
+ @dc.dataclass(frozen=True)
4661
+ class ErrorLogIo(AnyLogIo):
4662
+ error: 'CoroHttpServer.Error'
4663
+
4664
+ #
4665
+
4666
+ class AnyReadIo(Io): # noqa
4667
+ pass
4668
+
4669
+ @dc.dataclass(frozen=True)
4670
+ class ReadIo(AnyReadIo):
4671
+ sz: int
4672
+
4673
+ @dc.dataclass(frozen=True)
4674
+ class ReadLineIo(AnyReadIo):
4675
+ sz: int
4676
+
4677
+ #
4678
+
4679
+ @dc.dataclass(frozen=True)
4680
+ class WriteIo(Io):
4681
+ data: bytes
4682
+
4683
+ #
4684
+
4685
+ def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
4686
+ while True:
4687
+ gen = self.coro_handle_one()
4688
+
4689
+ o = next(gen)
4690
+ i: ta.Optional[bytes]
4691
+ while True:
4692
+ if isinstance(o, self.AnyLogIo):
4693
+ i = None
4694
+ yield o
4695
+
4696
+ elif isinstance(o, self.AnyReadIo):
4697
+ i = check_isinstance((yield o), bytes)
4698
+
4699
+ elif isinstance(o, self._Response):
4700
+ i = None
4701
+ r = self._preprocess_response(o)
4702
+ b = self._build_response_bytes(r)
4703
+ check_none((yield self.WriteIo(b)))
4704
+
4705
+ else:
4706
+ raise TypeError(o)
4707
+
4708
+ try:
4709
+ o = gen.send(i)
4710
+ except EOFError:
4711
+ return
4712
+ except StopIteration:
4713
+ break
4714
+
4715
+ def coro_handle_one(self) -> ta.Generator[
4716
+ ta.Union[AnyLogIo, AnyReadIo, _Response],
4717
+ ta.Optional[bytes],
4718
+ None,
4719
+ ]:
4720
+ # Parse request
4721
+
4722
+ gen = self._parser.coro_parse()
4723
+ sz = next(gen)
4724
+ while True:
4725
+ try:
4726
+ line = check_isinstance((yield self.ReadLineIo(sz)), bytes)
4727
+ sz = gen.send(line)
4728
+ except StopIteration as e:
4729
+ parsed = e.value
4730
+ break
4731
+
4732
+ if isinstance(parsed, EmptyParsedHttpResult):
4733
+ raise EOFError # noqa
4734
+
4735
+ if isinstance(parsed, ParseHttpRequestError):
4736
+ err = self._build_error(
4737
+ parsed.code,
4738
+ *parsed.message,
4739
+ version=parsed.version,
4740
+ )
4741
+ yield self.ErrorLogIo(err)
4742
+ yield self._build_error_response(err)
4743
+ return
4744
+
4745
+ parsed = check_isinstance(parsed, ParsedHttpRequest)
4746
+
4747
+ # Log
4748
+
4749
+ check_none((yield self.ParsedRequestLogIo(parsed)))
4750
+
4751
+ # Handle CONTINUE
4752
+
4753
+ if parsed.expects_continue:
4754
+ # https://bugs.python.org/issue1491
4755
+ # https://github.com/python/cpython/commit/0f476d49f8d4aa84210392bf13b59afc67b32b31
4756
+ yield self._Response(
4757
+ version=parsed.version,
4758
+ code=http.HTTPStatus.CONTINUE,
4759
+ )
4760
+
4761
+ # Read data
4762
+
4763
+ request_data: ta.Optional[bytes]
4764
+ if (cl := parsed.headers.get('Content-Length')) is not None:
4765
+ request_data = check_isinstance((yield self.ReadIo(int(cl))), bytes)
4766
+ else:
4767
+ request_data = None
4768
+
4769
+ # Build request
4770
+
4771
+ handler_request = HttpHandlerRequest(
4772
+ client_address=self._client_address,
4773
+ method=check_not_none(parsed.method),
4774
+ path=parsed.path,
4775
+ headers=parsed.headers,
4776
+ data=request_data,
4777
+ )
4778
+
4779
+ # Build handler response
4780
+
4781
+ try:
4782
+ handler_response = self._handler(handler_request)
4783
+
4784
+ except UnsupportedMethodHttpHandlerError:
4785
+ err = self._build_error(
4786
+ http.HTTPStatus.NOT_IMPLEMENTED,
4787
+ f'Unsupported method ({parsed.method!r})',
4788
+ version=parsed.version,
4789
+ method=parsed.method,
4790
+ )
4791
+ yield self.ErrorLogIo(err)
4792
+ yield self._build_error_response(err)
4793
+ return
4794
+
4795
+ # Build internal response
4796
+
4797
+ response_headers = handler_response.headers or {}
4798
+ response_data = handler_response.data
4799
+
4800
+ headers: ta.List[CoroHttpServer._Header] = [
4801
+ *self._make_default_headers(),
4802
+ ]
4803
+
4804
+ for k, v in response_headers.items():
4805
+ headers.append(self._Header(k, v))
4806
+
4807
+ if handler_response.close_connection and 'Connection' not in headers:
4808
+ headers.append(self._Header('Connection', 'close'))
4809
+
4810
+ yield self._Response(
4811
+ version=parsed.version,
4812
+ code=http.HTTPStatus(handler_response.status),
4813
+ headers=headers,
4814
+ data=response_data,
4815
+ close_connection=handler_response.close_connection,
4816
+ )
4817
+
4818
+
4819
+ ##
4820
+
4821
+
4822
+ class CoroHttpServerSocketHandler(SocketHandler):
4823
+ def __init__(
4824
+ self,
4825
+ client_address: SocketAddress,
4826
+ rfile: ta.BinaryIO,
4827
+ wfile: ta.BinaryIO,
4828
+ *,
4829
+ server_factory: CoroHttpServerFactory,
4830
+ log_handler: ta.Optional[ta.Callable[[CoroHttpServer, CoroHttpServer.AnyLogIo], None]] = None,
4831
+ ) -> None:
4832
+ super().__init__(
4833
+ client_address,
4834
+ rfile,
4835
+ wfile,
4836
+ )
4837
+
4838
+ self._server_factory = server_factory
4839
+ self._log_handler = log_handler
4840
+
4841
+ def handle(self) -> None:
4842
+ server = self._server_factory(self._client_address)
4843
+
4844
+ gen = server.coro_handle()
4845
+
4846
+ o = next(gen)
4847
+ while True:
4848
+ if isinstance(o, CoroHttpServer.AnyLogIo):
4849
+ i = None
4850
+ if self._log_handler is not None:
4851
+ self._log_handler(server, o)
4852
+
4853
+ elif isinstance(o, CoroHttpServer.ReadIo):
4854
+ i = self._rfile.read(o.sz)
4855
+
4856
+ elif isinstance(o, CoroHttpServer.ReadLineIo):
4857
+ i = self._rfile.readline(o.sz)
4858
+
4859
+ elif isinstance(o, CoroHttpServer.WriteIo):
4860
+ i = None
4861
+ self._wfile.write(o.data)
4862
+ self._wfile.flush()
4863
+
4864
+ else:
4865
+ raise TypeError(o)
4866
+
4867
+ try:
4868
+ if i is not None:
4869
+ o = gen.send(i)
4870
+ else:
4871
+ o = next(gen)
4872
+ except StopIteration:
4873
+ break
4874
+
4875
+
3706
4876
  ########################################
3707
4877
  # ../types.py
3708
4878
 
3709
4879
 
3710
- class AbstractServerContext(abc.ABC):
4880
+ class ServerContext(abc.ABC):
3711
4881
  @property
3712
4882
  @abc.abstractmethod
3713
4883
  def config(self) -> ServerConfig:
@@ -3724,12 +4894,24 @@ class AbstractServerContext(abc.ABC):
3724
4894
 
3725
4895
  @property
3726
4896
  @abc.abstractmethod
3727
- def pid_history(self) -> ta.Dict[int, 'AbstractSubprocess']:
4897
+ def pid_history(self) -> ta.Dict[int, 'Process']:
3728
4898
  raise NotImplementedError
3729
4899
 
3730
4900
 
4901
+ # class Dispatcher(abc.ABC):
4902
+ # pass
4903
+ #
4904
+ #
4905
+ # class OutputDispatcher(Dispatcher, abc.ABC):
4906
+ # pass
4907
+ #
4908
+ #
4909
+ # class InputDispatcher(Dispatcher, abc.ABC):
4910
+ # pass
4911
+
4912
+
3731
4913
  @functools.total_ordering
3732
- class AbstractSubprocess(abc.ABC):
4914
+ class Process(abc.ABC):
3733
4915
  @property
3734
4916
  @abc.abstractmethod
3735
4917
  def pid(self) -> int:
@@ -3748,7 +4930,7 @@ class AbstractSubprocess(abc.ABC):
3748
4930
 
3749
4931
  @property
3750
4932
  @abc.abstractmethod
3751
- def context(self) -> AbstractServerContext:
4933
+ def context(self) -> ServerContext:
3752
4934
  raise NotImplementedError
3753
4935
 
3754
4936
  @abc.abstractmethod
@@ -3784,12 +4966,12 @@ class AbstractSubprocess(abc.ABC):
3784
4966
  raise NotImplementedError
3785
4967
 
3786
4968
  @abc.abstractmethod
3787
- def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # dict[int, Dispatcher]
4969
+ def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # Dispatcher]:
3788
4970
  raise NotImplementedError
3789
4971
 
3790
4972
 
3791
4973
  @functools.total_ordering
3792
- class AbstractProcessGroup(abc.ABC):
4974
+ class ProcessGroup(abc.ABC):
3793
4975
  @property
3794
4976
  @abc.abstractmethod
3795
4977
  def config(self) -> ProcessGroupConfig:
@@ -3801,12 +4983,48 @@ class AbstractProcessGroup(abc.ABC):
3801
4983
  def __eq__(self, other):
3802
4984
  return self.config.priority == other.config.priority
3803
4985
 
4986
+ @abc.abstractmethod
4987
+ def transition(self) -> None:
4988
+ raise NotImplementedError
4989
+
4990
+ @abc.abstractmethod
4991
+ def stop_all(self) -> None:
4992
+ raise NotImplementedError
4993
+
4994
+ @property
4995
+ @abc.abstractmethod
4996
+ def name(self) -> str:
4997
+ raise NotImplementedError
4998
+
4999
+ @abc.abstractmethod
5000
+ def before_remove(self) -> None:
5001
+ raise NotImplementedError
5002
+
5003
+ @abc.abstractmethod
5004
+ def get_dispatchers(self) -> ta.Mapping[int, ta.Any]: # Dispatcher]:
5005
+ raise NotImplementedError
5006
+
5007
+ @abc.abstractmethod
5008
+ def reopen_logs(self) -> None:
5009
+ raise NotImplementedError
5010
+
5011
+ @abc.abstractmethod
5012
+ def get_unstopped_processes(self) -> ta.List[Process]:
5013
+ raise NotImplementedError
5014
+
5015
+ @abc.abstractmethod
5016
+ def after_setuid(self) -> None:
5017
+ raise NotImplementedError
5018
+
3804
5019
 
3805
5020
  ########################################
3806
5021
  # ../context.py
3807
5022
 
3808
5023
 
3809
- class ServerContext(AbstractServerContext):
5024
+ ServerEpoch = ta.NewType('ServerEpoch', int)
5025
+
5026
+
5027
+ class ServerContextImpl(ServerContext):
3810
5028
  def __init__(
3811
5029
  self,
3812
5030
  config: ServerConfig,
@@ -3820,7 +5038,7 @@ class ServerContext(AbstractServerContext):
3820
5038
  self._poller = poller
3821
5039
  self._epoch = epoch
3822
5040
 
3823
- self._pid_history: ta.Dict[int, AbstractSubprocess] = {}
5041
+ self._pid_history: ta.Dict[int, Process] = {}
3824
5042
  self._state: SupervisorState = SupervisorState.RUNNING
3825
5043
 
3826
5044
  if config.user is not None:
@@ -3853,7 +5071,7 @@ class ServerContext(AbstractServerContext):
3853
5071
  self._state = state
3854
5072
 
3855
5073
  @property
3856
- def pid_history(self) -> ta.Dict[int, AbstractSubprocess]:
5074
+ def pid_history(self) -> ta.Dict[int, Process]:
3857
5075
  return self._pid_history
3858
5076
 
3859
5077
  @property
@@ -4200,7 +5418,7 @@ def check_execv_args(filename, argv, st) -> None:
4200
5418
  class Dispatcher(abc.ABC):
4201
5419
  def __init__(
4202
5420
  self,
4203
- process: AbstractSubprocess,
5421
+ process: Process,
4204
5422
  channel: str,
4205
5423
  fd: int,
4206
5424
  *,
@@ -4219,7 +5437,7 @@ class Dispatcher(abc.ABC):
4219
5437
  return f'<{self.__class__.__name__} at {id(self)} for {self._process} ({self._channel})>'
4220
5438
 
4221
5439
  @property
4222
- def process(self) -> AbstractSubprocess:
5440
+ def process(self) -> Process:
4223
5441
  return self._process
4224
5442
 
4225
5443
  @property
@@ -4274,7 +5492,7 @@ class OutputDispatcher(Dispatcher):
4274
5492
 
4275
5493
  def __init__(
4276
5494
  self,
4277
- process: AbstractSubprocess,
5495
+ process: Process,
4278
5496
  event_type: ta.Type[ProcessCommunicationEvent],
4279
5497
  fd: int,
4280
5498
  **kwargs: ta.Any,
@@ -4483,7 +5701,7 @@ class OutputDispatcher(Dispatcher):
4483
5701
  class InputDispatcher(Dispatcher):
4484
5702
  def __init__(
4485
5703
  self,
4486
- process: AbstractSubprocess,
5704
+ process: Process,
4487
5705
  channel: str,
4488
5706
  fd: int,
4489
5707
  **kwargs: ta.Any,
@@ -4532,31 +5750,26 @@ class InputDispatcher(Dispatcher):
4532
5750
  ##
4533
5751
 
4534
5752
 
4535
- @dc.dataclass(frozen=True)
4536
- class SubprocessFactory:
4537
- fn: ta.Callable[[ProcessConfig, AbstractProcessGroup], AbstractSubprocess]
4538
-
4539
- def __call__(self, config: ProcessConfig, group: AbstractProcessGroup) -> AbstractSubprocess:
4540
- return self.fn(config, group)
5753
+ ProcessFactory = ta.NewType('ProcessFactory', Func[Process]) # (config: ProcessConfig, group: ProcessGroup)
4541
5754
 
4542
5755
 
4543
- class ProcessGroup(AbstractProcessGroup):
5756
+ class ProcessGroupImpl(ProcessGroup):
4544
5757
  def __init__(
4545
5758
  self,
4546
5759
  config: ProcessGroupConfig,
4547
5760
  context: ServerContext,
4548
5761
  *,
4549
- subprocess_factory: SubprocessFactory,
5762
+ process_factory: ProcessFactory,
4550
5763
  ):
4551
5764
  super().__init__()
4552
5765
 
4553
5766
  self._config = config
4554
5767
  self._context = context
4555
- self._subprocess_factory = subprocess_factory
5768
+ self._process_factory = process_factory
4556
5769
 
4557
5770
  self._processes = {}
4558
5771
  for pconfig in self._config.processes or []:
4559
- process = self._subprocess_factory(pconfig, self)
5772
+ process = self._process_factory(pconfig, self)
4560
5773
  self._processes[pconfig.name] = process
4561
5774
 
4562
5775
  @property
@@ -4568,7 +5781,7 @@ class ProcessGroup(AbstractProcessGroup):
4568
5781
  return self._config.name
4569
5782
 
4570
5783
  @property
4571
- def context(self) -> AbstractServerContext:
5784
+ def context(self) -> ServerContext:
4572
5785
  return self._context
4573
5786
 
4574
5787
  def __repr__(self):
@@ -4603,7 +5816,7 @@ class ProcessGroup(AbstractProcessGroup):
4603
5816
  # BACKOFF -> FATAL
4604
5817
  proc.give_up()
4605
5818
 
4606
- def get_unstopped_processes(self) -> ta.List[AbstractSubprocess]:
5819
+ def get_unstopped_processes(self) -> ta.List[Process]:
4607
5820
  return [x for x in self._processes.values() if not x.get_state().stopped]
4608
5821
 
4609
5822
  def get_dispatchers(self) -> ta.Dict[int, Dispatcher]:
@@ -4680,18 +5893,21 @@ class ProcessGroups:
4680
5893
  # ../process.py
4681
5894
 
4682
5895
 
5896
+ InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
5897
+
5898
+
4683
5899
  ##
4684
5900
 
4685
5901
 
4686
- class Subprocess(AbstractSubprocess):
5902
+ class ProcessImpl(Process):
4687
5903
  """A class to manage a subprocess."""
4688
5904
 
4689
5905
  def __init__(
4690
5906
  self,
4691
5907
  config: ProcessConfig,
4692
- group: AbstractProcessGroup,
5908
+ group: ProcessGroup,
4693
5909
  *,
4694
- context: AbstractServerContext,
5910
+ context: ServerContext,
4695
5911
  event_callbacks: EventCallbacks,
4696
5912
 
4697
5913
  inherited_fds: ta.Optional[InheritedFds] = None,
@@ -4730,7 +5946,7 @@ class Subprocess(AbstractSubprocess):
4730
5946
  return self._pid
4731
5947
 
4732
5948
  @property
4733
- def group(self) -> AbstractProcessGroup:
5949
+ def group(self) -> ProcessGroup:
4734
5950
  return self._group
4735
5951
 
4736
5952
  @property
@@ -4738,7 +5954,7 @@ class Subprocess(AbstractSubprocess):
4738
5954
  return self._config
4739
5955
 
4740
5956
  @property
4741
- def context(self) -> AbstractServerContext:
5957
+ def context(self) -> ServerContext:
4742
5958
  return self._context
4743
5959
 
4744
5960
  @property
@@ -5385,7 +6601,7 @@ class SignalHandler:
5385
6601
  def __init__(
5386
6602
  self,
5387
6603
  *,
5388
- context: ServerContext,
6604
+ context: ServerContextImpl,
5389
6605
  signal_receiver: SignalReceiver,
5390
6606
  process_groups: ProcessGroups,
5391
6607
  ) -> None:
@@ -5437,19 +6653,14 @@ class SignalHandler:
5437
6653
  ##
5438
6654
 
5439
6655
 
5440
- @dc.dataclass(frozen=True)
5441
- class ProcessGroupFactory:
5442
- fn: ta.Callable[[ProcessGroupConfig], ProcessGroup]
5443
-
5444
- def __call__(self, config: ProcessGroupConfig) -> ProcessGroup:
5445
- return self.fn(config)
6656
+ ProcessGroupFactory = ta.NewType('ProcessGroupFactory', Func[ProcessGroup]) # (config: ProcessGroupConfig)
5446
6657
 
5447
6658
 
5448
6659
  class Supervisor:
5449
6660
  def __init__(
5450
6661
  self,
5451
6662
  *,
5452
- context: ServerContext,
6663
+ context: ServerContextImpl,
5453
6664
  poller: Poller,
5454
6665
  process_groups: ProcessGroups,
5455
6666
  signal_handler: SignalHandler,
@@ -5473,7 +6684,7 @@ class Supervisor:
5473
6684
  #
5474
6685
 
5475
6686
  @property
5476
- def context(self) -> ServerContext:
6687
+ def context(self) -> ServerContextImpl:
5477
6688
  return self._context
5478
6689
 
5479
6690
  def get_state(self) -> SupervisorState:
@@ -5520,16 +6731,16 @@ class Supervisor:
5520
6731
  return True
5521
6732
 
5522
6733
  def get_process_map(self) -> ta.Dict[int, Dispatcher]:
5523
- process_map = {}
6734
+ process_map: ta.Dict[int, Dispatcher] = {}
5524
6735
  for group in self._process_groups:
5525
6736
  process_map.update(group.get_dispatchers())
5526
6737
  return process_map
5527
6738
 
5528
- def shutdown_report(self) -> ta.List[Subprocess]:
5529
- unstopped: ta.List[Subprocess] = []
6739
+ def shutdown_report(self) -> ta.List[Process]:
6740
+ unstopped: ta.List[Process] = []
5530
6741
 
5531
6742
  for group in self._process_groups:
5532
- unstopped.extend(group.get_unstopped_processes()) # type: ignore
6743
+ unstopped.extend(group.get_unstopped_processes())
5533
6744
 
5534
6745
  if unstopped:
5535
6746
  # throttle 'waiting for x to die' reports
@@ -5740,13 +6951,13 @@ class Supervisor:
5740
6951
 
5741
6952
 
5742
6953
  ########################################
5743
- # main.py
6954
+ # ../inject.py
5744
6955
 
5745
6956
 
5746
6957
  ##
5747
6958
 
5748
6959
 
5749
- def build_server_bindings(
6960
+ def bind_server(
5750
6961
  config: ServerConfig,
5751
6962
  *,
5752
6963
  server_epoch: ta.Optional[ServerEpoch] = None,
@@ -5757,8 +6968,8 @@ def build_server_bindings(
5757
6968
 
5758
6969
  inj.bind(get_poller_impl(), key=Poller, singleton=True),
5759
6970
 
5760
- inj.bind(ServerContext, singleton=True),
5761
- inj.bind(AbstractServerContext, to_key=ServerContext),
6971
+ inj.bind(ServerContextImpl, singleton=True),
6972
+ inj.bind(ServerContext, to_key=ServerContextImpl),
5762
6973
 
5763
6974
  inj.bind(EventCallbacks, singleton=True),
5764
6975
 
@@ -5767,21 +6978,10 @@ def build_server_bindings(
5767
6978
  inj.bind(SignalHandler, singleton=True),
5768
6979
  inj.bind(ProcessGroups, singleton=True),
5769
6980
  inj.bind(Supervisor, singleton=True),
5770
- ]
5771
-
5772
- #
5773
6981
 
5774
- def make_process_group_factory(injector: Injector) -> ProcessGroupFactory:
5775
- def inner(group_config: ProcessGroupConfig) -> ProcessGroup:
5776
- return injector.inject(functools.partial(ProcessGroup, group_config))
5777
- return ProcessGroupFactory(inner)
5778
- lst.append(inj.bind(make_process_group_factory))
5779
-
5780
- def make_subprocess_factory(injector: Injector) -> SubprocessFactory:
5781
- def inner(process_config: ProcessConfig, group: AbstractProcessGroup) -> AbstractSubprocess:
5782
- return injector.inject(functools.partial(Subprocess, process_config, group))
5783
- return SubprocessFactory(inner)
5784
- lst.append(inj.bind(make_subprocess_factory))
6982
+ inj.bind_factory(ProcessGroupFactory, ProcessGroupImpl),
6983
+ inj.bind_factory(ProcessFactory, ProcessImpl),
6984
+ ]
5785
6985
 
5786
6986
  #
5787
6987
 
@@ -5795,6 +6995,10 @@ def build_server_bindings(
5795
6995
  return inj.as_bindings(*lst)
5796
6996
 
5797
6997
 
6998
+ ########################################
6999
+ # main.py
7000
+
7001
+
5798
7002
  ##
5799
7003
 
5800
7004
 
@@ -5803,6 +7007,10 @@ def main(
5803
7007
  *,
5804
7008
  no_logging: bool = False,
5805
7009
  ) -> None:
7010
+ server_cls = CoroHttpServer # noqa
7011
+
7012
+ #
7013
+
5806
7014
  import argparse
5807
7015
 
5808
7016
  parser = argparse.ArgumentParser()
@@ -5836,14 +7044,14 @@ def main(
5836
7044
  prepare=prepare_server_config,
5837
7045
  )
5838
7046
 
5839
- injector = inj.create_injector(build_server_bindings(
7047
+ injector = inj.create_injector(bind_server(
5840
7048
  config,
5841
7049
  server_epoch=ServerEpoch(epoch),
5842
7050
  inherited_fds=inherited_fds,
5843
7051
  ))
5844
7052
 
5845
- context = injector.provide(ServerContext)
5846
- supervisor = injector.provide(Supervisor)
7053
+ context = injector[ServerContextImpl]
7054
+ supervisor = injector[Supervisor]
5847
7055
 
5848
7056
  try:
5849
7057
  supervisor.main()