ominfra 0.0.0.dev159__py3-none-any.whl → 0.0.0.dev161__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ominfra/scripts/manage.py CHANGED
@@ -100,10 +100,6 @@ CallableVersionOperator = ta.Callable[['Version', str], bool]
100
100
  CommandT = ta.TypeVar('CommandT', bound='Command')
101
101
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
102
102
 
103
- # deploy/atomics.py
104
- DeployAtomicPathSwapKind = ta.Literal['dir', 'file']
105
- DeployAtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
106
-
107
103
  # deploy/paths.py
108
104
  DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
109
105
  DeployPathPlaceholder = ta.Literal['app', 'tag'] # ta.TypeAlias
@@ -121,6 +117,10 @@ InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
121
117
  InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
122
118
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
123
119
 
120
+ # ../../omlish/os/atomics.py
121
+ AtomicPathSwapKind = ta.Literal['dir', 'file']
122
+ AtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
123
+
124
124
  # ../configs.py
125
125
  ConfigMapping = ta.Mapping[str, ta.Any]
126
126
 
@@ -2692,6 +2692,10 @@ def is_new_type(spec: ta.Any) -> bool:
2692
2692
  return isinstance(spec, types.FunctionType) and spec.__code__ is ta.NewType.__code__.co_consts[1] # type: ignore # noqa
2693
2693
 
2694
2694
 
2695
+ def get_new_type_supertype(spec: ta.Any) -> ta.Any:
2696
+ return spec.__supertype__
2697
+
2698
+
2695
2699
  def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
2696
2700
  seen = set()
2697
2701
  todo = list(reversed(cls.__subclasses__()))
@@ -4060,242 +4064,15 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
4060
4064
  return CommandNameMap(dct)
4061
4065
 
4062
4066
 
4063
- ########################################
4064
- # ../deploy/atomics.py
4065
-
4066
-
4067
- ##
4068
-
4069
-
4070
- class DeployAtomicPathSwap(abc.ABC):
4071
- def __init__(
4072
- self,
4073
- kind: DeployAtomicPathSwapKind,
4074
- dst_path: str,
4075
- *,
4076
- auto_commit: bool = False,
4077
- ) -> None:
4078
- super().__init__()
4079
-
4080
- self._kind = kind
4081
- self._dst_path = dst_path
4082
- self._auto_commit = auto_commit
4083
-
4084
- self._state: DeployAtomicPathSwapState = 'open'
4085
-
4086
- def __repr__(self) -> str:
4087
- return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
4088
-
4089
- @property
4090
- def kind(self) -> DeployAtomicPathSwapKind:
4091
- return self._kind
4092
-
4093
- @property
4094
- def dst_path(self) -> str:
4095
- return self._dst_path
4096
-
4097
- @property
4098
- @abc.abstractmethod
4099
- def tmp_path(self) -> str:
4100
- raise NotImplementedError
4101
-
4102
- #
4103
-
4104
- @property
4105
- def state(self) -> DeployAtomicPathSwapState:
4106
- return self._state
4107
-
4108
- def _check_state(self, *states: DeployAtomicPathSwapState) -> None:
4109
- if self._state not in states:
4110
- raise RuntimeError(f'Atomic path swap not in correct state: {self._state}, {states}')
4111
-
4112
- #
4113
-
4114
- @abc.abstractmethod
4115
- def _commit(self) -> None:
4116
- raise NotImplementedError
4117
-
4118
- def commit(self) -> None:
4119
- if self._state == 'committed':
4120
- return
4121
- self._check_state('open')
4122
- try:
4123
- self._commit()
4124
- except Exception: # noqa
4125
- self._abort()
4126
- raise
4127
- else:
4128
- self._state = 'committed'
4129
-
4130
- #
4131
-
4132
- @abc.abstractmethod
4133
- def _abort(self) -> None:
4134
- raise NotImplementedError
4135
-
4136
- def abort(self) -> None:
4137
- if self._state == 'aborted':
4138
- return
4139
- self._abort()
4140
- self._state = 'aborted'
4141
-
4142
- #
4143
-
4144
- def __enter__(self) -> 'DeployAtomicPathSwap':
4145
- return self
4146
-
4147
- def __exit__(self, exc_type, exc_val, exc_tb):
4148
- if (
4149
- exc_type is None and
4150
- self._auto_commit and
4151
- self._state == 'open'
4152
- ):
4153
- self.commit()
4154
- else:
4155
- self.abort()
4156
-
4157
-
4158
- #
4159
-
4160
-
4161
- class DeployAtomicPathSwapping(abc.ABC):
4162
- @abc.abstractmethod
4163
- def begin_atomic_path_swap(
4164
- self,
4165
- kind: DeployAtomicPathSwapKind,
4166
- dst_path: str,
4167
- *,
4168
- name_hint: ta.Optional[str] = None,
4169
- make_dirs: bool = False,
4170
- **kwargs: ta.Any,
4171
- ) -> DeployAtomicPathSwap:
4172
- raise NotImplementedError
4173
-
4174
-
4175
- ##
4176
-
4177
-
4178
- class OsRenameDeployAtomicPathSwap(DeployAtomicPathSwap):
4179
- def __init__(
4180
- self,
4181
- kind: DeployAtomicPathSwapKind,
4182
- dst_path: str,
4183
- tmp_path: str,
4184
- **kwargs: ta.Any,
4185
- ) -> None:
4186
- if kind == 'dir':
4187
- check.state(os.path.isdir(tmp_path))
4188
- elif kind == 'file':
4189
- check.state(os.path.isfile(tmp_path))
4190
- else:
4191
- raise TypeError(kind)
4192
-
4193
- super().__init__(
4194
- kind,
4195
- dst_path,
4196
- **kwargs,
4197
- )
4198
-
4199
- self._tmp_path = tmp_path
4200
-
4201
- @property
4202
- def tmp_path(self) -> str:
4203
- return self._tmp_path
4204
-
4205
- def _commit(self) -> None:
4206
- os.rename(self._tmp_path, self._dst_path)
4207
-
4208
- def _abort(self) -> None:
4209
- shutil.rmtree(self._tmp_path, ignore_errors=True)
4210
-
4211
-
4212
- class TempDirDeployAtomicPathSwapping(DeployAtomicPathSwapping):
4213
- def __init__(
4214
- self,
4215
- *,
4216
- temp_dir: ta.Optional[str] = None,
4217
- root_dir: ta.Optional[str] = None,
4218
- ) -> None:
4219
- super().__init__()
4220
-
4221
- if root_dir is not None:
4222
- root_dir = os.path.abspath(root_dir)
4223
- self._root_dir = root_dir
4224
- self._temp_dir = temp_dir
4225
-
4226
- def begin_atomic_path_swap(
4227
- self,
4228
- kind: DeployAtomicPathSwapKind,
4229
- dst_path: str,
4230
- *,
4231
- name_hint: ta.Optional[str] = None,
4232
- make_dirs: bool = False,
4233
- **kwargs: ta.Any,
4234
- ) -> DeployAtomicPathSwap:
4235
- dst_path = os.path.abspath(dst_path)
4236
- if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
4237
- raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
4238
-
4239
- dst_dir = os.path.dirname(dst_path)
4240
- if make_dirs:
4241
- os.makedirs(dst_dir, exist_ok=True)
4242
- if not os.path.isdir(dst_dir):
4243
- raise RuntimeError(f'Atomic path swap dst dir does not exist: {dst_dir}')
4244
-
4245
- if kind == 'dir':
4246
- tmp_path = tempfile.mkdtemp(prefix=name_hint, dir=self._temp_dir)
4247
- elif kind == 'file':
4248
- fd, tmp_path = tempfile.mkstemp(prefix=name_hint, dir=self._temp_dir)
4249
- os.close(fd)
4250
- else:
4251
- raise TypeError(kind)
4252
-
4253
- return OsRenameDeployAtomicPathSwap(
4254
- kind,
4255
- dst_path,
4256
- tmp_path,
4257
- **kwargs,
4258
- )
4259
-
4260
-
4261
4067
  ########################################
4262
4068
  # ../deploy/paths.py
4263
4069
  """
4264
- ~deploy
4265
- deploy.pid (flock)
4266
- /app
4267
- /<appplaceholder> - shallow clone
4268
- /conf
4269
- /env
4270
- <appplaceholder>.env
4271
- /nginx
4272
- <appplaceholder>.conf
4273
- /supervisor
4274
- <appplaceholder>.conf
4275
- /venv
4276
- /<appplaceholder>
4277
-
4278
- /tmp
4279
-
4280
- ?
4281
- /logs
4282
- /wrmsr--omlish--<placeholder>
4283
-
4284
- placeholder = <name>--<rev>--<when>
4285
-
4286
- ==
4287
-
4288
- for dn in [
4289
- 'app',
4290
- 'conf',
4291
- 'conf/env',
4292
- 'conf/nginx',
4293
- 'conf/supervisor',
4294
- 'venv',
4295
- ]:
4296
-
4297
- ==
4298
-
4070
+ TODO:
4071
+ - run/pidfile
4072
+ - logs/...
4073
+ - current symlink
4074
+ - conf/{nginx,supervisor}
4075
+ - env/?
4299
4076
  """
4300
4077
 
4301
4078
 
@@ -4307,7 +4084,7 @@ DEPLOY_PATH_PLACEHOLDER_SEPARATORS = '-.'
4307
4084
 
4308
4085
  DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
4309
4086
  'app',
4310
- 'tag', # <rev>-<dt>
4087
+ 'tag',
4311
4088
  ])
4312
4089
 
4313
4090
 
@@ -4534,14 +4311,28 @@ class DeployGitRepo:
4534
4311
  check.not_in('.', check.non_empty_str(self.path))
4535
4312
 
4536
4313
 
4314
+ @dc.dataclass(frozen=True)
4315
+ class DeployGitCheckout:
4316
+ repo: DeployGitRepo
4317
+ rev: DeployRev
4318
+
4319
+ subtrees: ta.Optional[ta.Sequence[str]] = None
4320
+
4321
+ def __post_init__(self) -> None:
4322
+ hash(self)
4323
+ check.non_empty_str(self.rev)
4324
+ if self.subtrees is not None:
4325
+ for st in self.subtrees:
4326
+ check.non_empty_str(st)
4327
+
4328
+
4537
4329
  ##
4538
4330
 
4539
4331
 
4540
4332
  @dc.dataclass(frozen=True)
4541
4333
  class DeploySpec:
4542
4334
  app: DeployApp
4543
- repo: DeployGitRepo
4544
- rev: DeployRev
4335
+ checkout: DeployGitCheckout
4545
4336
 
4546
4337
  def __post_init__(self) -> None:
4547
4338
  hash(self)
@@ -6008,9 +5799,7 @@ inj = Injection
6008
5799
  """
6009
5800
  TODO:
6010
5801
  - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
6011
- - namedtuple
6012
5802
  - literals
6013
- - newtypes?
6014
5803
  """
6015
5804
 
6016
5805
 
@@ -6020,7 +5809,7 @@ TODO:
6020
5809
  @dc.dataclass(frozen=True)
6021
5810
  class ObjMarshalOptions:
6022
5811
  raw_bytes: bool = False
6023
- nonstrict_dataclasses: bool = False
5812
+ non_strict_fields: bool = False
6024
5813
 
6025
5814
 
6026
5815
  class ObjMarshaler(abc.ABC):
@@ -6149,10 +5938,10 @@ class IterableObjMarshaler(ObjMarshaler):
6149
5938
 
6150
5939
 
6151
5940
  @dc.dataclass(frozen=True)
6152
- class DataclassObjMarshaler(ObjMarshaler):
5941
+ class FieldsObjMarshaler(ObjMarshaler):
6153
5942
  ty: type
6154
5943
  fs: ta.Mapping[str, ObjMarshaler]
6155
- nonstrict: bool = False
5944
+ non_strict: bool = False
6156
5945
 
6157
5946
  def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6158
5947
  return {
@@ -6164,7 +5953,7 @@ class DataclassObjMarshaler(ObjMarshaler):
6164
5953
  return self.ty(**{
6165
5954
  k: self.fs[k].unmarshal(v, ctx)
6166
5955
  for k, v in o.items()
6167
- if not (self.nonstrict or ctx.options.nonstrict_dataclasses) or k in self.fs
5956
+ if not (self.non_strict or ctx.options.non_strict_fields) or k in self.fs
6168
5957
  })
6169
5958
 
6170
5959
 
@@ -6296,7 +6085,7 @@ class ObjMarshalerManager:
6296
6085
  ty: ta.Any,
6297
6086
  rec: ta.Callable[[ta.Any], ObjMarshaler],
6298
6087
  *,
6299
- nonstrict_dataclasses: bool = False,
6088
+ non_strict_fields: bool = False,
6300
6089
  ) -> ObjMarshaler:
6301
6090
  if isinstance(ty, type):
6302
6091
  if abc.ABC in ty.__bases__:
@@ -6318,12 +6107,22 @@ class ObjMarshalerManager:
6318
6107
  return EnumObjMarshaler(ty)
6319
6108
 
6320
6109
  if dc.is_dataclass(ty):
6321
- return DataclassObjMarshaler(
6110
+ return FieldsObjMarshaler(
6322
6111
  ty,
6323
6112
  {f.name: rec(f.type) for f in dc.fields(ty)},
6324
- nonstrict=nonstrict_dataclasses,
6113
+ non_strict=non_strict_fields,
6325
6114
  )
6326
6115
 
6116
+ if issubclass(ty, tuple) and hasattr(ty, '_fields'):
6117
+ return FieldsObjMarshaler(
6118
+ ty,
6119
+ {p.name: rec(p.annotation) for p in inspect.signature(ty).parameters.values()},
6120
+ non_strict=non_strict_fields,
6121
+ )
6122
+
6123
+ if is_new_type(ty):
6124
+ return rec(get_new_type_supertype(ty))
6125
+
6327
6126
  if is_generic_alias(ty):
6328
6127
  try:
6329
6128
  mt = self._generic_mapping_types[ta.get_origin(ty)]
@@ -6519,6 +6318,201 @@ class JsonLogFormatter(logging.Formatter):
6519
6318
  return self._json_dumps(dct)
6520
6319
 
6521
6320
 
6321
+ ########################################
6322
+ # ../../../omlish/os/atomics.py
6323
+
6324
+
6325
+ ##
6326
+
6327
+
6328
+ class AtomicPathSwap(abc.ABC):
6329
+ def __init__(
6330
+ self,
6331
+ kind: AtomicPathSwapKind,
6332
+ dst_path: str,
6333
+ *,
6334
+ auto_commit: bool = False,
6335
+ ) -> None:
6336
+ super().__init__()
6337
+
6338
+ self._kind = kind
6339
+ self._dst_path = dst_path
6340
+ self._auto_commit = auto_commit
6341
+
6342
+ self._state: AtomicPathSwapState = 'open'
6343
+
6344
+ def __repr__(self) -> str:
6345
+ return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
6346
+
6347
+ @property
6348
+ def kind(self) -> AtomicPathSwapKind:
6349
+ return self._kind
6350
+
6351
+ @property
6352
+ def dst_path(self) -> str:
6353
+ return self._dst_path
6354
+
6355
+ @property
6356
+ @abc.abstractmethod
6357
+ def tmp_path(self) -> str:
6358
+ raise NotImplementedError
6359
+
6360
+ #
6361
+
6362
+ @property
6363
+ def state(self) -> AtomicPathSwapState:
6364
+ return self._state
6365
+
6366
+ def _check_state(self, *states: AtomicPathSwapState) -> None:
6367
+ if self._state not in states:
6368
+ raise RuntimeError(f'Atomic path swap not in correct state: {self._state}, {states}')
6369
+
6370
+ #
6371
+
6372
+ @abc.abstractmethod
6373
+ def _commit(self) -> None:
6374
+ raise NotImplementedError
6375
+
6376
+ def commit(self) -> None:
6377
+ if self._state == 'committed':
6378
+ return
6379
+ self._check_state('open')
6380
+ try:
6381
+ self._commit()
6382
+ except Exception: # noqa
6383
+ self._abort()
6384
+ raise
6385
+ else:
6386
+ self._state = 'committed'
6387
+
6388
+ #
6389
+
6390
+ @abc.abstractmethod
6391
+ def _abort(self) -> None:
6392
+ raise NotImplementedError
6393
+
6394
+ def abort(self) -> None:
6395
+ if self._state == 'aborted':
6396
+ return
6397
+ self._abort()
6398
+ self._state = 'aborted'
6399
+
6400
+ #
6401
+
6402
+ def __enter__(self) -> 'AtomicPathSwap':
6403
+ return self
6404
+
6405
+ def __exit__(self, exc_type, exc_val, exc_tb):
6406
+ if (
6407
+ exc_type is None and
6408
+ self._auto_commit and
6409
+ self._state == 'open'
6410
+ ):
6411
+ self.commit()
6412
+ else:
6413
+ self.abort()
6414
+
6415
+
6416
+ class AtomicPathSwapping(abc.ABC):
6417
+ @abc.abstractmethod
6418
+ def begin_atomic_path_swap(
6419
+ self,
6420
+ kind: AtomicPathSwapKind,
6421
+ dst_path: str,
6422
+ *,
6423
+ name_hint: ta.Optional[str] = None,
6424
+ make_dirs: bool = False,
6425
+ **kwargs: ta.Any,
6426
+ ) -> AtomicPathSwap:
6427
+ raise NotImplementedError
6428
+
6429
+
6430
+ ##
6431
+
6432
+
6433
+ class OsRenameAtomicPathSwap(AtomicPathSwap):
6434
+ def __init__(
6435
+ self,
6436
+ kind: AtomicPathSwapKind,
6437
+ dst_path: str,
6438
+ tmp_path: str,
6439
+ **kwargs: ta.Any,
6440
+ ) -> None:
6441
+ if kind == 'dir':
6442
+ check.state(os.path.isdir(tmp_path))
6443
+ elif kind == 'file':
6444
+ check.state(os.path.isfile(tmp_path))
6445
+ else:
6446
+ raise TypeError(kind)
6447
+
6448
+ super().__init__(
6449
+ kind,
6450
+ dst_path,
6451
+ **kwargs,
6452
+ )
6453
+
6454
+ self._tmp_path = tmp_path
6455
+
6456
+ @property
6457
+ def tmp_path(self) -> str:
6458
+ return self._tmp_path
6459
+
6460
+ def _commit(self) -> None:
6461
+ os.rename(self._tmp_path, self._dst_path)
6462
+
6463
+ def _abort(self) -> None:
6464
+ shutil.rmtree(self._tmp_path, ignore_errors=True)
6465
+
6466
+
6467
+ class TempDirAtomicPathSwapping(AtomicPathSwapping):
6468
+ def __init__(
6469
+ self,
6470
+ *,
6471
+ temp_dir: ta.Optional[str] = None,
6472
+ root_dir: ta.Optional[str] = None,
6473
+ ) -> None:
6474
+ super().__init__()
6475
+
6476
+ if root_dir is not None:
6477
+ root_dir = os.path.abspath(root_dir)
6478
+ self._root_dir = root_dir
6479
+ self._temp_dir = temp_dir
6480
+
6481
+ def begin_atomic_path_swap(
6482
+ self,
6483
+ kind: AtomicPathSwapKind,
6484
+ dst_path: str,
6485
+ *,
6486
+ name_hint: ta.Optional[str] = None,
6487
+ make_dirs: bool = False,
6488
+ **kwargs: ta.Any,
6489
+ ) -> AtomicPathSwap:
6490
+ dst_path = os.path.abspath(dst_path)
6491
+ if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
6492
+ raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
6493
+
6494
+ dst_dir = os.path.dirname(dst_path)
6495
+ if make_dirs:
6496
+ os.makedirs(dst_dir, exist_ok=True)
6497
+ if not os.path.isdir(dst_dir):
6498
+ raise RuntimeError(f'Atomic path swap dst dir does not exist: {dst_dir}')
6499
+
6500
+ if kind == 'dir':
6501
+ tmp_path = tempfile.mkdtemp(prefix=name_hint, dir=self._temp_dir)
6502
+ elif kind == 'file':
6503
+ fd, tmp_path = tempfile.mkstemp(prefix=name_hint, dir=self._temp_dir)
6504
+ os.close(fd)
6505
+ else:
6506
+ raise TypeError(kind)
6507
+
6508
+ return OsRenameAtomicPathSwap(
6509
+ kind,
6510
+ dst_path,
6511
+ tmp_path,
6512
+ **kwargs,
6513
+ )
6514
+
6515
+
6522
6516
  ########################################
6523
6517
  # ../../../omdev/interp/types.py
6524
6518
 
@@ -6749,34 +6743,13 @@ class PingCommandExecutor(CommandExecutor[PingCommand, PingCommand.Output]):
6749
6743
  CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
6750
6744
 
6751
6745
 
6752
- ########################################
6753
- # ../deploy/commands.py
6754
-
6755
-
6756
- ##
6757
-
6758
-
6759
- @dc.dataclass(frozen=True)
6760
- class DeployCommand(Command['DeployCommand.Output']):
6761
- @dc.dataclass(frozen=True)
6762
- class Output(Command.Output):
6763
- pass
6764
-
6765
-
6766
- class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
6767
- async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
6768
- log.info('Deploying!')
6769
-
6770
- return DeployCommand.Output()
6771
-
6772
-
6773
6746
  ########################################
6774
6747
  # ../deploy/tmp.py
6775
6748
 
6776
6749
 
6777
6750
  class DeployTmpManager(
6778
6751
  SingleDirDeployPathOwner,
6779
- DeployAtomicPathSwapping,
6752
+ AtomicPathSwapping,
6780
6753
  ):
6781
6754
  def __init__(
6782
6755
  self,
@@ -6789,18 +6762,18 @@ class DeployTmpManager(
6789
6762
  )
6790
6763
 
6791
6764
  @cached_nullary
6792
- def _swapping(self) -> DeployAtomicPathSwapping:
6793
- return TempDirDeployAtomicPathSwapping(
6765
+ def _swapping(self) -> AtomicPathSwapping:
6766
+ return TempDirAtomicPathSwapping(
6794
6767
  temp_dir=self._make_dir(),
6795
6768
  root_dir=check.non_empty_str(self._deploy_home),
6796
6769
  )
6797
6770
 
6798
6771
  def begin_atomic_path_swap(
6799
6772
  self,
6800
- kind: DeployAtomicPathSwapKind,
6773
+ kind: AtomicPathSwapKind,
6801
6774
  dst_path: str,
6802
6775
  **kwargs: ta.Any,
6803
- ) -> DeployAtomicPathSwap:
6776
+ ) -> AtomicPathSwap:
6804
6777
  return self._swapping().begin_atomic_path_swap(
6805
6778
  kind,
6806
6779
  dst_path,
@@ -8199,7 +8172,7 @@ class DeployGitManager(SingleDirDeployPathOwner):
8199
8172
  self,
8200
8173
  *,
8201
8174
  deploy_home: ta.Optional[DeployHome] = None,
8202
- atomics: DeployAtomicPathSwapping,
8175
+ atomics: AtomicPathSwapping,
8203
8176
  ) -> None:
8204
8177
  super().__init__(
8205
8178
  owned_dir='git',
@@ -8237,12 +8210,16 @@ class DeployGitManager(SingleDirDeployPathOwner):
8237
8210
  else:
8238
8211
  return f'https://{self._repo.host}/{self._repo.path}'
8239
8212
 
8213
+ #
8214
+
8240
8215
  async def _call(self, *cmd: str) -> None:
8241
8216
  await asyncio_subprocesses.check_call(
8242
8217
  *cmd,
8243
8218
  cwd=self._dir,
8244
8219
  )
8245
8220
 
8221
+ #
8222
+
8246
8223
  @async_cached_nullary
8247
8224
  async def init(self) -> None:
8248
8225
  os.makedirs(self._dir, exist_ok=True)
@@ -8256,7 +8233,9 @@ class DeployGitManager(SingleDirDeployPathOwner):
8256
8233
  await self.init()
8257
8234
  await self._call('git', 'fetch', '--depth=1', 'origin', rev)
8258
8235
 
8259
- async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
8236
+ #
8237
+
8238
+ async def checkout(self, checkout: DeployGitCheckout, dst_dir: str) -> None:
8260
8239
  check.state(not os.path.exists(dst_dir))
8261
8240
  with self._git._atomics.begin_atomic_path_swap( # noqa
8262
8241
  'dir',
@@ -8264,14 +8243,14 @@ class DeployGitManager(SingleDirDeployPathOwner):
8264
8243
  auto_commit=True,
8265
8244
  make_dirs=True,
8266
8245
  ) as dst_swap:
8267
- await self.fetch(rev)
8246
+ await self.fetch(checkout.rev)
8268
8247
 
8269
8248
  dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
8270
8249
  await dst_call('git', 'init')
8271
8250
 
8272
8251
  await dst_call('git', 'remote', 'add', 'local', self._dir)
8273
- await dst_call('git', 'fetch', '--depth=1', 'local', rev)
8274
- await dst_call('git', 'checkout', rev)
8252
+ await dst_call('git', 'fetch', '--depth=1', 'local', checkout.rev)
8253
+ await dst_call('git', 'checkout', checkout.rev, *(checkout.subtrees or []))
8275
8254
 
8276
8255
  def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
8277
8256
  try:
@@ -8280,8 +8259,8 @@ class DeployGitManager(SingleDirDeployPathOwner):
8280
8259
  repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
8281
8260
  return repo_dir
8282
8261
 
8283
- async def checkout(self, repo: DeployGitRepo, rev: DeployRev, dst_dir: str) -> None:
8284
- await self.get_repo_dir(repo).checkout(rev, dst_dir)
8262
+ async def checkout(self, checkout: DeployGitCheckout, dst_dir: str) -> None:
8263
+ await self.get_repo_dir(checkout.repo).checkout(checkout, dst_dir)
8285
8264
 
8286
8265
 
8287
8266
  ########################################
@@ -8298,7 +8277,7 @@ class DeployVenvManager(DeployPathOwner):
8298
8277
  self,
8299
8278
  *,
8300
8279
  deploy_home: ta.Optional[DeployHome] = None,
8301
- atomics: DeployAtomicPathSwapping,
8280
+ atomics: AtomicPathSwapping,
8302
8281
  ) -> None:
8303
8282
  super().__init__()
8304
8283
 
@@ -8919,15 +8898,14 @@ class DeployAppManager(DeployPathOwner):
8919
8898
  async def prepare_app(
8920
8899
  self,
8921
8900
  spec: DeploySpec,
8922
- ):
8923
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev, spec.key()))
8901
+ ) -> None:
8902
+ app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.checkout.rev, spec.key()))
8924
8903
  app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
8925
8904
 
8926
8905
  #
8927
8906
 
8928
8907
  await self._git.checkout(
8929
- spec.repo,
8930
- spec.rev,
8908
+ spec.checkout,
8931
8909
  app_dir,
8932
8910
  )
8933
8911
 
@@ -9650,6 +9628,34 @@ class SystemInterpProvider(InterpProvider):
9650
9628
  raise KeyError(version)
9651
9629
 
9652
9630
 
9631
+ ########################################
9632
+ # ../deploy/commands.py
9633
+
9634
+
9635
+ ##
9636
+
9637
+
9638
+ @dc.dataclass(frozen=True)
9639
+ class DeployCommand(Command['DeployCommand.Output']):
9640
+ spec: DeploySpec
9641
+
9642
+ @dc.dataclass(frozen=True)
9643
+ class Output(Command.Output):
9644
+ pass
9645
+
9646
+
9647
+ @dc.dataclass(frozen=True)
9648
+ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
9649
+ _apps: DeployAppManager
9650
+
9651
+ async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
9652
+ log.info('Deploying! %r', cmd.spec)
9653
+
9654
+ await self._apps.prepare_app(cmd.spec)
9655
+
9656
+ return DeployCommand.Output()
9657
+
9658
+
9653
9659
  ########################################
9654
9660
  # ../remote/inject.py
9655
9661
 
@@ -9815,7 +9821,7 @@ class DockerManageTargetConnector(ManageTargetConnector):
9815
9821
  if dmt.image is not None:
9816
9822
  sh_parts.extend(['run', '-i', dmt.image])
9817
9823
  elif dmt.container_id is not None:
9818
- sh_parts.extend(['exec', dmt.container_id])
9824
+ sh_parts.extend(['exec', '-i', dmt.container_id])
9819
9825
  else:
9820
9826
  raise ValueError(dmt)
9821
9827
 
@@ -10035,7 +10041,7 @@ def bind_deploy(
10035
10041
  inj.bind(DeployGitManager, singleton=True),
10036
10042
 
10037
10043
  inj.bind(DeployTmpManager, singleton=True),
10038
- inj.bind(DeployAtomicPathSwapping, to_key=DeployTmpManager),
10044
+ inj.bind(AtomicPathSwapping, to_key=DeployTmpManager),
10039
10045
 
10040
10046
  inj.bind(DeployVenvManager, singleton=True),
10041
10047