ominfra 0.0.0.dev172__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
@@ -6634,28 +6896,13 @@ TODO:
6634
6896
  ##
6635
6897
 
6636
6898
 
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
6899
  class DeployPathError(Exception):
6653
6900
  pass
6654
6901
 
6655
6902
 
6656
6903
  class DeployPathRenderable(abc.ABC):
6657
6904
  @abc.abstractmethod
6658
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6905
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6659
6906
  raise NotImplementedError
6660
6907
 
6661
6908
 
@@ -6666,26 +6913,30 @@ class DeployPathNamePart(DeployPathRenderable, abc.ABC):
6666
6913
  @classmethod
6667
6914
  def parse(cls, s: str) -> 'DeployPathNamePart':
6668
6915
  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:
6916
+ if s.startswith(DEPLOY_TAG_SIGIL):
6917
+ return TagDeployPathNamePart(s[1:])
6918
+ elif s in DEPLOY_TAG_DELIMITERS:
6672
6919
  return DelimiterDeployPathNamePart(s)
6673
6920
  else:
6674
6921
  return ConstDeployPathNamePart(s)
6675
6922
 
6676
6923
 
6677
6924
  @dc.dataclass(frozen=True)
6678
- class PlaceholderDeployPathNamePart(DeployPathNamePart):
6679
- placeholder: str # DeployPathPlaceholder
6925
+ class TagDeployPathNamePart(DeployPathNamePart):
6926
+ name: str
6680
6927
 
6681
6928
  def __post_init__(self) -> None:
6682
- check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
6929
+ check.in_(self.name, DEPLOY_TAGS_BY_NAME)
6683
6930
 
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
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
6687
6938
  else:
6688
- return DEPLOY_PATH_PLACEHOLDER_SIGIL + self.placeholder
6939
+ return DEPLOY_TAG_SIGIL + self.name
6689
6940
 
6690
6941
 
6691
6942
  @dc.dataclass(frozen=True)
@@ -6693,9 +6944,9 @@ class DelimiterDeployPathNamePart(DeployPathNamePart):
6693
6944
  delimiter: str
6694
6945
 
6695
6946
  def __post_init__(self) -> None:
6696
- check.in_(self.delimiter, DEPLOY_PATH_PLACEHOLDER_DELIMITERS)
6947
+ check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
6697
6948
 
6698
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6949
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6699
6950
  return self.delimiter
6700
6951
 
6701
6952
 
@@ -6705,10 +6956,10 @@ class ConstDeployPathNamePart(DeployPathNamePart):
6705
6956
 
6706
6957
  def __post_init__(self) -> None:
6707
6958
  check.non_empty_str(self.const)
6708
- for c in [*DEPLOY_PATH_PLACEHOLDER_DELIMITERS, DEPLOY_PATH_PLACEHOLDER_SIGIL, '/']:
6959
+ for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
6709
6960
  check.not_in(c, self.const)
6710
6961
 
6711
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6962
+ def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
6712
6963
  return self.const
6713
6964
 
6714
6965
 
@@ -6723,8 +6974,8 @@ class DeployPathName(DeployPathRenderable):
6723
6974
  if len(gl := list(g)) > 1:
6724
6975
  raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
6725
6976
 
6726
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6727
- 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)
6728
6979
 
6729
6980
  @classmethod
6730
6981
  def parse(cls, s: str) -> 'DeployPathName':
@@ -6734,7 +6985,7 @@ class DeployPathName(DeployPathRenderable):
6734
6985
  i = 0
6735
6986
  ps = []
6736
6987
  while i < len(s):
6737
- 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]
6738
6989
  if not ns:
6739
6990
  ps.append(s[i:])
6740
6991
  break
@@ -6758,8 +7009,8 @@ class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
6758
7009
  def kind(self) -> DeployPathKind:
6759
7010
  raise NotImplementedError
6760
7011
 
6761
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6762
- 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 '')
6763
7014
 
6764
7015
  @classmethod
6765
7016
  def parse(cls, s: str) -> 'DeployPathPart':
@@ -6805,20 +7056,20 @@ class DeployPath:
6805
7056
  for p in self.parts[:-1]:
6806
7057
  check.equal(p.kind, 'dir')
6807
7058
 
6808
- 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]] = {}
6809
7062
  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)
7063
+ if isinstance(np, TagDeployPathNamePart):
7064
+ pd.setdefault(np.tag, []).append(i)
7065
+ return pd
6815
7066
 
6816
7067
  @property
6817
7068
  def kind(self) -> ta.Literal['file', 'dir']:
6818
7069
  return self.parts[-1].kind
6819
7070
 
6820
- def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
6821
- 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])
6822
7073
 
6823
7074
  @classmethod
6824
7075
  def parse(cls, s: str) -> 'DeployPath':
@@ -6842,6 +7093,16 @@ def check_valid_deploy_spec_path(s: str) -> str:
6842
7093
  return s
6843
7094
 
6844
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
+
6845
7106
  ##
6846
7107
 
6847
7108
 
@@ -6887,7 +7148,7 @@ class DeployVenvSpec:
6887
7148
 
6888
7149
 
6889
7150
  @dc.dataclass(frozen=True)
6890
- class DeployConfFile:
7151
+ class DeployAppConfFile:
6891
7152
  path: str
6892
7153
  body: str
6893
7154
 
@@ -6899,7 +7160,7 @@ class DeployConfFile:
6899
7160
 
6900
7161
 
6901
7162
  @dc.dataclass(frozen=True)
6902
- class DeployConfLink(abc.ABC): # noqa
7163
+ class DeployAppConfLink(abc.ABC): # noqa
6903
7164
  """
6904
7165
  May be either:
6905
7166
  - @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -6915,11 +7176,11 @@ class DeployConfLink(abc.ABC): # noqa
6915
7176
  check.equal(self.src.count('/'), 1)
6916
7177
 
6917
7178
 
6918
- class AppDeployConfLink(DeployConfLink):
7179
+ class CurrentOnlyDeployAppConfLink(DeployAppConfLink):
6919
7180
  pass
6920
7181
 
6921
7182
 
6922
- class TagDeployConfLink(DeployConfLink):
7183
+ class AllActiveDeployAppConfLink(DeployAppConfLink):
6923
7184
  pass
6924
7185
 
6925
7186
 
@@ -6927,10 +7188,10 @@ class TagDeployConfLink(DeployConfLink):
6927
7188
 
6928
7189
 
6929
7190
  @dc.dataclass(frozen=True)
6930
- class DeployConfSpec:
6931
- files: ta.Optional[ta.Sequence[DeployConfFile]] = None
7191
+ class DeployAppConfSpec:
7192
+ files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
6932
7193
 
6933
- links: ta.Optional[ta.Sequence[DeployConfLink]] = None
7194
+ links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
6934
7195
 
6935
7196
  def __post_init__(self) -> None:
6936
7197
  if self.files:
@@ -6944,18 +7205,37 @@ class DeployConfSpec:
6944
7205
 
6945
7206
 
6946
7207
  @dc.dataclass(frozen=True)
6947
- class DeploySpec:
7208
+ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
6948
7209
  app: DeployApp
6949
7210
 
6950
7211
  git: DeployGitSpec
6951
7212
 
6952
7213
  venv: ta.Optional[DeployVenvSpec] = None
6953
7214
 
6954
- conf: ta.Optional[DeployConfSpec] = None
7215
+ conf: ta.Optional[DeployAppConfSpec] = None
6955
7216
 
6956
- @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
6957
7237
  def key(self) -> DeployKey:
6958
- return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
7238
+ return DeployKey(self._key_str())
6959
7239
 
6960
7240
 
6961
7241
  ########################################
@@ -7575,18 +7855,18 @@ class DeployConfManager:
7575
7855
 
7576
7856
  #
7577
7857
 
7578
- async def _write_conf_file(
7858
+ async def _write_app_conf_file(
7579
7859
  self,
7580
- cf: DeployConfFile,
7581
- conf_dir: str,
7860
+ acf: DeployAppConfFile,
7861
+ app_conf_dir: str,
7582
7862
  ) -> None:
7583
- conf_file = os.path.join(conf_dir, cf.path)
7584
- 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))
7585
7865
 
7586
7866
  os.makedirs(os.path.dirname(conf_file), exist_ok=True)
7587
7867
 
7588
7868
  with open(conf_file, 'w') as f: # noqa
7589
- f.write(cf.body)
7869
+ f.write(acf.body)
7590
7870
 
7591
7871
  #
7592
7872
 
@@ -7595,15 +7875,18 @@ class DeployConfManager:
7595
7875
  link_src: str
7596
7876
  link_dst: str
7597
7877
 
7598
- 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(
7599
7882
  self,
7600
- link: DeployConfLink,
7601
- app_tag: DeployAppTag,
7602
- conf_dir: str,
7603
- link_dir: str,
7883
+ link: DeployAppConfLink,
7884
+ tags: DeployTagMap,
7885
+ app_conf_dir: str,
7886
+ conf_link_dir: str,
7604
7887
  ) -> _ComputedConfLink:
7605
- link_src = os.path.join(conf_dir, link.src)
7606
- 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))
7607
7890
 
7608
7891
  #
7609
7892
 
@@ -7618,7 +7901,7 @@ class DeployConfManager:
7618
7901
  d, f = os.path.split(link.src)
7619
7902
  # TODO: check filename :|
7620
7903
  link_dst_pfx = d + '/'
7621
- link_dst_sfx = DEPLOY_PATH_PLACEHOLDER_SEPARATOR + f
7904
+ link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
7622
7905
 
7623
7906
  else: # noqa
7624
7907
  # @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
@@ -7632,10 +7915,10 @@ class DeployConfManager:
7632
7915
 
7633
7916
  #
7634
7917
 
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])
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)
7639
7922
  else:
7640
7923
  raise TypeError(link)
7641
7924
 
@@ -7646,7 +7929,7 @@ class DeployConfManager:
7646
7929
  link_dst_mid,
7647
7930
  link_dst_sfx,
7648
7931
  ])
7649
- link_dst = os.path.join(link_dir, link_dst_name)
7932
+ link_dst = os.path.join(conf_link_dir, link_dst_name)
7650
7933
 
7651
7934
  return DeployConfManager._ComputedConfLink(
7652
7935
  is_dir=is_dir,
@@ -7654,24 +7937,24 @@ class DeployConfManager:
7654
7937
  link_dst=link_dst,
7655
7938
  )
7656
7939
 
7657
- async def _make_conf_link(
7940
+ async def _make_app_conf_link(
7658
7941
  self,
7659
- link: DeployConfLink,
7660
- app_tag: DeployAppTag,
7661
- conf_dir: str,
7662
- link_dir: str,
7942
+ link: DeployAppConfLink,
7943
+ tags: DeployTagMap,
7944
+ app_conf_dir: str,
7945
+ conf_link_dir: str,
7663
7946
  ) -> None:
7664
- comp = self._compute_conf_link_dst(
7947
+ comp = self._compute_app_conf_link_dst(
7665
7948
  link,
7666
- app_tag,
7667
- conf_dir,
7668
- link_dir,
7949
+ tags,
7950
+ app_conf_dir,
7951
+ conf_link_dir,
7669
7952
  )
7670
7953
 
7671
7954
  #
7672
7955
 
7673
- check.arg(is_path_in_dir(conf_dir, comp.link_src))
7674
- 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))
7675
7958
 
7676
7959
  if comp.is_dir:
7677
7960
  check.arg(os.path.isdir(comp.link_src))
@@ -7689,27 +7972,27 @@ class DeployConfManager:
7689
7972
 
7690
7973
  #
7691
7974
 
7692
- async def write_conf(
7975
+ async def write_app_conf(
7693
7976
  self,
7694
- spec: DeployConfSpec,
7695
- app_tag: DeployAppTag,
7696
- conf_dir: str,
7697
- link_dir: str,
7977
+ spec: DeployAppConfSpec,
7978
+ tags: DeployTagMap,
7979
+ app_conf_dir: str,
7980
+ conf_link_dir: str,
7698
7981
  ) -> None:
7699
- for cf in spec.files or []:
7700
- await self._write_conf_file(
7701
- cf,
7702
- conf_dir,
7982
+ for acf in spec.files or []:
7983
+ await self._write_app_conf_file(
7984
+ acf,
7985
+ app_conf_dir,
7703
7986
  )
7704
7987
 
7705
7988
  #
7706
7989
 
7707
7990
  for link in spec.links or []:
7708
- await self._make_conf_link(
7991
+ await self._make_app_conf_link(
7709
7992
  link,
7710
- app_tag,
7711
- conf_dir,
7712
- link_dir,
7993
+ tags,
7994
+ app_conf_dir,
7995
+ conf_link_dir,
7713
7996
  )
7714
7997
 
7715
7998
 
@@ -9312,19 +9595,6 @@ def bind_commands(
9312
9595
  # ../deploy/apps.py
9313
9596
 
9314
9597
 
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
9598
  class DeployAppManager(DeployPathOwner):
9329
9599
  def __init__(
9330
9600
  self,
@@ -9345,32 +9615,27 @@ class DeployAppManager(DeployPathOwner):
9345
9615
 
9346
9616
  #
9347
9617
 
9348
- _APP_TAG_DIR_STR = 'tags/apps/@app/@tag/'
9349
- _APP_TAG_DIR = DeployPath.parse(_APP_TAG_DIR_STR)
9350
-
9351
- _CONF_TAG_DIR_STR = 'tags/conf/@tag--@app/'
9352
- _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)
9353
9620
 
9354
- _DEPLOY_DIR_STR = 'deploys/@tag--@app/'
9621
+ _DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
9355
9622
  _DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
9356
9623
 
9357
9624
  _APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
9358
- _CONF_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf')
9625
+ _CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
9359
9626
 
9360
9627
  @cached_nullary
9361
9628
  def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
9362
9629
  return {
9363
- self._APP_TAG_DIR,
9364
-
9365
- self._CONF_TAG_DIR,
9630
+ self._APP_DIR,
9366
9631
 
9367
9632
  self._DEPLOY_DIR,
9368
9633
 
9369
9634
  self._APP_DEPLOY_LINK,
9370
- self._CONF_DEPLOY_LINK,
9635
+ self._CONF_DEPLOY_DIR,
9371
9636
 
9372
9637
  *[
9373
- DeployPath.parse(f'{self._APP_TAG_DIR_STR}{sfx}/')
9638
+ DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
9374
9639
  for sfx in [
9375
9640
  'conf',
9376
9641
  'git',
@@ -9383,26 +9648,21 @@ class DeployAppManager(DeployPathOwner):
9383
9648
 
9384
9649
  async def prepare_app(
9385
9650
  self,
9386
- spec: DeploySpec,
9651
+ spec: DeployAppSpec,
9652
+ tags: DeployTagMap,
9387
9653
  ) -> None:
9388
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.git.rev, spec.key()))
9389
-
9390
- #
9391
-
9392
9654
  deploy_home = check.non_empty_str(self._deploy_home)
9393
9655
 
9394
9656
  def build_path(pth: DeployPath) -> str:
9395
- return os.path.join(deploy_home, pth.render(app_tag.placeholders()))
9657
+ return os.path.join(deploy_home, pth.render(tags))
9396
9658
 
9397
- app_tag_dir = build_path(self._APP_TAG_DIR)
9398
- conf_tag_dir = build_path(self._CONF_TAG_DIR)
9659
+ app_dir = build_path(self._APP_DIR)
9399
9660
  deploy_dir = build_path(self._DEPLOY_DIR)
9400
9661
  app_deploy_link = build_path(self._APP_DEPLOY_LINK)
9401
- conf_deploy_link_file = build_path(self._CONF_DEPLOY_LINK)
9402
9662
 
9403
9663
  #
9404
9664
 
9405
- os.makedirs(deploy_dir)
9665
+ os.makedirs(deploy_dir, exist_ok=True)
9406
9666
 
9407
9667
  deploying_link = os.path.join(deploy_home, 'deploys/deploying')
9408
9668
  relative_symlink(
@@ -9414,9 +9674,9 @@ class DeployAppManager(DeployPathOwner):
9414
9674
 
9415
9675
  #
9416
9676
 
9417
- os.makedirs(app_tag_dir)
9677
+ os.makedirs(app_dir)
9418
9678
  relative_symlink(
9419
- app_tag_dir,
9679
+ app_dir,
9420
9680
  app_deploy_link,
9421
9681
  target_is_directory=True,
9422
9682
  make_dirs=True,
@@ -9424,37 +9684,33 @@ class DeployAppManager(DeployPathOwner):
9424
9684
 
9425
9685
  #
9426
9686
 
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
- )
9687
+ deploy_conf_dir = os.path.join(deploy_dir, 'conf')
9688
+ os.makedirs(deploy_conf_dir, exist_ok=True)
9434
9689
 
9435
9690
  #
9436
9691
 
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)))
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)))
9456
9711
 
9457
9712
  current_link = os.path.join(deploy_home, 'deploys/current')
9713
+
9458
9714
  # if os.path.exists(current_link):
9459
9715
  # mirror_symlinks(
9460
9716
  # os.path.join(current_link, 'conf'),
@@ -9467,31 +9723,31 @@ class DeployAppManager(DeployPathOwner):
9467
9723
 
9468
9724
  #
9469
9725
 
9470
- git_dir = os.path.join(app_tag_dir, 'git')
9726
+ app_git_dir = os.path.join(app_dir, 'git')
9471
9727
  await self._git.checkout(
9472
9728
  spec.git,
9473
- git_dir,
9729
+ app_git_dir,
9474
9730
  )
9475
9731
 
9476
9732
  #
9477
9733
 
9478
9734
  if spec.venv is not None:
9479
- venv_dir = os.path.join(app_tag_dir, 'venv')
9735
+ app_venv_dir = os.path.join(app_dir, 'venv')
9480
9736
  await self._venvs.setup_venv(
9481
9737
  spec.venv,
9482
- git_dir,
9483
- venv_dir,
9738
+ app_git_dir,
9739
+ app_venv_dir,
9484
9740
  )
9485
9741
 
9486
9742
  #
9487
9743
 
9488
9744
  if spec.conf is not None:
9489
- conf_dir = os.path.join(app_tag_dir, 'conf')
9490
- await self._conf.write_conf(
9745
+ app_conf_dir = os.path.join(app_dir, 'conf')
9746
+ await self._conf.write_app_conf(
9491
9747
  spec.conf,
9492
- app_tag,
9493
- conf_dir,
9494
- conf_tag_dir,
9748
+ tags,
9749
+ app_conf_dir,
9750
+ deploy_conf_dir,
9495
9751
  )
9496
9752
 
9497
9753
  #
@@ -10232,18 +10488,37 @@ class SystemInterpProvider(InterpProvider):
10232
10488
  # ../deploy/deploy.py
10233
10489
 
10234
10490
 
10491
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
10492
+
10493
+
10494
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
10495
+
10496
+
10235
10497
  class DeployManager:
10236
10498
  def __init__(
10237
10499
  self,
10238
10500
  *,
10239
10501
  apps: DeployAppManager,
10240
10502
  paths: DeployPathsManager,
10503
+
10504
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
10241
10505
  ):
10242
10506
  super().__init__()
10243
10507
 
10244
10508
  self._apps = apps
10245
10509
  self._paths = paths
10246
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
+
10247
10522
  async def run_deploy(
10248
10523
  self,
10249
10524
  spec: DeploySpec,
@@ -10252,7 +10527,24 @@ class DeployManager:
10252
10527
 
10253
10528
  #
10254
10529
 
10255
- 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
+ )
10256
10548
 
10257
10549
 
10258
10550
  ########################################