ominfra 0.0.0.dev138__py3-none-any.whl → 0.0.0.dev140__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. ominfra/manage/__init__.py +13 -0
  2. ominfra/manage/{new/commands → commands}/base.py +9 -7
  3. ominfra/manage/{new/commands → commands}/subprocess.py +20 -15
  4. ominfra/manage/main.py +175 -0
  5. ominfra/manage/payload.py +35 -0
  6. ominfra/manage/spawning.py +100 -0
  7. ominfra/pyremote.py +18 -8
  8. ominfra/scripts/journald2aws.py +7 -0
  9. ominfra/{manage/new/_manage.py → scripts/manage.py} +248 -153
  10. ominfra/scripts/supervisor.py +7 -0
  11. {ominfra-0.0.0.dev138.dist-info → ominfra-0.0.0.dev140.dist-info}/METADATA +3 -3
  12. {ominfra-0.0.0.dev138.dist-info → ominfra-0.0.0.dev140.dist-info}/RECORD +17 -44
  13. ominfra/manage/deploy/_executor.py +0 -1415
  14. ominfra/manage/deploy/configs.py +0 -19
  15. ominfra/manage/deploy/executor/__init__.py +0 -1
  16. ominfra/manage/deploy/executor/base.py +0 -115
  17. ominfra/manage/deploy/executor/concerns/__init__.py +0 -0
  18. ominfra/manage/deploy/executor/concerns/dirs.py +0 -28
  19. ominfra/manage/deploy/executor/concerns/nginx.py +0 -47
  20. ominfra/manage/deploy/executor/concerns/repo.py +0 -17
  21. ominfra/manage/deploy/executor/concerns/supervisor.py +0 -46
  22. ominfra/manage/deploy/executor/concerns/systemd.py +0 -88
  23. ominfra/manage/deploy/executor/concerns/user.py +0 -25
  24. ominfra/manage/deploy/executor/concerns/venv.py +0 -22
  25. ominfra/manage/deploy/executor/main.py +0 -119
  26. ominfra/manage/deploy/poly/__init__.py +0 -1
  27. ominfra/manage/deploy/poly/_main.py +0 -975
  28. ominfra/manage/deploy/poly/base.py +0 -178
  29. ominfra/manage/deploy/poly/configs.py +0 -38
  30. ominfra/manage/deploy/poly/deploy.py +0 -25
  31. ominfra/manage/deploy/poly/main.py +0 -18
  32. ominfra/manage/deploy/poly/nginx.py +0 -60
  33. ominfra/manage/deploy/poly/repo.py +0 -41
  34. ominfra/manage/deploy/poly/runtime.py +0 -39
  35. ominfra/manage/deploy/poly/site.py +0 -11
  36. ominfra/manage/deploy/poly/supervisor.py +0 -64
  37. ominfra/manage/deploy/poly/venv.py +0 -52
  38. ominfra/manage/deploy/remote.py +0 -91
  39. ominfra/manage/manage.py +0 -12
  40. ominfra/manage/new/__init__.py +0 -1
  41. ominfra/manage/new/commands/__init__.py +0 -0
  42. ominfra/manage/new/main.py +0 -234
  43. /ominfra/manage/{deploy → commands}/__init__.py +0 -0
  44. {ominfra-0.0.0.dev138.dist-info → ominfra-0.0.0.dev140.dist-info}/LICENSE +0 -0
  45. {ominfra-0.0.0.dev138.dist-info → ominfra-0.0.0.dev140.dist-info}/WHEEL +0 -0
  46. {ominfra-0.0.0.dev138.dist-info → ominfra-0.0.0.dev140.dist-info}/entry_points.txt +0 -0
  47. {ominfra-0.0.0.dev138.dist-info → ominfra-0.0.0.dev140.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,13 @@
1
+ # @omlish-lite
2
+ """
3
+ Jobs:
4
+ - globals
5
+ - pkgs
6
+ - pyenv
7
+ - tailscale
8
+ - docker
9
+ - system nginx
10
+ - system service manager - systemd / supervisor
11
+ - users
12
+ - firewall
13
+ """
@@ -4,22 +4,24 @@ import dataclasses as dc
4
4
  import typing as ta
5
5
 
6
6
 
7
- CommandInputT = ta.TypeVar('CommandInputT', bound='Command.Input')
7
+ CommandT = ta.TypeVar('CommandT', bound='Command')
8
8
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
9
9
 
10
10
 
11
11
  ##
12
12
 
13
13
 
14
- class Command(abc.ABC, ta.Generic[CommandInputT, CommandOutputT]):
15
- @dc.dataclass(frozen=True)
16
- class Input(abc.ABC): # noqa
17
- pass
18
-
14
+ @dc.dataclass(frozen=True)
15
+ class Command(abc.ABC, ta.Generic[CommandOutputT]):
19
16
  @dc.dataclass(frozen=True)
20
17
  class Output(abc.ABC): # noqa
21
18
  pass
22
19
 
20
+
21
+ ##
22
+
23
+
24
+ class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
23
25
  @abc.abstractmethod
24
- def _execute(self, inp: CommandInputT) -> CommandOutputT:
26
+ def execute(self, i: CommandT) -> CommandOutputT:
25
27
  raise NotImplementedError
@@ -8,29 +8,29 @@ import typing as ta
8
8
  from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
9
9
 
10
10
  from .base import Command
11
+ from .base import CommandExecutor
11
12
 
12
13
 
13
14
  ##
14
15
 
15
16
 
16
- class SubprocessCommand(Command['SubprocessCommand.Input', 'SubprocessCommand.Output']):
17
- @dc.dataclass(frozen=True)
18
- class Input(Command.Input):
19
- args: ta.Sequence[str]
17
+ @dc.dataclass(frozen=True)
18
+ class SubprocessCommand(Command['SubprocessCommand.Output']):
19
+ args: ta.Sequence[str]
20
20
 
21
- shell: bool = False
22
- cwd: ta.Optional[str] = None
23
- env: ta.Optional[ta.Mapping[str, str]] = None
21
+ shell: bool = False
22
+ cwd: ta.Optional[str] = None
23
+ env: ta.Optional[ta.Mapping[str, str]] = None
24
24
 
25
- capture_stdout: bool = False
26
- capture_stderr: bool = False
25
+ capture_stdout: bool = False
26
+ capture_stderr: bool = False
27
27
 
28
- input: ta.Optional[bytes] = None
29
- timeout: ta.Optional[float] = None
28
+ input: ta.Optional[bytes] = None
29
+ timeout: ta.Optional[float] = None
30
30
 
31
- def __post_init__(self) -> None:
32
- if isinstance(self.args, str):
33
- raise TypeError(self.args)
31
+ def __post_init__(self) -> None:
32
+ if isinstance(self.args, str):
33
+ raise TypeError(self.args)
34
34
 
35
35
  @dc.dataclass(frozen=True)
36
36
  class Output(Command.Output):
@@ -42,7 +42,12 @@ class SubprocessCommand(Command['SubprocessCommand.Input', 'SubprocessCommand.Ou
42
42
  stdout: ta.Optional[bytes] = None
43
43
  stderr: ta.Optional[bytes] = None
44
44
 
45
- def _execute(self, inp: Input) -> Output:
45
+
46
+ ##
47
+
48
+
49
+ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
50
+ def execute(self, inp: SubprocessCommand) -> SubprocessCommand.Output:
46
51
  proc = subprocess.Popen(
47
52
  subprocess_maybe_shell_wrap_exec(*inp.args),
48
53
 
ominfra/manage/main.py ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ # @omlish-amalg ../scripts/manage.py
3
+ # ruff: noqa: UP006 UP007
4
+ """
5
+ manage.py -s 'docker run -i python:3.12'
6
+ manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
7
+ """
8
+ import json
9
+ import struct
10
+ import typing as ta
11
+
12
+ from omlish.lite.cached import static_init
13
+ from omlish.lite.json import json_dumps_compact
14
+ from omlish.lite.marshal import PolymorphicObjMarshaler
15
+ from omlish.lite.marshal import get_obj_marshaler
16
+ from omlish.lite.marshal import marshal_obj
17
+ from omlish.lite.marshal import register_opj_marshaler
18
+ from omlish.lite.marshal import unmarshal_obj
19
+
20
+ from ..pyremote import PyremoteBootstrapDriver
21
+ from ..pyremote import PyremoteBootstrapOptions
22
+ from ..pyremote import pyremote_bootstrap_finalize
23
+ from ..pyremote import pyremote_build_bootstrap_cmd
24
+ from .commands.base import Command
25
+ from .commands.subprocess import SubprocessCommand
26
+ from .commands.subprocess import SubprocessCommandExecutor
27
+ from .payload import get_payload_src
28
+ from .spawning import PySpawner
29
+
30
+
31
+ ##
32
+
33
+
34
+ _COMMAND_TYPES = {
35
+ 'subprocess': SubprocessCommand,
36
+ }
37
+
38
+
39
+ @static_init
40
+ def _register_command_marshaling() -> None:
41
+ for fn in [
42
+ lambda c: c,
43
+ lambda c: c.Output,
44
+ ]:
45
+ register_opj_marshaler(
46
+ fn(Command),
47
+ PolymorphicObjMarshaler.of([
48
+ PolymorphicObjMarshaler.Impl(
49
+ fn(cty),
50
+ k,
51
+ get_obj_marshaler(fn(cty)),
52
+ )
53
+ for k, cty in _COMMAND_TYPES.items()
54
+ ]),
55
+ )
56
+
57
+
58
+ ##
59
+
60
+
61
+ def _send_obj(f: ta.IO, o: ta.Any, ty: ta.Any = None) -> None:
62
+ j = json_dumps_compact(marshal_obj(o, ty))
63
+ d = j.encode('utf-8')
64
+
65
+ f.write(struct.pack('<I', len(d)))
66
+ f.write(d)
67
+ f.flush()
68
+
69
+
70
+ def _recv_obj(f: ta.IO, ty: ta.Any) -> ta.Any:
71
+ d = f.read(4)
72
+ if not d:
73
+ return None
74
+ if len(d) != 4:
75
+ raise EOFError
76
+
77
+ sz = struct.unpack('<I', d)[0]
78
+ d = f.read(sz)
79
+ if len(d) != sz:
80
+ raise EOFError
81
+
82
+ j = json.loads(d.decode('utf-8'))
83
+ return unmarshal_obj(j, ty)
84
+
85
+
86
+ ##
87
+
88
+
89
+ def _remote_main() -> None:
90
+ rt = pyremote_bootstrap_finalize() # noqa
91
+
92
+ while True:
93
+ i = _recv_obj(rt.input, Command)
94
+ if i is None:
95
+ break
96
+
97
+ if isinstance(i, SubprocessCommand):
98
+ o = SubprocessCommandExecutor().execute(i) # noqa
99
+ else:
100
+ raise TypeError(i)
101
+
102
+ _send_obj(rt.output, o, Command.Output)
103
+
104
+
105
+ ##
106
+
107
+
108
+ def _main() -> None:
109
+ import argparse
110
+
111
+ parser = argparse.ArgumentParser()
112
+
113
+ parser.add_argument('-s', '--shell')
114
+ parser.add_argument('-q', '--shell-quote', action='store_true')
115
+ parser.add_argument('--python', default='python3')
116
+ parser.add_argument('--debug', action='store_true')
117
+ parser.add_argument('--_payload-file')
118
+
119
+ args = parser.parse_args()
120
+
121
+ #
122
+
123
+ payload_src = get_payload_src(file=args._payload_file) # noqa
124
+
125
+ #
126
+
127
+ remote_src = '\n\n'.join([
128
+ '__name__ = "__remote__"',
129
+ payload_src,
130
+ '_remote_main()',
131
+ ])
132
+
133
+ #
134
+
135
+ bs_src = pyremote_build_bootstrap_cmd(__package__ or 'manage')
136
+
137
+ #
138
+
139
+ spawner = PySpawner(
140
+ bs_src,
141
+ shell=args.shell,
142
+ shell_quote=args.shell_quote,
143
+ python=args.python,
144
+ )
145
+ with spawner.spawn() as proc:
146
+ res = PyremoteBootstrapDriver( # noqa
147
+ remote_src,
148
+ PyremoteBootstrapOptions(
149
+ debug=args.debug,
150
+ ),
151
+ ).run(proc.stdin, proc.stdout)
152
+ # print(res)
153
+
154
+ #
155
+
156
+ for ci in [
157
+ SubprocessCommand(
158
+ args=['python3', '-'],
159
+ input=b'print(1)\n',
160
+ capture_stdout=True,
161
+ ),
162
+ SubprocessCommand(
163
+ args=['uname'],
164
+ capture_stdout=True,
165
+ ),
166
+ ]:
167
+ _send_obj(proc.stdin, ci, Command)
168
+
169
+ o = _recv_obj(proc.stdout, Command.Output)
170
+
171
+ print(o)
172
+
173
+
174
+ if __name__ == '__main__':
175
+ _main()
@@ -0,0 +1,35 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import inspect
3
+ import sys
4
+ import typing as ta
5
+
6
+ from omlish.lite.cached import cached_nullary
7
+
8
+
9
+ @cached_nullary
10
+ def _get_self_src() -> str:
11
+ return inspect.getsource(sys.modules[__name__])
12
+
13
+
14
+ def _is_src_amalg(src: str) -> bool:
15
+ for l in src.splitlines(): # noqa
16
+ if l.startswith('# @omlish-amalg-output '):
17
+ return True
18
+ return False
19
+
20
+
21
+ @cached_nullary
22
+ def _is_self_amalg() -> bool:
23
+ return _is_src_amalg(_get_self_src())
24
+
25
+
26
+ def get_payload_src(*, file: ta.Optional[str]) -> str:
27
+ if file is not None:
28
+ with open(file) as f:
29
+ return f.read()
30
+
31
+ if _is_self_amalg():
32
+ return _get_self_src()
33
+
34
+ import importlib.resources
35
+ return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
@@ -0,0 +1,100 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import contextlib
3
+ import dataclasses as dc
4
+ import shlex
5
+ import subprocess
6
+ import typing as ta
7
+
8
+ from omlish.lite.check import check_not_none
9
+ from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
10
+
11
+
12
+ class PySpawner:
13
+ DEFAULT_PYTHON = 'python3'
14
+
15
+ def __init__(
16
+ self,
17
+ src: str,
18
+ *,
19
+ shell: ta.Optional[str] = None,
20
+ shell_quote: bool = False,
21
+ python: str = DEFAULT_PYTHON,
22
+ stderr: ta.Optional[ta.Literal['pipe', 'stdout', 'devnull']] = None,
23
+ ) -> None:
24
+ super().__init__()
25
+
26
+ self._src = src
27
+ self._shell = shell
28
+ self._shell_quote = shell_quote
29
+ self._python = python
30
+ self._stderr = stderr
31
+
32
+ #
33
+
34
+ class _PreparedCmd(ta.NamedTuple):
35
+ cmd: ta.Sequence[str]
36
+ shell: bool
37
+
38
+ def _prepare_cmd(self) -> _PreparedCmd:
39
+ if self._shell is not None:
40
+ sh_src = f'{self._python} -c {shlex.quote(self._src)}'
41
+ if self._shell_quote:
42
+ sh_src = shlex.quote(sh_src)
43
+ sh_cmd = f'{self._shell} {sh_src}'
44
+ return PySpawner._PreparedCmd(
45
+ cmd=[sh_cmd],
46
+ shell=True,
47
+ )
48
+
49
+ else:
50
+ return PySpawner._PreparedCmd(
51
+ cmd=[self._python, '-c', self._src],
52
+ shell=False,
53
+ )
54
+
55
+ #
56
+
57
+ _STDERR_KWARG_MAP: ta.Mapping[str, int] = {
58
+ 'pipe': subprocess.PIPE,
59
+ 'stdout': subprocess.STDOUT,
60
+ 'devnull': subprocess.DEVNULL,
61
+ }
62
+
63
+ @dc.dataclass(frozen=True)
64
+ class Spawned:
65
+ stdin: ta.IO
66
+ stdout: ta.IO
67
+ stderr: ta.Optional[ta.IO]
68
+
69
+ @contextlib.contextmanager
70
+ def spawn(
71
+ self,
72
+ *,
73
+ timeout: ta.Optional[float] = None,
74
+ ) -> ta.Generator[Spawned, None, None]:
75
+ pc = self._prepare_cmd()
76
+
77
+ with subprocess.Popen(
78
+ subprocess_maybe_shell_wrap_exec(*pc.cmd),
79
+ shell=pc.shell,
80
+ stdin=subprocess.PIPE,
81
+ stdout=subprocess.PIPE,
82
+ stderr=self._STDERR_KWARG_MAP[self._stderr] if self._stderr is not None else None,
83
+ ) as proc:
84
+ stdin = check_not_none(proc.stdin)
85
+ stdout = check_not_none(proc.stdout)
86
+
87
+ try:
88
+ yield PySpawner.Spawned(
89
+ stdin=stdin,
90
+ stdout=stdout,
91
+ stderr=proc.stderr,
92
+ )
93
+
94
+ finally:
95
+ try:
96
+ stdin.close()
97
+ except BrokenPipeError:
98
+ pass
99
+
100
+ proc.wait(timeout)
ominfra/pyremote.py CHANGED
@@ -132,6 +132,8 @@ _PYREMOTE_BOOTSTRAP_SRC_FD = 101
132
132
 
133
133
  _PYREMOTE_BOOTSTRAP_CHILD_PID_VAR = '_OPYR_CHILD_PID'
134
134
  _PYREMOTE_BOOTSTRAP_ARGV0_VAR = '_OPYR_ARGV0'
135
+ _PYREMOTE_BOOTSTRAP_CONTEXT_NAME_VAR = '_OPYR_CONTEXT_NAME'
136
+ _PYREMOTE_BOOTSTRAP_SRC_FILE_VAR = '_OPYR_SRC_FILE'
135
137
  _PYREMOTE_BOOTSTRAP_OPTIONS_JSON_VAR = '_OPYR_OPTIONS_JSON'
136
138
 
137
139
  _PYREMOTE_BOOTSTRAP_ACK0 = b'OPYR000\n'
@@ -174,11 +176,10 @@ def _pyremote_bootstrap_main(context_name: str) -> None:
174
176
  for f in [r0, w0, r1, w1]:
175
177
  os.close(f)
176
178
 
177
- # Save child pid to close after relaunch
179
+ # Save vars
178
180
  os.environ[_PYREMOTE_BOOTSTRAP_CHILD_PID_VAR] = str(cp)
179
-
180
- # Save original argv0
181
181
  os.environ[_PYREMOTE_BOOTSTRAP_ARGV0_VAR] = sys.executable
182
+ os.environ[_PYREMOTE_BOOTSTRAP_CONTEXT_NAME_VAR] = context_name
182
183
 
183
184
  # Start repl reading stdin from r0
184
185
  os.execl(sys.executable, sys.executable + (_PYREMOTE_BOOTSTRAP_PROC_TITLE_FMT % (context_name,)))
@@ -229,6 +230,7 @@ def pyremote_build_bootstrap_cmd(context_name: str) -> str:
229
230
 
230
231
  '_PYREMOTE_BOOTSTRAP_CHILD_PID_VAR',
231
232
  '_PYREMOTE_BOOTSTRAP_ARGV0_VAR',
233
+ '_PYREMOTE_BOOTSTRAP_CONTEXT_NAME_VAR',
232
234
 
233
235
  '_PYREMOTE_BOOTSTRAP_ACK0',
234
236
  '_PYREMOTE_BOOTSTRAP_ACK1',
@@ -264,14 +266,15 @@ def pyremote_build_bootstrap_cmd(context_name: str) -> str:
264
266
  class PyremotePayloadRuntime:
265
267
  input: ta.BinaryIO
266
268
  output: ta.BinaryIO
269
+ context_name: str
267
270
  main_src: str
268
271
  options: PyremoteBootstrapOptions
269
272
  env_info: PyremoteEnvInfo
270
273
 
271
274
 
272
275
  def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
273
- # If json options var is not present we need to do initial finalization
274
- if _PYREMOTE_BOOTSTRAP_OPTIONS_JSON_VAR not in os.environ:
276
+ # If src file var is not present we need to do initial finalization
277
+ if _PYREMOTE_BOOTSTRAP_SRC_FILE_VAR not in os.environ:
275
278
  # Read second copy of main src
276
279
  r1 = os.fdopen(_PYREMOTE_BOOTSTRAP_SRC_FD, 'rb', 0)
277
280
  main_src = r1.read().decode('utf-8')
@@ -295,11 +298,14 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
295
298
  os.write(tfd, main_src.encode('utf-8'))
296
299
  os.close(tfd)
297
300
 
298
- # Set json options var
301
+ # Set vars
302
+ os.environ[_PYREMOTE_BOOTSTRAP_SRC_FILE_VAR] = tfn
299
303
  os.environ[_PYREMOTE_BOOTSTRAP_OPTIONS_JSON_VAR] = options_json.decode('utf-8')
300
304
 
301
305
  # Re-exec temp file
302
- os.execl(os.environ[_PYREMOTE_BOOTSTRAP_ARGV0_VAR], sys.orig_argv[0], tfn)
306
+ exe = os.environ[_PYREMOTE_BOOTSTRAP_ARGV0_VAR]
307
+ context_name = os.environ[_PYREMOTE_BOOTSTRAP_CONTEXT_NAME_VAR]
308
+ os.execl(exe, exe + (_PYREMOTE_BOOTSTRAP_PROC_TITLE_FMT % (context_name,)), tfn)
303
309
 
304
310
  else:
305
311
  # Load options json var
@@ -307,12 +313,15 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
307
313
  options = PyremoteBootstrapOptions(**json.loads(options_json_str))
308
314
 
309
315
  # Read temp source file
310
- with open(sys.orig_argv[1]) as sf:
316
+ with open(os.environ.pop(_PYREMOTE_BOOTSTRAP_SRC_FILE_VAR)) as sf:
311
317
  main_src = sf.read()
312
318
 
313
319
  # Restore original argv0
314
320
  sys.executable = os.environ.pop(_PYREMOTE_BOOTSTRAP_ARGV0_VAR)
315
321
 
322
+ # Grab context name
323
+ context_name = os.environ.pop(_PYREMOTE_BOOTSTRAP_CONTEXT_NAME_VAR)
324
+
316
325
  # Write third ack
317
326
  os.write(1, _PYREMOTE_BOOTSTRAP_ACK2)
318
327
 
@@ -335,6 +344,7 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
335
344
  return PyremotePayloadRuntime(
336
345
  input=input,
337
346
  output=output,
347
+ context_name=context_name,
338
348
  main_src=main_src,
339
349
  options=options,
340
350
  env_info=env_info,
@@ -58,6 +58,7 @@ TomlPos = int # ta.TypeAlias
58
58
 
59
59
  # ../../../../omlish/lite/cached.py
60
60
  T = ta.TypeVar('T')
61
+ CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
61
62
 
62
63
  # ../../../../omlish/lite/check.py
63
64
  SizedT = ta.TypeVar('SizedT', bound=ta.Sized)
@@ -915,6 +916,12 @@ def cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
915
916
  return _cached_nullary(fn)
916
917
 
917
918
 
919
+ def static_init(fn: CallableT) -> CallableT:
920
+ fn = cached_nullary(fn)
921
+ fn()
922
+ return fn
923
+
924
+
918
925
  ########################################
919
926
  # ../../../../../omlish/lite/check.py
920
927