ominfra 0.0.0.dev171__py3-none-any.whl → 0.0.0.dev173__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
  """
@@ -2823,6 +2849,54 @@ def format_num_bytes(num_bytes: int) -> str:
2823
2849
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
2824
2850
 
2825
2851
 
2852
+ ########################################
2853
+ # ../../../omlish/lite/typing.py
2854
+
2855
+
2856
+ ##
2857
+ # A workaround for typing deficiencies (like `Argument 2 to NewType(...) must be subclassable`).
2858
+
2859
+
2860
+ @dc.dataclass(frozen=True)
2861
+ class AnyFunc(ta.Generic[T]):
2862
+ fn: ta.Callable[..., T]
2863
+
2864
+ def __call__(self, *args: ta.Any, **kwargs: ta.Any) -> T:
2865
+ return self.fn(*args, **kwargs)
2866
+
2867
+
2868
+ @dc.dataclass(frozen=True)
2869
+ class Func0(ta.Generic[T]):
2870
+ fn: ta.Callable[[], T]
2871
+
2872
+ def __call__(self) -> T:
2873
+ return self.fn()
2874
+
2875
+
2876
+ @dc.dataclass(frozen=True)
2877
+ class Func1(ta.Generic[A0, T]):
2878
+ fn: ta.Callable[[A0], T]
2879
+
2880
+ def __call__(self, a0: A0) -> T:
2881
+ return self.fn(a0)
2882
+
2883
+
2884
+ @dc.dataclass(frozen=True)
2885
+ class Func2(ta.Generic[A0, A1, T]):
2886
+ fn: ta.Callable[[A0, A1], T]
2887
+
2888
+ def __call__(self, a0: A0, a1: A1) -> T:
2889
+ return self.fn(a0, a1)
2890
+
2891
+
2892
+ @dc.dataclass(frozen=True)
2893
+ class Func3(ta.Generic[A0, A1, A2, T]):
2894
+ fn: ta.Callable[[A0, A1, A2], T]
2895
+
2896
+ def __call__(self, a0: A0, a1: A1, a2: A2) -> T:
2897
+ return self.fn(a0, a1, a2)
2898
+
2899
+
2826
2900
  ########################################
2827
2901
  # ../../../omlish/logs/filters.py
2828
2902
 
@@ -4149,39 +4223,227 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
4149
4223
 
4150
4224
 
4151
4225
  ########################################
4152
- # ../deploy/types.py
4226
+ # ../deploy/tags.py
4153
4227
 
4154
4228
 
4155
4229
  ##
4156
4230
 
4157
4231
 
4158
- DeployHome = ta.NewType('DeployHome', str)
4232
+ DEPLOY_TAG_SIGIL = '@'
4159
4233
 
4160
- DeployApp = ta.NewType('DeployApp', str)
4161
- DeployTag = ta.NewType('DeployTag', str)
4162
- DeployRev = ta.NewType('DeployRev', str)
4163
- DeployKey = ta.NewType('DeployKey', str)
4234
+ DEPLOY_TAG_SEPARATOR = '--'
4235
+
4236
+ DEPLOY_TAG_DELIMITERS: ta.AbstractSet[str] = frozenset([
4237
+ DEPLOY_TAG_SEPARATOR,
4238
+ '.',
4239
+ ])
4240
+
4241
+ DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
4242
+ DEPLOY_TAG_SIGIL,
4243
+ *DEPLOY_TAG_DELIMITERS,
4244
+ '/',
4245
+ ])
4164
4246
 
4165
4247
 
4166
4248
  ##
4167
4249
 
4168
4250
 
4169
4251
  @dc.dataclass(frozen=True)
4170
- class DeployAppTag:
4171
- app: DeployApp
4172
- tag: DeployTag
4252
+ class DeployTag(abc.ABC): # noqa
4253
+ s: str
4173
4254
 
4174
4255
  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())
4256
+ check.not_in(abc.ABC, type(self).__bases__)
4257
+ check.non_empty_str(self.s)
4258
+ for ch in DEPLOY_TAG_ILLEGAL_STRS:
4259
+ check.state(ch not in self.s)
4178
4260
 
4179
- def placeholders(self) -> ta.Mapping[DeployPathPlaceholder, str]:
4180
- return {
4181
- 'app': self.app,
4182
- 'tag': self.tag,
4261
+ #
4262
+
4263
+ tag_name: ta.ClassVar[str]
4264
+ tag_kwarg: ta.ClassVar[str]
4265
+
4266
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
4267
+ super().__init_subclass__(**kwargs)
4268
+
4269
+ if abc.ABC in cls.__bases__:
4270
+ return
4271
+
4272
+ for b in cls.__bases__:
4273
+ if issubclass(b, DeployTag):
4274
+ check.in_(abc.ABC, b.__bases__)
4275
+
4276
+ check.non_empty_str(tn := cls.tag_name)
4277
+ check.equal(tn, tn.lower().strip())
4278
+ check.not_in('_', tn)
4279
+
4280
+ check.state(not hasattr(cls, 'tag_kwarg'))
4281
+ cls.tag_kwarg = tn.replace('-', '_')
4282
+
4283
+
4284
+ ##
4285
+
4286
+
4287
+ _DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
4288
+ DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
4289
+
4290
+ _DEPLOY_TAGS_BY_NAME: ta.Dict[str, ta.Type[DeployTag]] = {}
4291
+ DEPLOY_TAGS_BY_NAME: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_NAME
4292
+
4293
+ _DEPLOY_TAGS_BY_KWARG: ta.Dict[str, ta.Type[DeployTag]] = {}
4294
+ DEPLOY_TAGS_BY_KWARG: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_KWARG
4295
+
4296
+
4297
+ def _register_deploy_tag(cls):
4298
+ check.not_in(cls.tag_name, _DEPLOY_TAGS_BY_NAME)
4299
+ check.not_in(cls.tag_kwarg, _DEPLOY_TAGS_BY_KWARG)
4300
+
4301
+ _DEPLOY_TAGS.add(cls)
4302
+ _DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
4303
+ _DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
4304
+
4305
+ return cls
4306
+
4307
+
4308
+ ##
4309
+
4310
+
4311
+ @_register_deploy_tag
4312
+ class DeployTime(DeployTag):
4313
+ tag_name: ta.ClassVar[str] = 'time'
4314
+
4315
+
4316
+ ##
4317
+
4318
+
4319
+ class NameDeployTag(DeployTag, abc.ABC): # noqa
4320
+ pass
4321
+
4322
+
4323
+ @_register_deploy_tag
4324
+ class DeployApp(NameDeployTag):
4325
+ tag_name: ta.ClassVar[str] = 'app'
4326
+
4327
+
4328
+ @_register_deploy_tag
4329
+ class DeployConf(NameDeployTag):
4330
+ tag_name: ta.ClassVar[str] = 'conf'
4331
+
4332
+
4333
+ ##
4334
+
4335
+
4336
+ class KeyDeployTag(DeployTag, abc.ABC): # noqa
4337
+ pass
4338
+
4339
+
4340
+ @_register_deploy_tag
4341
+ class DeployKey(KeyDeployTag):
4342
+ tag_name: ta.ClassVar[str] = 'deploy-key'
4343
+
4344
+
4345
+ @_register_deploy_tag
4346
+ class DeployAppKey(KeyDeployTag):
4347
+ tag_name: ta.ClassVar[str] = 'app-key'
4348
+
4349
+
4350
+ ##
4351
+
4352
+
4353
+ class RevDeployTag(DeployTag, abc.ABC): # noqa
4354
+ pass
4355
+
4356
+
4357
+ @_register_deploy_tag
4358
+ class DeployAppRev(RevDeployTag):
4359
+ tag_name: ta.ClassVar[str] = 'app-rev'
4360
+
4361
+
4362
+ ##
4363
+
4364
+
4365
+ class DeployTagMap:
4366
+ def __init__(
4367
+ self,
4368
+ *args: DeployTag,
4369
+ **kwargs: str,
4370
+ ) -> None:
4371
+ super().__init__()
4372
+
4373
+ dct: ta.Dict[ta.Type[DeployTag], DeployTag] = {}
4374
+
4375
+ for a in args:
4376
+ c = type(check.isinstance(a, DeployTag))
4377
+ check.not_in(c, dct)
4378
+ dct[c] = a
4379
+
4380
+ for k, v in kwargs.items():
4381
+ c = DEPLOY_TAGS_BY_KWARG[k]
4382
+ check.not_in(c, dct)
4383
+ dct[c] = c(v)
4384
+
4385
+ self._dct = dct
4386
+ self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
4387
+
4388
+ #
4389
+
4390
+ def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
4391
+ return DeployTagMap(
4392
+ *self,
4393
+ *args,
4394
+ **kwargs,
4395
+ )
4396
+
4397
+ def remove(self, *tags_or_names: ta.Union[ta.Type[DeployTag], str]) -> 'DeployTagMap':
4398
+ dcs = {
4399
+ check.issubclass(a, DeployTag) if isinstance(a, type) else DEPLOY_TAGS_BY_NAME[a]
4400
+ for a in tags_or_names
4183
4401
  }
4184
4402
 
4403
+ return DeployTagMap(*[
4404
+ t
4405
+ for t in self._dct.values()
4406
+ if t not in dcs
4407
+ ])
4408
+
4409
+ #
4410
+
4411
+ def __repr__(self) -> str:
4412
+ return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
4413
+
4414
+ def __hash__(self) -> int:
4415
+ return hash(self._tup)
4416
+
4417
+ def __eq__(self, other: object) -> bool:
4418
+ if isinstance(other, DeployTagMap):
4419
+ return self._tup == other._tup
4420
+ else:
4421
+ return NotImplemented
4422
+
4423
+ #
4424
+
4425
+ def __len__(self) -> int:
4426
+ return len(self._dct)
4427
+
4428
+ def __iter__(self) -> ta.Iterator[DeployTag]:
4429
+ return iter(self._dct.values())
4430
+
4431
+ def __getitem__(self, key: ta.Union[ta.Type[DeployTag], str]) -> DeployTag:
4432
+ if isinstance(key, str):
4433
+ return self._dct[DEPLOY_TAGS_BY_NAME[key]]
4434
+ elif isinstance(key, type):
4435
+ return self._dct[key]
4436
+ else:
4437
+ raise TypeError(key)
4438
+
4439
+ def __contains__(self, key: ta.Union[ta.Type[DeployTag], str]) -> bool:
4440
+ if isinstance(key, str):
4441
+ return DEPLOY_TAGS_BY_NAME[key] in self._dct
4442
+ elif isinstance(key, type):
4443
+ return key in self._dct
4444
+ else:
4445
+ raise TypeError(key)
4446
+
4185
4447
 
4186
4448
  ########################################
4187
4449
  # ../remote/config.py
@@ -4320,15 +4582,12 @@ BEST_PYTHON_SH = """\
4320
4582
  bv=""
4321
4583
  bx=""
4322
4584
 
4323
- for version in "" 3 3.{8..13}; do
4324
- x="python$version"
4325
- v=$($x -c "import sys; print(sys.version_info[:])" 2>/dev/null)
4326
- if [ $? -eq 0 ]; then
4327
- cv=$(echo $v | tr -d "(), ")
4328
- if [ -z "$bv" ] || [ "$cv" \\> "$bv" ]; then
4329
- bv=$cv
4330
- bx=$x
4331
- fi
4585
+ for v in "" 3 3.{8..13}; do
4586
+ x="python$v"
4587
+ v=$($x -c "import sys; print((\\"%02d\\" * 3) % sys.version_info[:3])" 2>/dev/null)
4588
+ if [ $? -eq 0 ] && ([ -z "$bv" ] || [ "$v" \\> "$bv" ]); then
4589
+ bv=$v
4590
+ bx=$x
4332
4591
  fi
4333
4592
  done
4334
4593
 
@@ -6637,28 +6896,13 @@ TODO:
6637
6896
  ##
6638
6897
 
6639
6898
 
6640
- DEPLOY_PATH_PLACEHOLDER_SIGIL = '@'
6641
- DEPLOY_PATH_PLACEHOLDER_SEPARATOR = '--'
6642
-
6643
- DEPLOY_PATH_PLACEHOLDER_DELIMITERS: ta.AbstractSet[str] = frozenset([
6644
- DEPLOY_PATH_PLACEHOLDER_SEPARATOR,
6645
- '.',
6646
- ])
6647
-
6648
- DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
6649
- 'app',
6650
- 'tag',
6651
- 'conf',
6652
- ])
6653
-
6654
-
6655
6899
  class DeployPathError(Exception):
6656
6900
  pass
6657
6901
 
6658
6902
 
6659
6903
  class DeployPathRenderable(abc.ABC):
6660
6904
  @abc.abstractmethod
6661
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6905
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6662
6906
  raise NotImplementedError
6663
6907
 
6664
6908
 
@@ -6669,26 +6913,30 @@ class DeployPathNamePart(DeployPathRenderable, abc.ABC):
6669
6913
  @classmethod
6670
6914
  def parse(cls, s: str) -> 'DeployPathNamePart':
6671
6915
  check.non_empty_str(s)
6672
- if s.startswith(DEPLOY_PATH_PLACEHOLDER_SIGIL):
6673
- return PlaceholderDeployPathNamePart(s[1:])
6674
- elif s in DEPLOY_PATH_PLACEHOLDER_DELIMITERS:
6916
+ if s.startswith(DEPLOY_TAG_SIGIL):
6917
+ return TagDeployPathNamePart(s[1:])
6918
+ elif s in DEPLOY_TAG_DELIMITERS:
6675
6919
  return DelimiterDeployPathNamePart(s)
6676
6920
  else:
6677
6921
  return ConstDeployPathNamePart(s)
6678
6922
 
6679
6923
 
6680
6924
  @dc.dataclass(frozen=True)
6681
- class PlaceholderDeployPathNamePart(DeployPathNamePart):
6682
- placeholder: str # DeployPathPlaceholder
6925
+ class TagDeployPathNamePart(DeployPathNamePart):
6926
+ name: str
6683
6927
 
6684
6928
  def __post_init__(self) -> None:
6685
- check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
6929
+ check.in_(self.name, DEPLOY_TAGS_BY_NAME)
6686
6930
 
6687
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6688
- if placeholders is not None:
6689
- return placeholders[self.placeholder] # type: ignore
6931
+ @property
6932
+ def tag(self) -> ta.Type[DeployTag]:
6933
+ return DEPLOY_TAGS_BY_NAME[self.name]
6934
+
6935
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6936
+ if tags is not None:
6937
+ return tags[self.tag].s
6690
6938
  else:
6691
- return DEPLOY_PATH_PLACEHOLDER_SIGIL + self.placeholder
6939
+ return DEPLOY_TAG_SIGIL + self.name
6692
6940
 
6693
6941
 
6694
6942
  @dc.dataclass(frozen=True)
@@ -6696,9 +6944,9 @@ class DelimiterDeployPathNamePart(DeployPathNamePart):
6696
6944
  delimiter: str
6697
6945
 
6698
6946
  def __post_init__(self) -> None:
6699
- check.in_(self.delimiter, DEPLOY_PATH_PLACEHOLDER_DELIMITERS)
6947
+ check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
6700
6948
 
6701
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6949
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6702
6950
  return self.delimiter
6703
6951
 
6704
6952
 
@@ -6708,10 +6956,10 @@ class ConstDeployPathNamePart(DeployPathNamePart):
6708
6956
 
6709
6957
  def __post_init__(self) -> None:
6710
6958
  check.non_empty_str(self.const)
6711
- for c in [*DEPLOY_PATH_PLACEHOLDER_DELIMITERS, DEPLOY_PATH_PLACEHOLDER_SIGIL, '/']:
6959
+ for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
6712
6960
  check.not_in(c, self.const)
6713
6961
 
6714
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6962
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6715
6963
  return self.const
6716
6964
 
6717
6965
 
@@ -6726,8 +6974,8 @@ class DeployPathName(DeployPathRenderable):
6726
6974
  if len(gl := list(g)) > 1:
6727
6975
  raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
6728
6976
 
6729
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6730
- return ''.join(p.render(placeholders) for p in self.parts)
6977
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6978
+ return ''.join(p.render(tags) for p in self.parts)
6731
6979
 
6732
6980
  @classmethod
6733
6981
  def parse(cls, s: str) -> 'DeployPathName':
@@ -6737,7 +6985,7 @@ class DeployPathName(DeployPathRenderable):
6737
6985
  i = 0
6738
6986
  ps = []
6739
6987
  while i < len(s):
6740
- ns = [(n, d) for d in DEPLOY_PATH_PLACEHOLDER_DELIMITERS if (n := s.find(d, i)) >= 0]
6988
+ ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
6741
6989
  if not ns:
6742
6990
  ps.append(s[i:])
6743
6991
  break
@@ -6761,8 +7009,8 @@ class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
6761
7009
  def kind(self) -> DeployPathKind:
6762
7010
  raise NotImplementedError
6763
7011
 
6764
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6765
- return self.name.render(placeholders) + ('/' if self.kind == 'dir' else '')
7012
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
7013
+ return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
6766
7014
 
6767
7015
  @classmethod
6768
7016
  def parse(cls, s: str) -> 'DeployPathPart':
@@ -6808,20 +7056,20 @@ class DeployPath:
6808
7056
  for p in self.parts[:-1]:
6809
7057
  check.equal(p.kind, 'dir')
6810
7058
 
6811
- pd: ta.Dict[DeployPathPlaceholder, ta.List[int]] = {}
7059
+ @cached_nullary
7060
+ def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
7061
+ pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
6812
7062
  for i, np in enumerate(self.name_parts):
6813
- if isinstance(np, PlaceholderDeployPathNamePart):
6814
- pd.setdefault(ta.cast(DeployPathPlaceholder, np.placeholder), []).append(i)
6815
-
6816
- # if 'tag' in pd and 'app' not in pd:
6817
- # raise DeployPathError('Tag placeholder in path without app', self)
7063
+ if isinstance(np, TagDeployPathNamePart):
7064
+ pd.setdefault(np.tag, []).append(i)
7065
+ return pd
6818
7066
 
6819
7067
  @property
6820
7068
  def kind(self) -> ta.Literal['file', 'dir']:
6821
7069
  return self.parts[-1].kind
6822
7070
 
6823
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6824
- return ''.join([p.render(placeholders) for p in self.parts])
7071
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
7072
+ return ''.join([p.render(tags) for p in self.parts])
6825
7073
 
6826
7074
  @classmethod
6827
7075
  def parse(cls, s: str) -> 'DeployPath':
@@ -6845,6 +7093,16 @@ def check_valid_deploy_spec_path(s: str) -> str:
6845
7093
  return s
6846
7094
 
6847
7095
 
7096
+ class DeploySpecKeyed(ta.Generic[KeyDeployTagT]):
7097
+ @cached_nullary
7098
+ def _key_str(self) -> str:
7099
+ return hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8]
7100
+
7101
+ @abc.abstractmethod
7102
+ def key(self) -> KeyDeployTagT:
7103
+ raise NotImplementedError
7104
+
7105
+
6848
7106
  ##
6849
7107
 
6850
7108
 
@@ -6890,7 +7148,7 @@ class DeployVenvSpec:
6890
7148
 
6891
7149
 
6892
7150
  @dc.dataclass(frozen=True)
6893
- class DeployConfFile:
7151
+ class DeployAppConfFile:
6894
7152
  path: str
6895
7153
  body: str
6896
7154
 
@@ -6902,7 +7160,7 @@ class DeployConfFile:
6902
7160
 
6903
7161
 
6904
7162
  @dc.dataclass(frozen=True)
6905
- class DeployConfLink(abc.ABC): # noqa
7163
+ class DeployAppConfLink(abc.ABC): # noqa
6906
7164
  """
6907
7165
  May be either:
6908
7166
  - @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -6918,11 +7176,11 @@ class DeployConfLink(abc.ABC): # noqa
6918
7176
  check.equal(self.src.count('/'), 1)
6919
7177
 
6920
7178
 
6921
- class AppDeployConfLink(DeployConfLink):
7179
+ class CurrentOnlyDeployAppConfLink(DeployAppConfLink):
6922
7180
  pass
6923
7181
 
6924
7182
 
6925
- class TagDeployConfLink(DeployConfLink):
7183
+ class AllActiveDeployAppConfLink(DeployAppConfLink):
6926
7184
  pass
6927
7185
 
6928
7186
 
@@ -6930,10 +7188,10 @@ class TagDeployConfLink(DeployConfLink):
6930
7188
 
6931
7189
 
6932
7190
  @dc.dataclass(frozen=True)
6933
- class DeployConfSpec:
6934
- files: ta.Optional[ta.Sequence[DeployConfFile]] = None
7191
+ class DeployAppConfSpec:
7192
+ files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
6935
7193
 
6936
- links: ta.Optional[ta.Sequence[DeployConfLink]] = None
7194
+ links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
6937
7195
 
6938
7196
  def __post_init__(self) -> None:
6939
7197
  if self.files:
@@ -6947,18 +7205,37 @@ class DeployConfSpec:
6947
7205
 
6948
7206
 
6949
7207
  @dc.dataclass(frozen=True)
6950
- class DeploySpec:
7208
+ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
6951
7209
  app: DeployApp
6952
7210
 
6953
7211
  git: DeployGitSpec
6954
7212
 
6955
7213
  venv: ta.Optional[DeployVenvSpec] = None
6956
7214
 
6957
- conf: ta.Optional[DeployConfSpec] = None
7215
+ conf: ta.Optional[DeployAppConfSpec] = None
6958
7216
 
6959
- @cached_nullary
7217
+ # @ta.override
7218
+ def key(self) -> DeployAppKey:
7219
+ return DeployAppKey(self._key_str())
7220
+
7221
+
7222
+ ##
7223
+
7224
+
7225
+ @dc.dataclass(frozen=True)
7226
+ class DeploySpec(DeploySpecKeyed[DeployKey]):
7227
+ apps: ta.Sequence[DeployAppSpec]
7228
+
7229
+ def __post_init__(self) -> None:
7230
+ seen: ta.Set[DeployApp] = set()
7231
+ for a in self.apps:
7232
+ if a.app in seen:
7233
+ raise KeyError(a.app)
7234
+ seen.add(a.app)
7235
+
7236
+ # @ta.override
6960
7237
  def key(self) -> DeployKey:
6961
- return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
7238
+ return DeployKey(self._key_str())
6962
7239
 
6963
7240
 
6964
7241
  ########################################
@@ -7578,18 +7855,18 @@ class DeployConfManager:
7578
7855
 
7579
7856
  #
7580
7857
 
7581
- async def _write_conf_file(
7858
+ async def _write_app_conf_file(
7582
7859
  self,
7583
- cf: DeployConfFile,
7584
- conf_dir: str,
7860
+ acf: DeployAppConfFile,
7861
+ app_conf_dir: str,
7585
7862
  ) -> None:
7586
- conf_file = os.path.join(conf_dir, cf.path)
7587
- check.arg(is_path_in_dir(conf_dir, conf_file))
7863
+ conf_file = os.path.join(app_conf_dir, acf.path)
7864
+ check.arg(is_path_in_dir(app_conf_dir, conf_file))
7588
7865
 
7589
7866
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
7590
7867
 
7591
7868
  with open(conf_file, 'w') as f: # noqa
7592
- f.write(cf.body)
7869
+ f.write(acf.body)
7593
7870
 
7594
7871
  #
7595
7872
 
@@ -7598,15 +7875,18 @@ class DeployConfManager:
7598
7875
  link_src: str
7599
7876
  link_dst: str
7600
7877
 
7601
- def _compute_conf_link_dst(
7878
+ _UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
7879
+ _UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
7880
+
7881
+ def _compute_app_conf_link_dst(
7602
7882
  self,
7603
- link: DeployConfLink,
7604
- app_tag: DeployAppTag,
7605
- conf_dir: str,
7606
- link_dir: str,
7883
+ link: DeployAppConfLink,
7884
+ tags: DeployTagMap,
7885
+ app_conf_dir: str,
7886
+ conf_link_dir: str,
7607
7887
  ) -> _ComputedConfLink:
7608
- link_src = os.path.join(conf_dir, link.src)
7609
- check.arg(is_path_in_dir(conf_dir, link_src))
7888
+ link_src = os.path.join(app_conf_dir, link.src)
7889
+ check.arg(is_path_in_dir(app_conf_dir, link_src))
7610
7890
 
7611
7891
  #
7612
7892
 
@@ -7621,7 +7901,7 @@ class DeployConfManager:
7621
7901
  d, f = os.path.split(link.src)
7622
7902
  # TODO: check filename :|
7623
7903
  link_dst_pfx = d + '/'
7624
- link_dst_sfx = DEPLOY_PATH_PLACEHOLDER_SEPARATOR + f
7904
+ link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
7625
7905
 
7626
7906
  else: # noqa
7627
7907
  # @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -7635,10 +7915,10 @@ class DeployConfManager:
7635
7915
 
7636
7916
  #
7637
7917
 
7638
- if isinstance(link, AppDeployConfLink):
7639
- link_dst_mid = str(app_tag.app)
7640
- elif isinstance(link, TagDeployConfLink):
7641
- link_dst_mid = DEPLOY_PATH_PLACEHOLDER_SEPARATOR.join([app_tag.app, app_tag.tag])
7918
+ if isinstance(link, CurrentOnlyDeployAppConfLink):
7919
+ link_dst_mid = str(tags[DeployApp].s)
7920
+ elif isinstance(link, AllActiveDeployAppConfLink):
7921
+ link_dst_mid = self._UNIQUE_LINK_NAME.render(tags)
7642
7922
  else:
7643
7923
  raise TypeError(link)
7644
7924
 
@@ -7649,7 +7929,7 @@ class DeployConfManager:
7649
7929
  link_dst_mid,
7650
7930
  link_dst_sfx,
7651
7931
  ])
7652
- link_dst = os.path.join(link_dir, link_dst_name)
7932
+ link_dst = os.path.join(conf_link_dir, link_dst_name)
7653
7933
 
7654
7934
  return DeployConfManager._ComputedConfLink(
7655
7935
  is_dir=is_dir,
@@ -7657,24 +7937,24 @@ class DeployConfManager:
7657
7937
  link_dst=link_dst,
7658
7938
  )
7659
7939
 
7660
- async def _make_conf_link(
7940
+ async def _make_app_conf_link(
7661
7941
  self,
7662
- link: DeployConfLink,
7663
- app_tag: DeployAppTag,
7664
- conf_dir: str,
7665
- link_dir: str,
7942
+ link: DeployAppConfLink,
7943
+ tags: DeployTagMap,
7944
+ app_conf_dir: str,
7945
+ conf_link_dir: str,
7666
7946
  ) -> None:
7667
- comp = self._compute_conf_link_dst(
7947
+ comp = self._compute_app_conf_link_dst(
7668
7948
  link,
7669
- app_tag,
7670
- conf_dir,
7671
- link_dir,
7949
+ tags,
7950
+ app_conf_dir,
7951
+ conf_link_dir,
7672
7952
  )
7673
7953
 
7674
7954
  #
7675
7955
 
7676
- check.arg(is_path_in_dir(conf_dir, comp.link_src))
7677
- check.arg(is_path_in_dir(link_dir, comp.link_dst))
7956
+ check.arg(is_path_in_dir(app_conf_dir, comp.link_src))
7957
+ check.arg(is_path_in_dir(conf_link_dir, comp.link_dst))
7678
7958
 
7679
7959
  if comp.is_dir:
7680
7960
  check.arg(os.path.isdir(comp.link_src))
@@ -7692,27 +7972,27 @@ class DeployConfManager:
7692
7972
 
7693
7973
  #
7694
7974
 
7695
- async def write_conf(
7975
+ async def write_app_conf(
7696
7976
  self,
7697
- spec: DeployConfSpec,
7698
- app_tag: DeployAppTag,
7699
- conf_dir: str,
7700
- link_dir: str,
7977
+ spec: DeployAppConfSpec,
7978
+ tags: DeployTagMap,
7979
+ app_conf_dir: str,
7980
+ conf_link_dir: str,
7701
7981
  ) -> None:
7702
- for cf in spec.files or []:
7703
- await self._write_conf_file(
7704
- cf,
7705
- conf_dir,
7982
+ for acf in spec.files or []:
7983
+ await self._write_app_conf_file(
7984
+ acf,
7985
+ app_conf_dir,
7706
7986
  )
7707
7987
 
7708
7988
  #
7709
7989
 
7710
7990
  for link in spec.links or []:
7711
- await self._make_conf_link(
7991
+ await self._make_app_conf_link(
7712
7992
  link,
7713
- app_tag,
7714
- conf_dir,
7715
- link_dir,
7993
+ tags,
7994
+ app_conf_dir,
7995
+ conf_link_dir,
7716
7996
  )
7717
7997
 
7718
7998
 
@@ -9315,19 +9595,6 @@ def bind_commands(
9315
9595
  # ../deploy/apps.py
9316
9596
 
9317
9597
 
9318
- def make_deploy_tag(
9319
- rev: DeployRev,
9320
- key: DeployKey,
9321
- *,
9322
- utcnow: ta.Optional[datetime.datetime] = None,
9323
- ) -> DeployTag:
9324
- if utcnow is None:
9325
- utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
9326
- now_fmt = '%Y%m%dT%H%M%SZ'
9327
- now_str = utcnow.strftime(now_fmt)
9328
- return DeployTag('-'.join([now_str, rev, key]))
9329
-
9330
-
9331
9598
  class DeployAppManager(DeployPathOwner):
9332
9599
  def __init__(
9333
9600
  self,
@@ -9348,32 +9615,27 @@ class DeployAppManager(DeployPathOwner):
9348
9615
 
9349
9616
  #
9350
9617
 
9351
- _APP_TAG_DIR_STR = 'tags/apps/@app/@tag/'
9352
- _APP_TAG_DIR = DeployPath.parse(_APP_TAG_DIR_STR)
9353
-
9354
- _CONF_TAG_DIR_STR = 'tags/conf/@tag--@app/'
9355
- _CONF_TAG_DIR = DeployPath.parse(_CONF_TAG_DIR_STR)
9618
+ _APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
9619
+ _APP_DIR = DeployPath.parse(_APP_DIR_STR)
9356
9620
 
9357
- _DEPLOY_DIR_STR = 'deploys/@tag--@app/'
9621
+ _DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
9358
9622
  _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
9359
9623
 
9360
9624
  _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
9361
- _CONF_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf')
9625
+ _CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
9362
9626
 
9363
9627
  @cached_nullary
9364
9628
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
9365
9629
  return {
9366
- self._APP_TAG_DIR,
9367
-
9368
- self._CONF_TAG_DIR,
9630
+ self._APP_DIR,
9369
9631
 
9370
9632
  self._DEPLOY_DIR,
9371
9633
 
9372
9634
  self._APP_DEPLOY_LINK,
9373
- self._CONF_DEPLOY_LINK,
9635
+ self._CONF_DEPLOY_DIR,
9374
9636
 
9375
9637
  *[
9376
- DeployPath.parse(f'{self._APP_TAG_DIR_STR}{sfx}/')
9638
+ DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
9377
9639
  for sfx in [
9378
9640
  'conf',
9379
9641
  'git',
@@ -9386,26 +9648,21 @@ class DeployAppManager(DeployPathOwner):
9386
9648
 
9387
9649
  async def prepare_app(
9388
9650
  self,
9389
- spec: DeploySpec,
9651
+ spec: DeployAppSpec,
9652
+ tags: DeployTagMap,
9390
9653
  ) -> None:
9391
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.git.rev, spec.key()))
9392
-
9393
- #
9394
-
9395
9654
  deploy_home = check.non_empty_str(self._deploy_home)
9396
9655
 
9397
9656
  def build_path(pth: DeployPath) -> str:
9398
- return os.path.join(deploy_home, pth.render(app_tag.placeholders()))
9657
+ return os.path.join(deploy_home, pth.render(tags))
9399
9658
 
9400
- app_tag_dir = build_path(self._APP_TAG_DIR)
9401
- conf_tag_dir = build_path(self._CONF_TAG_DIR)
9659
+ app_dir = build_path(self._APP_DIR)
9402
9660
  deploy_dir = build_path(self._DEPLOY_DIR)
9403
9661
  app_deploy_link = build_path(self._APP_DEPLOY_LINK)
9404
- conf_deploy_link_file = build_path(self._CONF_DEPLOY_LINK)
9405
9662
 
9406
9663
  #
9407
9664
 
9408
- os.makedirs(deploy_dir)
9665
+ os.makedirs(deploy_dir, exist_ok=True)
9409
9666
 
9410
9667
  deploying_link = os.path.join(deploy_home, 'deploys/deploying')
9411
9668
  relative_symlink(
@@ -9417,9 +9674,9 @@ class DeployAppManager(DeployPathOwner):
9417
9674
 
9418
9675
  #
9419
9676
 
9420
- os.makedirs(app_tag_dir)
9677
+ os.makedirs(app_dir)
9421
9678
  relative_symlink(
9422
- app_tag_dir,
9679
+ app_dir,
9423
9680
  app_deploy_link,
9424
9681
  target_is_directory=True,
9425
9682
  make_dirs=True,
@@ -9427,37 +9684,33 @@ class DeployAppManager(DeployPathOwner):
9427
9684
 
9428
9685
  #
9429
9686
 
9430
- os.makedirs(conf_tag_dir)
9431
- relative_symlink(
9432
- conf_tag_dir,
9433
- conf_deploy_link_file,
9434
- target_is_directory=True,
9435
- make_dirs=True,
9436
- )
9687
+ deploy_conf_dir = os.path.join(deploy_dir, 'conf')
9688
+ os.makedirs(deploy_conf_dir, exist_ok=True)
9437
9689
 
9438
9690
  #
9439
9691
 
9440
- def mirror_symlinks(src: str, dst: str) -> None:
9441
- def mirror_link(lp: str) -> None:
9442
- check.state(os.path.islink(lp))
9443
- shutil.copy2(
9444
- lp,
9445
- os.path.join(dst, os.path.relpath(lp, src)),
9446
- follow_symlinks=False,
9447
- )
9448
-
9449
- for dp, dns, fns in os.walk(src, followlinks=False):
9450
- for fn in fns:
9451
- mirror_link(os.path.join(dp, fn))
9452
-
9453
- for dn in dns:
9454
- dp2 = os.path.join(dp, dn)
9455
- if os.path.islink(dp2):
9456
- mirror_link(dp2)
9457
- else:
9458
- os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
9692
+ # def mirror_symlinks(src: str, dst: str) -> None:
9693
+ # def mirror_link(lp: str) -> None:
9694
+ # check.state(os.path.islink(lp))
9695
+ # shutil.copy2(
9696
+ # lp,
9697
+ # os.path.join(dst, os.path.relpath(lp, src)),
9698
+ # follow_symlinks=False,
9699
+ # )
9700
+ #
9701
+ # for dp, dns, fns in os.walk(src, followlinks=False):
9702
+ # for fn in fns:
9703
+ # mirror_link(os.path.join(dp, fn))
9704
+ #
9705
+ # for dn in dns:
9706
+ # dp2 = os.path.join(dp, dn)
9707
+ # if os.path.islink(dp2):
9708
+ # mirror_link(dp2)
9709
+ # else:
9710
+ # os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
9459
9711
 
9460
9712
  current_link = os.path.join(deploy_home, 'deploys/current')
9713
+
9461
9714
  # if os.path.exists(current_link):
9462
9715
  # mirror_symlinks(
9463
9716
  # os.path.join(current_link, 'conf'),
@@ -9470,31 +9723,31 @@ class DeployAppManager(DeployPathOwner):
9470
9723
 
9471
9724
  #
9472
9725
 
9473
- git_dir = os.path.join(app_tag_dir, 'git')
9726
+ app_git_dir = os.path.join(app_dir, 'git')
9474
9727
  await self._git.checkout(
9475
9728
  spec.git,
9476
- git_dir,
9729
+ app_git_dir,
9477
9730
  )
9478
9731
 
9479
9732
  #
9480
9733
 
9481
9734
  if spec.venv is not None:
9482
- venv_dir = os.path.join(app_tag_dir, 'venv')
9735
+ app_venv_dir = os.path.join(app_dir, 'venv')
9483
9736
  await self._venvs.setup_venv(
9484
9737
  spec.venv,
9485
- git_dir,
9486
- venv_dir,
9738
+ app_git_dir,
9739
+ app_venv_dir,
9487
9740
  )
9488
9741
 
9489
9742
  #
9490
9743
 
9491
9744
  if spec.conf is not None:
9492
- conf_dir = os.path.join(app_tag_dir, 'conf')
9493
- await self._conf.write_conf(
9745
+ app_conf_dir = os.path.join(app_dir, 'conf')
9746
+ await self._conf.write_app_conf(
9494
9747
  spec.conf,
9495
- app_tag,
9496
- conf_dir,
9497
- conf_tag_dir,
9748
+ tags,
9749
+ app_conf_dir,
9750
+ deploy_conf_dir,
9498
9751
  )
9499
9752
 
9500
9753
  #
@@ -10235,18 +10488,37 @@ class SystemInterpProvider(InterpProvider):
10235
10488
  # ../deploy/deploy.py
10236
10489
 
10237
10490
 
10491
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
10492
+
10493
+
10494
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
10495
+
10496
+
10238
10497
  class DeployManager:
10239
10498
  def __init__(
10240
10499
  self,
10241
10500
  *,
10242
10501
  apps: DeployAppManager,
10243
10502
  paths: DeployPathsManager,
10503
+
10504
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
10244
10505
  ):
10245
10506
  super().__init__()
10246
10507
 
10247
10508
  self._apps = apps
10248
10509
  self._paths = paths
10249
10510
 
10511
+ self._utc_clock = utc_clock
10512
+
10513
+ def _utc_now(self) -> datetime.datetime:
10514
+ if self._utc_clock is not None:
10515
+ return self._utc_clock() # noqa
10516
+ else:
10517
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
10518
+
10519
+ def _make_deploy_time(self) -> DeployTime:
10520
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
10521
+
10250
10522
  async def run_deploy(
10251
10523
  self,
10252
10524
  spec: DeploySpec,
@@ -10255,7 +10527,24 @@ class DeployManager:
10255
10527
 
10256
10528
  #
10257
10529
 
10258
- await self._apps.prepare_app(spec)
10530
+ deploy_tags = DeployTagMap(
10531
+ self._make_deploy_time(),
10532
+ spec.key(),
10533
+ )
10534
+
10535
+ #
10536
+
10537
+ for app in spec.apps:
10538
+ app_tags = deploy_tags.add(
10539
+ app.app,
10540
+ app.key(),
10541
+ DeployAppRev(app.git.rev),
10542
+ )
10543
+
10544
+ await self._apps.prepare_app(
10545
+ app,
10546
+ app_tags,
10547
+ )
10259
10548
 
10260
10549
 
10261
10550
  ########################################