ominfra 0.0.0.dev157__py3-none-any.whl → 0.0.0.dev159__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
@@ -24,6 +24,7 @@ import decimal
24
24
  import enum
25
25
  import fractions
26
26
  import functools
27
+ import hashlib
27
28
  import inspect
28
29
  import itertools
29
30
  import json
@@ -41,6 +42,7 @@ import string
41
42
  import struct
42
43
  import subprocess
43
44
  import sys
45
+ import tempfile
44
46
  import threading
45
47
  import time
46
48
  import traceback
@@ -98,9 +100,13 @@ CallableVersionOperator = ta.Callable[['Version', str], bool]
98
100
  CommandT = ta.TypeVar('CommandT', bound='Command')
99
101
  CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
100
102
 
103
+ # deploy/atomics.py
104
+ DeployAtomicPathSwapKind = ta.Literal['dir', 'file']
105
+ DeployAtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
106
+
101
107
  # deploy/paths.py
102
108
  DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
103
- DeployPathSpec = ta.Literal['app', 'tag'] # ta.TypeAlias
109
+ DeployPathPlaceholder = ta.Literal['app', 'tag'] # ta.TypeAlias
104
110
 
105
111
  # ../../omlish/argparse/cli.py
106
112
  ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
@@ -118,7 +124,7 @@ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
118
124
  # ../configs.py
119
125
  ConfigMapping = ta.Mapping[str, ta.Any]
120
126
 
121
- # ../../omlish/lite/subprocesses.py
127
+ # ../../omlish/subprocesses.py
122
128
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
123
129
 
124
130
  # system/packages.py
@@ -1365,6 +1371,9 @@ class MainConfig:
1365
1371
  # ../deploy/config.py
1366
1372
 
1367
1373
 
1374
+ ##
1375
+
1376
+
1368
1377
  @dc.dataclass(frozen=True)
1369
1378
  class DeployConfig:
1370
1379
  deploy_home: ta.Optional[str] = None
@@ -1379,6 +1388,7 @@ DeployHome = ta.NewType('DeployHome', str)
1379
1388
  DeployApp = ta.NewType('DeployApp', str)
1380
1389
  DeployTag = ta.NewType('DeployTag', str)
1381
1390
  DeployRev = ta.NewType('DeployRev', str)
1391
+ DeployKey = ta.NewType('DeployKey', str)
1382
1392
 
1383
1393
 
1384
1394
  class DeployAppTag(ta.NamedTuple):
@@ -1539,7 +1549,7 @@ def _pyremote_bootstrap_main(context_name: str) -> None:
1539
1549
  # Get pid
1540
1550
  pid = os.getpid()
1541
1551
 
1542
- # Two copies of main src to be sent to parent
1552
+ # Two copies of payload src to be sent to parent
1543
1553
  r0, w0 = os.pipe()
1544
1554
  r1, w1 = os.pipe()
1545
1555
 
@@ -1578,17 +1588,17 @@ def _pyremote_bootstrap_main(context_name: str) -> None:
1578
1588
  # Write pid
1579
1589
  os.write(1, struct.pack('<Q', pid))
1580
1590
 
1581
- # Read main src from stdin
1582
- main_z_len = struct.unpack('<I', os.read(0, 4))[0]
1583
- if len(main_z := os.fdopen(0, 'rb').read(main_z_len)) != main_z_len:
1591
+ # Read payload src from stdin
1592
+ payload_z_len = struct.unpack('<I', os.read(0, 4))[0]
1593
+ if len(payload_z := os.fdopen(0, 'rb').read(payload_z_len)) != payload_z_len:
1584
1594
  raise EOFError
1585
- main_src = zlib.decompress(main_z)
1595
+ payload_src = zlib.decompress(payload_z)
1586
1596
 
1587
- # Write both copies of main src. Must write to w0 (parent stdin) before w1 (copy pipe) as pipe will likely fill
1588
- # and block and need to be drained by pyremote_bootstrap_finalize running in parent.
1597
+ # Write both copies of payload src. Must write to w0 (parent stdin) before w1 (copy pipe) as pipe will likely
1598
+ # fill and block and need to be drained by pyremote_bootstrap_finalize running in parent.
1589
1599
  for w in [w0, w1]:
1590
1600
  fp = os.fdopen(w, 'wb', 0)
1591
- fp.write(main_src)
1601
+ fp.write(payload_src)
1592
1602
  fp.close()
1593
1603
 
1594
1604
  # Write second ack
@@ -1652,7 +1662,7 @@ class PyremotePayloadRuntime:
1652
1662
  input: ta.BinaryIO
1653
1663
  output: ta.BinaryIO
1654
1664
  context_name: str
1655
- main_src: str
1665
+ payload_src: str
1656
1666
  options: PyremoteBootstrapOptions
1657
1667
  env_info: PyremoteEnvInfo
1658
1668
 
@@ -1660,9 +1670,9 @@ class PyremotePayloadRuntime:
1660
1670
  def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
1661
1671
  # If src file var is not present we need to do initial finalization
1662
1672
  if _PYREMOTE_BOOTSTRAP_SRC_FILE_VAR not in os.environ:
1663
- # Read second copy of main src
1673
+ # Read second copy of payload src
1664
1674
  r1 = os.fdopen(_PYREMOTE_BOOTSTRAP_SRC_FD, 'rb', 0)
1665
- main_src = r1.read().decode('utf-8')
1675
+ payload_src = r1.read().decode('utf-8')
1666
1676
  r1.close()
1667
1677
 
1668
1678
  # Reap boostrap child. Must be done after reading second copy of source because source may be too big to fit in
@@ -1680,7 +1690,7 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
1680
1690
  # Write temp source file
1681
1691
  import tempfile
1682
1692
  tfd, tfn = tempfile.mkstemp('-pyremote.py')
1683
- os.write(tfd, main_src.encode('utf-8'))
1693
+ os.write(tfd, payload_src.encode('utf-8'))
1684
1694
  os.close(tfd)
1685
1695
 
1686
1696
  # Set vars
@@ -1699,7 +1709,7 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
1699
1709
 
1700
1710
  # Read temp source file
1701
1711
  with open(os.environ.pop(_PYREMOTE_BOOTSTRAP_SRC_FILE_VAR)) as sf:
1702
- main_src = sf.read()
1712
+ payload_src = sf.read()
1703
1713
 
1704
1714
  # Restore vars
1705
1715
  sys.executable = os.environ.pop(_PYREMOTE_BOOTSTRAP_ARGV0_VAR)
@@ -1732,7 +1742,7 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
1732
1742
  input=input,
1733
1743
  output=output,
1734
1744
  context_name=context_name,
1735
- main_src=main_src,
1745
+ payload_src=payload_src,
1736
1746
  options=options,
1737
1747
  env_info=env_info,
1738
1748
  )
@@ -1744,31 +1754,31 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
1744
1754
  class PyremoteBootstrapDriver:
1745
1755
  def __init__(
1746
1756
  self,
1747
- main_src: ta.Union[str, ta.Sequence[str]],
1757
+ payload_src: ta.Union[str, ta.Sequence[str]],
1748
1758
  options: PyremoteBootstrapOptions = PyremoteBootstrapOptions(),
1749
1759
  ) -> None:
1750
1760
  super().__init__()
1751
1761
 
1752
- self._main_src = main_src
1762
+ self._payload_src = payload_src
1753
1763
  self._options = options
1754
1764
 
1755
- self._prepared_main_src = self._prepare_main_src(main_src, options)
1756
- self._main_z = zlib.compress(self._prepared_main_src.encode('utf-8'))
1765
+ self._prepared_payload_src = self._prepare_payload_src(payload_src, options)
1766
+ self._payload_z = zlib.compress(self._prepared_payload_src.encode('utf-8'))
1757
1767
 
1758
1768
  self._options_json = json.dumps(dc.asdict(options), indent=None, separators=(',', ':')).encode('utf-8') # noqa
1759
1769
  #
1760
1770
 
1761
1771
  @classmethod
1762
- def _prepare_main_src(
1772
+ def _prepare_payload_src(
1763
1773
  cls,
1764
- main_src: ta.Union[str, ta.Sequence[str]],
1774
+ payload_src: ta.Union[str, ta.Sequence[str]],
1765
1775
  options: PyremoteBootstrapOptions,
1766
1776
  ) -> str:
1767
1777
  parts: ta.List[str]
1768
- if isinstance(main_src, str):
1769
- parts = [main_src]
1778
+ if isinstance(payload_src, str):
1779
+ parts = [payload_src]
1770
1780
  else:
1771
- parts = list(main_src)
1781
+ parts = list(payload_src)
1772
1782
 
1773
1783
  if (mn := options.main_name_override) is not None:
1774
1784
  parts.insert(0, f'__name__ = {mn!r}')
@@ -1804,9 +1814,9 @@ class PyremoteBootstrapDriver:
1804
1814
  d = yield from self._read(8)
1805
1815
  pid = struct.unpack('<Q', d)[0]
1806
1816
 
1807
- # Write main src
1808
- yield from self._write(struct.pack('<I', len(self._main_z)))
1809
- yield from self._write(self._main_z)
1817
+ # Write payload src
1818
+ yield from self._write(struct.pack('<I', len(self._payload_z)))
1819
+ yield from self._write(self._payload_z)
1810
1820
 
1811
1821
  # Read second ack (after writing src copies)
1812
1822
  yield from self._expect(_PYREMOTE_BOOTSTRAP_ACK1)
@@ -2540,6 +2550,13 @@ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON
2540
2550
  json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
2541
2551
 
2542
2552
 
2553
+ ########################################
2554
+ # ../../../omlish/lite/logs.py
2555
+
2556
+
2557
+ log = logging.getLogger(__name__)
2558
+
2559
+
2543
2560
  ########################################
2544
2561
  # ../../../omlish/lite/maybes.py
2545
2562
 
@@ -2754,6 +2771,116 @@ def format_num_bytes(num_bytes: int) -> str:
2754
2771
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
2755
2772
 
2756
2773
 
2774
+ ########################################
2775
+ # ../../../omlish/logs/filters.py
2776
+
2777
+
2778
+ class TidLogFilter(logging.Filter):
2779
+ def filter(self, record):
2780
+ record.tid = threading.get_native_id()
2781
+ return True
2782
+
2783
+
2784
+ ########################################
2785
+ # ../../../omlish/logs/proxy.py
2786
+
2787
+
2788
+ class ProxyLogFilterer(logging.Filterer):
2789
+ def __init__(self, underlying: logging.Filterer) -> None: # noqa
2790
+ self._underlying = underlying
2791
+
2792
+ @property
2793
+ def underlying(self) -> logging.Filterer:
2794
+ return self._underlying
2795
+
2796
+ @property
2797
+ def filters(self):
2798
+ return self._underlying.filters
2799
+
2800
+ @filters.setter
2801
+ def filters(self, filters):
2802
+ self._underlying.filters = filters
2803
+
2804
+ def addFilter(self, filter): # noqa
2805
+ self._underlying.addFilter(filter)
2806
+
2807
+ def removeFilter(self, filter): # noqa
2808
+ self._underlying.removeFilter(filter)
2809
+
2810
+ def filter(self, record):
2811
+ return self._underlying.filter(record)
2812
+
2813
+
2814
+ class ProxyLogHandler(ProxyLogFilterer, logging.Handler):
2815
+ def __init__(self, underlying: logging.Handler) -> None: # noqa
2816
+ ProxyLogFilterer.__init__(self, underlying)
2817
+
2818
+ _underlying: logging.Handler
2819
+
2820
+ @property
2821
+ def underlying(self) -> logging.Handler:
2822
+ return self._underlying
2823
+
2824
+ def get_name(self):
2825
+ return self._underlying.get_name()
2826
+
2827
+ def set_name(self, name):
2828
+ self._underlying.set_name(name)
2829
+
2830
+ @property
2831
+ def name(self):
2832
+ return self._underlying.name
2833
+
2834
+ @property
2835
+ def level(self):
2836
+ return self._underlying.level
2837
+
2838
+ @level.setter
2839
+ def level(self, level):
2840
+ self._underlying.level = level
2841
+
2842
+ @property
2843
+ def formatter(self):
2844
+ return self._underlying.formatter
2845
+
2846
+ @formatter.setter
2847
+ def formatter(self, formatter):
2848
+ self._underlying.formatter = formatter
2849
+
2850
+ def createLock(self):
2851
+ self._underlying.createLock()
2852
+
2853
+ def acquire(self):
2854
+ self._underlying.acquire()
2855
+
2856
+ def release(self):
2857
+ self._underlying.release()
2858
+
2859
+ def setLevel(self, level):
2860
+ self._underlying.setLevel(level)
2861
+
2862
+ def format(self, record):
2863
+ return self._underlying.format(record)
2864
+
2865
+ def emit(self, record):
2866
+ self._underlying.emit(record)
2867
+
2868
+ def handle(self, record):
2869
+ return self._underlying.handle(record)
2870
+
2871
+ def setFormatter(self, fmt):
2872
+ self._underlying.setFormatter(fmt)
2873
+
2874
+ def flush(self):
2875
+ self._underlying.flush()
2876
+
2877
+ def close(self):
2878
+ self._underlying.close()
2879
+
2880
+ def handleError(self, record):
2881
+ self._underlying.handleError(record)
2882
+
2883
+
2757
2884
  ########################################
2758
2885
  # ../../../omlish/os/deathsig.py
2759
2886
 
@@ -3933,28 +4060,228 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
3933
4060
  return CommandNameMap(dct)
3934
4061
 
3935
4062
 
4063
+ ########################################
4064
+ # ../deploy/atomics.py
4065
+
4066
+
4067
+ ##
4068
+
4069
+
4070
+ class DeployAtomicPathSwap(abc.ABC):
4071
+ def __init__(
4072
+ self,
4073
+ kind: DeployAtomicPathSwapKind,
4074
+ dst_path: str,
4075
+ *,
4076
+ auto_commit: bool = False,
4077
+ ) -> None:
4078
+ super().__init__()
4079
+
4080
+ self._kind = kind
4081
+ self._dst_path = dst_path
4082
+ self._auto_commit = auto_commit
4083
+
4084
+ self._state: DeployAtomicPathSwapState = 'open'
4085
+
4086
+ def __repr__(self) -> str:
4087
+ return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
4088
+
4089
+ @property
4090
+ def kind(self) -> DeployAtomicPathSwapKind:
4091
+ return self._kind
4092
+
4093
+ @property
4094
+ def dst_path(self) -> str:
4095
+ return self._dst_path
4096
+
4097
+ @property
4098
+ @abc.abstractmethod
4099
+ def tmp_path(self) -> str:
4100
+ raise NotImplementedError
4101
+
4102
+ #
4103
+
4104
+ @property
4105
+ def state(self) -> DeployAtomicPathSwapState:
4106
+ return self._state
4107
+
4108
+ def _check_state(self, *states: DeployAtomicPathSwapState) -> None:
4109
+ if self._state not in states:
4110
+ raise RuntimeError(f'Atomic path swap not in correct state: {self._state}, {states}')
4111
+
4112
+ #
4113
+
4114
+ @abc.abstractmethod
4115
+ def _commit(self) -> None:
4116
+ raise NotImplementedError
4117
+
4118
+ def commit(self) -> None:
4119
+ if self._state == 'committed':
4120
+ return
4121
+ self._check_state('open')
4122
+ try:
4123
+ self._commit()
4124
+ except Exception: # noqa
4125
+ self._abort()
4126
+ raise
4127
+ else:
4128
+ self._state = 'committed'
4129
+
4130
+ #
4131
+
4132
+ @abc.abstractmethod
4133
+ def _abort(self) -> None:
4134
+ raise NotImplementedError
4135
+
4136
+ def abort(self) -> None:
4137
+ if self._state == 'aborted':
4138
+ return
4139
+ self._abort()
4140
+ self._state = 'aborted'
4141
+
4142
+ #
4143
+
4144
+ def __enter__(self) -> 'DeployAtomicPathSwap':
4145
+ return self
4146
+
4147
+ def __exit__(self, exc_type, exc_val, exc_tb):
4148
+ if (
4149
+ exc_type is None and
4150
+ self._auto_commit and
4151
+ self._state == 'open'
4152
+ ):
4153
+ self.commit()
4154
+ else:
4155
+ self.abort()
4156
+
4157
+
4158
+ #
4159
+
4160
+
4161
+ class DeployAtomicPathSwapping(abc.ABC):
4162
+ @abc.abstractmethod
4163
+ def begin_atomic_path_swap(
4164
+ self,
4165
+ kind: DeployAtomicPathSwapKind,
4166
+ dst_path: str,
4167
+ *,
4168
+ name_hint: ta.Optional[str] = None,
4169
+ make_dirs: bool = False,
4170
+ **kwargs: ta.Any,
4171
+ ) -> DeployAtomicPathSwap:
4172
+ raise NotImplementedError
4173
+
4174
+
4175
+ ##
4176
+
4177
+
4178
+ class OsRenameDeployAtomicPathSwap(DeployAtomicPathSwap):
4179
+ def __init__(
4180
+ self,
4181
+ kind: DeployAtomicPathSwapKind,
4182
+ dst_path: str,
4183
+ tmp_path: str,
4184
+ **kwargs: ta.Any,
4185
+ ) -> None:
4186
+ if kind == 'dir':
4187
+ check.state(os.path.isdir(tmp_path))
4188
+ elif kind == 'file':
4189
+ check.state(os.path.isfile(tmp_path))
4190
+ else:
4191
+ raise TypeError(kind)
4192
+
4193
+ super().__init__(
4194
+ kind,
4195
+ dst_path,
4196
+ **kwargs,
4197
+ )
4198
+
4199
+ self._tmp_path = tmp_path
4200
+
4201
+ @property
4202
+ def tmp_path(self) -> str:
4203
+ return self._tmp_path
4204
+
4205
+ def _commit(self) -> None:
4206
+ os.rename(self._tmp_path, self._dst_path)
4207
+
4208
+ def _abort(self) -> None:
4209
+ shutil.rmtree(self._tmp_path, ignore_errors=True)
4210
+
4211
+
4212
+ class TempDirDeployAtomicPathSwapping(DeployAtomicPathSwapping):
4213
+ def __init__(
4214
+ self,
4215
+ *,
4216
+ temp_dir: ta.Optional[str] = None,
4217
+ root_dir: ta.Optional[str] = None,
4218
+ ) -> None:
4219
+ super().__init__()
4220
+
4221
+ if root_dir is not None:
4222
+ root_dir = os.path.abspath(root_dir)
4223
+ self._root_dir = root_dir
4224
+ self._temp_dir = temp_dir
4225
+
4226
+ def begin_atomic_path_swap(
4227
+ self,
4228
+ kind: DeployAtomicPathSwapKind,
4229
+ dst_path: str,
4230
+ *,
4231
+ name_hint: ta.Optional[str] = None,
4232
+ make_dirs: bool = False,
4233
+ **kwargs: ta.Any,
4234
+ ) -> DeployAtomicPathSwap:
4235
+ dst_path = os.path.abspath(dst_path)
4236
+ if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
4237
+ raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
4238
+
4239
+ dst_dir = os.path.dirname(dst_path)
4240
+ if make_dirs:
4241
+ os.makedirs(dst_dir, exist_ok=True)
4242
+ if not os.path.isdir(dst_dir):
4243
+ raise RuntimeError(f'Atomic path swap dst dir does not exist: {dst_dir}')
4244
+
4245
+ if kind == 'dir':
4246
+ tmp_path = tempfile.mkdtemp(prefix=name_hint, dir=self._temp_dir)
4247
+ elif kind == 'file':
4248
+ fd, tmp_path = tempfile.mkstemp(prefix=name_hint, dir=self._temp_dir)
4249
+ os.close(fd)
4250
+ else:
4251
+ raise TypeError(kind)
4252
+
4253
+ return OsRenameDeployAtomicPathSwap(
4254
+ kind,
4255
+ dst_path,
4256
+ tmp_path,
4257
+ **kwargs,
4258
+ )
4259
+
4260
+
3936
4261
  ########################################
3937
4262
  # ../deploy/paths.py
3938
4263
  """
3939
4264
  ~deploy
3940
4265
  deploy.pid (flock)
3941
4266
  /app
3942
- /<appspec> - shallow clone
4267
+ /<appplaceholder> - shallow clone
3943
4268
  /conf
3944
4269
  /env
3945
- <appspec>.env
4270
+ <appplaceholder>.env
3946
4271
  /nginx
3947
- <appspec>.conf
4272
+ <appplaceholder>.conf
3948
4273
  /supervisor
3949
- <appspec>.conf
4274
+ <appplaceholder>.conf
3950
4275
  /venv
3951
- /<appspec>
4276
+ /<appplaceholder>
4277
+
4278
+ /tmp
3952
4279
 
3953
4280
  ?
3954
4281
  /logs
3955
- /wrmsr--omlish--<spec>
4282
+ /wrmsr--omlish--<placeholder>
3956
4283
 
3957
- spec = <name>--<rev>--<when>
4284
+ placeholder = <name>--<rev>--<when>
3958
4285
 
3959
4286
  ==
3960
4287
 
@@ -3975,10 +4302,10 @@ for dn in [
3975
4302
  ##
3976
4303
 
3977
4304
 
3978
- DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
3979
- DEPLOY_PATH_SPEC_SEPARATORS = '-.'
4305
+ DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER = '@'
4306
+ DEPLOY_PATH_PLACEHOLDER_SEPARATORS = '-.'
3980
4307
 
3981
- DEPLOY_PATH_SPECS: ta.FrozenSet[str] = frozenset([
4308
+ DEPLOY_PATH_PLACEHOLDERS: ta.FrozenSet[str] = frozenset([
3982
4309
  'app',
3983
4310
  'tag', # <rev>-<dt>
3984
4311
  ])
@@ -3996,7 +4323,7 @@ class DeployPathPart(abc.ABC): # noqa
3996
4323
  raise NotImplementedError
3997
4324
 
3998
4325
  @abc.abstractmethod
3999
- def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
4326
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
4000
4327
  raise NotImplementedError
4001
4328
 
4002
4329
 
@@ -4010,9 +4337,9 @@ class DirDeployPathPart(DeployPathPart, abc.ABC):
4010
4337
 
4011
4338
  @classmethod
4012
4339
  def parse(cls, s: str) -> 'DirDeployPathPart':
4013
- if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
4014
- check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
4015
- return SpecDirDeployPathPart(s[1:])
4340
+ if DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER in s:
4341
+ check.equal(s[0], DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER)
4342
+ return PlaceholderDirDeployPathPart(s[1:])
4016
4343
  else:
4017
4344
  return ConstDirDeployPathPart(s)
4018
4345
 
@@ -4024,13 +4351,13 @@ class FileDeployPathPart(DeployPathPart, abc.ABC):
4024
4351
 
4025
4352
  @classmethod
4026
4353
  def parse(cls, s: str) -> 'FileDeployPathPart':
4027
- if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
4028
- check.equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
4029
- if not any(c in s for c in DEPLOY_PATH_SPEC_SEPARATORS):
4030
- return SpecFileDeployPathPart(s[1:], '')
4354
+ if DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER in s:
4355
+ check.equal(s[0], DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER)
4356
+ if not any(c in s for c in DEPLOY_PATH_PLACEHOLDER_SEPARATORS):
4357
+ return PlaceholderFileDeployPathPart(s[1:], '')
4031
4358
  else:
4032
- p = min(f for c in DEPLOY_PATH_SPEC_SEPARATORS if (f := s.find(c)) > 0)
4033
- return SpecFileDeployPathPart(s[1:p], s[p:])
4359
+ p = min(f for c in DEPLOY_PATH_PLACEHOLDER_SEPARATORS if (f := s.find(c)) > 0)
4360
+ return PlaceholderFileDeployPathPart(s[1:p], s[p:])
4034
4361
  else:
4035
4362
  return ConstFileDeployPathPart(s)
4036
4363
 
@@ -4045,9 +4372,9 @@ class ConstDeployPathPart(DeployPathPart, abc.ABC):
4045
4372
  def __post_init__(self) -> None:
4046
4373
  check.non_empty_str(self.name)
4047
4374
  check.not_in('/', self.name)
4048
- check.not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
4375
+ check.not_in(DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER, self.name)
4049
4376
 
4050
- def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
4377
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
4051
4378
  return self.name
4052
4379
 
4053
4380
 
@@ -4063,40 +4390,40 @@ class ConstFileDeployPathPart(ConstDeployPathPart, FileDeployPathPart):
4063
4390
 
4064
4391
 
4065
4392
  @dc.dataclass(frozen=True)
4066
- class SpecDeployPathPart(DeployPathPart, abc.ABC):
4067
- spec: str # DeployPathSpec
4393
+ class PlaceholderDeployPathPart(DeployPathPart, abc.ABC):
4394
+ placeholder: str # DeployPathPlaceholder
4068
4395
 
4069
4396
  def __post_init__(self) -> None:
4070
- check.non_empty_str(self.spec)
4071
- for c in [*DEPLOY_PATH_SPEC_SEPARATORS, DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
4072
- check.not_in(c, self.spec)
4073
- check.in_(self.spec, DEPLOY_PATH_SPECS)
4074
-
4075
- def _render_spec(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
4076
- if specs is not None:
4077
- return specs[self.spec] # type: ignore
4397
+ check.non_empty_str(self.placeholder)
4398
+ for c in [*DEPLOY_PATH_PLACEHOLDER_SEPARATORS, DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER, '/']:
4399
+ check.not_in(c, self.placeholder)
4400
+ check.in_(self.placeholder, DEPLOY_PATH_PLACEHOLDERS)
4401
+
4402
+ def _render_placeholder(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
4403
+ if placeholders is not None:
4404
+ return placeholders[self.placeholder] # type: ignore
4078
4405
  else:
4079
- return DEPLOY_PATH_SPEC_PLACEHOLDER + self.spec
4406
+ return DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER + self.placeholder
4080
4407
 
4081
4408
 
4082
4409
  @dc.dataclass(frozen=True)
4083
- class SpecDirDeployPathPart(SpecDeployPathPart, DirDeployPathPart):
4084
- def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
4085
- return self._render_spec(specs)
4410
+ class PlaceholderDirDeployPathPart(PlaceholderDeployPathPart, DirDeployPathPart):
4411
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
4412
+ return self._render_placeholder(placeholders)
4086
4413
 
4087
4414
 
4088
4415
  @dc.dataclass(frozen=True)
4089
- class SpecFileDeployPathPart(SpecDeployPathPart, FileDeployPathPart):
4416
+ class PlaceholderFileDeployPathPart(PlaceholderDeployPathPart, FileDeployPathPart):
4090
4417
  suffix: str
4091
4418
 
4092
4419
  def __post_init__(self) -> None:
4093
4420
  super().__post_init__()
4094
4421
  if self.suffix:
4095
- for c in [DEPLOY_PATH_SPEC_PLACEHOLDER, '/']:
4422
+ for c in [DEPLOY_PATH_PLACEHOLDER_PLACEHOLDER, '/']:
4096
4423
  check.not_in(c, self.suffix)
4097
4424
 
4098
- def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
4099
- return self._render_spec(specs) + self.suffix
4425
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
4426
+ return self._render_placeholder(placeholders) + self.suffix
4100
4427
 
4101
4428
 
4102
4429
  ##
@@ -4107,28 +4434,30 @@ class DeployPath:
4107
4434
  parts: ta.Sequence[DeployPathPart]
4108
4435
 
4109
4436
  def __post_init__(self) -> None:
4437
+ hash(self)
4438
+
4110
4439
  check.not_empty(self.parts)
4111
4440
  for p in self.parts[:-1]:
4112
4441
  check.equal(p.kind, 'dir')
4113
4442
 
4114
4443
  pd = {}
4115
4444
  for i, p in enumerate(self.parts):
4116
- if isinstance(p, SpecDeployPathPart):
4117
- if p.spec in pd:
4118
- raise DeployPathError('Duplicate specs in path', self)
4119
- pd[p.spec] = i
4445
+ if isinstance(p, PlaceholderDeployPathPart):
4446
+ if p.placeholder in pd:
4447
+ raise DeployPathError('Duplicate placeholders in path', self)
4448
+ pd[p.placeholder] = i
4120
4449
 
4121
4450
  if 'tag' in pd:
4122
4451
  if 'app' not in pd or pd['app'] >= pd['tag']:
4123
- raise DeployPathError('Tag spec in path without preceding app', self)
4452
+ raise DeployPathError('Tag placeholder in path without preceding app', self)
4124
4453
 
4125
4454
  @property
4126
4455
  def kind(self) -> ta.Literal['file', 'dir']:
4127
4456
  return self.parts[-1].kind
4128
4457
 
4129
- def render(self, specs: ta.Optional[ta.Mapping[DeployPathSpec, str]] = None) -> str:
4458
+ def render(self, placeholders: ta.Optional[ta.Mapping[DeployPathPlaceholder, str]] = None) -> str:
4130
4459
  return os.path.join( # noqa
4131
- *[p.render(specs) for p in self.parts],
4460
+ *[p.render(placeholders) for p in self.parts],
4132
4461
  *([''] if self.kind == 'dir' else []),
4133
4462
  )
4134
4463
 
@@ -4141,10 +4470,10 @@ class DeployPath:
4141
4470
  else:
4142
4471
  tail_parse = FileDeployPathPart.parse
4143
4472
  ps = check.non_empty_str(s).split('/')
4144
- return cls([
4473
+ return cls((
4145
4474
  *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
4146
4475
  tail_parse(ps[-1]),
4147
- ])
4476
+ ))
4148
4477
 
4149
4478
 
4150
4479
  ##
@@ -4152,41 +4481,107 @@ class DeployPath:
4152
4481
 
4153
4482
  class DeployPathOwner(abc.ABC):
4154
4483
  @abc.abstractmethod
4155
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
4484
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
4156
4485
  raise NotImplementedError
4157
4486
 
4158
4487
 
4159
- ########################################
4160
- # ../remote/config.py
4161
-
4162
-
4163
- @dc.dataclass(frozen=True)
4164
- class RemoteConfig:
4165
- payload_file: ta.Optional[str] = None
4488
+ class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
4489
+ def __init__(
4490
+ self,
4491
+ *args: ta.Any,
4492
+ owned_dir: str,
4493
+ deploy_home: ta.Optional[DeployHome],
4494
+ **kwargs: ta.Any,
4495
+ ) -> None:
4496
+ super().__init__(*args, **kwargs)
4166
4497
 
4167
- set_pgid: bool = True
4498
+ check.not_in('/', owned_dir)
4499
+ self._owned_dir: str = check.non_empty_str(owned_dir)
4168
4500
 
4169
- deathsig: ta.Optional[str] = 'KILL'
4501
+ self._deploy_home = deploy_home
4170
4502
 
4171
- pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
4503
+ self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
4172
4504
 
4173
- forward_logging: bool = True
4505
+ @cached_nullary
4506
+ def _dir(self) -> str:
4507
+ return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
4174
4508
 
4175
- timebomb_delay_s: ta.Optional[float] = 60 * 60.
4509
+ @cached_nullary
4510
+ def _make_dir(self) -> str:
4511
+ if not os.path.isdir(d := self._dir()):
4512
+ os.makedirs(d, exist_ok=True)
4513
+ return d
4176
4514
 
4177
- heartbeat_interval_s: float = 3.
4515
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
4516
+ return self._owned_deploy_paths
4178
4517
 
4179
4518
 
4180
4519
  ########################################
4181
- # ../remote/payload.py
4182
-
4520
+ # ../deploy/specs.py
4183
4521
 
4184
- RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
4185
4522
 
4523
+ ##
4186
4524
 
4187
- @cached_nullary
4188
- def _get_self_src() -> str:
4189
- return inspect.getsource(sys.modules[__name__])
4525
+
4526
+ @dc.dataclass(frozen=True)
4527
+ class DeployGitRepo:
4528
+ host: ta.Optional[str] = None
4529
+ username: ta.Optional[str] = None
4530
+ path: ta.Optional[str] = None
4531
+
4532
+ def __post_init__(self) -> None:
4533
+ check.not_in('..', check.non_empty_str(self.host))
4534
+ check.not_in('.', check.non_empty_str(self.path))
4535
+
4536
+
4537
+ ##
4538
+
4539
+
4540
+ @dc.dataclass(frozen=True)
4541
+ class DeploySpec:
4542
+ app: DeployApp
4543
+ repo: DeployGitRepo
4544
+ rev: DeployRev
4545
+
4546
+ def __post_init__(self) -> None:
4547
+ hash(self)
4548
+
4549
+ @cached_nullary
4550
+ def key(self) -> DeployKey:
4551
+ return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
4552
+
4553
+
4554
+ ########################################
4555
+ # ../remote/config.py
4556
+
4557
+
4558
+ @dc.dataclass(frozen=True)
4559
+ class RemoteConfig:
4560
+ payload_file: ta.Optional[str] = None
4561
+
4562
+ set_pgid: bool = True
4563
+
4564
+ deathsig: ta.Optional[str] = 'KILL'
4565
+
4566
+ pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
4567
+
4568
+ forward_logging: bool = True
4569
+
4570
+ timebomb_delay_s: ta.Optional[float] = 60 * 60.
4571
+
4572
+ heartbeat_interval_s: float = 3.
4573
+
4574
+
4575
+ ########################################
4576
+ # ../remote/payload.py
4577
+
4578
+
4579
+ RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
4580
+
4581
+
4582
+ @cached_nullary
4583
+ def _get_self_src() -> str:
4584
+ return inspect.getsource(sys.modules[__name__])
4190
4585
 
4191
4586
 
4192
4587
  def _is_src_amalg(src: str) -> bool:
@@ -4216,6 +4611,75 @@ def get_remote_payload_src(
4216
4611
  return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
4217
4612
 
4218
4613
 
4614
+ ########################################
4615
+ # ../system/platforms.py
4616
+
4617
+
4618
+ ##
4619
+
4620
+
4621
+ @dc.dataclass(frozen=True)
4622
+ class Platform(abc.ABC): # noqa
4623
+ pass
4624
+
4625
+
4626
+ class LinuxPlatform(Platform, abc.ABC):
4627
+ pass
4628
+
4629
+
4630
+ class UbuntuPlatform(LinuxPlatform):
4631
+ pass
4632
+
4633
+
4634
+ class AmazonLinuxPlatform(LinuxPlatform):
4635
+ pass
4636
+
4637
+
4638
+ class GenericLinuxPlatform(LinuxPlatform):
4639
+ pass
4640
+
4641
+
4642
+ class DarwinPlatform(Platform):
4643
+ pass
4644
+
4645
+
4646
+ class UnknownPlatform(Platform):
4647
+ pass
4648
+
4649
+
4650
+ ##
4651
+
4652
+
4653
+ def _detect_system_platform() -> Platform:
4654
+ plat = sys.platform
4655
+
4656
+ if plat == 'linux':
4657
+ if (osr := LinuxOsRelease.read()) is None:
4658
+ return GenericLinuxPlatform()
4659
+
4660
+ if osr.id == 'amzn':
4661
+ return AmazonLinuxPlatform()
4662
+
4663
+ elif osr.id == 'ubuntu':
4664
+ return UbuntuPlatform()
4665
+
4666
+ else:
4667
+ return GenericLinuxPlatform()
4668
+
4669
+ elif plat == 'darwin':
4670
+ return DarwinPlatform()
4671
+
4672
+ else:
4673
+ return UnknownPlatform()
4674
+
4675
+
4676
+ @cached_nullary
4677
+ def detect_system_platform() -> Platform:
4678
+ platform = _detect_system_platform()
4679
+ log.info('Detected platform: %r', platform)
4680
+ return platform
4681
+
4682
+
4219
4683
  ########################################
4220
4684
  # ../targets/targets.py
4221
4685
  """
@@ -5540,367 +6004,97 @@ inj = Injection
5540
6004
 
5541
6005
 
5542
6006
  ########################################
5543
- # ../../../omlish/lite/logs.py
6007
+ # ../../../omlish/lite/marshal.py
5544
6008
  """
5545
6009
  TODO:
5546
- - translate json keys
5547
- - debug
6010
+ - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
6011
+ - namedtuple
6012
+ - literals
6013
+ - newtypes?
5548
6014
  """
5549
6015
 
5550
6016
 
5551
- log = logging.getLogger(__name__)
6017
+ ##
5552
6018
 
5553
6019
 
5554
- ##
6020
+ @dc.dataclass(frozen=True)
6021
+ class ObjMarshalOptions:
6022
+ raw_bytes: bool = False
6023
+ nonstrict_dataclasses: bool = False
5555
6024
 
5556
6025
 
5557
- class TidLogFilter(logging.Filter):
6026
+ class ObjMarshaler(abc.ABC):
6027
+ @abc.abstractmethod
6028
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6029
+ raise NotImplementedError
5558
6030
 
5559
- def filter(self, record):
5560
- record.tid = threading.get_native_id()
5561
- return True
6031
+ @abc.abstractmethod
6032
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6033
+ raise NotImplementedError
5562
6034
 
5563
6035
 
5564
- ##
6036
+ class NopObjMarshaler(ObjMarshaler):
6037
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6038
+ return o
5565
6039
 
6040
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6041
+ return o
5566
6042
 
5567
- class JsonLogFormatter(logging.Formatter):
5568
6043
 
5569
- KEYS: ta.Mapping[str, bool] = {
5570
- 'name': False,
5571
- 'msg': False,
5572
- 'args': False,
5573
- 'levelname': False,
5574
- 'levelno': False,
5575
- 'pathname': False,
5576
- 'filename': False,
5577
- 'module': False,
5578
- 'exc_info': True,
5579
- 'exc_text': True,
5580
- 'stack_info': True,
5581
- 'lineno': False,
5582
- 'funcName': False,
5583
- 'created': False,
5584
- 'msecs': False,
5585
- 'relativeCreated': False,
5586
- 'thread': False,
5587
- 'threadName': False,
5588
- 'processName': False,
5589
- 'process': False,
5590
- }
6044
+ @dc.dataclass()
6045
+ class ProxyObjMarshaler(ObjMarshaler):
6046
+ m: ta.Optional[ObjMarshaler] = None
5591
6047
 
5592
- def format(self, record: logging.LogRecord) -> str:
5593
- dct = {
5594
- k: v
5595
- for k, o in self.KEYS.items()
5596
- for v in [getattr(record, k)]
5597
- if not (o and v is None)
5598
- }
5599
- return json_dumps_compact(dct)
6048
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6049
+ return check.not_none(self.m).marshal(o, ctx)
5600
6050
 
6051
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6052
+ return check.not_none(self.m).unmarshal(o, ctx)
5601
6053
 
5602
- ##
5603
6054
 
6055
+ @dc.dataclass(frozen=True)
6056
+ class CastObjMarshaler(ObjMarshaler):
6057
+ ty: type
5604
6058
 
5605
- STANDARD_LOG_FORMAT_PARTS = [
5606
- ('asctime', '%(asctime)-15s'),
5607
- ('process', 'pid=%(process)-6s'),
5608
- ('thread', 'tid=%(thread)x'),
5609
- ('levelname', '%(levelname)s'),
5610
- ('name', '%(name)s'),
5611
- ('separator', '::'),
5612
- ('message', '%(message)s'),
5613
- ]
6059
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6060
+ return o
5614
6061
 
6062
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6063
+ return self.ty(o)
5615
6064
 
5616
- class StandardLogFormatter(logging.Formatter):
5617
6065
 
5618
- @staticmethod
5619
- def build_log_format(parts: ta.Iterable[ta.Tuple[str, str]]) -> str:
5620
- return ' '.join(v for k, v in parts)
6066
+ class DynamicObjMarshaler(ObjMarshaler):
6067
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6068
+ return ctx.manager.marshal_obj(o, opts=ctx.options)
5621
6069
 
5622
- converter = datetime.datetime.fromtimestamp # type: ignore
6070
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6071
+ return o
5623
6072
 
5624
- def formatTime(self, record, datefmt=None):
5625
- ct = self.converter(record.created) # type: ignore
5626
- if datefmt:
5627
- return ct.strftime(datefmt) # noqa
5628
- else:
5629
- t = ct.strftime('%Y-%m-%d %H:%M:%S')
5630
- return '%s.%03d' % (t, record.msecs) # noqa
5631
6073
 
6074
+ @dc.dataclass(frozen=True)
6075
+ class Base64ObjMarshaler(ObjMarshaler):
6076
+ ty: type
5632
6077
 
5633
- ##
6078
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6079
+ return base64.b64encode(o).decode('ascii')
5634
6080
 
6081
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6082
+ return self.ty(base64.b64decode(o))
5635
6083
 
5636
- class ProxyLogFilterer(logging.Filterer):
5637
- def __init__(self, underlying: logging.Filterer) -> None: # noqa
5638
- self._underlying = underlying
5639
6084
 
5640
- @property
5641
- def underlying(self) -> logging.Filterer:
5642
- return self._underlying
6085
+ @dc.dataclass(frozen=True)
6086
+ class BytesSwitchedObjMarshaler(ObjMarshaler):
6087
+ m: ObjMarshaler
5643
6088
 
5644
- @property
5645
- def filters(self):
5646
- return self._underlying.filters
6089
+ def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6090
+ if ctx.options.raw_bytes:
6091
+ return o
6092
+ return self.m.marshal(o, ctx)
5647
6093
 
5648
- @filters.setter
5649
- def filters(self, filters):
5650
- self._underlying.filters = filters
5651
-
5652
- def addFilter(self, filter): # noqa
5653
- self._underlying.addFilter(filter)
5654
-
5655
- def removeFilter(self, filter): # noqa
5656
- self._underlying.removeFilter(filter)
5657
-
5658
- def filter(self, record):
5659
- return self._underlying.filter(record)
5660
-
5661
-
5662
- class ProxyLogHandler(ProxyLogFilterer, logging.Handler):
5663
- def __init__(self, underlying: logging.Handler) -> None: # noqa
5664
- ProxyLogFilterer.__init__(self, underlying)
5665
-
5666
- _underlying: logging.Handler
5667
-
5668
- @property
5669
- def underlying(self) -> logging.Handler:
5670
- return self._underlying
5671
-
5672
- def get_name(self):
5673
- return self._underlying.get_name()
5674
-
5675
- def set_name(self, name):
5676
- self._underlying.set_name(name)
5677
-
5678
- @property
5679
- def name(self):
5680
- return self._underlying.name
5681
-
5682
- @property
5683
- def level(self):
5684
- return self._underlying.level
5685
-
5686
- @level.setter
5687
- def level(self, level):
5688
- self._underlying.level = level
5689
-
5690
- @property
5691
- def formatter(self):
5692
- return self._underlying.formatter
5693
-
5694
- @formatter.setter
5695
- def formatter(self, formatter):
5696
- self._underlying.formatter = formatter
5697
-
5698
- def createLock(self):
5699
- self._underlying.createLock()
5700
-
5701
- def acquire(self):
5702
- self._underlying.acquire()
5703
-
5704
- def release(self):
5705
- self._underlying.release()
5706
-
5707
- def setLevel(self, level):
5708
- self._underlying.setLevel(level)
5709
-
5710
- def format(self, record):
5711
- return self._underlying.format(record)
5712
-
5713
- def emit(self, record):
5714
- self._underlying.emit(record)
5715
-
5716
- def handle(self, record):
5717
- return self._underlying.handle(record)
5718
-
5719
- def setFormatter(self, fmt):
5720
- self._underlying.setFormatter(fmt)
5721
-
5722
- def flush(self):
5723
- self._underlying.flush()
5724
-
5725
- def close(self):
5726
- self._underlying.close()
5727
-
5728
- def handleError(self, record):
5729
- self._underlying.handleError(record)
5730
-
5731
-
5732
- ##
5733
-
5734
-
5735
- class StandardLogHandler(ProxyLogHandler):
5736
- pass
5737
-
5738
-
5739
- ##
5740
-
5741
-
5742
- @contextlib.contextmanager
5743
- def _locking_logging_module_lock() -> ta.Iterator[None]:
5744
- if hasattr(logging, '_acquireLock'):
5745
- logging._acquireLock() # noqa
5746
- try:
5747
- yield
5748
- finally:
5749
- logging._releaseLock() # type: ignore # noqa
5750
-
5751
- elif hasattr(logging, '_lock'):
5752
- # https://github.com/python/cpython/commit/74723e11109a320e628898817ab449b3dad9ee96
5753
- with logging._lock: # noqa
5754
- yield
5755
-
5756
- else:
5757
- raise Exception("Can't find lock in logging module")
5758
-
5759
-
5760
- def configure_standard_logging(
5761
- level: ta.Union[int, str] = logging.INFO,
5762
- *,
5763
- json: bool = False,
5764
- target: ta.Optional[logging.Logger] = None,
5765
- force: bool = False,
5766
- handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
5767
- ) -> ta.Optional[StandardLogHandler]:
5768
- with _locking_logging_module_lock():
5769
- if target is None:
5770
- target = logging.root
5771
-
5772
- #
5773
-
5774
- if not force:
5775
- if any(isinstance(h, StandardLogHandler) for h in list(target.handlers)):
5776
- return None
5777
-
5778
- #
5779
-
5780
- if handler_factory is not None:
5781
- handler = handler_factory()
5782
- else:
5783
- handler = logging.StreamHandler()
5784
-
5785
- #
5786
-
5787
- formatter: logging.Formatter
5788
- if json:
5789
- formatter = JsonLogFormatter()
5790
- else:
5791
- formatter = StandardLogFormatter(StandardLogFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
5792
- handler.setFormatter(formatter)
5793
-
5794
- #
5795
-
5796
- handler.addFilter(TidLogFilter())
5797
-
5798
- #
5799
-
5800
- target.addHandler(handler)
5801
-
5802
- #
5803
-
5804
- if level is not None:
5805
- target.setLevel(level)
5806
-
5807
- #
5808
-
5809
- return StandardLogHandler(handler)
5810
-
5811
-
5812
- ########################################
5813
- # ../../../omlish/lite/marshal.py
5814
- """
5815
- TODO:
5816
- - pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
5817
- - namedtuple
5818
- - literals
5819
- - newtypes?
5820
- """
5821
-
5822
-
5823
- ##
5824
-
5825
-
5826
- @dc.dataclass(frozen=True)
5827
- class ObjMarshalOptions:
5828
- raw_bytes: bool = False
5829
- nonstrict_dataclasses: bool = False
5830
-
5831
-
5832
- class ObjMarshaler(abc.ABC):
5833
- @abc.abstractmethod
5834
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5835
- raise NotImplementedError
5836
-
5837
- @abc.abstractmethod
5838
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5839
- raise NotImplementedError
5840
-
5841
-
5842
- class NopObjMarshaler(ObjMarshaler):
5843
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5844
- return o
5845
-
5846
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5847
- return o
5848
-
5849
-
5850
- @dc.dataclass()
5851
- class ProxyObjMarshaler(ObjMarshaler):
5852
- m: ta.Optional[ObjMarshaler] = None
5853
-
5854
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5855
- return check.not_none(self.m).marshal(o, ctx)
5856
-
5857
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5858
- return check.not_none(self.m).unmarshal(o, ctx)
5859
-
5860
-
5861
- @dc.dataclass(frozen=True)
5862
- class CastObjMarshaler(ObjMarshaler):
5863
- ty: type
5864
-
5865
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5866
- return o
5867
-
5868
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5869
- return self.ty(o)
5870
-
5871
-
5872
- class DynamicObjMarshaler(ObjMarshaler):
5873
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5874
- return ctx.manager.marshal_obj(o, opts=ctx.options)
5875
-
5876
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5877
- return o
5878
-
5879
-
5880
- @dc.dataclass(frozen=True)
5881
- class Base64ObjMarshaler(ObjMarshaler):
5882
- ty: type
5883
-
5884
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5885
- return base64.b64encode(o).decode('ascii')
5886
-
5887
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5888
- return self.ty(base64.b64decode(o))
5889
-
5890
-
5891
- @dc.dataclass(frozen=True)
5892
- class BytesSwitchedObjMarshaler(ObjMarshaler):
5893
- m: ObjMarshaler
5894
-
5895
- def marshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5896
- if ctx.options.raw_bytes:
5897
- return o
5898
- return self.m.marshal(o, ctx)
5899
-
5900
- def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
5901
- if ctx.options.raw_bytes:
5902
- return o
5903
- return self.m.unmarshal(o, ctx)
6094
+ def unmarshal(self, o: ta.Any, ctx: 'ObjMarshalContext') -> ta.Any:
6095
+ if ctx.options.raw_bytes:
6096
+ return o
6097
+ return self.m.unmarshal(o, ctx)
5904
6098
 
5905
6099
 
5906
6100
  @dc.dataclass(frozen=True)
@@ -6263,22 +6457,76 @@ def is_debugger_attached() -> bool:
6263
6457
  return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
6264
6458
 
6265
6459
 
6266
- REQUIRED_PYTHON_VERSION = (3, 8)
6460
+ LITE_REQUIRED_PYTHON_VERSION = (3, 8)
6267
6461
 
6268
6462
 
6269
- def check_runtime_version() -> None:
6270
- if sys.version_info < REQUIRED_PYTHON_VERSION:
6271
- raise OSError(f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
6463
+ def check_lite_runtime_version() -> None:
6464
+ if sys.version_info < LITE_REQUIRED_PYTHON_VERSION:
6465
+ raise OSError(f'Requires python {LITE_REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
6272
6466
 
6273
6467
 
6274
6468
  ########################################
6275
- # ../../../omdev/interp/types.py
6469
+ # ../../../omlish/logs/json.py
6470
+ """
6471
+ TODO:
6472
+ - translate json keys
6473
+ """
6276
6474
 
6277
6475
 
6278
- # See https://peps.python.org/pep-3149/
6279
- INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
6280
- ('debug', 'd'),
6281
- ('threaded', 't'),
6476
+ class JsonLogFormatter(logging.Formatter):
6477
+ KEYS: ta.Mapping[str, bool] = {
6478
+ 'name': False,
6479
+ 'msg': False,
6480
+ 'args': False,
6481
+ 'levelname': False,
6482
+ 'levelno': False,
6483
+ 'pathname': False,
6484
+ 'filename': False,
6485
+ 'module': False,
6486
+ 'exc_info': True,
6487
+ 'exc_text': True,
6488
+ 'stack_info': True,
6489
+ 'lineno': False,
6490
+ 'funcName': False,
6491
+ 'created': False,
6492
+ 'msecs': False,
6493
+ 'relativeCreated': False,
6494
+ 'thread': False,
6495
+ 'threadName': False,
6496
+ 'processName': False,
6497
+ 'process': False,
6498
+ }
6499
+
6500
+ def __init__(
6501
+ self,
6502
+ *args: ta.Any,
6503
+ json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
6504
+ **kwargs: ta.Any,
6505
+ ) -> None:
6506
+ super().__init__(*args, **kwargs)
6507
+
6508
+ if json_dumps is None:
6509
+ json_dumps = json_dumps_compact
6510
+ self._json_dumps = json_dumps
6511
+
6512
+ def format(self, record: logging.LogRecord) -> str:
6513
+ dct = {
6514
+ k: v
6515
+ for k, o in self.KEYS.items()
6516
+ for v in [getattr(record, k)]
6517
+ if not (o and v is None)
6518
+ }
6519
+ return self._json_dumps(dct)
6520
+
6521
+
6522
+ ########################################
6523
+ # ../../../omdev/interp/types.py
6524
+
6525
+
6526
+ # See https://peps.python.org/pep-3149/
6527
+ INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
6528
+ ('debug', 'd'),
6529
+ ('threaded', 't'),
6282
6530
  ])
6283
6531
 
6284
6532
  INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
@@ -6522,6 +6770,44 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
6522
6770
  return DeployCommand.Output()
6523
6771
 
6524
6772
 
6773
+ ########################################
6774
+ # ../deploy/tmp.py
6775
+
6776
+
6777
+ class DeployTmpManager(
6778
+ SingleDirDeployPathOwner,
6779
+ DeployAtomicPathSwapping,
6780
+ ):
6781
+ def __init__(
6782
+ self,
6783
+ *,
6784
+ deploy_home: ta.Optional[DeployHome] = None,
6785
+ ) -> None:
6786
+ super().__init__(
6787
+ owned_dir='tmp',
6788
+ deploy_home=deploy_home,
6789
+ )
6790
+
6791
+ @cached_nullary
6792
+ def _swapping(self) -> DeployAtomicPathSwapping:
6793
+ return TempDirDeployAtomicPathSwapping(
6794
+ temp_dir=self._make_dir(),
6795
+ root_dir=check.non_empty_str(self._deploy_home),
6796
+ )
6797
+
6798
+ def begin_atomic_path_swap(
6799
+ self,
6800
+ kind: DeployAtomicPathSwapKind,
6801
+ dst_path: str,
6802
+ **kwargs: ta.Any,
6803
+ ) -> DeployAtomicPathSwap:
6804
+ return self._swapping().begin_atomic_path_swap(
6805
+ kind,
6806
+ dst_path,
6807
+ **kwargs,
6808
+ )
6809
+
6810
+
6525
6811
  ########################################
6526
6812
  # ../marshal.py
6527
6813
 
@@ -6614,76 +6900,138 @@ class RemoteChannelImpl(RemoteChannel):
6614
6900
 
6615
6901
 
6616
6902
  ########################################
6617
- # ../system/platforms.py
6903
+ # ../system/config.py
6618
6904
 
6619
6905
 
6620
- ##
6906
+ @dc.dataclass(frozen=True)
6907
+ class SystemConfig:
6908
+ platform: ta.Optional[Platform] = None
6621
6909
 
6622
6910
 
6623
- @dc.dataclass(frozen=True)
6624
- class Platform(abc.ABC): # noqa
6625
- pass
6911
+ ########################################
6912
+ # ../../../omlish/logs/standard.py
6913
+ """
6914
+ TODO:
6915
+ - structured
6916
+ - prefixed
6917
+ - debug
6918
+ - optional noisy? noisy will never be lite - some kinda configure_standard callback mechanism?
6919
+ """
6626
6920
 
6627
6921
 
6628
- class LinuxPlatform(Platform, abc.ABC):
6629
- pass
6922
+ ##
6630
6923
 
6631
6924
 
6632
- class UbuntuPlatform(LinuxPlatform):
6633
- pass
6925
+ STANDARD_LOG_FORMAT_PARTS = [
6926
+ ('asctime', '%(asctime)-15s'),
6927
+ ('process', 'pid=%(process)-6s'),
6928
+ ('thread', 'tid=%(thread)x'),
6929
+ ('levelname', '%(levelname)s'),
6930
+ ('name', '%(name)s'),
6931
+ ('separator', '::'),
6932
+ ('message', '%(message)s'),
6933
+ ]
6634
6934
 
6635
6935
 
6636
- class AmazonLinuxPlatform(LinuxPlatform):
6637
- pass
6936
+ class StandardLogFormatter(logging.Formatter):
6937
+ @staticmethod
6938
+ def build_log_format(parts: ta.Iterable[ta.Tuple[str, str]]) -> str:
6939
+ return ' '.join(v for k, v in parts)
6638
6940
 
6941
+ converter = datetime.datetime.fromtimestamp # type: ignore
6639
6942
 
6640
- class GenericLinuxPlatform(LinuxPlatform):
6641
- pass
6943
+ def formatTime(self, record, datefmt=None):
6944
+ ct = self.converter(record.created) # type: ignore
6945
+ if datefmt:
6946
+ return ct.strftime(datefmt) # noqa
6947
+ else:
6948
+ t = ct.strftime('%Y-%m-%d %H:%M:%S')
6949
+ return '%s.%03d' % (t, record.msecs) # noqa
6642
6950
 
6643
6951
 
6644
- class DarwinPlatform(Platform):
6645
- pass
6952
+ ##
6646
6953
 
6647
6954
 
6648
- class UnknownPlatform(Platform):
6649
- pass
6955
+ class StandardConfiguredLogHandler(ProxyLogHandler):
6956
+ def __init_subclass__(cls, **kwargs):
6957
+ raise TypeError('This class serves only as a marker and should not be subclassed.')
6650
6958
 
6651
6959
 
6652
6960
  ##
6653
6961
 
6654
6962
 
6655
- def _detect_system_platform() -> Platform:
6656
- plat = sys.platform
6963
+ @contextlib.contextmanager
6964
+ def _locking_logging_module_lock() -> ta.Iterator[None]:
6965
+ if hasattr(logging, '_acquireLock'):
6966
+ logging._acquireLock() # noqa
6967
+ try:
6968
+ yield
6969
+ finally:
6970
+ logging._releaseLock() # type: ignore # noqa
6657
6971
 
6658
- if plat == 'linux':
6659
- if (osr := LinuxOsRelease.read()) is None:
6660
- return GenericLinuxPlatform()
6972
+ elif hasattr(logging, '_lock'):
6973
+ # https://github.com/python/cpython/commit/74723e11109a320e628898817ab449b3dad9ee96
6974
+ with logging._lock: # noqa
6975
+ yield
6661
6976
 
6662
- if osr.id == 'amzn':
6663
- return AmazonLinuxPlatform()
6977
+ else:
6978
+ raise Exception("Can't find lock in logging module")
6664
6979
 
6665
- elif osr.id == 'ubuntu':
6666
- return UbuntuPlatform()
6667
6980
 
6981
+ def configure_standard_logging(
6982
+ level: ta.Union[int, str] = logging.INFO,
6983
+ *,
6984
+ json: bool = False,
6985
+ target: ta.Optional[logging.Logger] = None,
6986
+ force: bool = False,
6987
+ handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
6988
+ ) -> ta.Optional[StandardConfiguredLogHandler]:
6989
+ with _locking_logging_module_lock():
6990
+ if target is None:
6991
+ target = logging.root
6992
+
6993
+ #
6994
+
6995
+ if not force:
6996
+ if any(isinstance(h, StandardConfiguredLogHandler) for h in list(target.handlers)):
6997
+ return None
6998
+
6999
+ #
7000
+
7001
+ if handler_factory is not None:
7002
+ handler = handler_factory()
6668
7003
  else:
6669
- return GenericLinuxPlatform()
7004
+ handler = logging.StreamHandler()
6670
7005
 
6671
- elif plat == 'darwin':
6672
- return DarwinPlatform()
7006
+ #
6673
7007
 
6674
- else:
6675
- return UnknownPlatform()
7008
+ formatter: logging.Formatter
7009
+ if json:
7010
+ formatter = JsonLogFormatter()
7011
+ else:
7012
+ formatter = StandardLogFormatter(StandardLogFormatter.build_log_format(STANDARD_LOG_FORMAT_PARTS))
7013
+ handler.setFormatter(formatter)
6676
7014
 
7015
+ #
6677
7016
 
6678
- @cached_nullary
6679
- def detect_system_platform() -> Platform:
6680
- platform = _detect_system_platform()
6681
- log.info('Detected platform: %r', platform)
6682
- return platform
7017
+ handler.addFilter(TidLogFilter())
7018
+
7019
+ #
7020
+
7021
+ target.addHandler(handler)
7022
+
7023
+ #
7024
+
7025
+ if level is not None:
7026
+ target.setLevel(level)
7027
+
7028
+ #
7029
+
7030
+ return StandardConfiguredLogHandler(handler)
6683
7031
 
6684
7032
 
6685
7033
  ########################################
6686
- # ../../../omlish/lite/subprocesses.py
7034
+ # ../../../omlish/subprocesses.py
6687
7035
 
6688
7036
 
6689
7037
  ##
@@ -6734,8 +7082,8 @@ def subprocess_close(
6734
7082
  ##
6735
7083
 
6736
7084
 
6737
- class AbstractSubprocesses(abc.ABC): # noqa
6738
- DEFAULT_LOGGER: ta.ClassVar[ta.Optional[logging.Logger]] = log
7085
+ class BaseSubprocesses(abc.ABC): # noqa
7086
+ DEFAULT_LOGGER: ta.ClassVar[ta.Optional[logging.Logger]] = None
6739
7087
 
6740
7088
  def __init__(
6741
7089
  self,
@@ -6748,6 +7096,9 @@ class AbstractSubprocesses(abc.ABC): # noqa
6748
7096
  self._log = log if log is not None else self.DEFAULT_LOGGER
6749
7097
  self._try_exceptions = try_exceptions if try_exceptions is not None else self.DEFAULT_TRY_EXCEPTIONS
6750
7098
 
7099
+ def set_logger(self, log: ta.Optional[logging.Logger]) -> None:
7100
+ self._log = log
7101
+
6751
7102
  #
6752
7103
 
6753
7104
  def prepare_args(
@@ -6859,23 +7210,25 @@ class AbstractSubprocesses(abc.ABC): # noqa
6859
7210
  ##
6860
7211
 
6861
7212
 
6862
- class Subprocesses(AbstractSubprocesses):
7213
+ class AbstractSubprocesses(BaseSubprocesses, abc.ABC):
7214
+ @abc.abstractmethod
6863
7215
  def check_call(
6864
7216
  self,
6865
7217
  *cmd: str,
6866
7218
  stdout: ta.Any = sys.stderr,
6867
7219
  **kwargs: ta.Any,
6868
7220
  ) -> None:
6869
- with self.prepare_and_wrap(*cmd, stdout=stdout, **kwargs) as (cmd, kwargs): # noqa
6870
- subprocess.check_call(cmd, **kwargs)
7221
+ raise NotImplementedError
6871
7222
 
7223
+ @abc.abstractmethod
6872
7224
  def check_output(
6873
7225
  self,
6874
7226
  *cmd: str,
6875
7227
  **kwargs: ta.Any,
6876
7228
  ) -> bytes:
6877
- with self.prepare_and_wrap(*cmd, **kwargs) as (cmd, kwargs): # noqa
6878
- return subprocess.check_output(cmd, **kwargs)
7229
+ raise NotImplementedError
7230
+
7231
+ #
6879
7232
 
6880
7233
  def check_output_str(
6881
7234
  self,
@@ -6917,49 +7270,149 @@ class Subprocesses(AbstractSubprocesses):
6917
7270
  return ret.decode().strip()
6918
7271
 
6919
7272
 
6920
- subprocesses = Subprocesses()
6921
-
6922
-
6923
- ########################################
6924
- # ../commands/local.py
7273
+ ##
6925
7274
 
6926
7275
 
6927
- class LocalCommandExecutor(CommandExecutor):
6928
- def __init__(
7276
+ class Subprocesses(AbstractSubprocesses):
7277
+ def check_call(
6929
7278
  self,
6930
- *,
6931
- command_executors: CommandExecutorMap,
7279
+ *cmd: str,
7280
+ stdout: ta.Any = sys.stderr,
7281
+ **kwargs: ta.Any,
6932
7282
  ) -> None:
6933
- super().__init__()
6934
-
6935
- self._command_executors = command_executors
7283
+ with self.prepare_and_wrap(*cmd, stdout=stdout, **kwargs) as (cmd, kwargs): # noqa
7284
+ subprocess.check_call(cmd, **kwargs)
6936
7285
 
6937
- async def execute(self, cmd: Command) -> Command.Output:
6938
- ce: CommandExecutor = self._command_executors[type(cmd)]
6939
- return await ce.execute(cmd)
7286
+ def check_output(
7287
+ self,
7288
+ *cmd: str,
7289
+ **kwargs: ta.Any,
7290
+ ) -> bytes:
7291
+ with self.prepare_and_wrap(*cmd, **kwargs) as (cmd, kwargs): # noqa
7292
+ return subprocess.check_output(cmd, **kwargs)
6940
7293
 
6941
7294
 
6942
- ########################################
6943
- # ../remote/execution.py
6944
- """
6945
- TODO:
6946
- - sequence all messages
6947
- """
7295
+ subprocesses = Subprocesses()
6948
7296
 
6949
7297
 
6950
7298
  ##
6951
7299
 
6952
7300
 
6953
- class _RemoteProtocol:
6954
- class Message(abc.ABC): # noqa
6955
- async def send(self, chan: RemoteChannel) -> None:
6956
- await chan.send_obj(self, _RemoteProtocol.Message)
6957
-
6958
- @classmethod
6959
- async def recv(cls: ta.Type[T], chan: RemoteChannel) -> ta.Optional[T]:
6960
- return await chan.recv_obj(cls)
7301
+ class AbstractAsyncSubprocesses(BaseSubprocesses):
7302
+ @abc.abstractmethod
7303
+ async def check_call(
7304
+ self,
7305
+ *cmd: str,
7306
+ stdout: ta.Any = sys.stderr,
7307
+ **kwargs: ta.Any,
7308
+ ) -> None:
7309
+ raise NotImplementedError
7310
+
7311
+ @abc.abstractmethod
7312
+ async def check_output(
7313
+ self,
7314
+ *cmd: str,
7315
+ **kwargs: ta.Any,
7316
+ ) -> bytes:
7317
+ raise NotImplementedError
7318
+
7319
+ #
7320
+
7321
+ async def check_output_str(
7322
+ self,
7323
+ *cmd: str,
7324
+ **kwargs: ta.Any,
7325
+ ) -> str:
7326
+ return (await self.check_output(*cmd, **kwargs)).decode().strip()
7327
+
7328
+ #
7329
+
7330
+ async def try_call(
7331
+ self,
7332
+ *cmd: str,
7333
+ **kwargs: ta.Any,
7334
+ ) -> bool:
7335
+ if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
7336
+ return False
7337
+ else:
7338
+ return True
7339
+
7340
+ async def try_output(
7341
+ self,
7342
+ *cmd: str,
7343
+ **kwargs: ta.Any,
7344
+ ) -> ta.Optional[bytes]:
7345
+ if isinstance(ret := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
7346
+ return None
7347
+ else:
7348
+ return ret
7349
+
7350
+ async def try_output_str(
7351
+ self,
7352
+ *cmd: str,
7353
+ **kwargs: ta.Any,
7354
+ ) -> ta.Optional[str]:
7355
+ if (ret := await self.try_output(*cmd, **kwargs)) is None:
7356
+ return None
7357
+ else:
7358
+ return ret.decode().strip()
7359
+
7360
+
7361
+ ########################################
7362
+ # ../bootstrap.py
7363
+
7364
+
7365
+ @dc.dataclass(frozen=True)
7366
+ class MainBootstrap:
7367
+ main_config: MainConfig = MainConfig()
7368
+
7369
+ deploy_config: DeployConfig = DeployConfig()
7370
+
7371
+ remote_config: RemoteConfig = RemoteConfig()
7372
+
7373
+ system_config: SystemConfig = SystemConfig()
6961
7374
 
6962
- #
7375
+
7376
+ ########################################
7377
+ # ../commands/local.py
7378
+
7379
+
7380
+ class LocalCommandExecutor(CommandExecutor):
7381
+ def __init__(
7382
+ self,
7383
+ *,
7384
+ command_executors: CommandExecutorMap,
7385
+ ) -> None:
7386
+ super().__init__()
7387
+
7388
+ self._command_executors = command_executors
7389
+
7390
+ async def execute(self, cmd: Command) -> Command.Output:
7391
+ ce: CommandExecutor = self._command_executors[type(cmd)]
7392
+ return await ce.execute(cmd)
7393
+
7394
+
7395
+ ########################################
7396
+ # ../remote/execution.py
7397
+ """
7398
+ TODO:
7399
+ - sequence all messages
7400
+ """
7401
+
7402
+
7403
+ ##
7404
+
7405
+
7406
+ class _RemoteProtocol:
7407
+ class Message(abc.ABC): # noqa
7408
+ async def send(self, chan: RemoteChannel) -> None:
7409
+ await chan.send_obj(self, _RemoteProtocol.Message)
7410
+
7411
+ @classmethod
7412
+ async def recv(cls: ta.Type[T], chan: RemoteChannel) -> ta.Optional[T]:
7413
+ return await chan.recv_obj(cls)
7414
+
7415
+ #
6963
7416
 
6964
7417
  class Request(Message, abc.ABC): # noqa
6965
7418
  pass
@@ -7337,16 +7790,7 @@ class RemoteCommandExecutor(CommandExecutor):
7337
7790
 
7338
7791
 
7339
7792
  ########################################
7340
- # ../system/config.py
7341
-
7342
-
7343
- @dc.dataclass(frozen=True)
7344
- class SystemConfig:
7345
- platform: ta.Optional[Platform] = None
7346
-
7347
-
7348
- ########################################
7349
- # ../../../omlish/lite/asyncio/subprocesses.py
7793
+ # ../../../omlish/asyncs/asyncio/subprocesses.py
7350
7794
 
7351
7795
 
7352
7796
  ##
@@ -7357,6 +7801,8 @@ class AsyncioProcessCommunicator:
7357
7801
  self,
7358
7802
  proc: asyncio.subprocess.Process,
7359
7803
  loop: ta.Optional[ta.Any] = None,
7804
+ *,
7805
+ log: ta.Optional[logging.Logger] = None,
7360
7806
  ) -> None:
7361
7807
  super().__init__()
7362
7808
 
@@ -7365,6 +7811,7 @@ class AsyncioProcessCommunicator:
7365
7811
 
7366
7812
  self._proc = proc
7367
7813
  self._loop = loop
7814
+ self._log = log
7368
7815
 
7369
7816
  self._transport: asyncio.base_subprocess.BaseSubprocessTransport = check.isinstance(
7370
7817
  proc._transport, # type: ignore # noqa
@@ -7380,19 +7827,19 @@ class AsyncioProcessCommunicator:
7380
7827
  try:
7381
7828
  if input is not None:
7382
7829
  stdin.write(input)
7383
- if self._debug:
7384
- log.debug('%r communicate: feed stdin (%s bytes)', self, len(input))
7830
+ if self._debug and self._log is not None:
7831
+ self._log.debug('%r communicate: feed stdin (%s bytes)', self, len(input))
7385
7832
 
7386
7833
  await stdin.drain()
7387
7834
 
7388
7835
  except (BrokenPipeError, ConnectionResetError) as exc:
7389
7836
  # communicate() ignores BrokenPipeError and ConnectionResetError. write() and drain() can raise these
7390
7837
  # exceptions.
7391
- if self._debug:
7392
- log.debug('%r communicate: stdin got %r', self, exc)
7838
+ if self._debug and self._log is not None:
7839
+ self._log.debug('%r communicate: stdin got %r', self, exc)
7393
7840
 
7394
- if self._debug:
7395
- log.debug('%r communicate: close stdin', self)
7841
+ if self._debug and self._log is not None:
7842
+ self._log.debug('%r communicate: close stdin', self)
7396
7843
 
7397
7844
  stdin.close()
7398
7845
 
@@ -7408,15 +7855,15 @@ class AsyncioProcessCommunicator:
7408
7855
  check.equal(fd, 1)
7409
7856
  stream = check.not_none(self._proc.stdout)
7410
7857
 
7411
- if self._debug:
7858
+ if self._debug and self._log is not None:
7412
7859
  name = 'stdout' if fd == 1 else 'stderr'
7413
- log.debug('%r communicate: read %s', self, name)
7860
+ self._log.debug('%r communicate: read %s', self, name)
7414
7861
 
7415
7862
  output = await stream.read()
7416
7863
 
7417
- if self._debug:
7864
+ if self._debug and self._log is not None:
7418
7865
  name = 'stdout' if fd == 1 else 'stderr'
7419
- log.debug('%r communicate: close %s', self, name)
7866
+ self._log.debug('%r communicate: close %s', self, name)
7420
7867
 
7421
7868
  transport.close()
7422
7869
 
@@ -7465,7 +7912,7 @@ class AsyncioProcessCommunicator:
7465
7912
  ##
7466
7913
 
7467
7914
 
7468
- class AsyncioSubprocesses(AbstractSubprocesses):
7915
+ class AsyncioSubprocesses(AbstractAsyncSubprocesses):
7469
7916
  async def communicate(
7470
7917
  self,
7471
7918
  proc: asyncio.subprocess.Process,
@@ -7562,45 +8009,6 @@ class AsyncioSubprocesses(AbstractSubprocesses):
7562
8009
  with self.prepare_and_wrap(*cmd, stdout=subprocess.PIPE, check=True, **kwargs) as (cmd, kwargs): # noqa
7563
8010
  return check.not_none((await self.run(*cmd, **kwargs)).stdout)
7564
8011
 
7565
- async def check_output_str(
7566
- self,
7567
- *cmd: str,
7568
- **kwargs: ta.Any,
7569
- ) -> str:
7570
- return (await self.check_output(*cmd, **kwargs)).decode().strip()
7571
-
7572
- #
7573
-
7574
- async def try_call(
7575
- self,
7576
- *cmd: str,
7577
- **kwargs: ta.Any,
7578
- ) -> bool:
7579
- if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
7580
- return False
7581
- else:
7582
- return True
7583
-
7584
- async def try_output(
7585
- self,
7586
- *cmd: str,
7587
- **kwargs: ta.Any,
7588
- ) -> ta.Optional[bytes]:
7589
- if isinstance(ret := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
7590
- return None
7591
- else:
7592
- return ret
7593
-
7594
- async def try_output_str(
7595
- self,
7596
- *cmd: str,
7597
- **kwargs: ta.Any,
7598
- ) -> ta.Optional[str]:
7599
- if (ret := await self.try_output(*cmd, **kwargs)) is None:
7600
- return None
7601
- else:
7602
- return ret.decode().strip()
7603
-
7604
8012
 
7605
8013
  asyncio_subprocesses = AsyncioSubprocesses()
7606
8014
 
@@ -7700,21 +8108,6 @@ class InterpInspector:
7700
8108
  INTERP_INSPECTOR = InterpInspector()
7701
8109
 
7702
8110
 
7703
- ########################################
7704
- # ../bootstrap.py
7705
-
7706
-
7707
- @dc.dataclass(frozen=True)
7708
- class MainBootstrap:
7709
- main_config: MainConfig = MainConfig()
7710
-
7711
- deploy_config: DeployConfig = DeployConfig()
7712
-
7713
- remote_config: RemoteConfig = RemoteConfig()
7714
-
7715
- system_config: SystemConfig = SystemConfig()
7716
-
7717
-
7718
8111
  ########################################
7719
8112
  # ../commands/subprocess.py
7720
8113
 
@@ -7801,44 +8194,22 @@ github.com/wrmsr/omlish@rev
7801
8194
  ##
7802
8195
 
7803
8196
 
7804
- @dc.dataclass(frozen=True)
7805
- class DeployGitRepo:
7806
- host: ta.Optional[str] = None
7807
- username: ta.Optional[str] = None
7808
- path: ta.Optional[str] = None
7809
-
7810
- def __post_init__(self) -> None:
7811
- check.not_in('..', check.non_empty_str(self.host))
7812
- check.not_in('.', check.non_empty_str(self.path))
7813
-
7814
-
7815
- @dc.dataclass(frozen=True)
7816
- class DeployGitSpec:
7817
- repo: DeployGitRepo
7818
- rev: DeployRev
7819
-
7820
-
7821
- ##
7822
-
7823
-
7824
- class DeployGitManager(DeployPathOwner):
8197
+ class DeployGitManager(SingleDirDeployPathOwner):
7825
8198
  def __init__(
7826
8199
  self,
7827
8200
  *,
7828
- deploy_home: DeployHome,
8201
+ deploy_home: ta.Optional[DeployHome] = None,
8202
+ atomics: DeployAtomicPathSwapping,
7829
8203
  ) -> None:
7830
- super().__init__()
8204
+ super().__init__(
8205
+ owned_dir='git',
8206
+ deploy_home=deploy_home,
8207
+ )
7831
8208
 
7832
- self._deploy_home = deploy_home
7833
- self._dir = os.path.join(deploy_home, 'git')
8209
+ self._atomics = atomics
7834
8210
 
7835
8211
  self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
7836
8212
 
7837
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7838
- return {
7839
- DeployPath.parse('git'),
7840
- }
7841
-
7842
8213
  class RepoDir:
7843
8214
  def __init__(
7844
8215
  self,
@@ -7850,7 +8221,7 @@ class DeployGitManager(DeployPathOwner):
7850
8221
  self._git = git
7851
8222
  self._repo = repo
7852
8223
  self._dir = os.path.join(
7853
- self._git._dir, # noqa
8224
+ self._git._make_dir(), # noqa
7854
8225
  check.non_empty_str(repo.host),
7855
8226
  check.non_empty_str(repo.path),
7856
8227
  )
@@ -7887,18 +8258,20 @@ class DeployGitManager(DeployPathOwner):
7887
8258
 
7888
8259
  async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
7889
8260
  check.state(not os.path.exists(dst_dir))
8261
+ with self._git._atomics.begin_atomic_path_swap( # noqa
8262
+ 'dir',
8263
+ dst_dir,
8264
+ auto_commit=True,
8265
+ make_dirs=True,
8266
+ ) as dst_swap:
8267
+ await self.fetch(rev)
7890
8268
 
7891
- await self.fetch(rev)
7892
-
7893
- # FIXME: temp dir swap
7894
- os.makedirs(dst_dir)
8269
+ dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
8270
+ await dst_call('git', 'init')
7895
8271
 
7896
- dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_dir)
7897
- await dst_call('git', 'init')
7898
-
7899
- await dst_call('git', 'remote', 'add', 'local', self._dir)
7900
- await dst_call('git', 'fetch', '--depth=1', 'local', rev)
7901
- await dst_call('git', 'checkout', rev)
8272
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
8273
+ await dst_call('git', 'fetch', '--depth=1', 'local', rev)
8274
+ await dst_call('git', 'checkout', rev)
7902
8275
 
7903
8276
  def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
7904
8277
  try:
@@ -7907,8 +8280,8 @@ class DeployGitManager(DeployPathOwner):
7907
8280
  repo_dir = self._repo_dirs[repo] = DeployGitManager.RepoDir(self, repo)
7908
8281
  return repo_dir
7909
8282
 
7910
- async def checkout(self, spec: DeployGitSpec, dst_dir: str) -> None:
7911
- await self.get_repo_dir(spec.repo).checkout(spec.rev, dst_dir)
8283
+ async def checkout(self, repo: DeployGitRepo, rev: DeployRev, dst_dir: str) -> None:
8284
+ await self.get_repo_dir(repo).checkout(rev, dst_dir)
7912
8285
 
7913
8286
 
7914
8287
  ########################################
@@ -7924,14 +8297,19 @@ class DeployVenvManager(DeployPathOwner):
7924
8297
  def __init__(
7925
8298
  self,
7926
8299
  *,
7927
- deploy_home: DeployHome,
8300
+ deploy_home: ta.Optional[DeployHome] = None,
8301
+ atomics: DeployAtomicPathSwapping,
7928
8302
  ) -> None:
7929
8303
  super().__init__()
7930
8304
 
7931
8305
  self._deploy_home = deploy_home
7932
- self._dir = os.path.join(deploy_home, 'venvs')
8306
+ self._atomics = atomics
8307
+
8308
+ @cached_nullary
8309
+ def _dir(self) -> str:
8310
+ return os.path.join(check.non_empty_str(self._deploy_home), 'venvs')
7933
8311
 
7934
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8312
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7935
8313
  return {
7936
8314
  DeployPath.parse('venvs/@app/@tag/'),
7937
8315
  }
@@ -7945,6 +8323,8 @@ class DeployVenvManager(DeployPathOwner):
7945
8323
  ) -> None:
7946
8324
  sys_exe = 'python3'
7947
8325
 
8326
+ # !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
8327
+ # garbage collect orphaned dirs.
7948
8328
  await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
7949
8329
 
7950
8330
  #
@@ -7966,58 +8346,200 @@ class DeployVenvManager(DeployPathOwner):
7966
8346
 
7967
8347
  async def setup_app_venv(self, app_tag: DeployAppTag) -> None:
7968
8348
  await self.setup_venv(
7969
- os.path.join(self._deploy_home, 'apps', app_tag.app, app_tag.tag),
7970
- os.path.join(self._deploy_home, 'venvs', app_tag.app, app_tag.tag),
8349
+ os.path.join(check.non_empty_str(self._deploy_home), 'apps', app_tag.app, app_tag.tag),
8350
+ os.path.join(self._dir(), app_tag.app, app_tag.tag),
7971
8351
  )
7972
8352
 
7973
8353
 
7974
8354
  ########################################
7975
- # ../remote/spawning.py
8355
+ # ../remote/_main.py
7976
8356
 
7977
8357
 
7978
8358
  ##
7979
8359
 
7980
8360
 
7981
- class RemoteSpawning(abc.ABC):
7982
- @dc.dataclass(frozen=True)
7983
- class Target:
7984
- shell: ta.Optional[str] = None
7985
- shell_quote: bool = False
8361
+ class _RemoteExecutionLogHandler(logging.Handler):
8362
+ def __init__(self, fn: ta.Callable[[str], None]) -> None:
8363
+ super().__init__()
8364
+ self._fn = fn
7986
8365
 
7987
- DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
7988
- python: str = DEFAULT_PYTHON
8366
+ def emit(self, record):
8367
+ msg = self.format(record)
8368
+ self._fn(msg)
7989
8369
 
7990
- stderr: ta.Optional[str] = None # SubprocessChannelOption
7991
8370
 
7992
- @dc.dataclass(frozen=True)
7993
- class Spawned:
7994
- stdin: asyncio.StreamWriter
7995
- stdout: asyncio.StreamReader
7996
- stderr: ta.Optional[asyncio.StreamReader]
8371
+ ##
7997
8372
 
7998
- @abc.abstractmethod
7999
- def spawn(
8373
+
8374
+ class _RemoteExecutionMain:
8375
+ def __init__(
8000
8376
  self,
8001
- tgt: Target,
8002
- src: str,
8003
- *,
8004
- timeout: ta.Optional[float] = None,
8005
- debug: bool = False,
8006
- ) -> ta.AsyncContextManager[Spawned]:
8007
- raise NotImplementedError
8377
+ chan: RemoteChannel,
8378
+ ) -> None:
8379
+ super().__init__()
8008
8380
 
8381
+ self._chan = chan
8009
8382
 
8010
- ##
8383
+ self.__bootstrap: ta.Optional[MainBootstrap] = None
8384
+ self.__injector: ta.Optional[Injector] = None
8011
8385
 
8386
+ @property
8387
+ def _bootstrap(self) -> MainBootstrap:
8388
+ return check.not_none(self.__bootstrap)
8012
8389
 
8013
- class SubprocessRemoteSpawning(RemoteSpawning):
8014
- class _PreparedCmd(ta.NamedTuple): # noqa
8015
- cmd: ta.Sequence[str]
8016
- shell: bool
8390
+ @property
8391
+ def _injector(self) -> Injector:
8392
+ return check.not_none(self.__injector)
8017
8393
 
8018
- def _prepare_cmd(
8394
+ #
8395
+
8396
+ def _timebomb_main(
8019
8397
  self,
8020
- tgt: RemoteSpawning.Target,
8398
+ delay_s: float,
8399
+ *,
8400
+ sig: int = signal.SIGINT,
8401
+ code: int = 1,
8402
+ ) -> None:
8403
+ time.sleep(delay_s)
8404
+
8405
+ if (pgid := os.getpgid(0)) == os.getpid():
8406
+ os.killpg(pgid, sig)
8407
+
8408
+ os._exit(code) # noqa
8409
+
8410
+ @cached_nullary
8411
+ def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
8412
+ if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
8413
+ return None
8414
+
8415
+ thr = threading.Thread(
8416
+ target=functools.partial(self._timebomb_main, tbd),
8417
+ name=f'{self.__class__.__name__}.timebomb',
8418
+ daemon=True,
8419
+ )
8420
+
8421
+ thr.start()
8422
+
8423
+ log.debug('Started timebomb thread: %r', thr)
8424
+
8425
+ return thr
8426
+
8427
+ #
8428
+
8429
+ @cached_nullary
8430
+ def _log_handler(self) -> _RemoteLogHandler:
8431
+ return _RemoteLogHandler(self._chan)
8432
+
8433
+ #
8434
+
8435
+ async def _setup(self) -> None:
8436
+ check.none(self.__bootstrap)
8437
+ check.none(self.__injector)
8438
+
8439
+ # Bootstrap
8440
+
8441
+ self.__bootstrap = check.not_none(await self._chan.recv_obj(MainBootstrap))
8442
+
8443
+ if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
8444
+ pycharm_debug_connect(prd)
8445
+
8446
+ self.__injector = main_bootstrap(self._bootstrap)
8447
+
8448
+ self._chan.set_marshaler(self._injector[ObjMarshalerManager])
8449
+
8450
+ # Post-bootstrap
8451
+
8452
+ if self._bootstrap.remote_config.set_pgid:
8453
+ if os.getpgid(0) != os.getpid():
8454
+ log.debug('Setting pgid')
8455
+ os.setpgid(0, 0)
8456
+
8457
+ if (ds := self._bootstrap.remote_config.deathsig) is not None:
8458
+ log.debug('Setting deathsig: %s', ds)
8459
+ set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
8460
+
8461
+ self._timebomb_thread()
8462
+
8463
+ if self._bootstrap.remote_config.forward_logging:
8464
+ log.debug('Installing log forwarder')
8465
+ logging.root.addHandler(self._log_handler())
8466
+
8467
+ #
8468
+
8469
+ async def run(self) -> None:
8470
+ await self._setup()
8471
+
8472
+ executor = self._injector[LocalCommandExecutor]
8473
+
8474
+ handler = _RemoteCommandHandler(self._chan, executor)
8475
+
8476
+ await handler.run()
8477
+
8478
+
8479
+ def _remote_execution_main() -> None:
8480
+ rt = pyremote_bootstrap_finalize() # noqa
8481
+
8482
+ async def inner() -> None:
8483
+ input = await asyncio_open_stream_reader(rt.input) # noqa
8484
+ output = await asyncio_open_stream_writer(rt.output)
8485
+
8486
+ chan = RemoteChannelImpl(
8487
+ input,
8488
+ output,
8489
+ )
8490
+
8491
+ await _RemoteExecutionMain(chan).run()
8492
+
8493
+ asyncio.run(inner())
8494
+
8495
+
8496
+ ########################################
8497
+ # ../remote/spawning.py
8498
+
8499
+
8500
+ ##
8501
+
8502
+
8503
+ class RemoteSpawning(abc.ABC):
8504
+ @dc.dataclass(frozen=True)
8505
+ class Target:
8506
+ shell: ta.Optional[str] = None
8507
+ shell_quote: bool = False
8508
+
8509
+ DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
8510
+ python: str = DEFAULT_PYTHON
8511
+
8512
+ stderr: ta.Optional[str] = None # SubprocessChannelOption
8513
+
8514
+ @dc.dataclass(frozen=True)
8515
+ class Spawned:
8516
+ stdin: asyncio.StreamWriter
8517
+ stdout: asyncio.StreamReader
8518
+ stderr: ta.Optional[asyncio.StreamReader]
8519
+
8520
+ @abc.abstractmethod
8521
+ def spawn(
8522
+ self,
8523
+ tgt: Target,
8524
+ src: str,
8525
+ *,
8526
+ timeout: ta.Optional[float] = None,
8527
+ debug: bool = False,
8528
+ ) -> ta.AsyncContextManager[Spawned]:
8529
+ raise NotImplementedError
8530
+
8531
+
8532
+ ##
8533
+
8534
+
8535
+ class SubprocessRemoteSpawning(RemoteSpawning):
8536
+ class _PreparedCmd(ta.NamedTuple): # noqa
8537
+ cmd: ta.Sequence[str]
8538
+ shell: bool
8539
+
8540
+ def _prepare_cmd(
8541
+ self,
8542
+ tgt: RemoteSpawning.Target,
8021
8543
  src: str,
8022
8544
  ) -> _PreparedCmd:
8023
8545
  if tgt.shell is not None:
@@ -8360,20 +8882,22 @@ def bind_commands(
8360
8882
 
8361
8883
  def make_deploy_tag(
8362
8884
  rev: DeployRev,
8363
- now: ta.Optional[datetime.datetime] = None,
8885
+ key: DeployKey,
8886
+ *,
8887
+ utcnow: ta.Optional[datetime.datetime] = None,
8364
8888
  ) -> DeployTag:
8365
- if now is None:
8366
- now = datetime.datetime.utcnow() # noqa
8367
- now_fmt = '%Y%m%dT%H%M%S'
8368
- now_str = now.strftime(now_fmt)
8369
- return DeployTag('-'.join([rev, now_str]))
8889
+ if utcnow is None:
8890
+ utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
8891
+ now_fmt = '%Y%m%dT%H%M%SZ'
8892
+ now_str = utcnow.strftime(now_fmt)
8893
+ return DeployTag('-'.join([now_str, rev, key]))
8370
8894
 
8371
8895
 
8372
8896
  class DeployAppManager(DeployPathOwner):
8373
8897
  def __init__(
8374
8898
  self,
8375
8899
  *,
8376
- deploy_home: DeployHome,
8900
+ deploy_home: ta.Optional[DeployHome] = None,
8377
8901
  git: DeployGitManager,
8378
8902
  venvs: DeployVenvManager,
8379
8903
  ) -> None:
@@ -8383,29 +8907,27 @@ class DeployAppManager(DeployPathOwner):
8383
8907
  self._git = git
8384
8908
  self._venvs = venvs
8385
8909
 
8386
- self._dir = os.path.join(deploy_home, 'apps')
8910
+ @cached_nullary
8911
+ def _dir(self) -> str:
8912
+ return os.path.join(check.non_empty_str(self._deploy_home), 'apps')
8387
8913
 
8388
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8914
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8389
8915
  return {
8390
8916
  DeployPath.parse('apps/@app/@tag'),
8391
8917
  }
8392
8918
 
8393
8919
  async def prepare_app(
8394
8920
  self,
8395
- app: DeployApp,
8396
- rev: DeployRev,
8397
- repo: DeployGitRepo,
8921
+ spec: DeploySpec,
8398
8922
  ):
8399
- app_tag = DeployAppTag(app, make_deploy_tag(rev))
8400
- app_dir = os.path.join(self._dir, app, app_tag.tag)
8923
+ app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev, spec.key()))
8924
+ app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
8401
8925
 
8402
8926
  #
8403
8927
 
8404
8928
  await self._git.checkout(
8405
- DeployGitSpec(
8406
- repo=repo,
8407
- rev=rev,
8408
- ),
8929
+ spec.repo,
8930
+ spec.rev,
8409
8931
  app_dir,
8410
8932
  )
8411
8933
 
@@ -8415,145 +8937,122 @@ class DeployAppManager(DeployPathOwner):
8415
8937
 
8416
8938
 
8417
8939
  ########################################
8418
- # ../remote/_main.py
8419
-
8420
-
8421
- ##
8422
-
8423
-
8424
- class _RemoteExecutionLogHandler(logging.Handler):
8425
- def __init__(self, fn: ta.Callable[[str], None]) -> None:
8426
- super().__init__()
8427
- self._fn = fn
8428
-
8429
- def emit(self, record):
8430
- msg = self.format(record)
8431
- self._fn(msg)
8940
+ # ../remote/connection.py
8432
8941
 
8433
8942
 
8434
8943
  ##
8435
8944
 
8436
8945
 
8437
- class _RemoteExecutionMain:
8946
+ class PyremoteRemoteExecutionConnector:
8438
8947
  def __init__(
8439
8948
  self,
8440
- chan: RemoteChannel,
8949
+ *,
8950
+ spawning: RemoteSpawning,
8951
+ msh: ObjMarshalerManager,
8952
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
8441
8953
  ) -> None:
8442
8954
  super().__init__()
8443
8955
 
8444
- self._chan = chan
8956
+ self._spawning = spawning
8957
+ self._msh = msh
8958
+ self._payload_file = payload_file
8445
8959
 
8446
- self.__bootstrap: ta.Optional[MainBootstrap] = None
8447
- self.__injector: ta.Optional[Injector] = None
8960
+ #
8448
8961
 
8449
- @property
8450
- def _bootstrap(self) -> MainBootstrap:
8451
- return check.not_none(self.__bootstrap)
8962
+ @cached_nullary
8963
+ def _payload_src(self) -> str:
8964
+ return get_remote_payload_src(file=self._payload_file)
8452
8965
 
8453
- @property
8454
- def _injector(self) -> Injector:
8455
- return check.not_none(self.__injector)
8966
+ @cached_nullary
8967
+ def _remote_src(self) -> ta.Sequence[str]:
8968
+ return [
8969
+ self._payload_src(),
8970
+ '_remote_execution_main()',
8971
+ ]
8972
+
8973
+ @cached_nullary
8974
+ def _spawn_src(self) -> str:
8975
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
8456
8976
 
8457
8977
  #
8458
8978
 
8459
- def _timebomb_main(
8979
+ @contextlib.asynccontextmanager
8980
+ async def connect(
8460
8981
  self,
8461
- delay_s: float,
8462
- *,
8463
- sig: int = signal.SIGINT,
8464
- code: int = 1,
8465
- ) -> None:
8466
- time.sleep(delay_s)
8467
-
8468
- if (pgid := os.getpgid(0)) == os.getpid():
8469
- os.killpg(pgid, sig)
8982
+ tgt: RemoteSpawning.Target,
8983
+ bs: MainBootstrap,
8984
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
8985
+ spawn_src = self._spawn_src()
8986
+ remote_src = self._remote_src()
8470
8987
 
8471
- os._exit(code) # noqa
8988
+ async with self._spawning.spawn(
8989
+ tgt,
8990
+ spawn_src,
8991
+ debug=bs.main_config.debug,
8992
+ ) as proc:
8993
+ res = await PyremoteBootstrapDriver( # noqa
8994
+ remote_src,
8995
+ PyremoteBootstrapOptions(
8996
+ debug=bs.main_config.debug,
8997
+ ),
8998
+ ).async_run(
8999
+ proc.stdout,
9000
+ proc.stdin,
9001
+ )
8472
9002
 
8473
- @cached_nullary
8474
- def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
8475
- if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
8476
- return None
9003
+ chan = RemoteChannelImpl(
9004
+ proc.stdout,
9005
+ proc.stdin,
9006
+ msh=self._msh,
9007
+ )
8477
9008
 
8478
- thr = threading.Thread(
8479
- target=functools.partial(self._timebomb_main, tbd),
8480
- name=f'{self.__class__.__name__}.timebomb',
8481
- daemon=True,
8482
- )
9009
+ await chan.send_obj(bs)
8483
9010
 
8484
- thr.start()
9011
+ rce: RemoteCommandExecutor
9012
+ async with aclosing(RemoteCommandExecutor(chan)) as rce:
9013
+ await rce.start()
8485
9014
 
8486
- log.debug('Started timebomb thread: %r', thr)
9015
+ yield rce
8487
9016
 
8488
- return thr
8489
9017
 
8490
- #
9018
+ ##
8491
9019
 
8492
- @cached_nullary
8493
- def _log_handler(self) -> _RemoteLogHandler:
8494
- return _RemoteLogHandler(self._chan)
8495
9020
 
8496
- #
8497
-
8498
- async def _setup(self) -> None:
8499
- check.none(self.__bootstrap)
8500
- check.none(self.__injector)
8501
-
8502
- # Bootstrap
8503
-
8504
- self.__bootstrap = check.not_none(await self._chan.recv_obj(MainBootstrap))
8505
-
8506
- if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
8507
- pycharm_debug_connect(prd)
8508
-
8509
- self.__injector = main_bootstrap(self._bootstrap)
8510
-
8511
- self._chan.set_marshaler(self._injector[ObjMarshalerManager])
8512
-
8513
- # Post-bootstrap
8514
-
8515
- if self._bootstrap.remote_config.set_pgid:
8516
- if os.getpgid(0) != os.getpid():
8517
- log.debug('Setting pgid')
8518
- os.setpgid(0, 0)
8519
-
8520
- if (ds := self._bootstrap.remote_config.deathsig) is not None:
8521
- log.debug('Setting deathsig: %s', ds)
8522
- set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
8523
-
8524
- self._timebomb_thread()
8525
-
8526
- if self._bootstrap.remote_config.forward_logging:
8527
- log.debug('Installing log forwarder')
8528
- logging.root.addHandler(self._log_handler())
8529
-
8530
- #
8531
-
8532
- async def run(self) -> None:
8533
- await self._setup()
8534
-
8535
- executor = self._injector[LocalCommandExecutor]
8536
-
8537
- handler = _RemoteCommandHandler(self._chan, executor)
8538
-
8539
- await handler.run()
9021
+ class InProcessRemoteExecutionConnector:
9022
+ def __init__(
9023
+ self,
9024
+ *,
9025
+ msh: ObjMarshalerManager,
9026
+ local_executor: LocalCommandExecutor,
9027
+ ) -> None:
9028
+ super().__init__()
8540
9029
 
9030
+ self._msh = msh
9031
+ self._local_executor = local_executor
8541
9032
 
8542
- def _remote_execution_main() -> None:
8543
- rt = pyremote_bootstrap_finalize() # noqa
9033
+ @contextlib.asynccontextmanager
9034
+ async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9035
+ r0, w0 = asyncio_create_bytes_channel()
9036
+ r1, w1 = asyncio_create_bytes_channel()
8544
9037
 
8545
- async def inner() -> None:
8546
- input = await asyncio_open_stream_reader(rt.input) # noqa
8547
- output = await asyncio_open_stream_writer(rt.output)
9038
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
9039
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
8548
9040
 
8549
- chan = RemoteChannelImpl(
8550
- input,
8551
- output,
9041
+ rch = _RemoteCommandHandler(
9042
+ remote_chan,
9043
+ self._local_executor,
8552
9044
  )
9045
+ rch_task = asyncio.create_task(rch.run()) # noqa
9046
+ try:
9047
+ rce: RemoteCommandExecutor
9048
+ async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
9049
+ await rce.start()
8553
9050
 
8554
- await _RemoteExecutionMain(chan).run()
9051
+ yield rce
8555
9052
 
8556
- asyncio.run(inner())
9053
+ finally:
9054
+ rch.stop()
9055
+ await rch_task
8557
9056
 
8558
9057
 
8559
9058
  ########################################
@@ -9152,122 +9651,31 @@ class SystemInterpProvider(InterpProvider):
9152
9651
 
9153
9652
 
9154
9653
  ########################################
9155
- # ../remote/connection.py
9156
-
9654
+ # ../remote/inject.py
9157
9655
 
9158
- ##
9159
9656
 
9657
+ def bind_remote(
9658
+ *,
9659
+ remote_config: RemoteConfig,
9660
+ ) -> InjectorBindings:
9661
+ lst: ta.List[InjectorBindingOrBindings] = [
9662
+ inj.bind(remote_config),
9160
9663
 
9161
- class PyremoteRemoteExecutionConnector:
9162
- def __init__(
9163
- self,
9164
- *,
9165
- spawning: RemoteSpawning,
9166
- msh: ObjMarshalerManager,
9167
- payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
9168
- ) -> None:
9169
- super().__init__()
9664
+ inj.bind(SubprocessRemoteSpawning, singleton=True),
9665
+ inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
9170
9666
 
9171
- self._spawning = spawning
9172
- self._msh = msh
9173
- self._payload_file = payload_file
9667
+ inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
9668
+ inj.bind(InProcessRemoteExecutionConnector, singleton=True),
9669
+ ]
9174
9670
 
9175
9671
  #
9176
9672
 
9177
- @cached_nullary
9178
- def _payload_src(self) -> str:
9179
- return get_remote_payload_src(file=self._payload_file)
9180
-
9181
- @cached_nullary
9182
- def _remote_src(self) -> ta.Sequence[str]:
9183
- return [
9184
- self._payload_src(),
9185
- '_remote_execution_main()',
9186
- ]
9187
-
9188
- @cached_nullary
9189
- def _spawn_src(self) -> str:
9190
- return pyremote_build_bootstrap_cmd(__package__ or 'manage')
9673
+ if (pf := remote_config.payload_file) is not None:
9674
+ lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
9191
9675
 
9192
9676
  #
9193
9677
 
9194
- @contextlib.asynccontextmanager
9195
- async def connect(
9196
- self,
9197
- tgt: RemoteSpawning.Target,
9198
- bs: MainBootstrap,
9199
- ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9200
- spawn_src = self._spawn_src()
9201
- remote_src = self._remote_src()
9202
-
9203
- async with self._spawning.spawn(
9204
- tgt,
9205
- spawn_src,
9206
- debug=bs.main_config.debug,
9207
- ) as proc:
9208
- res = await PyremoteBootstrapDriver( # noqa
9209
- remote_src,
9210
- PyremoteBootstrapOptions(
9211
- debug=bs.main_config.debug,
9212
- ),
9213
- ).async_run(
9214
- proc.stdout,
9215
- proc.stdin,
9216
- )
9217
-
9218
- chan = RemoteChannelImpl(
9219
- proc.stdout,
9220
- proc.stdin,
9221
- msh=self._msh,
9222
- )
9223
-
9224
- await chan.send_obj(bs)
9225
-
9226
- rce: RemoteCommandExecutor
9227
- async with aclosing(RemoteCommandExecutor(chan)) as rce:
9228
- await rce.start()
9229
-
9230
- yield rce
9231
-
9232
-
9233
- ##
9234
-
9235
-
9236
- class InProcessRemoteExecutionConnector:
9237
- def __init__(
9238
- self,
9239
- *,
9240
- msh: ObjMarshalerManager,
9241
- local_executor: LocalCommandExecutor,
9242
- ) -> None:
9243
- super().__init__()
9244
-
9245
- self._msh = msh
9246
- self._local_executor = local_executor
9247
-
9248
- @contextlib.asynccontextmanager
9249
- async def connect(self) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
9250
- r0, w0 = asyncio_create_bytes_channel()
9251
- r1, w1 = asyncio_create_bytes_channel()
9252
-
9253
- remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
9254
- local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
9255
-
9256
- rch = _RemoteCommandHandler(
9257
- remote_chan,
9258
- self._local_executor,
9259
- )
9260
- rch_task = asyncio.create_task(rch.run()) # noqa
9261
- try:
9262
- rce: RemoteCommandExecutor
9263
- async with aclosing(RemoteCommandExecutor(local_chan)) as rce:
9264
- await rce.start()
9265
-
9266
- yield rce
9267
-
9268
- finally:
9269
- rch.stop()
9270
- await rch_task
9678
+ return inj.as_bindings(*lst)
9271
9679
 
9272
9680
 
9273
9681
  ########################################
@@ -9318,132 +9726,6 @@ def bind_system(
9318
9726
  return inj.as_bindings(*lst)
9319
9727
 
9320
9728
 
9321
- ########################################
9322
- # ../../../omdev/interp/resolvers.py
9323
-
9324
-
9325
- INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
9326
- cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
9327
- }
9328
-
9329
-
9330
- class InterpResolver:
9331
- def __init__(
9332
- self,
9333
- providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
9334
- ) -> None:
9335
- super().__init__()
9336
-
9337
- self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
9338
-
9339
- async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
9340
- lst = [
9341
- (i, si)
9342
- for i, p in enumerate(self._providers.values())
9343
- for si in await p.get_installed_versions(spec)
9344
- if spec.contains(si)
9345
- ]
9346
-
9347
- slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
9348
- if not slst:
9349
- return None
9350
-
9351
- bi, bv = slst[-1]
9352
- bp = list(self._providers.values())[bi]
9353
- return (bp, bv)
9354
-
9355
- async def resolve(
9356
- self,
9357
- spec: InterpSpecifier,
9358
- *,
9359
- install: bool = False,
9360
- ) -> ta.Optional[Interp]:
9361
- tup = await self._resolve_installed(spec)
9362
- if tup is not None:
9363
- bp, bv = tup
9364
- return await bp.get_installed_version(bv)
9365
-
9366
- if not install:
9367
- return None
9368
-
9369
- tp = list(self._providers.values())[0] # noqa
9370
-
9371
- sv = sorted(
9372
- [s for s in await tp.get_installable_versions(spec) if s in spec],
9373
- key=lambda s: s.version,
9374
- )
9375
- if not sv:
9376
- return None
9377
-
9378
- bv = sv[-1]
9379
- return await tp.install_version(bv)
9380
-
9381
- async def list(self, spec: InterpSpecifier) -> None:
9382
- print('installed:')
9383
- for n, p in self._providers.items():
9384
- lst = [
9385
- si
9386
- for si in await p.get_installed_versions(spec)
9387
- if spec.contains(si)
9388
- ]
9389
- if lst:
9390
- print(f' {n}')
9391
- for si in lst:
9392
- print(f' {si}')
9393
-
9394
- print()
9395
-
9396
- print('installable:')
9397
- for n, p in self._providers.items():
9398
- lst = [
9399
- si
9400
- for si in await p.get_installable_versions(spec)
9401
- if spec.contains(si)
9402
- ]
9403
- if lst:
9404
- print(f' {n}')
9405
- for si in lst:
9406
- print(f' {si}')
9407
-
9408
-
9409
- DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
9410
- # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
9411
- PyenvInterpProvider(try_update=True),
9412
-
9413
- RunningInterpProvider(),
9414
-
9415
- SystemInterpProvider(),
9416
- ]])
9417
-
9418
-
9419
- ########################################
9420
- # ../remote/inject.py
9421
-
9422
-
9423
- def bind_remote(
9424
- *,
9425
- remote_config: RemoteConfig,
9426
- ) -> InjectorBindings:
9427
- lst: ta.List[InjectorBindingOrBindings] = [
9428
- inj.bind(remote_config),
9429
-
9430
- inj.bind(SubprocessRemoteSpawning, singleton=True),
9431
- inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
9432
-
9433
- inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
9434
- inj.bind(InProcessRemoteExecutionConnector, singleton=True),
9435
- ]
9436
-
9437
- #
9438
-
9439
- if (pf := remote_config.payload_file) is not None:
9440
- lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
9441
-
9442
- #
9443
-
9444
- return inj.as_bindings(*lst)
9445
-
9446
-
9447
9729
  ########################################
9448
9730
  # ../targets/connection.py
9449
9731
 
@@ -9579,33 +9861,101 @@ class SshManageTargetConnector(ManageTargetConnector):
9579
9861
 
9580
9862
 
9581
9863
  ########################################
9582
- # ../deploy/interp.py
9864
+ # ../../../omdev/interp/resolvers.py
9583
9865
 
9584
9866
 
9585
- ##
9867
+ INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
9868
+ cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
9869
+ }
9586
9870
 
9587
9871
 
9588
- @dc.dataclass(frozen=True)
9589
- class InterpCommand(Command['InterpCommand.Output']):
9590
- spec: str
9591
- install: bool = False
9872
+ class InterpResolver:
9873
+ def __init__(
9874
+ self,
9875
+ providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
9876
+ ) -> None:
9877
+ super().__init__()
9592
9878
 
9593
- @dc.dataclass(frozen=True)
9594
- class Output(Command.Output):
9595
- exe: str
9596
- version: str
9597
- opts: InterpOpts
9879
+ self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
9598
9880
 
9881
+ async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
9882
+ lst = [
9883
+ (i, si)
9884
+ for i, p in enumerate(self._providers.values())
9885
+ for si in await p.get_installed_versions(spec)
9886
+ if spec.contains(si)
9887
+ ]
9599
9888
 
9600
- class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
9601
- async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
9602
- i = InterpSpecifier.parse(check.not_none(cmd.spec))
9603
- o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
9604
- return InterpCommand.Output(
9605
- exe=o.exe,
9606
- version=str(o.version.version),
9607
- opts=o.version.opts,
9889
+ slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
9890
+ if not slst:
9891
+ return None
9892
+
9893
+ bi, bv = slst[-1]
9894
+ bp = list(self._providers.values())[bi]
9895
+ return (bp, bv)
9896
+
9897
+ async def resolve(
9898
+ self,
9899
+ spec: InterpSpecifier,
9900
+ *,
9901
+ install: bool = False,
9902
+ ) -> ta.Optional[Interp]:
9903
+ tup = await self._resolve_installed(spec)
9904
+ if tup is not None:
9905
+ bp, bv = tup
9906
+ return await bp.get_installed_version(bv)
9907
+
9908
+ if not install:
9909
+ return None
9910
+
9911
+ tp = list(self._providers.values())[0] # noqa
9912
+
9913
+ sv = sorted(
9914
+ [s for s in await tp.get_installable_versions(spec) if s in spec],
9915
+ key=lambda s: s.version,
9608
9916
  )
9917
+ if not sv:
9918
+ return None
9919
+
9920
+ bv = sv[-1]
9921
+ return await tp.install_version(bv)
9922
+
9923
+ async def list(self, spec: InterpSpecifier) -> None:
9924
+ print('installed:')
9925
+ for n, p in self._providers.items():
9926
+ lst = [
9927
+ si
9928
+ for si in await p.get_installed_versions(spec)
9929
+ if spec.contains(si)
9930
+ ]
9931
+ if lst:
9932
+ print(f' {n}')
9933
+ for si in lst:
9934
+ print(f' {si}')
9935
+
9936
+ print()
9937
+
9938
+ print('installable:')
9939
+ for n, p in self._providers.items():
9940
+ lst = [
9941
+ si
9942
+ for si in await p.get_installable_versions(spec)
9943
+ if spec.contains(si)
9944
+ ]
9945
+ if lst:
9946
+ print(f' {n}')
9947
+ for si in lst:
9948
+ print(f' {si}')
9949
+
9950
+
9951
+ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
9952
+ # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
9953
+ PyenvInterpProvider(try_update=True),
9954
+
9955
+ RunningInterpProvider(),
9956
+
9957
+ SystemInterpProvider(),
9958
+ ]])
9609
9959
 
9610
9960
 
9611
9961
  ########################################
@@ -9637,6 +9987,36 @@ def bind_targets() -> InjectorBindings:
9637
9987
  return inj.as_bindings(*lst)
9638
9988
 
9639
9989
 
9990
+ ########################################
9991
+ # ../deploy/interp.py
9992
+
9993
+
9994
+ ##
9995
+
9996
+
9997
+ @dc.dataclass(frozen=True)
9998
+ class InterpCommand(Command['InterpCommand.Output']):
9999
+ spec: str
10000
+ install: bool = False
10001
+
10002
+ @dc.dataclass(frozen=True)
10003
+ class Output(Command.Output):
10004
+ exe: str
10005
+ version: str
10006
+ opts: InterpOpts
10007
+
10008
+
10009
+ class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
10010
+ async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
10011
+ i = InterpSpecifier.parse(check.not_none(cmd.spec))
10012
+ o = check.not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
10013
+ return InterpCommand.Output(
10014
+ exe=o.exe,
10015
+ version=str(o.version.version),
10016
+ opts=o.version.opts,
10017
+ )
10018
+
10019
+
9640
10020
  ########################################
9641
10021
  # ../deploy/inject.py
9642
10022
 
@@ -9648,10 +10028,19 @@ def bind_deploy(
9648
10028
  lst: ta.List[InjectorBindingOrBindings] = [
9649
10029
  inj.bind(deploy_config),
9650
10030
 
10031
+ #
10032
+
9651
10033
  inj.bind(DeployAppManager, singleton=True),
10034
+
9652
10035
  inj.bind(DeployGitManager, singleton=True),
10036
+
10037
+ inj.bind(DeployTmpManager, singleton=True),
10038
+ inj.bind(DeployAtomicPathSwapping, to_key=DeployTmpManager),
10039
+
9653
10040
  inj.bind(DeployVenvManager, singleton=True),
9654
10041
 
10042
+ #
10043
+
9655
10044
  bind_command(DeployCommand, DeployCommandExecutor),
9656
10045
  bind_command(InterpCommand, InterpCommandExecutor),
9657
10046
  ]