ominfra 0.0.0.dev152__py3-none-any.whl → 0.0.0.dev153__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)