ominfra 0.0.0.dev154__py3-none-any.whl → 0.0.0.dev156__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.
- ominfra/manage/bootstrap.py +4 -0
- ominfra/manage/bootstrap_.py +5 -0
- ominfra/manage/commands/inject.py +8 -11
- ominfra/manage/commands/{execution.py → local.py} +1 -5
- ominfra/manage/commands/ping.py +23 -0
- ominfra/manage/commands/subprocess.py +3 -4
- ominfra/manage/commands/types.py +8 -0
- ominfra/manage/deploy/apps.py +72 -0
- ominfra/manage/deploy/config.py +8 -0
- ominfra/manage/deploy/git.py +136 -0
- ominfra/manage/deploy/inject.py +21 -0
- ominfra/manage/deploy/paths.py +81 -28
- ominfra/manage/deploy/types.py +13 -0
- ominfra/manage/deploy/venvs.py +66 -0
- ominfra/manage/inject.py +20 -4
- ominfra/manage/main.py +15 -27
- ominfra/manage/remote/_main.py +1 -1
- ominfra/manage/remote/config.py +0 -2
- ominfra/manage/remote/connection.py +7 -24
- ominfra/manage/remote/execution.py +1 -1
- ominfra/manage/remote/inject.py +3 -14
- ominfra/manage/remote/spawning.py +2 -2
- ominfra/manage/system/commands.py +22 -2
- ominfra/manage/system/config.py +3 -1
- ominfra/manage/system/inject.py +16 -6
- ominfra/manage/system/packages.py +38 -14
- ominfra/manage/system/platforms.py +72 -0
- ominfra/manage/targets/__init__.py +0 -0
- ominfra/manage/targets/connection.py +150 -0
- ominfra/manage/targets/inject.py +42 -0
- ominfra/manage/targets/targets.py +87 -0
- ominfra/scripts/journald2aws.py +205 -134
- ominfra/scripts/manage.py +2192 -734
- ominfra/scripts/supervisor.py +187 -25
- ominfra/supervisor/configs.py +163 -18
- {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev156.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev156.dist-info}/RECORD +42 -31
- ominfra/manage/system/types.py +0 -5
- /ominfra/manage/{commands → deploy}/interp.py +0 -0
- {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev156.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev156.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev156.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev154.dist-info → ominfra-0.0.0.dev156.dist-info}/top_level.txt +0 -0
ominfra/manage/bootstrap.py
CHANGED
@@ -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()
|
ominfra/manage/bootstrap_.py
CHANGED
@@ -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 .
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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)
|
@@ -6,8 +6,7 @@ import subprocess
|
|
6
6
|
import time
|
7
7
|
import typing as ta
|
8
8
|
|
9
|
-
from omlish.lite.asyncio.subprocesses import
|
10
|
-
from omlish.lite.asyncio.subprocesses import asyncio_subprocess_popen
|
9
|
+
from omlish.lite.asyncio.subprocesses import asyncio_subprocesses
|
11
10
|
from omlish.lite.check import check
|
12
11
|
from omlish.lite.subprocesses import SUBPROCESS_CHANNEL_OPTION_VALUES
|
13
12
|
from omlish.lite.subprocesses import SubprocessChannelOption
|
@@ -51,7 +50,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
|
|
51
50
|
class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
|
52
51
|
async def execute(self, cmd: SubprocessCommand) -> SubprocessCommand.Output:
|
53
52
|
proc: asyncio.subprocess.Process
|
54
|
-
async with
|
53
|
+
async with asyncio_subprocesses.popen(
|
55
54
|
*subprocess_maybe_shell_wrap_exec(*cmd.cmd),
|
56
55
|
|
57
56
|
shell=cmd.shell,
|
@@ -65,7 +64,7 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
|
|
65
64
|
timeout=cmd.timeout,
|
66
65
|
) as proc:
|
67
66
|
start_time = time.time()
|
68
|
-
stdout, stderr = await
|
67
|
+
stdout, stderr = await asyncio_subprocesses.communicate(
|
69
68
|
proc,
|
70
69
|
input=cmd.input,
|
71
70
|
timeout=cmd.timeout,
|
@@ -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,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_subprocesses
|
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_subprocesses.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_subprocesses.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)
|
ominfra/manage/deploy/inject.py
CHANGED
@@ -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)
|
ominfra/manage/deploy/paths.py
CHANGED
@@ -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
|
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) -> '
|
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
|
88
|
+
check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
|
89
|
+
return SpecDirDeployPathPart(s[1:])
|
76
90
|
else:
|
77
|
-
return
|
91
|
+
return ConstDirDeployPathPart(s)
|
78
92
|
|
79
93
|
|
80
|
-
class
|
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) -> '
|
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
|
-
|
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
|
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
|
128
|
+
class ConstDirDeployPathPart(ConstDeployPathPart, DirDeployPathPart):
|
111
129
|
pass
|
112
130
|
|
113
131
|
|
114
|
-
class
|
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
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
163
|
+
class SpecFileDeployPathPart(SpecDeployPathPart, FileDeployPathPart):
|
132
164
|
suffix: str
|
133
165
|
|
134
166
|
def __post_init__(self) -> None:
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
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 =
|
213
|
+
tail_parse = DirDeployPathPart.parse
|
170
214
|
s = s[:-1]
|
171
215
|
else:
|
172
|
-
tail_parse =
|
216
|
+
tail_parse = FileDeployPathPart.parse
|
173
217
|
ps = check.non_empty_str(s).split('/')
|
174
218
|
return cls([
|
175
|
-
*([
|
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
|