ominfra 0.0.0.dev172__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
@@ -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
  ########################################