ominfra 0.0.0.dev177__py3-none-any.whl → 0.0.0.dev179__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
@@ -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
@@ -5121,6 +5126,33 @@ def as_injector_bindings(*args: InjectorBindingOrBindings) -> InjectorBindings:
5121
5126
  ##
5122
5127
 
5123
5128
 
5129
+ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
5130
+ pm: ta.Dict[InjectorKey, InjectorProvider] = {}
5131
+ am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
5132
+
5133
+ for b in bs.bindings():
5134
+ if b.key.array:
5135
+ al = am.setdefault(b.key, [])
5136
+ if isinstance(b.provider, ArrayInjectorProvider):
5137
+ al.extend(b.provider.ps)
5138
+ else:
5139
+ al.append(b.provider)
5140
+ else:
5141
+ if b.key in pm:
5142
+ raise KeyError(b.key)
5143
+ pm[b.key] = b.provider
5144
+
5145
+ if am:
5146
+ for k, aps in am.items():
5147
+ pm[k] = ArrayInjectorProvider(aps)
5148
+
5149
+ return pm
5150
+
5151
+
5152
+ ###
5153
+ # overrides
5154
+
5155
+
5124
5156
  @dc.dataclass(frozen=True)
5125
5157
  class OverridesInjectorBindings(InjectorBindings):
5126
5158
  p: InjectorBindings
@@ -5142,30 +5174,160 @@ def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) ->
5142
5174
  return OverridesInjectorBindings(p, m)
5143
5175
 
5144
5176
 
5145
- ##
5177
+ ###
5178
+ # scopes
5146
5179
 
5147
5180
 
5148
- def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
5149
- pm: ta.Dict[InjectorKey, InjectorProvider] = {}
5150
- am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
5181
+ class InjectorScope(abc.ABC): # noqa
5182
+ def __init__(
5183
+ self,
5184
+ *,
5185
+ _i: Injector,
5186
+ ) -> None:
5187
+ check.not_in(abc.ABC, type(self).__bases__)
5151
5188
 
5152
- for b in bs.bindings():
5153
- if b.key.array:
5154
- al = am.setdefault(b.key, [])
5155
- if isinstance(b.provider, ArrayInjectorProvider):
5156
- al.extend(b.provider.ps)
5157
- else:
5158
- al.append(b.provider)
5189
+ super().__init__()
5190
+
5191
+ self._i = _i
5192
+
5193
+ all_seeds: ta.Iterable[_InjectorScopeSeed] = self._i.provide(InjectorKey(_InjectorScopeSeed, array=True))
5194
+ self._sks = {s.k for s in all_seeds if s.sc is type(self)}
5195
+
5196
+ #
5197
+
5198
+ @dc.dataclass(frozen=True)
5199
+ class State:
5200
+ seeds: ta.Dict[InjectorKey, ta.Any]
5201
+ provisions: ta.Dict[InjectorKey, ta.Any] = dc.field(default_factory=dict)
5202
+
5203
+ def new_state(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> State:
5204
+ vs = dict(vs)
5205
+ check.equal(set(vs.keys()), self._sks)
5206
+ return InjectorScope.State(vs)
5207
+
5208
+ #
5209
+
5210
+ @abc.abstractmethod
5211
+ def state(self) -> State:
5212
+ raise NotImplementedError
5213
+
5214
+ @abc.abstractmethod
5215
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.ContextManager[None]:
5216
+ raise NotImplementedError
5217
+
5218
+
5219
+ class ExclusiveInjectorScope(InjectorScope, abc.ABC):
5220
+ _st: ta.Optional[InjectorScope.State] = None
5221
+
5222
+ def state(self) -> InjectorScope.State:
5223
+ return check.not_none(self._st)
5224
+
5225
+ @contextlib.contextmanager
5226
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.Iterator[None]:
5227
+ check.none(self._st)
5228
+ self._st = self.new_state(vs)
5229
+ try:
5230
+ yield
5231
+ finally:
5232
+ self._st = None
5233
+
5234
+
5235
+ class ContextvarInjectorScope(InjectorScope, abc.ABC):
5236
+ _cv: contextvars.ContextVar
5237
+
5238
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
5239
+ super().__init_subclass__(**kwargs)
5240
+ check.not_in(abc.ABC, cls.__bases__)
5241
+ check.state(not hasattr(cls, '_cv'))
5242
+ cls._cv = contextvars.ContextVar(f'{cls.__name__}_cv')
5243
+
5244
+ def state(self) -> InjectorScope.State:
5245
+ return self._cv.get()
5246
+
5247
+ @contextlib.contextmanager
5248
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.Iterator[None]:
5249
+ try:
5250
+ self._cv.get()
5251
+ except LookupError:
5252
+ pass
5159
5253
  else:
5160
- if b.key in pm:
5161
- raise KeyError(b.key)
5162
- pm[b.key] = b.provider
5254
+ raise RuntimeError(f'Scope already entered: {self}')
5255
+ st = self.new_state(vs)
5256
+ tok = self._cv.set(st)
5257
+ try:
5258
+ yield
5259
+ finally:
5260
+ self._cv.reset(tok)
5163
5261
 
5164
- if am:
5165
- for k, aps in am.items():
5166
- pm[k] = ArrayInjectorProvider(aps)
5167
5262
 
5168
- return pm
5263
+ #
5264
+
5265
+
5266
+ @dc.dataclass(frozen=True)
5267
+ class ScopedInjectorProvider(InjectorProvider):
5268
+ p: InjectorProvider
5269
+ k: InjectorKey
5270
+ sc: ta.Type[InjectorScope]
5271
+
5272
+ def __post_init__(self) -> None:
5273
+ check.isinstance(self.p, InjectorProvider)
5274
+ check.isinstance(self.k, InjectorKey)
5275
+ check.issubclass(self.sc, InjectorScope)
5276
+
5277
+ def provider_fn(self) -> InjectorProviderFn:
5278
+ def pfn(i: Injector) -> ta.Any:
5279
+ st = i[self.sc].state()
5280
+ try:
5281
+ return st.provisions[self.k]
5282
+ except KeyError:
5283
+ pass
5284
+ v = ufn(i)
5285
+ st.provisions[self.k] = v
5286
+ return v
5287
+
5288
+ ufn = self.p.provider_fn()
5289
+ return pfn
5290
+
5291
+
5292
+ @dc.dataclass(frozen=True)
5293
+ class _ScopeSeedInjectorProvider(InjectorProvider):
5294
+ k: InjectorKey
5295
+ sc: ta.Type[InjectorScope]
5296
+
5297
+ def __post_init__(self) -> None:
5298
+ check.isinstance(self.k, InjectorKey)
5299
+ check.issubclass(self.sc, InjectorScope)
5300
+
5301
+ def provider_fn(self) -> InjectorProviderFn:
5302
+ def pfn(i: Injector) -> ta.Any:
5303
+ st = i[self.sc].state()
5304
+ return st.seeds[self.k]
5305
+ return pfn
5306
+
5307
+
5308
+ def bind_injector_scope(sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5309
+ return InjectorBinder.bind(sc, singleton=True)
5310
+
5311
+
5312
+ #
5313
+
5314
+
5315
+ @dc.dataclass(frozen=True)
5316
+ class _InjectorScopeSeed:
5317
+ sc: ta.Type['InjectorScope']
5318
+ k: InjectorKey
5319
+
5320
+ def __post_init__(self) -> None:
5321
+ check.issubclass(self.sc, InjectorScope)
5322
+ check.isinstance(self.k, InjectorKey)
5323
+
5324
+
5325
+ def bind_injector_scope_seed(k: ta.Any, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5326
+ kk = as_injector_key(k)
5327
+ return as_injector_bindings(
5328
+ InjectorBinding(kk, _ScopeSeedInjectorProvider(kk, sc)),
5329
+ InjectorBinder.bind(_InjectorScopeSeed(sc, kk), array=True),
5330
+ )
5169
5331
 
5170
5332
 
5171
5333
  ###
@@ -5312,13 +5474,21 @@ _INJECTOR_EAGER_ARRAY_KEY: InjectorKey[_InjectorEager] = InjectorKey(_InjectorEa
5312
5474
 
5313
5475
 
5314
5476
  class _Injector(Injector):
5477
+ _DEFAULT_BINDINGS: ta.ClassVar[ta.List[InjectorBinding]] = []
5478
+
5315
5479
  def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
5316
5480
  super().__init__()
5317
5481
 
5318
5482
  self._bs = check.isinstance(bs, InjectorBindings)
5319
5483
  self._p: ta.Optional[Injector] = check.isinstance(p, (Injector, type(None)))
5320
5484
 
5321
- self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
5485
+ self._pfm = {
5486
+ k: v.provider_fn()
5487
+ for k, v in build_injector_provider_map(as_injector_bindings(
5488
+ *self._DEFAULT_BINDINGS,
5489
+ bs,
5490
+ )).items()
5491
+ }
5322
5492
 
5323
5493
  if _INJECTOR_INJECTOR_KEY in self._pfm:
5324
5494
  raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
@@ -5485,6 +5655,7 @@ class InjectorBinder:
5485
5655
  to_const: ta.Any = None,
5486
5656
  to_key: ta.Any = None,
5487
5657
 
5658
+ in_: ta.Optional[ta.Type[InjectorScope]] = None,
5488
5659
  singleton: bool = False,
5489
5660
 
5490
5661
  eager: bool = False,
@@ -5494,12 +5665,12 @@ class InjectorBinder:
5494
5665
  if isinstance(obj, cls._BANNED_BIND_TYPES):
5495
5666
  raise TypeError(obj)
5496
5667
 
5497
- ##
5668
+ #
5498
5669
 
5499
5670
  if key is not None:
5500
5671
  key = as_injector_key(key)
5501
5672
 
5502
- ##
5673
+ #
5503
5674
 
5504
5675
  has_to = (
5505
5676
  to_fn is not None or
@@ -5529,7 +5700,7 @@ class InjectorBinder:
5529
5700
  key = InjectorKey(type(obj))
5530
5701
  del has_to
5531
5702
 
5532
- ##
5703
+ #
5533
5704
 
5534
5705
  if tag is not None:
5535
5706
  if key.tag is not None:
@@ -5539,7 +5710,7 @@ class InjectorBinder:
5539
5710
  if array is not None:
5540
5711
  key = dc.replace(key, array=array)
5541
5712
 
5542
- ##
5713
+ #
5543
5714
 
5544
5715
  providers: ta.List[InjectorProvider] = []
5545
5716
  if to_fn is not None:
@@ -5554,23 +5725,34 @@ class InjectorBinder:
5554
5725
  raise TypeError('Must specify provider')
5555
5726
  if len(providers) > 1:
5556
5727
  raise TypeError('May not specify multiple providers')
5557
- provider, = providers
5728
+ provider = check.single(providers)
5558
5729
 
5559
- ##
5730
+ #
5560
5731
 
5732
+ pws: ta.List[ta.Any] = []
5733
+ if in_ is not None:
5734
+ check.issubclass(in_, InjectorScope)
5735
+ check.not_in(abc.ABC, in_.__bases__)
5736
+ pws.append(functools.partial(ScopedInjectorProvider, k=key, sc=in_))
5561
5737
  if singleton:
5562
- provider = SingletonInjectorProvider(provider)
5738
+ pws.append(SingletonInjectorProvider)
5739
+ if len(pws) > 1:
5740
+ raise TypeError('May not specify multiple provider wrappers')
5741
+ elif pws:
5742
+ provider = check.single(pws)(provider)
5743
+
5744
+ #
5563
5745
 
5564
5746
  binding = InjectorBinding(key, provider)
5565
5747
 
5566
- ##
5748
+ #
5567
5749
 
5568
5750
  extras: ta.List[InjectorBinding] = []
5569
5751
 
5570
5752
  if eager:
5571
5753
  extras.append(bind_injector_eager_key(key))
5572
5754
 
5573
- ##
5755
+ #
5574
5756
 
5575
5757
  if extras:
5576
5758
  return as_injector_bindings(binding, *extras)
@@ -5643,7 +5825,8 @@ def bind_injector_eager_key(key: ta.Any) -> InjectorBinding:
5643
5825
  return InjectorBinding(_INJECTOR_EAGER_ARRAY_KEY, ConstInjectorProvider(_InjectorEager(as_injector_key(key))))
5644
5826
 
5645
5827
 
5646
- ##
5828
+ ###
5829
+ # api
5647
5830
 
5648
5831
 
5649
5832
  class InjectionApi:
@@ -5663,9 +5846,19 @@ class InjectionApi:
5663
5846
  def as_bindings(self, *args: InjectorBindingOrBindings) -> InjectorBindings:
5664
5847
  return as_injector_bindings(*args)
5665
5848
 
5849
+ # overrides
5850
+
5666
5851
  def override(self, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
5667
5852
  return injector_override(p, *args)
5668
5853
 
5854
+ # scopes
5855
+
5856
+ def bind_scope(self, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5857
+ return bind_injector_scope(sc)
5858
+
5859
+ def bind_scope_seed(self, k: ta.Any, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
5860
+ return bind_injector_scope_seed(k, sc)
5861
+
5669
5862
  # injector
5670
5863
 
5671
5864
  def create_injector(self, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
@@ -5686,6 +5879,7 @@ class InjectionApi:
5686
5879
  to_const: ta.Any = None,
5687
5880
  to_key: ta.Any = None,
5688
5881
 
5882
+ in_: ta.Optional[ta.Type[InjectorScope]] = None,
5689
5883
  singleton: bool = False,
5690
5884
 
5691
5885
  eager: bool = False,
@@ -5702,6 +5896,7 @@ class InjectionApi:
5702
5896
  to_const=to_const,
5703
5897
  to_key=to_key,
5704
5898
 
5899
+ in_=in_,
5705
5900
  singleton=singleton,
5706
5901
 
5707
5902
  eager=eager,
@@ -6511,6 +6706,9 @@ class TempDirAtomicPathSwapping(AtomicPathSwapping):
6511
6706
  # ../../../omdev/interp/types.py
6512
6707
 
6513
6708
 
6709
+ ##
6710
+
6711
+
6514
6712
  # See https://peps.python.org/pep-3149/
6515
6713
  INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
6516
6714
  ('debug', 'd'),
@@ -6542,6 +6740,9 @@ class InterpOpts:
6542
6740
  return s, cls(**kw)
6543
6741
 
6544
6742
 
6743
+ ##
6744
+
6745
+
6545
6746
  @dc.dataclass(frozen=True)
6546
6747
  class InterpVersion:
6547
6748
  version: Version
@@ -6567,6 +6768,9 @@ class InterpVersion:
6567
6768
  return None
6568
6769
 
6569
6770
 
6771
+ ##
6772
+
6773
+
6570
6774
  @dc.dataclass(frozen=True)
6571
6775
  class InterpSpecifier:
6572
6776
  specifier: Specifier
@@ -6594,12 +6798,25 @@ class InterpSpecifier:
6594
6798
  return self.contains(iv)
6595
6799
 
6596
6800
 
6801
+ ##
6802
+
6803
+
6597
6804
  @dc.dataclass(frozen=True)
6598
6805
  class Interp:
6599
6806
  exe: str
6600
6807
  version: InterpVersion
6601
6808
 
6602
6809
 
6810
+ ########################################
6811
+ # ../../../omdev/interp/uv/inject.py
6812
+
6813
+
6814
+ def bind_interp_uv() -> InjectorBindings:
6815
+ lst: ta.List[InjectorBindingOrBindings] = []
6816
+
6817
+ return inj.as_bindings(*lst)
6818
+
6819
+
6603
6820
  ########################################
6604
6821
  # ../../configs.py
6605
6822
 
@@ -7513,6 +7730,50 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
7513
7730
  return ret.decode().strip()
7514
7731
 
7515
7732
 
7733
+ ########################################
7734
+ # ../../../omdev/interp/providers/base.py
7735
+ """
7736
+ TODO:
7737
+ - backends
7738
+ - local builds
7739
+ - deadsnakes?
7740
+ - uv
7741
+ - loose versions
7742
+ """
7743
+
7744
+
7745
+ ##
7746
+
7747
+
7748
+ class InterpProvider(abc.ABC):
7749
+ name: ta.ClassVar[str]
7750
+
7751
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
7752
+ super().__init_subclass__(**kwargs)
7753
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
7754
+ sfx = 'InterpProvider'
7755
+ if not cls.__name__.endswith(sfx):
7756
+ raise NameError(cls)
7757
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
7758
+
7759
+ @abc.abstractmethod
7760
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
7761
+ raise NotImplementedError
7762
+
7763
+ @abc.abstractmethod
7764
+ def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
7765
+ raise NotImplementedError
7766
+
7767
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
7768
+ return []
7769
+
7770
+ async def install_version(self, version: InterpVersion) -> Interp:
7771
+ raise TypeError
7772
+
7773
+
7774
+ InterpProviders = ta.NewType('InterpProviders', ta.Sequence[InterpProvider])
7775
+
7776
+
7516
7777
  ########################################
7517
7778
  # ../bootstrap.py
7518
7779
 
@@ -7547,6 +7808,37 @@ class LocalCommandExecutor(CommandExecutor):
7547
7808
  return await ce.execute(cmd)
7548
7809
 
7549
7810
 
7811
+ ########################################
7812
+ # ../deploy/deploy.py
7813
+
7814
+
7815
+ DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
7816
+
7817
+
7818
+ DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
7819
+
7820
+
7821
+ class DeployManager:
7822
+ def __init__(
7823
+ self,
7824
+ *,
7825
+
7826
+ utc_clock: ta.Optional[DeployManagerUtcClock] = None,
7827
+ ):
7828
+ super().__init__()
7829
+
7830
+ self._utc_clock = utc_clock
7831
+
7832
+ def _utc_now(self) -> datetime.datetime:
7833
+ if self._utc_clock is not None:
7834
+ return self._utc_clock() # noqa
7835
+ else:
7836
+ return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
7837
+
7838
+ def make_deploy_time(self) -> DeployTime:
7839
+ return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
7840
+
7841
+
7550
7842
  ########################################
7551
7843
  # ../deploy/paths/paths.py
7552
7844
  """
@@ -8616,25 +8908,110 @@ class InterpInspector:
8616
8908
  return ret
8617
8909
 
8618
8910
 
8619
- INTERP_INSPECTOR = InterpInspector()
8620
-
8621
-
8622
8911
  ########################################
8623
- # ../commands/subprocess.py
8624
-
8625
-
8626
- ##
8912
+ # ../../../omdev/interp/resolvers.py
8627
8913
 
8628
8914
 
8629
8915
  @dc.dataclass(frozen=True)
8630
- class SubprocessCommand(Command['SubprocessCommand.Output']):
8631
- cmd: ta.Sequence[str]
8916
+ class InterpResolverProviders:
8917
+ providers: ta.Sequence[ta.Tuple[str, InterpProvider]]
8632
8918
 
8633
- shell: bool = False
8634
- cwd: ta.Optional[str] = None
8635
- env: ta.Optional[ta.Mapping[str, str]] = None
8636
8919
 
8637
- stdout: str = 'pipe' # SubprocessChannelOption
8920
+ class InterpResolver:
8921
+ def __init__(
8922
+ self,
8923
+ providers: InterpResolverProviders,
8924
+ ) -> None:
8925
+ super().__init__()
8926
+
8927
+ self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers.providers)
8928
+
8929
+ async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
8930
+ lst = [
8931
+ (i, si)
8932
+ for i, p in enumerate(self._providers.values())
8933
+ for si in await p.get_installed_versions(spec)
8934
+ if spec.contains(si)
8935
+ ]
8936
+
8937
+ slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
8938
+ if not slst:
8939
+ return None
8940
+
8941
+ bi, bv = slst[-1]
8942
+ bp = list(self._providers.values())[bi]
8943
+ return (bp, bv)
8944
+
8945
+ async def resolve(
8946
+ self,
8947
+ spec: InterpSpecifier,
8948
+ *,
8949
+ install: bool = False,
8950
+ ) -> ta.Optional[Interp]:
8951
+ tup = await self._resolve_installed(spec)
8952
+ if tup is not None:
8953
+ bp, bv = tup
8954
+ return await bp.get_installed_version(bv)
8955
+
8956
+ if not install:
8957
+ return None
8958
+
8959
+ tp = list(self._providers.values())[0] # noqa
8960
+
8961
+ sv = sorted(
8962
+ [s for s in await tp.get_installable_versions(spec) if s in spec],
8963
+ key=lambda s: s.version,
8964
+ )
8965
+ if not sv:
8966
+ return None
8967
+
8968
+ bv = sv[-1]
8969
+ return await tp.install_version(bv)
8970
+
8971
+ async def list(self, spec: InterpSpecifier) -> None:
8972
+ print('installed:')
8973
+ for n, p in self._providers.items():
8974
+ lst = [
8975
+ si
8976
+ for si in await p.get_installed_versions(spec)
8977
+ if spec.contains(si)
8978
+ ]
8979
+ if lst:
8980
+ print(f' {n}')
8981
+ for si in lst:
8982
+ print(f' {si}')
8983
+
8984
+ print()
8985
+
8986
+ print('installable:')
8987
+ for n, p in self._providers.items():
8988
+ lst = [
8989
+ si
8990
+ for si in await p.get_installable_versions(spec)
8991
+ if spec.contains(si)
8992
+ ]
8993
+ if lst:
8994
+ print(f' {n}')
8995
+ for si in lst:
8996
+ print(f' {si}')
8997
+
8998
+
8999
+ ########################################
9000
+ # ../commands/subprocess.py
9001
+
9002
+
9003
+ ##
9004
+
9005
+
9006
+ @dc.dataclass(frozen=True)
9007
+ class SubprocessCommand(Command['SubprocessCommand.Output']):
9008
+ cmd: ta.Sequence[str]
9009
+
9010
+ shell: bool = False
9011
+ cwd: ta.Optional[str] = None
9012
+ env: ta.Optional[ta.Mapping[str, str]] = None
9013
+
9014
+ stdout: str = 'pipe' # SubprocessChannelOption
8638
9015
  stderr: str = 'pipe' # SubprocessChannelOption
8639
9016
 
8640
9017
  input: ta.Optional[bytes] = None
@@ -9270,47 +9647,7 @@ class YumSystemPackageManager(SystemPackageManager):
9270
9647
 
9271
9648
 
9272
9649
  ########################################
9273
- # ../../../omdev/interp/providers.py
9274
- """
9275
- TODO:
9276
- - backends
9277
- - local builds
9278
- - deadsnakes?
9279
- - uv
9280
- - loose versions
9281
- """
9282
-
9283
-
9284
- ##
9285
-
9286
-
9287
- class InterpProvider(abc.ABC):
9288
- name: ta.ClassVar[str]
9289
-
9290
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
9291
- super().__init_subclass__(**kwargs)
9292
- if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
9293
- sfx = 'InterpProvider'
9294
- if not cls.__name__.endswith(sfx):
9295
- raise NameError(cls)
9296
- setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
9297
-
9298
- @abc.abstractmethod
9299
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
9300
- raise NotImplementedError
9301
-
9302
- @abc.abstractmethod
9303
- def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
9304
- raise NotImplementedError
9305
-
9306
- async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
9307
- return []
9308
-
9309
- async def install_version(self, version: InterpVersion) -> Interp:
9310
- raise TypeError
9311
-
9312
-
9313
- ##
9650
+ # ../../../omdev/interp/providers/running.py
9314
9651
 
9315
9652
 
9316
9653
  class RunningInterpProvider(InterpProvider):
@@ -9331,314 +9668,132 @@ class RunningInterpProvider(InterpProvider):
9331
9668
 
9332
9669
 
9333
9670
  ########################################
9334
- # ../commands/inject.py
9671
+ # ../../../omdev/interp/providers/system.py
9672
+ """
9673
+ TODO:
9674
+ - python, python3, python3.12, ...
9675
+ - check if path py's are venvs: sys.prefix != sys.base_prefix
9676
+ """
9335
9677
 
9336
9678
 
9337
9679
  ##
9338
9680
 
9339
9681
 
9340
- def bind_command(
9341
- command_cls: ta.Type[Command],
9342
- executor_cls: ta.Optional[ta.Type[CommandExecutor]],
9343
- ) -> InjectorBindings:
9344
- lst: ta.List[InjectorBindingOrBindings] = [
9345
- inj.bind(CommandRegistration(command_cls), array=True),
9346
- ]
9347
-
9348
- if executor_cls is not None:
9349
- lst.extend([
9350
- inj.bind(executor_cls, singleton=True),
9351
- inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
9352
- ])
9682
+ class SystemInterpProvider(InterpProvider):
9683
+ @dc.dataclass(frozen=True)
9684
+ class Options:
9685
+ cmd: str = 'python3' # FIXME: unused lol
9686
+ path: ta.Optional[str] = None
9353
9687
 
9354
- return inj.as_bindings(*lst)
9688
+ inspect: bool = False
9355
9689
 
9690
+ def __init__(
9691
+ self,
9692
+ options: Options = Options(),
9693
+ *,
9694
+ inspector: ta.Optional[InterpInspector] = None,
9695
+ ) -> None:
9696
+ super().__init__()
9356
9697
 
9357
- ##
9698
+ self._options = options
9358
9699
 
9700
+ self._inspector = inspector
9359
9701
 
9360
- @dc.dataclass(frozen=True)
9361
- class _FactoryCommandExecutor(CommandExecutor):
9362
- factory: ta.Callable[[], CommandExecutor]
9702
+ #
9363
9703
 
9364
- def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
9365
- return self.factory().execute(i)
9704
+ @staticmethod
9705
+ def _re_which(
9706
+ pat: re.Pattern,
9707
+ *,
9708
+ mode: int = os.F_OK | os.X_OK,
9709
+ path: ta.Optional[str] = None,
9710
+ ) -> ta.List[str]:
9711
+ if path is None:
9712
+ path = os.environ.get('PATH', None)
9713
+ if path is None:
9714
+ try:
9715
+ path = os.confstr('CS_PATH')
9716
+ except (AttributeError, ValueError):
9717
+ path = os.defpath
9366
9718
 
9719
+ if not path:
9720
+ return []
9367
9721
 
9368
- ##
9722
+ path = os.fsdecode(path)
9723
+ pathlst = path.split(os.pathsep)
9369
9724
 
9725
+ def _access_check(fn: str, mode: int) -> bool:
9726
+ return os.path.exists(fn) and os.access(fn, mode)
9370
9727
 
9371
- def bind_commands(
9372
- *,
9373
- main_config: MainConfig,
9374
- ) -> InjectorBindings:
9375
- lst: ta.List[InjectorBindingOrBindings] = [
9376
- inj.bind_array(CommandRegistration),
9377
- inj.bind_array_type(CommandRegistration, CommandRegistrations),
9728
+ out = []
9729
+ seen = set()
9730
+ for d in pathlst:
9731
+ normdir = os.path.normcase(d)
9732
+ if normdir not in seen:
9733
+ seen.add(normdir)
9734
+ if not _access_check(normdir, mode):
9735
+ continue
9736
+ for thefile in os.listdir(d):
9737
+ name = os.path.join(d, thefile)
9738
+ if not (
9739
+ os.path.isfile(name) and
9740
+ pat.fullmatch(thefile) and
9741
+ _access_check(name, mode)
9742
+ ):
9743
+ continue
9744
+ out.append(name)
9378
9745
 
9379
- inj.bind_array(CommandExecutorRegistration),
9380
- inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
9746
+ return out
9381
9747
 
9382
- inj.bind(build_command_name_map, singleton=True),
9383
- ]
9748
+ @cached_nullary
9749
+ def exes(self) -> ta.List[str]:
9750
+ return self._re_which(
9751
+ re.compile(r'python3(\.\d+)?'),
9752
+ path=self._options.path,
9753
+ )
9384
9754
 
9385
9755
  #
9386
9756
 
9387
- def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
9388
- return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
9757
+ async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
9758
+ if not self._options.inspect:
9759
+ s = os.path.basename(exe)
9760
+ if s.startswith('python'):
9761
+ s = s[len('python'):]
9762
+ if '.' in s:
9763
+ try:
9764
+ return InterpVersion.parse(s)
9765
+ except InvalidVersion:
9766
+ pass
9767
+ ii = await check.not_none(self._inspector).inspect(exe)
9768
+ return ii.iv if ii is not None else None
9389
9769
 
9390
- lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
9770
+ async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
9771
+ lst = []
9772
+ for e in self.exes():
9773
+ if (ev := await self.get_exe_version(e)) is None:
9774
+ log.debug('Invalid system version: %s', e)
9775
+ continue
9776
+ lst.append((e, ev))
9777
+ return lst
9391
9778
 
9392
9779
  #
9393
9780
 
9394
- def provide_command_executor_map(
9395
- injector: Injector,
9396
- crs: CommandExecutorRegistrations,
9397
- ) -> CommandExecutorMap:
9398
- dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
9781
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
9782
+ return [ev for e, ev in await self.exe_versions()]
9399
9783
 
9400
- cr: CommandExecutorRegistration
9401
- for cr in crs:
9402
- if cr.command_cls in dct:
9403
- raise KeyError(cr.command_cls)
9404
-
9405
- factory = functools.partial(injector.provide, cr.executor_cls)
9406
- if main_config.debug:
9407
- ce = factory()
9408
- else:
9409
- ce = _FactoryCommandExecutor(factory)
9410
-
9411
- dct[cr.command_cls] = ce
9412
-
9413
- return CommandExecutorMap(dct)
9414
-
9415
- lst.extend([
9416
- inj.bind(provide_command_executor_map, singleton=True),
9417
-
9418
- inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
9419
- ])
9420
-
9421
- #
9422
-
9423
- lst.extend([
9424
- bind_command(PingCommand, PingCommandExecutor),
9425
- bind_command(SubprocessCommand, SubprocessCommandExecutor),
9426
- ])
9427
-
9428
- #
9429
-
9430
- return inj.as_bindings(*lst)
9431
-
9432
-
9433
- ########################################
9434
- # ../deploy/paths/manager.py
9435
-
9436
-
9437
- class DeployPathsManager:
9438
- def __init__(
9439
- self,
9440
- *,
9441
- deploy_path_owners: DeployPathOwners,
9442
- ) -> None:
9443
- super().__init__()
9444
-
9445
- self._deploy_path_owners = deploy_path_owners
9446
-
9447
- @cached_nullary
9448
- def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
9449
- dct: ta.Dict[DeployPath, DeployPathOwner] = {}
9450
- for o in self._deploy_path_owners:
9451
- for p in o.get_owned_deploy_paths():
9452
- if p in dct:
9453
- raise DeployPathError(f'Duplicate deploy path owner: {p}')
9454
- dct[p] = o
9455
- return dct
9456
-
9457
- def validate_deploy_paths(self) -> None:
9458
- self.owners_by_path()
9459
-
9460
-
9461
- ########################################
9462
- # ../deploy/tmp.py
9463
-
9464
-
9465
- class DeployHomeAtomics(Func1[DeployHome, AtomicPathSwapping]):
9466
- pass
9467
-
9468
-
9469
- class DeployTmpManager(
9470
- SingleDirDeployPathOwner,
9471
- ):
9472
- def __init__(self) -> None:
9473
- super().__init__(
9474
- owned_dir='tmp',
9475
- )
9476
-
9477
- def get_swapping(self, home: DeployHome) -> AtomicPathSwapping:
9478
- return TempDirAtomicPathSwapping(
9479
- temp_dir=self._make_dir(home),
9480
- root_dir=check.non_empty_str(home),
9481
- )
9482
-
9483
-
9484
- ########################################
9485
- # ../remote/connection.py
9486
-
9487
-
9488
- ##
9489
-
9490
-
9491
- class PyremoteRemoteExecutionConnector:
9492
- def __init__(
9493
- self,
9494
- *,
9495
- spawning: RemoteSpawning,
9496
- msh: ObjMarshalerManager,
9497
- payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
9498
- ) -> None:
9499
- super().__init__()
9500
-
9501
- self._spawning = spawning
9502
- self._msh = msh
9503
- self._payload_file = payload_file
9504
-
9505
- #
9506
-
9507
- @cached_nullary
9508
- def _payload_src(self) -> str:
9509
- return get_remote_payload_src(file=self._payload_file)
9510
-
9511
- @cached_nullary
9512
- def _remote_src(self) -> ta.Sequence[str]:
9513
- return [
9514
- self._payload_src(),
9515
- '_remote_execution_main()',
9516
- ]
9517
-
9518
- @cached_nullary
9519
- def _spawn_src(self) -> str:
9520
- return pyremote_build_bootstrap_cmd(__package__ or 'manage')
9521
-
9522
- #
9523
-
9524
- @contextlib.asynccontextmanager
9525
- async def connect(
9526
- self,
9527
- tgt: RemoteSpawning.Target,
9528
- bs: MainBootstrap,
9529
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9530
- spawn_src = self._spawn_src()
9531
- remote_src = self._remote_src()
9532
-
9533
- async with self._spawning.spawn(
9534
- tgt,
9535
- spawn_src,
9536
- debug=bs.main_config.debug,
9537
- ) as proc:
9538
- res = await PyremoteBootstrapDriver( # noqa
9539
- remote_src,
9540
- PyremoteBootstrapOptions(
9541
- debug=bs.main_config.debug,
9542
- ),
9543
- ).async_run(
9544
- proc.stdout,
9545
- proc.stdin,
9546
- )
9547
-
9548
- chan = RemoteChannelImpl(
9549
- proc.stdout,
9550
- proc.stdin,
9551
- msh=self._msh,
9784
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
9785
+ for e, ev in await self.exe_versions():
9786
+ if ev != version:
9787
+ continue
9788
+ return Interp(
9789
+ exe=e,
9790
+ version=ev,
9552
9791
  )
9553
-
9554
- await chan.send_obj(bs)
9555
-
9556
- rce: RemoteCommandExecutor
9557
- async with aclosing(RemoteCommandExecutor(chan)) as rce:
9558
- await rce.start()
9559
-
9560
- yield rce
9561
-
9562
-
9563
- ##
9564
-
9565
-
9566
- class InProcessRemoteExecutionConnector:
9567
- def __init__(
9568
- self,
9569
- *,
9570
- msh: ObjMarshalerManager,
9571
- local_executor: LocalCommandExecutor,
9572
- ) -> None:
9573
- super().__init__()
9574
-
9575
- self._msh = msh
9576
- self._local_executor = local_executor
9577
-
9578
- @contextlib.asynccontextmanager
9579
- async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9580
- r0, w0 = asyncio_create_bytes_channel()
9581
- r1, w1 = asyncio_create_bytes_channel()
9582
-
9583
- remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
9584
- local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
9585
-
9586
- rch = _RemoteCommandHandler(
9587
- remote_chan,
9588
- self._local_executor,
9589
- )
9590
- rch_task = asyncio.create_task(rch.run()) # noqa
9591
- try:
9592
- rce: RemoteCommandExecutor
9593
- async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
9594
- await rce.start()
9595
-
9596
- yield rce
9597
-
9598
- finally:
9599
- rch.stop()
9600
- await rch_task
9601
-
9602
-
9603
- ########################################
9604
- # ../system/commands.py
9605
-
9606
-
9607
- ##
9608
-
9609
-
9610
- @dc.dataclass(frozen=True)
9611
- class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
9612
- pkgs: ta.Sequence[str] = ()
9613
-
9614
- def __post_init__(self) -> None:
9615
- check.not_isinstance(self.pkgs, str)
9616
-
9617
- @dc.dataclass(frozen=True)
9618
- class Output(Command.Output):
9619
- pkgs: ta.Sequence[SystemPackage]
9620
-
9621
-
9622
- class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
9623
- def __init__(
9624
- self,
9625
- *,
9626
- mgr: SystemPackageManager,
9627
- ) -> None:
9628
- super().__init__()
9629
-
9630
- self._mgr = mgr
9631
-
9632
- async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
9633
- log.info('Checking system package!')
9634
-
9635
- ret = await self._mgr.query(*cmd.pkgs)
9636
-
9637
- return CheckSystemPackageCommand.Output(list(ret.values()))
9792
+ raise KeyError(version)
9638
9793
 
9639
9794
 
9640
9795
  ########################################
9641
- # ../../../omdev/interp/pyenv.py
9796
+ # ../../../omdev/interp/pyenv/pyenv.py
9642
9797
  """
9643
9798
  TODO:
9644
9799
  - custom tags
@@ -9868,9 +10023,10 @@ class PyenvVersionInstaller:
9868
10023
  opts: ta.Optional[PyenvInstallOpts] = None,
9869
10024
  interp_opts: InterpOpts = InterpOpts(),
9870
10025
  *,
10026
+ pyenv: Pyenv,
10027
+
9871
10028
  install_name: ta.Optional[str] = None,
9872
10029
  no_default_opts: bool = False,
9873
- pyenv: Pyenv = Pyenv(),
9874
10030
  ) -> None:
9875
10031
  super().__init__()
9876
10032
 
@@ -9960,26 +10116,26 @@ class PyenvVersionInstaller:
9960
10116
 
9961
10117
 
9962
10118
  class PyenvInterpProvider(InterpProvider):
9963
- def __init__(
9964
- self,
9965
- pyenv: Pyenv = Pyenv(),
10119
+ @dc.dataclass(frozen=True)
10120
+ class Options:
10121
+ inspect: bool = False
9966
10122
 
9967
- inspect: bool = False,
9968
- inspector: InterpInspector = INTERP_INSPECTOR,
10123
+ try_update: bool = False
9969
10124
 
10125
+ def __init__(
10126
+ self,
10127
+ options: Options = Options(),
9970
10128
  *,
9971
-
9972
- try_update: bool = False,
10129
+ pyenv: Pyenv,
10130
+ inspector: InterpInspector,
9973
10131
  ) -> None:
9974
10132
  super().__init__()
9975
10133
 
9976
- self._pyenv = pyenv
10134
+ self._options = options
9977
10135
 
9978
- self._inspect = inspect
10136
+ self._pyenv = pyenv
9979
10137
  self._inspector = inspector
9980
10138
 
9981
- self._try_update = try_update
9982
-
9983
10139
  #
9984
10140
 
9985
10141
  @staticmethod
@@ -10004,7 +10160,7 @@ class PyenvInterpProvider(InterpProvider):
10004
10160
 
10005
10161
  async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
10006
10162
  iv: ta.Optional[InterpVersion]
10007
- if self._inspect:
10163
+ if self._options.inspect:
10008
10164
  try:
10009
10165
  iv = check.not_none(await self._inspector.inspect(ep)).iv
10010
10166
  except Exception as e: # noqa
@@ -10060,7 +10216,7 @@ class PyenvInterpProvider(InterpProvider):
10060
10216
  async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10061
10217
  lst = await self._get_installable_versions(spec)
10062
10218
 
10063
- if self._try_update and not any(v in spec for v in lst):
10219
+ if self._options.try_update and not any(v in spec for v in lst):
10064
10220
  if self._pyenv.update():
10065
10221
  lst = await self._get_installable_versions(spec)
10066
10222
 
@@ -10076,6 +10232,7 @@ class PyenvInterpProvider(InterpProvider):
10076
10232
  installer = PyenvVersionInstaller(
10077
10233
  inst_version,
10078
10234
  interp_opts=inst_opts,
10235
+ pyenv=self._pyenv,
10079
10236
  )
10080
10237
 
10081
10238
  exe = await installer.install()
@@ -10083,116 +10240,344 @@ class PyenvInterpProvider(InterpProvider):
10083
10240
 
10084
10241
 
10085
10242
  ########################################
10086
- # ../../../omdev/interp/system.py
10087
- """
10088
- TODO:
10089
- - python, python3, python3.12, ...
10090
- - check if path py's are venvs: sys.prefix != sys.base_prefix
10091
- """
10243
+ # ../commands/inject.py
10244
+
10245
+
10246
+ ##
10247
+
10248
+
10249
+ def bind_command(
10250
+ command_cls: ta.Type[Command],
10251
+ executor_cls: ta.Optional[ta.Type[CommandExecutor]],
10252
+ ) -> InjectorBindings:
10253
+ lst: ta.List[InjectorBindingOrBindings] = [
10254
+ inj.bind(CommandRegistration(command_cls), array=True),
10255
+ ]
10256
+
10257
+ if executor_cls is not None:
10258
+ lst.extend([
10259
+ inj.bind(executor_cls, singleton=True),
10260
+ inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
10261
+ ])
10262
+
10263
+ return inj.as_bindings(*lst)
10264
+
10265
+
10266
+ ##
10267
+
10268
+
10269
+ @dc.dataclass(frozen=True)
10270
+ class _FactoryCommandExecutor(CommandExecutor):
10271
+ factory: ta.Callable[[], CommandExecutor]
10272
+
10273
+ def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
10274
+ return self.factory().execute(i)
10275
+
10276
+
10277
+ ##
10278
+
10279
+
10280
+ def bind_commands(
10281
+ *,
10282
+ main_config: MainConfig,
10283
+ ) -> InjectorBindings:
10284
+ lst: ta.List[InjectorBindingOrBindings] = [
10285
+ inj.bind_array(CommandRegistration),
10286
+ inj.bind_array_type(CommandRegistration, CommandRegistrations),
10287
+
10288
+ inj.bind_array(CommandExecutorRegistration),
10289
+ inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
10290
+
10291
+ inj.bind(build_command_name_map, singleton=True),
10292
+ ]
10293
+
10294
+ #
10295
+
10296
+ def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
10297
+ return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
10298
+
10299
+ lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
10300
+
10301
+ #
10302
+
10303
+ def provide_command_executor_map(
10304
+ injector: Injector,
10305
+ crs: CommandExecutorRegistrations,
10306
+ ) -> CommandExecutorMap:
10307
+ dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
10308
+
10309
+ cr: CommandExecutorRegistration
10310
+ for cr in crs:
10311
+ if cr.command_cls in dct:
10312
+ raise KeyError(cr.command_cls)
10313
+
10314
+ factory = functools.partial(injector.provide, cr.executor_cls)
10315
+ if main_config.debug:
10316
+ ce = factory()
10317
+ else:
10318
+ ce = _FactoryCommandExecutor(factory)
10319
+
10320
+ dct[cr.command_cls] = ce
10321
+
10322
+ return CommandExecutorMap(dct)
10323
+
10324
+ lst.extend([
10325
+ inj.bind(provide_command_executor_map, singleton=True),
10326
+
10327
+ inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
10328
+ ])
10329
+
10330
+ #
10331
+
10332
+ lst.extend([
10333
+ bind_command(PingCommand, PingCommandExecutor),
10334
+ bind_command(SubprocessCommand, SubprocessCommandExecutor),
10335
+ ])
10336
+
10337
+ #
10338
+
10339
+ return inj.as_bindings(*lst)
10340
+
10341
+
10342
+ ########################################
10343
+ # ../deploy/paths/manager.py
10344
+
10345
+
10346
+ class DeployPathsManager:
10347
+ def __init__(
10348
+ self,
10349
+ *,
10350
+ deploy_path_owners: DeployPathOwners,
10351
+ ) -> None:
10352
+ super().__init__()
10353
+
10354
+ self._deploy_path_owners = deploy_path_owners
10355
+
10356
+ @cached_nullary
10357
+ def owners_by_path(self) -> ta.Mapping[DeployPath, DeployPathOwner]:
10358
+ dct: ta.Dict[DeployPath, DeployPathOwner] = {}
10359
+ for o in self._deploy_path_owners:
10360
+ for p in o.get_owned_deploy_paths():
10361
+ if p in dct:
10362
+ raise DeployPathError(f'Duplicate deploy path owner: {p}')
10363
+ dct[p] = o
10364
+ return dct
10365
+
10366
+ def validate_deploy_paths(self) -> None:
10367
+ self.owners_by_path()
10368
+
10369
+
10370
+ ########################################
10371
+ # ../deploy/tmp.py
10372
+
10373
+
10374
+ class DeployHomeAtomics(Func1[DeployHome, AtomicPathSwapping]):
10375
+ pass
10376
+
10377
+
10378
+ class DeployTmpManager(
10379
+ SingleDirDeployPathOwner,
10380
+ ):
10381
+ def __init__(self) -> None:
10382
+ super().__init__(
10383
+ owned_dir='tmp',
10384
+ )
10385
+
10386
+ def get_swapping(self, home: DeployHome) -> AtomicPathSwapping:
10387
+ return TempDirAtomicPathSwapping(
10388
+ temp_dir=self._make_dir(home),
10389
+ root_dir=check.non_empty_str(home),
10390
+ )
10391
+
10392
+
10393
+ ########################################
10394
+ # ../remote/connection.py
10395
+
10396
+
10397
+ ##
10398
+
10399
+
10400
+ class PyremoteRemoteExecutionConnector:
10401
+ def __init__(
10402
+ self,
10403
+ *,
10404
+ spawning: RemoteSpawning,
10405
+ msh: ObjMarshalerManager,
10406
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
10407
+ ) -> None:
10408
+ super().__init__()
10409
+
10410
+ self._spawning = spawning
10411
+ self._msh = msh
10412
+ self._payload_file = payload_file
10413
+
10414
+ #
10415
+
10416
+ @cached_nullary
10417
+ def _payload_src(self) -> str:
10418
+ return get_remote_payload_src(file=self._payload_file)
10419
+
10420
+ @cached_nullary
10421
+ def _remote_src(self) -> ta.Sequence[str]:
10422
+ return [
10423
+ self._payload_src(),
10424
+ '_remote_execution_main()',
10425
+ ]
10426
+
10427
+ @cached_nullary
10428
+ def _spawn_src(self) -> str:
10429
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
10430
+
10431
+ #
10432
+
10433
+ @contextlib.asynccontextmanager
10434
+ async def connect(
10435
+ self,
10436
+ tgt: RemoteSpawning.Target,
10437
+ bs: MainBootstrap,
10438
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
10439
+ spawn_src = self._spawn_src()
10440
+ remote_src = self._remote_src()
10441
+
10442
+ async with self._spawning.spawn(
10443
+ tgt,
10444
+ spawn_src,
10445
+ debug=bs.main_config.debug,
10446
+ ) as proc:
10447
+ res = await PyremoteBootstrapDriver( # noqa
10448
+ remote_src,
10449
+ PyremoteBootstrapOptions(
10450
+ debug=bs.main_config.debug,
10451
+ ),
10452
+ ).async_run(
10453
+ proc.stdout,
10454
+ proc.stdin,
10455
+ )
10456
+
10457
+ chan = RemoteChannelImpl(
10458
+ proc.stdout,
10459
+ proc.stdin,
10460
+ msh=self._msh,
10461
+ )
10462
+
10463
+ await chan.send_obj(bs)
10464
+
10465
+ rce: RemoteCommandExecutor
10466
+ async with aclosing(RemoteCommandExecutor(chan)) as rce:
10467
+ await rce.start()
10468
+
10469
+ yield rce
10470
+
10471
+
10472
+ ##
10473
+
10474
+
10475
+ class InProcessRemoteExecutionConnector:
10476
+ def __init__(
10477
+ self,
10478
+ *,
10479
+ msh: ObjMarshalerManager,
10480
+ local_executor: LocalCommandExecutor,
10481
+ ) -> None:
10482
+ super().__init__()
10483
+
10484
+ self._msh = msh
10485
+ self._local_executor = local_executor
10486
+
10487
+ @contextlib.asynccontextmanager
10488
+ async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
10489
+ r0, w0 = asyncio_create_bytes_channel()
10490
+ r1, w1 = asyncio_create_bytes_channel()
10491
+
10492
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
10493
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
10494
+
10495
+ rch = _RemoteCommandHandler(
10496
+ remote_chan,
10497
+ self._local_executor,
10498
+ )
10499
+ rch_task = asyncio.create_task(rch.run()) # noqa
10500
+ try:
10501
+ rce: RemoteCommandExecutor
10502
+ async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
10503
+ await rce.start()
10504
+
10505
+ yield rce
10506
+
10507
+ finally:
10508
+ rch.stop()
10509
+ await rch_task
10510
+
10511
+
10512
+ ########################################
10513
+ # ../system/commands.py
10514
+
10515
+
10516
+ ##
10517
+
10518
+
10519
+ @dc.dataclass(frozen=True)
10520
+ class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
10521
+ pkgs: ta.Sequence[str] = ()
10522
+
10523
+ def __post_init__(self) -> None:
10524
+ check.not_isinstance(self.pkgs, str)
10525
+
10526
+ @dc.dataclass(frozen=True)
10527
+ class Output(Command.Output):
10528
+ pkgs: ta.Sequence[SystemPackage]
10092
10529
 
10093
10530
 
10094
- ##
10531
+ class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
10532
+ def __init__(
10533
+ self,
10534
+ *,
10535
+ mgr: SystemPackageManager,
10536
+ ) -> None:
10537
+ super().__init__()
10095
10538
 
10539
+ self._mgr = mgr
10096
10540
 
10097
- @dc.dataclass(frozen=True)
10098
- class SystemInterpProvider(InterpProvider):
10099
- cmd: str = 'python3'
10100
- path: ta.Optional[str] = None
10541
+ async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
10542
+ log.info('Checking system package!')
10101
10543
 
10102
- inspect: bool = False
10103
- inspector: InterpInspector = INTERP_INSPECTOR
10544
+ ret = await self._mgr.query(*cmd.pkgs)
10104
10545
 
10105
- #
10546
+ return CheckSystemPackageCommand.Output(list(ret.values()))
10106
10547
 
10107
- @staticmethod
10108
- def _re_which(
10109
- pat: re.Pattern,
10110
- *,
10111
- mode: int = os.F_OK | os.X_OK,
10112
- path: ta.Optional[str] = None,
10113
- ) -> ta.List[str]:
10114
- if path is None:
10115
- path = os.environ.get('PATH', None)
10116
- if path is None:
10117
- try:
10118
- path = os.confstr('CS_PATH')
10119
- except (AttributeError, ValueError):
10120
- path = os.defpath
10121
10548
 
10122
- if not path:
10123
- return []
10549
+ ########################################
10550
+ # ../../../omdev/interp/providers/inject.py
10124
10551
 
10125
- path = os.fsdecode(path)
10126
- pathlst = path.split(os.pathsep)
10127
10552
 
10128
- def _access_check(fn: str, mode: int) -> bool:
10129
- return os.path.exists(fn) and os.access(fn, mode)
10553
+ def bind_interp_providers() -> InjectorBindings:
10554
+ lst: ta.List[InjectorBindingOrBindings] = [
10555
+ inj.bind_array(InterpProvider),
10556
+ inj.bind_array_type(InterpProvider, InterpProviders),
10130
10557
 
10131
- out = []
10132
- seen = set()
10133
- for d in pathlst:
10134
- normdir = os.path.normcase(d)
10135
- if normdir not in seen:
10136
- seen.add(normdir)
10137
- if not _access_check(normdir, mode):
10138
- continue
10139
- for thefile in os.listdir(d):
10140
- name = os.path.join(d, thefile)
10141
- if not (
10142
- os.path.isfile(name) and
10143
- pat.fullmatch(thefile) and
10144
- _access_check(name, mode)
10145
- ):
10146
- continue
10147
- out.append(name)
10558
+ inj.bind(RunningInterpProvider, singleton=True),
10559
+ inj.bind(InterpProvider, to_key=RunningInterpProvider, array=True),
10148
10560
 
10149
- return out
10561
+ inj.bind(SystemInterpProvider, singleton=True),
10562
+ inj.bind(InterpProvider, to_key=SystemInterpProvider, array=True),
10563
+ ]
10150
10564
 
10151
- @cached_nullary
10152
- def exes(self) -> ta.List[str]:
10153
- return self._re_which(
10154
- re.compile(r'python3(\.\d+)?'),
10155
- path=self.path,
10156
- )
10565
+ return inj.as_bindings(*lst)
10157
10566
 
10158
- #
10159
10567
 
10160
- async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
10161
- if not self.inspect:
10162
- s = os.path.basename(exe)
10163
- if s.startswith('python'):
10164
- s = s[len('python'):]
10165
- if '.' in s:
10166
- try:
10167
- return InterpVersion.parse(s)
10168
- except InvalidVersion:
10169
- pass
10170
- ii = await self.inspector.inspect(exe)
10171
- return ii.iv if ii is not None else None
10568
+ ########################################
10569
+ # ../../../omdev/interp/pyenv/inject.py
10172
10570
 
10173
- async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
10174
- lst = []
10175
- for e in self.exes():
10176
- if (ev := await self.get_exe_version(e)) is None:
10177
- log.debug('Invalid system version: %s', e)
10178
- continue
10179
- lst.append((e, ev))
10180
- return lst
10181
10571
 
10182
- #
10572
+ def bind_interp_pyenv() -> InjectorBindings:
10573
+ lst: ta.List[InjectorBindingOrBindings] = [
10574
+ inj.bind(Pyenv, singleton=True),
10183
10575
 
10184
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
10185
- return [ev for e, ev in await self.exe_versions()]
10576
+ inj.bind(PyenvInterpProvider, singleton=True),
10577
+ inj.bind(InterpProvider, to_key=PyenvInterpProvider, array=True),
10578
+ ]
10186
10579
 
10187
- async def get_installed_version(self, version: InterpVersion) -> Interp:
10188
- for e, ev in await self.exe_versions():
10189
- if ev != version:
10190
- continue
10191
- return Interp(
10192
- exe=e,
10193
- version=ev,
10194
- )
10195
- raise KeyError(version)
10580
+ return inj.as_bindings(*lst)
10196
10581
 
10197
10582
 
10198
10583
  ########################################
@@ -10559,101 +10944,44 @@ class SshManageTargetConnector(ManageTargetConnector):
10559
10944
 
10560
10945
 
10561
10946
  ########################################
10562
- # ../../../omdev/interp/resolvers.py
10563
-
10564
-
10565
- INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
10566
- cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
10567
- }
10568
-
10569
-
10570
- class InterpResolver:
10571
- def __init__(
10572
- self,
10573
- providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
10574
- ) -> None:
10575
- super().__init__()
10576
-
10577
- self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
10578
-
10579
- async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
10580
- lst = [
10581
- (i, si)
10582
- for i, p in enumerate(self._providers.values())
10583
- for si in await p.get_installed_versions(spec)
10584
- if spec.contains(si)
10585
- ]
10586
-
10587
- slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
10588
- if not slst:
10589
- return None
10947
+ # ../../../omdev/interp/inject.py
10590
10948
 
10591
- bi, bv = slst[-1]
10592
- bp = list(self._providers.values())[bi]
10593
- return (bp, bv)
10594
10949
 
10595
- async def resolve(
10596
- self,
10597
- spec: InterpSpecifier,
10598
- *,
10599
- install: bool = False,
10600
- ) -> ta.Optional[Interp]:
10601
- tup = await self._resolve_installed(spec)
10602
- if tup is not None:
10603
- bp, bv = tup
10604
- return await bp.get_installed_version(bv)
10950
+ def bind_interp() -> InjectorBindings:
10951
+ lst: ta.List[InjectorBindingOrBindings] = [
10952
+ bind_interp_providers(),
10605
10953
 
10606
- if not install:
10607
- return None
10954
+ bind_interp_pyenv(),
10608
10955
 
10609
- tp = list(self._providers.values())[0] # noqa
10956
+ bind_interp_uv(),
10610
10957
 
10611
- sv = sorted(
10612
- [s for s in await tp.get_installable_versions(spec) if s in spec],
10613
- key=lambda s: s.version,
10614
- )
10615
- if not sv:
10616
- return None
10958
+ inj.bind(InterpInspector, singleton=True),
10959
+ ]
10617
10960
 
10618
- bv = sv[-1]
10619
- return await tp.install_version(bv)
10961
+ #
10620
10962
 
10621
- async def list(self, spec: InterpSpecifier) -> None:
10622
- print('installed:')
10623
- for n, p in self._providers.items():
10624
- lst = [
10625
- si
10626
- for si in await p.get_installed_versions(spec)
10627
- if spec.contains(si)
10963
+ def provide_interp_resolver_providers(injector: Injector) -> InterpResolverProviders:
10964
+ # FIXME: lol
10965
+ rps: ta.List[ta.Any] = [
10966
+ injector.provide(c)
10967
+ for c in [
10968
+ PyenvInterpProvider,
10969
+ RunningInterpProvider,
10970
+ SystemInterpProvider,
10628
10971
  ]
10629
- if lst:
10630
- print(f' {n}')
10631
- for si in lst:
10632
- print(f' {si}')
10633
-
10634
- print()
10972
+ ]
10635
10973
 
10636
- print('installable:')
10637
- for n, p in self._providers.items():
10638
- lst = [
10639
- si
10640
- for si in await p.get_installable_versions(spec)
10641
- if spec.contains(si)
10642
- ]
10643
- if lst:
10644
- print(f' {n}')
10645
- for si in lst:
10646
- print(f' {si}')
10974
+ return InterpResolverProviders([(rp.name, rp) for rp in rps])
10647
10975
 
10976
+ lst.append(inj.bind(provide_interp_resolver_providers, singleton=True))
10648
10977
 
10649
- DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
10650
- # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
10651
- PyenvInterpProvider(try_update=True),
10978
+ lst.extend([
10979
+ inj.bind(InterpResolver, singleton=True),
10980
+ ])
10652
10981
 
10653
- RunningInterpProvider(),
10982
+ #
10654
10983
 
10655
- SystemInterpProvider(),
10656
- ]])
10984
+ return inj.as_bindings(*lst)
10657
10985
 
10658
10986
 
10659
10987
  ########################################
@@ -10685,6 +11013,15 @@ def bind_targets() -> InjectorBindings:
10685
11013
  return inj.as_bindings(*lst)
10686
11014
 
10687
11015
 
11016
+ ########################################
11017
+ # ../../../omdev/interp/default.py
11018
+
11019
+
11020
+ @cached_nullary
11021
+ def get_default_interp_resolver() -> InterpResolver:
11022
+ return inj.create_injector(bind_interp())[InterpResolver]
11023
+
11024
+
10688
11025
  ########################################
10689
11026
  # ../deploy/interp.py
10690
11027
 
@@ -10707,7 +11044,7 @@ class InterpCommand(Command['InterpCommand.Output']):
10707
11044
  class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
10708
11045
  async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
10709
11046
  i = InterpSpecifier.parse(check.not_none(cmd.spec))
10710
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
11047
+ o = check.not_none(await get_default_interp_resolver().resolve(i, install=cmd.install))
10711
11048
  return InterpCommand.Output(
10712
11049
  exe=o.exe,
10713
11050
  version=str(o.version.version),
@@ -10734,7 +11071,7 @@ class DeployVenvManager:
10734
11071
  ) -> None:
10735
11072
  if spec.interp is not None:
10736
11073
  i = InterpSpecifier.parse(check.not_none(spec.interp))
10737
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i))
11074
+ o = check.not_none(await get_default_interp_resolver().resolve(i))
10738
11075
  sys_exe = o.exe
10739
11076
  else:
10740
11077
  sys_exe = 'python3'
@@ -10929,65 +11266,46 @@ class DeployAppManager(DeployPathOwner):
10929
11266
 
10930
11267
 
10931
11268
  ########################################
10932
- # ../deploy/deploy.py
10933
-
10934
-
10935
- DEPLOY_TAG_DATETIME_FMT = '%Y%m%dT%H%M%SZ'
11269
+ # ../deploy/driver.py
10936
11270
 
10937
11271
 
10938
- DeployManagerUtcClock = ta.NewType('DeployManagerUtcClock', Func0[datetime.datetime])
11272
+ class DeployDriverFactory(Func1[DeploySpec, ta.ContextManager['DeployDriver']]):
11273
+ pass
10939
11274
 
10940
11275
 
10941
- class DeployManager:
11276
+ class DeployDriver:
10942
11277
  def __init__(
10943
11278
  self,
10944
11279
  *,
10945
- apps: DeployAppManager,
10946
- paths: DeployPathsManager,
11280
+ spec: DeploySpec,
11281
+ home: DeployHome,
11282
+ time: DeployTime,
10947
11283
 
10948
- utc_clock: ta.Optional[DeployManagerUtcClock] = None,
10949
- ):
11284
+ paths: DeployPathsManager,
11285
+ apps: DeployAppManager,
11286
+ ) -> None:
10950
11287
  super().__init__()
10951
11288
 
10952
- self._apps = apps
10953
- self._paths = paths
10954
-
10955
- self._utc_clock = utc_clock
10956
-
10957
- def _utc_now(self) -> datetime.datetime:
10958
- if self._utc_clock is not None:
10959
- return self._utc_clock() # noqa
10960
- else:
10961
- return datetime.datetime.now(tz=datetime.timezone.utc) # noqa
11289
+ self._spec = spec
11290
+ self._home = home
11291
+ self._time = time
10962
11292
 
10963
- def _make_deploy_time(self) -> DeployTime:
10964
- return DeployTime(self._utc_now().strftime(DEPLOY_TAG_DATETIME_FMT))
11293
+ self._paths = paths
11294
+ self._apps = apps
10965
11295
 
10966
- async def run_deploy(
10967
- self,
10968
- spec: DeploySpec,
10969
- ) -> None:
11296
+ async def drive_deploy(self) -> None:
10970
11297
  self._paths.validate_deploy_paths()
10971
11298
 
10972
11299
  #
10973
11300
 
10974
- hs = check.non_empty_str(spec.home)
10975
- hs = os.path.expanduser(hs)
10976
- hs = os.path.realpath(hs)
10977
- hs = os.path.abspath(hs)
10978
-
10979
- home = DeployHome(hs)
10980
-
10981
- #
10982
-
10983
11301
  deploy_tags = DeployTagMap(
10984
- self._make_deploy_time(),
10985
- spec.key(),
11302
+ self._time,
11303
+ self._spec.key(),
10986
11304
  )
10987
11305
 
10988
11306
  #
10989
11307
 
10990
- for app in spec.apps:
11308
+ for app in self._spec.apps:
10991
11309
  app_tags = deploy_tags.add(
10992
11310
  app.app,
10993
11311
  app.key(),
@@ -10996,7 +11314,7 @@ class DeployManager:
10996
11314
 
10997
11315
  await self._apps.prepare_app(
10998
11316
  app,
10999
- home,
11317
+ self._home,
11000
11318
  app_tags,
11001
11319
  )
11002
11320
 
@@ -11019,12 +11337,13 @@ class DeployCommand(Command['DeployCommand.Output']):
11019
11337
 
11020
11338
  @dc.dataclass(frozen=True)
11021
11339
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
11022
- _deploy: DeployManager
11340
+ _driver_factory: DeployDriverFactory
11023
11341
 
11024
11342
  async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
11025
11343
  log.info('Deploying! %r', cmd.spec)
11026
11344
 
11027
- await self._deploy.run_deploy(cmd.spec)
11345
+ with self._driver_factory(cmd.spec) as driver:
11346
+ await driver.drive_deploy()
11028
11347
 
11029
11348
  return DeployCommand.Output()
11030
11349
 
@@ -11033,6 +11352,57 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
11033
11352
  # ../deploy/inject.py
11034
11353
 
11035
11354
 
11355
+ ##
11356
+
11357
+
11358
+ class DeployInjectorScope(ContextvarInjectorScope):
11359
+ pass
11360
+
11361
+
11362
+ def bind_deploy_scope() -> InjectorBindings:
11363
+ lst: ta.List[InjectorBindingOrBindings] = [
11364
+ inj.bind_scope(DeployInjectorScope),
11365
+ inj.bind_scope_seed(DeploySpec, DeployInjectorScope),
11366
+
11367
+ inj.bind(DeployDriver, in_=DeployInjectorScope),
11368
+ ]
11369
+
11370
+ #
11371
+
11372
+ def provide_deploy_driver_factory(injector: Injector, sc: DeployInjectorScope) -> DeployDriverFactory:
11373
+ @contextlib.contextmanager
11374
+ def factory(spec: DeploySpec) -> ta.Iterator[DeployDriver]:
11375
+ with sc.enter({
11376
+ inj.as_key(DeploySpec): spec,
11377
+ }):
11378
+ yield injector[DeployDriver]
11379
+ return DeployDriverFactory(factory)
11380
+ lst.append(inj.bind(provide_deploy_driver_factory, singleton=True))
11381
+
11382
+ #
11383
+
11384
+ def provide_deploy_home(deploy: DeploySpec) -> DeployHome:
11385
+ hs = check.non_empty_str(deploy.home)
11386
+ hs = os.path.expanduser(hs)
11387
+ hs = os.path.realpath(hs)
11388
+ hs = os.path.abspath(hs)
11389
+ return DeployHome(hs)
11390
+ lst.append(inj.bind(provide_deploy_home, in_=DeployInjectorScope))
11391
+
11392
+ #
11393
+
11394
+ def provide_deploy_time(deploys: DeployManager) -> DeployTime:
11395
+ return deploys.make_deploy_time()
11396
+ lst.append(inj.bind(provide_deploy_time, in_=DeployInjectorScope))
11397
+
11398
+ #
11399
+
11400
+ return inj.as_bindings(*lst)
11401
+
11402
+
11403
+ ##
11404
+
11405
+
11036
11406
  def bind_deploy(
11037
11407
  *,
11038
11408
  deploy_config: DeployConfig,
@@ -11041,6 +11411,8 @@ def bind_deploy(
11041
11411
  inj.bind(deploy_config),
11042
11412
 
11043
11413
  bind_deploy_paths(),
11414
+
11415
+ bind_deploy_scope(),
11044
11416
  ]
11045
11417
 
11046
11418
  #