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

Sign up to get free protection for your applications and to get access to all the features.
ominfra/scripts/manage.py CHANGED
@@ -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