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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. ominfra/manage/bootstrap.py +4 -0
  2. ominfra/manage/bootstrap_.py +5 -0
  3. ominfra/manage/commands/inject.py +8 -11
  4. ominfra/manage/commands/{execution.py → local.py} +1 -5
  5. ominfra/manage/commands/ping.py +23 -0
  6. ominfra/manage/commands/types.py +8 -0
  7. ominfra/manage/deploy/apps.py +72 -0
  8. ominfra/manage/deploy/config.py +8 -0
  9. ominfra/manage/deploy/git.py +136 -0
  10. ominfra/manage/deploy/inject.py +21 -0
  11. ominfra/manage/deploy/paths.py +81 -28
  12. ominfra/manage/deploy/types.py +13 -0
  13. ominfra/manage/deploy/venvs.py +66 -0
  14. ominfra/manage/inject.py +20 -4
  15. ominfra/manage/main.py +15 -27
  16. ominfra/manage/remote/_main.py +1 -1
  17. ominfra/manage/remote/config.py +0 -2
  18. ominfra/manage/remote/connection.py +7 -24
  19. ominfra/manage/remote/execution.py +1 -1
  20. ominfra/manage/remote/inject.py +3 -14
  21. ominfra/manage/system/commands.py +22 -2
  22. ominfra/manage/system/config.py +3 -1
  23. ominfra/manage/system/inject.py +16 -6
  24. ominfra/manage/system/packages.py +33 -7
  25. ominfra/manage/system/platforms.py +72 -0
  26. ominfra/manage/targets/__init__.py +0 -0
  27. ominfra/manage/targets/connection.py +150 -0
  28. ominfra/manage/targets/inject.py +42 -0
  29. ominfra/manage/targets/targets.py +87 -0
  30. ominfra/scripts/journald2aws.py +24 -7
  31. ominfra/scripts/manage.py +1880 -438
  32. ominfra/scripts/supervisor.py +187 -25
  33. ominfra/supervisor/configs.py +163 -18
  34. {ominfra-0.0.0.dev153.dist-info → ominfra-0.0.0.dev155.dist-info}/METADATA +3 -3
  35. {ominfra-0.0.0.dev153.dist-info → ominfra-0.0.0.dev155.dist-info}/RECORD +40 -29
  36. ominfra/manage/system/types.py +0 -5
  37. /ominfra/manage/{commands → deploy}/interp.py +0 -0
  38. {ominfra-0.0.0.dev153.dist-info → ominfra-0.0.0.dev155.dist-info}/LICENSE +0 -0
  39. {ominfra-0.0.0.dev153.dist-info → ominfra-0.0.0.dev155.dist-info}/WHEEL +0 -0
  40. {ominfra-0.0.0.dev153.dist-info → ominfra-0.0.0.dev155.dist-info}/entry_points.txt +0 -0
  41. {ominfra-0.0.0.dev153.dist-info → ominfra-0.0.0.dev155.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,8 @@
1
+ # ruff: noqa: UP006 UP007
1
2
  import dataclasses as dc
2
3
 
3
4
  from .config import MainConfig
5
+ from .deploy.config import DeployConfig
4
6
  from .remote.config import RemoteConfig
5
7
  from .system.config import SystemConfig
6
8
 
@@ -9,6 +11,8 @@ from .system.config import SystemConfig
9
11
  class MainBootstrap:
10
12
  main_config: MainConfig = MainConfig()
11
13
 
14
+ deploy_config: DeployConfig = DeployConfig()
15
+
12
16
  remote_config: RemoteConfig = RemoteConfig()
13
17
 
14
18
  system_config: SystemConfig = SystemConfig()
@@ -1,3 +1,4 @@
1
+ # ruff: noqa: UP006 UP007
1
2
  from omlish.lite.inject import Injector
2
3
  from omlish.lite.inject import inj
3
4
  from omlish.lite.logs import configure_standard_logging
@@ -12,8 +13,12 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
12
13
 
13
14
  injector = inj.create_injector(bind_main( # noqa
14
15
  main_config=bs.main_config,
16
+
17
+ deploy_config=bs.deploy_config,
15
18
  remote_config=bs.remote_config,
16
19
  system_config=bs.system_config,
20
+
21
+ main_bootstrap=bs,
17
22
  ))
18
23
 
19
24
  return injector
@@ -18,13 +18,13 @@ from .base import CommandNameMap
18
18
  from .base import CommandRegistration
19
19
  from .base import CommandRegistrations
20
20
  from .base import build_command_name_map
21
- from .execution import CommandExecutorMap
22
- from .execution import LocalCommandExecutor
23
- from .interp import InterpCommand
24
- from .interp import InterpCommandExecutor
21
+ from .local import LocalCommandExecutor
25
22
  from .marshal import install_command_marshaling
23
+ from .ping import PingCommand
24
+ from .ping import PingCommandExecutor
26
25
  from .subprocess import SubprocessCommand
27
26
  from .subprocess import SubprocessCommandExecutor
27
+ from .types import CommandExecutorMap
28
28
 
29
29
 
30
30
  ##
@@ -113,13 +113,10 @@ def bind_commands(
113
113
 
114
114
  #
115
115
 
116
- command_cls: ta.Any
117
- executor_cls: ta.Any
118
- for command_cls, executor_cls in [
119
- (SubprocessCommand, SubprocessCommandExecutor),
120
- (InterpCommand, InterpCommandExecutor),
121
- ]:
122
- lst.append(bind_command(command_cls, executor_cls))
116
+ lst.extend([
117
+ bind_command(PingCommand, PingCommandExecutor),
118
+ bind_command(SubprocessCommand, SubprocessCommandExecutor),
119
+ ])
123
120
 
124
121
  #
125
122
 
@@ -1,11 +1,7 @@
1
1
  # ruff: noqa: UP006 UP007
2
- import typing as ta
3
-
4
2
  from .base import Command
5
3
  from .base import CommandExecutor
6
-
7
-
8
- CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
4
+ from .types import CommandExecutorMap
9
5
 
10
6
 
11
7
  class LocalCommandExecutor(CommandExecutor):
@@ -0,0 +1,23 @@
1
+ # ruff: noqa: TC003 UP006 UP007
2
+ import dataclasses as dc
3
+ import time
4
+
5
+ from .base import Command
6
+ from .base import CommandExecutor
7
+
8
+
9
+ ##
10
+
11
+
12
+ @dc.dataclass(frozen=True)
13
+ class PingCommand(Command['PingCommand.Output']):
14
+ time: float = dc.field(default_factory=time.time)
15
+
16
+ @dc.dataclass(frozen=True)
17
+ class Output(Command.Output):
18
+ time: float
19
+
20
+
21
+ class PingCommandExecutor(CommandExecutor[PingCommand, PingCommand.Output]):
22
+ async def execute(self, cmd: PingCommand) -> PingCommand.Output:
23
+ return PingCommand.Output(cmd.time)
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from .base import Command
5
+ from .base import CommandExecutor
6
+
7
+
8
+ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
@@ -0,0 +1,72 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import datetime
3
+ import os.path
4
+ import typing as ta
5
+
6
+ from .git import DeployGitManager
7
+ from .git import DeployGitRepo
8
+ from .git import DeployGitSpec
9
+ from .paths import DeployPath
10
+ from .paths import DeployPathOwner
11
+ from .types import DeployApp
12
+ from .types import DeployAppTag
13
+ from .types import DeployHome
14
+ from .types import DeployRev
15
+ from .types import DeployTag
16
+ from .venvs import DeployVenvManager
17
+
18
+
19
+ def make_deploy_tag(
20
+ rev: DeployRev,
21
+ now: ta.Optional[datetime.datetime] = None,
22
+ ) -> DeployTag:
23
+ if now is None:
24
+ now = datetime.datetime.utcnow() # noqa
25
+ now_fmt = '%Y%m%dT%H%M%S'
26
+ now_str = now.strftime(now_fmt)
27
+ return DeployTag('-'.join([rev, now_str]))
28
+
29
+
30
+ class DeployAppManager(DeployPathOwner):
31
+ def __init__(
32
+ self,
33
+ *,
34
+ deploy_home: DeployHome,
35
+ git: DeployGitManager,
36
+ venvs: DeployVenvManager,
37
+ ) -> None:
38
+ super().__init__()
39
+
40
+ self._deploy_home = deploy_home
41
+ self._git = git
42
+ self._venvs = venvs
43
+
44
+ self._dir = os.path.join(deploy_home, 'apps')
45
+
46
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
47
+ return {
48
+ DeployPath.parse('apps/@app/@tag'),
49
+ }
50
+
51
+ async def prepare_app(
52
+ self,
53
+ app: DeployApp,
54
+ rev: DeployRev,
55
+ repo: DeployGitRepo,
56
+ ):
57
+ app_tag = DeployAppTag(app, make_deploy_tag(rev))
58
+ app_dir = os.path.join(self._dir, app, app_tag.tag)
59
+
60
+ #
61
+
62
+ await self._git.checkout(
63
+ DeployGitSpec(
64
+ repo=repo,
65
+ rev=rev,
66
+ ),
67
+ app_dir,
68
+ )
69
+
70
+ #
71
+
72
+ await self._venvs.setup_app_venv(app_tag)
@@ -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 DeployConfig:
8
+ deploy_home: ta.Optional[str] = None
@@ -0,0 +1,136 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - 'repos'?
5
+
6
+ git/github.com/wrmsr/omlish <- bootstrap repo
7
+ - shallow clone off bootstrap into /apps
8
+
9
+ github.com/wrmsr/omlish@rev
10
+ """
11
+ import dataclasses as dc
12
+ import functools
13
+ import os.path
14
+ import typing as ta
15
+
16
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_check_call
17
+ from omlish.lite.cached import async_cached_nullary
18
+ from omlish.lite.check import check
19
+
20
+ from .paths import DeployPath
21
+ from .paths import DeployPathOwner
22
+ from .types import DeployHome
23
+ from .types import DeployRev
24
+
25
+
26
+ ##
27
+
28
+
29
+ @dc.dataclass(frozen=True)
30
+ class DeployGitRepo:
31
+ host: ta.Optional[str] = None
32
+ username: ta.Optional[str] = None
33
+ path: ta.Optional[str] = None
34
+
35
+ def __post_init__(self) -> None:
36
+ check.not_in('..', check.non_empty_str(self.host))
37
+ check.not_in('.', check.non_empty_str(self.path))
38
+
39
+
40
+ @dc.dataclass(frozen=True)
41
+ class DeployGitSpec:
42
+ repo: DeployGitRepo
43
+ rev: DeployRev
44
+
45
+
46
+ ##
47
+
48
+
49
+ class DeployGitManager(DeployPathOwner):
50
+ def __init__(
51
+ self,
52
+ *,
53
+ deploy_home: DeployHome,
54
+ ) -> None:
55
+ super().__init__()
56
+
57
+ self._deploy_home = deploy_home
58
+ self._dir = os.path.join(deploy_home, 'git')
59
+
60
+ self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
61
+
62
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
63
+ return {
64
+ DeployPath.parse('git'),
65
+ }
66
+
67
+ class RepoDir:
68
+ def __init__(
69
+ self,
70
+ git: 'DeployGitManager',
71
+ repo: DeployGitRepo,
72
+ ) -> None:
73
+ super().__init__()
74
+
75
+ self._git = git
76
+ self._repo = repo
77
+ self._dir = os.path.join(
78
+ self._git._dir, # noqa
79
+ check.non_empty_str(repo.host),
80
+ check.non_empty_str(repo.path),
81
+ )
82
+
83
+ @property
84
+ def repo(self) -> DeployGitRepo:
85
+ return self._repo
86
+
87
+ @property
88
+ def url(self) -> str:
89
+ if self._repo.username is not None:
90
+ return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
91
+ else:
92
+ return f'https://{self._repo.host}/{self._repo.path}'
93
+
94
+ async def _call(self, *cmd: str) -> None:
95
+ await asyncio_subprocess_check_call(
96
+ *cmd,
97
+ cwd=self._dir,
98
+ )
99
+
100
+ @async_cached_nullary
101
+ async def init(self) -> None:
102
+ os.makedirs(self._dir, exist_ok=True)
103
+ if os.path.exists(os.path.join(self._dir, '.git')):
104
+ return
105
+
106
+ await self._call('git', 'init')
107
+ await self._call('git', 'remote', 'add', 'origin', self.url)
108
+
109
+ async def fetch(self, rev: DeployRev) -> None:
110
+ await self.init()
111
+ await self._call('git', 'fetch', '--depth=1', 'origin', rev)
112
+
113
+ async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
114
+ check.state(not os.path.exists(dst_dir))
115
+
116
+ await self.fetch(rev)
117
+
118
+ # FIXME: temp dir swap
119
+ os.makedirs(dst_dir)
120
+
121
+ dst_call = functools.partial(asyncio_subprocess_check_call, cwd=dst_dir)
122
+ await dst_call('git', 'init')
123
+
124
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
125
+ await dst_call('git', 'fetch', '--depth=1', 'local', rev)
126
+ await dst_call('git', 'checkout', rev)
127
+
128
+ def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
129
+ try:
130
+ return self._repo_dirs[repo]
131
+ except KeyError:
132
+ repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
133
+ return repo_dir
134
+
135
+ async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
136
+ await self.get_repo_dir(spec.repo).checkout(spec.rev, dst_dir)
@@ -1,4 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ import os.path
2
3
  import typing as ta
3
4
 
4
5
  from omlish.lite.inject import InjectorBindingOrBindings
@@ -6,14 +7,34 @@ from omlish.lite.inject import InjectorBindings
6
7
  from omlish.lite.inject import inj
7
8
 
8
9
  from ..commands.inject import bind_command
10
+ from .apps import DeployAppManager
9
11
  from .commands import DeployCommand
10
12
  from .commands import DeployCommandExecutor
13
+ from .config import DeployConfig
14
+ from .git import DeployGitManager
15
+ from .interp import InterpCommand
16
+ from .interp import InterpCommandExecutor
17
+ from .types import DeployHome
18
+ from .venvs import DeployVenvManager
11
19
 
12
20
 
13
21
  def bind_deploy(
22
+ *,
23
+ deploy_config: DeployConfig,
14
24
  ) -> InjectorBindings:
15
25
  lst: ta.List[InjectorBindingOrBindings] = [
26
+ inj.bind(deploy_config),
27
+
28
+ inj.bind(DeployAppManager, singleton=True),
29
+ inj.bind(DeployGitManager, singleton=True),
30
+ inj.bind(DeployVenvManager, singleton=True),
31
+
16
32
  bind_command(DeployCommand, DeployCommandExecutor),
33
+ bind_command(InterpCommand, InterpCommandExecutor),
17
34
  ]
18
35
 
36
+ if (dh := deploy_config.deploy_home) is not None:
37
+ dh = os.path.abspath(os.path.expanduser(dh))
38
+ lst.append(inj.bind(dh, key=DeployHome))
39
+
19
40
  return inj.as_bindings(*lst)
@@ -30,6 +30,9 @@ for dn in [
30
30
  'conf/supervisor',
31
31
  'venv',
32
32
  ]:
33
+
34
+ ==
35
+
33
36
  """
34
37
  import abc
35
38
  import dataclasses as dc
@@ -40,12 +43,23 @@ from omlish.lite.check import check
40
43
 
41
44
 
42
45
  DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
46
+ DeployPathSpec = ta.Literal['app', 'tag'] # ta.TypeAlias
43
47
 
44
48
 
45
49
  ##
46
50
 
47
51
 
48
52
  DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
53
+ DEPLOY_PATH_SPEC_SEPARATORS = '-.'
54
+
55
+ DEPLOY_PATH_SPECS: ta.FrozenSet[str] = frozenset([
56
+ 'app',
57
+ 'tag', # <rev>-<dt>
58
+ ])
59
+
60
+
61
+ class DeployPathError(Exception):
62
+ pass
49
63
 
50
64
 
51
65
  @dc.dataclass(frozen=True)
@@ -56,39 +70,43 @@ class DeployPathPart(abc.ABC): # noqa
56
70
  raise NotImplementedError
57
71
 
58
72
  @abc.abstractmethod
59
- def render(self) -> str:
73
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
60
74
  raise NotImplementedError
61
75
 
62
76
 
63
77
  #
64
78
 
65
79
 
66
- class DeployPathDir(DeployPathPart, abc.ABC):
80
+ class DirDeployPathPart(DeployPathPart, abc.ABC):
67
81
  @property
68
82
  def kind(self) -> DeployPathKind:
69
83
  return 'dir'
70
84
 
71
85
  @classmethod
72
- def parse(cls, s: str) -> 'DeployPathDir':
86
+ def parse(cls, s: str) -> 'DirDeployPathPart':
73
87
  if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
74
- check.equal(s, DEPLOY_PATH_SPEC_PLACEHOLDER)
75
- return SpecDeployPathDir()
88
+ check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
89
+ return SpecDirDeployPathPart(s[1:])
76
90
  else:
77
- return ConstDeployPathDir(s)
91
+ return ConstDirDeployPathPart(s)
78
92
 
79
93
 
80
- class DeployPathFile(DeployPathPart, abc.ABC):
94
+ class FileDeployPathPart(DeployPathPart, abc.ABC):
81
95
  @property
82
96
  def kind(self) -> DeployPathKind:
83
97
  return 'file'
84
98
 
85
99
  @classmethod
86
- def parse(cls, s: str) -> 'DeployPathFile':
100
+ def parse(cls, s: str) -> 'FileDeployPathPart':
87
101
  if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
88
102
  check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
89
- return SpecDeployPathFile(s[1:])
103
+ if not any(c in s for c in DEPLOY_PATH_SPEC_SEPARATORS):
104
+ return SpecFileDeployPathPart(s[1:], '')
105
+ else:
106
+ p = min(f for c in DEPLOY_PATH_SPEC_SEPARATORS if (f := s.find(c)) > 0)
107
+ return SpecFileDeployPathPart(s[1:p], s[p:])
90
108
  else:
91
- return ConstDeployPathFile(s)
109
+ return ConstFileDeployPathPart(s)
92
110
 
93
111
 
94
112
  #
@@ -103,41 +121,56 @@ class ConstDeployPathPart(DeployPathPart, abc.ABC):
103
121
  check.not_in('/', self.name)
104
122
  check.not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
105
123
 
106
- def render(self) -> str:
124
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
107
125
  return self.name
108
126
 
109
127
 
110
- class ConstDeployPathDir(ConstDeployPathPart, DeployPathDir):
128
+ class ConstDirDeployPathPart(ConstDeployPathPart, DirDeployPathPart):
111
129
  pass
112
130
 
113
131
 
114
- class ConstDeployPathFile(ConstDeployPathPart, DeployPathFile):
132
+ class ConstFileDeployPathPart(ConstDeployPathPart, FileDeployPathPart):
115
133
  pass
116
134
 
117
135
 
118
136
  #
119
137
 
120
138
 
139
+ @dc.dataclass(frozen=True)
121
140
  class SpecDeployPathPart(DeployPathPart, abc.ABC):
122
- pass
141
+ spec: str # DeployPathSpec
142
+
143
+ def __post_init__(self) -> None:
144
+ check.non_empty_str(self.spec)
145
+ for c in [*DEPLOY_PATH_SPEC_SEPARATORS, DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
146
+ check.not_in(c, self.spec)
147
+ check.in_(self.spec, DEPLOY_PATH_SPECS)
148
+
149
+ def _render_spec(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
150
+ if specs is not None:
151
+ return specs[self.spec] # type: ignore
152
+ else:
153
+ return DEPLOY_PATH_SPEC_PLACEHOLDER + self.spec
123
154
 
124
155
 
125
- class SpecDeployPathDir(SpecDeployPathPart, DeployPathDir):
126
- def render(self) -> str:
127
- return DEPLOY_PATH_SPEC_PLACEHOLDER
156
+ @dc.dataclass(frozen=True)
157
+ class SpecDirDeployPathPart(SpecDeployPathPart, DirDeployPathPart):
158
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
159
+ return self._render_spec(specs)
128
160
 
129
161
 
130
162
  @dc.dataclass(frozen=True)
131
- class SpecDeployPathFile(SpecDeployPathPart, DeployPathFile):
163
+ class SpecFileDeployPathPart(SpecDeployPathPart, FileDeployPathPart):
132
164
  suffix: str
133
165
 
134
166
  def __post_init__(self) -> None:
135
- check.non_empty_str(self.suffix)
136
- check.not_in('/', self.suffix)
137
- check.not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.suffix)
167
+ super().__post_init__()
168
+ if self.suffix:
169
+ for c in [DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
170
+ check.not_in(c, self.suffix)
138
171
 
139
- def render(self) -> str:
140
- return DEPLOY_PATH_SPEC_PLACEHOLDER + self.suffix
172
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
173
+ return self._render_spec(specs) + self.suffix
141
174
 
142
175
 
143
176
  ##
@@ -152,13 +185,24 @@ class DeployPath:
152
185
  for p in self.parts[:-1]:
153
186
  check.equal(p.kind, 'dir')
154
187
 
188
+ pd = {}
189
+ for i, p in enumerate(self.parts):
190
+ if isinstance(p, SpecDeployPathPart):
191
+ if p.spec in pd:
192
+ raise DeployPathError('Duplicate specs in path', self)
193
+ pd[p.spec] = i
194
+
195
+ if 'tag' in pd:
196
+ if 'app' not in pd or pd['app'] >= pd['tag']:
197
+ raise DeployPathError('Tag spec in path without preceding app', self)
198
+
155
199
  @property
156
200
  def kind(self) -> ta.Literal['file', 'dir']:
157
201
  return self.parts[-1].kind
158
202
 
159
- def render(self) -> str:
203
+ def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
160
204
  return os.path.join( # noqa
161
- *[p.render() for p in self.parts],
205
+ *[p.render(specs) for p in self.parts],
162
206
  *([''] if self.kind == 'dir' else []),
163
207
  )
164
208
 
@@ -166,12 +210,21 @@ class DeployPath:
166
210
  def parse(cls, s: str) -> 'DeployPath':
167
211
  tail_parse: ta.Callable[[str], DeployPathPart]
168
212
  if s.endswith('/'):
169
- tail_parse = DeployPathDir.parse
213
+ tail_parse = DirDeployPathPart.parse
170
214
  s = s[:-1]
171
215
  else:
172
- tail_parse = DeployPathFile.parse
216
+ tail_parse = FileDeployPathPart.parse
173
217
  ps = check.non_empty_str(s).split('/')
174
218
  return cls([
175
- *([DeployPathDir.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
219
+ *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
176
220
  tail_parse(ps[-1]),
177
221
  ])
222
+
223
+
224
+ ##
225
+
226
+
227
+ class DeployPathOwner(abc.ABC):
228
+ @abc.abstractmethod
229
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
230
+ raise NotImplementedError
@@ -0,0 +1,13 @@
1
+ import typing as ta
2
+
3
+
4
+ DeployHome = ta.NewType('DeployHome', str)
5
+
6
+ DeployApp = ta.NewType('DeployApp', str)
7
+ DeployTag = ta.NewType('DeployTag', str)
8
+ DeployRev = ta.NewType('DeployRev', str)
9
+
10
+
11
+ class DeployAppTag(ta.NamedTuple):
12
+ app: DeployApp
13
+ tag: DeployTag
@@ -0,0 +1,66 @@
1
+ # ruff: noqa: UP006 UP007
2
+ """
3
+ TODO:
4
+ - interp
5
+ - share more code with pyproject?
6
+ """
7
+ import os.path
8
+ import typing as ta
9
+
10
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_check_call
11
+
12
+ from .paths import DeployPath
13
+ from .paths import DeployPathOwner
14
+ from .types import DeployAppTag
15
+ from .types import DeployHome
16
+
17
+
18
+ class DeployVenvManager(DeployPathOwner):
19
+ def __init__(
20
+ self,
21
+ *,
22
+ deploy_home: DeployHome,
23
+ ) -> None:
24
+ super().__init__()
25
+
26
+ self._deploy_home = deploy_home
27
+ self._dir = os.path.join(deploy_home, 'venvs')
28
+
29
+ def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
30
+ return {
31
+ DeployPath.parse('venvs/@app/@tag/'),
32
+ }
33
+
34
+ async def setup_venv(
35
+ self,
36
+ app_dir: str,
37
+ venv_dir: str,
38
+ *,
39
+ use_uv: bool = True,
40
+ ) -> None:
41
+ sys_exe = 'python3'
42
+
43
+ await asyncio_subprocess_check_call(sys_exe, '-m', 'venv', venv_dir)
44
+
45
+ #
46
+
47
+ venv_exe = os.path.join(venv_dir, 'bin', 'python3')
48
+
49
+ #
50
+
51
+ reqs_txt = os.path.join(app_dir, 'requirements.txt')
52
+
53
+ if os.path.isfile(reqs_txt):
54
+ if use_uv:
55
+ await asyncio_subprocess_check_call(venv_exe, '-m', 'pip', 'install', 'uv')
56
+ pip_cmd = ['-m', 'uv', 'pip']
57
+ else:
58
+ pip_cmd = ['-m', 'pip']
59
+
60
+ await asyncio_subprocess_check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
61
+
62
+ async def setup_app_venv(self, app_tag: DeployAppTag) -> None:
63
+ await self.setup_venv(
64
+ os.path.join(self._deploy_home, 'apps', app_tag.app, app_tag.tag),
65
+ os.path.join(self._deploy_home, 'venvs', app_tag.app, app_tag.tag),
66
+ )