ominfra 0.0.0.dev142__py3-none-any.whl → 0.0.0.dev143__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import contextlib
3
+ import dataclasses as dc
4
+ import logging
5
+ import typing as ta
6
+
7
+ from omlish.lite.cached import cached_nullary
8
+ from omlish.lite.check import check_not_none
9
+ from omlish.lite.logs import log
10
+ from omlish.lite.marshal import ObjMarshalerManager
11
+ from omlish.lite.pycharm import pycharm_debug_connect
12
+
13
+ from ...pyremote import PyremoteBootstrapDriver
14
+ from ...pyremote import PyremoteBootstrapOptions
15
+ from ...pyremote import pyremote_bootstrap_finalize
16
+ from ...pyremote import pyremote_build_bootstrap_cmd
17
+ from ..bootstrap import MainBootstrap
18
+ from ..commands.base import Command
19
+ from ..commands.base import CommandException
20
+ from ..commands.base import CommandExecutor
21
+ from ..commands.base import CommandOutputOrException
22
+ from ..commands.base import CommandOutputOrExceptionData
23
+ from .channel import RemoteChannel
24
+ from .payload import RemoteExecutionPayloadFile
25
+ from .payload import get_remote_payload_src
26
+ from .spawning import RemoteSpawning
27
+
28
+
29
+ if ta.TYPE_CHECKING:
30
+ from ..bootstrap_ import main_bootstrap
31
+ else:
32
+ main_bootstrap: ta.Any = None
33
+
34
+
35
+ ##
36
+
37
+
38
+ def _remote_execution_main() -> None:
39
+ rt = pyremote_bootstrap_finalize() # noqa
40
+
41
+ chan = RemoteChannel(
42
+ rt.input,
43
+ rt.output,
44
+ )
45
+
46
+ bs = check_not_none(chan.recv_obj(MainBootstrap))
47
+
48
+ if (prd := bs.remote_config.pycharm_remote_debug) is not None:
49
+ pycharm_debug_connect(prd)
50
+
51
+ injector = main_bootstrap(bs)
52
+
53
+ chan.set_marshaler(injector[ObjMarshalerManager])
54
+
55
+ ce = injector[CommandExecutor]
56
+
57
+ while True:
58
+ i = chan.recv_obj(Command)
59
+ if i is None:
60
+ break
61
+
62
+ r = ce.try_execute(
63
+ i,
64
+ log=log,
65
+ omit_exc_object=True,
66
+ )
67
+
68
+ chan.send_obj(r)
69
+
70
+
71
+ ##
72
+
73
+
74
+ @dc.dataclass()
75
+ class RemoteCommandError(Exception):
76
+ e: CommandException
77
+
78
+
79
+ class RemoteCommandExecutor(CommandExecutor):
80
+ def __init__(self, chan: RemoteChannel) -> None:
81
+ super().__init__()
82
+
83
+ self._chan = chan
84
+
85
+ def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
86
+ self._chan.send_obj(cmd, Command)
87
+
88
+ if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
89
+ raise EOFError
90
+
91
+ return r
92
+
93
+ # @ta.override
94
+ def execute(self, cmd: Command) -> Command.Output:
95
+ r = self._remote_execute(cmd)
96
+ if (e := r.exception) is not None:
97
+ raise RemoteCommandError(e)
98
+ else:
99
+ return check_not_none(r.output)
100
+
101
+ # @ta.override
102
+ def try_execute(
103
+ self,
104
+ cmd: Command,
105
+ *,
106
+ log: ta.Optional[logging.Logger] = None,
107
+ omit_exc_object: bool = False,
108
+ ) -> CommandOutputOrException:
109
+ try:
110
+ r = self._remote_execute(cmd)
111
+
112
+ except Exception as e: # noqa
113
+ if log is not None:
114
+ log.exception('Exception executing remote command: %r', type(cmd))
115
+
116
+ return CommandOutputOrExceptionData(exception=CommandException.of(
117
+ e,
118
+ omit_exc_object=omit_exc_object,
119
+ cmd=cmd,
120
+ ))
121
+
122
+ else:
123
+ return r
124
+
125
+
126
+ ##
127
+
128
+
129
+ class RemoteExecution:
130
+ def __init__(
131
+ self,
132
+ *,
133
+ spawning: RemoteSpawning,
134
+ msh: ObjMarshalerManager,
135
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
136
+ ) -> None:
137
+ super().__init__()
138
+
139
+ self._spawning = spawning
140
+ self._msh = msh
141
+ self._payload_file = payload_file
142
+
143
+ #
144
+
145
+ @cached_nullary
146
+ def _payload_src(self) -> str:
147
+ return get_remote_payload_src(file=self._payload_file)
148
+
149
+ @cached_nullary
150
+ def _remote_src(self) -> ta.Sequence[str]:
151
+ return [
152
+ self._payload_src(),
153
+ '_remote_execution_main()',
154
+ ]
155
+
156
+ @cached_nullary
157
+ def _spawn_src(self) -> str:
158
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
159
+
160
+ #
161
+
162
+ @contextlib.contextmanager
163
+ def connect(
164
+ self,
165
+ tgt: RemoteSpawning.Target,
166
+ bs: MainBootstrap,
167
+ ) -> ta.Generator[RemoteCommandExecutor, None, None]:
168
+ spawn_src = self._spawn_src()
169
+ remote_src = self._remote_src()
170
+
171
+ with self._spawning.spawn(
172
+ tgt,
173
+ spawn_src,
174
+ ) as proc:
175
+ res = PyremoteBootstrapDriver( # noqa
176
+ remote_src,
177
+ PyremoteBootstrapOptions(
178
+ debug=bs.main_config.debug,
179
+ ),
180
+ ).run(
181
+ proc.stdout,
182
+ proc.stdin,
183
+ )
184
+
185
+ chan = RemoteChannel(
186
+ proc.stdout,
187
+ proc.stdin,
188
+ msh=self._msh,
189
+ )
190
+
191
+ chan.send_obj(bs)
192
+
193
+ yield RemoteCommandExecutor(chan)
@@ -0,0 +1,29 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.inject import InjectorBindingOrBindings
5
+ from omlish.lite.inject import InjectorBindings
6
+ from omlish.lite.inject import inj
7
+
8
+ from .config import RemoteConfig
9
+ from .execution import RemoteExecution
10
+ from .payload import RemoteExecutionPayloadFile
11
+ from .spawning import RemoteSpawning
12
+
13
+
14
+ def bind_remote(
15
+ *,
16
+ remote_config: RemoteConfig,
17
+ ) -> InjectorBindings:
18
+ lst: ta.List[InjectorBindingOrBindings] = [
19
+ inj.bind(remote_config),
20
+
21
+ inj.bind(RemoteSpawning, singleton=True),
22
+
23
+ inj.bind(RemoteExecution, singleton=True),
24
+ ]
25
+
26
+ if (pf := remote_config.payload_file) is not None:
27
+ lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
28
+
29
+ return inj.as_bindings(*lst)
@@ -6,6 +6,9 @@ import typing as ta
6
6
  from omlish.lite.cached import cached_nullary
7
7
 
8
8
 
9
+ RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
10
+
11
+
9
12
  @cached_nullary
10
13
  def _get_self_src() -> str:
11
14
  return inspect.getsource(sys.modules[__name__])
@@ -23,7 +26,10 @@ def _is_self_amalg() -> bool:
23
26
  return _is_src_amalg(_get_self_src())
24
27
 
25
28
 
26
- def get_payload_src(*, file: ta.Optional[str]) -> str:
29
+ def get_remote_payload_src(
30
+ *,
31
+ file: ta.Optional[RemoteExecutionPayloadFile],
32
+ ) -> str:
27
33
  if file is not None:
28
34
  with open(file) as f:
29
35
  return f.read()
@@ -11,25 +11,16 @@ from omlish.lite.subprocesses import SubprocessChannelOption
11
11
  from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
12
12
 
13
13
 
14
- class PySpawner:
15
- DEFAULT_PYTHON = 'python3'
14
+ class RemoteSpawning:
15
+ @dc.dataclass(frozen=True)
16
+ class Target:
17
+ shell: ta.Optional[str] = None
18
+ shell_quote: bool = False
16
19
 
17
- def __init__(
18
- self,
19
- src: str,
20
- *,
21
- shell: ta.Optional[str] = None,
22
- shell_quote: bool = False,
23
- python: str = DEFAULT_PYTHON,
24
- stderr: ta.Optional[SubprocessChannelOption] = None,
25
- ) -> None:
26
- super().__init__()
27
-
28
- self._src = src
29
- self._shell = shell
30
- self._shell_quote = shell_quote
31
- self._python = python
32
- self._stderr = stderr
20
+ DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
21
+ python: str = DEFAULT_PYTHON
22
+
23
+ stderr: ta.Optional[str] = None # SubprocessChannelOption
33
24
 
34
25
  #
35
26
 
@@ -37,20 +28,24 @@ class PySpawner:
37
28
  cmd: ta.Sequence[str]
38
29
  shell: bool
39
30
 
40
- def _prepare_cmd(self) -> _PreparedCmd:
41
- if self._shell is not None:
42
- sh_src = f'{self._python} -c {shlex.quote(self._src)}'
43
- if self._shell_quote:
31
+ def _prepare_cmd(
32
+ self,
33
+ tgt: Target,
34
+ src: str,
35
+ ) -> _PreparedCmd:
36
+ if tgt.shell is not None:
37
+ sh_src = f'{tgt.python} -c {shlex.quote(src)}'
38
+ if tgt.shell_quote:
44
39
  sh_src = shlex.quote(sh_src)
45
- sh_cmd = f'{self._shell} {sh_src}'
46
- return PySpawner._PreparedCmd(
40
+ sh_cmd = f'{tgt.shell} {sh_src}'
41
+ return RemoteSpawning._PreparedCmd(
47
42
  cmd=[sh_cmd],
48
43
  shell=True,
49
44
  )
50
45
 
51
46
  else:
52
- return PySpawner._PreparedCmd(
53
- cmd=[self._python, '-c', self._src],
47
+ return RemoteSpawning._PreparedCmd(
48
+ cmd=[tgt.python, '-c', src],
54
49
  shell=False,
55
50
  )
56
51
 
@@ -65,23 +60,28 @@ class PySpawner:
65
60
  @contextlib.contextmanager
66
61
  def spawn(
67
62
  self,
63
+ tgt: Target,
64
+ src: str,
68
65
  *,
69
66
  timeout: ta.Optional[float] = None,
70
67
  ) -> ta.Generator[Spawned, None, None]:
71
- pc = self._prepare_cmd()
68
+ pc = self._prepare_cmd(tgt, src)
72
69
 
73
70
  with subprocess.Popen(
74
71
  subprocess_maybe_shell_wrap_exec(*pc.cmd),
75
72
  shell=pc.shell,
76
73
  stdin=subprocess.PIPE,
77
74
  stdout=subprocess.PIPE,
78
- stderr=SUBPROCESS_CHANNEL_OPTION_VALUES[self._stderr] if self._stderr is not None else None,
75
+ stderr=(
76
+ SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, tgt.stderr)]
77
+ if tgt.stderr is not None else None
78
+ ),
79
79
  ) as proc:
80
80
  stdin = check_not_none(proc.stdin)
81
81
  stdout = check_not_none(proc.stdout)
82
82
 
83
83
  try:
84
- yield PySpawner.Spawned(
84
+ yield RemoteSpawning.Spawned(
85
85
  stdin=stdin,
86
86
  stdout=stdout,
87
87
  stderr=proc.stderr,
ominfra/pyremote.py CHANGED
@@ -2,6 +2,9 @@
2
2
  # @omlish-lite
3
3
  """
4
4
  Basically this: https://mitogen.networkgenomics.com/howitworks.html
5
+
6
+ TODO:
7
+ - log: ta.Optional[logging.Logger] = None + log.debug's
5
8
  """
6
9
  import base64
7
10
  import dataclasses as dc
@@ -23,6 +26,9 @@ import zlib
23
26
  class PyremoteBootstrapOptions:
24
27
  debug: bool = False
25
28
 
29
+ DEFAULT_MAIN_NAME_OVERRIDE: ta.ClassVar[str] = '__pyremote__'
30
+ main_name_override: ta.Optional[str] = DEFAULT_MAIN_NAME_OVERRIDE
31
+
26
32
 
27
33
  ##
28
34
 
@@ -339,6 +345,10 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
339
345
  os.dup2(nfd := os.open('/dev/null', os.O_WRONLY), 1)
340
346
  os.close(nfd)
341
347
 
348
+ if (mn := options.main_name_override) is not None:
349
+ # Inspections like typing.get_type_hints need an entry in sys.modules.
350
+ sys.modules[mn] = sys.modules['__main__']
351
+
342
352
  # Write fourth ack
343
353
  output.write(_PYREMOTE_BOOTSTRAP_ACK3)
344
354
 
@@ -357,14 +367,41 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
357
367
 
358
368
 
359
369
  class PyremoteBootstrapDriver:
360
- def __init__(self, main_src: str, options: PyremoteBootstrapOptions = PyremoteBootstrapOptions()) -> None:
370
+ def __init__(
371
+ self,
372
+ main_src: ta.Union[str, ta.Sequence[str]],
373
+ options: PyremoteBootstrapOptions = PyremoteBootstrapOptions(),
374
+ ) -> None:
361
375
  super().__init__()
362
376
 
363
377
  self._main_src = main_src
364
- self._main_z = zlib.compress(main_src.encode('utf-8'))
365
-
366
378
  self._options = options
379
+
380
+ self._prepared_main_src = self._prepare_main_src(main_src, options)
381
+ self._main_z = zlib.compress(self._prepared_main_src.encode('utf-8'))
382
+
367
383
  self._options_json = json.dumps(dc.asdict(options), indent=None, separators=(',', ':')).encode('utf-8') # noqa
384
+ #
385
+
386
+ @classmethod
387
+ def _prepare_main_src(
388
+ cls,
389
+ main_src: ta.Union[str, ta.Sequence[str]],
390
+ options: PyremoteBootstrapOptions,
391
+ ) -> str:
392
+ parts: ta.List[str]
393
+ if isinstance(main_src, str):
394
+ parts = [main_src]
395
+ else:
396
+ parts = list(main_src)
397
+
398
+ if (mn := options.main_name_override) is not None:
399
+ parts.insert(0, f'__name__ = {mn!r}')
400
+
401
+ if len(parts) == 1:
402
+ return parts[0]
403
+ else:
404
+ return '\n\n'.join(parts)
368
405
 
369
406
  #
370
407