ominfra 0.0.0.dev120__py3-none-any.whl → 0.0.0.dev122__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,6 +18,7 @@ import fcntl
18
18
  import fractions
19
19
  import functools
20
20
  import grp
21
+ import inspect
21
22
  import itertools
22
23
  import json
23
24
  import logging
@@ -30,6 +31,7 @@ import select
30
31
  import shlex
31
32
  import signal
32
33
  import stat
34
+ import string
33
35
  import sys
34
36
  import syslog
35
37
  import tempfile
@@ -54,6 +56,11 @@ if sys.version_info < (3, 8):
54
56
  ########################################
55
57
 
56
58
 
59
+ # ../../../omdev/toml/parser.py
60
+ TomlParseFloat = ta.Callable[[str], ta.Any]
61
+ TomlKey = ta.Tuple[str, ...]
62
+ TomlPos = int # ta.TypeAlias
63
+
57
64
  # ../compat.py
58
65
  T = ta.TypeVar('T')
59
66
 
@@ -61,6 +68,837 @@ T = ta.TypeVar('T')
61
68
  ProcessState = int # ta.TypeAlias
62
69
  SupervisorState = int # ta.TypeAlias
63
70
 
71
+ # ../../../omlish/lite/inject.py
72
+ InjectorKeyCls = ta.Union[type, ta.NewType]
73
+ InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
74
+ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
75
+ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
76
+
77
+ # ../../configs.py
78
+ ConfigMapping = ta.Mapping[str, ta.Any]
79
+
80
+ # ../context.py
81
+ ServerEpoch = ta.NewType('ServerEpoch', int)
82
+ InheritedFds = ta.NewType('InheritedFds', ta.FrozenSet[int])
83
+
84
+
85
+ ########################################
86
+ # ../../../omdev/toml/parser.py
87
+ # SPDX-License-Identifier: MIT
88
+ # SPDX-FileCopyrightText: 2021 Taneli Hukkinen
89
+ # Licensed to PSF under a Contributor Agreement.
90
+ #
91
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
92
+ # --------------------------------------------
93
+ #
94
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
95
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
96
+ # documentation.
97
+ #
98
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
99
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
100
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
101
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
102
+ # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
103
+ # Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
104
+ #
105
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
106
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
107
+ # any such work a brief summary of the changes made to Python.
108
+ #
109
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
110
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
111
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
112
+ # RIGHTS.
113
+ #
114
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
115
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
116
+ # ADVISED OF THE POSSIBILITY THEREOF.
117
+ #
118
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
119
+ #
120
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
121
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
122
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
123
+ #
124
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
125
+ # License Agreement.
126
+ #
127
+ # https://github.com/python/cpython/blob/9ce90206b7a4649600218cf0bd4826db79c9a312/Lib/tomllib/_parser.py
128
+
129
+
130
+ ##
131
+
132
+
133
+ _TOML_TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
134
+
135
+ TOML_RE_NUMBER = re.compile(
136
+ r"""
137
+ 0
138
+ (?:
139
+ x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
140
+ |
141
+ b[01](?:_?[01])* # bin
142
+ |
143
+ o[0-7](?:_?[0-7])* # oct
144
+ )
145
+ |
146
+ [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
147
+ (?P<floatpart>
148
+ (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
149
+ (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
150
+ )
151
+ """,
152
+ flags=re.VERBOSE,
153
+ )
154
+ TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
155
+ TOML_RE_DATETIME = re.compile(
156
+ rf"""
157
+ ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
158
+ (?:
159
+ [Tt ]
160
+ {_TOML_TIME_RE_STR}
161
+ (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
162
+ )?
163
+ """,
164
+ flags=re.VERBOSE,
165
+ )
166
+
167
+
168
+ def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
169
+ """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
170
+
171
+ Raises ValueError if the match does not correspond to a valid date or datetime.
172
+ """
173
+ (
174
+ year_str,
175
+ month_str,
176
+ day_str,
177
+ hour_str,
178
+ minute_str,
179
+ sec_str,
180
+ micros_str,
181
+ zulu_time,
182
+ offset_sign_str,
183
+ offset_hour_str,
184
+ offset_minute_str,
185
+ ) = match.groups()
186
+ year, month, day = int(year_str), int(month_str), int(day_str)
187
+ if hour_str is None:
188
+ return datetime.date(year, month, day)
189
+ hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
190
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
191
+ if offset_sign_str:
192
+ tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
193
+ offset_hour_str, offset_minute_str, offset_sign_str,
194
+ )
195
+ elif zulu_time:
196
+ tz = datetime.UTC
197
+ else: # local date-time
198
+ tz = None
199
+ return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
200
+
201
+
202
+ @functools.lru_cache() # noqa
203
+ def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
204
+ sign = 1 if sign_str == '+' else -1
205
+ return datetime.timezone(
206
+ datetime.timedelta(
207
+ hours=sign * int(hour_str),
208
+ minutes=sign * int(minute_str),
209
+ ),
210
+ )
211
+
212
+
213
+ def toml_match_to_localtime(match: re.Match) -> datetime.time:
214
+ hour_str, minute_str, sec_str, micros_str = match.groups()
215
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
216
+ return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
217
+
218
+
219
+ def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
220
+ if match.group('floatpart'):
221
+ return parse_float(match.group())
222
+ return int(match.group(), 0)
223
+
224
+
225
+ TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
226
+
227
+ # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
228
+ # functions.
229
+ TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
230
+ TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
231
+
232
+ TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
233
+ TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
234
+
235
+ TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
236
+
237
+ TOML_WS = frozenset(' \t')
238
+ TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
239
+ TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
240
+ TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
241
+ TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
242
+
243
+ TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
244
+ {
245
+ '\\b': '\u0008', # backspace
246
+ '\\t': '\u0009', # tab
247
+ '\\n': '\u000A', # linefeed
248
+ '\\f': '\u000C', # form feed
249
+ '\\r': '\u000D', # carriage return
250
+ '\\"': '\u0022', # quote
251
+ '\\\\': '\u005C', # backslash
252
+ },
253
+ )
254
+
255
+
256
+ class TomlDecodeError(ValueError):
257
+ """An error raised if a document is not valid TOML."""
258
+
259
+
260
+ def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
261
+ """Parse TOML from a binary file object."""
262
+ b = fp.read()
263
+ try:
264
+ s = b.decode()
265
+ except AttributeError:
266
+ raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
267
+ return toml_loads(s, parse_float=parse_float)
268
+
269
+
270
+ def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
271
+ """Parse TOML from a string."""
272
+
273
+ # The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
274
+ try:
275
+ src = s.replace('\r\n', '\n')
276
+ except (AttributeError, TypeError):
277
+ raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
278
+ pos = 0
279
+ out = TomlOutput(TomlNestedDict(), TomlFlags())
280
+ header: TomlKey = ()
281
+ parse_float = toml_make_safe_parse_float(parse_float)
282
+
283
+ # Parse one statement at a time (typically means one line in TOML source)
284
+ while True:
285
+ # 1. Skip line leading whitespace
286
+ pos = toml_skip_chars(src, pos, TOML_WS)
287
+
288
+ # 2. Parse rules. Expect one of the following:
289
+ # - end of file
290
+ # - end of line
291
+ # - comment
292
+ # - key/value pair
293
+ # - append dict to list (and move to its namespace)
294
+ # - create dict (and move to its namespace)
295
+ # Skip trailing whitespace when applicable.
296
+ try:
297
+ char = src[pos]
298
+ except IndexError:
299
+ break
300
+ if char == '\n':
301
+ pos += 1
302
+ continue
303
+ if char in TOML_KEY_INITIAL_CHARS:
304
+ pos = toml_key_value_rule(src, pos, out, header, parse_float)
305
+ pos = toml_skip_chars(src, pos, TOML_WS)
306
+ elif char == '[':
307
+ try:
308
+ second_char: ta.Optional[str] = src[pos + 1]
309
+ except IndexError:
310
+ second_char = None
311
+ out.flags.finalize_pending()
312
+ if second_char == '[':
313
+ pos, header = toml_create_list_rule(src, pos, out)
314
+ else:
315
+ pos, header = toml_create_dict_rule(src, pos, out)
316
+ pos = toml_skip_chars(src, pos, TOML_WS)
317
+ elif char != '#':
318
+ raise toml_suffixed_err(src, pos, 'Invalid statement')
319
+
320
+ # 3. Skip comment
321
+ pos = toml_skip_comment(src, pos)
322
+
323
+ # 4. Expect end of line or end of file
324
+ try:
325
+ char = src[pos]
326
+ except IndexError:
327
+ break
328
+ if char != '\n':
329
+ raise toml_suffixed_err(
330
+ src, pos, 'Expected newline or end of document after a statement',
331
+ )
332
+ pos += 1
333
+
334
+ return out.data.dict
335
+
336
+
337
+ class TomlFlags:
338
+ """Flags that map to parsed keys/namespaces."""
339
+
340
+ # Marks an immutable namespace (inline array or inline table).
341
+ FROZEN = 0
342
+ # Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
343
+ EXPLICIT_NEST = 1
344
+
345
+ def __init__(self) -> None:
346
+ self._flags: ta.Dict[str, dict] = {}
347
+ self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
348
+
349
+ def add_pending(self, key: TomlKey, flag: int) -> None:
350
+ self._pending_flags.add((key, flag))
351
+
352
+ def finalize_pending(self) -> None:
353
+ for key, flag in self._pending_flags:
354
+ self.set(key, flag, recursive=False)
355
+ self._pending_flags.clear()
356
+
357
+ def unset_all(self, key: TomlKey) -> None:
358
+ cont = self._flags
359
+ for k in key[:-1]:
360
+ if k not in cont:
361
+ return
362
+ cont = cont[k]['nested']
363
+ cont.pop(key[-1], None)
364
+
365
+ def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
366
+ cont = self._flags
367
+ key_parent, key_stem = key[:-1], key[-1]
368
+ for k in key_parent:
369
+ if k not in cont:
370
+ cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
371
+ cont = cont[k]['nested']
372
+ if key_stem not in cont:
373
+ cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
374
+ cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
375
+
376
+ def is_(self, key: TomlKey, flag: int) -> bool:
377
+ if not key:
378
+ return False # document root has no flags
379
+ cont = self._flags
380
+ for k in key[:-1]:
381
+ if k not in cont:
382
+ return False
383
+ inner_cont = cont[k]
384
+ if flag in inner_cont['recursive_flags']:
385
+ return True
386
+ cont = inner_cont['nested']
387
+ key_stem = key[-1]
388
+ if key_stem in cont:
389
+ cont = cont[key_stem]
390
+ return flag in cont['flags'] or flag in cont['recursive_flags']
391
+ return False
392
+
393
+
394
+ class TomlNestedDict:
395
+ def __init__(self) -> None:
396
+ # The parsed content of the TOML document
397
+ self.dict: ta.Dict[str, ta.Any] = {}
398
+
399
+ def get_or_create_nest(
400
+ self,
401
+ key: TomlKey,
402
+ *,
403
+ access_lists: bool = True,
404
+ ) -> dict:
405
+ cont: ta.Any = self.dict
406
+ for k in key:
407
+ if k not in cont:
408
+ cont[k] = {}
409
+ cont = cont[k]
410
+ if access_lists and isinstance(cont, list):
411
+ cont = cont[-1]
412
+ if not isinstance(cont, dict):
413
+ raise KeyError('There is no nest behind this key')
414
+ return cont
415
+
416
+ def append_nest_to_list(self, key: TomlKey) -> None:
417
+ cont = self.get_or_create_nest(key[:-1])
418
+ last_key = key[-1]
419
+ if last_key in cont:
420
+ list_ = cont[last_key]
421
+ if not isinstance(list_, list):
422
+ raise KeyError('An object other than list found behind this key')
423
+ list_.append({})
424
+ else:
425
+ cont[last_key] = [{}]
426
+
427
+
428
+ class TomlOutput(ta.NamedTuple):
429
+ data: TomlNestedDict
430
+ flags: TomlFlags
431
+
432
+
433
+ def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
434
+ try:
435
+ while src[pos] in chars:
436
+ pos += 1
437
+ except IndexError:
438
+ pass
439
+ return pos
440
+
441
+
442
+ def toml_skip_until(
443
+ src: str,
444
+ pos: TomlPos,
445
+ expect: str,
446
+ *,
447
+ error_on: ta.FrozenSet[str],
448
+ error_on_eof: bool,
449
+ ) -> TomlPos:
450
+ try:
451
+ new_pos = src.index(expect, pos)
452
+ except ValueError:
453
+ new_pos = len(src)
454
+ if error_on_eof:
455
+ raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
456
+
457
+ if not error_on.isdisjoint(src[pos:new_pos]):
458
+ while src[pos] not in error_on:
459
+ pos += 1
460
+ raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
461
+ return new_pos
462
+
463
+
464
+ def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
465
+ try:
466
+ char: ta.Optional[str] = src[pos]
467
+ except IndexError:
468
+ char = None
469
+ if char == '#':
470
+ return toml_skip_until(
471
+ src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
472
+ )
473
+ return pos
474
+
475
+
476
+ def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
477
+ while True:
478
+ pos_before_skip = pos
479
+ pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
480
+ pos = toml_skip_comment(src, pos)
481
+ if pos == pos_before_skip:
482
+ return pos
483
+
484
+
485
+ def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
486
+ pos += 1 # Skip "["
487
+ pos = toml_skip_chars(src, pos, TOML_WS)
488
+ pos, key = toml_parse_key(src, pos)
489
+
490
+ if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
491
+ raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
492
+ out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
493
+ try:
494
+ out.data.get_or_create_nest(key)
495
+ except KeyError:
496
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
497
+
498
+ if not src.startswith(']', pos):
499
+ raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
500
+ return pos + 1, key
501
+
502
+
503
+ def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
504
+ pos += 2 # Skip "[["
505
+ pos = toml_skip_chars(src, pos, TOML_WS)
506
+ pos, key = toml_parse_key(src, pos)
507
+
508
+ if out.flags.is_(key, TomlFlags.FROZEN):
509
+ raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
510
+ # Free the namespace now that it points to another empty list item...
511
+ out.flags.unset_all(key)
512
+ # ...but this key precisely is still prohibited from table declaration
513
+ out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
514
+ try:
515
+ out.data.append_nest_to_list(key)
516
+ except KeyError:
517
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
518
+
519
+ if not src.startswith(']]', pos):
520
+ raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
521
+ return pos + 2, key
522
+
523
+
524
+ def toml_key_value_rule(
525
+ src: str,
526
+ pos: TomlPos,
527
+ out: TomlOutput,
528
+ header: TomlKey,
529
+ parse_float: TomlParseFloat,
530
+ ) -> TomlPos:
531
+ pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
532
+ key_parent, key_stem = key[:-1], key[-1]
533
+ abs_key_parent = header + key_parent
534
+
535
+ relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
536
+ for cont_key in relative_path_cont_keys:
537
+ # Check that dotted key syntax does not redefine an existing table
538
+ if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
539
+ raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
540
+ # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
541
+ # table sections.
542
+ out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
543
+
544
+ if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
545
+ raise toml_suffixed_err(
546
+ src,
547
+ pos,
548
+ f'Cannot mutate immutable namespace {abs_key_parent}',
549
+ )
550
+
551
+ try:
552
+ nest = out.data.get_or_create_nest(abs_key_parent)
553
+ except KeyError:
554
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
555
+ if key_stem in nest:
556
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
557
+ # Mark inline table and array namespaces recursively immutable
558
+ if isinstance(value, (dict, list)):
559
+ out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
560
+ nest[key_stem] = value
561
+ return pos
562
+
563
+
564
+ def toml_parse_key_value_pair(
565
+ src: str,
566
+ pos: TomlPos,
567
+ parse_float: TomlParseFloat,
568
+ ) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
569
+ pos, key = toml_parse_key(src, pos)
570
+ try:
571
+ char: ta.Optional[str] = src[pos]
572
+ except IndexError:
573
+ char = None
574
+ if char != '=':
575
+ raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
576
+ pos += 1
577
+ pos = toml_skip_chars(src, pos, TOML_WS)
578
+ pos, value = toml_parse_value(src, pos, parse_float)
579
+ return pos, key, value
580
+
581
+
582
+ def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
583
+ pos, key_part = toml_parse_key_part(src, pos)
584
+ key: TomlKey = (key_part,)
585
+ pos = toml_skip_chars(src, pos, TOML_WS)
586
+ while True:
587
+ try:
588
+ char: ta.Optional[str] = src[pos]
589
+ except IndexError:
590
+ char = None
591
+ if char != '.':
592
+ return pos, key
593
+ pos += 1
594
+ pos = toml_skip_chars(src, pos, TOML_WS)
595
+ pos, key_part = toml_parse_key_part(src, pos)
596
+ key += (key_part,)
597
+ pos = toml_skip_chars(src, pos, TOML_WS)
598
+
599
+
600
+ def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
601
+ try:
602
+ char: ta.Optional[str] = src[pos]
603
+ except IndexError:
604
+ char = None
605
+ if char in TOML_BARE_KEY_CHARS:
606
+ start_pos = pos
607
+ pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
608
+ return pos, src[start_pos:pos]
609
+ if char == "'":
610
+ return toml_parse_literal_str(src, pos)
611
+ if char == '"':
612
+ return toml_parse_one_line_basic_str(src, pos)
613
+ raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
614
+
615
+
616
+ def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
617
+ pos += 1
618
+ return toml_parse_basic_str(src, pos, multiline=False)
619
+
620
+
621
+ def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
622
+ pos += 1
623
+ array: list = []
624
+
625
+ pos = toml_skip_comments_and_array_ws(src, pos)
626
+ if src.startswith(']', pos):
627
+ return pos + 1, array
628
+ while True:
629
+ pos, val = toml_parse_value(src, pos, parse_float)
630
+ array.append(val)
631
+ pos = toml_skip_comments_and_array_ws(src, pos)
632
+
633
+ c = src[pos:pos + 1]
634
+ if c == ']':
635
+ return pos + 1, array
636
+ if c != ',':
637
+ raise toml_suffixed_err(src, pos, 'Unclosed array')
638
+ pos += 1
639
+
640
+ pos = toml_skip_comments_and_array_ws(src, pos)
641
+ if src.startswith(']', pos):
642
+ return pos + 1, array
643
+
644
+
645
+ def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
646
+ pos += 1
647
+ nested_dict = TomlNestedDict()
648
+ flags = TomlFlags()
649
+
650
+ pos = toml_skip_chars(src, pos, TOML_WS)
651
+ if src.startswith('}', pos):
652
+ return pos + 1, nested_dict.dict
653
+ while True:
654
+ pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
655
+ key_parent, key_stem = key[:-1], key[-1]
656
+ if flags.is_(key, TomlFlags.FROZEN):
657
+ raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
658
+ try:
659
+ nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
660
+ except KeyError:
661
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
662
+ if key_stem in nest:
663
+ raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
664
+ nest[key_stem] = value
665
+ pos = toml_skip_chars(src, pos, TOML_WS)
666
+ c = src[pos:pos + 1]
667
+ if c == '}':
668
+ return pos + 1, nested_dict.dict
669
+ if c != ',':
670
+ raise toml_suffixed_err(src, pos, 'Unclosed inline table')
671
+ if isinstance(value, (dict, list)):
672
+ flags.set(key, TomlFlags.FROZEN, recursive=True)
673
+ pos += 1
674
+ pos = toml_skip_chars(src, pos, TOML_WS)
675
+
676
+
677
+ def toml_parse_basic_str_escape(
678
+ src: str,
679
+ pos: TomlPos,
680
+ *,
681
+ multiline: bool = False,
682
+ ) -> ta.Tuple[TomlPos, str]:
683
+ escape_id = src[pos:pos + 2]
684
+ pos += 2
685
+ if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
686
+ # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
687
+ # newline.
688
+ if escape_id != '\\\n':
689
+ pos = toml_skip_chars(src, pos, TOML_WS)
690
+ try:
691
+ char = src[pos]
692
+ except IndexError:
693
+ return pos, ''
694
+ if char != '\n':
695
+ raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
696
+ pos += 1
697
+ pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
698
+ return pos, ''
699
+ if escape_id == '\\u':
700
+ return toml_parse_hex_char(src, pos, 4)
701
+ if escape_id == '\\U':
702
+ return toml_parse_hex_char(src, pos, 8)
703
+ try:
704
+ return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
705
+ except KeyError:
706
+ raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
707
+
708
+
709
+ def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
710
+ return toml_parse_basic_str_escape(src, pos, multiline=True)
711
+
712
+
713
+ def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
714
+ hex_str = src[pos:pos + hex_len]
715
+ if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
716
+ raise toml_suffixed_err(src, pos, 'Invalid hex value')
717
+ pos += hex_len
718
+ hex_int = int(hex_str, 16)
719
+ if not toml_is_unicode_scalar_value(hex_int):
720
+ raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
721
+ return pos, chr(hex_int)
722
+
723
+
724
+ def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
725
+ pos += 1 # Skip starting apostrophe
726
+ start_pos = pos
727
+ pos = toml_skip_until(
728
+ src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
729
+ )
730
+ return pos + 1, src[start_pos:pos] # Skip ending apostrophe
731
+
732
+
733
+ def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
734
+ pos += 3
735
+ if src.startswith('\n', pos):
736
+ pos += 1
737
+
738
+ if literal:
739
+ delim = "'"
740
+ end_pos = toml_skip_until(
741
+ src,
742
+ pos,
743
+ "'''",
744
+ error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
745
+ error_on_eof=True,
746
+ )
747
+ result = src[pos:end_pos]
748
+ pos = end_pos + 3
749
+ else:
750
+ delim = '"'
751
+ pos, result = toml_parse_basic_str(src, pos, multiline=True)
752
+
753
+ # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
754
+ if not src.startswith(delim, pos):
755
+ return pos, result
756
+ pos += 1
757
+ if not src.startswith(delim, pos):
758
+ return pos, result + delim
759
+ pos += 1
760
+ return pos, result + (delim * 2)
761
+
762
+
763
+ def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
764
+ if multiline:
765
+ error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
766
+ parse_escapes = toml_parse_basic_str_escape_multiline
767
+ else:
768
+ error_on = TOML_ILLEGAL_BASIC_STR_CHARS
769
+ parse_escapes = toml_parse_basic_str_escape
770
+ result = ''
771
+ start_pos = pos
772
+ while True:
773
+ try:
774
+ char = src[pos]
775
+ except IndexError:
776
+ raise toml_suffixed_err(src, pos, 'Unterminated string') from None
777
+ if char == '"':
778
+ if not multiline:
779
+ return pos + 1, result + src[start_pos:pos]
780
+ if src.startswith('"""', pos):
781
+ return pos + 3, result + src[start_pos:pos]
782
+ pos += 1
783
+ continue
784
+ if char == '\\':
785
+ result += src[start_pos:pos]
786
+ pos, parsed_escape = parse_escapes(src, pos)
787
+ result += parsed_escape
788
+ start_pos = pos
789
+ continue
790
+ if char in error_on:
791
+ raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
792
+ pos += 1
793
+
794
+
795
+ def toml_parse_value( # noqa: C901
796
+ src: str,
797
+ pos: TomlPos,
798
+ parse_float: TomlParseFloat,
799
+ ) -> ta.Tuple[TomlPos, ta.Any]:
800
+ try:
801
+ char: ta.Optional[str] = src[pos]
802
+ except IndexError:
803
+ char = None
804
+
805
+ # IMPORTANT: order conditions based on speed of checking and likelihood
806
+
807
+ # Basic strings
808
+ if char == '"':
809
+ if src.startswith('"""', pos):
810
+ return toml_parse_multiline_str(src, pos, literal=False)
811
+ return toml_parse_one_line_basic_str(src, pos)
812
+
813
+ # Literal strings
814
+ if char == "'":
815
+ if src.startswith("'''", pos):
816
+ return toml_parse_multiline_str(src, pos, literal=True)
817
+ return toml_parse_literal_str(src, pos)
818
+
819
+ # Booleans
820
+ if char == 't':
821
+ if src.startswith('true', pos):
822
+ return pos + 4, True
823
+ if char == 'f':
824
+ if src.startswith('false', pos):
825
+ return pos + 5, False
826
+
827
+ # Arrays
828
+ if char == '[':
829
+ return toml_parse_array(src, pos, parse_float)
830
+
831
+ # Inline tables
832
+ if char == '{':
833
+ return toml_parse_inline_table(src, pos, parse_float)
834
+
835
+ # Dates and times
836
+ datetime_match = TOML_RE_DATETIME.match(src, pos)
837
+ if datetime_match:
838
+ try:
839
+ datetime_obj = toml_match_to_datetime(datetime_match)
840
+ except ValueError as e:
841
+ raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
842
+ return datetime_match.end(), datetime_obj
843
+ localtime_match = TOML_RE_LOCALTIME.match(src, pos)
844
+ if localtime_match:
845
+ return localtime_match.end(), toml_match_to_localtime(localtime_match)
846
+
847
+ # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
848
+ # located after handling of dates and times.
849
+ number_match = TOML_RE_NUMBER.match(src, pos)
850
+ if number_match:
851
+ return number_match.end(), toml_match_to_number(number_match, parse_float)
852
+
853
+ # Special floats
854
+ first_three = src[pos:pos + 3]
855
+ if first_three in {'inf', 'nan'}:
856
+ return pos + 3, parse_float(first_three)
857
+ first_four = src[pos:pos + 4]
858
+ if first_four in {'-inf', '+inf', '-nan', '+nan'}:
859
+ return pos + 4, parse_float(first_four)
860
+
861
+ raise toml_suffixed_err(src, pos, 'Invalid value')
862
+
863
+
864
+ def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
865
+ """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
866
+
867
+ def coord_repr(src: str, pos: TomlPos) -> str:
868
+ if pos >= len(src):
869
+ return 'end of document'
870
+ line = src.count('\n', 0, pos) + 1
871
+ if line == 1:
872
+ column = pos + 1
873
+ else:
874
+ column = pos - src.rindex('\n', 0, pos)
875
+ return f'line {line}, column {column}'
876
+
877
+ return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
878
+
879
+
880
+ def toml_is_unicode_scalar_value(codepoint: int) -> bool:
881
+ return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
882
+
883
+
884
+ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
885
+ """A decorator to make `parse_float` safe.
886
+
887
+ `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
888
+ thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
889
+ """
890
+ # The default `float` callable never returns illegal types. Optimize it.
891
+ if parse_float is float:
892
+ return float
893
+
894
+ def safe_parse_float(float_str: str) -> ta.Any:
895
+ float_value = parse_float(float_str)
896
+ if isinstance(float_value, (dict, list)):
897
+ raise ValueError('parse_float must not return dicts or lists') # noqa
898
+ return float_value
899
+
900
+ return safe_parse_float
901
+
64
902
 
65
903
  ########################################
66
904
  # ../compat.py
@@ -216,6 +1054,19 @@ def close_fd(fd: int) -> bool:
216
1054
  return True
217
1055
 
218
1056
 
1057
+ def is_fd_open(fd: int) -> bool:
1058
+ try:
1059
+ n = os.dup(fd)
1060
+ except OSError:
1061
+ return False
1062
+ os.close(n)
1063
+ return True
1064
+
1065
+
1066
+ def get_open_fds(limit: int) -> ta.FrozenSet[int]:
1067
+ return frozenset(filter(is_fd_open, range(limit)))
1068
+
1069
+
219
1070
  def mktempfile(suffix: str, prefix: str, dir: str) -> str: # noqa
220
1071
  fd, filename = tempfile.mkstemp(suffix, prefix, dir)
221
1072
  os.close(fd)
@@ -490,7 +1341,7 @@ class _cached_nullary: # noqa
490
1341
  return bound
491
1342
 
492
1343
 
493
- def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
1344
+ def cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
494
1345
  return _cached_nullary(fn)
495
1346
 
496
1347
 
@@ -581,6 +1432,50 @@ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON
581
1432
  json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
582
1433
 
583
1434
 
1435
+ ########################################
1436
+ # ../../../omlish/lite/maybes.py
1437
+
1438
+
1439
+ class Maybe(ta.Generic[T]):
1440
+ @property
1441
+ @abc.abstractmethod
1442
+ def present(self) -> bool:
1443
+ raise NotImplementedError
1444
+
1445
+ @abc.abstractmethod
1446
+ def must(self) -> T:
1447
+ raise NotImplementedError
1448
+
1449
+ @classmethod
1450
+ def just(cls, v: T) -> 'Maybe[T]':
1451
+ return tuple.__new__(_Maybe, (v,)) # noqa
1452
+
1453
+ _empty: ta.ClassVar['Maybe']
1454
+
1455
+ @classmethod
1456
+ def empty(cls) -> 'Maybe[T]':
1457
+ return Maybe._empty
1458
+
1459
+
1460
+ class _Maybe(Maybe[T], tuple):
1461
+ __slots__ = ()
1462
+
1463
+ def __init_subclass__(cls, **kwargs):
1464
+ raise TypeError
1465
+
1466
+ @property
1467
+ def present(self) -> bool:
1468
+ return bool(self)
1469
+
1470
+ def must(self) -> T:
1471
+ if not self:
1472
+ raise ValueError
1473
+ return self[0]
1474
+
1475
+
1476
+ Maybe._empty = tuple.__new__(_Maybe, ()) # noqa
1477
+
1478
+
584
1479
  ########################################
585
1480
  # ../../../omlish/lite/reflect.py
586
1481
 
@@ -616,6 +1511,14 @@ def get_optional_alias_arg(spec: ta.Any) -> ta.Any:
616
1511
  return it
617
1512
 
618
1513
 
1514
+ def is_new_type(spec: ta.Any) -> bool:
1515
+ if isinstance(ta.NewType, type):
1516
+ return isinstance(spec, ta.NewType)
1517
+ else:
1518
+ # Before https://github.com/python/cpython/commit/c2f33dfc83ab270412bf243fb21f724037effa1a
1519
+ return isinstance(spec, types.FunctionType) and spec.__code__ is ta.NewType.__code__.co_consts[1] # type: ignore # noqa
1520
+
1521
+
619
1522
  def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
620
1523
  seen = set()
621
1524
  todo = list(reversed(cls.__subclasses__()))
@@ -629,179 +1532,685 @@ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
629
1532
 
630
1533
 
631
1534
  ########################################
632
- # ../configs.py
1535
+ # ../states.py
1536
+
1537
+
1538
+ ##
1539
+
1540
+
1541
+ def _names_by_code(states: ta.Any) -> ta.Dict[int, str]:
1542
+ d = {}
1543
+ for name in states.__dict__:
1544
+ if not name.startswith('__'):
1545
+ code = getattr(states, name)
1546
+ d[code] = name
1547
+ return d
1548
+
1549
+
1550
+ ##
1551
+
1552
+
1553
+ class ProcessStates:
1554
+ STOPPED = 0
1555
+ STARTING = 10
1556
+ RUNNING = 20
1557
+ BACKOFF = 30
1558
+ STOPPING = 40
1559
+ EXITED = 100
1560
+ FATAL = 200
1561
+ UNKNOWN = 1000
1562
+
1563
+
1564
+ STOPPED_STATES = (
1565
+ ProcessStates.STOPPED,
1566
+ ProcessStates.EXITED,
1567
+ ProcessStates.FATAL,
1568
+ ProcessStates.UNKNOWN,
1569
+ )
1570
+
1571
+ RUNNING_STATES = (
1572
+ ProcessStates.RUNNING,
1573
+ ProcessStates.BACKOFF,
1574
+ ProcessStates.STARTING,
1575
+ )
1576
+
1577
+ SIGNALLABLE_STATES = (
1578
+ ProcessStates.RUNNING,
1579
+ ProcessStates.STARTING,
1580
+ ProcessStates.STOPPING,
1581
+ )
1582
+
1583
+
1584
+ _process_states_by_code = _names_by_code(ProcessStates)
1585
+
1586
+
1587
+ def get_process_state_description(code: ProcessState) -> str:
1588
+ return check_not_none(_process_states_by_code.get(code))
1589
+
1590
+
1591
+ ##
1592
+
1593
+
1594
+ class SupervisorStates:
1595
+ FATAL = 2
1596
+ RUNNING = 1
1597
+ RESTARTING = 0
1598
+ SHUTDOWN = -1
1599
+
1600
+
1601
+ _supervisor_states_by_code = _names_by_code(SupervisorStates)
1602
+
1603
+
1604
+ def get_supervisor_state_description(code: SupervisorState) -> str:
1605
+ return check_not_none(_supervisor_states_by_code.get(code))
1606
+
1607
+
1608
+ ########################################
1609
+ # ../../../omlish/lite/inject.py
1610
+
1611
+
1612
+ ###
1613
+ # types
633
1614
 
634
1615
 
635
1616
  @dc.dataclass(frozen=True)
636
- class ProcessConfig:
637
- name: str
638
- command: str
1617
+ class InjectorKey:
1618
+ cls: InjectorKeyCls
1619
+ tag: ta.Any = None
1620
+ array: bool = False
639
1621
 
640
- uid: ta.Optional[int] = None
641
- directory: ta.Optional[str] = None
642
- umask: ta.Optional[int] = None
643
- priority: int = 999
644
1622
 
645
- autostart: bool = True
646
- autorestart: str = 'unexpected'
1623
+ ##
647
1624
 
648
- startsecs: int = 1
649
- startretries: int = 3
650
1625
 
651
- numprocs: int = 1
652
- numprocs_start: int = 0
1626
+ class InjectorProvider(abc.ABC):
1627
+ @abc.abstractmethod
1628
+ def provider_fn(self) -> InjectorProviderFn:
1629
+ raise NotImplementedError
653
1630
 
654
- @dc.dataclass(frozen=True)
655
- class Log:
656
- file: ta.Optional[str] = None
657
- capture_maxbytes: ta.Optional[int] = None
658
- events_enabled: bool = False
659
- syslog: bool = False
660
- backups: ta.Optional[int] = None
661
- maxbytes: ta.Optional[int] = None
662
1631
 
663
- stdout: Log = Log()
664
- stderr: Log = Log()
1632
+ ##
665
1633
 
666
- stopsignal: int = signal.SIGTERM
667
- stopwaitsecs: int = 10
668
- stopasgroup: bool = False
669
1634
 
670
- killasgroup: bool = False
1635
+ @dc.dataclass(frozen=True)
1636
+ class InjectorBinding:
1637
+ key: InjectorKey
1638
+ provider: InjectorProvider
671
1639
 
672
- exitcodes: ta.Sequence[int] = (0,)
673
1640
 
674
- redirect_stderr: bool = False
1641
+ class InjectorBindings(abc.ABC):
1642
+ @abc.abstractmethod
1643
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
1644
+ raise NotImplementedError
675
1645
 
676
- environment: ta.Optional[ta.Mapping[str, str]] = None
1646
+ ##
1647
+
1648
+
1649
+ class Injector(abc.ABC):
1650
+ @abc.abstractmethod
1651
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
1652
+ raise NotImplementedError
1653
+
1654
+ @abc.abstractmethod
1655
+ def provide(self, key: ta.Any) -> ta.Any:
1656
+ raise NotImplementedError
1657
+
1658
+ @abc.abstractmethod
1659
+ def provide_kwargs(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
1660
+ raise NotImplementedError
1661
+
1662
+ @abc.abstractmethod
1663
+ def inject(self, obj: ta.Any) -> ta.Any:
1664
+ raise NotImplementedError
1665
+
1666
+
1667
+ ###
1668
+ # exceptions
677
1669
 
678
1670
 
679
1671
  @dc.dataclass(frozen=True)
680
- class ProcessGroupConfig:
681
- name: str
1672
+ class InjectorKeyError(Exception):
1673
+ key: InjectorKey
682
1674
 
683
- priority: int = 999
1675
+ source: ta.Any = None
1676
+ name: ta.Optional[str] = None
684
1677
 
685
- processes: ta.Optional[ta.Sequence[ProcessConfig]] = None
1678
+
1679
+ @dc.dataclass(frozen=True)
1680
+ class UnboundInjectorKeyError(InjectorKeyError):
1681
+ pass
686
1682
 
687
1683
 
688
1684
  @dc.dataclass(frozen=True)
689
- class ServerConfig:
690
- user: ta.Optional[str] = None
691
- nodaemon: bool = False
692
- umask: int = 0o22
693
- directory: ta.Optional[str] = None
694
- logfile: str = 'supervisord.log'
695
- logfile_maxbytes: int = 50 * 1024 * 1024
696
- logfile_backups: int = 10
697
- loglevel: int = logging.INFO
698
- pidfile: str = 'supervisord.pid'
699
- identifier: str = 'supervisor'
700
- child_logdir: str = '/dev/null'
701
- minfds: int = 1024
702
- minprocs: int = 200
703
- nocleanup: bool = False
704
- strip_ansi: bool = False
705
- silent: bool = False
1685
+ class DuplicateInjectorKeyError(InjectorKeyError):
1686
+ pass
706
1687
 
707
- groups: ta.Optional[ta.Sequence[ProcessGroupConfig]] = None
1688
+
1689
+ ###
1690
+ # keys
1691
+
1692
+
1693
+ def as_injector_key(o: ta.Any) -> InjectorKey:
1694
+ if o is inspect.Parameter.empty:
1695
+ raise TypeError(o)
1696
+ if isinstance(o, InjectorKey):
1697
+ return o
1698
+ if isinstance(o, type) or is_new_type(o):
1699
+ return InjectorKey(o)
1700
+ raise TypeError(o)
1701
+
1702
+
1703
+ ###
1704
+ # providers
1705
+
1706
+
1707
+ @dc.dataclass(frozen=True)
1708
+ class FnInjectorProvider(InjectorProvider):
1709
+ fn: ta.Any
1710
+
1711
+ def __post_init__(self) -> None:
1712
+ check_not_isinstance(self.fn, type)
1713
+
1714
+ def provider_fn(self) -> InjectorProviderFn:
1715
+ def pfn(i: Injector) -> ta.Any:
1716
+ return i.inject(self.fn)
1717
+
1718
+ return pfn
1719
+
1720
+
1721
+ @dc.dataclass(frozen=True)
1722
+ class CtorInjectorProvider(InjectorProvider):
1723
+ cls: type
1724
+
1725
+ def __post_init__(self) -> None:
1726
+ check_isinstance(self.cls, type)
1727
+
1728
+ def provider_fn(self) -> InjectorProviderFn:
1729
+ def pfn(i: Injector) -> ta.Any:
1730
+ return i.inject(self.cls)
1731
+
1732
+ return pfn
1733
+
1734
+
1735
+ @dc.dataclass(frozen=True)
1736
+ class ConstInjectorProvider(InjectorProvider):
1737
+ v: ta.Any
1738
+
1739
+ def provider_fn(self) -> InjectorProviderFn:
1740
+ return lambda _: self.v
1741
+
1742
+
1743
+ @dc.dataclass(frozen=True)
1744
+ class SingletonInjectorProvider(InjectorProvider):
1745
+ p: InjectorProvider
1746
+
1747
+ def __post_init__(self) -> None:
1748
+ check_isinstance(self.p, InjectorProvider)
1749
+
1750
+ def provider_fn(self) -> InjectorProviderFn:
1751
+ v = not_set = object()
1752
+
1753
+ def pfn(i: Injector) -> ta.Any:
1754
+ nonlocal v
1755
+ if v is not_set:
1756
+ v = ufn(i)
1757
+ return v
1758
+
1759
+ ufn = self.p.provider_fn()
1760
+ return pfn
1761
+
1762
+
1763
+ @dc.dataclass(frozen=True)
1764
+ class LinkInjectorProvider(InjectorProvider):
1765
+ k: InjectorKey
1766
+
1767
+ def __post_init__(self) -> None:
1768
+ check_isinstance(self.k, InjectorKey)
1769
+
1770
+ def provider_fn(self) -> InjectorProviderFn:
1771
+ def pfn(i: Injector) -> ta.Any:
1772
+ return i.provide(self.k)
1773
+
1774
+ return pfn
1775
+
1776
+
1777
+ @dc.dataclass(frozen=True)
1778
+ class ArrayInjectorProvider(InjectorProvider):
1779
+ ps: ta.Sequence[InjectorProvider]
1780
+
1781
+ def provider_fn(self) -> InjectorProviderFn:
1782
+ ps = [p.provider_fn() for p in self.ps]
1783
+
1784
+ def pfn(i: Injector) -> ta.Any:
1785
+ rv = []
1786
+ for ep in ps:
1787
+ o = ep(i)
1788
+ rv.append(o)
1789
+ return rv
1790
+
1791
+ return pfn
1792
+
1793
+
1794
+ ###
1795
+ # bindings
1796
+
1797
+
1798
+ @dc.dataclass(frozen=True)
1799
+ class _InjectorBindings(InjectorBindings):
1800
+ bs: ta.Optional[ta.Sequence[InjectorBinding]] = None
1801
+ ps: ta.Optional[ta.Sequence[InjectorBindings]] = None
1802
+
1803
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
1804
+ if self.bs is not None:
1805
+ yield from self.bs
1806
+ if self.ps is not None:
1807
+ for p in self.ps:
1808
+ yield from p.bindings()
1809
+
1810
+
1811
+ def as_injector_bindings(*args: InjectorBindingOrBindings) -> InjectorBindings:
1812
+ bs: ta.List[InjectorBinding] = []
1813
+ ps: ta.List[InjectorBindings] = []
1814
+
1815
+ for a in args:
1816
+ if isinstance(a, InjectorBindings):
1817
+ ps.append(a)
1818
+ elif isinstance(a, InjectorBinding):
1819
+ bs.append(a)
1820
+ else:
1821
+ raise TypeError(a)
1822
+
1823
+ return _InjectorBindings(
1824
+ bs or None,
1825
+ ps or None,
1826
+ )
1827
+
1828
+
1829
+ ##
1830
+
1831
+
1832
+ @dc.dataclass(frozen=True)
1833
+ class OverridesInjectorBindings(InjectorBindings):
1834
+ p: InjectorBindings
1835
+ m: ta.Mapping[InjectorKey, InjectorBinding]
1836
+
1837
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
1838
+ for b in self.p.bindings():
1839
+ yield self.m.get(b.key, b)
1840
+
1841
+
1842
+ def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
1843
+ m: ta.Dict[InjectorKey, InjectorBinding] = {}
1844
+
1845
+ for b in as_injector_bindings(*args).bindings():
1846
+ if b.key in m:
1847
+ raise DuplicateInjectorKeyError(b.key)
1848
+ m[b.key] = b
1849
+
1850
+ return OverridesInjectorBindings(p, m)
1851
+
1852
+
1853
+ ##
1854
+
1855
+
1856
+ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
1857
+ pm: ta.Dict[InjectorKey, InjectorProvider] = {}
1858
+ am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
1859
+
1860
+ for b in bs.bindings():
1861
+ if b.key.array:
1862
+ am.setdefault(b.key, []).append(b.provider)
1863
+ else:
1864
+ if b.key in pm:
1865
+ raise KeyError(b.key)
1866
+ pm[b.key] = b.provider
1867
+
1868
+ if am:
1869
+ for k, aps in am.items():
1870
+ pm[k] = ArrayInjectorProvider(aps)
1871
+
1872
+ return pm
1873
+
1874
+
1875
+ ###
1876
+ # inspection
1877
+
1878
+
1879
+ _INJECTION_SIGNATURE_CACHE: ta.MutableMapping[ta.Any, inspect.Signature] = weakref.WeakKeyDictionary()
1880
+
1881
+
1882
+ def _injection_signature(obj: ta.Any) -> inspect.Signature:
1883
+ try:
1884
+ return _INJECTION_SIGNATURE_CACHE[obj]
1885
+ except TypeError:
1886
+ return inspect.signature(obj)
1887
+ except KeyError:
1888
+ pass
1889
+ sig = inspect.signature(obj)
1890
+ _INJECTION_SIGNATURE_CACHE[obj] = sig
1891
+ return sig
1892
+
1893
+
1894
+ class InjectionKwarg(ta.NamedTuple):
1895
+ name: str
1896
+ key: InjectorKey
1897
+ has_default: bool
1898
+
1899
+
1900
+ class InjectionKwargsTarget(ta.NamedTuple):
1901
+ obj: ta.Any
1902
+ kwargs: ta.Sequence[InjectionKwarg]
1903
+
1904
+
1905
+ def build_injection_kwargs_target(
1906
+ obj: ta.Any,
1907
+ *,
1908
+ skip_args: int = 0,
1909
+ skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
1910
+ raw_optional: bool = False,
1911
+ ) -> InjectionKwargsTarget:
1912
+ sig = _injection_signature(obj)
1913
+
1914
+ seen: ta.Set[InjectorKey] = set(map(as_injector_key, skip_kwargs)) if skip_kwargs is not None else set()
1915
+ kws: ta.List[InjectionKwarg] = []
1916
+ for p in list(sig.parameters.values())[skip_args:]:
1917
+ if p.annotation is inspect.Signature.empty:
1918
+ if p.default is not inspect.Parameter.empty:
1919
+ raise KeyError(f'{obj}, {p.name}')
1920
+ continue
1921
+
1922
+ if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
1923
+ raise TypeError(sig)
1924
+
1925
+ ann = p.annotation
1926
+ if (
1927
+ not raw_optional and
1928
+ is_optional_alias(ann)
1929
+ ):
1930
+ ann = get_optional_alias_arg(ann)
1931
+
1932
+ k = as_injector_key(ann)
1933
+
1934
+ if k in seen:
1935
+ raise DuplicateInjectorKeyError(k)
1936
+ seen.add(k)
1937
+
1938
+ kws.append(InjectionKwarg(
1939
+ p.name,
1940
+ k,
1941
+ p.default is not inspect.Parameter.empty,
1942
+ ))
1943
+
1944
+ return InjectionKwargsTarget(
1945
+ obj,
1946
+ kws,
1947
+ )
1948
+
1949
+
1950
+ ###
1951
+ # binder
1952
+
1953
+
1954
+ class InjectorBinder:
1955
+ def __new__(cls, *args, **kwargs): # noqa
1956
+ raise TypeError
1957
+
1958
+ _FN_TYPES: ta.Tuple[type, ...] = (
1959
+ types.FunctionType,
1960
+ types.MethodType,
1961
+
1962
+ classmethod,
1963
+ staticmethod,
1964
+
1965
+ functools.partial,
1966
+ functools.partialmethod,
1967
+ )
708
1968
 
709
1969
  @classmethod
710
- def new(
1970
+ def _is_fn(cls, obj: ta.Any) -> bool:
1971
+ return isinstance(obj, cls._FN_TYPES)
1972
+
1973
+ @classmethod
1974
+ def bind_as_fn(cls, icls: ta.Type[T]) -> ta.Type[T]:
1975
+ check_isinstance(icls, type)
1976
+ if icls not in cls._FN_TYPES:
1977
+ cls._FN_TYPES = (*cls._FN_TYPES, icls)
1978
+ return icls
1979
+
1980
+ _BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
1981
+ InjectorProvider,
1982
+ )
1983
+
1984
+ @classmethod
1985
+ def bind(
711
1986
  cls,
712
- umask: ta.Union[int, str] = 0o22,
713
- directory: ta.Optional[str] = None,
714
- logfile: str = 'supervisord.log',
715
- logfile_maxbytes: ta.Union[int, str] = 50 * 1024 * 1024,
716
- loglevel: ta.Union[int, str] = logging.INFO,
717
- pidfile: str = 'supervisord.pid',
718
- child_logdir: ta.Optional[str] = None,
719
- **kwargs: ta.Any,
720
- ) -> 'ServerConfig':
721
- return cls(
722
- umask=octal_type(umask),
723
- directory=existing_directory(directory) if directory is not None else None,
724
- logfile=existing_dirpath(logfile),
725
- logfile_maxbytes=byte_size(logfile_maxbytes),
726
- loglevel=logging_level(loglevel),
727
- pidfile=existing_dirpath(pidfile),
728
- child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
729
- **kwargs,
1987
+ obj: ta.Any,
1988
+ *,
1989
+ key: ta.Any = None,
1990
+ tag: ta.Any = None,
1991
+ array: ta.Optional[bool] = None, # noqa
1992
+
1993
+ to_fn: ta.Any = None,
1994
+ to_ctor: ta.Any = None,
1995
+ to_const: ta.Any = None,
1996
+ to_key: ta.Any = None,
1997
+
1998
+ singleton: bool = False,
1999
+ ) -> InjectorBinding:
2000
+ if obj is None or obj is inspect.Parameter.empty:
2001
+ raise TypeError(obj)
2002
+ if isinstance(obj, cls._BANNED_BIND_TYPES):
2003
+ raise TypeError(obj)
2004
+
2005
+ ##
2006
+
2007
+ if key is not None:
2008
+ key = as_injector_key(key)
2009
+
2010
+ ##
2011
+
2012
+ has_to = (
2013
+ to_fn is not None or
2014
+ to_ctor is not None or
2015
+ to_const is not None or
2016
+ to_key is not None
730
2017
  )
2018
+ if isinstance(obj, InjectorKey):
2019
+ if key is None:
2020
+ key = obj
2021
+ elif isinstance(obj, type):
2022
+ if not has_to:
2023
+ to_ctor = obj
2024
+ if key is None:
2025
+ key = InjectorKey(obj)
2026
+ elif cls._is_fn(obj) and not has_to:
2027
+ to_fn = obj
2028
+ if key is None:
2029
+ sig = _injection_signature(obj)
2030
+ ty = check_isinstance(sig.return_annotation, type)
2031
+ key = InjectorKey(ty)
2032
+ else:
2033
+ if to_const is not None:
2034
+ raise TypeError('Cannot bind instance with to_const')
2035
+ to_const = obj
2036
+ if key is None:
2037
+ key = InjectorKey(type(obj))
2038
+ del has_to
731
2039
 
2040
+ ##
732
2041
 
733
- ########################################
734
- # ../states.py
2042
+ if tag is not None:
2043
+ if key.tag is not None:
2044
+ raise TypeError('Tag already set')
2045
+ key = dc.replace(key, tag=tag)
2046
+
2047
+ if array is not None:
2048
+ key = dc.replace(key, array=array)
2049
+
2050
+ ##
2051
+
2052
+ providers: ta.List[InjectorProvider] = []
2053
+ if to_fn is not None:
2054
+ providers.append(FnInjectorProvider(to_fn))
2055
+ if to_ctor is not None:
2056
+ providers.append(CtorInjectorProvider(to_ctor))
2057
+ if to_const is not None:
2058
+ providers.append(ConstInjectorProvider(to_const))
2059
+ if to_key is not None:
2060
+ providers.append(LinkInjectorProvider(as_injector_key(to_key)))
2061
+ if not providers:
2062
+ raise TypeError('Must specify provider')
2063
+ if len(providers) > 1:
2064
+ raise TypeError('May not specify multiple providers')
2065
+ provider, = providers
2066
+
2067
+ ##
2068
+
2069
+ if singleton:
2070
+ provider = SingletonInjectorProvider(provider)
2071
+
2072
+ ##
2073
+
2074
+ binding = InjectorBinding(key, provider)
2075
+
2076
+ ##
2077
+
2078
+ return binding
2079
+
2080
+
2081
+ ###
2082
+ # injector
2083
+
2084
+
2085
+ _INJECTOR_INJECTOR_KEY = InjectorKey(Injector)
2086
+
2087
+
2088
+ class _Injector(Injector):
2089
+ def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
2090
+ super().__init__()
2091
+
2092
+ self._bs = check_isinstance(bs, InjectorBindings)
2093
+ self._p: ta.Optional[Injector] = check_isinstance(p, (Injector, type(None)))
2094
+
2095
+ self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
2096
+
2097
+ if _INJECTOR_INJECTOR_KEY in self._pfm:
2098
+ raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
735
2099
 
2100
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
2101
+ key = as_injector_key(key)
736
2102
 
737
- ##
2103
+ if key == _INJECTOR_INJECTOR_KEY:
2104
+ return Maybe.just(self)
738
2105
 
2106
+ fn = self._pfm.get(key)
2107
+ if fn is not None:
2108
+ return Maybe.just(fn(self))
739
2109
 
740
- def _names_by_code(states: ta.Any) -> ta.Dict[int, str]:
741
- d = {}
742
- for name in states.__dict__:
743
- if not name.startswith('__'):
744
- code = getattr(states, name)
745
- d[code] = name
746
- return d
2110
+ if self._p is not None:
2111
+ pv = self._p.try_provide(key)
2112
+ if pv is not None:
2113
+ return Maybe.empty()
747
2114
 
2115
+ return Maybe.empty()
748
2116
 
749
- ##
2117
+ def provide(self, key: ta.Any) -> ta.Any:
2118
+ v = self.try_provide(key)
2119
+ if v.present:
2120
+ return v.must()
2121
+ raise UnboundInjectorKeyError(key)
750
2122
 
2123
+ def provide_kwargs(self, obj: ta.Any) -> ta.Mapping[str, ta.Any]:
2124
+ kt = build_injection_kwargs_target(obj)
2125
+ ret: ta.Dict[str, ta.Any] = {}
2126
+ for kw in kt.kwargs:
2127
+ if kw.has_default:
2128
+ if not (mv := self.try_provide(kw.key)).present:
2129
+ continue
2130
+ v = mv.must()
2131
+ else:
2132
+ v = self.provide(kw.key)
2133
+ ret[kw.name] = v
2134
+ return ret
751
2135
 
752
- class ProcessStates:
753
- STOPPED = 0
754
- STARTING = 10
755
- RUNNING = 20
756
- BACKOFF = 30
757
- STOPPING = 40
758
- EXITED = 100
759
- FATAL = 200
760
- UNKNOWN = 1000
2136
+ def inject(self, obj: ta.Any) -> ta.Any:
2137
+ kws = self.provide_kwargs(obj)
2138
+ return obj(**kws)
761
2139
 
762
2140
 
763
- STOPPED_STATES = (
764
- ProcessStates.STOPPED,
765
- ProcessStates.EXITED,
766
- ProcessStates.FATAL,
767
- ProcessStates.UNKNOWN,
768
- )
2141
+ ###
2142
+ # injection helpers
769
2143
 
770
- RUNNING_STATES = (
771
- ProcessStates.RUNNING,
772
- ProcessStates.BACKOFF,
773
- ProcessStates.STARTING,
774
- )
775
2144
 
776
- SIGNALLABLE_STATES = (
777
- ProcessStates.RUNNING,
778
- ProcessStates.STARTING,
779
- ProcessStates.STOPPING,
780
- )
2145
+ class Injection:
2146
+ def __new__(cls, *args, **kwargs): # noqa
2147
+ raise TypeError
781
2148
 
2149
+ # keys
782
2150
 
783
- _process_states_by_code = _names_by_code(ProcessStates)
2151
+ @classmethod
2152
+ def as_key(cls, o: ta.Any) -> InjectorKey:
2153
+ return as_injector_key(o)
784
2154
 
2155
+ @classmethod
2156
+ def array(cls, o: ta.Any) -> InjectorKey:
2157
+ return dc.replace(as_injector_key(o), array=True)
785
2158
 
786
- def get_process_state_description(code: ProcessState) -> str:
787
- return check_not_none(_process_states_by_code.get(code))
2159
+ @classmethod
2160
+ def tag(cls, o: ta.Any, t: ta.Any) -> InjectorKey:
2161
+ return dc.replace(as_injector_key(o), tag=t)
788
2162
 
2163
+ # bindings
789
2164
 
790
- ##
2165
+ @classmethod
2166
+ def as_bindings(cls, *args: InjectorBindingOrBindings) -> InjectorBindings:
2167
+ return as_injector_bindings(*args)
791
2168
 
2169
+ @classmethod
2170
+ def override(cls, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
2171
+ return injector_override(p, *args)
792
2172
 
793
- class SupervisorStates:
794
- FATAL = 2
795
- RUNNING = 1
796
- RESTARTING = 0
797
- SHUTDOWN = -1
2173
+ # binder
2174
+
2175
+ @classmethod
2176
+ def bind(
2177
+ cls,
2178
+ obj: ta.Any,
2179
+ *,
2180
+ key: ta.Any = None,
2181
+ tag: ta.Any = None,
2182
+ array: ta.Optional[bool] = None, # noqa
2183
+
2184
+ to_fn: ta.Any = None,
2185
+ to_ctor: ta.Any = None,
2186
+ to_const: ta.Any = None,
2187
+ to_key: ta.Any = None,
2188
+
2189
+ singleton: bool = False,
2190
+ ) -> InjectorBinding:
2191
+ return InjectorBinder.bind(
2192
+ obj,
2193
+
2194
+ key=key,
2195
+ tag=tag,
2196
+ array=array,
2197
+
2198
+ to_fn=to_fn,
2199
+ to_ctor=to_ctor,
2200
+ to_const=to_const,
2201
+ to_key=to_key,
2202
+
2203
+ singleton=singleton,
2204
+ )
798
2205
 
2206
+ # injector
799
2207
 
800
- _supervisor_states_by_code = _names_by_code(SupervisorStates)
2208
+ @classmethod
2209
+ def create_injector(cls, *args: InjectorBindingOrBindings, p: ta.Optional[Injector] = None) -> Injector:
2210
+ return _Injector(as_injector_bindings(*args), p)
801
2211
 
802
2212
 
803
- def get_supervisor_state_description(code: SupervisorState) -> str:
804
- return check_not_none(_supervisor_states_by_code.get(code))
2213
+ inj = Injection
805
2214
 
806
2215
 
807
2216
  ########################################
@@ -1576,6 +2985,66 @@ def unmarshal_obj(o: ta.Any, ty: ta.Union[ta.Type[T], ta.Any]) -> T:
1576
2985
  return get_obj_marshaler(ty).unmarshal(o)
1577
2986
 
1578
2987
 
2988
+ ########################################
2989
+ # ../../configs.py
2990
+
2991
+
2992
+ def read_config_file(
2993
+ path: str,
2994
+ cls: ta.Type[T],
2995
+ *,
2996
+ prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
2997
+ ) -> T:
2998
+ with open(path) as cf:
2999
+ if path.endswith('.toml'):
3000
+ config_dct = toml_loads(cf.read())
3001
+ else:
3002
+ config_dct = json.loads(cf.read())
3003
+
3004
+ if prepare is not None:
3005
+ config_dct = prepare(config_dct) # type: ignore
3006
+
3007
+ return unmarshal_obj(config_dct, cls)
3008
+
3009
+
3010
+ def build_config_named_children(
3011
+ o: ta.Union[
3012
+ ta.Sequence[ConfigMapping],
3013
+ ta.Mapping[str, ConfigMapping],
3014
+ None,
3015
+ ],
3016
+ *,
3017
+ name_key: str = 'name',
3018
+ ) -> ta.Optional[ta.Sequence[ConfigMapping]]:
3019
+ if o is None:
3020
+ return None
3021
+
3022
+ lst: ta.List[ConfigMapping] = []
3023
+ if isinstance(o, ta.Mapping):
3024
+ for k, v in o.items():
3025
+ check_isinstance(v, ta.Mapping)
3026
+ if name_key in v:
3027
+ n = v[name_key]
3028
+ if k != n:
3029
+ raise KeyError(f'Given names do not match: {n} != {k}')
3030
+ lst.append(v)
3031
+ else:
3032
+ lst.append({name_key: k, **v})
3033
+
3034
+ else:
3035
+ check_not_isinstance(o, str)
3036
+ lst.extend(o)
3037
+
3038
+ seen = set()
3039
+ for d in lst:
3040
+ n = d['name']
3041
+ if n in d:
3042
+ raise KeyError(f'Duplicate name: {n}')
3043
+ seen.add(n)
3044
+
3045
+ return lst
3046
+
3047
+
1579
3048
  ########################################
1580
3049
  # ../events.py
1581
3050
 
@@ -2108,6 +3577,127 @@ else:
2108
3577
  Poller = SelectPoller
2109
3578
 
2110
3579
 
3580
+ ########################################
3581
+ # ../configs.py
3582
+
3583
+
3584
+ ##
3585
+
3586
+
3587
+ @dc.dataclass(frozen=True)
3588
+ class ProcessConfig:
3589
+ name: str
3590
+ command: str
3591
+
3592
+ uid: ta.Optional[int] = None
3593
+ directory: ta.Optional[str] = None
3594
+ umask: ta.Optional[int] = None
3595
+ priority: int = 999
3596
+
3597
+ autostart: bool = True
3598
+ autorestart: str = 'unexpected'
3599
+
3600
+ startsecs: int = 1
3601
+ startretries: int = 3
3602
+
3603
+ numprocs: int = 1
3604
+ numprocs_start: int = 0
3605
+
3606
+ @dc.dataclass(frozen=True)
3607
+ class Log:
3608
+ file: ta.Optional[str] = None
3609
+ capture_maxbytes: ta.Optional[int] = None
3610
+ events_enabled: bool = False
3611
+ syslog: bool = False
3612
+ backups: ta.Optional[int] = None
3613
+ maxbytes: ta.Optional[int] = None
3614
+
3615
+ stdout: Log = Log()
3616
+ stderr: Log = Log()
3617
+
3618
+ stopsignal: int = signal.SIGTERM
3619
+ stopwaitsecs: int = 10
3620
+ stopasgroup: bool = False
3621
+
3622
+ killasgroup: bool = False
3623
+
3624
+ exitcodes: ta.Sequence[int] = (0,)
3625
+
3626
+ redirect_stderr: bool = False
3627
+
3628
+ environment: ta.Optional[ta.Mapping[str, str]] = None
3629
+
3630
+
3631
+ @dc.dataclass(frozen=True)
3632
+ class ProcessGroupConfig:
3633
+ name: str
3634
+
3635
+ priority: int = 999
3636
+
3637
+ processes: ta.Optional[ta.Sequence[ProcessConfig]] = None
3638
+
3639
+
3640
+ @dc.dataclass(frozen=True)
3641
+ class ServerConfig:
3642
+ user: ta.Optional[str] = None
3643
+ nodaemon: bool = False
3644
+ umask: int = 0o22
3645
+ directory: ta.Optional[str] = None
3646
+ logfile: str = 'supervisord.log'
3647
+ logfile_maxbytes: int = 50 * 1024 * 1024
3648
+ logfile_backups: int = 10
3649
+ loglevel: int = logging.INFO
3650
+ pidfile: str = 'supervisord.pid'
3651
+ identifier: str = 'supervisor'
3652
+ child_logdir: str = '/dev/null'
3653
+ minfds: int = 1024
3654
+ minprocs: int = 200
3655
+ nocleanup: bool = False
3656
+ strip_ansi: bool = False
3657
+ silent: bool = False
3658
+
3659
+ groups: ta.Optional[ta.Sequence[ProcessGroupConfig]] = None
3660
+
3661
+ @classmethod
3662
+ def new(
3663
+ cls,
3664
+ umask: ta.Union[int, str] = 0o22,
3665
+ directory: ta.Optional[str] = None,
3666
+ logfile: str = 'supervisord.log',
3667
+ logfile_maxbytes: ta.Union[int, str] = 50 * 1024 * 1024,
3668
+ loglevel: ta.Union[int, str] = logging.INFO,
3669
+ pidfile: str = 'supervisord.pid',
3670
+ child_logdir: ta.Optional[str] = None,
3671
+ **kwargs: ta.Any,
3672
+ ) -> 'ServerConfig':
3673
+ return cls(
3674
+ umask=octal_type(umask),
3675
+ directory=existing_directory(directory) if directory is not None else None,
3676
+ logfile=existing_dirpath(logfile),
3677
+ logfile_maxbytes=byte_size(logfile_maxbytes),
3678
+ loglevel=logging_level(loglevel),
3679
+ pidfile=existing_dirpath(pidfile),
3680
+ child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
3681
+ **kwargs,
3682
+ )
3683
+
3684
+
3685
+ ##
3686
+
3687
+
3688
+ def prepare_process_group_config(dct: ConfigMapping) -> ConfigMapping:
3689
+ out = dict(dct)
3690
+ out['processes'] = build_config_named_children(out.get('processes'))
3691
+ return out
3692
+
3693
+
3694
+ def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.Any]:
3695
+ out = dict(dct)
3696
+ group_dcts = build_config_named_children(out.get('groups'))
3697
+ out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
3698
+ return out
3699
+
3700
+
2111
3701
  ########################################
2112
3702
  # ../types.py
2113
3703
 
@@ -2132,6 +3722,11 @@ class AbstractServerContext(abc.ABC):
2132
3722
  def pid_history(self) -> ta.Dict[int, 'AbstractSubprocess']:
2133
3723
  raise NotImplementedError
2134
3724
 
3725
+ @property
3726
+ @abc.abstractmethod
3727
+ def inherited_fds(self) -> ta.FrozenSet[int]:
3728
+ raise NotImplementedError
3729
+
2135
3730
 
2136
3731
  class AbstractSubprocess(abc.ABC):
2137
3732
  @property
@@ -2163,12 +3758,14 @@ class ServerContext(AbstractServerContext):
2163
3758
  self,
2164
3759
  config: ServerConfig,
2165
3760
  *,
2166
- epoch: int = 0,
3761
+ epoch: ServerEpoch = ServerEpoch(0),
3762
+ inherited_fds: ta.Optional[InheritedFds] = None,
2167
3763
  ) -> None:
2168
3764
  super().__init__()
2169
3765
 
2170
3766
  self._config = config
2171
3767
  self._epoch = epoch
3768
+ self._inherited_fds = InheritedFds(frozenset(inherited_fds or []))
2172
3769
 
2173
3770
  self._pid_history: ta.Dict[int, AbstractSubprocess] = {}
2174
3771
  self._state: SupervisorState = SupervisorStates.RUNNING
@@ -2192,7 +3789,7 @@ class ServerContext(AbstractServerContext):
2192
3789
  return self._config
2193
3790
 
2194
3791
  @property
2195
- def epoch(self) -> int:
3792
+ def epoch(self) -> ServerEpoch:
2196
3793
  return self._epoch
2197
3794
 
2198
3795
  @property
@@ -2222,6 +3819,10 @@ class ServerContext(AbstractServerContext):
2222
3819
  def gid(self) -> ta.Optional[int]:
2223
3820
  return self._gid
2224
3821
 
3822
+ @property
3823
+ def inherited_fds(self) -> InheritedFds:
3824
+ return self._inherited_fds
3825
+
2225
3826
  ##
2226
3827
 
2227
3828
  def set_signals(self) -> None:
@@ -2865,6 +4466,9 @@ class InputDispatcher(Dispatcher):
2865
4466
  # ../process.py
2866
4467
 
2867
4468
 
4469
+ ##
4470
+
4471
+
2868
4472
  @functools.total_ordering
2869
4473
  class Subprocess(AbstractSubprocess):
2870
4474
  """A class to manage a subprocess."""
@@ -2890,7 +4494,12 @@ class Subprocess(AbstractSubprocess):
2890
4494
  spawn_err = None # error message attached by spawn() if any
2891
4495
  group = None # ProcessGroup instance if process is in the group
2892
4496
 
2893
- def __init__(self, config: ProcessConfig, group: 'ProcessGroup', context: AbstractServerContext) -> None:
4497
+ def __init__(
4498
+ self,
4499
+ config: ProcessConfig,
4500
+ group: 'ProcessGroup',
4501
+ context: AbstractServerContext,
4502
+ ) -> None:
2894
4503
  super().__init__()
2895
4504
  self._config = config
2896
4505
  self.group = group
@@ -3134,8 +4743,10 @@ class Subprocess(AbstractSubprocess):
3134
4743
  os.dup2(self._pipes['child_stdout'], 2)
3135
4744
  else:
3136
4745
  os.dup2(self._pipes['child_stderr'], 2)
3137
- # FIXME: leave debugger fds
4746
+
3138
4747
  for i in range(3, self.context.config.minfds):
4748
+ if i in self.context.inherited_fds:
4749
+ continue
3139
4750
  close_fd(i)
3140
4751
 
3141
4752
  def _spawn_as_child(self, filename: str, argv: ta.Sequence[str]) -> None:
@@ -3529,15 +5140,39 @@ class Subprocess(AbstractSubprocess):
3529
5140
  pass
3530
5141
 
3531
5142
 
5143
+ ##
5144
+
5145
+
5146
+ @dc.dataclass(frozen=True)
5147
+ class SubprocessFactory:
5148
+ fn: ta.Callable[[ProcessConfig, 'ProcessGroup'], Subprocess]
5149
+
5150
+ def __call__(self, config: ProcessConfig, group: 'ProcessGroup') -> Subprocess:
5151
+ return self.fn(config, group)
5152
+
5153
+
3532
5154
  @functools.total_ordering
3533
5155
  class ProcessGroup:
3534
- def __init__(self, config: ProcessGroupConfig, context: ServerContext):
5156
+ def __init__(
5157
+ self,
5158
+ config: ProcessGroupConfig,
5159
+ context: ServerContext,
5160
+ *,
5161
+ subprocess_factory: ta.Optional[SubprocessFactory] = None,
5162
+ ):
3535
5163
  super().__init__()
3536
5164
  self.config = config
3537
5165
  self.context = context
5166
+
5167
+ if subprocess_factory is None:
5168
+ def make_subprocess(config: ProcessConfig, group: ProcessGroup) -> Subprocess:
5169
+ return Subprocess(config, group, self.context)
5170
+ subprocess_factory = SubprocessFactory(make_subprocess)
5171
+ self._subprocess_factory = subprocess_factory
5172
+
3538
5173
  self.processes = {}
3539
5174
  for pconfig in self.config.processes or []:
3540
- process = Subprocess(pconfig, self, self.context)
5175
+ process = self._subprocess_factory(pconfig, self)
3541
5176
  self.processes[pconfig.name] = process
3542
5177
 
3543
5178
  def __lt__(self, other):
@@ -3607,12 +5242,32 @@ def timeslice(period: int, when: float) -> int:
3607
5242
  return int(when - (when % period))
3608
5243
 
3609
5244
 
5245
+ @dc.dataclass(frozen=True)
5246
+ class ProcessGroupFactory:
5247
+ fn: ta.Callable[[ProcessGroupConfig], ProcessGroup]
5248
+
5249
+ def __call__(self, config: ProcessGroupConfig) -> ProcessGroup:
5250
+ return self.fn(config)
5251
+
5252
+
3610
5253
  class Supervisor:
3611
5254
 
3612
- def __init__(self, context: ServerContext) -> None:
5255
+ def __init__(
5256
+ self,
5257
+ context: ServerContext,
5258
+ *,
5259
+ process_group_factory: ta.Optional[ProcessGroupFactory] = None,
5260
+ ) -> None:
3613
5261
  super().__init__()
3614
5262
 
3615
5263
  self._context = context
5264
+
5265
+ if process_group_factory is None:
5266
+ def make_process_group(config: ProcessGroupConfig) -> ProcessGroup:
5267
+ return ProcessGroup(config, self._context)
5268
+ process_group_factory = ProcessGroupFactory(make_process_group)
5269
+ self._process_group_factory = process_group_factory
5270
+
3616
5271
  self._ticks: ta.Dict[int, float] = {}
3617
5272
  self._process_groups: ta.Dict[str, ProcessGroup] = {} # map of process group name to process group object
3618
5273
  self._stop_groups: ta.Optional[ta.List[ProcessGroup]] = None # list used for priority ordered shutdown
@@ -3654,7 +5309,7 @@ class Supervisor:
3654
5309
  if name in self._process_groups:
3655
5310
  return False
3656
5311
 
3657
- group = self._process_groups[name] = ProcessGroup(config, self._context)
5312
+ group = self._process_groups[name] = self._process_group_factory(config)
3658
5313
  group.after_setuid()
3659
5314
 
3660
5315
  EVENT_CALLBACKS.notify(ProcessGroupAddedEvent(name))
@@ -3924,6 +5579,53 @@ class Supervisor:
3924
5579
  # main.py
3925
5580
 
3926
5581
 
5582
+ ##
5583
+
5584
+
5585
+ def build_server_bindings(
5586
+ config: ServerConfig,
5587
+ *,
5588
+ server_epoch: ta.Optional[ServerEpoch] = None,
5589
+ inherited_fds: ta.Optional[InheritedFds] = None,
5590
+ ) -> InjectorBindings:
5591
+ lst: ta.List[InjectorBindingOrBindings] = [
5592
+ inj.bind(config),
5593
+
5594
+ inj.bind(ServerContext, singleton=True),
5595
+ inj.bind(AbstractServerContext, to_key=ServerContext),
5596
+
5597
+ inj.bind(Supervisor, singleton=True),
5598
+ ]
5599
+
5600
+ #
5601
+
5602
+ def make_process_group_factory(injector: Injector) -> ProcessGroupFactory:
5603
+ def inner(group_config: ProcessGroupConfig) -> ProcessGroup:
5604
+ return injector.inject(functools.partial(ProcessGroup, group_config))
5605
+ return ProcessGroupFactory(inner)
5606
+ lst.append(inj.bind(make_process_group_factory))
5607
+
5608
+ def make_subprocess_factory(injector: Injector) -> SubprocessFactory:
5609
+ def inner(process_config: ProcessConfig, group: ProcessGroup) -> Subprocess:
5610
+ return injector.inject(functools.partial(Subprocess, process_config, group))
5611
+ return SubprocessFactory(inner)
5612
+ lst.append(inj.bind(make_subprocess_factory))
5613
+
5614
+ #
5615
+
5616
+ if server_epoch is not None:
5617
+ lst.append(inj.bind(server_epoch, key=ServerEpoch))
5618
+ if inherited_fds is not None:
5619
+ lst.append(inj.bind(inherited_fds, key=InheritedFds))
5620
+
5621
+ #
5622
+
5623
+ return inj.as_bindings(*lst)
5624
+
5625
+
5626
+ ##
5627
+
5628
+
3927
5629
  def main(
3928
5630
  argv: ta.Optional[ta.Sequence[str]] = None,
3929
5631
  *,
@@ -3934,6 +5636,7 @@ def main(
3934
5636
  parser = argparse.ArgumentParser()
3935
5637
  parser.add_argument('config_file', metavar='config-file')
3936
5638
  parser.add_argument('--no-journald', action='store_true')
5639
+ parser.add_argument('--inherit-initial-fds', action='store_true')
3937
5640
  args = parser.parse_args(argv)
3938
5641
 
3939
5642
  #
@@ -3949,20 +5652,27 @@ def main(
3949
5652
 
3950
5653
  #
3951
5654
 
5655
+ initial_fds: ta.Optional[InheritedFds] = None
5656
+ if args.inherit_initial_fds:
5657
+ initial_fds = InheritedFds(get_open_fds(0x10000))
5658
+
3952
5659
  # if we hup, restart by making a new Supervisor()
3953
5660
  for epoch in itertools.count():
3954
- with open(cf) as f:
3955
- config_src = f.read()
3956
-
3957
- config_dct = json.loads(config_src)
3958
- config: ServerConfig = unmarshal_obj(config_dct, ServerConfig)
5661
+ config = read_config_file(
5662
+ os.path.expanduser(cf),
5663
+ ServerConfig,
5664
+ prepare=prepare_server_config,
5665
+ )
3959
5666
 
3960
- context = ServerContext(
5667
+ injector = inj.create_injector(build_server_bindings(
3961
5668
  config,
3962
- epoch=epoch,
3963
- )
5669
+ server_epoch=ServerEpoch(epoch),
5670
+ inherited_fds=initial_fds,
5671
+ ))
5672
+
5673
+ context = injector.provide(ServerContext)
5674
+ supervisor = injector.provide(Supervisor)
3964
5675
 
3965
- supervisor = Supervisor(context)
3966
5676
  try:
3967
5677
  supervisor.main()
3968
5678
  except ExitNow: