ominfra 0.0.0.dev172__py3-none-any.whl → 0.0.0.dev174__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/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
  ########################################