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.
ominfra/scripts/manage.py CHANGED
@@ -77,6 +77,9 @@ TomlParseFloat = ta.Callable[[str], ta.Any]
77
77
  TomlKey = ta.Tuple[str, ...]
78
78
  TomlPos = int # ta.TypeAlias
79
79
 
80
+ # deploy/paths/types.py
81
+ DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
82
+
80
83
  # ../../omlish/asyncs/asyncio/timeouts.py
81
84
  AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
82
85
 
@@ -92,6 +95,11 @@ CheckOnRaiseFn = ta.Callable[[Exception], None] # ta.TypeAlias
92
95
  CheckExceptionFactory = ta.Callable[..., Exception] # ta.TypeAlias
93
96
  CheckArgsRenderer = ta.Callable[..., ta.Optional[str]] # ta.TypeAlias
94
97
 
98
+ # ../../omlish/lite/typing.py
99
+ A0 = ta.TypeVar('A0')
100
+ A1 = ta.TypeVar('A1')
101
+ A2 = ta.TypeVar('A2')
102
+
95
103
  # ../../omdev/packaging/specifiers.py
96
104
  UnparsedVersion = ta.Union['Version', str]
97
105
  UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
@@ -101,10 +109,6 @@ CallableVersionOperator = ta.Callable[['Version', str], bool]
101
109
  CommandT = ta.TypeVar('CommandT', bound='Command')
102
110
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
103
111
 
104
- # deploy/types.py
105
- DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
106
- DeployPathPlaceholder = ta.Literal['app', 'tag', 'conf'] # ta.TypeAlias
107
-
108
112
  # ../../omlish/argparse/cli.py
109
113
  ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
110
114
 
@@ -125,6 +129,9 @@ AtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
125
129
  # ../configs.py
126
130
  ConfigMapping = ta.Mapping[str, ta.Any]
127
131
 
132
+ # deploy/specs.py
133
+ KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
134
+
128
135
  # ../../omlish/subprocesses.py
129
136
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
130
137
 
@@ -1380,6 +1387,25 @@ class DeployConfig:
1380
1387
  deploy_home: ta.Optional[str] = None
1381
1388
 
1382
1389
 
1390
+ ########################################
1391
+ # ../deploy/paths/types.py
1392
+
1393
+
1394
+ ##
1395
+
1396
+
1397
+ ########################################
1398
+ # ../deploy/types.py
1399
+
1400
+
1401
+ ##
1402
+
1403
+
1404
+ DeployHome = ta.NewType('DeployHome', str)
1405
+
1406
+ DeployRev = ta.NewType('DeployRev', str)
1407
+
1408
+
1383
1409
  ########################################
1384
1410
  # ../../pyremote.py
1385
1411
  """
@@ -2641,6 +2667,9 @@ def pycharm_debug_preamble(prd: PycharmRemoteDebug) -> str:
2641
2667
  # ../../../omlish/lite/reflect.py
2642
2668
 
2643
2669
 
2670
+ ##
2671
+
2672
+
2644
2673
  _GENERIC_ALIAS_TYPES = (
2645
2674
  ta._GenericAlias, # type: ignore # noqa
2646
2675
  *([ta._SpecialGenericAlias] if hasattr(ta, '_SpecialGenericAlias') else []), # noqa
@@ -2658,6 +2687,9 @@ is_union_alias = functools.partial(is_generic_alias, origin=ta.Union)
2658
2687
  is_callable_alias = functools.partial(is_generic_alias, origin=ta.Callable)
2659
2688
 
2660
2689
 
2690
+ ##
2691
+
2692
+
2661
2693
  def is_optional_alias(spec: ta.Any) -> bool:
2662
2694
  return (
2663
2695
  isinstance(spec, _GENERIC_ALIAS_TYPES) and # noqa
@@ -2672,6 +2704,9 @@ def get_optional_alias_arg(spec: ta.Any) -> ta.Any:
2672
2704
  return it
2673
2705
 
2674
2706
 
2707
+ ##
2708
+
2709
+
2675
2710
  def is_new_type(spec: ta.Any) -> bool:
2676
2711
  if isinstance(ta.NewType, type):
2677
2712
  return isinstance(spec, ta.NewType)
@@ -2684,6 +2719,26 @@ def get_new_type_supertype(spec: ta.Any) -> ta.Any:
2684
2719
  return spec.__supertype__
2685
2720
 
2686
2721
 
2722
+ ##
2723
+
2724
+
2725
+ def is_literal_type(spec: ta.Any) -> bool:
2726
+ if hasattr(ta, '_LiteralGenericAlias'):
2727
+ return isinstance(spec, ta._LiteralGenericAlias) # noqa
2728
+ else:
2729
+ return (
2730
+ isinstance(spec, ta._GenericAlias) and # type: ignore # noqa
2731
+ spec.__origin__ is ta.Literal
2732
+ )
2733
+
2734
+
2735
+ def get_literal_type_args(spec: ta.Any) -> ta.Iterable[ta.Any]:
2736
+ return spec.__args__
2737
+
2738
+
2739
+ ##
2740
+
2741
+
2687
2742
  def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
2688
2743
  seen = set()
2689
2744
  todo = list(reversed(cls.__subclasses__()))
@@ -2823,6 +2878,54 @@ def format_num_bytes(num_bytes: int) -> str:
2823
2878
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
2824
2879
 
2825
2880
 
2881
+ ########################################
2882
+ # ../../../omlish/lite/typing.py
2883
+
2884
+
2885
+ ##
2886
+ # A workaround for typing deficiencies (like `Argument 2 to NewType(...) must be subclassable`).
2887
+
2888
+
2889
+ @dc.dataclass(frozen=True)
2890
+ class AnyFunc(ta.Generic[T]):
2891
+ fn: ta.Callable[..., T]
2892
+
2893
+ def __call__(self, *args: ta.Any, **kwargs: ta.Any) -> T:
2894
+ return self.fn(*args, **kwargs)
2895
+
2896
+
2897
+ @dc.dataclass(frozen=True)
2898
+ class Func0(ta.Generic[T]):
2899
+ fn: ta.Callable[[], T]
2900
+
2901
+ def __call__(self) -> T:
2902
+ return self.fn()
2903
+
2904
+
2905
+ @dc.dataclass(frozen=True)
2906
+ class Func1(ta.Generic[A0, T]):
2907
+ fn: ta.Callable[[A0], T]
2908
+
2909
+ def __call__(self, a0: A0) -> T:
2910
+ return self.fn(a0)
2911
+
2912
+
2913
+ @dc.dataclass(frozen=True)
2914
+ class Func2(ta.Generic[A0, A1, T]):
2915
+ fn: ta.Callable[[A0, A1], T]
2916
+
2917
+ def __call__(self, a0: A0, a1: A1) -> T:
2918
+ return self.fn(a0, a1)
2919
+
2920
+
2921
+ @dc.dataclass(frozen=True)
2922
+ class Func3(ta.Generic[A0, A1, A2, T]):
2923
+ fn: ta.Callable[[A0, A1, A2], T]
2924
+
2925
+ def __call__(self, a0: A0, a1: A1, a2: A2) -> T:
2926
+ return self.fn(a0, a1, a2)
2927
+
2928
+
2826
2929
  ########################################
2827
2930
  # ../../../omlish/logs/filters.py
2828
2931
 
@@ -4149,39 +4252,227 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
4149
4252
 
4150
4253
 
4151
4254
  ########################################
4152
- # ../deploy/types.py
4255
+ # ../deploy/tags.py
4153
4256
 
4154
4257
 
4155
4258
  ##
4156
4259
 
4157
4260
 
4158
- DeployHome = ta.NewType('DeployHome', str)
4261
+ DEPLOY_TAG_SIGIL = '@'
4159
4262
 
4160
- DeployApp = ta.NewType('DeployApp', str)
4161
- DeployTag = ta.NewType('DeployTag', str)
4162
- DeployRev = ta.NewType('DeployRev', str)
4163
- DeployKey = ta.NewType('DeployKey', str)
4263
+ DEPLOY_TAG_SEPARATOR = '--'
4264
+
4265
+ DEPLOY_TAG_DELIMITERS: ta.AbstractSet[str] = frozenset([
4266
+ DEPLOY_TAG_SEPARATOR,
4267
+ '.',
4268
+ ])
4269
+
4270
+ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
4271
+ DEPLOY_TAG_SIGIL,
4272
+ *DEPLOY_TAG_DELIMITERS,
4273
+ '/',
4274
+ ])
4164
4275
 
4165
4276
 
4166
4277
  ##
4167
4278
 
4168
4279
 
4169
4280
  @dc.dataclass(frozen=True)
4170
- class DeployAppTag:
4171
- app: DeployApp
4172
- tag: DeployTag
4281
+ class DeployTag(abc.ABC): # noqa
4282
+ s: str
4173
4283
 
4174
4284
  def __post_init__(self) -> None:
4175
- for s in [self.app, self.tag]:
4176
- check.non_empty_str(s)
4177
- check.equal(s, s.strip())
4285
+ check.not_in(abc.ABC, type(self).__bases__)
4286
+ check.non_empty_str(self.s)
4287
+ for ch in DEPLOY_TAG_ILLEGAL_STRS:
4288
+ check.state(ch not in self.s)
4178
4289
 
4179
- def placeholders(self) -> ta.Mapping[DeployPathPlaceholder, str]:
4180
- return {
4181
- 'app': self.app,
4182
- 'tag': self.tag,
4290
+ #
4291
+
4292
+ tag_name: ta.ClassVar[str]
4293
+ tag_kwarg: ta.ClassVar[str]
4294
+
4295
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
4296
+ super().__init_subclass__(**kwargs)
4297
+
4298
+ if abc.ABC in cls.__bases__:
4299
+ return
4300
+
4301
+ for b in cls.__bases__:
4302
+ if issubclass(b, DeployTag):
4303
+ check.in_(abc.ABC, b.__bases__)
4304
+
4305
+ check.non_empty_str(tn := cls.tag_name)
4306
+ check.equal(tn, tn.lower().strip())
4307
+ check.not_in('_', tn)
4308
+
4309
+ check.state(not hasattr(cls, 'tag_kwarg'))
4310
+ cls.tag_kwarg = tn.replace('-', '_')
4311
+
4312
+
4313
+ ##
4314
+
4315
+
4316
+ _DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
4317
+ DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
4318
+
4319
+ _DEPLOY_TAGS_BY_NAME: ta.Dict[str, ta.Type[DeployTag]] = {}
4320
+ DEPLOY_TAGS_BY_NAME: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_NAME
4321
+
4322
+ _DEPLOY_TAGS_BY_KWARG: ta.Dict[str, ta.Type[DeployTag]] = {}
4323
+ DEPLOY_TAGS_BY_KWARG: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_KWARG
4324
+
4325
+
4326
+ def _register_deploy_tag(cls):
4327
+ check.not_in(cls.tag_name, _DEPLOY_TAGS_BY_NAME)
4328
+ check.not_in(cls.tag_kwarg, _DEPLOY_TAGS_BY_KWARG)
4329
+
4330
+ _DEPLOY_TAGS.add(cls)
4331
+ _DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
4332
+ _DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
4333
+
4334
+ return cls
4335
+
4336
+
4337
+ ##
4338
+
4339
+
4340
+ @_register_deploy_tag
4341
+ class DeployTime(DeployTag):
4342
+ tag_name: ta.ClassVar[str] = 'time'
4343
+
4344
+
4345
+ ##
4346
+
4347
+
4348
+ class NameDeployTag(DeployTag, abc.ABC): # noqa
4349
+ pass
4350
+
4351
+
4352
+ @_register_deploy_tag
4353
+ class DeployApp(NameDeployTag):
4354
+ tag_name: ta.ClassVar[str] = 'app'
4355
+
4356
+
4357
+ @_register_deploy_tag
4358
+ class DeployConf(NameDeployTag):
4359
+ tag_name: ta.ClassVar[str] = 'conf'
4360
+
4361
+
4362
+ ##
4363
+
4364
+
4365
+ class KeyDeployTag(DeployTag, abc.ABC): # noqa
4366
+ pass
4367
+
4368
+
4369
+ @_register_deploy_tag
4370
+ class DeployKey(KeyDeployTag):
4371
+ tag_name: ta.ClassVar[str] = 'deploy-key'
4372
+
4373
+
4374
+ @_register_deploy_tag
4375
+ class DeployAppKey(KeyDeployTag):
4376
+ tag_name: ta.ClassVar[str] = 'app-key'
4377
+
4378
+
4379
+ ##
4380
+
4381
+
4382
+ class RevDeployTag(DeployTag, abc.ABC): # noqa
4383
+ pass
4384
+
4385
+
4386
+ @_register_deploy_tag
4387
+ class DeployAppRev(RevDeployTag):
4388
+ tag_name: ta.ClassVar[str] = 'app-rev'
4389
+
4390
+
4391
+ ##
4392
+
4393
+
4394
+ class DeployTagMap:
4395
+ def __init__(
4396
+ self,
4397
+ *args: DeployTag,
4398
+ **kwargs: str,
4399
+ ) -> None:
4400
+ super().__init__()
4401
+
4402
+ dct: ta.Dict[ta.Type[DeployTag], DeployTag] = {}
4403
+
4404
+ for a in args:
4405
+ c = type(check.isinstance(a, DeployTag))
4406
+ check.not_in(c, dct)
4407
+ dct[c] = a
4408
+
4409
+ for k, v in kwargs.items():
4410
+ c = DEPLOY_TAGS_BY_KWARG[k]
4411
+ check.not_in(c, dct)
4412
+ dct[c] = c(v)
4413
+
4414
+ self._dct = dct
4415
+ self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
4416
+
4417
+ #
4418
+
4419
+ def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
4420
+ return DeployTagMap(
4421
+ *self,
4422
+ *args,
4423
+ **kwargs,
4424
+ )
4425
+
4426
+ def remove(self, *tags_or_names: ta.Union[ta.Type[DeployTag], str]) -> 'DeployTagMap':
4427
+ dcs = {
4428
+ check.issubclass(a, DeployTag) if isinstance(a, type) else DEPLOY_TAGS_BY_NAME[a]
4429
+ for a in tags_or_names
4183
4430
  }
4184
4431
 
4432
+ return DeployTagMap(*[
4433
+ t
4434
+ for t in self._dct.values()
4435
+ if t not in dcs
4436
+ ])
4437
+
4438
+ #
4439
+
4440
+ def __repr__(self) -> str:
4441
+ return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
4442
+
4443
+ def __hash__(self) -> int:
4444
+ return hash(self._tup)
4445
+
4446
+ def __eq__(self, other: object) -> bool:
4447
+ if isinstance(other, DeployTagMap):
4448
+ return self._tup == other._tup
4449
+ else:
4450
+ return NotImplemented
4451
+
4452
+ #
4453
+
4454
+ def __len__(self) -> int:
4455
+ return len(self._dct)
4456
+
4457
+ def __iter__(self) -> ta.Iterator[DeployTag]:
4458
+ return iter(self._dct.values())
4459
+
4460
+ def __getitem__(self, key: ta.Union[ta.Type[DeployTag], str]) -> DeployTag:
4461
+ if isinstance(key, str):
4462
+ return self._dct[DEPLOY_TAGS_BY_NAME[key]]
4463
+ elif isinstance(key, type):
4464
+ return self._dct[key]
4465
+ else:
4466
+ raise TypeError(key)
4467
+
4468
+ def __contains__(self, key: ta.Union[ta.Type[DeployTag], str]) -> bool:
4469
+ if isinstance(key, str):
4470
+ return DEPLOY_TAGS_BY_NAME[key] in self._dct
4471
+ elif isinstance(key, type):
4472
+ return key in self._dct
4473
+ else:
4474
+ raise TypeError(key)
4475
+
4185
4476
 
4186
4477
  ########################################
4187
4478
  # ../remote/config.py
@@ -5787,6 +6078,18 @@ class OptionalObjMarshaler(ObjMarshaler):
5787
6078
  return self.item.unmarshal(o, ctx)
5788
6079
 
5789
6080
 
6081
+ @dc.dataclass(frozen=True)
6082
+ class LiteralObjMarshaler(ObjMarshaler):
6083
+ item: ObjMarshaler
6084
+ vs: frozenset
6085
+
6086
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6087
+ return self.item.marshal(check.in_(o, self.vs), ctx)
6088
+
6089
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6090
+ return check.in_(self.item.unmarshal(o, ctx), self.vs)
6091
+
6092
+
5790
6093
  @dc.dataclass(frozen=True)
5791
6094
  class MappingObjMarshaler(ObjMarshaler):
5792
6095
  ty: type
@@ -5998,6 +6301,11 @@ class ObjMarshalerManager:
5998
6301
  if is_new_type(ty):
5999
6302
  return rec(get_new_type_supertype(ty))
6000
6303
 
6304
+ if is_literal_type(ty):
6305
+ lvs = frozenset(get_literal_type_args(ty))
6306
+ lty = check.single(set(map(type, lvs)))
6307
+ return LiteralObjMarshaler(rec(lty), lvs)
6308
+
6001
6309
  if is_generic_alias(ty):
6002
6310
  try:
6003
6311
  mt = self._generic_mapping_types[ta.get_origin(ty)]
@@ -6634,28 +6942,13 @@ TODO:
6634
6942
  ##
6635
6943
 
6636
6944
 
6637
- DEPLOY_PATH_PLACEHOLDER_SIGIL = '@'
6638
- DEPLOY_PATH_PLACEHOLDER_SEPARATOR = '--'
6639
-
6640
- DEPLOY_PATH_PLACEHOLDER_DELIMITERS: ta.AbstractSet[str] = frozenset([
6641
- DEPLOY_PATH_PLACEHOLDER_SEPARATOR,
6642
- '.',
6643
- ])
6644
-
6645
- DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
6646
- 'app',
6647
- 'tag',
6648
- 'conf',
6649
- ])
6650
-
6651
-
6652
6945
  class DeployPathError(Exception):
6653
6946
  pass
6654
6947
 
6655
6948
 
6656
6949
  class DeployPathRenderable(abc.ABC):
6657
6950
  @abc.abstractmethod
6658
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6951
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6659
6952
  raise NotImplementedError
6660
6953
 
6661
6954
 
@@ -6666,26 +6959,30 @@ class DeployPathNamePart(DeployPathRenderable, abc.ABC):
6666
6959
  @classmethod
6667
6960
  def parse(cls, s: str) -> 'DeployPathNamePart':
6668
6961
  check.non_empty_str(s)
6669
- if s.startswith(DEPLOY_PATH_PLACEHOLDER_SIGIL):
6670
- return PlaceholderDeployPathNamePart(s[1:])
6671
- elif s in DEPLOY_PATH_PLACEHOLDER_DELIMITERS:
6962
+ if s.startswith(DEPLOY_TAG_SIGIL):
6963
+ return TagDeployPathNamePart(s[1:])
6964
+ elif s in DEPLOY_TAG_DELIMITERS:
6672
6965
  return DelimiterDeployPathNamePart(s)
6673
6966
  else:
6674
6967
  return ConstDeployPathNamePart(s)
6675
6968
 
6676
6969
 
6677
6970
  @dc.dataclass(frozen=True)
6678
- class PlaceholderDeployPathNamePart(DeployPathNamePart):
6679
- placeholder: str # DeployPathPlaceholder
6971
+ class TagDeployPathNamePart(DeployPathNamePart):
6972
+ name: str
6680
6973
 
6681
6974
  def __post_init__(self) -> None:
6682
- check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
6975
+ check.in_(self.name, DEPLOY_TAGS_BY_NAME)
6683
6976
 
6684
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6685
- if placeholders is not None:
6686
- return placeholders[self.placeholder] # type: ignore
6977
+ @property
6978
+ def tag(self) -> ta.Type[DeployTag]:
6979
+ return DEPLOY_TAGS_BY_NAME[self.name]
6980
+
6981
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6982
+ if tags is not None:
6983
+ return tags[self.tag].s
6687
6984
  else:
6688
- return DEPLOY_PATH_PLACEHOLDER_SIGIL + self.placeholder
6985
+ return DEPLOY_TAG_SIGIL + self.name
6689
6986
 
6690
6987
 
6691
6988
  @dc.dataclass(frozen=True)
@@ -6693,9 +6990,9 @@ class DelimiterDeployPathNamePart(DeployPathNamePart):
6693
6990
  delimiter: str
6694
6991
 
6695
6992
  def __post_init__(self) -> None:
6696
- check.in_(self.delimiter, DEPLOY_PATH_PLACEHOLDER_DELIMITERS)
6993
+ check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
6697
6994
 
6698
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6995
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6699
6996
  return self.delimiter
6700
6997
 
6701
6998
 
@@ -6705,10 +7002,10 @@ class ConstDeployPathNamePart(DeployPathNamePart):
6705
7002
 
6706
7003
  def __post_init__(self) -> None:
6707
7004
  check.non_empty_str(self.const)
6708
- for c in [*DEPLOY_PATH_PLACEHOLDER_DELIMITERS, DEPLOY_PATH_PLACEHOLDER_SIGIL, '/']:
7005
+ for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
6709
7006
  check.not_in(c, self.const)
6710
7007
 
6711
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
7008
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6712
7009
  return self.const
6713
7010
 
6714
7011
 
@@ -6723,8 +7020,8 @@ class DeployPathName(DeployPathRenderable):
6723
7020
  if len(gl := list(g)) > 1:
6724
7021
  raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
6725
7022
 
6726
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6727
- return ''.join(p.render(placeholders) for p in self.parts)
7023
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
7024
+ return ''.join(p.render(tags) for p in self.parts)
6728
7025
 
6729
7026
  @classmethod
6730
7027
  def parse(cls, s: str) -> 'DeployPathName':
@@ -6734,7 +7031,7 @@ class DeployPathName(DeployPathRenderable):
6734
7031
  i = 0
6735
7032
  ps = []
6736
7033
  while i < len(s):
6737
- ns = [(n, d) for d in DEPLOY_PATH_PLACEHOLDER_DELIMITERS if (n := s.find(d, i)) >= 0]
7034
+ ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
6738
7035
  if not ns:
6739
7036
  ps.append(s[i:])
6740
7037
  break
@@ -6758,8 +7055,8 @@ class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
6758
7055
  def kind(self) -> DeployPathKind:
6759
7056
  raise NotImplementedError
6760
7057
 
6761
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6762
- return self.name.render(placeholders) + ('/' if self.kind == 'dir' else '')
7058
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
7059
+ return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
6763
7060
 
6764
7061
  @classmethod
6765
7062
  def parse(cls, s: str) -> 'DeployPathPart':
@@ -6805,20 +7102,20 @@ class DeployPath:
6805
7102
  for p in self.parts[:-1]:
6806
7103
  check.equal(p.kind, 'dir')
6807
7104
 
6808
- pd: ta.Dict[DeployPathPlaceholder, ta.List[int]] = {}
7105
+ @cached_nullary
7106
+ def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
7107
+ pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
6809
7108
  for i, np in enumerate(self.name_parts):
6810
- if isinstance(np, PlaceholderDeployPathNamePart):
6811
- pd.setdefault(ta.cast(DeployPathPlaceholder, np.placeholder), []).append(i)
6812
-
6813
- # if 'tag' in pd and 'app' not in pd:
6814
- # raise DeployPathError('Tag placeholder in path without app', self)
7109
+ if isinstance(np, TagDeployPathNamePart):
7110
+ pd.setdefault(np.tag, []).append(i)
7111
+ return pd
6815
7112
 
6816
7113
  @property
6817
7114
  def kind(self) -> ta.Literal['file', 'dir']:
6818
7115
  return self.parts[-1].kind
6819
7116
 
6820
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6821
- return ''.join([p.render(placeholders) for p in self.parts])
7117
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
7118
+ return ''.join([p.render(tags) for p in self.parts])
6822
7119
 
6823
7120
  @classmethod
6824
7121
  def parse(cls, s: str) -> 'DeployPath':
@@ -6842,6 +7139,16 @@ def check_valid_deploy_spec_path(s: str) -> str:
6842
7139
  return s
6843
7140
 
6844
7141
 
7142
+ class DeploySpecKeyed(ta.Generic[KeyDeployTagT]):
7143
+ @cached_nullary
7144
+ def _key_str(self) -> str:
7145
+ return hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8]
7146
+
7147
+ @abc.abstractmethod
7148
+ def key(self) -> KeyDeployTagT:
7149
+ raise NotImplementedError
7150
+
7151
+
6845
7152
  ##
6846
7153
 
6847
7154
 
@@ -6887,7 +7194,7 @@ class DeployVenvSpec:
6887
7194
 
6888
7195
 
6889
7196
  @dc.dataclass(frozen=True)
6890
- class DeployConfFile:
7197
+ class DeployAppConfFile:
6891
7198
  path: str
6892
7199
  body: str
6893
7200
 
@@ -6899,7 +7206,7 @@ class DeployConfFile:
6899
7206
 
6900
7207
 
6901
7208
  @dc.dataclass(frozen=True)
6902
- class DeployConfLink(abc.ABC): # noqa
7209
+ class DeployAppConfLink(abc.ABC): # noqa
6903
7210
  """
6904
7211
  May be either:
6905
7212
  - @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -6915,11 +7222,11 @@ class DeployConfLink(abc.ABC): # noqa
6915
7222
  check.equal(self.src.count('/'), 1)
6916
7223
 
6917
7224
 
6918
- class AppDeployConfLink(DeployConfLink):
7225
+ class CurrentOnlyDeployAppConfLink(DeployAppConfLink):
6919
7226
  pass
6920
7227
 
6921
7228
 
6922
- class TagDeployConfLink(DeployConfLink):
7229
+ class AllActiveDeployAppConfLink(DeployAppConfLink):
6923
7230
  pass
6924
7231
 
6925
7232
 
@@ -6927,10 +7234,10 @@ class TagDeployConfLink(DeployConfLink):
6927
7234
 
6928
7235
 
6929
7236
  @dc.dataclass(frozen=True)
6930
- class DeployConfSpec:
6931
- files: ta.Optional[ta.Sequence[DeployConfFile]] = None
7237
+ class DeployAppConfSpec:
7238
+ files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
6932
7239
 
6933
- links: ta.Optional[ta.Sequence[DeployConfLink]] = None
7240
+ links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
6934
7241
 
6935
7242
  def __post_init__(self) -> None:
6936
7243
  if self.files:
@@ -6944,18 +7251,37 @@ class DeployConfSpec:
6944
7251
 
6945
7252
 
6946
7253
  @dc.dataclass(frozen=True)
6947
- class DeploySpec:
7254
+ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
6948
7255
  app: DeployApp
6949
7256
 
6950
7257
  git: DeployGitSpec
6951
7258
 
6952
7259
  venv: ta.Optional[DeployVenvSpec] = None
6953
7260
 
6954
- conf: ta.Optional[DeployConfSpec] = None
7261
+ conf: ta.Optional[DeployAppConfSpec] = None
6955
7262
 
6956
- @cached_nullary
7263
+ # @ta.override
7264
+ def key(self) -> DeployAppKey:
7265
+ return DeployAppKey(self._key_str())
7266
+
7267
+
7268
+ ##
7269
+
7270
+
7271
+ @dc.dataclass(frozen=True)
7272
+ class DeploySpec(DeploySpecKeyed[DeployKey]):
7273
+ apps: ta.Sequence[DeployAppSpec]
7274
+
7275
+ def __post_init__(self) -> None:
7276
+ seen: ta.Set[DeployApp] = set()
7277
+ for a in self.apps:
7278
+ if a.app in seen:
7279
+ raise KeyError(a.app)
7280
+ seen.add(a.app)
7281
+
7282
+ # @ta.override
6957
7283
  def key(self) -> DeployKey:
6958
- return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
7284
+ return DeployKey(self._key_str())
6959
7285
 
6960
7286
 
6961
7287
  ########################################
@@ -7575,18 +7901,18 @@ class DeployConfManager:
7575
7901
 
7576
7902
  #
7577
7903
 
7578
- async def _write_conf_file(
7904
+ async def _write_app_conf_file(
7579
7905
  self,
7580
- cf: DeployConfFile,
7581
- conf_dir: str,
7906
+ acf: DeployAppConfFile,
7907
+ app_conf_dir: str,
7582
7908
  ) -> None:
7583
- conf_file = os.path.join(conf_dir, cf.path)
7584
- check.arg(is_path_in_dir(conf_dir, conf_file))
7909
+ conf_file = os.path.join(app_conf_dir, acf.path)
7910
+ check.arg(is_path_in_dir(app_conf_dir, conf_file))
7585
7911
 
7586
7912
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
7587
7913
 
7588
7914
  with open(conf_file, 'w') as f: # noqa
7589
- f.write(cf.body)
7915
+ f.write(acf.body)
7590
7916
 
7591
7917
  #
7592
7918
 
@@ -7595,15 +7921,18 @@ class DeployConfManager:
7595
7921
  link_src: str
7596
7922
  link_dst: str
7597
7923
 
7598
- def _compute_conf_link_dst(
7924
+ _UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
7925
+ _UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
7926
+
7927
+ def _compute_app_conf_link_dst(
7599
7928
  self,
7600
- link: DeployConfLink,
7601
- app_tag: DeployAppTag,
7602
- conf_dir: str,
7603
- link_dir: str,
7929
+ link: DeployAppConfLink,
7930
+ tags: DeployTagMap,
7931
+ app_conf_dir: str,
7932
+ conf_link_dir: str,
7604
7933
  ) -> _ComputedConfLink:
7605
- link_src = os.path.join(conf_dir, link.src)
7606
- check.arg(is_path_in_dir(conf_dir, link_src))
7934
+ link_src = os.path.join(app_conf_dir, link.src)
7935
+ check.arg(is_path_in_dir(app_conf_dir, link_src))
7607
7936
 
7608
7937
  #
7609
7938
 
@@ -7618,7 +7947,7 @@ class DeployConfManager:
7618
7947
  d, f = os.path.split(link.src)
7619
7948
  # TODO: check filename :|
7620
7949
  link_dst_pfx = d + '/'
7621
- link_dst_sfx = DEPLOY_PATH_PLACEHOLDER_SEPARATOR + f
7950
+ link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
7622
7951
 
7623
7952
  else: # noqa
7624
7953
  # @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -7632,10 +7961,10 @@ class DeployConfManager:
7632
7961
 
7633
7962
  #
7634
7963
 
7635
- if isinstance(link, AppDeployConfLink):
7636
- link_dst_mid = str(app_tag.app)
7637
- elif isinstance(link, TagDeployConfLink):
7638
- link_dst_mid = DEPLOY_PATH_PLACEHOLDER_SEPARATOR.join([app_tag.app, app_tag.tag])
7964
+ if isinstance(link, CurrentOnlyDeployAppConfLink):
7965
+ link_dst_mid = str(tags[DeployApp].s)
7966
+ elif isinstance(link, AllActiveDeployAppConfLink):
7967
+ link_dst_mid = self._UNIQUE_LINK_NAME.render(tags)
7639
7968
  else:
7640
7969
  raise TypeError(link)
7641
7970
 
@@ -7646,7 +7975,7 @@ class DeployConfManager:
7646
7975
  link_dst_mid,
7647
7976
  link_dst_sfx,
7648
7977
  ])
7649
- link_dst = os.path.join(link_dir, link_dst_name)
7978
+ link_dst = os.path.join(conf_link_dir, link_dst_name)
7650
7979
 
7651
7980
  return DeployConfManager._ComputedConfLink(
7652
7981
  is_dir=is_dir,
@@ -7654,24 +7983,24 @@ class DeployConfManager:
7654
7983
  link_dst=link_dst,
7655
7984
  )
7656
7985
 
7657
- async def _make_conf_link(
7986
+ async def _make_app_conf_link(
7658
7987
  self,
7659
- link: DeployConfLink,
7660
- app_tag: DeployAppTag,
7661
- conf_dir: str,
7662
- link_dir: str,
7988
+ link: DeployAppConfLink,
7989
+ tags: DeployTagMap,
7990
+ app_conf_dir: str,
7991
+ conf_link_dir: str,
7663
7992
  ) -> None:
7664
- comp = self._compute_conf_link_dst(
7993
+ comp = self._compute_app_conf_link_dst(
7665
7994
  link,
7666
- app_tag,
7667
- conf_dir,
7668
- link_dir,
7995
+ tags,
7996
+ app_conf_dir,
7997
+ conf_link_dir,
7669
7998
  )
7670
7999
 
7671
8000
  #
7672
8001
 
7673
- check.arg(is_path_in_dir(conf_dir, comp.link_src))
7674
- check.arg(is_path_in_dir(link_dir, comp.link_dst))
8002
+ check.arg(is_path_in_dir(app_conf_dir, comp.link_src))
8003
+ check.arg(is_path_in_dir(conf_link_dir, comp.link_dst))
7675
8004
 
7676
8005
  if comp.is_dir:
7677
8006
  check.arg(os.path.isdir(comp.link_src))
@@ -7689,27 +8018,27 @@ class DeployConfManager:
7689
8018
 
7690
8019
  #
7691
8020
 
7692
- async def write_conf(
8021
+ async def write_app_conf(
7693
8022
  self,
7694
- spec: DeployConfSpec,
7695
- app_tag: DeployAppTag,
7696
- conf_dir: str,
7697
- link_dir: str,
8023
+ spec: DeployAppConfSpec,
8024
+ tags: DeployTagMap,
8025
+ app_conf_dir: str,
8026
+ conf_link_dir: str,
7698
8027
  ) -> None:
7699
- for cf in spec.files or []:
7700
- await self._write_conf_file(
7701
- cf,
7702
- conf_dir,
8028
+ for acf in spec.files or []:
8029
+ await self._write_app_conf_file(
8030
+ acf,
8031
+ app_conf_dir,
7703
8032
  )
7704
8033
 
7705
8034
  #
7706
8035
 
7707
8036
  for link in spec.links or []:
7708
- await self._make_conf_link(
8037
+ await self._make_app_conf_link(
7709
8038
  link,
7710
- app_tag,
7711
- conf_dir,
7712
- link_dir,
8039
+ tags,
8040
+ app_conf_dir,
8041
+ conf_link_dir,
7713
8042
  )
7714
8043
 
7715
8044
 
@@ -9312,19 +9641,6 @@ def bind_commands(
9312
9641
  # ../deploy/apps.py
9313
9642
 
9314
9643
 
9315
- def make_deploy_tag(
9316
- rev: DeployRev,
9317
- key: DeployKey,
9318
- *,
9319
- utcnow: ta.Optional[datetime.datetime] = None,
9320
- ) -> DeployTag:
9321
- if utcnow is None:
9322
- utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
9323
- now_fmt = '%Y%m%dT%H%M%SZ'
9324
- now_str = utcnow.strftime(now_fmt)
9325
- return DeployTag('-'.join([now_str, rev, key]))
9326
-
9327
-
9328
9644
  class DeployAppManager(DeployPathOwner):
9329
9645
  def __init__(
9330
9646
  self,
@@ -9345,32 +9661,27 @@ class DeployAppManager(DeployPathOwner):
9345
9661
 
9346
9662
  #
9347
9663
 
9348
- _APP_TAG_DIR_STR = 'tags/apps/@app/@tag/'
9349
- _APP_TAG_DIR = DeployPath.parse(_APP_TAG_DIR_STR)
9664
+ _APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
9665
+ _APP_DIR = DeployPath.parse(_APP_DIR_STR)
9350
9666
 
9351
- _CONF_TAG_DIR_STR = 'tags/conf/@tag--@app/'
9352
- _CONF_TAG_DIR = DeployPath.parse(_CONF_TAG_DIR_STR)
9353
-
9354
- _DEPLOY_DIR_STR = 'deploys/@tag--@app/'
9667
+ _DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
9355
9668
  _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
9356
9669
 
9357
9670
  _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
9358
- _CONF_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf')
9671
+ _CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
9359
9672
 
9360
9673
  @cached_nullary
9361
9674
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
9362
9675
  return {
9363
- self._APP_TAG_DIR,
9364
-
9365
- self._CONF_TAG_DIR,
9676
+ self._APP_DIR,
9366
9677
 
9367
9678
  self._DEPLOY_DIR,
9368
9679
 
9369
9680
  self._APP_DEPLOY_LINK,
9370
- self._CONF_DEPLOY_LINK,
9681
+ self._CONF_DEPLOY_DIR,
9371
9682
 
9372
9683
  *[
9373
- DeployPath.parse(f'{self._APP_TAG_DIR_STR}{sfx}/')
9684
+ DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
9374
9685
  for sfx in [
9375
9686
  'conf',
9376
9687
  'git',
@@ -9383,26 +9694,21 @@ class DeployAppManager(DeployPathOwner):
9383
9694
 
9384
9695
  async def prepare_app(
9385
9696
  self,
9386
- spec: DeploySpec,
9697
+ spec: DeployAppSpec,
9698
+ tags: DeployTagMap,
9387
9699
  ) -> None:
9388
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.git.rev, spec.key()))
9389
-
9390
- #
9391
-
9392
9700
  deploy_home = check.non_empty_str(self._deploy_home)
9393
9701
 
9394
9702
  def build_path(pth: DeployPath) -> str:
9395
- return os.path.join(deploy_home, pth.render(app_tag.placeholders()))
9703
+ return os.path.join(deploy_home, pth.render(tags))
9396
9704
 
9397
- app_tag_dir = build_path(self._APP_TAG_DIR)
9398
- conf_tag_dir = build_path(self._CONF_TAG_DIR)
9705
+ app_dir = build_path(self._APP_DIR)
9399
9706
  deploy_dir = build_path(self._DEPLOY_DIR)
9400
9707
  app_deploy_link = build_path(self._APP_DEPLOY_LINK)
9401
- conf_deploy_link_file = build_path(self._CONF_DEPLOY_LINK)
9402
9708
 
9403
9709
  #
9404
9710
 
9405
- os.makedirs(deploy_dir)
9711
+ os.makedirs(deploy_dir, exist_ok=True)
9406
9712
 
9407
9713
  deploying_link = os.path.join(deploy_home, 'deploys/deploying')
9408
9714
  relative_symlink(
@@ -9414,9 +9720,9 @@ class DeployAppManager(DeployPathOwner):
9414
9720
 
9415
9721
  #
9416
9722
 
9417
- os.makedirs(app_tag_dir)
9723
+ os.makedirs(app_dir)
9418
9724
  relative_symlink(
9419
- app_tag_dir,
9725
+ app_dir,
9420
9726
  app_deploy_link,
9421
9727
  target_is_directory=True,
9422
9728
  make_dirs=True,
@@ -9424,37 +9730,33 @@ class DeployAppManager(DeployPathOwner):
9424
9730
 
9425
9731
  #
9426
9732
 
9427
- os.makedirs(conf_tag_dir)
9428
- relative_symlink(
9429
- conf_tag_dir,
9430
- conf_deploy_link_file,
9431
- target_is_directory=True,
9432
- make_dirs=True,
9433
- )
9733
+ deploy_conf_dir = os.path.join(deploy_dir, 'conf')
9734
+ os.makedirs(deploy_conf_dir, exist_ok=True)
9434
9735
 
9435
9736
  #
9436
9737
 
9437
- def mirror_symlinks(src: str, dst: str) -> None:
9438
- def mirror_link(lp: str) -> None:
9439
- check.state(os.path.islink(lp))
9440
- shutil.copy2(
9441
- lp,
9442
- os.path.join(dst, os.path.relpath(lp, src)),
9443
- follow_symlinks=False,
9444
- )
9445
-
9446
- for dp, dns, fns in os.walk(src, followlinks=False):
9447
- for fn in fns:
9448
- mirror_link(os.path.join(dp, fn))
9449
-
9450
- for dn in dns:
9451
- dp2 = os.path.join(dp, dn)
9452
- if os.path.islink(dp2):
9453
- mirror_link(dp2)
9454
- else:
9455
- os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
9738
+ # def mirror_symlinks(src: str, dst: str) -> None:
9739
+ # def mirror_link(lp: str) -> None:
9740
+ # check.state(os.path.islink(lp))
9741
+ # shutil.copy2(
9742
+ # lp,
9743
+ # os.path.join(dst, os.path.relpath(lp, src)),
9744
+ # follow_symlinks=False,
9745
+ # )
9746
+ #
9747
+ # for dp, dns, fns in os.walk(src, followlinks=False):
9748
+ # for fn in fns:
9749
+ # mirror_link(os.path.join(dp, fn))
9750
+ #
9751
+ # for dn in dns:
9752
+ # dp2 = os.path.join(dp, dn)
9753
+ # if os.path.islink(dp2):
9754
+ # mirror_link(dp2)
9755
+ # else:
9756
+ # os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
9456
9757
 
9457
9758
  current_link = os.path.join(deploy_home, 'deploys/current')
9759
+
9458
9760
  # if os.path.exists(current_link):
9459
9761
  # mirror_symlinks(
9460
9762
  # os.path.join(current_link, 'conf'),
@@ -9467,31 +9769,31 @@ class DeployAppManager(DeployPathOwner):
9467
9769
 
9468
9770
  #
9469
9771
 
9470
- git_dir = os.path.join(app_tag_dir, 'git')
9772
+ app_git_dir = os.path.join(app_dir, 'git')
9471
9773
  await self._git.checkout(
9472
9774
  spec.git,
9473
- git_dir,
9775
+ app_git_dir,
9474
9776
  )
9475
9777
 
9476
9778
  #
9477
9779
 
9478
9780
  if spec.venv is not None:
9479
- venv_dir = os.path.join(app_tag_dir, 'venv')
9781
+ app_venv_dir = os.path.join(app_dir, 'venv')
9480
9782
  await self._venvs.setup_venv(
9481
9783
  spec.venv,
9482
- git_dir,
9483
- venv_dir,
9784
+ app_git_dir,
9785
+ app_venv_dir,
9484
9786
  )
9485
9787
 
9486
9788
  #
9487
9789
 
9488
9790
  if spec.conf is not None:
9489
- conf_dir = os.path.join(app_tag_dir, 'conf')
9490
- await self._conf.write_conf(
9791
+ app_conf_dir = os.path.join(app_dir, 'conf')
9792
+ await self._conf.write_app_conf(
9491
9793
  spec.conf,
9492
- app_tag,
9493
- conf_dir,
9494
- conf_tag_dir,
9794
+ tags,
9795
+ app_conf_dir,
9796
+ deploy_conf_dir,
9495
9797
  )
9496
9798
 
9497
9799
  #
@@ -10232,18 +10534,37 @@ class SystemInterpProvider(InterpProvider):
10232
10534
  # ../deploy/deploy.py
10233
10535
 
10234
10536
 
10537
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
10538
+
10539
+
10540
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
10541
+
10542
+
10235
10543
  class DeployManager:
10236
10544
  def __init__(
10237
10545
  self,
10238
10546
  *,
10239
10547
  apps: DeployAppManager,
10240
10548
  paths: DeployPathsManager,
10549
+
10550
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
10241
10551
  ):
10242
10552
  super().__init__()
10243
10553
 
10244
10554
  self._apps = apps
10245
10555
  self._paths = paths
10246
10556
 
10557
+ self._utc_clock = utc_clock
10558
+
10559
+ def _utc_now(self) -> datetime.datetime:
10560
+ if self._utc_clock is not None:
10561
+ return self._utc_clock() # noqa
10562
+ else:
10563
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
10564
+
10565
+ def _make_deploy_time(self) -> DeployTime:
10566
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
10567
+
10247
10568
  async def run_deploy(
10248
10569
  self,
10249
10570
  spec: DeploySpec,
@@ -10252,7 +10573,24 @@ class DeployManager:
10252
10573
 
10253
10574
  #
10254
10575
 
10255
- await self._apps.prepare_app(spec)
10576
+ deploy_tags = DeployTagMap(
10577
+ self._make_deploy_time(),
10578
+ spec.key(),
10579
+ )
10580
+
10581
+ #
10582
+
10583
+ for app in spec.apps:
10584
+ app_tags = deploy_tags.add(
10585
+ app.app,
10586
+ app.key(),
10587
+ DeployAppRev(app.git.rev),
10588
+ )
10589
+
10590
+ await self._apps.prepare_app(
10591
+ app,
10592
+ app_tags,
10593
+ )
10256
10594
 
10257
10595
 
10258
10596
  ########################################