ominfra 0.0.0.dev145__py3-none-any.whl → 0.0.0.dev147__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.
@@ -20,6 +20,8 @@ from .base import CommandRegistrations
20
20
  from .base import build_command_name_map
21
21
  from .execution import CommandExecutorMap
22
22
  from .execution import LocalCommandExecutor
23
+ from .interp import InterpCommand
24
+ from .interp import InterpCommandExecutor
23
25
  from .marshal import install_command_marshaling
24
26
  from .subprocess import SubprocessCommand
25
27
  from .subprocess import SubprocessCommandExecutor
@@ -111,8 +113,11 @@ def bind_commands(
111
113
 
112
114
  #
113
115
 
116
+ command_cls: ta.Any
117
+ executor_cls: ta.Any
114
118
  for command_cls, executor_cls in [
115
119
  (SubprocessCommand, SubprocessCommandExecutor),
120
+ (InterpCommand, InterpCommandExecutor),
116
121
  ]:
117
122
  lst.append(bind_command(command_cls, executor_cls))
118
123
 
@@ -0,0 +1,39 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+
4
+ from omdev.interp.resolvers import DEFAULT_INTERP_RESOLVER
5
+ from omdev.interp.types import InterpOpts
6
+ from omdev.interp.types import InterpSpecifier
7
+ from omlish.lite.check import check_not_none
8
+
9
+ from ..commands.base import Command
10
+ from ..commands.base import CommandExecutor
11
+
12
+
13
+ ##
14
+
15
+
16
+ @dc.dataclass(frozen=True)
17
+ class InterpCommand(Command['InterpCommand.Output']):
18
+ spec: str
19
+ install: bool = False
20
+
21
+ @dc.dataclass(frozen=True)
22
+ class Output(Command.Output):
23
+ exe: str
24
+ version: str
25
+ opts: InterpOpts
26
+
27
+
28
+ ##
29
+
30
+
31
+ class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
32
+ def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
33
+ i = InterpSpecifier.parse(check_not_none(cmd.spec))
34
+ o = check_not_none(DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
35
+ return InterpCommand.Output(
36
+ exe=o.exe,
37
+ version=str(o.version.version),
38
+ opts=o.version.opts,
39
+ )
@@ -5,6 +5,7 @@ import subprocess
5
5
  import time
6
6
  import typing as ta
7
7
 
8
+ from omlish.lite.check import check_not_isinstance
8
9
  from omlish.lite.subprocesses import SUBPROCESS_CHANNEL_OPTION_VALUES
9
10
  from omlish.lite.subprocesses import SubprocessChannelOption
10
11
  from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
@@ -31,8 +32,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
31
32
  timeout: ta.Optional[float] = None
32
33
 
33
34
  def __post_init__(self) -> None:
34
- if isinstance(self.cmd, str):
35
- raise TypeError(self.cmd)
35
+ check_not_isinstance(self.cmd, str)
36
36
 
37
37
  @dc.dataclass(frozen=True)
38
38
  class Output(Command.Output):
@@ -1,6 +1,8 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import dataclasses as dc
3
3
 
4
+ from omlish.lite.logs import log
5
+
4
6
  from ..commands.base import Command
5
7
  from ..commands.base import CommandExecutor
6
8
 
@@ -20,4 +22,6 @@ class DeployCommand(Command['DeployCommand.Output']):
20
22
 
21
23
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
22
24
  def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
25
+ log.info('Deploying!')
26
+
23
27
  return DeployCommand.Output()
@@ -31,3 +31,150 @@ for dn in [
31
31
  'venv',
32
32
  ]:
33
33
  """
34
+ import abc
35
+ import dataclasses as dc
36
+ import os.path
37
+ import typing as ta
38
+
39
+ from omlish.lite.check import check_equal
40
+ from omlish.lite.check import check_non_empty
41
+ from omlish.lite.check import check_non_empty_str
42
+ from omlish.lite.check import check_not_in
43
+
44
+
45
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
46
+
47
+
48
+ ##
49
+
50
+
51
+ DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
52
+
53
+
54
+ @dc.dataclass(frozen=True)
55
+ class DeployPathPart(abc.ABC): # noqa
56
+ @property
57
+ @abc.abstractmethod
58
+ def kind(self) -> DeployPathKind:
59
+ raise NotImplementedError
60
+
61
+ @abc.abstractmethod
62
+ def render(self) -> str:
63
+ raise NotImplementedError
64
+
65
+
66
+ #
67
+
68
+
69
+ class DeployPathDir(DeployPathPart, abc.ABC):
70
+ @property
71
+ def kind(self) -> DeployPathKind:
72
+ return 'dir'
73
+
74
+ @classmethod
75
+ def parse(cls, s: str) -> 'DeployPathDir':
76
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
77
+ check_equal(s, DEPLOY_PATH_SPEC_PLACEHOLDER)
78
+ return SpecDeployPathDir()
79
+ else:
80
+ return ConstDeployPathDir(s)
81
+
82
+
83
+ class DeployPathFile(DeployPathPart, abc.ABC):
84
+ @property
85
+ def kind(self) -> DeployPathKind:
86
+ return 'file'
87
+
88
+ @classmethod
89
+ def parse(cls, s: str) -> 'DeployPathFile':
90
+ if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
91
+ check_equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
92
+ return SpecDeployPathFile(s[1:])
93
+ else:
94
+ return ConstDeployPathFile(s)
95
+
96
+
97
+ #
98
+
99
+
100
+ @dc.dataclass(frozen=True)
101
+ class ConstDeployPathPart(DeployPathPart, abc.ABC):
102
+ name: str
103
+
104
+ def __post_init__(self) -> None:
105
+ check_non_empty_str(self.name)
106
+ check_not_in('/', self.name)
107
+ check_not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
108
+
109
+ def render(self) -> str:
110
+ return self.name
111
+
112
+
113
+ class ConstDeployPathDir(ConstDeployPathPart, DeployPathDir):
114
+ pass
115
+
116
+
117
+ class ConstDeployPathFile(ConstDeployPathPart, DeployPathFile):
118
+ pass
119
+
120
+
121
+ #
122
+
123
+
124
+ class SpecDeployPathPart(DeployPathPart, abc.ABC):
125
+ pass
126
+
127
+
128
+ class SpecDeployPathDir(SpecDeployPathPart, DeployPathDir):
129
+ def render(self) -> str:
130
+ return DEPLOY_PATH_SPEC_PLACEHOLDER
131
+
132
+
133
+ @dc.dataclass(frozen=True)
134
+ class SpecDeployPathFile(SpecDeployPathPart, DeployPathFile):
135
+ suffix: str
136
+
137
+ def __post_init__(self) -> None:
138
+ check_non_empty_str(self.suffix)
139
+ check_not_in('/', self.suffix)
140
+ check_not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.suffix)
141
+
142
+ def render(self) -> str:
143
+ return DEPLOY_PATH_SPEC_PLACEHOLDER + self.suffix
144
+
145
+
146
+ ##
147
+
148
+
149
+ @dc.dataclass(frozen=True)
150
+ class DeployPath:
151
+ parts: ta.Sequence[DeployPathPart]
152
+
153
+ def __post_init__(self) -> None:
154
+ check_non_empty(self.parts)
155
+ for p in self.parts[:-1]:
156
+ check_equal(p.kind, 'dir')
157
+
158
+ @property
159
+ def kind(self) -> ta.Literal['file', 'dir']:
160
+ return self.parts[-1].kind
161
+
162
+ def render(self) -> str:
163
+ return os.path.join( # noqa
164
+ *[p.render() for p in self.parts],
165
+ *([''] if self.kind == 'dir' else []),
166
+ )
167
+
168
+ @classmethod
169
+ def parse(cls, s: str) -> 'DeployPath':
170
+ tail_parse: ta.Callable[[str], DeployPathPart]
171
+ if s.endswith('/'):
172
+ tail_parse = DeployPathDir.parse
173
+ s = s[:-1]
174
+ else:
175
+ tail_parse = DeployPathFile.parse
176
+ ps = check_non_empty_str(s).split('/')
177
+ return cls([
178
+ *([DeployPathDir.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
179
+ tail_parse(ps[-1]),
180
+ ])
ominfra/manage/main.py CHANGED
@@ -6,6 +6,7 @@ manage.py -s 'docker run -i python:3.12'
6
6
  manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
7
7
  """
8
8
  import contextlib
9
+ import json
9
10
  import typing as ta
10
11
 
11
12
  from omlish.lite.logs import log # noqa
@@ -18,9 +19,7 @@ from .bootstrap_ import main_bootstrap
18
19
  from .commands.base import Command
19
20
  from .commands.base import CommandExecutor
20
21
  from .commands.execution import LocalCommandExecutor
21
- from .commands.subprocess import SubprocessCommand
22
22
  from .config import MainConfig
23
- from .deploy.command import DeployCommand
24
23
  from .remote.config import RemoteConfig
25
24
  from .remote.execution import RemoteExecution
26
25
  from .remote.spawning import RemoteSpawning
@@ -78,12 +77,15 @@ def _main() -> None:
78
77
 
79
78
  #
80
79
 
80
+ msh = injector[ObjMarshalerManager]
81
+
81
82
  cmds: ta.List[Command] = []
83
+ cmd: Command
82
84
  for c in args.command:
83
- if c == 'deploy':
84
- cmds.append(DeployCommand())
85
- else:
86
- cmds.append(SubprocessCommand([c]))
85
+ if not c.startswith('{'):
86
+ c = json.dumps({c: {}})
87
+ cmd = msh.unmarshal_obj(json.loads(c), Command)
88
+ cmds.append(cmd)
87
89
 
88
90
  #
89
91
 
@@ -103,9 +105,13 @@ def _main() -> None:
103
105
  ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
104
106
 
105
107
  for cmd in cmds:
106
- r = ce.try_execute(cmd)
108
+ r = ce.try_execute(
109
+ cmd,
110
+ log=log,
111
+ omit_exc_object=True,
112
+ )
107
113
 
108
- print(injector[ObjMarshalerManager].marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
114
+ print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
109
115
 
110
116
 
111
117
  if __name__ == '__main__':
@@ -1,6 +1,7 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import json
3
3
  import struct
4
+ import threading
4
5
  import typing as ta
5
6
 
6
7
  from omlish.lite.json import json_dumps_compact
@@ -25,10 +26,12 @@ class RemoteChannel:
25
26
  self._output = output
26
27
  self._msh = msh
27
28
 
29
+ self._lock = threading.RLock()
30
+
28
31
  def set_marshaler(self, msh: ObjMarshalerManager) -> None:
29
32
  self._msh = msh
30
33
 
31
- def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
34
+ def _send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
32
35
  j = json_dumps_compact(self._msh.marshal_obj(o, ty))
33
36
  d = j.encode('utf-8')
34
37
 
@@ -36,7 +39,11 @@ class RemoteChannel:
36
39
  self._output.write(d)
37
40
  self._output.flush()
38
41
 
39
- def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
42
+ def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
43
+ with self._lock:
44
+ return self._send_obj(o, ty)
45
+
46
+ def _recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
40
47
  d = self._input.read(4)
41
48
  if not d:
42
49
  return None
@@ -50,3 +57,7 @@ class RemoteChannel:
50
57
 
51
58
  j = json.loads(d.decode('utf-8'))
52
59
  return self._msh.unmarshal_obj(j, ty)
60
+
61
+ def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
62
+ with self._lock:
63
+ return self._recv_obj(ty)
@@ -2,6 +2,7 @@
2
2
  import contextlib
3
3
  import dataclasses as dc
4
4
  import logging
5
+ import threading
5
6
  import typing as ta
6
7
 
7
8
  from omlish.lite.cached import cached_nullary
@@ -36,6 +37,32 @@ else:
36
37
  ##
37
38
 
38
39
 
40
+ class _RemoteExecutionLogHandler(logging.Handler):
41
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
42
+ super().__init__()
43
+ self._fn = fn
44
+
45
+ def emit(self, record):
46
+ msg = self.format(record)
47
+ self._fn(msg)
48
+
49
+
50
+ @dc.dataclass(frozen=True)
51
+ class _RemoteExecutionRequest:
52
+ c: Command
53
+
54
+
55
+ @dc.dataclass(frozen=True)
56
+ class _RemoteExecutionLog:
57
+ s: str
58
+
59
+
60
+ @dc.dataclass(frozen=True)
61
+ class _RemoteExecutionResponse:
62
+ r: ta.Optional[CommandOutputOrExceptionData] = None
63
+ l: ta.Optional[_RemoteExecutionLog] = None
64
+
65
+
39
66
  def _remote_execution_main() -> None:
40
67
  rt = pyremote_bootstrap_finalize() # noqa
41
68
 
@@ -53,20 +80,44 @@ def _remote_execution_main() -> None:
53
80
 
54
81
  chan.set_marshaler(injector[ObjMarshalerManager])
55
82
 
83
+ #
84
+
85
+ log_lock = threading.RLock()
86
+ send_logs = False
87
+
88
+ def log_fn(s: str) -> None:
89
+ with log_lock:
90
+ if send_logs:
91
+ chan.send_obj(_RemoteExecutionResponse(l=_RemoteExecutionLog(s)))
92
+
93
+ log_handler = _RemoteExecutionLogHandler(log_fn)
94
+ logging.root.addHandler(log_handler)
95
+
96
+ #
97
+
56
98
  ce = injector[LocalCommandExecutor]
57
99
 
58
100
  while True:
59
- i = chan.recv_obj(Command)
60
- if i is None:
101
+ req = chan.recv_obj(_RemoteExecutionRequest)
102
+ if req is None:
61
103
  break
62
104
 
105
+ with log_lock:
106
+ send_logs = True
107
+
63
108
  r = ce.try_execute(
64
- i,
109
+ req.c,
65
110
  log=log,
66
111
  omit_exc_object=True,
67
112
  )
68
113
 
69
- chan.send_obj(r)
114
+ with log_lock:
115
+ send_logs = False
116
+
117
+ chan.send_obj(_RemoteExecutionResponse(r=CommandOutputOrExceptionData(
118
+ output=r.output,
119
+ exception=r.exception,
120
+ )))
70
121
 
71
122
 
72
123
  ##
@@ -84,12 +135,17 @@ class RemoteCommandExecutor(CommandExecutor):
84
135
  self._chan = chan
85
136
 
86
137
  def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
87
- self._chan.send_obj(cmd, Command)
138
+ self._chan.send_obj(_RemoteExecutionRequest(cmd))
139
+
140
+ while True:
141
+ if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
142
+ raise EOFError
88
143
 
89
- if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
90
- raise EOFError
144
+ if r.l is not None:
145
+ log.info(r.l.s)
91
146
 
92
- return r
147
+ if r.r is not None:
148
+ return r.r
93
149
 
94
150
  # @ta.override
95
151
  def execute(self, cmd: Command) -> Command.Output: