ominfra 0.0.0.dev176__py3-none-any.whl → 0.0.0.dev178__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
@@ -17,6 +17,7 @@ import base64
17
17
  import collections
18
18
  import collections.abc
19
19
  import contextlib
20
+ import contextvars
20
21
  import ctypes as ct
21
22
  import dataclasses as dc
22
23
  import datetime
@@ -1384,7 +1385,7 @@ class MainConfig:
1384
1385
 
1385
1386
  @dc.dataclass(frozen=True)
1386
1387
  class DeployConfig:
1387
- deploy_home: ta.Optional[str] = None
1388
+ pass
1388
1389
 
1389
1390
 
1390
1391
  ########################################
@@ -4905,6 +4906,10 @@ class InjectorBinding:
4905
4906
  key: InjectorKey
4906
4907
  provider: InjectorProvider
4907
4908
 
4909
+ def __post_init__(self) -> None:
4910
+ check.isinstance(self.key, InjectorKey)
4911
+ check.isinstance(self.provider, InjectorProvider)
4912
+
4908
4913
 
4909
4914
  class InjectorBindings(abc.ABC):
4910
4915
  @abc.abstractmethod
@@ -5168,6 +5173,164 @@ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey,
5168
5173
  return pm
5169
5174
 
5170
5175
 
5176
+ ###
5177
+ # scopes
5178
+
5179
+
5180
+ class InjectorScope(abc.ABC): # noqa
5181
+ def __init__(
5182
+ self,
5183
+ *,
5184
+ _i: Injector,
5185
+ ) -> None:
5186
+ check.not_in(abc.ABC, type(self).__bases__)
5187
+
5188
+ super().__init__()
5189
+
5190
+ self._i = _i
5191
+
5192
+ all_seeds: ta.Iterable[_InjectorScopeSeed] = self._i.provide(InjectorKey(_InjectorScopeSeed, array=True))
5193
+ self._sks = {s.k for s in all_seeds if s.sc is type(self)}
5194
+
5195
+ #
5196
+
5197
+ @dc.dataclass(frozen=True)
5198
+ class State:
5199
+ seeds: ta.Dict[InjectorKey, ta.Any]
5200
+ prvs: ta.Dict[InjectorKey, ta.Any] = dc.field(default_factory=dict)
5201
+
5202
+ def new_state(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> State:
5203
+ vs = dict(vs)
5204
+ check.equal(set(vs.keys()), self._sks)
5205
+ return InjectorScope.State(vs)
5206
+
5207
+ #
5208
+
5209
+ @abc.abstractmethod
5210
+ def state(self) -> State:
5211
+ raise NotImplementedError
5212
+
5213
+ @abc.abstractmethod
5214
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.ContextManager[None]:
5215
+ raise NotImplementedError
5216
+
5217
+
5218
+ class ExclusiveInjectorScope(InjectorScope, abc.ABC):
5219
+ _st: ta.Optional[InjectorScope.State] = None
5220
+
5221
+ def state(self) -> InjectorScope.State:
5222
+ return check.not_none(self._st)
5223
+
5224
+ @contextlib.contextmanager
5225
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.Iterator[None]:
5226
+ check.none(self._st)
5227
+ self._st = self.new_state(vs)
5228
+ try:
5229
+ yield
5230
+ finally:
5231
+ self._st = None
5232
+
5233
+
5234
+ class ContextvarInjectorScope(InjectorScope, abc.ABC):
5235
+ _cv: contextvars.ContextVar
5236
+
5237
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
5238
+ super().__init_subclass__(**kwargs)
5239
+ check.not_in(abc.ABC, cls.__bases__)
5240
+ check.state(not hasattr(cls, '_cv'))
5241
+ cls._cv = contextvars.ContextVar(f'{cls.__name__}_cv')
5242
+
5243
+ def state(self) -> InjectorScope.State:
5244
+ return self._cv.get()
5245
+
5246
+ @contextlib.contextmanager
5247
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.Iterator[None]:
5248
+ try:
5249
+ self._cv.get()
5250
+ except LookupError:
5251
+ pass
5252
+ else:
5253
+ raise RuntimeError(f'Scope already entered: {self}')
5254
+ st = self.new_state(vs)
5255
+ tok = self._cv.set(st)
5256
+ try:
5257
+ yield
5258
+ finally:
5259
+ self._cv.reset(tok)
5260
+
5261
+
5262
+ #
5263
+
5264
+
5265
+ @dc.dataclass(frozen=True)
5266
+ class ScopedInjectorProvider(InjectorProvider):
5267
+ p: InjectorProvider
5268
+ k: InjectorKey
5269
+ sc: ta.Type[InjectorScope]
5270
+
5271
+ def __post_init__(self) -> None:
5272
+ check.isinstance(self.p, InjectorProvider)
5273
+ check.isinstance(self.k, InjectorKey)
5274
+ check.issubclass(self.sc, InjectorScope)
5275
+
5276
+ def provider_fn(self) -> InjectorProviderFn:
5277
+ def pfn(i: Injector) -> ta.Any:
5278
+ st = i[self.sc].state()
5279
+ try:
5280
+ return st.prvs[self.k]
5281
+ except KeyError:
5282
+ pass
5283
+ v = ufn(i)
5284
+ st.prvs[self.k] = v
5285
+ return v
5286
+
5287
+ ufn = self.p.provider_fn()
5288
+ return pfn
5289
+
5290
+
5291
+ @dc.dataclass(frozen=True)
5292
+ class _ScopeSeedInjectorProvider(InjectorProvider):
5293
+ k: InjectorKey
5294
+ sc: ta.Type[InjectorScope]
5295
+
5296
+ def __post_init__(self) -> None:
5297
+ check.isinstance(self.k, InjectorKey)
5298
+ check.issubclass(self.sc, InjectorScope)
5299
+
5300
+ def provider_fn(self) -> InjectorProviderFn:
5301
+ def pfn(i: Injector) -> ta.Any:
5302
+ st = i[self.sc].state()
5303
+ return st.seeds[self.k]
5304
+ return pfn
5305
+
5306
+
5307
+ def bind_injector_scope(sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5308
+ return as_injector_bindings(
5309
+ InjectorBinder.bind(sc, singleton=True),
5310
+ )
5311
+
5312
+
5313
+ #
5314
+
5315
+
5316
+ @dc.dataclass(frozen=True)
5317
+ class _InjectorScopeSeed:
5318
+ sc: ta.Type['InjectorScope']
5319
+ k: InjectorKey
5320
+
5321
+ def __post_init__(self) -> None:
5322
+ check.issubclass(self.sc, InjectorScope)
5323
+ check.isinstance(self.k, InjectorKey)
5324
+
5325
+
5326
+ def bind_injector_scope_seed(k: ta.Any, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5327
+ kk = as_injector_key(k)
5328
+ return as_injector_bindings(
5329
+ InjectorBinding(kk, _ScopeSeedInjectorProvider(kk, sc)),
5330
+ InjectorBinder.bind(_InjectorScopeSeed(sc, kk), array=True),
5331
+ )
5332
+
5333
+
5171
5334
  ###
5172
5335
  # inspection
5173
5336
 
@@ -5312,13 +5475,21 @@ _INJECTOR_EAGER_ARRAY_KEY: InjectorKey[_InjectorEager] = InjectorKey(_InjectorEa
5312
5475
 
5313
5476
 
5314
5477
  class _Injector(Injector):
5478
+ _DEFAULT_BINDINGS: ta.ClassVar[ta.List[InjectorBinding]] = []
5479
+
5315
5480
  def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
5316
5481
  super().__init__()
5317
5482
 
5318
5483
  self._bs = check.isinstance(bs, InjectorBindings)
5319
5484
  self._p: ta.Optional[Injector] = check.isinstance(p, (Injector, type(None)))
5320
5485
 
5321
- self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
5486
+ self._pfm = {
5487
+ k: v.provider_fn()
5488
+ for k, v in build_injector_provider_map(as_injector_bindings(
5489
+ *self._DEFAULT_BINDINGS,
5490
+ bs,
5491
+ )).items()
5492
+ }
5322
5493
 
5323
5494
  if _INJECTOR_INJECTOR_KEY in self._pfm:
5324
5495
  raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
@@ -5485,6 +5656,7 @@ class InjectorBinder:
5485
5656
  to_const: ta.Any = None,
5486
5657
  to_key: ta.Any = None,
5487
5658
 
5659
+ in_: ta.Optional[ta.Type[InjectorScope]] = None,
5488
5660
  singleton: bool = False,
5489
5661
 
5490
5662
  eager: bool = False,
@@ -5494,12 +5666,12 @@ class InjectorBinder:
5494
5666
  if isinstance(obj, cls._BANNED_BIND_TYPES):
5495
5667
  raise TypeError(obj)
5496
5668
 
5497
- ##
5669
+ #
5498
5670
 
5499
5671
  if key is not None:
5500
5672
  key = as_injector_key(key)
5501
5673
 
5502
- ##
5674
+ #
5503
5675
 
5504
5676
  has_to = (
5505
5677
  to_fn is not None or
@@ -5529,7 +5701,7 @@ class InjectorBinder:
5529
5701
  key = InjectorKey(type(obj))
5530
5702
  del has_to
5531
5703
 
5532
- ##
5704
+ #
5533
5705
 
5534
5706
  if tag is not None:
5535
5707
  if key.tag is not None:
@@ -5539,7 +5711,7 @@ class InjectorBinder:
5539
5711
  if array is not None:
5540
5712
  key = dc.replace(key, array=array)
5541
5713
 
5542
- ##
5714
+ #
5543
5715
 
5544
5716
  providers: ta.List[InjectorProvider] = []
5545
5717
  if to_fn is not None:
@@ -5554,23 +5726,34 @@ class InjectorBinder:
5554
5726
  raise TypeError('Must specify provider')
5555
5727
  if len(providers) > 1:
5556
5728
  raise TypeError('May not specify multiple providers')
5557
- provider, = providers
5729
+ provider = check.single(providers)
5558
5730
 
5559
- ##
5731
+ #
5560
5732
 
5733
+ pws: ta.List[ta.Any] = []
5734
+ if in_ is not None:
5735
+ check.issubclass(in_, InjectorScope)
5736
+ check.not_in(abc.ABC, in_.__bases__)
5737
+ pws.append(functools.partial(ScopedInjectorProvider, k=key, sc=in_))
5561
5738
  if singleton:
5562
- provider = SingletonInjectorProvider(provider)
5739
+ pws.append(SingletonInjectorProvider)
5740
+ if len(pws) > 1:
5741
+ raise TypeError('May not specify multiple provider wrappers')
5742
+ elif pws:
5743
+ provider = check.single(pws)(provider)
5744
+
5745
+ #
5563
5746
 
5564
5747
  binding = InjectorBinding(key, provider)
5565
5748
 
5566
- ##
5749
+ #
5567
5750
 
5568
5751
  extras: ta.List[InjectorBinding] = []
5569
5752
 
5570
5753
  if eager:
5571
5754
  extras.append(bind_injector_eager_key(key))
5572
5755
 
5573
- ##
5756
+ #
5574
5757
 
5575
5758
  if extras:
5576
5759
  return as_injector_bindings(binding, *extras)
@@ -5643,7 +5826,8 @@ def bind_injector_eager_key(key: ta.Any) -> InjectorBinding:
5643
5826
  return InjectorBinding(_INJECTOR_EAGER_ARRAY_KEY, ConstInjectorProvider(_InjectorEager(as_injector_key(key))))
5644
5827
 
5645
5828
 
5646
- ##
5829
+ ###
5830
+ # api
5647
5831
 
5648
5832
 
5649
5833
  class InjectionApi:
@@ -5666,6 +5850,14 @@ class InjectionApi:
5666
5850
  def override(self, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
5667
5851
  return injector_override(p, *args)
5668
5852
 
5853
+ # scopes
5854
+
5855
+ def bind_scope(self, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5856
+ return bind_injector_scope(sc)
5857
+
5858
+ def bind_scope_seed(self, k: ta.Any, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5859
+ return bind_injector_scope_seed(k, sc)
5860
+
5669
5861
  # injector
5670
5862
 
5671
5863
  def create_injector(self, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
@@ -5686,6 +5878,7 @@ class InjectionApi:
5686
5878
  to_const: ta.Any = None,
5687
5879
  to_key: ta.Any = None,
5688
5880
 
5881
+ in_: ta.Optional[ta.Type[InjectorScope]] = None,
5689
5882
  singleton: bool = False,
5690
5883
 
5691
5884
  eager: bool = False,
@@ -5702,6 +5895,7 @@ class InjectionApi:
5702
5895
  to_const=to_const,
5703
5896
  to_key=to_key,
5704
5897
 
5898
+ in_=in_,
5705
5899
  singleton=singleton,
5706
5900
 
5707
5901
  eager=eager,
@@ -7547,6 +7741,37 @@ class LocalCommandExecutor(CommandExecutor):
7547
7741
  return await ce.execute(cmd)
7548
7742
 
7549
7743
 
7744
+ ########################################
7745
+ # ../deploy/deploy.py
7746
+
7747
+
7748
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
7749
+
7750
+
7751
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
7752
+
7753
+
7754
+ class DeployManager:
7755
+ def __init__(
7756
+ self,
7757
+ *,
7758
+
7759
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
7760
+ ):
7761
+ super().__init__()
7762
+
7763
+ self._utc_clock = utc_clock
7764
+
7765
+ def _utc_now(self) -> datetime.datetime:
7766
+ if self._utc_clock is not None:
7767
+ return self._utc_clock() # noqa
7768
+ else:
7769
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
7770
+
7771
+ def make_deploy_time(self) -> DeployTime:
7772
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
7773
+
7774
+
7550
7775
  ########################################
7551
7776
  # ../deploy/paths/paths.py
7552
7777
  """
@@ -7885,9 +8110,13 @@ class DeployAppSpec(DeploySpecKeyed[DeployAppKey]):
7885
8110
 
7886
8111
  @dc.dataclass(frozen=True)
7887
8112
  class DeploySpec(DeploySpecKeyed[DeployKey]):
8113
+ home: DeployHome
8114
+
7888
8115
  apps: ta.Sequence[DeployAppSpec]
7889
8116
 
7890
8117
  def __post_init__(self) -> None:
8118
+ check.non_empty_str(self.home)
8119
+
7891
8120
  seen: ta.Set[DeployApp] = set()
7892
8121
  for a in self.apps:
7893
8122
  if a.app in seen:
@@ -8707,17 +8936,6 @@ TODO:
8707
8936
 
8708
8937
 
8709
8938
  class DeployConfManager:
8710
- def __init__(
8711
- self,
8712
- *,
8713
- deploy_home: ta.Optional[DeployHome] = None,
8714
- ) -> None:
8715
- super().__init__()
8716
-
8717
- self._deploy_home = deploy_home
8718
-
8719
- #
8720
-
8721
8939
  async def _write_app_conf_file(
8722
8940
  self,
8723
8941
  acf: DeployAppConfFile,
@@ -8884,7 +9102,6 @@ class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
8884
9102
  self,
8885
9103
  *args: ta.Any,
8886
9104
  owned_dir: str,
8887
- deploy_home: ta.Optional[DeployHome],
8888
9105
  **kwargs: ta.Any,
8889
9106
  ) -> None:
8890
9107
  super().__init__(*args, **kwargs)
@@ -8892,17 +9109,13 @@ class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
8892
9109
  check.not_in('/', owned_dir)
8893
9110
  self._owned_dir: str = check.non_empty_str(owned_dir)
8894
9111
 
8895
- self._deploy_home = deploy_home
8896
-
8897
9112
  self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
8898
9113
 
8899
- @cached_nullary
8900
- def _dir(self) -> str:
8901
- return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
9114
+ def _dir(self, home: DeployHome) -> str:
9115
+ return os.path.join(check.non_empty_str(home), self._owned_dir)
8902
9116
 
8903
- @cached_nullary
8904
- def _make_dir(self) -> str:
8905
- if not os.path.isdir(d := self._dir()):
9117
+ def _make_dir(self, home: DeployHome) -> str:
9118
+ if not os.path.isdir(d := self._dir(home)):
8906
9119
  os.makedirs(d, exist_ok=True)
8907
9120
  return d
8908
9121
 
@@ -9442,122 +9655,6 @@ def bind_commands(
9442
9655
  return inj.as_bindings(*lst)
9443
9656
 
9444
9657
 
9445
- ########################################
9446
- # ../deploy/git.py
9447
- """
9448
- TODO:
9449
- - 'repos'?
9450
-
9451
- git/github.com/wrmsr/omlish <- bootstrap repo
9452
- - shallow clone off bootstrap into /apps
9453
-
9454
- github.com/wrmsr/omlish@rev
9455
- """
9456
-
9457
-
9458
- ##
9459
-
9460
-
9461
- class DeployGitManager(SingleDirDeployPathOwner):
9462
- def __init__(
9463
- self,
9464
- *,
9465
- deploy_home: ta.Optional[DeployHome] = None,
9466
- atomics: AtomicPathSwapping,
9467
- ) -> None:
9468
- super().__init__(
9469
- owned_dir='git',
9470
- deploy_home=deploy_home,
9471
- )
9472
-
9473
- self._atomics = atomics
9474
-
9475
- self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
9476
-
9477
- class RepoDir:
9478
- def __init__(
9479
- self,
9480
- git: 'DeployGitManager',
9481
- repo: DeployGitRepo,
9482
- ) -> None:
9483
- super().__init__()
9484
-
9485
- self._git = git
9486
- self._repo = repo
9487
- self._dir = os.path.join(
9488
- self._git._make_dir(), # noqa
9489
- check.non_empty_str(repo.host),
9490
- check.non_empty_str(repo.path),
9491
- )
9492
-
9493
- @property
9494
- def repo(self) -> DeployGitRepo:
9495
- return self._repo
9496
-
9497
- @property
9498
- def url(self) -> str:
9499
- if self._repo.username is not None:
9500
- return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
9501
- else:
9502
- return f'https://{self._repo.host}/{self._repo.path}'
9503
-
9504
- #
9505
-
9506
- async def _call(self, *cmd: str) -> None:
9507
- await asyncio_subprocesses.check_call(
9508
- *cmd,
9509
- cwd=self._dir,
9510
- )
9511
-
9512
- #
9513
-
9514
- @async_cached_nullary
9515
- async def init(self) -> None:
9516
- os.makedirs(self._dir, exist_ok=True)
9517
- if os.path.exists(os.path.join(self._dir, '.git')):
9518
- return
9519
-
9520
- await self._call('git', 'init')
9521
- await self._call('git', 'remote', 'add', 'origin', self.url)
9522
-
9523
- async def fetch(self, rev: DeployRev) -> None:
9524
- await self.init()
9525
- await self._call('git', 'fetch', '--depth=1', 'origin', rev)
9526
-
9527
- #
9528
-
9529
- async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
9530
- check.state(not os.path.exists(dst_dir))
9531
- with self._git._atomics.begin_atomic_path_swap( # noqa
9532
- 'dir',
9533
- dst_dir,
9534
- auto_commit=True,
9535
- make_dirs=True,
9536
- ) as dst_swap:
9537
- await self.fetch(spec.rev)
9538
-
9539
- dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
9540
- await dst_call('git', 'init')
9541
-
9542
- await dst_call('git', 'remote', 'add', 'local', self._dir)
9543
- await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
9544
- await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
9545
-
9546
- def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
9547
- try:
9548
- return self._repo_dirs[repo]
9549
- except KeyError:
9550
- repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
9551
- return repo_dir
9552
-
9553
- async def checkout(
9554
- self,
9555
- spec: DeployGitSpec,
9556
- dst_dir: str,
9557
- ) -> None:
9558
- await self.get_repo_dir(spec.repo).checkout(spec, dst_dir)
9559
-
9560
-
9561
9658
  ########################################
9562
9659
  # ../deploy/paths/manager.py
9563
9660
 
@@ -9566,12 +9663,10 @@ class DeployPathsManager:
9566
9663
  def __init__(
9567
9664
  self,
9568
9665
  *,
9569
- deploy_home: ta.Optional[DeployHome],
9570
9666
  deploy_path_owners: DeployPathOwners,
9571
9667
  ) -> None:
9572
9668
  super().__init__()
9573
9669
 
9574
- self._deploy_home = deploy_home
9575
9670
  self._deploy_path_owners = deploy_path_owners
9576
9671
 
9577
9672
  @cached_nullary
@@ -9592,37 +9687,22 @@ class DeployPathsManager:
9592
9687
  # ../deploy/tmp.py
9593
9688
 
9594
9689
 
9690
+ class DeployHomeAtomics(Func1[DeployHome, AtomicPathSwapping]):
9691
+ pass
9692
+
9693
+
9595
9694
  class DeployTmpManager(
9596
9695
  SingleDirDeployPathOwner,
9597
- AtomicPathSwapping,
9598
9696
  ):
9599
- def __init__(
9600
- self,
9601
- *,
9602
- deploy_home: ta.Optional[DeployHome] = None,
9603
- ) -> None:
9697
+ def __init__(self) -> None:
9604
9698
  super().__init__(
9605
9699
  owned_dir='tmp',
9606
- deploy_home=deploy_home,
9607
9700
  )
9608
9701
 
9609
- @cached_nullary
9610
- def _swapping(self) -> AtomicPathSwapping:
9702
+ def get_swapping(self, home: DeployHome) -> AtomicPathSwapping:
9611
9703
  return TempDirAtomicPathSwapping(
9612
- temp_dir=self._make_dir(),
9613
- root_dir=check.non_empty_str(self._deploy_home),
9614
- )
9615
-
9616
- def begin_atomic_path_swap(
9617
- self,
9618
- kind: AtomicPathSwapKind,
9619
- dst_path: str,
9620
- **kwargs: ta.Any,
9621
- ) -> AtomicPathSwap:
9622
- return self._swapping().begin_atomic_path_swap(
9623
- kind,
9624
- dst_path,
9625
- **kwargs,
9704
+ temp_dir=self._make_dir(home),
9705
+ root_dir=check.non_empty_str(home),
9626
9706
  )
9627
9707
 
9628
9708
 
@@ -10081,11 +10161,11 @@ class PyenvVersionInstaller:
10081
10161
  full_args = [
10082
10162
  os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
10083
10163
  *conf_args,
10084
- self.install_dir(),
10164
+ await self.install_dir(),
10085
10165
  ]
10086
10166
  else:
10087
10167
  full_args = [
10088
- self._pyenv.exe(),
10168
+ await self._pyenv.exe(),
10089
10169
  'install',
10090
10170
  *conf_args,
10091
10171
  ]
@@ -10340,6 +10420,137 @@ class SystemInterpProvider(InterpProvider):
10340
10420
  raise KeyError(version)
10341
10421
 
10342
10422
 
10423
+ ########################################
10424
+ # ../deploy/git.py
10425
+ """
10426
+ TODO:
10427
+ - 'repos'?
10428
+
10429
+ git/github.com/wrmsr/omlish <- bootstrap repo
10430
+ - shallow clone off bootstrap into /apps
10431
+
10432
+ github.com/wrmsr/omlish@rev
10433
+ """
10434
+
10435
+
10436
+ ##
10437
+
10438
+
10439
+ class DeployGitManager(SingleDirDeployPathOwner):
10440
+ def __init__(
10441
+ self,
10442
+ *,
10443
+ atomics: DeployHomeAtomics,
10444
+ ) -> None:
10445
+ super().__init__(
10446
+ owned_dir='git',
10447
+ )
10448
+
10449
+ self._atomics = atomics
10450
+
10451
+ self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
10452
+
10453
+ class RepoDir:
10454
+ def __init__(
10455
+ self,
10456
+ git: 'DeployGitManager',
10457
+ repo: DeployGitRepo,
10458
+ home: DeployHome,
10459
+ ) -> None:
10460
+ super().__init__()
10461
+
10462
+ self._git = git
10463
+ self._repo = repo
10464
+ self._home = home
10465
+ self._dir = os.path.join(
10466
+ self._git._make_dir(home), # noqa
10467
+ check.non_empty_str(repo.host),
10468
+ check.non_empty_str(repo.path),
10469
+ )
10470
+
10471
+ @property
10472
+ def repo(self) -> DeployGitRepo:
10473
+ return self._repo
10474
+
10475
+ @property
10476
+ def url(self) -> str:
10477
+ if self._repo.username is not None:
10478
+ return f'{self._repo.username}@{self._repo.host}:{self._repo.path}'
10479
+ else:
10480
+ return f'https://{self._repo.host}/{self._repo.path}'
10481
+
10482
+ #
10483
+
10484
+ async def _call(self, *cmd: str) -> None:
10485
+ await asyncio_subprocesses.check_call(
10486
+ *cmd,
10487
+ cwd=self._dir,
10488
+ )
10489
+
10490
+ #
10491
+
10492
+ @async_cached_nullary
10493
+ async def init(self) -> None:
10494
+ os.makedirs(self._dir, exist_ok=True)
10495
+ if os.path.exists(os.path.join(self._dir, '.git')):
10496
+ return
10497
+
10498
+ await self._call('git', 'init')
10499
+ await self._call('git', 'remote', 'add', 'origin', self.url)
10500
+
10501
+ async def fetch(self, rev: DeployRev) -> None:
10502
+ await self.init()
10503
+ await self._call('git', 'fetch', '--depth=1', 'origin', rev)
10504
+
10505
+ #
10506
+
10507
+ async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
10508
+ check.state(not os.path.exists(dst_dir))
10509
+ with self._git._atomics(self._home).begin_atomic_path_swap( # noqa
10510
+ 'dir',
10511
+ dst_dir,
10512
+ auto_commit=True,
10513
+ make_dirs=True,
10514
+ ) as dst_swap:
10515
+ await self.fetch(spec.rev)
10516
+
10517
+ dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
10518
+ await dst_call('git', 'init')
10519
+
10520
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
10521
+ await dst_call('git', 'fetch', '--depth=1', 'local', spec.rev)
10522
+ await dst_call('git', 'checkout', spec.rev, *(spec.subtrees or []))
10523
+
10524
+ def get_repo_dir(
10525
+ self,
10526
+ repo: DeployGitRepo,
10527
+ home: DeployHome,
10528
+ ) -> RepoDir:
10529
+ try:
10530
+ return self._repo_dirs[repo]
10531
+ except KeyError:
10532
+ repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(
10533
+ self,
10534
+ repo,
10535
+ home,
10536
+ )
10537
+ return repo_dir
10538
+
10539
+ async def checkout(
10540
+ self,
10541
+ spec: DeployGitSpec,
10542
+ home: DeployHome,
10543
+ dst_dir: str,
10544
+ ) -> None:
10545
+ await self.get_repo_dir(
10546
+ spec.repo,
10547
+ home,
10548
+ ).checkout(
10549
+ spec,
10550
+ dst_dir,
10551
+ )
10552
+
10553
+
10343
10554
  ########################################
10344
10555
  # ../deploy/paths/inject.py
10345
10556
 
@@ -10739,18 +10950,10 @@ TODO:
10739
10950
 
10740
10951
 
10741
10952
  class DeployVenvManager:
10742
- def __init__(
10743
- self,
10744
- *,
10745
- atomics: AtomicPathSwapping,
10746
- ) -> None:
10747
- super().__init__()
10748
-
10749
- self._atomics = atomics
10750
-
10751
10953
  async def setup_venv(
10752
10954
  self,
10753
10955
  spec: DeployVenvSpec,
10956
+ home: DeployHome,
10754
10957
  git_dir: str,
10755
10958
  venv_dir: str,
10756
10959
  ) -> None:
@@ -10793,16 +10996,12 @@ class DeployAppManager(DeployPathOwner):
10793
10996
  def __init__(
10794
10997
  self,
10795
10998
  *,
10796
- deploy_home: ta.Optional[DeployHome] = None,
10797
-
10798
10999
  conf: DeployConfManager,
10799
11000
  git: DeployGitManager,
10800
11001
  venvs: DeployVenvManager,
10801
11002
  ) -> None:
10802
11003
  super().__init__()
10803
11004
 
10804
- self._deploy_home = deploy_home
10805
-
10806
11005
  self._conf = conf
10807
11006
  self._git = git
10808
11007
  self._venvs = venvs
@@ -10843,12 +11042,13 @@ class DeployAppManager(DeployPathOwner):
10843
11042
  async def prepare_app(
10844
11043
  self,
10845
11044
  spec: DeployAppSpec,
11045
+ home: DeployHome,
10846
11046
  tags: DeployTagMap,
10847
11047
  ) -> None:
10848
- deploy_home = check.non_empty_str(self._deploy_home)
11048
+ check.non_empty_str(home)
10849
11049
 
10850
11050
  def build_path(pth: DeployPath) -> str:
10851
- return os.path.join(deploy_home, pth.render(tags))
11051
+ return os.path.join(home, pth.render(tags))
10852
11052
 
10853
11053
  app_dir = build_path(self._APP_DIR)
10854
11054
  deploy_dir = build_path(self._DEPLOY_DIR)
@@ -10858,7 +11058,7 @@ class DeployAppManager(DeployPathOwner):
10858
11058
 
10859
11059
  os.makedirs(deploy_dir, exist_ok=True)
10860
11060
 
10861
- deploying_link = os.path.join(deploy_home, 'deploys/deploying')
11061
+ deploying_link = os.path.join(home, 'deploys/deploying')
10862
11062
  if os.path.exists(deploying_link):
10863
11063
  os.unlink(deploying_link)
10864
11064
  relative_symlink(
@@ -10905,7 +11105,7 @@ class DeployAppManager(DeployPathOwner):
10905
11105
  # else:
10906
11106
  # os.makedirs(os.path.join(dst, os.path.relpath(dp2, src)))
10907
11107
 
10908
- current_link = os.path.join(deploy_home, 'deploys/current')
11108
+ current_link = os.path.join(home, 'deploys/current')
10909
11109
 
10910
11110
  # if os.path.exists(current_link):
10911
11111
  # mirror_symlinks(
@@ -10922,6 +11122,7 @@ class DeployAppManager(DeployPathOwner):
10922
11122
  app_git_dir = os.path.join(app_dir, 'git')
10923
11123
  await self._git.checkout(
10924
11124
  spec.git,
11125
+ home,
10925
11126
  app_git_dir,
10926
11127
  )
10927
11128
 
@@ -10931,6 +11132,7 @@ class DeployAppManager(DeployPathOwner):
10931
11132
  app_venv_dir = os.path.join(app_dir, 'venv')
10932
11133
  await self._venvs.setup_venv(
10933
11134
  spec.venv,
11135
+ home,
10934
11136
  app_git_dir,
10935
11137
  app_venv_dir,
10936
11138
  )
@@ -10952,56 +11154,53 @@ class DeployAppManager(DeployPathOwner):
10952
11154
 
10953
11155
 
10954
11156
  ########################################
10955
- # ../deploy/deploy.py
11157
+ # ../deploy/driver.py
10956
11158
 
10957
11159
 
10958
- DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
10959
-
10960
-
10961
- DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
11160
+ class DeployDriverFactory(Func1[DeploySpec, ta.ContextManager['DeployDriver']]):
11161
+ pass
10962
11162
 
10963
11163
 
10964
- class DeployManager:
11164
+ class DeployDriver:
10965
11165
  def __init__(
10966
11166
  self,
10967
11167
  *,
10968
- apps: DeployAppManager,
10969
- paths: DeployPathsManager,
11168
+ spec: DeploySpec,
10970
11169
 
10971
- utc_clock: ta.Optional[DeployManagerUtcClock] = None,
10972
- ):
11170
+ deploys: DeployManager,
11171
+ paths: DeployPathsManager,
11172
+ apps: DeployAppManager,
11173
+ ) -> None:
10973
11174
  super().__init__()
10974
11175
 
10975
- self._apps = apps
11176
+ self._spec = spec
11177
+
11178
+ self._deploys = deploys
10976
11179
  self._paths = paths
11180
+ self._apps = apps
10977
11181
 
10978
- self._utc_clock = utc_clock
11182
+ async def drive_deploy(self) -> None:
11183
+ self._paths.validate_deploy_paths()
10979
11184
 
10980
- def _utc_now(self) -> datetime.datetime:
10981
- if self._utc_clock is not None:
10982
- return self._utc_clock() # noqa
10983
- else:
10984
- return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
11185
+ #
10985
11186
 
10986
- def _make_deploy_time(self) -> DeployTime:
10987
- return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
11187
+ hs = check.non_empty_str(self._spec.home)
11188
+ hs = os.path.expanduser(hs)
11189
+ hs = os.path.realpath(hs)
11190
+ hs = os.path.abspath(hs)
10988
11191
 
10989
- async def run_deploy(
10990
- self,
10991
- spec: DeploySpec,
10992
- ) -> None:
10993
- self._paths.validate_deploy_paths()
11192
+ home = DeployHome(hs)
10994
11193
 
10995
11194
  #
10996
11195
 
10997
11196
  deploy_tags = DeployTagMap(
10998
- self._make_deploy_time(),
10999
- spec.key(),
11197
+ self._deploys.make_deploy_time(),
11198
+ self._spec.key(),
11000
11199
  )
11001
11200
 
11002
11201
  #
11003
11202
 
11004
- for app in spec.apps:
11203
+ for app in self._spec.apps:
11005
11204
  app_tags = deploy_tags.add(
11006
11205
  app.app,
11007
11206
  app.key(),
@@ -11010,6 +11209,7 @@ class DeployManager:
11010
11209
 
11011
11210
  await self._apps.prepare_app(
11012
11211
  app,
11212
+ home,
11013
11213
  app_tags,
11014
11214
  )
11015
11215
 
@@ -11032,12 +11232,13 @@ class DeployCommand(Command['DeployCommand.Output']):
11032
11232
 
11033
11233
  @dc.dataclass(frozen=True)
11034
11234
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
11035
- _deploy: DeployManager
11235
+ _driver_factory: DeployDriverFactory
11036
11236
 
11037
11237
  async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
11038
11238
  log.info('Deploying! %r', cmd.spec)
11039
11239
 
11040
- await self._deploy.run_deploy(cmd.spec)
11240
+ with self._driver_factory(cmd.spec) as driver:
11241
+ await driver.drive_deploy()
11041
11242
 
11042
11243
  return DeployCommand.Output()
11043
11244
 
@@ -11046,6 +11247,10 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
11046
11247
  # ../deploy/inject.py
11047
11248
 
11048
11249
 
11250
+ class DeployInjectorScope(ContextvarInjectorScope):
11251
+ pass
11252
+
11253
+
11049
11254
  def bind_deploy(
11050
11255
  *,
11051
11256
  deploy_config: DeployConfig,
@@ -11077,13 +11282,37 @@ def bind_deploy(
11077
11282
  bind_manager(DeployManager),
11078
11283
 
11079
11284
  bind_manager(DeployTmpManager),
11080
- inj.bind(AtomicPathSwapping, to_key=DeployTmpManager),
11081
11285
 
11082
11286
  bind_manager(DeployVenvManager),
11083
11287
  ])
11084
11288
 
11085
11289
  #
11086
11290
 
11291
+ def provide_deploy_home_atomics(tmp: DeployTmpManager) -> DeployHomeAtomics:
11292
+ return DeployHomeAtomics(tmp.get_swapping)
11293
+ lst.append(inj.bind(provide_deploy_home_atomics, singleton=True))
11294
+
11295
+ #
11296
+
11297
+ def provide_deploy_driver_factory(injector: Injector, sc: DeployInjectorScope) -> DeployDriverFactory:
11298
+ @contextlib.contextmanager
11299
+ def factory(spec: DeploySpec) -> ta.Iterator[DeployDriver]:
11300
+ with sc.enter({
11301
+ inj.as_key(DeploySpec): spec,
11302
+ }):
11303
+ yield injector[DeployDriver]
11304
+ return DeployDriverFactory(factory)
11305
+ lst.append(inj.bind(provide_deploy_driver_factory, singleton=True))
11306
+
11307
+ lst.extend([
11308
+ inj.bind_scope(DeployInjectorScope),
11309
+ inj.bind_scope_seed(DeploySpec, DeployInjectorScope),
11310
+
11311
+ inj.bind(DeployDriver, in_=DeployInjectorScope),
11312
+ ])
11313
+
11314
+ #
11315
+
11087
11316
  lst.extend([
11088
11317
  bind_command(DeployCommand, DeployCommandExecutor),
11089
11318
  bind_command(InterpCommand, InterpCommandExecutor),
@@ -11091,10 +11320,6 @@ def bind_deploy(
11091
11320
 
11092
11321
  #
11093
11322
 
11094
- if (dh := deploy_config.deploy_home) is not None:
11095
- dh = os.path.abspath(os.path.expanduser(dh))
11096
- lst.append(inj.bind(dh, key=DeployHome))
11097
-
11098
11323
  return inj.as_bindings(*lst)
11099
11324
 
11100
11325
 
@@ -11190,8 +11415,6 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
11190
11415
 
11191
11416
  @dc.dataclass(frozen=True)
11192
11417
  class ManageConfig:
11193
- deploy_home: ta.Optional[str] = None
11194
-
11195
11418
  targets: ta.Optional[ta.Mapping[str, ManageTarget]] = None
11196
11419
 
11197
11420
 
@@ -11223,8 +11446,6 @@ class MainCli(ArgparseCli):
11223
11446
 
11224
11447
  argparse_arg('--debug', action='store_true'),
11225
11448
 
11226
- argparse_arg('--deploy-home'),
11227
-
11228
11449
  argparse_arg('target'),
11229
11450
  argparse_arg('-f', '--command-file', action='append'),
11230
11451
  argparse_arg('command', nargs='*'),
@@ -11237,9 +11458,7 @@ class MainCli(ArgparseCli):
11237
11458
  debug=bool(self.args.debug),
11238
11459
  ),
11239
11460
 
11240
- deploy_config=DeployConfig(
11241
- deploy_home=self.args.deploy_home or self.config().deploy_home,
11242
- ),
11461
+ deploy_config=DeployConfig(),
11243
11462
 
11244
11463
  remote_config=RemoteConfig(
11245
11464
  payload_file=self.args._payload_file, # noqa