ominfra 0.0.0.dev176__py3-none-any.whl → 0.0.0.dev178__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
@@ -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