ominfra 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.
@@ -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()