ominfra 0.0.0.dev172__py3-none-any.whl → 0.0.0.dev174__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
- import datetime
3
2
  import os.path
4
- import shutil
5
3
  import typing as ta
6
4
 
7
5
  from omlish.lite.cached import cached_nullary
@@ -12,28 +10,12 @@ from .conf import DeployConfManager
12
10
  from .git import DeployGitManager
13
11
  from .paths.owners import DeployPathOwner
14
12
  from .paths.paths import DeployPath
15
- from .specs import DeploySpec
16
- from .types import DeployAppTag
13
+ from .specs import DeployAppSpec
14
+ from .tags import DeployTagMap
17
15
  from .types import DeployHome
18
- from .types import DeployKey
19
- from .types import DeployRev
20
- from .types import DeployTag
21
16
  from .venvs import DeployVenvManager
22
17
 
23
18
 
24
- def make_deploy_tag(
25
- rev: DeployRev,
26
- key: DeployKey,
27
- *,
28
- utcnow: ta.Optional[datetime.datetime] = None,
29
- ) -> DeployTag:
30
- if utcnow is None:
31
- utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
32
- now_fmt = '%Y%m%dT%H%M%SZ'
33
- now_str = utcnow.strftime(now_fmt)
34
- return DeployTag('-'.join([now_str, rev, key]))
35
-
36
-
37
19
  class DeployAppManager(DeployPathOwner):
38
20
  def __init__(
39
21
  self,
@@ -54,32 +36,27 @@ class DeployAppManager(DeployPathOwner):
54
36
 
55
37
  #
56
38
 
57
- _APP_TAG_DIR_STR = 'tags/apps/@app/@tag/'
58
- _APP_TAG_DIR = DeployPath.parse(_APP_TAG_DIR_STR)
39
+ _APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
40
+ _APP_DIR = DeployPath.parse(_APP_DIR_STR)
59
41
 
60
- _CONF_TAG_DIR_STR = 'tags/conf/@tag--@app/'
61
- _CONF_TAG_DIR = DeployPath.parse(_CONF_TAG_DIR_STR)
62
-
63
- _DEPLOY_DIR_STR = 'deploys/@tag--@app/'
42
+ _DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
64
43
  _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
65
44
 
66
45
  _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
67
- _CONF_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf')
46
+ _CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
68
47
 
69
48
  @cached_nullary
70
49
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
71
50
  return {
72
- self._APP_TAG_DIR,
73
-
74
- self._CONF_TAG_DIR,
51
+ self._APP_DIR,
75
52
 
76
53
  self._DEPLOY_DIR,
77
54
 
78
55
  self._APP_DEPLOY_LINK,
79
- self._CONF_DEPLOY_LINK,
56
+ self._CONF_DEPLOY_DIR,
80
57
 
81
58
  *[
82
- DeployPath.parse(f'{self._APP_TAG_DIR_STR}{sfx}/')
59
+ DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
83
60
  for sfx in [
84
61
  'conf',
85
62
  'git',
@@ -92,26 +69,21 @@ class DeployAppManager(DeployPathOwner):
92
69
 
93
70
  async def prepare_app(
94
71
  self,
95
- spec: DeploySpec,
72
+ spec: DeployAppSpec,
73
+ tags: DeployTagMap,
96
74
  ) -> None:
97
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.git.rev, spec.key()))
98
-
99
- #
100
-
101
75
  deploy_home = check.non_empty_str(self._deploy_home)
102
76
 
103
77
  def build_path(pth: DeployPath) -> str:
104
- return os.path.join(deploy_home, pth.render(app_tag.placeholders()))
78
+ return os.path.join(deploy_home, pth.render(tags))
105
79
 
106
- app_tag_dir = build_path(self._APP_TAG_DIR)
107
- conf_tag_dir = build_path(self._CONF_TAG_DIR)
80
+ app_dir = build_path(self._APP_DIR)
108
81
  deploy_dir = build_path(self._DEPLOY_DIR)
109
82
  app_deploy_link = build_path(self._APP_DEPLOY_LINK)
110
- conf_deploy_link_file = build_path(self._CONF_DEPLOY_LINK)
111
83
 
112
84
  #
113
85
 
114
- os.makedirs(deploy_dir)
86
+ os.makedirs(deploy_dir, exist_ok=True)
115
87
 
116
88
  deploying_link = os.path.join(deploy_home, 'deploys/deploying')
117
89
  relative_symlink(
@@ -123,9 +95,9 @@ class DeployAppManager(DeployPathOwner):
123
95
 
124
96
  #
125
97
 
126
- os.makedirs(app_tag_dir)
98
+ os.makedirs(app_dir)
127
99
  relative_symlink(
128
- app_tag_dir,
100
+ app_dir,
129
101
  app_deploy_link,
130
102
  target_is_directory=True,
131
103
  make_dirs=True,
@@ -133,37 +105,33 @@ class DeployAppManager(DeployPathOwner):
133
105
 
134
106
  #
135
107
 
136
- os.makedirs(conf_tag_dir)
137
- relative_symlink(
138
- conf_tag_dir,
139
- conf_deploy_link_file,
140
- target_is_directory=True,
141
- make_dirs=True,
142
- )
108
+ deploy_conf_dir = os.path.join(deploy_dir, 'conf')
109
+ os.makedirs(deploy_conf_dir, exist_ok=True)
143
110
 
144
111
  #
145
112
 
146
- def mirror_symlinks(src: str, dst: str) -> None:
147
- def mirror_link(lp: str) -> None:
148
- check.state(os.path.islink(lp))
149
- shutil.copy2(
150
- lp,
151
- os.path.join(dst, os.path.relpath(lp, src)),
152
- follow_symlinks=False,
153
- )
154
-
155
- for dp, dns, fns in os.walk(src, followlinks=False):
156
- for fn in fns:
157
- mirror_link(os.path.join(dp, fn))
158
-
159
- for dn in dns:
160
- dp2 = os.path.join(dp, dn)
161
- if os.path.islink(dp2):
162
- mirror_link(dp2)
163
- else:
164
- os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
113
+ # def mirror_symlinks(src: str, dst: str) -> None:
114
+ # def mirror_link(lp: str) -> None:
115
+ # check.state(os.path.islink(lp))
116
+ # shutil.copy2(
117
+ # lp,
118
+ # os.path.join(dst, os.path.relpath(lp, src)),
119
+ # follow_symlinks=False,
120
+ # )
121
+ #
122
+ # for dp, dns, fns in os.walk(src, followlinks=False):
123
+ # for fn in fns:
124
+ # mirror_link(os.path.join(dp, fn))
125
+ #
126
+ # for dn in dns:
127
+ # dp2 = os.path.join(dp, dn)
128
+ # if os.path.islink(dp2):
129
+ # mirror_link(dp2)
130
+ # else:
131
+ # os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
165
132
 
166
133
  current_link = os.path.join(deploy_home, 'deploys/current')
134
+
167
135
  # if os.path.exists(current_link):
168
136
  # mirror_symlinks(
169
137
  # os.path.join(current_link, 'conf'),
@@ -176,31 +144,31 @@ class DeployAppManager(DeployPathOwner):
176
144
 
177
145
  #
178
146
 
179
- git_dir = os.path.join(app_tag_dir, 'git')
147
+ app_git_dir = os.path.join(app_dir, 'git')
180
148
  await self._git.checkout(
181
149
  spec.git,
182
- git_dir,
150
+ app_git_dir,
183
151
  )
184
152
 
185
153
  #
186
154
 
187
155
  if spec.venv is not None:
188
- venv_dir = os.path.join(app_tag_dir, 'venv')
156
+ app_venv_dir = os.path.join(app_dir, 'venv')
189
157
  await self._venvs.setup_venv(
190
158
  spec.venv,
191
- git_dir,
192
- venv_dir,
159
+ app_git_dir,
160
+ app_venv_dir,
193
161
  )
194
162
 
195
163
  #
196
164
 
197
165
  if spec.conf is not None:
198
- conf_dir = os.path.join(app_tag_dir, 'conf')
199
- await self._conf.write_conf(
166
+ app_conf_dir = os.path.join(app_dir, 'conf')
167
+ await self._conf.write_app_conf(
200
168
  spec.conf,
201
- app_tag,
202
- conf_dir,
203
- conf_tag_dir,
169
+ tags,
170
+ app_conf_dir,
171
+ deploy_conf_dir,
204
172
  )
205
173
 
206
174
  #
@@ -23,13 +23,15 @@ from omlish.lite.check import check
23
23
  from omlish.os.paths import is_path_in_dir
24
24
  from omlish.os.paths import relative_symlink
25
25
 
26
- from .paths.paths import DEPLOY_PATH_PLACEHOLDER_SEPARATOR
27
- from .specs import AppDeployConfLink
28
- from .specs import DeployConfFile
29
- from .specs import DeployConfLink
30
- from .specs import DeployConfSpec
31
- from .specs import TagDeployConfLink
32
- from .types import DeployAppTag
26
+ from .paths.paths import DeployPath
27
+ from .specs import AllActiveDeployAppConfLink
28
+ from .specs import CurrentOnlyDeployAppConfLink
29
+ from .specs import DeployAppConfFile
30
+ from .specs import DeployAppConfLink
31
+ from .specs import DeployAppConfSpec
32
+ from .tags import DEPLOY_TAG_SEPARATOR
33
+ from .tags import DeployApp
34
+ from .tags import DeployTagMap
33
35
  from .types import DeployHome
34
36
 
35
37
 
@@ -45,18 +47,18 @@ class DeployConfManager:
45
47
 
46
48
  #
47
49
 
48
- async def _write_conf_file(
50
+ async def _write_app_conf_file(
49
51
  self,
50
- cf: DeployConfFile,
51
- conf_dir: str,
52
+ acf: DeployAppConfFile,
53
+ app_conf_dir: str,
52
54
  ) -> None:
53
- conf_file = os.path.join(conf_dir, cf.path)
54
- check.arg(is_path_in_dir(conf_dir, conf_file))
55
+ conf_file = os.path.join(app_conf_dir, acf.path)
56
+ check.arg(is_path_in_dir(app_conf_dir, conf_file))
55
57
 
56
58
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
57
59
 
58
60
  with open(conf_file, 'w') as f: # noqa
59
- f.write(cf.body)
61
+ f.write(acf.body)
60
62
 
61
63
  #
62
64
 
@@ -65,15 +67,18 @@ class DeployConfManager:
65
67
  link_src: str
66
68
  link_dst: str
67
69
 
68
- def _compute_conf_link_dst(
70
+ _UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
71
+ _UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
72
+
73
+ def _compute_app_conf_link_dst(
69
74
  self,
70
- link: DeployConfLink,
71
- app_tag: DeployAppTag,
72
- conf_dir: str,
73
- link_dir: str,
75
+ link: DeployAppConfLink,
76
+ tags: DeployTagMap,
77
+ app_conf_dir: str,
78
+ conf_link_dir: str,
74
79
  ) -> _ComputedConfLink:
75
- link_src = os.path.join(conf_dir, link.src)
76
- check.arg(is_path_in_dir(conf_dir, link_src))
80
+ link_src = os.path.join(app_conf_dir, link.src)
81
+ check.arg(is_path_in_dir(app_conf_dir, link_src))
77
82
 
78
83
  #
79
84
 
@@ -88,7 +93,7 @@ class DeployConfManager:
88
93
  d, f = os.path.split(link.src)
89
94
  # TODO: check filename :|
90
95
  link_dst_pfx = d + '/'
91
- link_dst_sfx = DEPLOY_PATH_PLACEHOLDER_SEPARATOR + f
96
+ link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
92
97
 
93
98
  else: # noqa
94
99
  # @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -102,10 +107,10 @@ class DeployConfManager:
102
107
 
103
108
  #
104
109
 
105
- if isinstance(link, AppDeployConfLink):
106
- link_dst_mid = str(app_tag.app)
107
- elif isinstance(link, TagDeployConfLink):
108
- link_dst_mid = DEPLOY_PATH_PLACEHOLDER_SEPARATOR.join([app_tag.app, app_tag.tag])
110
+ if isinstance(link, CurrentOnlyDeployAppConfLink):
111
+ link_dst_mid = str(tags[DeployApp].s)
112
+ elif isinstance(link, AllActiveDeployAppConfLink):
113
+ link_dst_mid = self._UNIQUE_LINK_NAME.render(tags)
109
114
  else:
110
115
  raise TypeError(link)
111
116
 
@@ -116,7 +121,7 @@ class DeployConfManager:
116
121
  link_dst_mid,
117
122
  link_dst_sfx,
118
123
  ])
119
- link_dst = os.path.join(link_dir, link_dst_name)
124
+ link_dst = os.path.join(conf_link_dir, link_dst_name)
120
125
 
121
126
  return DeployConfManager._ComputedConfLink(
122
127
  is_dir=is_dir,
@@ -124,24 +129,24 @@ class DeployConfManager:
124
129
  link_dst=link_dst,
125
130
  )
126
131
 
127
- async def _make_conf_link(
132
+ async def _make_app_conf_link(
128
133
  self,
129
- link: DeployConfLink,
130
- app_tag: DeployAppTag,
131
- conf_dir: str,
132
- link_dir: str,
134
+ link: DeployAppConfLink,
135
+ tags: DeployTagMap,
136
+ app_conf_dir: str,
137
+ conf_link_dir: str,
133
138
  ) -> None:
134
- comp = self._compute_conf_link_dst(
139
+ comp = self._compute_app_conf_link_dst(
135
140
  link,
136
- app_tag,
137
- conf_dir,
138
- link_dir,
141
+ tags,
142
+ app_conf_dir,
143
+ conf_link_dir,
139
144
  )
140
145
 
141
146
  #
142
147
 
143
- check.arg(is_path_in_dir(conf_dir, comp.link_src))
144
- check.arg(is_path_in_dir(link_dir, comp.link_dst))
148
+ check.arg(is_path_in_dir(app_conf_dir, comp.link_src))
149
+ check.arg(is_path_in_dir(conf_link_dir, comp.link_dst))
145
150
 
146
151
  if comp.is_dir:
147
152
  check.arg(os.path.isdir(comp.link_src))
@@ -159,25 +164,25 @@ class DeployConfManager:
159
164
 
160
165
  #
161
166
 
162
- async def write_conf(
167
+ async def write_app_conf(
163
168
  self,
164
- spec: DeployConfSpec,
165
- app_tag: DeployAppTag,
166
- conf_dir: str,
167
- link_dir: str,
169
+ spec: DeployAppConfSpec,
170
+ tags: DeployTagMap,
171
+ app_conf_dir: str,
172
+ conf_link_dir: str,
168
173
  ) -> None:
169
- for cf in spec.files or []:
170
- await self._write_conf_file(
171
- cf,
172
- conf_dir,
174
+ for acf in spec.files or []:
175
+ await self._write_app_conf_file(
176
+ acf,
177
+ app_conf_dir,
173
178
  )
174
179
 
175
180
  #
176
181
 
177
182
  for link in spec.links or []:
178
- await self._make_conf_link(
183
+ await self._make_app_conf_link(
179
184
  link,
180
- app_tag,
181
- conf_dir,
182
- link_dir,
185
+ tags,
186
+ app_conf_dir,
187
+ conf_link_dir,
183
188
  )
@@ -1,7 +1,21 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ import datetime
3
+ import typing as ta
4
+
5
+ from omlish.lite.typing import Func0
6
+
2
7
  from .apps import DeployAppManager
3
8
  from .paths.manager import DeployPathsManager
4
9
  from .specs import DeploySpec
10
+ from .tags import DeployAppRev
11
+ from .tags import DeployTagMap
12
+ from .tags import DeployTime
13
+
14
+
15
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
16
+
17
+
18
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
5
19
 
6
20
 
7
21
  class DeployManager:
@@ -10,12 +24,25 @@ class DeployManager:
10
24
  *,
11
25
  apps: DeployAppManager,
12
26
  paths: DeployPathsManager,
27
+
28
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
13
29
  ):
14
30
  super().__init__()
15
31
 
16
32
  self._apps = apps
17
33
  self._paths = paths
18
34
 
35
+ self._utc_clock = utc_clock
36
+
37
+ def _utc_now(self) -> datetime.datetime:
38
+ if self._utc_clock is not None:
39
+ return self._utc_clock() # noqa
40
+ else:
41
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
42
+
43
+ def _make_deploy_time(self) -> DeployTime:
44
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
45
+
19
46
  async def run_deploy(
20
47
  self,
21
48
  spec: DeploySpec,
@@ -24,4 +51,21 @@ class DeployManager:
24
51
 
25
52
  #
26
53
 
27
- await self._apps.prepare_app(spec)
54
+ deploy_tags = DeployTagMap(
55
+ self._make_deploy_time(),
56
+ spec.key(),
57
+ )
58
+
59
+ #
60
+
61
+ for app in spec.apps:
62
+ app_tags = deploy_tags.add(
63
+ app.app,
64
+ app.key(),
65
+ DeployAppRev(app.git.rev),
66
+ )
67
+
68
+ await self._apps.prepare_app(
69
+ app,
70
+ app_tags,
71
+ )
@@ -13,38 +13,28 @@ import dataclasses as dc
13
13
  import itertools
14
14
  import typing as ta
15
15
 
16
+ from omlish.lite.cached import cached_nullary
16
17
  from omlish.lite.check import check
17
18
  from omlish.lite.strings import split_keep_delimiter
18
19
 
19
- from ..types import DeployPathKind
20
- from ..types import DeployPathPlaceholder
20
+ from ..tags import DEPLOY_TAG_DELIMITERS
21
+ from ..tags import DEPLOY_TAG_SIGIL
22
+ from ..tags import DEPLOY_TAGS_BY_NAME
23
+ from ..tags import DeployTag
24
+ from ..tags import DeployTagMap
25
+ from .types import DeployPathKind
21
26
 
22
27
 
23
28
  ##
24
29
 
25
30
 
26
- DEPLOY_PATH_PLACEHOLDER_SIGIL = '@'
27
- DEPLOY_PATH_PLACEHOLDER_SEPARATOR = '--'
28
-
29
- DEPLOY_PATH_PLACEHOLDER_DELIMITERS: ta.AbstractSet[str] = frozenset([
30
- DEPLOY_PATH_PLACEHOLDER_SEPARATOR,
31
- '.',
32
- ])
33
-
34
- DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
35
- 'app',
36
- 'tag',
37
- 'conf',
38
- ])
39
-
40
-
41
31
  class DeployPathError(Exception):
42
32
  pass
43
33
 
44
34
 
45
35
  class DeployPathRenderable(abc.ABC):
46
36
  @abc.abstractmethod
47
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
37
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
48
38
  raise NotImplementedError
49
39
 
50
40
 
@@ -55,26 +45,30 @@ class DeployPathNamePart(DeployPathRenderable, abc.ABC):
55
45
  @classmethod
56
46
  def parse(cls, s: str) -> 'DeployPathNamePart':
57
47
  check.non_empty_str(s)
58
- if s.startswith(DEPLOY_PATH_PLACEHOLDER_SIGIL):
59
- return PlaceholderDeployPathNamePart(s[1:])
60
- elif s in DEPLOY_PATH_PLACEHOLDER_DELIMITERS:
48
+ if s.startswith(DEPLOY_TAG_SIGIL):
49
+ return TagDeployPathNamePart(s[1:])
50
+ elif s in DEPLOY_TAG_DELIMITERS:
61
51
  return DelimiterDeployPathNamePart(s)
62
52
  else:
63
53
  return ConstDeployPathNamePart(s)
64
54
 
65
55
 
66
56
  @dc.dataclass(frozen=True)
67
- class PlaceholderDeployPathNamePart(DeployPathNamePart):
68
- placeholder: str # DeployPathPlaceholder
57
+ class TagDeployPathNamePart(DeployPathNamePart):
58
+ name: str
69
59
 
70
60
  def __post_init__(self) -> None:
71
- check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
61
+ check.in_(self.name, DEPLOY_TAGS_BY_NAME)
72
62
 
73
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
74
- if placeholders is not None:
75
- return placeholders[self.placeholder] # type: ignore
63
+ @property
64
+ def tag(self) -> ta.Type[DeployTag]:
65
+ return DEPLOY_TAGS_BY_NAME[self.name]
66
+
67
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
68
+ if tags is not None:
69
+ return tags[self.tag].s
76
70
  else:
77
- return DEPLOY_PATH_PLACEHOLDER_SIGIL + self.placeholder
71
+ return DEPLOY_TAG_SIGIL + self.name
78
72
 
79
73
 
80
74
  @dc.dataclass(frozen=True)
@@ -82,9 +76,9 @@ class DelimiterDeployPathNamePart(DeployPathNamePart):
82
76
  delimiter: str
83
77
 
84
78
  def __post_init__(self) -> None:
85
- check.in_(self.delimiter, DEPLOY_PATH_PLACEHOLDER_DELIMITERS)
79
+ check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
86
80
 
87
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
81
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
88
82
  return self.delimiter
89
83
 
90
84
 
@@ -94,10 +88,10 @@ class ConstDeployPathNamePart(DeployPathNamePart):
94
88
 
95
89
  def __post_init__(self) -> None:
96
90
  check.non_empty_str(self.const)
97
- for c in [*DEPLOY_PATH_PLACEHOLDER_DELIMITERS, DEPLOY_PATH_PLACEHOLDER_SIGIL, '/']:
91
+ for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
98
92
  check.not_in(c, self.const)
99
93
 
100
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
94
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
101
95
  return self.const
102
96
 
103
97
 
@@ -112,8 +106,8 @@ class DeployPathName(DeployPathRenderable):
112
106
  if len(gl := list(g)) > 1:
113
107
  raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
114
108
 
115
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
116
- return ''.join(p.render(placeholders) for p in self.parts)
109
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
110
+ return ''.join(p.render(tags) for p in self.parts)
117
111
 
118
112
  @classmethod
119
113
  def parse(cls, s: str) -> 'DeployPathName':
@@ -123,7 +117,7 @@ class DeployPathName(DeployPathRenderable):
123
117
  i = 0
124
118
  ps = []
125
119
  while i < len(s):
126
- ns = [(n, d) for d in DEPLOY_PATH_PLACEHOLDER_DELIMITERS if (n := s.find(d, i)) >= 0]
120
+ ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
127
121
  if not ns:
128
122
  ps.append(s[i:])
129
123
  break
@@ -147,8 +141,8 @@ class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
147
141
  def kind(self) -> DeployPathKind:
148
142
  raise NotImplementedError
149
143
 
150
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
151
- return self.name.render(placeholders) + ('/' if self.kind == 'dir' else '')
144
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
145
+ return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
152
146
 
153
147
  @classmethod
154
148
  def parse(cls, s: str) -> 'DeployPathPart':
@@ -194,20 +188,20 @@ class DeployPath:
194
188
  for p in self.parts[:-1]:
195
189
  check.equal(p.kind, 'dir')
196
190
 
197
- pd: ta.Dict[DeployPathPlaceholder, ta.List[int]] = {}
191
+ @cached_nullary
192
+ def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
193
+ pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
198
194
  for i, np in enumerate(self.name_parts):
199
- if isinstance(np, PlaceholderDeployPathNamePart):
200
- pd.setdefault(ta.cast(DeployPathPlaceholder, np.placeholder), []).append(i)
201
-
202
- # if 'tag' in pd and 'app' not in pd:
203
- # raise DeployPathError('Tag placeholder in path without app', self)
195
+ if isinstance(np, TagDeployPathNamePart):
196
+ pd.setdefault(np.tag, []).append(i)
197
+ return pd
204
198
 
205
199
  @property
206
200
  def kind(self) -> ta.Literal['file', 'dir']:
207
201
  return self.parts[-1].kind
208
202
 
209
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
210
- return ''.join([p.render(placeholders) for p in self.parts])
203
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
204
+ return ''.join([p.render(tags) for p in self.parts])
211
205
 
212
206
  @classmethod
213
207
  def parse(cls, s: str) -> 'DeployPath':
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+
5
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
6
+
7
+
8
+ ##