ominfra 0.0.0.dev152__py3-none-any.whl → 0.0.0.dev154__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.
@@ -2,6 +2,7 @@ import dataclasses as dc
2
2
 
3
3
  from .config import MainConfig
4
4
  from .remote.config import RemoteConfig
5
+ from .system.config import SystemConfig
5
6
 
6
7
 
7
8
  @dc.dataclass(frozen=True)
@@ -9,3 +10,5 @@ class MainBootstrap:
9
10
  main_config: MainConfig = MainConfig()
10
11
 
11
12
  remote_config: RemoteConfig = RemoteConfig()
13
+
14
+ system_config: SystemConfig = SystemConfig()
@@ -13,6 +13,7 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
13
13
  injector = inj.create_injector(bind_main( # noqa
14
14
  main_config=bs.main_config,
15
15
  remote_config=bs.remote_config,
16
+ system_config=bs.system_config,
16
17
  ))
17
18
 
18
19
  return injector
@@ -25,9 +25,6 @@ class InterpCommand(Command['InterpCommand.Output']):
25
25
  opts: InterpOpts
26
26
 
27
27
 
28
- ##
29
-
30
-
31
28
  class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
32
29
  async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
33
30
  i = InterpSpecifier.parse(check.not_none(cmd.spec))
@@ -48,9 +48,6 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
48
48
  stderr: ta.Optional[bytes] = None
49
49
 
50
50
 
51
- ##
52
-
53
-
54
51
  class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
55
52
  async def execute(self, cmd: SubprocessCommand) -> SubprocessCommand.Output:
56
53
  proc: asyncio.subprocess.Process
@@ -17,9 +17,6 @@ class DeployCommand(Command['DeployCommand.Output']):
17
17
  pass
18
18
 
19
19
 
20
- ##
21
-
22
-
23
20
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
24
21
  async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
25
22
  log.info('Deploying!')
@@ -6,8 +6,8 @@ from omlish.lite.inject import InjectorBindings
6
6
  from omlish.lite.inject import inj
7
7
 
8
8
  from ..commands.inject import bind_command
9
- from .command import DeployCommand
10
- from .command import DeployCommandExecutor
9
+ from .commands import DeployCommand
10
+ from .commands import DeployCommandExecutor
11
11
 
12
12
 
13
13
  def bind_deploy(
ominfra/manage/inject.py CHANGED
@@ -13,6 +13,8 @@ from .marshal import ObjMarshalerInstaller
13
13
  from .marshal import ObjMarshalerInstallers
14
14
  from .remote.config import RemoteConfig
15
15
  from .remote.inject import bind_remote
16
+ from .system.config import SystemConfig
17
+ from .system.inject import bind_system
16
18
 
17
19
 
18
20
  ##
@@ -22,6 +24,7 @@ def bind_main(
22
24
  *,
23
25
  main_config: MainConfig,
24
26
  remote_config: RemoteConfig,
27
+ system_config: SystemConfig,
25
28
  ) -> InjectorBindings:
26
29
  lst: ta.List[InjectorBindingOrBindings] = [
27
30
  inj.bind(main_config),
@@ -30,11 +33,15 @@ def bind_main(
30
33
  main_config=main_config,
31
34
  ),
32
35
 
36
+ bind_deploy(),
37
+
33
38
  bind_remote(
34
39
  remote_config=remote_config,
35
40
  ),
36
41
 
37
- bind_deploy(),
42
+ bind_system(
43
+ system_config=system_config,
44
+ ),
38
45
  ]
39
46
 
40
47
  #
ominfra/manage/main.py CHANGED
@@ -68,6 +68,8 @@ class MainCli(ArgparseCli):
68
68
  ) if self.args.pycharm_debug_port is not None else None,
69
69
 
70
70
  timebomb_delay_s=self.args.remote_timebomb_delay_s,
71
+
72
+ use_in_process_remote_executor=True,
71
73
  ),
72
74
  )
73
75
 
@@ -8,8 +8,8 @@ import threading
8
8
  import time
9
9
  import typing as ta
10
10
 
11
- from omlish.lite.asyncio.asyncio import asyncio_open_stream_reader
12
- from omlish.lite.asyncio.asyncio import asyncio_open_stream_writer
11
+ from omlish.asyncs.asyncio.streams import asyncio_open_stream_reader
12
+ from omlish.asyncs.asyncio.streams import asyncio_open_stream_writer
13
13
  from omlish.lite.cached import cached_nullary
14
14
  from omlish.lite.check import check
15
15
  from omlish.lite.inject import Injector
@@ -20,3 +20,5 @@ class RemoteConfig:
20
20
  timebomb_delay_s: ta.Optional[float] = 60 * 60.
21
21
 
22
22
  heartbeat_interval_s: float = 3.
23
+
24
+ use_in_process_remote_executor: bool = False
@@ -1,8 +1,10 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import abc
3
+ import asyncio
3
4
  import contextlib
4
5
  import typing as ta
5
6
 
7
+ from omlish.asyncs.asyncio.channels import asyncio_create_bytes_channel
6
8
  from omlish.lite.cached import cached_nullary
7
9
  from omlish.lite.marshal import ObjMarshalerManager
8
10
 
@@ -10,9 +12,11 @@ from ...pyremote import PyremoteBootstrapDriver
10
12
  from ...pyremote import PyremoteBootstrapOptions
11
13
  from ...pyremote import pyremote_build_bootstrap_cmd
12
14
  from ..bootstrap import MainBootstrap
15
+ from ..commands.execution import LocalCommandExecutor
13
16
  from ._main import _remote_execution_main # noqa
14
17
  from .channel import RemoteChannelImpl
15
18
  from .execution import RemoteCommandExecutor
19
+ from .execution import _RemoteCommandHandler
16
20
  from .payload import RemoteExecutionPayloadFile
17
21
  from .payload import get_remote_payload_src
18
22
  from .spawning import RemoteSpawning
@@ -104,3 +108,47 @@ class PyremoteRemoteExecutionConnector(RemoteExecutionConnector):
104
108
  await rce.start()
105
109
 
106
110
  yield rce
111
+
112
+
113
+ ##
114
+
115
+
116
+ class InProcessRemoteExecutionConnector(RemoteExecutionConnector):
117
+ def __init__(
118
+ self,
119
+ *,
120
+ msh: ObjMarshalerManager,
121
+ local_executor: LocalCommandExecutor,
122
+ ) -> None:
123
+ super().__init__()
124
+
125
+ self._msh = msh
126
+ self._local_executor = local_executor
127
+
128
+ @contextlib.asynccontextmanager
129
+ async def connect(
130
+ self,
131
+ tgt: RemoteSpawning.Target,
132
+ bs: MainBootstrap,
133
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
134
+ r0, w0 = asyncio_create_bytes_channel()
135
+ r1, w1 = asyncio_create_bytes_channel()
136
+
137
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
138
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
139
+
140
+ rch = _RemoteCommandHandler(
141
+ remote_chan,
142
+ self._local_executor,
143
+ )
144
+ rch_task = asyncio.create_task(rch.run()) # noqa
145
+ try:
146
+ rce: RemoteCommandExecutor
147
+ async with contextlib.aclosing(RemoteCommandExecutor(local_chan)) as rce:
148
+ await rce.start()
149
+
150
+ yield rce
151
+
152
+ finally:
153
+ rch.stop()
154
+ await rch_task
@@ -1,9 +1,14 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - sequence all messages
5
+ """
2
6
  import abc
3
7
  import asyncio
4
8
  import dataclasses as dc
5
9
  import itertools
6
10
  import logging
11
+ import time
7
12
  import typing as ta
8
13
 
9
14
  from omlish.lite.check import check
@@ -96,38 +101,80 @@ class _RemoteLogHandler(logging.Handler):
96
101
 
97
102
 
98
103
  class _RemoteCommandHandler:
104
+ DEFAULT_PING_INTERVAL_S: float = 3.
105
+
99
106
  def __init__(
100
107
  self,
101
108
  chan: RemoteChannel,
102
109
  executor: CommandExecutor,
103
110
  *,
104
111
  stop: ta.Optional[asyncio.Event] = None,
112
+ ping_interval_s: float = DEFAULT_PING_INTERVAL_S,
105
113
  ) -> None:
106
114
  super().__init__()
107
115
 
108
116
  self._chan = chan
109
117
  self._executor = executor
110
118
  self._stop = stop if stop is not None else asyncio.Event()
119
+ self._ping_interval_s = ping_interval_s
111
120
 
112
121
  self._cmds_by_seq: ta.Dict[int, _RemoteCommandHandler._Command] = {}
113
122
 
123
+ self._last_ping_send: float = 0.
124
+ self._ping_in_flight: bool = False
125
+ self._last_ping_recv: ta.Optional[float] = None
126
+
127
+ def stop(self) -> None:
128
+ self._stop.set()
129
+
114
130
  @dc.dataclass(frozen=True)
115
131
  class _Command:
116
132
  req: _RemoteProtocol.CommandRequest
117
133
  fut: asyncio.Future
118
134
 
119
135
  async def run(self) -> None:
136
+ log.debug('_RemoteCommandHandler loop start: %r', self)
137
+
120
138
  stop_task = asyncio.create_task(self._stop.wait())
121
139
  recv_task: ta.Optional[asyncio.Task] = None
122
140
 
123
141
  while not self._stop.is_set():
124
142
  if recv_task is None:
125
- recv_task = asyncio.create_task(_RemoteProtocol.Request.recv(self._chan))
143
+ recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
126
144
 
127
- done, pending = await asyncio.wait([
128
- stop_task,
129
- recv_task,
130
- ], return_when=asyncio.FIRST_COMPLETED)
145
+ if not self._ping_in_flight:
146
+ if not self._last_ping_recv:
147
+ ping_wait_time = 0.
148
+ else:
149
+ ping_wait_time = self._ping_interval_s - (time.time() - self._last_ping_recv)
150
+ else:
151
+ ping_wait_time = float('inf')
152
+ wait_time = min(self._ping_interval_s, ping_wait_time)
153
+ log.debug('_RemoteCommandHandler loop wait: %f', wait_time)
154
+
155
+ done, pending = await asyncio.wait(
156
+ [
157
+ stop_task,
158
+ recv_task,
159
+ ],
160
+ return_when=asyncio.FIRST_COMPLETED,
161
+ timeout=wait_time,
162
+ )
163
+
164
+ #
165
+
166
+ if (
167
+ (time.time() - self._last_ping_send >= self._ping_interval_s) and
168
+ not self._ping_in_flight
169
+ ):
170
+ now = time.time()
171
+ self._last_ping_send = now
172
+ self._ping_in_flight = True
173
+ await _RemoteProtocol.PingRequest(
174
+ time=now,
175
+ ).send(self._chan)
176
+
177
+ #
131
178
 
132
179
  if recv_task in done:
133
180
  msg: ta.Optional[_RemoteProtocol.Message] = check.isinstance(
@@ -141,6 +188,20 @@ class _RemoteCommandHandler:
141
188
 
142
189
  await self._handle_message(msg)
143
190
 
191
+ log.debug('_RemoteCommandHandler loop stopping: %r', self)
192
+
193
+ for task in [
194
+ stop_task,
195
+ recv_task,
196
+ ]:
197
+ if task is not None and not task.done():
198
+ task.cancel()
199
+
200
+ for cmd in self._cmds_by_seq.values():
201
+ cmd.fut.cancel()
202
+
203
+ log.debug('_RemoteCommandHandler loop exited: %r', self)
204
+
144
205
  async def _handle_message(self, msg: _RemoteProtocol.Message) -> None:
145
206
  if isinstance(msg, _RemoteProtocol.PingRequest):
146
207
  log.debug('Ping: %r', msg)
@@ -148,6 +209,12 @@ class _RemoteCommandHandler:
148
209
  time=msg.time,
149
210
  ).send(self._chan)
150
211
 
212
+ elif isinstance(msg, _RemoteProtocol.PingResponse):
213
+ latency_s = time.time() - msg.time
214
+ log.debug('Pong: %0.2f ms %r', latency_s * 1000., msg)
215
+ self._last_ping_recv = time.time()
216
+ self._ping_in_flight = False
217
+
151
218
  elif isinstance(msg, _RemoteProtocol.CommandRequest):
152
219
  fut = asyncio.create_task(self._handle_command_request(msg))
153
220
  self._cmds_by_seq[msg.seq] = _RemoteCommandHandler._Command(
@@ -229,16 +296,23 @@ class RemoteCommandExecutor(CommandExecutor):
229
296
  if recv_task is None:
230
297
  recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
231
298
 
232
- done, pending = await asyncio.wait([
233
- stop_task,
234
- queue_task,
235
- recv_task,
236
- ], return_when=asyncio.FIRST_COMPLETED)
299
+ done, pending = await asyncio.wait(
300
+ [
301
+ stop_task,
302
+ queue_task,
303
+ recv_task,
304
+ ],
305
+ return_when=asyncio.FIRST_COMPLETED,
306
+ )
307
+
308
+ #
237
309
 
238
310
  if queue_task in done:
239
311
  req = check.isinstance(queue_task.result(), RemoteCommandExecutor._Request)
240
312
  queue_task = None
241
- await self._handle_request(req)
313
+ await self._handle_queued_request(req)
314
+
315
+ #
242
316
 
243
317
  if recv_task in done:
244
318
  msg: ta.Optional[_RemoteProtocol.Message] = check.isinstance(
@@ -268,7 +342,7 @@ class RemoteCommandExecutor(CommandExecutor):
268
342
 
269
343
  log.debug('RemoteCommandExecutor loop exited: %r', self)
270
344
 
271
- async def _handle_request(self, req: _Request) -> None:
345
+ async def _handle_queued_request(self, req: _Request) -> None:
272
346
  self._reqs_by_seq[req.seq] = req
273
347
  await _RemoteProtocol.CommandRequest(
274
348
  seq=req.seq,
@@ -282,6 +356,10 @@ class RemoteCommandExecutor(CommandExecutor):
282
356
  time=msg.time,
283
357
  ).send(self._chan)
284
358
 
359
+ elif isinstance(msg, _RemoteProtocol.PingResponse):
360
+ latency_s = time.time() - msg.time
361
+ log.debug('Pong: %0.2f ms %r', latency_s * 1000., msg)
362
+
285
363
  elif isinstance(msg, _RemoteProtocol.LogResponse):
286
364
  log.info(msg.s)
287
365
 
@@ -6,6 +6,7 @@ from omlish.lite.inject import InjectorBindings
6
6
  from omlish.lite.inject import inj
7
7
 
8
8
  from .config import RemoteConfig
9
+ from .connection import InProcessRemoteExecutionConnector
9
10
  from .connection import PyremoteRemoteExecutionConnector
10
11
  from .connection import RemoteExecutionConnector
11
12
  from .payload import RemoteExecutionPayloadFile
@@ -22,12 +23,26 @@ def bind_remote(
22
23
 
23
24
  inj.bind(SubprocessRemoteSpawning, singleton=True),
24
25
  inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
25
-
26
- inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
27
- inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
28
26
  ]
29
27
 
28
+ #
29
+
30
+ if remote_config.use_in_process_remote_executor:
31
+ lst.extend([
32
+ inj.bind(InProcessRemoteExecutionConnector, singleton=True),
33
+ inj.bind(RemoteExecutionConnector, to_key=InProcessRemoteExecutionConnector),
34
+ ])
35
+ else:
36
+ lst.extend([
37
+ inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
38
+ inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
39
+ ])
40
+
41
+ #
42
+
30
43
  if (pf := remote_config.payload_file) is not None:
31
- lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
44
+ lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
45
+
46
+ #
32
47
 
33
48
  return inj.as_bindings(*lst)
File without changes
@@ -0,0 +1,24 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+
4
+ from omlish.lite.logs import log
5
+
6
+ from ..commands.base import Command
7
+ from ..commands.base import CommandExecutor
8
+
9
+
10
+ ##
11
+
12
+
13
+ @dc.dataclass(frozen=True)
14
+ class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
15
+ @dc.dataclass(frozen=True)
16
+ class Output(Command.Output):
17
+ pass
18
+
19
+
20
+ class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
21
+ async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
22
+ log.info('Checking system package!')
23
+
24
+ return CheckSystemPackageCommand.Output()
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import typing as ta
4
+
5
+
6
+ @dc.dataclass(frozen=True)
7
+ class SystemConfig:
8
+ platform: ta.Optional[str] = None
@@ -0,0 +1,54 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import sys
3
+ import typing as ta
4
+
5
+ from omlish.lite.inject import InjectorBindingOrBindings
6
+ from omlish.lite.inject import InjectorBindings
7
+ from omlish.lite.inject import inj
8
+
9
+ from ..commands.inject import bind_command
10
+ from .commands import CheckSystemPackageCommand
11
+ from .commands import CheckSystemPackageCommandExecutor
12
+ from .config import SystemConfig
13
+ from .packages import AptSystemPackageManager
14
+ from .packages import BrewSystemPackageManager
15
+ from .packages import SystemPackageManager
16
+ from .types import SystemPlatform
17
+
18
+
19
+ def bind_system(
20
+ *,
21
+ system_config: SystemConfig,
22
+ ) -> InjectorBindings:
23
+ lst: ta.List[InjectorBindingOrBindings] = [
24
+ inj.bind(system_config),
25
+ ]
26
+
27
+ #
28
+
29
+ platform = system_config.platform or sys.platform
30
+ lst.append(inj.bind(platform, key=SystemPlatform))
31
+
32
+ #
33
+
34
+ if platform == 'linux':
35
+ lst.extend([
36
+ inj.bind(AptSystemPackageManager, singleton=True),
37
+ inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
38
+ ])
39
+
40
+ elif platform == 'darwin':
41
+ lst.extend([
42
+ inj.bind(BrewSystemPackageManager, singleton=True),
43
+ inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
44
+ ])
45
+
46
+ #
47
+
48
+ lst.extend([
49
+ bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
50
+ ])
51
+
52
+ #
53
+
54
+ return inj.as_bindings(*lst)
@@ -0,0 +1,106 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - yum/rpm
5
+ """
6
+ import abc
7
+ import dataclasses as dc
8
+ import json
9
+ import os
10
+ import typing as ta
11
+
12
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_check_call
13
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_check_output
14
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_run
15
+ from omlish.lite.check import check
16
+
17
+
18
+ SystemPackageOrStr = ta.Union['SystemPackage', str]
19
+
20
+
21
+ @dc.dataclass(frozen=True)
22
+ class SystemPackage:
23
+ name: str
24
+ version: ta.Optional[str] = None
25
+
26
+
27
+ class SystemPackageManager(abc.ABC):
28
+ @abc.abstractmethod
29
+ def update(self) -> ta.Awaitable[None]:
30
+ raise NotImplementedError
31
+
32
+ @abc.abstractmethod
33
+ def upgrade(self) -> ta.Awaitable[None]:
34
+ raise NotImplementedError
35
+
36
+ @abc.abstractmethod
37
+ def install(self, *packages: SystemPackageOrStr) -> ta.Awaitable[None]:
38
+ raise NotImplementedError
39
+
40
+ @abc.abstractmethod
41
+ def query(self, *packages: SystemPackageOrStr) -> ta.Awaitable[ta.Mapping[str, SystemPackage]]:
42
+ raise NotImplementedError
43
+
44
+
45
+ class BrewSystemPackageManager(SystemPackageManager):
46
+ async def update(self) -> None:
47
+ await asyncio_subprocess_check_call('brew', 'update')
48
+
49
+ async def upgrade(self) -> None:
50
+ await asyncio_subprocess_check_call('brew', 'upgrade')
51
+
52
+ async def install(self, *packages: SystemPackageOrStr) -> None:
53
+ es: ta.List[str] = []
54
+ for p in packages:
55
+ if isinstance(p, SystemPackage):
56
+ es.append(p.name + (f'@{p.version}' if p.version is not None else ''))
57
+ else:
58
+ es.append(p)
59
+ await asyncio_subprocess_check_call('brew', 'install', *es)
60
+
61
+ async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
62
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
63
+ o = await asyncio_subprocess_check_output('brew', 'info', '--json', *pns)
64
+ j = json.loads(o.decode())
65
+ d: ta.Dict[str, SystemPackage] = {}
66
+ for e in j:
67
+ if not e['installed']:
68
+ continue
69
+ d[e['name']] = SystemPackage(
70
+ name=e['name'],
71
+ version=e['installed'][0]['version'],
72
+ )
73
+ return d
74
+
75
+
76
+ class AptSystemPackageManager(SystemPackageManager):
77
+ _APT_ENV: ta.ClassVar[ta.Mapping[str, str]] = {
78
+ 'DEBIAN_FRONTEND': 'noninteractive',
79
+ }
80
+
81
+ async def update(self) -> None:
82
+ await asyncio_subprocess_check_call('apt', 'update', env={**os.environ, **self._APT_ENV})
83
+
84
+ async def upgrade(self) -> None:
85
+ await asyncio_subprocess_check_call('apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
86
+
87
+ async def install(self, *packages: SystemPackageOrStr) -> None:
88
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
89
+ await asyncio_subprocess_check_call('apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
90
+
91
+ async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
92
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
93
+ cmd = ['dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns]
94
+ stdout, stderr = await asyncio_subprocess_run(
95
+ *cmd,
96
+ capture_output=True,
97
+ check=False,
98
+ )
99
+ d: ta.Dict[str, SystemPackage] = {}
100
+ for l in check.not_none(stdout).decode('utf-8').strip().splitlines():
101
+ n, v = l.split('=', 1)
102
+ d[n] = SystemPackage(
103
+ name=n,
104
+ version=v,
105
+ )
106
+ return d
@@ -0,0 +1,5 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+
5
+ SystemPlatform = ta.NewType('SystemPlatform', str)