ominfra 0.0.0.dev171__py3-none-any.whl → 0.0.0.dev173__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
  """
@@ -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
  ########################################