ominfra 0.0.0.dev175__py3-none-any.whl → 0.0.0.dev177__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/configs.py +4 -2
- ominfra/manage/commands/marshal.py +1 -1
- ominfra/manage/deploy/apps.py +9 -8
- ominfra/manage/deploy/conf.py +0 -12
- ominfra/manage/deploy/config.py +1 -2
- ominfra/manage/deploy/deploy.py +13 -0
- ominfra/manage/deploy/git.py +24 -9
- ominfra/manage/deploy/inject.py +7 -8
- ominfra/manage/deploy/paths/manager.py +0 -3
- ominfra/manage/deploy/paths/owners.py +4 -10
- ominfra/manage/deploy/specs.py +5 -0
- ominfra/manage/deploy/tags.py +4 -0
- ominfra/manage/deploy/tmp.py +9 -28
- ominfra/manage/deploy/venvs.py +13 -11
- ominfra/manage/main.py +10 -7
- ominfra/scripts/journald2aws.py +40 -5
- ominfra/scripts/manage.py +1418 -1381
- ominfra/scripts/supervisor.py +40 -5
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev177.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev177.dist-info}/RECORD +24 -24
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev177.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev177.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev177.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev175.dist-info → ominfra-0.0.0.dev177.dist-info}/top_level.txt +0 -0
ominfra/scripts/manage.py
CHANGED
@@ -129,12 +129,12 @@ AtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
|
|
129
129
|
# ../configs.py
|
130
130
|
ConfigMapping = ta.Mapping[str, ta.Any]
|
131
131
|
|
132
|
-
# deploy/specs.py
|
133
|
-
KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
|
134
|
-
|
135
132
|
# ../../omlish/subprocesses.py
|
136
133
|
SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
|
137
134
|
|
135
|
+
# deploy/specs.py
|
136
|
+
KeyDeployTagT = ta.TypeVar('KeyDeployTagT', bound='KeyDeployTag')
|
137
|
+
|
138
138
|
# system/packages.py
|
139
139
|
SystemPackageOrStr = ta.Union['SystemPackage', str]
|
140
140
|
|
@@ -1384,7 +1384,7 @@ class MainConfig:
|
|
1384
1384
|
|
1385
1385
|
@dc.dataclass(frozen=True)
|
1386
1386
|
class DeployConfig:
|
1387
|
-
|
1387
|
+
pass
|
1388
1388
|
|
1389
1389
|
|
1390
1390
|
########################################
|
@@ -4251,229 +4251,6 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
|
|
4251
4251
|
return CommandNameMap(dct)
|
4252
4252
|
|
4253
4253
|
|
4254
|
-
########################################
|
4255
|
-
# ../deploy/tags.py
|
4256
|
-
|
4257
|
-
|
4258
|
-
##
|
4259
|
-
|
4260
|
-
|
4261
|
-
DEPLOY_TAG_SIGIL = '@'
|
4262
|
-
|
4263
|
-
DEPLOY_TAG_SEPARATOR = '--'
|
4264
|
-
|
4265
|
-
DEPLOY_TAG_DELIMITERS: ta.AbstractSet[str] = frozenset([
|
4266
|
-
DEPLOY_TAG_SEPARATOR,
|
4267
|
-
'.',
|
4268
|
-
])
|
4269
|
-
|
4270
|
-
DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
|
4271
|
-
DEPLOY_TAG_SIGIL,
|
4272
|
-
*DEPLOY_TAG_DELIMITERS,
|
4273
|
-
'/',
|
4274
|
-
])
|
4275
|
-
|
4276
|
-
|
4277
|
-
##
|
4278
|
-
|
4279
|
-
|
4280
|
-
@dc.dataclass(frozen=True)
|
4281
|
-
class DeployTag(abc.ABC): # noqa
|
4282
|
-
s: str
|
4283
|
-
|
4284
|
-
def __post_init__(self) -> None:
|
4285
|
-
check.not_in(abc.ABC, type(self).__bases__)
|
4286
|
-
check.non_empty_str(self.s)
|
4287
|
-
for ch in DEPLOY_TAG_ILLEGAL_STRS:
|
4288
|
-
check.state(ch not in self.s)
|
4289
|
-
|
4290
|
-
#
|
4291
|
-
|
4292
|
-
tag_name: ta.ClassVar[str]
|
4293
|
-
tag_kwarg: ta.ClassVar[str]
|
4294
|
-
|
4295
|
-
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
4296
|
-
super().__init_subclass__(**kwargs)
|
4297
|
-
|
4298
|
-
if abc.ABC in cls.__bases__:
|
4299
|
-
return
|
4300
|
-
|
4301
|
-
for b in cls.__bases__:
|
4302
|
-
if issubclass(b, DeployTag):
|
4303
|
-
check.in_(abc.ABC, b.__bases__)
|
4304
|
-
|
4305
|
-
check.non_empty_str(tn := cls.tag_name)
|
4306
|
-
check.equal(tn, tn.lower().strip())
|
4307
|
-
check.not_in('_', tn)
|
4308
|
-
|
4309
|
-
check.state(not hasattr(cls, 'tag_kwarg'))
|
4310
|
-
cls.tag_kwarg = tn.replace('-', '_')
|
4311
|
-
|
4312
|
-
|
4313
|
-
##
|
4314
|
-
|
4315
|
-
|
4316
|
-
_DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
|
4317
|
-
DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
|
4318
|
-
|
4319
|
-
_DEPLOY_TAGS_BY_NAME: ta.Dict[str, ta.Type[DeployTag]] = {}
|
4320
|
-
DEPLOY_TAGS_BY_NAME: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_NAME
|
4321
|
-
|
4322
|
-
_DEPLOY_TAGS_BY_KWARG: ta.Dict[str, ta.Type[DeployTag]] = {}
|
4323
|
-
DEPLOY_TAGS_BY_KWARG: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_KWARG
|
4324
|
-
|
4325
|
-
|
4326
|
-
def _register_deploy_tag(cls):
|
4327
|
-
check.not_in(cls.tag_name, _DEPLOY_TAGS_BY_NAME)
|
4328
|
-
check.not_in(cls.tag_kwarg, _DEPLOY_TAGS_BY_KWARG)
|
4329
|
-
|
4330
|
-
_DEPLOY_TAGS.add(cls)
|
4331
|
-
_DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
|
4332
|
-
_DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
|
4333
|
-
|
4334
|
-
return cls
|
4335
|
-
|
4336
|
-
|
4337
|
-
##
|
4338
|
-
|
4339
|
-
|
4340
|
-
@_register_deploy_tag
|
4341
|
-
class DeployTime(DeployTag):
|
4342
|
-
tag_name: ta.ClassVar[str] = 'time'
|
4343
|
-
|
4344
|
-
|
4345
|
-
##
|
4346
|
-
|
4347
|
-
|
4348
|
-
class NameDeployTag(DeployTag, abc.ABC): # noqa
|
4349
|
-
pass
|
4350
|
-
|
4351
|
-
|
4352
|
-
@_register_deploy_tag
|
4353
|
-
class DeployApp(NameDeployTag):
|
4354
|
-
tag_name: ta.ClassVar[str] = 'app'
|
4355
|
-
|
4356
|
-
|
4357
|
-
@_register_deploy_tag
|
4358
|
-
class DeployConf(NameDeployTag):
|
4359
|
-
tag_name: ta.ClassVar[str] = 'conf'
|
4360
|
-
|
4361
|
-
|
4362
|
-
##
|
4363
|
-
|
4364
|
-
|
4365
|
-
class KeyDeployTag(DeployTag, abc.ABC): # noqa
|
4366
|
-
pass
|
4367
|
-
|
4368
|
-
|
4369
|
-
@_register_deploy_tag
|
4370
|
-
class DeployKey(KeyDeployTag):
|
4371
|
-
tag_name: ta.ClassVar[str] = 'deploy-key'
|
4372
|
-
|
4373
|
-
|
4374
|
-
@_register_deploy_tag
|
4375
|
-
class DeployAppKey(KeyDeployTag):
|
4376
|
-
tag_name: ta.ClassVar[str] = 'app-key'
|
4377
|
-
|
4378
|
-
|
4379
|
-
##
|
4380
|
-
|
4381
|
-
|
4382
|
-
class RevDeployTag(DeployTag, abc.ABC): # noqa
|
4383
|
-
pass
|
4384
|
-
|
4385
|
-
|
4386
|
-
@_register_deploy_tag
|
4387
|
-
class DeployAppRev(RevDeployTag):
|
4388
|
-
tag_name: ta.ClassVar[str] = 'app-rev'
|
4389
|
-
|
4390
|
-
|
4391
|
-
##
|
4392
|
-
|
4393
|
-
|
4394
|
-
class DeployTagMap:
|
4395
|
-
def __init__(
|
4396
|
-
self,
|
4397
|
-
*args: DeployTag,
|
4398
|
-
**kwargs: str,
|
4399
|
-
) -> None:
|
4400
|
-
super().__init__()
|
4401
|
-
|
4402
|
-
dct: ta.Dict[ta.Type[DeployTag], DeployTag] = {}
|
4403
|
-
|
4404
|
-
for a in args:
|
4405
|
-
c = type(check.isinstance(a, DeployTag))
|
4406
|
-
check.not_in(c, dct)
|
4407
|
-
dct[c] = a
|
4408
|
-
|
4409
|
-
for k, v in kwargs.items():
|
4410
|
-
c = DEPLOY_TAGS_BY_KWARG[k]
|
4411
|
-
check.not_in(c, dct)
|
4412
|
-
dct[c] = c(v)
|
4413
|
-
|
4414
|
-
self._dct = dct
|
4415
|
-
self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
|
4416
|
-
|
4417
|
-
#
|
4418
|
-
|
4419
|
-
def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
|
4420
|
-
return DeployTagMap(
|
4421
|
-
*self,
|
4422
|
-
*args,
|
4423
|
-
**kwargs,
|
4424
|
-
)
|
4425
|
-
|
4426
|
-
def remove(self, *tags_or_names: ta.Union[ta.Type[DeployTag], str]) -> 'DeployTagMap':
|
4427
|
-
dcs = {
|
4428
|
-
check.issubclass(a, DeployTag) if isinstance(a, type) else DEPLOY_TAGS_BY_NAME[a]
|
4429
|
-
for a in tags_or_names
|
4430
|
-
}
|
4431
|
-
|
4432
|
-
return DeployTagMap(*[
|
4433
|
-
t
|
4434
|
-
for t in self._dct.values()
|
4435
|
-
if t not in dcs
|
4436
|
-
])
|
4437
|
-
|
4438
|
-
#
|
4439
|
-
|
4440
|
-
def __repr__(self) -> str:
|
4441
|
-
return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
|
4442
|
-
|
4443
|
-
def __hash__(self) -> int:
|
4444
|
-
return hash(self._tup)
|
4445
|
-
|
4446
|
-
def __eq__(self, other: object) -> bool:
|
4447
|
-
if isinstance(other, DeployTagMap):
|
4448
|
-
return self._tup == other._tup
|
4449
|
-
else:
|
4450
|
-
return NotImplemented
|
4451
|
-
|
4452
|
-
#
|
4453
|
-
|
4454
|
-
def __len__(self) -> int:
|
4455
|
-
return len(self._dct)
|
4456
|
-
|
4457
|
-
def __iter__(self) -> ta.Iterator[DeployTag]:
|
4458
|
-
return iter(self._dct.values())
|
4459
|
-
|
4460
|
-
def __getitem__(self, key: ta.Union[ta.Type[DeployTag], str]) -> DeployTag:
|
4461
|
-
if isinstance(key, str):
|
4462
|
-
return self._dct[DEPLOY_TAGS_BY_NAME[key]]
|
4463
|
-
elif isinstance(key, type):
|
4464
|
-
return self._dct[key]
|
4465
|
-
else:
|
4466
|
-
raise TypeError(key)
|
4467
|
-
|
4468
|
-
def __contains__(self, key: ta.Union[ta.Type[DeployTag], str]) -> bool:
|
4469
|
-
if isinstance(key, str):
|
4470
|
-
return DEPLOY_TAGS_BY_NAME[key] in self._dct
|
4471
|
-
elif isinstance(key, type):
|
4472
|
-
return key in self._dct
|
4473
|
-
else:
|
4474
|
-
raise TypeError(key)
|
4475
|
-
|
4476
|
-
|
4477
4254
|
########################################
|
4478
4255
|
# ../remote/config.py
|
4479
4256
|
|
@@ -6135,6 +5912,18 @@ class FieldsObjMarshaler(ObjMarshaler):
|
|
6135
5912
|
})
|
6136
5913
|
|
6137
5914
|
|
5915
|
+
@dc.dataclass(frozen=True)
|
5916
|
+
class SingleFieldObjMarshaler(ObjMarshaler):
|
5917
|
+
ty: type
|
5918
|
+
fld: str
|
5919
|
+
|
5920
|
+
def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
|
5921
|
+
return getattr(o, self.fld)
|
5922
|
+
|
5923
|
+
def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
|
5924
|
+
return self.ty(**{self.fld: o})
|
5925
|
+
|
5926
|
+
|
6138
5927
|
@dc.dataclass(frozen=True)
|
6139
5928
|
class PolymorphicObjMarshaler(ObjMarshaler):
|
6140
5929
|
class Impl(ta.NamedTuple):
|
@@ -6209,7 +5998,7 @@ _DEFAULT_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
|
|
6209
5998
|
**{t: IterableObjMarshaler(t, DynamicObjMarshaler()) for t in (list, tuple, set, frozenset)},
|
6210
5999
|
**{t: MappingObjMarshaler(t, DynamicObjMarshaler(), DynamicObjMarshaler()) for t in (dict,)},
|
6211
6000
|
|
6212
|
-
|
6001
|
+
**{t: DynamicObjMarshaler() for t in (ta.Any, object)},
|
6213
6002
|
|
6214
6003
|
**{t: DatetimeObjMarshaler(t) for t in (datetime.date, datetime.time, datetime.datetime)},
|
6215
6004
|
decimal.Decimal: DecimalObjMarshaler(),
|
@@ -6234,6 +6023,16 @@ _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES: ta.Dict[ta.Any, type] = {
|
|
6234
6023
|
##
|
6235
6024
|
|
6236
6025
|
|
6026
|
+
_REGISTERED_OBJ_MARSHALERS_BY_TYPE: ta.MutableMapping[type, ObjMarshaler] = weakref.WeakKeyDictionary()
|
6027
|
+
|
6028
|
+
|
6029
|
+
def register_type_obj_marshaler(ty: type, om: ObjMarshaler) -> None:
|
6030
|
+
_REGISTERED_OBJ_MARSHALERS_BY_TYPE[ty] = om
|
6031
|
+
|
6032
|
+
|
6033
|
+
##
|
6034
|
+
|
6035
|
+
|
6237
6036
|
class ObjMarshalerManager:
|
6238
6037
|
def __init__(
|
6239
6038
|
self,
|
@@ -6243,6 +6042,8 @@ class ObjMarshalerManager:
|
|
6243
6042
|
default_obj_marshalers: ta.Dict[ta.Any, ObjMarshaler] = _DEFAULT_OBJ_MARSHALERS, # noqa
|
6244
6043
|
generic_mapping_types: ta.Dict[ta.Any, type] = _OBJ_MARSHALER_GENERIC_MAPPING_TYPES, # noqa
|
6245
6044
|
generic_iterable_types: ta.Dict[ta.Any, type] = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES, # noqa
|
6045
|
+
|
6046
|
+
registered_obj_marshalers: ta.Mapping[type, ObjMarshaler] = _REGISTERED_OBJ_MARSHALERS_BY_TYPE,
|
6246
6047
|
) -> None:
|
6247
6048
|
super().__init__()
|
6248
6049
|
|
@@ -6251,6 +6052,7 @@ class ObjMarshalerManager:
|
|
6251
6052
|
self._obj_marshalers = dict(default_obj_marshalers)
|
6252
6053
|
self._generic_mapping_types = generic_mapping_types
|
6253
6054
|
self._generic_iterable_types = generic_iterable_types
|
6055
|
+
self._registered_obj_marshalers = registered_obj_marshalers
|
6254
6056
|
|
6255
6057
|
self._lock = threading.RLock()
|
6256
6058
|
self._marshalers: ta.Dict[ta.Any, ObjMarshaler] = dict(_DEFAULT_OBJ_MARSHALERS)
|
@@ -6266,6 +6068,9 @@ class ObjMarshalerManager:
|
|
6266
6068
|
non_strict_fields: bool = False,
|
6267
6069
|
) -> ObjMarshaler:
|
6268
6070
|
if isinstance(ty, type):
|
6071
|
+
if (reg := self._registered_obj_marshalers.get(ty)) is not None:
|
6072
|
+
return reg
|
6073
|
+
|
6269
6074
|
if abc.ABC in ty.__bases__:
|
6270
6075
|
impls = [ity for ity in deep_subclasses(ty) if abc.ABC not in ity.__bases__] # type: ignore
|
6271
6076
|
if all(ity.__qualname__.endswith(ty.__name__) for ity in impls):
|
@@ -6330,9 +6135,15 @@ class ObjMarshalerManager:
|
|
6330
6135
|
|
6331
6136
|
#
|
6332
6137
|
|
6333
|
-
def
|
6138
|
+
def set_obj_marshaler(
|
6139
|
+
self,
|
6140
|
+
ty: ta.Any,
|
6141
|
+
m: ObjMarshaler,
|
6142
|
+
*,
|
6143
|
+
override: bool = False,
|
6144
|
+
) -> None:
|
6334
6145
|
with self._lock:
|
6335
|
-
if ty in self._obj_marshalers:
|
6146
|
+
if not override and ty in self._obj_marshalers:
|
6336
6147
|
raise KeyError(ty)
|
6337
6148
|
self._obj_marshalers[ty] = m
|
6338
6149
|
|
@@ -6423,7 +6234,7 @@ class ObjMarshalContext:
|
|
6423
6234
|
|
6424
6235
|
OBJ_MARSHALER_MANAGER = ObjMarshalerManager()
|
6425
6236
|
|
6426
|
-
|
6237
|
+
set_obj_marshaler = OBJ_MARSHALER_MANAGER.set_obj_marshaler
|
6427
6238
|
get_obj_marshaler = OBJ_MARSHALER_MANAGER.get_obj_marshaler
|
6428
6239
|
|
6429
6240
|
marshal_obj = OBJ_MARSHALER_MANAGER.marshal_obj
|
@@ -6825,6 +6636,7 @@ def read_config_file(
|
|
6825
6636
|
cls: ta.Type[T],
|
6826
6637
|
*,
|
6827
6638
|
prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
|
6639
|
+
msh: ObjMarshalerManager = OBJ_MARSHALER_MANAGER,
|
6828
6640
|
) -> T:
|
6829
6641
|
with open(path) as cf:
|
6830
6642
|
config_dct = parse_config_file(os.path.basename(path), cf)
|
@@ -6832,7 +6644,7 @@ def read_config_file(
|
|
6832
6644
|
if prepare is not None:
|
6833
6645
|
config_dct = prepare(config_dct)
|
6834
6646
|
|
6835
|
-
return unmarshal_obj(config_dct, cls)
|
6647
|
+
return msh.unmarshal_obj(config_dct, cls)
|
6836
6648
|
|
6837
6649
|
|
6838
6650
|
def build_config_named_children(
|
@@ -6885,7 +6697,7 @@ def install_command_marshaling(
|
|
6885
6697
|
lambda c: c,
|
6886
6698
|
lambda c: c.Output,
|
6887
6699
|
]:
|
6888
|
-
msh.
|
6700
|
+
msh.set_obj_marshaler(
|
6889
6701
|
fn(Command),
|
6890
6702
|
PolymorphicObjMarshaler.of([
|
6891
6703
|
PolymorphicObjMarshaler.Impl(
|
@@ -6927,355 +6739,228 @@ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command
|
|
6927
6739
|
|
6928
6740
|
|
6929
6741
|
########################################
|
6930
|
-
# ../deploy/
|
6931
|
-
"""
|
6932
|
-
TODO:
|
6933
|
-
- run/{.pid,.sock}
|
6934
|
-
- logs/...
|
6935
|
-
- current symlink
|
6936
|
-
- conf/{nginx,supervisor}
|
6937
|
-
- env/?
|
6938
|
-
- apps/<app>/shared
|
6939
|
-
"""
|
6742
|
+
# ../deploy/tags.py
|
6940
6743
|
|
6941
6744
|
|
6942
6745
|
##
|
6943
6746
|
|
6944
6747
|
|
6945
|
-
|
6946
|
-
pass
|
6947
|
-
|
6748
|
+
DEPLOY_TAG_SIGIL = '@'
|
6948
6749
|
|
6949
|
-
|
6950
|
-
@abc.abstractmethod
|
6951
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
6952
|
-
raise NotImplementedError
|
6750
|
+
DEPLOY_TAG_SEPARATOR = '--'
|
6953
6751
|
|
6752
|
+
DEPLOY_TAG_DELIMITERS: ta.AbstractSet[str] = frozenset([
|
6753
|
+
DEPLOY_TAG_SEPARATOR,
|
6754
|
+
'.',
|
6755
|
+
])
|
6954
6756
|
|
6955
|
-
|
6757
|
+
DEPLOY_TAG_ILLEGAL_STRS: ta.AbstractSet[str] = frozenset([
|
6758
|
+
DEPLOY_TAG_SIGIL,
|
6759
|
+
*DEPLOY_TAG_DELIMITERS,
|
6760
|
+
'/',
|
6761
|
+
])
|
6956
6762
|
|
6957
6763
|
|
6958
|
-
|
6959
|
-
@classmethod
|
6960
|
-
def parse(cls, s: str) -> 'DeployPathNamePart':
|
6961
|
-
check.non_empty_str(s)
|
6962
|
-
if s.startswith(DEPLOY_TAG_SIGIL):
|
6963
|
-
return TagDeployPathNamePart(s[1:])
|
6964
|
-
elif s in DEPLOY_TAG_DELIMITERS:
|
6965
|
-
return DelimiterDeployPathNamePart(s)
|
6966
|
-
else:
|
6967
|
-
return ConstDeployPathNamePart(s)
|
6764
|
+
##
|
6968
6765
|
|
6969
6766
|
|
6970
6767
|
@dc.dataclass(frozen=True)
|
6971
|
-
class
|
6972
|
-
|
6768
|
+
class DeployTag(abc.ABC): # noqa
|
6769
|
+
s: str
|
6973
6770
|
|
6974
6771
|
def __post_init__(self) -> None:
|
6975
|
-
check.
|
6772
|
+
check.not_in(abc.ABC, type(self).__bases__)
|
6773
|
+
check.non_empty_str(self.s)
|
6774
|
+
for ch in DEPLOY_TAG_ILLEGAL_STRS:
|
6775
|
+
check.state(ch not in self.s)
|
6976
6776
|
|
6977
|
-
|
6978
|
-
def tag(self) -> ta.Type[DeployTag]:
|
6979
|
-
return DEPLOY_TAGS_BY_NAME[self.name]
|
6777
|
+
#
|
6980
6778
|
|
6981
|
-
|
6982
|
-
|
6983
|
-
return tags[self.tag].s
|
6984
|
-
else:
|
6985
|
-
return DEPLOY_TAG_SIGIL + self.name
|
6779
|
+
tag_name: ta.ClassVar[str]
|
6780
|
+
tag_kwarg: ta.ClassVar[str]
|
6986
6781
|
|
6782
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
6783
|
+
super().__init_subclass__(**kwargs)
|
6987
6784
|
|
6988
|
-
|
6989
|
-
|
6990
|
-
delimiter: str
|
6785
|
+
if abc.ABC in cls.__bases__:
|
6786
|
+
return
|
6991
6787
|
|
6992
|
-
|
6993
|
-
|
6788
|
+
for b in cls.__bases__:
|
6789
|
+
if issubclass(b, DeployTag):
|
6790
|
+
check.in_(abc.ABC, b.__bases__)
|
6994
6791
|
|
6995
|
-
|
6996
|
-
|
6792
|
+
check.non_empty_str(tn := cls.tag_name)
|
6793
|
+
check.equal(tn, tn.lower().strip())
|
6794
|
+
check.not_in('_', tn)
|
6997
6795
|
|
6796
|
+
check.state(not hasattr(cls, 'tag_kwarg'))
|
6797
|
+
cls.tag_kwarg = tn.replace('-', '_')
|
6998
6798
|
|
6999
|
-
@dc.dataclass(frozen=True)
|
7000
|
-
class ConstDeployPathNamePart(DeployPathNamePart):
|
7001
|
-
const: str
|
7002
6799
|
|
7003
|
-
|
7004
|
-
check.non_empty_str(self.const)
|
7005
|
-
for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
|
7006
|
-
check.not_in(c, self.const)
|
6800
|
+
##
|
7007
6801
|
|
7008
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7009
|
-
return self.const
|
7010
6802
|
|
6803
|
+
_DEPLOY_TAGS: ta.Set[ta.Type[DeployTag]] = set()
|
6804
|
+
DEPLOY_TAGS: ta.AbstractSet[ta.Type[DeployTag]] = _DEPLOY_TAGS
|
7011
6805
|
|
7012
|
-
|
7013
|
-
|
7014
|
-
parts: ta.Sequence[DeployPathNamePart]
|
6806
|
+
_DEPLOY_TAGS_BY_NAME: ta.Dict[str, ta.Type[DeployTag]] = {}
|
6807
|
+
DEPLOY_TAGS_BY_NAME: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_NAME
|
7015
6808
|
|
7016
|
-
|
7017
|
-
|
7018
|
-
check.not_empty(self.parts)
|
7019
|
-
for k, g in itertools.groupby(self.parts, type):
|
7020
|
-
if len(gl := list(g)) > 1:
|
7021
|
-
raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
|
6809
|
+
_DEPLOY_TAGS_BY_KWARG: ta.Dict[str, ta.Type[DeployTag]] = {}
|
6810
|
+
DEPLOY_TAGS_BY_KWARG: ta.Mapping[str, ta.Type[DeployTag]] = _DEPLOY_TAGS_BY_KWARG
|
7022
6811
|
|
7023
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7024
|
-
return ''.join(p.render(tags) for p in self.parts)
|
7025
6812
|
|
7026
|
-
|
7027
|
-
|
7028
|
-
|
7029
|
-
check.not_in('/', s)
|
6813
|
+
def _register_deploy_tag(cls):
|
6814
|
+
check.not_in(cls.tag_name, _DEPLOY_TAGS_BY_NAME)
|
6815
|
+
check.not_in(cls.tag_kwarg, _DEPLOY_TAGS_BY_KWARG)
|
7030
6816
|
|
7031
|
-
|
7032
|
-
|
7033
|
-
|
7034
|
-
ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
|
7035
|
-
if not ns:
|
7036
|
-
ps.append(s[i:])
|
7037
|
-
break
|
7038
|
-
n, d = min(ns)
|
7039
|
-
ps.append(check.non_empty_str(s[i:n]))
|
7040
|
-
ps.append(s[n:n + len(d)])
|
7041
|
-
i = n + len(d)
|
6817
|
+
_DEPLOY_TAGS.add(cls)
|
6818
|
+
_DEPLOY_TAGS_BY_NAME[cls.tag_name] = cls
|
6819
|
+
_DEPLOY_TAGS_BY_KWARG[cls.tag_kwarg] = cls
|
7042
6820
|
|
7043
|
-
|
6821
|
+
register_type_obj_marshaler(cls, SingleFieldObjMarshaler(cls, 's'))
|
6822
|
+
|
6823
|
+
return cls
|
7044
6824
|
|
7045
6825
|
|
7046
6826
|
##
|
7047
6827
|
|
7048
6828
|
|
7049
|
-
@
|
7050
|
-
class
|
7051
|
-
|
6829
|
+
@_register_deploy_tag
|
6830
|
+
class DeployTime(DeployTag):
|
6831
|
+
tag_name: ta.ClassVar[str] = 'time'
|
7052
6832
|
|
7053
|
-
@property
|
7054
|
-
@abc.abstractmethod
|
7055
|
-
def kind(self) -> DeployPathKind:
|
7056
|
-
raise NotImplementedError
|
7057
6833
|
|
7058
|
-
|
7059
|
-
return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
|
6834
|
+
##
|
7060
6835
|
|
7061
|
-
@classmethod
|
7062
|
-
def parse(cls, s: str) -> 'DeployPathPart':
|
7063
|
-
if (is_dir := s.endswith('/')):
|
7064
|
-
s = s[:-1]
|
7065
|
-
check.non_empty_str(s)
|
7066
|
-
check.not_in('/', s)
|
7067
6836
|
|
7068
|
-
|
7069
|
-
|
7070
|
-
return DirDeployPathPart(n)
|
7071
|
-
else:
|
7072
|
-
return FileDeployPathPart(n)
|
6837
|
+
class NameDeployTag(DeployTag, abc.ABC): # noqa
|
6838
|
+
pass
|
7073
6839
|
|
7074
6840
|
|
7075
|
-
|
7076
|
-
|
7077
|
-
|
7078
|
-
return 'dir'
|
6841
|
+
@_register_deploy_tag
|
6842
|
+
class DeployApp(NameDeployTag):
|
6843
|
+
tag_name: ta.ClassVar[str] = 'app'
|
7079
6844
|
|
7080
6845
|
|
7081
|
-
|
7082
|
-
|
7083
|
-
|
7084
|
-
return 'file'
|
6846
|
+
@_register_deploy_tag
|
6847
|
+
class DeployConf(NameDeployTag):
|
6848
|
+
tag_name: ta.ClassVar[str] = 'conf'
|
7085
6849
|
|
7086
6850
|
|
7087
6851
|
##
|
7088
6852
|
|
7089
6853
|
|
7090
|
-
|
7091
|
-
|
7092
|
-
parts: ta.Sequence[DeployPathPart]
|
7093
|
-
|
7094
|
-
@property
|
7095
|
-
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
7096
|
-
for p in self.parts:
|
7097
|
-
yield from p.name.parts
|
7098
|
-
|
7099
|
-
def __post_init__(self) -> None:
|
7100
|
-
hash(self)
|
7101
|
-
check.not_empty(self.parts)
|
7102
|
-
for p in self.parts[:-1]:
|
7103
|
-
check.equal(p.kind, 'dir')
|
7104
|
-
|
7105
|
-
@cached_nullary
|
7106
|
-
def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
|
7107
|
-
pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
|
7108
|
-
for i, np in enumerate(self.name_parts):
|
7109
|
-
if isinstance(np, TagDeployPathNamePart):
|
7110
|
-
pd.setdefault(np.tag, []).append(i)
|
7111
|
-
return pd
|
7112
|
-
|
7113
|
-
@property
|
7114
|
-
def kind(self) -> DeployPathKind:
|
7115
|
-
return self.parts[-1].kind
|
6854
|
+
class KeyDeployTag(DeployTag, abc.ABC): # noqa
|
6855
|
+
pass
|
7116
6856
|
|
7117
|
-
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7118
|
-
return ''.join([p.render(tags) for p in self.parts])
|
7119
6857
|
|
7120
|
-
|
7121
|
-
|
7122
|
-
|
7123
|
-
ps = split_keep_delimiter(s, '/')
|
7124
|
-
return cls(tuple(DeployPathPart.parse(p) for p in ps))
|
6858
|
+
@_register_deploy_tag
|
6859
|
+
class DeployKey(KeyDeployTag):
|
6860
|
+
tag_name: ta.ClassVar[str] = 'deploy-key'
|
7125
6861
|
|
7126
6862
|
|
7127
|
-
|
7128
|
-
|
6863
|
+
@_register_deploy_tag
|
6864
|
+
class DeployAppKey(KeyDeployTag):
|
6865
|
+
tag_name: ta.ClassVar[str] = 'app-key'
|
7129
6866
|
|
7130
6867
|
|
7131
6868
|
##
|
7132
6869
|
|
7133
6870
|
|
7134
|
-
|
7135
|
-
|
7136
|
-
for c in ['..', '//']:
|
7137
|
-
check.not_in(c, s)
|
7138
|
-
check.arg(not s.startswith('/'))
|
7139
|
-
return s
|
7140
|
-
|
6871
|
+
class RevDeployTag(DeployTag, abc.ABC): # noqa
|
6872
|
+
pass
|
7141
6873
|
|
7142
|
-
class DeploySpecKeyed(ta.Generic[KeyDeployTagT]):
|
7143
|
-
@cached_nullary
|
7144
|
-
def _key_str(self) -> str:
|
7145
|
-
return hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8]
|
7146
6874
|
|
7147
|
-
|
7148
|
-
|
7149
|
-
|
6875
|
+
@_register_deploy_tag
|
6876
|
+
class DeployAppRev(RevDeployTag):
|
6877
|
+
tag_name: ta.ClassVar[str] = 'app-rev'
|
7150
6878
|
|
7151
6879
|
|
7152
6880
|
##
|
7153
6881
|
|
7154
6882
|
|
7155
|
-
|
7156
|
-
|
7157
|
-
|
7158
|
-
|
7159
|
-
|
6883
|
+
class DeployTagMap:
|
6884
|
+
def __init__(
|
6885
|
+
self,
|
6886
|
+
*args: DeployTag,
|
6887
|
+
**kwargs: str,
|
6888
|
+
) -> None:
|
6889
|
+
super().__init__()
|
7160
6890
|
|
7161
|
-
|
7162
|
-
check.not_in('..', check.non_empty_str(self.host))
|
7163
|
-
check.not_in('.', check.non_empty_str(self.path))
|
6891
|
+
dct: ta.Dict[ta.Type[DeployTag], DeployTag] = {}
|
7164
6892
|
|
6893
|
+
for a in args:
|
6894
|
+
c = type(check.isinstance(a, DeployTag))
|
6895
|
+
check.not_in(c, dct)
|
6896
|
+
dct[c] = a
|
7165
6897
|
|
7166
|
-
|
7167
|
-
|
7168
|
-
|
7169
|
-
|
7170
|
-
|
7171
|
-
subtrees: ta.Optional[ta.Sequence[str]] = None
|
7172
|
-
|
7173
|
-
def __post_init__(self) -> None:
|
7174
|
-
check.non_empty_str(self.rev)
|
7175
|
-
if self.subtrees is not None:
|
7176
|
-
for st in self.subtrees:
|
7177
|
-
check.non_empty_str(st)
|
7178
|
-
|
7179
|
-
|
7180
|
-
##
|
7181
|
-
|
7182
|
-
|
7183
|
-
@dc.dataclass(frozen=True)
|
7184
|
-
class DeployVenvSpec:
|
7185
|
-
interp: ta.Optional[str] = None
|
7186
|
-
|
7187
|
-
requirements_files: ta.Optional[ta.Sequence[str]] = None
|
7188
|
-
extra_dependencies: ta.Optional[ta.Sequence[str]] = None
|
7189
|
-
|
7190
|
-
use_uv: bool = False
|
7191
|
-
|
7192
|
-
|
7193
|
-
##
|
7194
|
-
|
7195
|
-
|
7196
|
-
@dc.dataclass(frozen=True)
|
7197
|
-
class DeployAppConfFile:
|
7198
|
-
path: str
|
7199
|
-
body: str
|
7200
|
-
|
7201
|
-
def __post_init__(self) -> None:
|
7202
|
-
check_valid_deploy_spec_path(self.path)
|
7203
|
-
|
7204
|
-
|
7205
|
-
#
|
7206
|
-
|
7207
|
-
|
7208
|
-
@dc.dataclass(frozen=True)
|
7209
|
-
class DeployAppConfLink: # noqa
|
7210
|
-
"""
|
7211
|
-
May be either:
|
7212
|
-
- @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
7213
|
-
- @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
7214
|
-
- @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
7215
|
-
"""
|
7216
|
-
|
7217
|
-
src: str
|
7218
|
-
|
7219
|
-
kind: ta.Literal['current_only', 'all_active'] = 'current_only'
|
7220
|
-
|
7221
|
-
def __post_init__(self) -> None:
|
7222
|
-
check_valid_deploy_spec_path(self.src)
|
7223
|
-
if '/' in self.src:
|
7224
|
-
check.equal(self.src.count('/'), 1)
|
7225
|
-
|
7226
|
-
|
7227
|
-
#
|
7228
|
-
|
7229
|
-
|
7230
|
-
@dc.dataclass(frozen=True)
|
7231
|
-
class DeployAppConfSpec:
|
7232
|
-
files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
|
7233
|
-
|
7234
|
-
links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
|
7235
|
-
|
7236
|
-
def __post_init__(self) -> None:
|
7237
|
-
if self.files:
|
7238
|
-
seen: ta.Set[str] = set()
|
7239
|
-
for f in self.files:
|
7240
|
-
check.not_in(f.path, seen)
|
7241
|
-
seen.add(f.path)
|
6898
|
+
for k, v in kwargs.items():
|
6899
|
+
c = DEPLOY_TAGS_BY_KWARG[k]
|
6900
|
+
check.not_in(c, dct)
|
6901
|
+
dct[c] = c(v)
|
7242
6902
|
|
6903
|
+
self._dct = dct
|
6904
|
+
self._tup = tuple(sorted((type(t).tag_kwarg, t.s) for t in dct.values()))
|
7243
6905
|
|
7244
|
-
|
6906
|
+
#
|
7245
6907
|
|
6908
|
+
def add(self, *args: ta.Any, **kwargs: ta.Any) -> 'DeployTagMap':
|
6909
|
+
return DeployTagMap(
|
6910
|
+
*self,
|
6911
|
+
*args,
|
6912
|
+
**kwargs,
|
6913
|
+
)
|
7246
6914
|
|
7247
|
-
|
7248
|
-
|
7249
|
-
|
6915
|
+
def remove(self, *tags_or_names: ta.Union[ta.Type[DeployTag], str]) -> 'DeployTagMap':
|
6916
|
+
dcs = {
|
6917
|
+
check.issubclass(a, DeployTag) if isinstance(a, type) else DEPLOY_TAGS_BY_NAME[a]
|
6918
|
+
for a in tags_or_names
|
6919
|
+
}
|
7250
6920
|
|
7251
|
-
|
6921
|
+
return DeployTagMap(*[
|
6922
|
+
t
|
6923
|
+
for t in self._dct.values()
|
6924
|
+
if t not in dcs
|
6925
|
+
])
|
7252
6926
|
|
7253
|
-
|
6927
|
+
#
|
7254
6928
|
|
7255
|
-
|
6929
|
+
def __repr__(self) -> str:
|
6930
|
+
return f'{self.__class__.__name__}({", ".join(f"{k}={v!r}" for k, v in self._tup)})'
|
7256
6931
|
|
7257
|
-
|
7258
|
-
|
7259
|
-
return DeployAppKey(self._key_str())
|
6932
|
+
def __hash__(self) -> int:
|
6933
|
+
return hash(self._tup)
|
7260
6934
|
|
6935
|
+
def __eq__(self, other: object) -> bool:
|
6936
|
+
if isinstance(other, DeployTagMap):
|
6937
|
+
return self._tup == other._tup
|
6938
|
+
else:
|
6939
|
+
return NotImplemented
|
7261
6940
|
|
7262
|
-
|
6941
|
+
#
|
7263
6942
|
|
6943
|
+
def __len__(self) -> int:
|
6944
|
+
return len(self._dct)
|
7264
6945
|
|
7265
|
-
|
7266
|
-
|
7267
|
-
apps: ta.Sequence[DeployAppSpec]
|
6946
|
+
def __iter__(self) -> ta.Iterator[DeployTag]:
|
6947
|
+
return iter(self._dct.values())
|
7268
6948
|
|
7269
|
-
def
|
7270
|
-
|
7271
|
-
|
7272
|
-
|
7273
|
-
|
7274
|
-
|
6949
|
+
def __getitem__(self, key: ta.Union[ta.Type[DeployTag], str]) -> DeployTag:
|
6950
|
+
if isinstance(key, str):
|
6951
|
+
return self._dct[DEPLOY_TAGS_BY_NAME[key]]
|
6952
|
+
elif isinstance(key, type):
|
6953
|
+
return self._dct[key]
|
6954
|
+
else:
|
6955
|
+
raise TypeError(key)
|
7275
6956
|
|
7276
|
-
|
7277
|
-
|
7278
|
-
|
6957
|
+
def __contains__(self, key: ta.Union[ta.Type[DeployTag], str]) -> bool:
|
6958
|
+
if isinstance(key, str):
|
6959
|
+
return DEPLOY_TAGS_BY_NAME[key] in self._dct
|
6960
|
+
elif isinstance(key, type):
|
6961
|
+
return key in self._dct
|
6962
|
+
else:
|
6963
|
+
raise TypeError(key)
|
7279
6964
|
|
7280
6965
|
|
7281
6966
|
########################################
|
@@ -7863,293 +7548,424 @@ class LocalCommandExecutor(CommandExecutor):
|
|
7863
7548
|
|
7864
7549
|
|
7865
7550
|
########################################
|
7866
|
-
# ../deploy/
|
7551
|
+
# ../deploy/paths/paths.py
|
7867
7552
|
"""
|
7868
7553
|
TODO:
|
7869
|
-
-
|
7870
|
-
-
|
7871
|
-
|
7872
|
-
|
7873
|
-
-
|
7874
|
-
|
7875
|
-
- 2) for each subdir not needing modification, hardlink into temp dir
|
7876
|
-
- 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
|
7877
|
-
- 4) write (or if deleting, omit) new files
|
7878
|
-
- 5) swap top level
|
7879
|
-
- ** whole deploy can be atomic(-ish) - do this for everything **
|
7880
|
-
- just a '/deploy/current' dir
|
7881
|
-
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
7882
|
-
- ** ensure (enforce) equivalent relpath nesting
|
7554
|
+
- run/{.pid,.sock}
|
7555
|
+
- logs/...
|
7556
|
+
- current symlink
|
7557
|
+
- conf/{nginx,supervisor}
|
7558
|
+
- env/?
|
7559
|
+
- apps/<app>/shared
|
7883
7560
|
"""
|
7884
7561
|
|
7885
7562
|
|
7886
|
-
|
7887
|
-
def __init__(
|
7888
|
-
self,
|
7889
|
-
*,
|
7890
|
-
deploy_home: ta.Optional[DeployHome] = None,
|
7891
|
-
) -> None:
|
7892
|
-
super().__init__()
|
7893
|
-
|
7894
|
-
self._deploy_home = deploy_home
|
7563
|
+
##
|
7895
7564
|
|
7896
|
-
#
|
7897
7565
|
|
7898
|
-
|
7899
|
-
|
7900
|
-
acf: DeployAppConfFile,
|
7901
|
-
app_conf_dir: str,
|
7902
|
-
) -> None:
|
7903
|
-
conf_file = os.path.join(app_conf_dir, acf.path)
|
7904
|
-
check.arg(is_path_in_dir(app_conf_dir, conf_file))
|
7566
|
+
class DeployPathError(Exception):
|
7567
|
+
pass
|
7905
7568
|
|
7906
|
-
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
7907
7569
|
|
7908
|
-
|
7909
|
-
|
7570
|
+
class DeployPathRenderable(abc.ABC):
|
7571
|
+
@abc.abstractmethod
|
7572
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7573
|
+
raise NotImplementedError
|
7910
7574
|
|
7911
|
-
#
|
7912
7575
|
|
7913
|
-
|
7914
|
-
conf: DeployConf
|
7915
|
-
is_dir: bool
|
7916
|
-
link_src: str
|
7917
|
-
link_dst: str
|
7576
|
+
##
|
7918
7577
|
|
7919
|
-
_UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
|
7920
|
-
_UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
|
7921
7578
|
|
7579
|
+
class DeployPathNamePart(DeployPathRenderable, abc.ABC):
|
7922
7580
|
@classmethod
|
7923
|
-
def
|
7924
|
-
|
7925
|
-
|
7926
|
-
|
7927
|
-
|
7928
|
-
|
7929
|
-
|
7930
|
-
|
7931
|
-
check.arg(is_path_in_dir(app_conf_dir, link_src))
|
7581
|
+
def parse(cls, s: str) -> 'DeployPathNamePart':
|
7582
|
+
check.non_empty_str(s)
|
7583
|
+
if s.startswith(DEPLOY_TAG_SIGIL):
|
7584
|
+
return TagDeployPathNamePart(s[1:])
|
7585
|
+
elif s in DEPLOY_TAG_DELIMITERS:
|
7586
|
+
return DelimiterDeployPathNamePart(s)
|
7587
|
+
else:
|
7588
|
+
return ConstDeployPathNamePart(s)
|
7932
7589
|
|
7933
|
-
#
|
7934
7590
|
|
7935
|
-
|
7936
|
-
|
7937
|
-
|
7938
|
-
conf = DeployConf(link.src.split('/')[0])
|
7939
|
-
link_dst_pfx = link.src
|
7940
|
-
link_dst_sfx = ''
|
7591
|
+
@dc.dataclass(frozen=True)
|
7592
|
+
class TagDeployPathNamePart(DeployPathNamePart):
|
7593
|
+
name: str
|
7941
7594
|
|
7942
|
-
|
7943
|
-
|
7944
|
-
d, f = os.path.split(link.src)
|
7945
|
-
# TODO: check filename :|
|
7946
|
-
conf = DeployConf(d)
|
7947
|
-
link_dst_pfx = d + '/'
|
7948
|
-
link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
|
7595
|
+
def __post_init__(self) -> None:
|
7596
|
+
check.in_(self.name, DEPLOY_TAGS_BY_NAME)
|
7949
7597
|
|
7950
|
-
|
7951
|
-
|
7952
|
-
|
7953
|
-
l, _, r = link.src.partition('.')
|
7954
|
-
conf = DeployConf(l)
|
7955
|
-
link_dst_pfx = l + '/'
|
7956
|
-
link_dst_sfx = '.' + r
|
7957
|
-
else:
|
7958
|
-
conf = DeployConf(link.src)
|
7959
|
-
link_dst_pfx = link.src + '/'
|
7960
|
-
link_dst_sfx = ''
|
7598
|
+
@property
|
7599
|
+
def tag(self) -> ta.Type[DeployTag]:
|
7600
|
+
return DEPLOY_TAGS_BY_NAME[self.name]
|
7961
7601
|
|
7962
|
-
|
7602
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7603
|
+
if tags is not None:
|
7604
|
+
return tags[self.tag].s
|
7605
|
+
else:
|
7606
|
+
return DEPLOY_TAG_SIGIL + self.name
|
7963
7607
|
|
7964
|
-
if link.kind == 'current_only':
|
7965
|
-
link_dst_mid = str(tags[DeployApp].s)
|
7966
|
-
elif link.kind == 'all_active':
|
7967
|
-
link_dst_mid = cls._UNIQUE_LINK_NAME.render(tags)
|
7968
|
-
else:
|
7969
|
-
raise TypeError(link)
|
7970
7608
|
|
7971
|
-
|
7609
|
+
@dc.dataclass(frozen=True)
|
7610
|
+
class DelimiterDeployPathNamePart(DeployPathNamePart):
|
7611
|
+
delimiter: str
|
7972
7612
|
|
7973
|
-
|
7974
|
-
|
7975
|
-
link_dst_mid,
|
7976
|
-
link_dst_sfx,
|
7977
|
-
])
|
7978
|
-
link_dst = os.path.join(conf_link_dir, link_dst_name)
|
7613
|
+
def __post_init__(self) -> None:
|
7614
|
+
check.in_(self.delimiter, DEPLOY_TAG_DELIMITERS)
|
7979
7615
|
|
7980
|
-
|
7981
|
-
|
7982
|
-
is_dir=is_dir,
|
7983
|
-
link_src=link_src,
|
7984
|
-
link_dst=link_dst,
|
7985
|
-
)
|
7616
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7617
|
+
return self.delimiter
|
7986
7618
|
|
7987
|
-
async def _make_app_conf_link(
|
7988
|
-
self,
|
7989
|
-
link: DeployAppConfLink,
|
7990
|
-
tags: DeployTagMap,
|
7991
|
-
app_conf_dir: str,
|
7992
|
-
conf_link_dir: str,
|
7993
|
-
) -> None:
|
7994
|
-
comp = self._compute_app_conf_link_dst(
|
7995
|
-
link,
|
7996
|
-
tags,
|
7997
|
-
app_conf_dir,
|
7998
|
-
conf_link_dir,
|
7999
|
-
)
|
8000
7619
|
|
8001
|
-
|
7620
|
+
@dc.dataclass(frozen=True)
|
7621
|
+
class ConstDeployPathNamePart(DeployPathNamePart):
|
7622
|
+
const: str
|
8002
7623
|
|
8003
|
-
|
8004
|
-
check.
|
7624
|
+
def __post_init__(self) -> None:
|
7625
|
+
check.non_empty_str(self.const)
|
7626
|
+
for c in [*DEPLOY_TAG_DELIMITERS, DEPLOY_TAG_SIGIL, '/']:
|
7627
|
+
check.not_in(c, self.const)
|
8005
7628
|
|
8006
|
-
|
8007
|
-
|
8008
|
-
else:
|
8009
|
-
check.arg(os.path.isfile(comp.link_src))
|
7629
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7630
|
+
return self.const
|
8010
7631
|
|
8011
|
-
#
|
8012
7632
|
|
8013
|
-
|
8014
|
-
|
8015
|
-
|
8016
|
-
target_is_directory=comp.is_dir,
|
8017
|
-
make_dirs=True,
|
8018
|
-
)
|
7633
|
+
@dc.dataclass(frozen=True)
|
7634
|
+
class DeployPathName(DeployPathRenderable):
|
7635
|
+
parts: ta.Sequence[DeployPathNamePart]
|
8019
7636
|
|
8020
|
-
|
7637
|
+
def __post_init__(self) -> None:
|
7638
|
+
hash(self)
|
7639
|
+
check.not_empty(self.parts)
|
7640
|
+
for k, g in itertools.groupby(self.parts, type):
|
7641
|
+
if len(gl := list(g)) > 1:
|
7642
|
+
raise DeployPathError(f'May not have consecutive path name part types: {k} {gl}')
|
8021
7643
|
|
8022
|
-
|
8023
|
-
|
8024
|
-
spec: DeployAppConfSpec,
|
8025
|
-
tags: DeployTagMap,
|
8026
|
-
app_conf_dir: str,
|
8027
|
-
conf_link_dir: str,
|
8028
|
-
) -> None:
|
8029
|
-
for acf in spec.files or []:
|
8030
|
-
await self._write_app_conf_file(
|
8031
|
-
acf,
|
8032
|
-
app_conf_dir,
|
8033
|
-
)
|
7644
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7645
|
+
return ''.join(p.render(tags) for p in self.parts)
|
8034
7646
|
|
8035
|
-
|
7647
|
+
@classmethod
|
7648
|
+
def parse(cls, s: str) -> 'DeployPathName':
|
7649
|
+
check.non_empty_str(s)
|
7650
|
+
check.not_in('/', s)
|
8036
7651
|
|
8037
|
-
|
8038
|
-
|
8039
|
-
|
8040
|
-
|
8041
|
-
|
8042
|
-
|
8043
|
-
|
7652
|
+
i = 0
|
7653
|
+
ps = []
|
7654
|
+
while i < len(s):
|
7655
|
+
ns = [(n, d) for d in DEPLOY_TAG_DELIMITERS if (n := s.find(d, i)) >= 0]
|
7656
|
+
if not ns:
|
7657
|
+
ps.append(s[i:])
|
7658
|
+
break
|
7659
|
+
n, d = min(ns)
|
7660
|
+
ps.append(check.non_empty_str(s[i:n]))
|
7661
|
+
ps.append(s[n:n + len(d)])
|
7662
|
+
i = n + len(d)
|
8044
7663
|
|
7664
|
+
return cls(tuple(DeployPathNamePart.parse(p) for p in ps))
|
8045
7665
|
|
8046
|
-
########################################
|
8047
|
-
# ../deploy/paths/owners.py
|
8048
7666
|
|
7667
|
+
##
|
8049
7668
|
|
8050
|
-
|
7669
|
+
|
7670
|
+
@dc.dataclass(frozen=True)
|
7671
|
+
class DeployPathPart(DeployPathRenderable, abc.ABC): # noqa
|
7672
|
+
name: DeployPathName
|
7673
|
+
|
7674
|
+
@property
|
8051
7675
|
@abc.abstractmethod
|
8052
|
-
def
|
7676
|
+
def kind(self) -> DeployPathKind:
|
8053
7677
|
raise NotImplementedError
|
8054
7678
|
|
7679
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7680
|
+
return self.name.render(tags) + ('/' if self.kind == 'dir' else '')
|
8055
7681
|
|
8056
|
-
|
7682
|
+
@classmethod
|
7683
|
+
def parse(cls, s: str) -> 'DeployPathPart':
|
7684
|
+
if (is_dir := s.endswith('/')):
|
7685
|
+
s = s[:-1]
|
7686
|
+
check.non_empty_str(s)
|
7687
|
+
check.not_in('/', s)
|
8057
7688
|
|
7689
|
+
n = DeployPathName.parse(s)
|
7690
|
+
if is_dir:
|
7691
|
+
return DirDeployPathPart(n)
|
7692
|
+
else:
|
7693
|
+
return FileDeployPathPart(n)
|
8058
7694
|
|
8059
|
-
class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
|
8060
|
-
def __init__(
|
8061
|
-
self,
|
8062
|
-
*args: ta.Any,
|
8063
|
-
owned_dir: str,
|
8064
|
-
deploy_home: ta.Optional[DeployHome],
|
8065
|
-
**kwargs: ta.Any,
|
8066
|
-
) -> None:
|
8067
|
-
super().__init__(*args, **kwargs)
|
8068
7695
|
|
8069
|
-
|
8070
|
-
|
7696
|
+
class DirDeployPathPart(DeployPathPart):
|
7697
|
+
@property
|
7698
|
+
def kind(self) -> DeployPathKind:
|
7699
|
+
return 'dir'
|
8071
7700
|
|
8072
|
-
self._deploy_home = deploy_home
|
8073
7701
|
|
8074
|
-
|
7702
|
+
class FileDeployPathPart(DeployPathPart):
|
7703
|
+
@property
|
7704
|
+
def kind(self) -> DeployPathKind:
|
7705
|
+
return 'file'
|
8075
7706
|
|
8076
|
-
@cached_nullary
|
8077
|
-
def _dir(self) -> str:
|
8078
|
-
return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
|
8079
7707
|
|
8080
|
-
|
8081
|
-
def _make_dir(self) -> str:
|
8082
|
-
if not os.path.isdir(d := self._dir()):
|
8083
|
-
os.makedirs(d, exist_ok=True)
|
8084
|
-
return d
|
7708
|
+
##
|
8085
7709
|
|
8086
|
-
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8087
|
-
return self._owned_deploy_paths
|
8088
7710
|
|
7711
|
+
@dc.dataclass(frozen=True)
|
7712
|
+
class DeployPath:
|
7713
|
+
parts: ta.Sequence[DeployPathPart]
|
8089
7714
|
|
8090
|
-
|
8091
|
-
|
8092
|
-
|
8093
|
-
|
8094
|
-
- sequence all messages
|
8095
|
-
"""
|
7715
|
+
@property
|
7716
|
+
def name_parts(self) -> ta.Iterator[DeployPathNamePart]:
|
7717
|
+
for p in self.parts:
|
7718
|
+
yield from p.name.parts
|
8096
7719
|
|
7720
|
+
def __post_init__(self) -> None:
|
7721
|
+
hash(self)
|
7722
|
+
check.not_empty(self.parts)
|
7723
|
+
for p in self.parts[:-1]:
|
7724
|
+
check.equal(p.kind, 'dir')
|
8097
7725
|
|
8098
|
-
|
7726
|
+
@cached_nullary
|
7727
|
+
def tag_indices(self) -> ta.Mapping[ta.Type[DeployTag], ta.Sequence[int]]:
|
7728
|
+
pd: ta.Dict[ta.Type[DeployTag], ta.List[int]] = {}
|
7729
|
+
for i, np in enumerate(self.name_parts):
|
7730
|
+
if isinstance(np, TagDeployPathNamePart):
|
7731
|
+
pd.setdefault(np.tag, []).append(i)
|
7732
|
+
return pd
|
8099
7733
|
|
7734
|
+
@property
|
7735
|
+
def kind(self) -> DeployPathKind:
|
7736
|
+
return self.parts[-1].kind
|
8100
7737
|
|
8101
|
-
|
8102
|
-
|
8103
|
-
async def send(self, chan: RemoteChannel) -> None:
|
8104
|
-
await chan.send_obj(self, _RemoteProtocol.Message)
|
7738
|
+
def render(self, tags: ta.Optional[DeployTagMap] = None) -> str:
|
7739
|
+
return ''.join([p.render(tags) for p in self.parts])
|
8105
7740
|
|
8106
|
-
|
8107
|
-
|
8108
|
-
|
7741
|
+
@classmethod
|
7742
|
+
def parse(cls, s: str) -> 'DeployPath':
|
7743
|
+
check.non_empty_str(s)
|
7744
|
+
ps = split_keep_delimiter(s, '/')
|
7745
|
+
return cls(tuple(DeployPathPart.parse(p) for p in ps))
|
8109
7746
|
|
8110
|
-
#
|
8111
7747
|
|
8112
|
-
|
8113
|
-
|
7748
|
+
########################################
|
7749
|
+
# ../deploy/specs.py
|
8114
7750
|
|
8115
|
-
@dc.dataclass(frozen=True)
|
8116
|
-
class CommandRequest(Request):
|
8117
|
-
seq: int
|
8118
|
-
cmd: Command
|
8119
7751
|
|
8120
|
-
|
8121
|
-
class PingRequest(Request):
|
8122
|
-
time: float
|
7752
|
+
##
|
8123
7753
|
|
8124
|
-
#
|
8125
7754
|
|
8126
|
-
|
8127
|
-
|
7755
|
+
def check_valid_deploy_spec_path(s: str) -> str:
|
7756
|
+
check.non_empty_str(s)
|
7757
|
+
for c in ['..', '//']:
|
7758
|
+
check.not_in(c, s)
|
7759
|
+
check.arg(not s.startswith('/'))
|
7760
|
+
return s
|
8128
7761
|
|
8129
|
-
@dc.dataclass(frozen=True)
|
8130
|
-
class LogResponse(Response):
|
8131
|
-
s: str
|
8132
7762
|
|
8133
|
-
|
8134
|
-
|
8135
|
-
|
8136
|
-
|
7763
|
+
class DeploySpecKeyed(ta.Generic[KeyDeployTagT]):
|
7764
|
+
@cached_nullary
|
7765
|
+
def _key_str(self) -> str:
|
7766
|
+
return hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8]
|
8137
7767
|
|
8138
|
-
@
|
8139
|
-
|
8140
|
-
|
7768
|
+
@abc.abstractmethod
|
7769
|
+
def key(self) -> KeyDeployTagT:
|
7770
|
+
raise NotImplementedError
|
8141
7771
|
|
8142
7772
|
|
8143
7773
|
##
|
8144
7774
|
|
8145
7775
|
|
8146
|
-
|
8147
|
-
|
8148
|
-
|
8149
|
-
|
8150
|
-
|
8151
|
-
|
8152
|
-
|
7776
|
+
@dc.dataclass(frozen=True)
|
7777
|
+
class DeployGitRepo:
|
7778
|
+
host: ta.Optional[str] = None
|
7779
|
+
username: ta.Optional[str] = None
|
7780
|
+
path: ta.Optional[str] = None
|
7781
|
+
|
7782
|
+
def __post_init__(self) -> None:
|
7783
|
+
check.not_in('..', check.non_empty_str(self.host))
|
7784
|
+
check.not_in('.', check.non_empty_str(self.path))
|
7785
|
+
|
7786
|
+
|
7787
|
+
@dc.dataclass(frozen=True)
|
7788
|
+
class DeployGitSpec:
|
7789
|
+
repo: DeployGitRepo
|
7790
|
+
rev: DeployRev
|
7791
|
+
|
7792
|
+
subtrees: ta.Optional[ta.Sequence[str]] = None
|
7793
|
+
|
7794
|
+
def __post_init__(self) -> None:
|
7795
|
+
check.non_empty_str(self.rev)
|
7796
|
+
if self.subtrees is not None:
|
7797
|
+
for st in self.subtrees:
|
7798
|
+
check.non_empty_str(st)
|
7799
|
+
|
7800
|
+
|
7801
|
+
##
|
7802
|
+
|
7803
|
+
|
7804
|
+
@dc.dataclass(frozen=True)
|
7805
|
+
class DeployVenvSpec:
|
7806
|
+
interp: ta.Optional[str] = None
|
7807
|
+
|
7808
|
+
requirements_files: ta.Optional[ta.Sequence[str]] = None
|
7809
|
+
extra_dependencies: ta.Optional[ta.Sequence[str]] = None
|
7810
|
+
|
7811
|
+
use_uv: bool = False
|
7812
|
+
|
7813
|
+
|
7814
|
+
##
|
7815
|
+
|
7816
|
+
|
7817
|
+
@dc.dataclass(frozen=True)
|
7818
|
+
class DeployAppConfFile:
|
7819
|
+
path: str
|
7820
|
+
body: str
|
7821
|
+
|
7822
|
+
def __post_init__(self) -> None:
|
7823
|
+
check_valid_deploy_spec_path(self.path)
|
7824
|
+
|
7825
|
+
|
7826
|
+
#
|
7827
|
+
|
7828
|
+
|
7829
|
+
@dc.dataclass(frozen=True)
|
7830
|
+
class DeployAppConfLink: # noqa
|
7831
|
+
"""
|
7832
|
+
May be either:
|
7833
|
+
- @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
7834
|
+
- @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
7835
|
+
- @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
7836
|
+
"""
|
7837
|
+
|
7838
|
+
src: str
|
7839
|
+
|
7840
|
+
kind: ta.Literal['current_only', 'all_active'] = 'current_only'
|
7841
|
+
|
7842
|
+
def __post_init__(self) -> None:
|
7843
|
+
check_valid_deploy_spec_path(self.src)
|
7844
|
+
if '/' in self.src:
|
7845
|
+
check.equal(self.src.count('/'), 1)
|
7846
|
+
|
7847
|
+
|
7848
|
+
#
|
7849
|
+
|
7850
|
+
|
7851
|
+
@dc.dataclass(frozen=True)
|
7852
|
+
class DeployAppConfSpec:
|
7853
|
+
files: ta.Optional[ta.Sequence[DeployAppConfFile]] = None
|
7854
|
+
|
7855
|
+
links: ta.Optional[ta.Sequence[DeployAppConfLink]] = None
|
7856
|
+
|
7857
|
+
def __post_init__(self) -> None:
|
7858
|
+
if self.files:
|
7859
|
+
seen: ta.Set[str] = set()
|
7860
|
+
for f in self.files:
|
7861
|
+
check.not_in(f.path, seen)
|
7862
|
+
seen.add(f.path)
|
7863
|
+
|
7864
|
+
|
7865
|
+
##
|
7866
|
+
|
7867
|
+
|
7868
|
+
@dc.dataclass(frozen=True)
|
7869
|
+
class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
|
7870
|
+
app: DeployApp
|
7871
|
+
|
7872
|
+
git: DeployGitSpec
|
7873
|
+
|
7874
|
+
venv: ta.Optional[DeployVenvSpec] = None
|
7875
|
+
|
7876
|
+
conf: ta.Optional[DeployAppConfSpec] = None
|
7877
|
+
|
7878
|
+
# @ta.override
|
7879
|
+
def key(self) -> DeployAppKey:
|
7880
|
+
return DeployAppKey(self._key_str())
|
7881
|
+
|
7882
|
+
|
7883
|
+
##
|
7884
|
+
|
7885
|
+
|
7886
|
+
@dc.dataclass(frozen=True)
|
7887
|
+
class DeploySpec(DeploySpecKeyed[DeployKey]):
|
7888
|
+
home: DeployHome
|
7889
|
+
|
7890
|
+
apps: ta.Sequence[DeployAppSpec]
|
7891
|
+
|
7892
|
+
def __post_init__(self) -> None:
|
7893
|
+
check.non_empty_str(self.home)
|
7894
|
+
|
7895
|
+
seen: ta.Set[DeployApp] = set()
|
7896
|
+
for a in self.apps:
|
7897
|
+
if a.app in seen:
|
7898
|
+
raise KeyError(a.app)
|
7899
|
+
seen.add(a.app)
|
7900
|
+
|
7901
|
+
# @ta.override
|
7902
|
+
def key(self) -> DeployKey:
|
7903
|
+
return DeployKey(self._key_str())
|
7904
|
+
|
7905
|
+
|
7906
|
+
########################################
|
7907
|
+
# ../remote/execution.py
|
7908
|
+
"""
|
7909
|
+
TODO:
|
7910
|
+
- sequence all messages
|
7911
|
+
"""
|
7912
|
+
|
7913
|
+
|
7914
|
+
##
|
7915
|
+
|
7916
|
+
|
7917
|
+
class _RemoteProtocol:
|
7918
|
+
class Message(abc.ABC): # noqa
|
7919
|
+
async def send(self, chan: RemoteChannel) -> None:
|
7920
|
+
await chan.send_obj(self, _RemoteProtocol.Message)
|
7921
|
+
|
7922
|
+
@classmethod
|
7923
|
+
async def recv(cls: ta.Type[T], chan: RemoteChannel) -> ta.Optional[T]:
|
7924
|
+
return await chan.recv_obj(cls)
|
7925
|
+
|
7926
|
+
#
|
7927
|
+
|
7928
|
+
class Request(Message, abc.ABC): # noqa
|
7929
|
+
pass
|
7930
|
+
|
7931
|
+
@dc.dataclass(frozen=True)
|
7932
|
+
class CommandRequest(Request):
|
7933
|
+
seq: int
|
7934
|
+
cmd: Command
|
7935
|
+
|
7936
|
+
@dc.dataclass(frozen=True)
|
7937
|
+
class PingRequest(Request):
|
7938
|
+
time: float
|
7939
|
+
|
7940
|
+
#
|
7941
|
+
|
7942
|
+
class Response(Message, abc.ABC): # noqa
|
7943
|
+
pass
|
7944
|
+
|
7945
|
+
@dc.dataclass(frozen=True)
|
7946
|
+
class LogResponse(Response):
|
7947
|
+
s: str
|
7948
|
+
|
7949
|
+
@dc.dataclass(frozen=True)
|
7950
|
+
class CommandResponse(Response):
|
7951
|
+
seq: int
|
7952
|
+
res: CommandOutputOrExceptionData
|
7953
|
+
|
7954
|
+
@dc.dataclass(frozen=True)
|
7955
|
+
class PingResponse(Response):
|
7956
|
+
time: float
|
7957
|
+
|
7958
|
+
|
7959
|
+
##
|
7960
|
+
|
7961
|
+
|
7962
|
+
class _RemoteLogHandler(logging.Handler):
|
7963
|
+
def __init__(
|
7964
|
+
self,
|
7965
|
+
chan: RemoteChannel,
|
7966
|
+
loop: ta.Any = None,
|
7967
|
+
) -> None:
|
7968
|
+
super().__init__()
|
8153
7969
|
|
8154
7970
|
self._chan = chan
|
8155
7971
|
self._loop = loop
|
@@ -8874,236 +8690,212 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
|
|
8874
8690
|
|
8875
8691
|
|
8876
8692
|
########################################
|
8877
|
-
# ../deploy/
|
8693
|
+
# ../deploy/conf.py
|
8878
8694
|
"""
|
8879
8695
|
TODO:
|
8880
|
-
-
|
8881
|
-
|
8882
|
-
|
8883
|
-
|
8884
|
-
|
8885
|
-
|
8696
|
+
- @conf DeployPathPlaceholder? :|
|
8697
|
+
- post-deploy: remove any dir_links not present in new spec
|
8698
|
+
- * only if succeeded * - otherwise, remove any dir_links present in new spec but not previously present?
|
8699
|
+
- no such thing as 'previously present'.. build a 'deploy state' and pass it back?
|
8700
|
+
- ** whole thing can be atomic **
|
8701
|
+
- 1) new atomic temp dir
|
8702
|
+
- 2) for each subdir not needing modification, hardlink into temp dir
|
8703
|
+
- 3) for each subdir needing modification, new subdir, hardlink all files not needing modification
|
8704
|
+
- 4) write (or if deleting, omit) new files
|
8705
|
+
- 5) swap top level
|
8706
|
+
- ** whole deploy can be atomic(-ish) - do this for everything **
|
8707
|
+
- just a '/deploy/current' dir
|
8708
|
+
- some things (venvs) cannot be moved, thus the /deploy/venvs dir
|
8709
|
+
- ** ensure (enforce) equivalent relpath nesting
|
8886
8710
|
"""
|
8887
8711
|
|
8888
8712
|
|
8889
|
-
|
8890
|
-
|
8891
|
-
|
8892
|
-
class DeployGitManager(SingleDirDeployPathOwner):
|
8893
|
-
def __init__(
|
8713
|
+
class DeployConfManager:
|
8714
|
+
async def _write_app_conf_file(
|
8894
8715
|
self,
|
8895
|
-
|
8896
|
-
|
8897
|
-
atomics: AtomicPathSwapping,
|
8716
|
+
acf: DeployAppConfFile,
|
8717
|
+
app_conf_dir: str,
|
8898
8718
|
) -> None:
|
8899
|
-
|
8900
|
-
|
8901
|
-
deploy_home=deploy_home,
|
8902
|
-
)
|
8719
|
+
conf_file = os.path.join(app_conf_dir, acf.path)
|
8720
|
+
check.arg(is_path_in_dir(app_conf_dir, conf_file))
|
8903
8721
|
|
8904
|
-
|
8722
|
+
os.makedirs(os.path.dirname(conf_file), exist_ok=True)
|
8905
8723
|
|
8906
|
-
|
8724
|
+
with open(conf_file, 'w') as f: # noqa
|
8725
|
+
f.write(acf.body)
|
8907
8726
|
|
8908
|
-
|
8909
|
-
def __init__(
|
8910
|
-
self,
|
8911
|
-
git: 'DeployGitManager',
|
8912
|
-
repo: DeployGitRepo,
|
8913
|
-
) -> None:
|
8914
|
-
super().__init__()
|
8727
|
+
#
|
8915
8728
|
|
8916
|
-
|
8917
|
-
|
8918
|
-
|
8919
|
-
|
8920
|
-
|
8921
|
-
check.non_empty_str(repo.path),
|
8922
|
-
)
|
8729
|
+
class _ComputedConfLink(ta.NamedTuple):
|
8730
|
+
conf: DeployConf
|
8731
|
+
is_dir: bool
|
8732
|
+
link_src: str
|
8733
|
+
link_dst: str
|
8923
8734
|
|
8924
|
-
|
8925
|
-
|
8926
|
-
return self._repo
|
8735
|
+
_UNIQUE_LINK_NAME_STR = '@app--@time--@app-key'
|
8736
|
+
_UNIQUE_LINK_NAME = DeployPath.parse(_UNIQUE_LINK_NAME_STR)
|
8927
8737
|
|
8928
|
-
|
8929
|
-
|
8930
|
-
|
8931
|
-
|
8932
|
-
|
8933
|
-
|
8934
|
-
|
8935
|
-
|
8936
|
-
|
8937
|
-
|
8938
|
-
await asyncio_subprocesses.check_call(
|
8939
|
-
*cmd,
|
8940
|
-
cwd=self._dir,
|
8941
|
-
)
|
8738
|
+
@classmethod
|
8739
|
+
def _compute_app_conf_link_dst(
|
8740
|
+
cls,
|
8741
|
+
link: DeployAppConfLink,
|
8742
|
+
tags: DeployTagMap,
|
8743
|
+
app_conf_dir: str,
|
8744
|
+
conf_link_dir: str,
|
8745
|
+
) -> _ComputedConfLink:
|
8746
|
+
link_src = os.path.join(app_conf_dir, link.src)
|
8747
|
+
check.arg(is_path_in_dir(app_conf_dir, link_src))
|
8942
8748
|
|
8943
8749
|
#
|
8944
8750
|
|
8945
|
-
|
8946
|
-
|
8947
|
-
|
8948
|
-
|
8949
|
-
|
8751
|
+
if (is_dir := link.src.endswith('/')):
|
8752
|
+
# @conf/ - links a directory in root of app conf dir to conf/@conf/@dst/
|
8753
|
+
check.arg(link.src.count('/') == 1)
|
8754
|
+
conf = DeployConf(link.src.split('/')[0])
|
8755
|
+
link_dst_pfx = link.src
|
8756
|
+
link_dst_sfx = ''
|
8950
8757
|
|
8951
|
-
|
8952
|
-
|
8758
|
+
elif '/' in link.src:
|
8759
|
+
# @conf/file - links a single file in a single subdir to conf/@conf/@dst--file
|
8760
|
+
d, f = os.path.split(link.src)
|
8761
|
+
# TODO: check filename :|
|
8762
|
+
conf = DeployConf(d)
|
8763
|
+
link_dst_pfx = d + '/'
|
8764
|
+
link_dst_sfx = DEPLOY_TAG_SEPARATOR + f
|
8953
8765
|
|
8954
|
-
|
8955
|
-
|
8956
|
-
|
8766
|
+
else: # noqa
|
8767
|
+
# @conf(.ext)* - links a single file in root of app conf dir to conf/@conf/@dst(.ext)*
|
8768
|
+
if '.' in link.src:
|
8769
|
+
l, _, r = link.src.partition('.')
|
8770
|
+
conf = DeployConf(l)
|
8771
|
+
link_dst_pfx = l + '/'
|
8772
|
+
link_dst_sfx = '.' + r
|
8773
|
+
else:
|
8774
|
+
conf = DeployConf(link.src)
|
8775
|
+
link_dst_pfx = link.src + '/'
|
8776
|
+
link_dst_sfx = ''
|
8957
8777
|
|
8958
8778
|
#
|
8959
8779
|
|
8960
|
-
|
8961
|
-
|
8962
|
-
|
8963
|
-
|
8964
|
-
|
8965
|
-
|
8966
|
-
make_dirs=True,
|
8967
|
-
) as dst_swap:
|
8968
|
-
await self.fetch(spec.rev)
|
8969
|
-
|
8970
|
-
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
|
8971
|
-
await dst_call('git', 'init')
|
8972
|
-
|
8973
|
-
await dst_call('git', 'remote', 'add', 'local', self._dir)
|
8974
|
-
await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
|
8975
|
-
await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
|
8976
|
-
|
8977
|
-
def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
|
8978
|
-
try:
|
8979
|
-
return self._repo_dirs[repo]
|
8980
|
-
except KeyError:
|
8981
|
-
repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
|
8982
|
-
return repo_dir
|
8983
|
-
|
8984
|
-
async def checkout(
|
8985
|
-
self,
|
8986
|
-
spec: DeployGitSpec,
|
8987
|
-
dst_dir: str,
|
8988
|
-
) -> None:
|
8989
|
-
await self.get_repo_dir(spec.repo).checkout(spec, dst_dir)
|
8780
|
+
if link.kind == 'current_only':
|
8781
|
+
link_dst_mid = str(tags[DeployApp].s)
|
8782
|
+
elif link.kind == 'all_active':
|
8783
|
+
link_dst_mid = cls._UNIQUE_LINK_NAME.render(tags)
|
8784
|
+
else:
|
8785
|
+
raise TypeError(link)
|
8990
8786
|
|
8787
|
+
#
|
8991
8788
|
|
8992
|
-
|
8993
|
-
|
8789
|
+
link_dst_name = ''.join([
|
8790
|
+
link_dst_pfx,
|
8791
|
+
link_dst_mid,
|
8792
|
+
link_dst_sfx,
|
8793
|
+
])
|
8794
|
+
link_dst = os.path.join(conf_link_dir, link_dst_name)
|
8994
8795
|
|
8796
|
+
return DeployConfManager._ComputedConfLink(
|
8797
|
+
conf=conf,
|
8798
|
+
is_dir=is_dir,
|
8799
|
+
link_src=link_src,
|
8800
|
+
link_dst=link_dst,
|
8801
|
+
)
|
8995
8802
|
|
8996
|
-
|
8997
|
-
def __init__(
|
8803
|
+
async def _make_app_conf_link(
|
8998
8804
|
self,
|
8999
|
-
|
9000
|
-
|
9001
|
-
|
8805
|
+
link: DeployAppConfLink,
|
8806
|
+
tags: DeployTagMap,
|
8807
|
+
app_conf_dir: str,
|
8808
|
+
conf_link_dir: str,
|
9002
8809
|
) -> None:
|
9003
|
-
|
8810
|
+
comp = self._compute_app_conf_link_dst(
|
8811
|
+
link,
|
8812
|
+
tags,
|
8813
|
+
app_conf_dir,
|
8814
|
+
conf_link_dir,
|
8815
|
+
)
|
9004
8816
|
|
9005
|
-
|
9006
|
-
self._deploy_path_owners = deploy_path_owners
|
8817
|
+
#
|
9007
8818
|
|
9008
|
-
|
9009
|
-
|
9010
|
-
dct: ta.Dict[DeployPath, DeployPathOwner] = {}
|
9011
|
-
for o in self._deploy_path_owners:
|
9012
|
-
for p in o.get_owned_deploy_paths():
|
9013
|
-
if p in dct:
|
9014
|
-
raise DeployPathError(f'Duplicate deploy path owner: {p}')
|
9015
|
-
dct[p] = o
|
9016
|
-
return dct
|
8819
|
+
check.arg(is_path_in_dir(app_conf_dir, comp.link_src))
|
8820
|
+
check.arg(is_path_in_dir(conf_link_dir, comp.link_dst))
|
9017
8821
|
|
9018
|
-
|
9019
|
-
|
8822
|
+
if comp.is_dir:
|
8823
|
+
check.arg(os.path.isdir(comp.link_src))
|
8824
|
+
else:
|
8825
|
+
check.arg(os.path.isfile(comp.link_src))
|
9020
8826
|
|
8827
|
+
#
|
9021
8828
|
|
9022
|
-
|
9023
|
-
|
8829
|
+
relative_symlink( # noqa
|
8830
|
+
comp.link_src,
|
8831
|
+
comp.link_dst,
|
8832
|
+
target_is_directory=comp.is_dir,
|
8833
|
+
make_dirs=True,
|
8834
|
+
)
|
9024
8835
|
|
8836
|
+
#
|
9025
8837
|
|
9026
|
-
|
9027
|
-
SingleDirDeployPathOwner,
|
9028
|
-
AtomicPathSwapping,
|
9029
|
-
):
|
9030
|
-
def __init__(
|
8838
|
+
async def write_app_conf(
|
9031
8839
|
self,
|
9032
|
-
|
9033
|
-
|
8840
|
+
spec: DeployAppConfSpec,
|
8841
|
+
tags: DeployTagMap,
|
8842
|
+
app_conf_dir: str,
|
8843
|
+
conf_link_dir: str,
|
9034
8844
|
) -> None:
|
9035
|
-
|
9036
|
-
|
9037
|
-
|
9038
|
-
|
8845
|
+
for acf in spec.files or []:
|
8846
|
+
await self._write_app_conf_file(
|
8847
|
+
acf,
|
8848
|
+
app_conf_dir,
|
8849
|
+
)
|
9039
8850
|
|
9040
|
-
|
9041
|
-
def _swapping(self) -> AtomicPathSwapping:
|
9042
|
-
return TempDirAtomicPathSwapping(
|
9043
|
-
temp_dir=self._make_dir(),
|
9044
|
-
root_dir=check.non_empty_str(self._deploy_home),
|
9045
|
-
)
|
8851
|
+
#
|
9046
8852
|
|
9047
|
-
|
9048
|
-
self
|
9049
|
-
|
9050
|
-
|
9051
|
-
|
9052
|
-
|
9053
|
-
|
9054
|
-
kind,
|
9055
|
-
dst_path,
|
9056
|
-
**kwargs,
|
9057
|
-
)
|
8853
|
+
for link in spec.links or []:
|
8854
|
+
await self._make_app_conf_link(
|
8855
|
+
link,
|
8856
|
+
tags,
|
8857
|
+
app_conf_dir,
|
8858
|
+
conf_link_dir,
|
8859
|
+
)
|
9058
8860
|
|
9059
8861
|
|
9060
8862
|
########################################
|
9061
|
-
# ../deploy/
|
9062
|
-
"""
|
9063
|
-
TODO:
|
9064
|
-
- interp
|
9065
|
-
- share more code with pyproject?
|
9066
|
-
"""
|
8863
|
+
# ../deploy/paths/owners.py
|
9067
8864
|
|
9068
8865
|
|
9069
|
-
class
|
9070
|
-
|
9071
|
-
|
9072
|
-
|
9073
|
-
atomics: AtomicPathSwapping,
|
9074
|
-
) -> None:
|
9075
|
-
super().__init__()
|
8866
|
+
class DeployPathOwner(abc.ABC):
|
8867
|
+
@abc.abstractmethod
|
8868
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8869
|
+
raise NotImplementedError
|
9076
8870
|
|
9077
|
-
self._atomics = atomics
|
9078
8871
|
|
9079
|
-
|
9080
|
-
self,
|
9081
|
-
spec: DeployVenvSpec,
|
9082
|
-
git_dir: str,
|
9083
|
-
venv_dir: str,
|
9084
|
-
) -> None:
|
9085
|
-
sys_exe = 'python3'
|
8872
|
+
DeployPathOwners = ta.NewType('DeployPathOwners', ta.Sequence[DeployPathOwner])
|
9086
8873
|
|
9087
|
-
# !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
|
9088
|
-
# garbage collect orphaned dirs.
|
9089
|
-
await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
|
9090
8874
|
|
9091
|
-
|
8875
|
+
class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
|
8876
|
+
def __init__(
|
8877
|
+
self,
|
8878
|
+
*args: ta.Any,
|
8879
|
+
owned_dir: str,
|
8880
|
+
**kwargs: ta.Any,
|
8881
|
+
) -> None:
|
8882
|
+
super().__init__(*args, **kwargs)
|
9092
8883
|
|
9093
|
-
|
8884
|
+
check.not_in('/', owned_dir)
|
8885
|
+
self._owned_dir: str = check.non_empty_str(owned_dir)
|
9094
8886
|
|
9095
|
-
|
8887
|
+
self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
|
9096
8888
|
|
9097
|
-
|
8889
|
+
def _dir(self, home: DeployHome) -> str:
|
8890
|
+
return os.path.join(check.non_empty_str(home), self._owned_dir)
|
9098
8891
|
|
9099
|
-
|
9100
|
-
|
9101
|
-
|
9102
|
-
|
9103
|
-
else:
|
9104
|
-
pip_cmd = ['-m', 'pip']
|
8892
|
+
def _make_dir(self, home: DeployHome) -> str:
|
8893
|
+
if not os.path.isdir(d := self._dir(home)):
|
8894
|
+
os.makedirs(d, exist_ok=True)
|
8895
|
+
return d
|
9105
8896
|
|
9106
|
-
|
8897
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
8898
|
+
return self._owned_deploy_paths
|
9107
8899
|
|
9108
8900
|
|
9109
8901
|
########################################
|
@@ -9639,183 +9431,55 @@ def bind_commands(
|
|
9639
9431
|
|
9640
9432
|
|
9641
9433
|
########################################
|
9642
|
-
# ../deploy/
|
9434
|
+
# ../deploy/paths/manager.py
|
9643
9435
|
|
9644
9436
|
|
9645
|
-
class
|
9437
|
+
class DeployPathsManager:
|
9646
9438
|
def __init__(
|
9647
9439
|
self,
|
9648
9440
|
*,
|
9649
|
-
|
9650
|
-
|
9651
|
-
conf: DeployConfManager,
|
9652
|
-
git: DeployGitManager,
|
9653
|
-
venvs: DeployVenvManager,
|
9441
|
+
deploy_path_owners: DeployPathOwners,
|
9654
9442
|
) -> None:
|
9655
9443
|
super().__init__()
|
9656
9444
|
|
9657
|
-
self.
|
9658
|
-
|
9659
|
-
self._conf = conf
|
9660
|
-
self._git = git
|
9661
|
-
self._venvs = venvs
|
9662
|
-
|
9663
|
-
#
|
9664
|
-
|
9665
|
-
_APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
|
9666
|
-
_APP_DIR = DeployPath.parse(_APP_DIR_STR)
|
9667
|
-
|
9668
|
-
_DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
|
9669
|
-
_DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
|
9670
|
-
|
9671
|
-
_APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
|
9672
|
-
_CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
|
9445
|
+
self._deploy_path_owners = deploy_path_owners
|
9673
9446
|
|
9674
9447
|
@cached_nullary
|
9675
|
-
def
|
9676
|
-
|
9677
|
-
|
9678
|
-
|
9679
|
-
|
9680
|
-
|
9681
|
-
|
9682
|
-
|
9683
|
-
|
9684
|
-
*[
|
9685
|
-
DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
|
9686
|
-
for sfx in [
|
9687
|
-
'conf',
|
9688
|
-
'git',
|
9689
|
-
'venv',
|
9690
|
-
]
|
9691
|
-
],
|
9692
|
-
}
|
9693
|
-
|
9694
|
-
#
|
9695
|
-
|
9696
|
-
async def prepare_app(
|
9697
|
-
self,
|
9698
|
-
spec: DeployAppSpec,
|
9699
|
-
tags: DeployTagMap,
|
9700
|
-
) -> None:
|
9701
|
-
deploy_home = check.non_empty_str(self._deploy_home)
|
9448
|
+
def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
|
9449
|
+
dct: ta.Dict[DeployPath, DeployPathOwner] = {}
|
9450
|
+
for o in self._deploy_path_owners:
|
9451
|
+
for p in o.get_owned_deploy_paths():
|
9452
|
+
if p in dct:
|
9453
|
+
raise DeployPathError(f'Duplicate deploy path owner: {p}')
|
9454
|
+
dct[p] = o
|
9455
|
+
return dct
|
9702
9456
|
|
9703
|
-
|
9704
|
-
|
9457
|
+
def validate_deploy_paths(self) -> None:
|
9458
|
+
self.owners_by_path()
|
9705
9459
|
|
9706
|
-
app_dir = build_path(self._APP_DIR)
|
9707
|
-
deploy_dir = build_path(self._DEPLOY_DIR)
|
9708
|
-
app_deploy_link = build_path(self._APP_DEPLOY_LINK)
|
9709
9460
|
|
9710
|
-
|
9461
|
+
########################################
|
9462
|
+
# ../deploy/tmp.py
|
9711
9463
|
|
9712
|
-
os.makedirs(deploy_dir, exist_ok=True)
|
9713
9464
|
|
9714
|
-
|
9715
|
-
|
9716
|
-
deploy_dir,
|
9717
|
-
deploying_link,
|
9718
|
-
target_is_directory=True,
|
9719
|
-
make_dirs=True,
|
9720
|
-
)
|
9465
|
+
class DeployHomeAtomics(Func1[DeployHome, AtomicPathSwapping]):
|
9466
|
+
pass
|
9721
9467
|
|
9722
|
-
#
|
9723
9468
|
|
9724
|
-
|
9725
|
-
|
9726
|
-
|
9727
|
-
|
9728
|
-
|
9729
|
-
|
9469
|
+
class DeployTmpManager(
|
9470
|
+
SingleDirDeployPathOwner,
|
9471
|
+
):
|
9472
|
+
def __init__(self) -> None:
|
9473
|
+
super().__init__(
|
9474
|
+
owned_dir='tmp',
|
9730
9475
|
)
|
9731
9476
|
|
9732
|
-
|
9733
|
-
|
9734
|
-
|
9735
|
-
|
9736
|
-
|
9737
|
-
#
|
9738
|
-
|
9739
|
-
# def mirror_symlinks(src: str, dst: str) -> None:
|
9740
|
-
# def mirror_link(lp: str) -> None:
|
9741
|
-
# check.state(os.path.islink(lp))
|
9742
|
-
# shutil.copy2(
|
9743
|
-
# lp,
|
9744
|
-
# os.path.join(dst, os.path.relpath(lp, src)),
|
9745
|
-
# follow_symlinks=False,
|
9746
|
-
# )
|
9747
|
-
#
|
9748
|
-
# for dp, dns, fns in os.walk(src, followlinks=False):
|
9749
|
-
# for fn in fns:
|
9750
|
-
# mirror_link(os.path.join(dp, fn))
|
9751
|
-
#
|
9752
|
-
# for dn in dns:
|
9753
|
-
# dp2 = os.path.join(dp, dn)
|
9754
|
-
# if os.path.islink(dp2):
|
9755
|
-
# mirror_link(dp2)
|
9756
|
-
# else:
|
9757
|
-
# os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
|
9758
|
-
|
9759
|
-
current_link = os.path.join(deploy_home, 'deploys/current')
|
9760
|
-
|
9761
|
-
# if os.path.exists(current_link):
|
9762
|
-
# mirror_symlinks(
|
9763
|
-
# os.path.join(current_link, 'conf'),
|
9764
|
-
# conf_tag_dir,
|
9765
|
-
# )
|
9766
|
-
# mirror_symlinks(
|
9767
|
-
# os.path.join(current_link, 'apps'),
|
9768
|
-
# os.path.join(deploy_dir, 'apps'),
|
9769
|
-
# )
|
9770
|
-
|
9771
|
-
#
|
9772
|
-
|
9773
|
-
app_git_dir = os.path.join(app_dir, 'git')
|
9774
|
-
await self._git.checkout(
|
9775
|
-
spec.git,
|
9776
|
-
app_git_dir,
|
9477
|
+
def get_swapping(self, home: DeployHome) -> AtomicPathSwapping:
|
9478
|
+
return TempDirAtomicPathSwapping(
|
9479
|
+
temp_dir=self._make_dir(home),
|
9480
|
+
root_dir=check.non_empty_str(home),
|
9777
9481
|
)
|
9778
9482
|
|
9779
|
-
#
|
9780
|
-
|
9781
|
-
if spec.venv is not None:
|
9782
|
-
app_venv_dir = os.path.join(app_dir, 'venv')
|
9783
|
-
await self._venvs.setup_venv(
|
9784
|
-
spec.venv,
|
9785
|
-
app_git_dir,
|
9786
|
-
app_venv_dir,
|
9787
|
-
)
|
9788
|
-
|
9789
|
-
#
|
9790
|
-
|
9791
|
-
if spec.conf is not None:
|
9792
|
-
app_conf_dir = os.path.join(app_dir, 'conf')
|
9793
|
-
await self._conf.write_app_conf(
|
9794
|
-
spec.conf,
|
9795
|
-
tags,
|
9796
|
-
app_conf_dir,
|
9797
|
-
deploy_conf_dir,
|
9798
|
-
)
|
9799
|
-
|
9800
|
-
#
|
9801
|
-
|
9802
|
-
os.replace(deploying_link, current_link)
|
9803
|
-
|
9804
|
-
|
9805
|
-
########################################
|
9806
|
-
# ../deploy/paths/inject.py
|
9807
|
-
|
9808
|
-
|
9809
|
-
def bind_deploy_paths() -> InjectorBindings:
|
9810
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
9811
|
-
inj.bind_array(DeployPathOwner),
|
9812
|
-
inj.bind_array_type(DeployPathOwner, DeployPathOwners),
|
9813
|
-
|
9814
|
-
inj.bind(DeployPathsManager, singleton=True),
|
9815
|
-
]
|
9816
|
-
|
9817
|
-
return inj.as_bindings(*lst)
|
9818
|
-
|
9819
9483
|
|
9820
9484
|
########################################
|
9821
9485
|
# ../remote/connection.py
|
@@ -10272,11 +9936,11 @@ class PyenvVersionInstaller:
|
|
10272
9936
|
full_args = [
|
10273
9937
|
os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
|
10274
9938
|
*conf_args,
|
10275
|
-
self.install_dir(),
|
9939
|
+
await self.install_dir(),
|
10276
9940
|
]
|
10277
9941
|
else:
|
10278
9942
|
full_args = [
|
10279
|
-
self._pyenv.exe(),
|
9943
|
+
await self._pyenv.exe(),
|
10280
9944
|
'install',
|
10281
9945
|
*conf_args,
|
10282
9946
|
]
|
@@ -10532,90 +10196,489 @@ class SystemInterpProvider(InterpProvider):
|
|
10532
10196
|
|
10533
10197
|
|
10534
10198
|
########################################
|
10535
|
-
# ../deploy/
|
10199
|
+
# ../deploy/git.py
|
10200
|
+
"""
|
10201
|
+
TODO:
|
10202
|
+
- 'repos'?
|
10536
10203
|
|
10204
|
+
git/github.com/wrmsr/omlish <- bootstrap repo
|
10205
|
+
- shallow clone off bootstrap into /apps
|
10537
10206
|
|
10538
|
-
|
10207
|
+
github.com/wrmsr/omlish@rev
|
10208
|
+
"""
|
10539
10209
|
|
10540
10210
|
|
10541
|
-
|
10211
|
+
##
|
10542
10212
|
|
10543
10213
|
|
10544
|
-
class
|
10214
|
+
class DeployGitManager(SingleDirDeployPathOwner):
|
10545
10215
|
def __init__(
|
10546
10216
|
self,
|
10547
10217
|
*,
|
10548
|
-
|
10549
|
-
|
10218
|
+
atomics: DeployHomeAtomics,
|
10219
|
+
) -> None:
|
10220
|
+
super().__init__(
|
10221
|
+
owned_dir='git',
|
10222
|
+
)
|
10550
10223
|
|
10551
|
-
|
10552
|
-
):
|
10553
|
-
super().__init__()
|
10224
|
+
self._atomics = atomics
|
10554
10225
|
|
10555
|
-
self.
|
10556
|
-
self._paths = paths
|
10226
|
+
self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
|
10557
10227
|
|
10558
|
-
|
10228
|
+
class RepoDir:
|
10229
|
+
def __init__(
|
10230
|
+
self,
|
10231
|
+
git: 'DeployGitManager',
|
10232
|
+
repo: DeployGitRepo,
|
10233
|
+
home: DeployHome,
|
10234
|
+
) -> None:
|
10235
|
+
super().__init__()
|
10559
10236
|
|
10560
|
-
|
10561
|
-
|
10562
|
-
|
10563
|
-
|
10564
|
-
|
10237
|
+
self._git = git
|
10238
|
+
self._repo = repo
|
10239
|
+
self._home = home
|
10240
|
+
self._dir = os.path.join(
|
10241
|
+
self._git._make_dir(home), # noqa
|
10242
|
+
check.non_empty_str(repo.host),
|
10243
|
+
check.non_empty_str(repo.path),
|
10244
|
+
)
|
10565
10245
|
|
10566
|
-
|
10567
|
-
|
10246
|
+
@property
|
10247
|
+
def repo(self) -> DeployGitRepo:
|
10248
|
+
return self._repo
|
10568
10249
|
|
10569
|
-
|
10570
|
-
|
10571
|
-
|
10572
|
-
|
10573
|
-
|
10250
|
+
@property
|
10251
|
+
def url(self) -> str:
|
10252
|
+
if self._repo.username is not None:
|
10253
|
+
return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
|
10254
|
+
else:
|
10255
|
+
return f'https://{self._repo.host}/{self._repo.path}'
|
10574
10256
|
|
10575
10257
|
#
|
10576
10258
|
|
10577
|
-
|
10578
|
-
|
10579
|
-
|
10580
|
-
|
10259
|
+
async def _call(self, *cmd: str) -> None:
|
10260
|
+
await asyncio_subprocesses.check_call(
|
10261
|
+
*cmd,
|
10262
|
+
cwd=self._dir,
|
10263
|
+
)
|
10581
10264
|
|
10582
10265
|
#
|
10583
10266
|
|
10584
|
-
|
10585
|
-
|
10586
|
-
|
10587
|
-
|
10588
|
-
|
10589
|
-
)
|
10590
|
-
|
10591
|
-
await self._apps.prepare_app(
|
10592
|
-
app,
|
10593
|
-
app_tags,
|
10594
|
-
)
|
10267
|
+
@async_cached_nullary
|
10268
|
+
async def init(self) -> None:
|
10269
|
+
os.makedirs(self._dir, exist_ok=True)
|
10270
|
+
if os.path.exists(os.path.join(self._dir, '.git')):
|
10271
|
+
return
|
10595
10272
|
|
10273
|
+
await self._call('git', 'init')
|
10274
|
+
await self._call('git', 'remote', 'add', 'origin', self.url)
|
10596
10275
|
|
10597
|
-
|
10598
|
-
|
10276
|
+
async def fetch(self, rev: DeployRev) -> None:
|
10277
|
+
await self.init()
|
10278
|
+
await self._call('git', 'fetch', '--depth=1', 'origin', rev)
|
10599
10279
|
|
10280
|
+
#
|
10600
10281
|
|
10601
|
-
def
|
10602
|
-
|
10603
|
-
|
10604
|
-
|
10282
|
+
async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
|
10283
|
+
check.state(not os.path.exists(dst_dir))
|
10284
|
+
with self._git._atomics(self._home).begin_atomic_path_swap( # noqa
|
10285
|
+
'dir',
|
10286
|
+
dst_dir,
|
10287
|
+
auto_commit=True,
|
10288
|
+
make_dirs=True,
|
10289
|
+
) as dst_swap:
|
10290
|
+
await self.fetch(spec.rev)
|
10291
|
+
|
10292
|
+
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
|
10293
|
+
await dst_call('git', 'init')
|
10294
|
+
|
10295
|
+
await dst_call('git', 'remote', 'add', 'local', self._dir)
|
10296
|
+
await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
|
10297
|
+
await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
|
10298
|
+
|
10299
|
+
def get_repo_dir(
|
10300
|
+
self,
|
10301
|
+
repo: DeployGitRepo,
|
10302
|
+
home: DeployHome,
|
10303
|
+
) -> RepoDir:
|
10304
|
+
try:
|
10305
|
+
return self._repo_dirs[repo]
|
10306
|
+
except KeyError:
|
10307
|
+
repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(
|
10308
|
+
self,
|
10309
|
+
repo,
|
10310
|
+
home,
|
10311
|
+
)
|
10312
|
+
return repo_dir
|
10313
|
+
|
10314
|
+
async def checkout(
|
10315
|
+
self,
|
10316
|
+
spec: DeployGitSpec,
|
10317
|
+
home: DeployHome,
|
10318
|
+
dst_dir: str,
|
10319
|
+
) -> None:
|
10320
|
+
await self.get_repo_dir(
|
10321
|
+
spec.repo,
|
10322
|
+
home,
|
10323
|
+
).checkout(
|
10324
|
+
spec,
|
10325
|
+
dst_dir,
|
10326
|
+
)
|
10327
|
+
|
10328
|
+
|
10329
|
+
########################################
|
10330
|
+
# ../deploy/paths/inject.py
|
10331
|
+
|
10332
|
+
|
10333
|
+
def bind_deploy_paths() -> InjectorBindings:
|
10334
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10335
|
+
inj.bind_array(DeployPathOwner),
|
10336
|
+
inj.bind_array_type(DeployPathOwner, DeployPathOwners),
|
10337
|
+
|
10338
|
+
inj.bind(DeployPathsManager, singleton=True),
|
10339
|
+
]
|
10340
|
+
|
10341
|
+
return inj.as_bindings(*lst)
|
10342
|
+
|
10343
|
+
|
10344
|
+
########################################
|
10345
|
+
# ../remote/inject.py
|
10346
|
+
|
10347
|
+
|
10348
|
+
def bind_remote(
|
10349
|
+
*,
|
10350
|
+
remote_config: RemoteConfig,
|
10351
|
+
) -> InjectorBindings:
|
10605
10352
|
lst: ta.List[InjectorBindingOrBindings] = [
|
10606
10353
|
inj.bind(remote_config),
|
10607
10354
|
|
10608
10355
|
inj.bind(SubprocessRemoteSpawning, singleton=True),
|
10609
10356
|
inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
|
10610
10357
|
|
10611
|
-
inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
|
10612
|
-
inj.bind(InProcessRemoteExecutionConnector, singleton=True),
|
10358
|
+
inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
|
10359
|
+
inj.bind(InProcessRemoteExecutionConnector, singleton=True),
|
10360
|
+
]
|
10361
|
+
|
10362
|
+
#
|
10363
|
+
|
10364
|
+
if (pf := remote_config.payload_file) is not None:
|
10365
|
+
lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
|
10366
|
+
|
10367
|
+
#
|
10368
|
+
|
10369
|
+
return inj.as_bindings(*lst)
|
10370
|
+
|
10371
|
+
|
10372
|
+
########################################
|
10373
|
+
# ../system/inject.py
|
10374
|
+
|
10375
|
+
|
10376
|
+
def bind_system(
|
10377
|
+
*,
|
10378
|
+
system_config: SystemConfig,
|
10379
|
+
) -> InjectorBindings:
|
10380
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10381
|
+
inj.bind(system_config),
|
10382
|
+
]
|
10383
|
+
|
10384
|
+
#
|
10385
|
+
|
10386
|
+
platform = system_config.platform or detect_system_platform()
|
10387
|
+
lst.append(inj.bind(platform, key=Platform))
|
10388
|
+
|
10389
|
+
#
|
10390
|
+
|
10391
|
+
if isinstance(platform, AmazonLinuxPlatform):
|
10392
|
+
lst.extend([
|
10393
|
+
inj.bind(YumSystemPackageManager, singleton=True),
|
10394
|
+
inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
|
10395
|
+
])
|
10396
|
+
|
10397
|
+
elif isinstance(platform, LinuxPlatform):
|
10398
|
+
lst.extend([
|
10399
|
+
inj.bind(AptSystemPackageManager, singleton=True),
|
10400
|
+
inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
|
10401
|
+
])
|
10402
|
+
|
10403
|
+
elif isinstance(platform, DarwinPlatform):
|
10404
|
+
lst.extend([
|
10405
|
+
inj.bind(BrewSystemPackageManager, singleton=True),
|
10406
|
+
inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
|
10407
|
+
])
|
10408
|
+
|
10409
|
+
#
|
10410
|
+
|
10411
|
+
lst.extend([
|
10412
|
+
bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
|
10413
|
+
])
|
10414
|
+
|
10415
|
+
#
|
10416
|
+
|
10417
|
+
return inj.as_bindings(*lst)
|
10418
|
+
|
10419
|
+
|
10420
|
+
########################################
|
10421
|
+
# ../targets/connection.py
|
10422
|
+
|
10423
|
+
|
10424
|
+
##
|
10425
|
+
|
10426
|
+
|
10427
|
+
class ManageTargetConnector(abc.ABC):
|
10428
|
+
@abc.abstractmethod
|
10429
|
+
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10430
|
+
raise NotImplementedError
|
10431
|
+
|
10432
|
+
def _default_python(self, python: ta.Optional[ta.Sequence[str]]) -> ta.Sequence[str]:
|
10433
|
+
check.not_isinstance(python, str)
|
10434
|
+
if python is not None:
|
10435
|
+
return python
|
10436
|
+
else:
|
10437
|
+
return ['sh', '-c', get_best_python_sh(), '--']
|
10438
|
+
|
10439
|
+
|
10440
|
+
##
|
10441
|
+
|
10442
|
+
|
10443
|
+
ManageTargetConnectorMap = ta.NewType('ManageTargetConnectorMap', ta.Mapping[ta.Type[ManageTarget], ManageTargetConnector]) # noqa
|
10444
|
+
|
10445
|
+
|
10446
|
+
@dc.dataclass(frozen=True)
|
10447
|
+
class TypeSwitchedManageTargetConnector(ManageTargetConnector):
|
10448
|
+
connectors: ManageTargetConnectorMap
|
10449
|
+
|
10450
|
+
def get_connector(self, ty: ta.Type[ManageTarget]) -> ManageTargetConnector:
|
10451
|
+
for k, v in self.connectors.items():
|
10452
|
+
if issubclass(ty, k):
|
10453
|
+
return v
|
10454
|
+
raise KeyError(ty)
|
10455
|
+
|
10456
|
+
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10457
|
+
return self.get_connector(type(tgt)).connect(tgt)
|
10458
|
+
|
10459
|
+
|
10460
|
+
##
|
10461
|
+
|
10462
|
+
|
10463
|
+
@dc.dataclass(frozen=True)
|
10464
|
+
class LocalManageTargetConnector(ManageTargetConnector):
|
10465
|
+
_local_executor: LocalCommandExecutor
|
10466
|
+
_in_process_connector: InProcessRemoteExecutionConnector
|
10467
|
+
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10468
|
+
_bootstrap: MainBootstrap
|
10469
|
+
|
10470
|
+
@contextlib.asynccontextmanager
|
10471
|
+
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10472
|
+
lmt = check.isinstance(tgt, LocalManageTarget)
|
10473
|
+
|
10474
|
+
if isinstance(lmt, InProcessManageTarget):
|
10475
|
+
imt = check.isinstance(lmt, InProcessManageTarget)
|
10476
|
+
|
10477
|
+
if imt.mode == InProcessManageTarget.Mode.DIRECT:
|
10478
|
+
yield self._local_executor
|
10479
|
+
|
10480
|
+
elif imt.mode == InProcessManageTarget.Mode.FAKE_REMOTE:
|
10481
|
+
async with self._in_process_connector.connect() as rce:
|
10482
|
+
yield rce
|
10483
|
+
|
10484
|
+
else:
|
10485
|
+
raise TypeError(imt.mode)
|
10486
|
+
|
10487
|
+
elif isinstance(lmt, SubprocessManageTarget):
|
10488
|
+
async with self._pyremote_connector.connect(
|
10489
|
+
RemoteSpawning.Target(
|
10490
|
+
python=self._default_python(lmt.python),
|
10491
|
+
),
|
10492
|
+
self._bootstrap,
|
10493
|
+
) as rce:
|
10494
|
+
yield rce
|
10495
|
+
|
10496
|
+
else:
|
10497
|
+
raise TypeError(lmt)
|
10498
|
+
|
10499
|
+
|
10500
|
+
##
|
10501
|
+
|
10502
|
+
|
10503
|
+
@dc.dataclass(frozen=True)
|
10504
|
+
class DockerManageTargetConnector(ManageTargetConnector):
|
10505
|
+
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10506
|
+
_bootstrap: MainBootstrap
|
10507
|
+
|
10508
|
+
@contextlib.asynccontextmanager
|
10509
|
+
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10510
|
+
dmt = check.isinstance(tgt, DockerManageTarget)
|
10511
|
+
|
10512
|
+
sh_parts: ta.List[str] = ['docker']
|
10513
|
+
if dmt.image is not None:
|
10514
|
+
sh_parts.extend(['run', '-i', dmt.image])
|
10515
|
+
elif dmt.container_id is not None:
|
10516
|
+
sh_parts.extend(['exec', '-i', dmt.container_id])
|
10517
|
+
else:
|
10518
|
+
raise ValueError(dmt)
|
10519
|
+
|
10520
|
+
async with self._pyremote_connector.connect(
|
10521
|
+
RemoteSpawning.Target(
|
10522
|
+
shell=' '.join(sh_parts),
|
10523
|
+
python=self._default_python(dmt.python),
|
10524
|
+
),
|
10525
|
+
self._bootstrap,
|
10526
|
+
) as rce:
|
10527
|
+
yield rce
|
10528
|
+
|
10529
|
+
|
10530
|
+
##
|
10531
|
+
|
10532
|
+
|
10533
|
+
@dc.dataclass(frozen=True)
|
10534
|
+
class SshManageTargetConnector(ManageTargetConnector):
|
10535
|
+
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10536
|
+
_bootstrap: MainBootstrap
|
10537
|
+
|
10538
|
+
@contextlib.asynccontextmanager
|
10539
|
+
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10540
|
+
smt = check.isinstance(tgt, SshManageTarget)
|
10541
|
+
|
10542
|
+
sh_parts: ta.List[str] = ['ssh']
|
10543
|
+
if smt.key_file is not None:
|
10544
|
+
sh_parts.extend(['-i', smt.key_file])
|
10545
|
+
addr = check.not_none(smt.host)
|
10546
|
+
if smt.username is not None:
|
10547
|
+
addr = f'{smt.username}@{addr}'
|
10548
|
+
sh_parts.append(addr)
|
10549
|
+
|
10550
|
+
async with self._pyremote_connector.connect(
|
10551
|
+
RemoteSpawning.Target(
|
10552
|
+
shell=' '.join(sh_parts),
|
10553
|
+
shell_quote=True,
|
10554
|
+
python=self._default_python(smt.python),
|
10555
|
+
),
|
10556
|
+
self._bootstrap,
|
10557
|
+
) as rce:
|
10558
|
+
yield rce
|
10559
|
+
|
10560
|
+
|
10561
|
+
########################################
|
10562
|
+
# ../../../omdev/interp/resolvers.py
|
10563
|
+
|
10564
|
+
|
10565
|
+
INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
|
10566
|
+
cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
|
10567
|
+
}
|
10568
|
+
|
10569
|
+
|
10570
|
+
class InterpResolver:
|
10571
|
+
def __init__(
|
10572
|
+
self,
|
10573
|
+
providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
|
10574
|
+
) -> None:
|
10575
|
+
super().__init__()
|
10576
|
+
|
10577
|
+
self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
|
10578
|
+
|
10579
|
+
async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
|
10580
|
+
lst = [
|
10581
|
+
(i, si)
|
10582
|
+
for i, p in enumerate(self._providers.values())
|
10583
|
+
for si in await p.get_installed_versions(spec)
|
10584
|
+
if spec.contains(si)
|
10585
|
+
]
|
10586
|
+
|
10587
|
+
slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
|
10588
|
+
if not slst:
|
10589
|
+
return None
|
10590
|
+
|
10591
|
+
bi, bv = slst[-1]
|
10592
|
+
bp = list(self._providers.values())[bi]
|
10593
|
+
return (bp, bv)
|
10594
|
+
|
10595
|
+
async def resolve(
|
10596
|
+
self,
|
10597
|
+
spec: InterpSpecifier,
|
10598
|
+
*,
|
10599
|
+
install: bool = False,
|
10600
|
+
) -> ta.Optional[Interp]:
|
10601
|
+
tup = await self._resolve_installed(spec)
|
10602
|
+
if tup is not None:
|
10603
|
+
bp, bv = tup
|
10604
|
+
return await bp.get_installed_version(bv)
|
10605
|
+
|
10606
|
+
if not install:
|
10607
|
+
return None
|
10608
|
+
|
10609
|
+
tp = list(self._providers.values())[0] # noqa
|
10610
|
+
|
10611
|
+
sv = sorted(
|
10612
|
+
[s for s in await tp.get_installable_versions(spec) if s in spec],
|
10613
|
+
key=lambda s: s.version,
|
10614
|
+
)
|
10615
|
+
if not sv:
|
10616
|
+
return None
|
10617
|
+
|
10618
|
+
bv = sv[-1]
|
10619
|
+
return await tp.install_version(bv)
|
10620
|
+
|
10621
|
+
async def list(self, spec: InterpSpecifier) -> None:
|
10622
|
+
print('installed:')
|
10623
|
+
for n, p in self._providers.items():
|
10624
|
+
lst = [
|
10625
|
+
si
|
10626
|
+
for si in await p.get_installed_versions(spec)
|
10627
|
+
if spec.contains(si)
|
10628
|
+
]
|
10629
|
+
if lst:
|
10630
|
+
print(f' {n}')
|
10631
|
+
for si in lst:
|
10632
|
+
print(f' {si}')
|
10633
|
+
|
10634
|
+
print()
|
10635
|
+
|
10636
|
+
print('installable:')
|
10637
|
+
for n, p in self._providers.items():
|
10638
|
+
lst = [
|
10639
|
+
si
|
10640
|
+
for si in await p.get_installable_versions(spec)
|
10641
|
+
if spec.contains(si)
|
10642
|
+
]
|
10643
|
+
if lst:
|
10644
|
+
print(f' {n}')
|
10645
|
+
for si in lst:
|
10646
|
+
print(f' {si}')
|
10647
|
+
|
10648
|
+
|
10649
|
+
DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
|
10650
|
+
# pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
|
10651
|
+
PyenvInterpProvider(try_update=True),
|
10652
|
+
|
10653
|
+
RunningInterpProvider(),
|
10654
|
+
|
10655
|
+
SystemInterpProvider(),
|
10656
|
+
]])
|
10657
|
+
|
10658
|
+
|
10659
|
+
########################################
|
10660
|
+
# ../targets/inject.py
|
10661
|
+
|
10662
|
+
|
10663
|
+
def bind_targets() -> InjectorBindings:
|
10664
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
10665
|
+
inj.bind(LocalManageTargetConnector, singleton=True),
|
10666
|
+
inj.bind(DockerManageTargetConnector, singleton=True),
|
10667
|
+
inj.bind(SshManageTargetConnector, singleton=True),
|
10668
|
+
|
10669
|
+
inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
|
10670
|
+
inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
|
10613
10671
|
]
|
10614
10672
|
|
10615
10673
|
#
|
10616
10674
|
|
10617
|
-
|
10618
|
-
|
10675
|
+
def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
|
10676
|
+
return ManageTargetConnectorMap({
|
10677
|
+
LocalManageTarget: injector[LocalManageTargetConnector],
|
10678
|
+
DockerManageTarget: injector[DockerManageTargetConnector],
|
10679
|
+
SshManageTarget: injector[SshManageTargetConnector],
|
10680
|
+
})
|
10681
|
+
lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
|
10619
10682
|
|
10620
10683
|
#
|
10621
10684
|
|
@@ -10623,290 +10686,319 @@ def bind_remote(
|
|
10623
10686
|
|
10624
10687
|
|
10625
10688
|
########################################
|
10626
|
-
# ../
|
10689
|
+
# ../deploy/interp.py
|
10627
10690
|
|
10628
10691
|
|
10629
|
-
|
10630
|
-
*,
|
10631
|
-
system_config: SystemConfig,
|
10632
|
-
) -> InjectorBindings:
|
10633
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
10634
|
-
inj.bind(system_config),
|
10635
|
-
]
|
10692
|
+
##
|
10636
10693
|
|
10637
|
-
#
|
10638
10694
|
|
10639
|
-
|
10640
|
-
|
10695
|
+
@dc.dataclass(frozen=True)
|
10696
|
+
class InterpCommand(Command['InterpCommand.Output']):
|
10697
|
+
spec: str
|
10698
|
+
install: bool = False
|
10641
10699
|
|
10642
|
-
|
10700
|
+
@dc.dataclass(frozen=True)
|
10701
|
+
class Output(Command.Output):
|
10702
|
+
exe: str
|
10703
|
+
version: str
|
10704
|
+
opts: InterpOpts
|
10643
10705
|
|
10644
|
-
if isinstance(platform, AmazonLinuxPlatform):
|
10645
|
-
lst.extend([
|
10646
|
-
inj.bind(YumSystemPackageManager, singleton=True),
|
10647
|
-
inj.bind(SystemPackageManager, to_key=YumSystemPackageManager),
|
10648
|
-
])
|
10649
10706
|
|
10650
|
-
|
10651
|
-
|
10652
|
-
|
10653
|
-
|
10654
|
-
|
10707
|
+
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
10708
|
+
async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
10709
|
+
i = InterpSpecifier.parse(check.not_none(cmd.spec))
|
10710
|
+
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
|
10711
|
+
return InterpCommand.Output(
|
10712
|
+
exe=o.exe,
|
10713
|
+
version=str(o.version.version),
|
10714
|
+
opts=o.version.opts,
|
10715
|
+
)
|
10655
10716
|
|
10656
|
-
elif isinstance(platform, DarwinPlatform):
|
10657
|
-
lst.extend([
|
10658
|
-
inj.bind(BrewSystemPackageManager, singleton=True),
|
10659
|
-
inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
|
10660
|
-
])
|
10661
10717
|
|
10662
|
-
|
10718
|
+
########################################
|
10719
|
+
# ../deploy/venvs.py
|
10720
|
+
"""
|
10721
|
+
TODO:
|
10722
|
+
- interp
|
10723
|
+
- share more code with pyproject?
|
10724
|
+
"""
|
10663
10725
|
|
10664
|
-
lst.extend([
|
10665
|
-
bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
|
10666
|
-
])
|
10667
10726
|
|
10668
|
-
|
10727
|
+
class DeployVenvManager:
|
10728
|
+
async def setup_venv(
|
10729
|
+
self,
|
10730
|
+
spec: DeployVenvSpec,
|
10731
|
+
home: DeployHome,
|
10732
|
+
git_dir: str,
|
10733
|
+
venv_dir: str,
|
10734
|
+
) -> None:
|
10735
|
+
if spec.interp is not None:
|
10736
|
+
i = InterpSpecifier.parse(check.not_none(spec.interp))
|
10737
|
+
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i))
|
10738
|
+
sys_exe = o.exe
|
10739
|
+
else:
|
10740
|
+
sys_exe = 'python3'
|
10669
10741
|
|
10670
|
-
|
10742
|
+
#
|
10743
|
+
|
10744
|
+
# !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
|
10745
|
+
# garbage collect orphaned dirs.
|
10746
|
+
await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
|
10671
10747
|
|
10748
|
+
#
|
10672
10749
|
|
10673
|
-
|
10674
|
-
# ../targets/connection.py
|
10750
|
+
venv_exe = os.path.join(venv_dir, 'bin', 'python3')
|
10675
10751
|
|
10752
|
+
#
|
10676
10753
|
|
10677
|
-
|
10754
|
+
reqs_txt = os.path.join(git_dir, 'requirements.txt')
|
10678
10755
|
|
10756
|
+
if os.path.isfile(reqs_txt):
|
10757
|
+
if spec.use_uv:
|
10758
|
+
await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
|
10759
|
+
pip_cmd = ['-m', 'uv', 'pip']
|
10760
|
+
else:
|
10761
|
+
pip_cmd = ['-m', 'pip']
|
10679
10762
|
|
10680
|
-
|
10681
|
-
@abc.abstractmethod
|
10682
|
-
def connect(self, tgt: ManageTarget) -> ta.AsyncContextManager[CommandExecutor]:
|
10683
|
-
raise NotImplementedError
|
10763
|
+
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
10684
10764
|
|
10685
|
-
def _default_python(self, python: ta.Optional[ta.Sequence[str]]) -> ta.Sequence[str]:
|
10686
|
-
check.not_isinstance(python, str)
|
10687
|
-
if python is not None:
|
10688
|
-
return python
|
10689
|
-
else:
|
10690
|
-
return ['sh', '-c', get_best_python_sh(), '--']
|
10691
10765
|
|
10766
|
+
########################################
|
10767
|
+
# ../deploy/apps.py
|
10692
10768
|
|
10693
|
-
##
|
10694
10769
|
|
10770
|
+
class DeployAppManager(DeployPathOwner):
|
10771
|
+
def __init__(
|
10772
|
+
self,
|
10773
|
+
*,
|
10774
|
+
conf: DeployConfManager,
|
10775
|
+
git: DeployGitManager,
|
10776
|
+
venvs: DeployVenvManager,
|
10777
|
+
) -> None:
|
10778
|
+
super().__init__()
|
10695
10779
|
|
10696
|
-
|
10780
|
+
self._conf = conf
|
10781
|
+
self._git = git
|
10782
|
+
self._venvs = venvs
|
10697
10783
|
|
10784
|
+
#
|
10698
10785
|
|
10699
|
-
|
10700
|
-
|
10701
|
-
connectors: ManageTargetConnectorMap
|
10786
|
+
_APP_DIR_STR = 'apps/@app/@time--@app-rev--@app-key/'
|
10787
|
+
_APP_DIR = DeployPath.parse(_APP_DIR_STR)
|
10702
10788
|
|
10703
|
-
|
10704
|
-
|
10705
|
-
if issubclass(ty, k):
|
10706
|
-
return v
|
10707
|
-
raise KeyError(ty)
|
10789
|
+
_DEPLOY_DIR_STR = 'deploys/@time--@deploy-key/'
|
10790
|
+
_DEPLOY_DIR = DeployPath.parse(_DEPLOY_DIR_STR)
|
10708
10791
|
|
10709
|
-
|
10710
|
-
|
10792
|
+
_APP_DEPLOY_LINK = DeployPath.parse(f'{_DEPLOY_DIR_STR}apps/@app')
|
10793
|
+
_CONF_DEPLOY_DIR = DeployPath.parse(f'{_DEPLOY_DIR_STR}conf/@conf/')
|
10711
10794
|
|
10795
|
+
@cached_nullary
|
10796
|
+
def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
|
10797
|
+
return {
|
10798
|
+
self._APP_DIR,
|
10712
10799
|
|
10713
|
-
|
10800
|
+
self._DEPLOY_DIR,
|
10714
10801
|
|
10802
|
+
self._APP_DEPLOY_LINK,
|
10803
|
+
self._CONF_DEPLOY_DIR,
|
10715
10804
|
|
10716
|
-
|
10717
|
-
|
10718
|
-
|
10719
|
-
|
10720
|
-
|
10721
|
-
|
10805
|
+
*[
|
10806
|
+
DeployPath.parse(f'{self._APP_DIR_STR}{sfx}/')
|
10807
|
+
for sfx in [
|
10808
|
+
'conf',
|
10809
|
+
'git',
|
10810
|
+
'venv',
|
10811
|
+
]
|
10812
|
+
],
|
10813
|
+
}
|
10722
10814
|
|
10723
|
-
|
10724
|
-
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10725
|
-
lmt = check.isinstance(tgt, LocalManageTarget)
|
10815
|
+
#
|
10726
10816
|
|
10727
|
-
|
10728
|
-
|
10817
|
+
async def prepare_app(
|
10818
|
+
self,
|
10819
|
+
spec: DeployAppSpec,
|
10820
|
+
home: DeployHome,
|
10821
|
+
tags: DeployTagMap,
|
10822
|
+
) -> None:
|
10823
|
+
check.non_empty_str(home)
|
10729
10824
|
|
10730
|
-
|
10731
|
-
|
10825
|
+
def build_path(pth: DeployPath) -> str:
|
10826
|
+
return os.path.join(home, pth.render(tags))
|
10732
10827
|
|
10733
|
-
|
10734
|
-
|
10735
|
-
|
10828
|
+
app_dir = build_path(self._APP_DIR)
|
10829
|
+
deploy_dir = build_path(self._DEPLOY_DIR)
|
10830
|
+
app_deploy_link = build_path(self._APP_DEPLOY_LINK)
|
10736
10831
|
|
10737
|
-
|
10738
|
-
raise TypeError(imt.mode)
|
10832
|
+
#
|
10739
10833
|
|
10740
|
-
|
10741
|
-
async with self._pyremote_connector.connect(
|
10742
|
-
RemoteSpawning.Target(
|
10743
|
-
python=self._default_python(lmt.python),
|
10744
|
-
),
|
10745
|
-
self._bootstrap,
|
10746
|
-
) as rce:
|
10747
|
-
yield rce
|
10834
|
+
os.makedirs(deploy_dir, exist_ok=True)
|
10748
10835
|
|
10749
|
-
|
10750
|
-
|
10836
|
+
deploying_link = os.path.join(home, 'deploys/deploying')
|
10837
|
+
if os.path.exists(deploying_link):
|
10838
|
+
os.unlink(deploying_link)
|
10839
|
+
relative_symlink(
|
10840
|
+
deploy_dir,
|
10841
|
+
deploying_link,
|
10842
|
+
target_is_directory=True,
|
10843
|
+
make_dirs=True,
|
10844
|
+
)
|
10751
10845
|
|
10846
|
+
#
|
10752
10847
|
|
10753
|
-
|
10848
|
+
os.makedirs(app_dir)
|
10849
|
+
relative_symlink(
|
10850
|
+
app_dir,
|
10851
|
+
app_deploy_link,
|
10852
|
+
target_is_directory=True,
|
10853
|
+
make_dirs=True,
|
10854
|
+
)
|
10754
10855
|
|
10856
|
+
#
|
10755
10857
|
|
10756
|
-
|
10757
|
-
|
10758
|
-
|
10759
|
-
|
10858
|
+
deploy_conf_dir = os.path.join(deploy_dir, 'conf')
|
10859
|
+
os.makedirs(deploy_conf_dir, exist_ok=True)
|
10860
|
+
|
10861
|
+
#
|
10862
|
+
|
10863
|
+
# def mirror_symlinks(src: str, dst: str) -> None:
|
10864
|
+
# def mirror_link(lp: str) -> None:
|
10865
|
+
# check.state(os.path.islink(lp))
|
10866
|
+
# shutil.copy2(
|
10867
|
+
# lp,
|
10868
|
+
# os.path.join(dst, os.path.relpath(lp, src)),
|
10869
|
+
# follow_symlinks=False,
|
10870
|
+
# )
|
10871
|
+
#
|
10872
|
+
# for dp, dns, fns in os.walk(src, followlinks=False):
|
10873
|
+
# for fn in fns:
|
10874
|
+
# mirror_link(os.path.join(dp, fn))
|
10875
|
+
#
|
10876
|
+
# for dn in dns:
|
10877
|
+
# dp2 = os.path.join(dp, dn)
|
10878
|
+
# if os.path.islink(dp2):
|
10879
|
+
# mirror_link(dp2)
|
10880
|
+
# else:
|
10881
|
+
# os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
|
10882
|
+
|
10883
|
+
current_link = os.path.join(home, 'deploys/current')
|
10884
|
+
|
10885
|
+
# if os.path.exists(current_link):
|
10886
|
+
# mirror_symlinks(
|
10887
|
+
# os.path.join(current_link, 'conf'),
|
10888
|
+
# conf_tag_dir,
|
10889
|
+
# )
|
10890
|
+
# mirror_symlinks(
|
10891
|
+
# os.path.join(current_link, 'apps'),
|
10892
|
+
# os.path.join(deploy_dir, 'apps'),
|
10893
|
+
# )
|
10760
10894
|
|
10761
|
-
|
10762
|
-
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10763
|
-
dmt = check.isinstance(tgt, DockerManageTarget)
|
10895
|
+
#
|
10764
10896
|
|
10765
|
-
|
10766
|
-
|
10767
|
-
|
10768
|
-
|
10769
|
-
|
10770
|
-
|
10771
|
-
raise ValueError(dmt)
|
10897
|
+
app_git_dir = os.path.join(app_dir, 'git')
|
10898
|
+
await self._git.checkout(
|
10899
|
+
spec.git,
|
10900
|
+
home,
|
10901
|
+
app_git_dir,
|
10902
|
+
)
|
10772
10903
|
|
10773
|
-
|
10774
|
-
RemoteSpawning.Target(
|
10775
|
-
shell=' '.join(sh_parts),
|
10776
|
-
python=self._default_python(dmt.python),
|
10777
|
-
),
|
10778
|
-
self._bootstrap,
|
10779
|
-
) as rce:
|
10780
|
-
yield rce
|
10904
|
+
#
|
10781
10905
|
|
10906
|
+
if spec.venv is not None:
|
10907
|
+
app_venv_dir = os.path.join(app_dir, 'venv')
|
10908
|
+
await self._venvs.setup_venv(
|
10909
|
+
spec.venv,
|
10910
|
+
home,
|
10911
|
+
app_git_dir,
|
10912
|
+
app_venv_dir,
|
10913
|
+
)
|
10782
10914
|
|
10783
|
-
|
10915
|
+
#
|
10784
10916
|
|
10917
|
+
if spec.conf is not None:
|
10918
|
+
app_conf_dir = os.path.join(app_dir, 'conf')
|
10919
|
+
await self._conf.write_app_conf(
|
10920
|
+
spec.conf,
|
10921
|
+
tags,
|
10922
|
+
app_conf_dir,
|
10923
|
+
deploy_conf_dir,
|
10924
|
+
)
|
10785
10925
|
|
10786
|
-
|
10787
|
-
class SshManageTargetConnector(ManageTargetConnector):
|
10788
|
-
_pyremote_connector: PyremoteRemoteExecutionConnector
|
10789
|
-
_bootstrap: MainBootstrap
|
10926
|
+
#
|
10790
10927
|
|
10791
|
-
|
10792
|
-
async def connect(self, tgt: ManageTarget) -> ta.AsyncGenerator[CommandExecutor, None]:
|
10793
|
-
smt = check.isinstance(tgt, SshManageTarget)
|
10928
|
+
os.replace(deploying_link, current_link)
|
10794
10929
|
|
10795
|
-
sh_parts: ta.List[str] = ['ssh']
|
10796
|
-
if smt.key_file is not None:
|
10797
|
-
sh_parts.extend(['-i', smt.key_file])
|
10798
|
-
addr = check.not_none(smt.host)
|
10799
|
-
if smt.username is not None:
|
10800
|
-
addr = f'{smt.username}@{addr}'
|
10801
|
-
sh_parts.append(addr)
|
10802
10930
|
|
10803
|
-
|
10804
|
-
|
10805
|
-
shell=' '.join(sh_parts),
|
10806
|
-
shell_quote=True,
|
10807
|
-
python=self._default_python(smt.python),
|
10808
|
-
),
|
10809
|
-
self._bootstrap,
|
10810
|
-
) as rce:
|
10811
|
-
yield rce
|
10931
|
+
########################################
|
10932
|
+
# ../deploy/deploy.py
|
10812
10933
|
|
10813
10934
|
|
10814
|
-
|
10815
|
-
# ../../../omdev/interp/resolvers.py
|
10935
|
+
DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
|
10816
10936
|
|
10817
10937
|
|
10818
|
-
|
10819
|
-
cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
|
10820
|
-
}
|
10938
|
+
DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
|
10821
10939
|
|
10822
10940
|
|
10823
|
-
class
|
10941
|
+
class DeployManager:
|
10824
10942
|
def __init__(
|
10825
10943
|
self,
|
10826
|
-
|
10827
|
-
|
10944
|
+
*,
|
10945
|
+
apps: DeployAppManager,
|
10946
|
+
paths: DeployPathsManager,
|
10947
|
+
|
10948
|
+
utc_clock: ta.Optional[DeployManagerUtcClock] = None,
|
10949
|
+
):
|
10828
10950
|
super().__init__()
|
10829
10951
|
|
10830
|
-
self.
|
10952
|
+
self._apps = apps
|
10953
|
+
self._paths = paths
|
10831
10954
|
|
10832
|
-
|
10833
|
-
lst = [
|
10834
|
-
(i, si)
|
10835
|
-
for i, p in enumerate(self._providers.values())
|
10836
|
-
for si in await p.get_installed_versions(spec)
|
10837
|
-
if spec.contains(si)
|
10838
|
-
]
|
10955
|
+
self._utc_clock = utc_clock
|
10839
10956
|
|
10840
|
-
|
10841
|
-
if not
|
10842
|
-
return
|
10957
|
+
def _utc_now(self) -> datetime.datetime:
|
10958
|
+
if self._utc_clock is not None:
|
10959
|
+
return self._utc_clock() # noqa
|
10960
|
+
else:
|
10961
|
+
return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
|
10843
10962
|
|
10844
|
-
|
10845
|
-
|
10846
|
-
return (bp, bv)
|
10963
|
+
def _make_deploy_time(self) -> DeployTime:
|
10964
|
+
return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
|
10847
10965
|
|
10848
|
-
async def
|
10966
|
+
async def run_deploy(
|
10849
10967
|
self,
|
10850
|
-
spec:
|
10851
|
-
|
10852
|
-
|
10853
|
-
) -> ta.Optional[Interp]:
|
10854
|
-
tup = await self._resolve_installed(spec)
|
10855
|
-
if tup is not None:
|
10856
|
-
bp, bv = tup
|
10857
|
-
return await bp.get_installed_version(bv)
|
10858
|
-
|
10859
|
-
if not install:
|
10860
|
-
return None
|
10861
|
-
|
10862
|
-
tp = list(self._providers.values())[0] # noqa
|
10863
|
-
|
10864
|
-
sv = sorted(
|
10865
|
-
[s for s in await tp.get_installable_versions(spec) if s in spec],
|
10866
|
-
key=lambda s: s.version,
|
10867
|
-
)
|
10868
|
-
if not sv:
|
10869
|
-
return None
|
10968
|
+
spec: DeploySpec,
|
10969
|
+
) -> None:
|
10970
|
+
self._paths.validate_deploy_paths()
|
10870
10971
|
|
10871
|
-
|
10872
|
-
return await tp.install_version(bv)
|
10972
|
+
#
|
10873
10973
|
|
10874
|
-
|
10875
|
-
|
10876
|
-
|
10877
|
-
|
10878
|
-
si
|
10879
|
-
for si in await p.get_installed_versions(spec)
|
10880
|
-
if spec.contains(si)
|
10881
|
-
]
|
10882
|
-
if lst:
|
10883
|
-
print(f' {n}')
|
10884
|
-
for si in lst:
|
10885
|
-
print(f' {si}')
|
10974
|
+
hs = check.non_empty_str(spec.home)
|
10975
|
+
hs = os.path.expanduser(hs)
|
10976
|
+
hs = os.path.realpath(hs)
|
10977
|
+
hs = os.path.abspath(hs)
|
10886
10978
|
|
10887
|
-
|
10979
|
+
home = DeployHome(hs)
|
10888
10980
|
|
10889
|
-
|
10890
|
-
for n, p in self._providers.items():
|
10891
|
-
lst = [
|
10892
|
-
si
|
10893
|
-
for si in await p.get_installable_versions(spec)
|
10894
|
-
if spec.contains(si)
|
10895
|
-
]
|
10896
|
-
if lst:
|
10897
|
-
print(f' {n}')
|
10898
|
-
for si in lst:
|
10899
|
-
print(f' {si}')
|
10981
|
+
#
|
10900
10982
|
|
10983
|
+
deploy_tags = DeployTagMap(
|
10984
|
+
self._make_deploy_time(),
|
10985
|
+
spec.key(),
|
10986
|
+
)
|
10901
10987
|
|
10902
|
-
|
10903
|
-
# pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
|
10904
|
-
PyenvInterpProvider(try_update=True),
|
10988
|
+
#
|
10905
10989
|
|
10906
|
-
|
10990
|
+
for app in spec.apps:
|
10991
|
+
app_tags = deploy_tags.add(
|
10992
|
+
app.app,
|
10993
|
+
app.key(),
|
10994
|
+
DeployAppRev(app.git.rev),
|
10995
|
+
)
|
10907
10996
|
|
10908
|
-
|
10909
|
-
|
10997
|
+
await self._apps.prepare_app(
|
10998
|
+
app,
|
10999
|
+
home,
|
11000
|
+
app_tags,
|
11001
|
+
)
|
10910
11002
|
|
10911
11003
|
|
10912
11004
|
########################################
|
@@ -10937,65 +11029,6 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
|
|
10937
11029
|
return DeployCommand.Output()
|
10938
11030
|
|
10939
11031
|
|
10940
|
-
########################################
|
10941
|
-
# ../targets/inject.py
|
10942
|
-
|
10943
|
-
|
10944
|
-
def bind_targets() -> InjectorBindings:
|
10945
|
-
lst: ta.List[InjectorBindingOrBindings] = [
|
10946
|
-
inj.bind(LocalManageTargetConnector, singleton=True),
|
10947
|
-
inj.bind(DockerManageTargetConnector, singleton=True),
|
10948
|
-
inj.bind(SshManageTargetConnector, singleton=True),
|
10949
|
-
|
10950
|
-
inj.bind(TypeSwitchedManageTargetConnector, singleton=True),
|
10951
|
-
inj.bind(ManageTargetConnector, to_key=TypeSwitchedManageTargetConnector),
|
10952
|
-
]
|
10953
|
-
|
10954
|
-
#
|
10955
|
-
|
10956
|
-
def provide_manage_target_connector_map(injector: Injector) -> ManageTargetConnectorMap:
|
10957
|
-
return ManageTargetConnectorMap({
|
10958
|
-
LocalManageTarget: injector[LocalManageTargetConnector],
|
10959
|
-
DockerManageTarget: injector[DockerManageTargetConnector],
|
10960
|
-
SshManageTarget: injector[SshManageTargetConnector],
|
10961
|
-
})
|
10962
|
-
lst.append(inj.bind(provide_manage_target_connector_map, singleton=True))
|
10963
|
-
|
10964
|
-
#
|
10965
|
-
|
10966
|
-
return inj.as_bindings(*lst)
|
10967
|
-
|
10968
|
-
|
10969
|
-
########################################
|
10970
|
-
# ../deploy/interp.py
|
10971
|
-
|
10972
|
-
|
10973
|
-
##
|
10974
|
-
|
10975
|
-
|
10976
|
-
@dc.dataclass(frozen=True)
|
10977
|
-
class InterpCommand(Command['InterpCommand.Output']):
|
10978
|
-
spec: str
|
10979
|
-
install: bool = False
|
10980
|
-
|
10981
|
-
@dc.dataclass(frozen=True)
|
10982
|
-
class Output(Command.Output):
|
10983
|
-
exe: str
|
10984
|
-
version: str
|
10985
|
-
opts: InterpOpts
|
10986
|
-
|
10987
|
-
|
10988
|
-
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
10989
|
-
async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
10990
|
-
i = InterpSpecifier.parse(check.not_none(cmd.spec))
|
10991
|
-
o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
|
10992
|
-
return InterpCommand.Output(
|
10993
|
-
exe=o.exe,
|
10994
|
-
version=str(o.version.version),
|
10995
|
-
opts=o.version.opts,
|
10996
|
-
)
|
10997
|
-
|
10998
|
-
|
10999
11032
|
########################################
|
11000
11033
|
# ../deploy/inject.py
|
11001
11034
|
|
@@ -11031,13 +11064,18 @@ def bind_deploy(
|
|
11031
11064
|
bind_manager(DeployManager),
|
11032
11065
|
|
11033
11066
|
bind_manager(DeployTmpManager),
|
11034
|
-
inj.bind(AtomicPathSwapping, to_key=DeployTmpManager),
|
11035
11067
|
|
11036
11068
|
bind_manager(DeployVenvManager),
|
11037
11069
|
])
|
11038
11070
|
|
11039
11071
|
#
|
11040
11072
|
|
11073
|
+
def provide_deploy_home_atomics(tmp: DeployTmpManager) -> DeployHomeAtomics:
|
11074
|
+
return DeployHomeAtomics(tmp.get_swapping)
|
11075
|
+
lst.append(inj.bind(provide_deploy_home_atomics, singleton=True))
|
11076
|
+
|
11077
|
+
#
|
11078
|
+
|
11041
11079
|
lst.extend([
|
11042
11080
|
bind_command(DeployCommand, DeployCommandExecutor),
|
11043
11081
|
bind_command(InterpCommand, InterpCommandExecutor),
|
@@ -11045,10 +11083,6 @@ def bind_deploy(
|
|
11045
11083
|
|
11046
11084
|
#
|
11047
11085
|
|
11048
|
-
if (dh := deploy_config.deploy_home) is not None:
|
11049
|
-
dh = os.path.abspath(os.path.expanduser(dh))
|
11050
|
-
lst.append(inj.bind(dh, key=DeployHome))
|
11051
|
-
|
11052
11086
|
return inj.as_bindings(*lst)
|
11053
11087
|
|
11054
11088
|
|
@@ -11175,10 +11209,9 @@ class MainCli(ArgparseCli):
|
|
11175
11209
|
|
11176
11210
|
argparse_arg('--debug', action='store_true'),
|
11177
11211
|
|
11178
|
-
argparse_arg('--deploy-home'),
|
11179
|
-
|
11180
11212
|
argparse_arg('target'),
|
11181
|
-
argparse_arg('command',
|
11213
|
+
argparse_arg('-f', '--command-file', action='append'),
|
11214
|
+
argparse_arg('command', nargs='*'),
|
11182
11215
|
)
|
11183
11216
|
async def run(self) -> None:
|
11184
11217
|
bs = MainBootstrap(
|
@@ -11188,9 +11221,7 @@ class MainCli(ArgparseCli):
|
|
11188
11221
|
debug=bool(self.args.debug),
|
11189
11222
|
),
|
11190
11223
|
|
11191
|
-
deploy_config=DeployConfig(
|
11192
|
-
deploy_home=self.args.deploy_home,
|
11193
|
-
),
|
11224
|
+
deploy_config=DeployConfig(),
|
11194
11225
|
|
11195
11226
|
remote_config=RemoteConfig(
|
11196
11227
|
payload_file=self.args._payload_file, # noqa
|
@@ -11224,13 +11255,19 @@ class MainCli(ArgparseCli):
|
|
11224
11255
|
#
|
11225
11256
|
|
11226
11257
|
cmds: ta.List[Command] = []
|
11258
|
+
|
11227
11259
|
cmd: Command
|
11228
|
-
|
11260
|
+
|
11261
|
+
for c in self.args.command or []:
|
11229
11262
|
if not c.startswith('{'):
|
11230
11263
|
c = json.dumps({c: {}})
|
11231
11264
|
cmd = msh.unmarshal_obj(json.loads(c), Command)
|
11232
11265
|
cmds.append(cmd)
|
11233
11266
|
|
11267
|
+
for cf in self.args.command_file or []:
|
11268
|
+
cmd = read_config_file(cf, Command, msh=msh)
|
11269
|
+
cmds.append(cmd)
|
11270
|
+
|
11234
11271
|
#
|
11235
11272
|
|
11236
11273
|
async with injector[ManageTargetConnector].connect(tgt) as ce:
|