ominfra 0.0.0.dev158__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,6 +100,10 @@ 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
109
  DeployPathPlaceholder = ta.Literal['app', 'tag'] # ta.TypeAlias
@@ -1382,6 +1388,7 @@ DeployHome = ta.NewType('DeployHome', str)
1382
1388
  DeployApp = ta.NewType('DeployApp', str)
1383
1389
  DeployTag = ta.NewType('DeployTag', str)
1384
1390
  DeployRev = ta.NewType('DeployRev', str)
1391
+ DeployKey = ta.NewType('DeployKey', str)
1385
1392
 
1386
1393
 
1387
1394
  class DeployAppTag(ta.NamedTuple):
@@ -4053,6 +4060,204 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
4053
4060
  return CommandNameMap(dct)
4054
4061
 
4055
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
+
4056
4261
  ########################################
4057
4262
  # ../deploy/paths.py
4058
4263
  """
@@ -4070,6 +4275,8 @@ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
4070
4275
  /venv
4071
4276
  /<appplaceholder>
4072
4277
 
4278
+ /tmp
4279
+
4073
4280
  ?
4074
4281
  /logs
4075
4282
  /wrmsr--omlish--<placeholder>
@@ -4227,6 +4434,8 @@ class DeployPath:
4227
4434
  parts: ta.Sequence[DeployPathPart]
4228
4435
 
4229
4436
  def __post_init__(self) -> None:
4437
+ hash(self)
4438
+
4230
4439
  check.not_empty(self.parts)
4231
4440
  for p in self.parts[:-1]:
4232
4441
  check.equal(p.kind, 'dir')
@@ -4261,10 +4470,10 @@ class DeployPath:
4261
4470
  else:
4262
4471
  tail_parse = FileDeployPathPart.parse
4263
4472
  ps = check.non_empty_str(s).split('/')
4264
- return cls([
4473
+ return cls((
4265
4474
  *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
4266
4475
  tail_parse(ps[-1]),
4267
- ])
4476
+ ))
4268
4477
 
4269
4478
 
4270
4479
  ##
@@ -4272,10 +4481,41 @@ class DeployPath:
4272
4481
 
4273
4482
  class DeployPathOwner(abc.ABC):
4274
4483
  @abc.abstractmethod
4275
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
4484
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
4276
4485
  raise NotImplementedError
4277
4486
 
4278
4487
 
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)
4497
+
4498
+ check.not_in('/', owned_dir)
4499
+ self._owned_dir: str = check.non_empty_str(owned_dir)
4500
+
4501
+ self._deploy_home = deploy_home
4502
+
4503
+ self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
4504
+
4505
+ @cached_nullary
4506
+ def _dir(self) -> str:
4507
+ return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
4508
+
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
4514
+
4515
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
4516
+ return self._owned_deploy_paths
4517
+
4518
+
4279
4519
  ########################################
4280
4520
  # ../deploy/specs.py
4281
4521
 
@@ -4303,6 +4543,13 @@ class DeploySpec:
4303
4543
  repo: DeployGitRepo
4304
4544
  rev: DeployRev
4305
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
+
4306
4553
 
4307
4554
  ########################################
4308
4555
  # ../remote/config.py
@@ -6210,12 +6457,12 @@ def is_debugger_attached() -> bool:
6210
6457
  return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
6211
6458
 
6212
6459
 
6213
- REQUIRED_PYTHON_VERSION = (3, 8)
6460
+ LITE_REQUIRED_PYTHON_VERSION = (3, 8)
6214
6461
 
6215
6462
 
6216
- def check_runtime_version() -> None:
6217
- if sys.version_info < REQUIRED_PYTHON_VERSION:
6218
- 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
6219
6466
 
6220
6467
 
6221
6468
  ########################################
@@ -6523,6 +6770,44 @@ class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]
6523
6770
  return DeployCommand.Output()
6524
6771
 
6525
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
+
6526
6811
  ########################################
6527
6812
  # ../marshal.py
6528
6813
 
@@ -6630,6 +6915,7 @@ TODO:
6630
6915
  - structured
6631
6916
  - prefixed
6632
6917
  - debug
6918
+ - optional noisy? noisy will never be lite - some kinda configure_standard callback mechanism?
6633
6919
  """
6634
6920
 
6635
6921
 
@@ -6666,8 +6952,9 @@ class StandardLogFormatter(logging.Formatter):
6666
6952
  ##
6667
6953
 
6668
6954
 
6669
- class StandardLogHandler(ProxyLogHandler):
6670
- 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.')
6671
6958
 
6672
6959
 
6673
6960
  ##
@@ -6698,7 +6985,7 @@ def configure_standard_logging(
6698
6985
  target: ta.Optional[logging.Logger] = None,
6699
6986
  force: bool = False,
6700
6987
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
6701
- ) -> ta.Optional[StandardLogHandler]:
6988
+ ) -> ta.Optional[StandardConfiguredLogHandler]:
6702
6989
  with _locking_logging_module_lock():
6703
6990
  if target is None:
6704
6991
  target = logging.root
@@ -6706,7 +6993,7 @@ def configure_standard_logging(
6706
6993
  #
6707
6994
 
6708
6995
  if not force:
6709
- if any(isinstance(h, StandardLogHandler) for h in list(target.handlers)):
6996
+ if any(isinstance(h, StandardConfiguredLogHandler) for h in list(target.handlers)):
6710
6997
  return None
6711
6998
 
6712
6999
  #
@@ -6740,7 +7027,7 @@ def configure_standard_logging(
6740
7027
 
6741
7028
  #
6742
7029
 
6743
- return StandardLogHandler(handler)
7030
+ return StandardConfiguredLogHandler(handler)
6744
7031
 
6745
7032
 
6746
7033
  ########################################
@@ -7907,27 +8194,22 @@ github.com/wrmsr/omlish@rev
7907
8194
  ##
7908
8195
 
7909
8196
 
7910
- class DeployGitManager(DeployPathOwner):
8197
+ class DeployGitManager(SingleDirDeployPathOwner):
7911
8198
  def __init__(
7912
8199
  self,
7913
8200
  *,
7914
8201
  deploy_home: ta.Optional[DeployHome] = None,
8202
+ atomics: DeployAtomicPathSwapping,
7915
8203
  ) -> None:
7916
- super().__init__()
8204
+ super().__init__(
8205
+ owned_dir='git',
8206
+ deploy_home=deploy_home,
8207
+ )
7917
8208
 
7918
- self._deploy_home = deploy_home
8209
+ self._atomics = atomics
7919
8210
 
7920
8211
  self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
7921
8212
 
7922
- @cached_nullary
7923
- def _dir(self) -> str:
7924
- return os.path.join(check.non_empty_str(self._deploy_home), 'git')
7925
-
7926
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
7927
- return {
7928
- DeployPath.parse('git'),
7929
- }
7930
-
7931
8213
  class RepoDir:
7932
8214
  def __init__(
7933
8215
  self,
@@ -7939,7 +8221,7 @@ class DeployGitManager(DeployPathOwner):
7939
8221
  self._git = git
7940
8222
  self._repo = repo
7941
8223
  self._dir = os.path.join(
7942
- self._git._dir(), # noqa
8224
+ self._git._make_dir(), # noqa
7943
8225
  check.non_empty_str(repo.host),
7944
8226
  check.non_empty_str(repo.path),
7945
8227
  )
@@ -7976,18 +8258,20 @@ class DeployGitManager(DeployPathOwner):
7976
8258
 
7977
8259
  async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
7978
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)
7979
8268
 
7980
- await self.fetch(rev)
8269
+ dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
8270
+ await dst_call('git', 'init')
7981
8271
 
7982
- # FIXME: temp dir swap
7983
- os.makedirs(dst_dir)
7984
-
7985
- dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_dir)
7986
- await dst_call('git', 'init')
7987
-
7988
- await dst_call('git', 'remote', 'add', 'local', self._dir)
7989
- await dst_call('git', 'fetch', '--depth=1', 'local', rev)
7990
- 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)
7991
8275
 
7992
8276
  def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
7993
8277
  try:
@@ -8014,16 +8298,18 @@ class DeployVenvManager(DeployPathOwner):
8014
8298
  self,
8015
8299
  *,
8016
8300
  deploy_home: ta.Optional[DeployHome] = None,
8301
+ atomics: DeployAtomicPathSwapping,
8017
8302
  ) -> None:
8018
8303
  super().__init__()
8019
8304
 
8020
8305
  self._deploy_home = deploy_home
8306
+ self._atomics = atomics
8021
8307
 
8022
8308
  @cached_nullary
8023
8309
  def _dir(self) -> str:
8024
8310
  return os.path.join(check.non_empty_str(self._deploy_home), 'venvs')
8025
8311
 
8026
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8312
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8027
8313
  return {
8028
8314
  DeployPath.parse('venvs/@app/@tag/'),
8029
8315
  }
@@ -8037,6 +8323,8 @@ class DeployVenvManager(DeployPathOwner):
8037
8323
  ) -> None:
8038
8324
  sys_exe = 'python3'
8039
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.
8040
8328
  await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
8041
8329
 
8042
8330
  #
@@ -8594,13 +8882,15 @@ def bind_commands(
8594
8882
 
8595
8883
  def make_deploy_tag(
8596
8884
  rev: DeployRev,
8597
- now: ta.Optional[datetime.datetime] = None,
8885
+ key: DeployKey,
8886
+ *,
8887
+ utcnow: ta.Optional[datetime.datetime] = None,
8598
8888
  ) -> DeployTag:
8599
- if now is None:
8600
- now = datetime.datetime.utcnow() # noqa
8601
- now_fmt = '%Y%m%dT%H%M%S'
8602
- now_str = now.strftime(now_fmt)
8603
- return DeployTag('-'.join([now_str, rev]))
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]))
8604
8894
 
8605
8895
 
8606
8896
  class DeployAppManager(DeployPathOwner):
@@ -8621,7 +8911,7 @@ class DeployAppManager(DeployPathOwner):
8621
8911
  def _dir(self) -> str:
8622
8912
  return os.path.join(check.non_empty_str(self._deploy_home), 'apps')
8623
8913
 
8624
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8914
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
8625
8915
  return {
8626
8916
  DeployPath.parse('apps/@app/@tag'),
8627
8917
  }
@@ -8630,7 +8920,7 @@ class DeployAppManager(DeployPathOwner):
8630
8920
  self,
8631
8921
  spec: DeploySpec,
8632
8922
  ):
8633
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev))
8923
+ app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev, spec.key()))
8634
8924
  app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
8635
8925
 
8636
8926
  #
@@ -9738,10 +10028,19 @@ def bind_deploy(
9738
10028
  lst: ta.List[InjectorBindingOrBindings] = [
9739
10029
  inj.bind(deploy_config),
9740
10030
 
10031
+ #
10032
+
9741
10033
  inj.bind(DeployAppManager, singleton=True),
10034
+
9742
10035
  inj.bind(DeployGitManager, singleton=True),
10036
+
10037
+ inj.bind(DeployTmpManager, singleton=True),
10038
+ inj.bind(DeployAtomicPathSwapping, to_key=DeployTmpManager),
10039
+
9743
10040
  inj.bind(DeployVenvManager, singleton=True),
9744
10041
 
10042
+ #
10043
+
9745
10044
  bind_command(DeployCommand, DeployCommandExecutor),
9746
10045
  bind_command(InterpCommand, InterpCommandExecutor),
9747
10046
  ]
@@ -5303,12 +5303,12 @@ def is_debugger_attached() -> bool:
5303
5303
  return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
5304
5304
 
5305
5305
 
5306
- REQUIRED_PYTHON_VERSION = (3, 8)
5306
+ LITE_REQUIRED_PYTHON_VERSION = (3, 8)
5307
5307
 
5308
5308
 
5309
- def check_runtime_version() -> None:
5310
- if sys.version_info < REQUIRED_PYTHON_VERSION:
5311
- raise OSError(f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
5309
+ def check_lite_runtime_version() -> None:
5310
+ if sys.version_info < LITE_REQUIRED_PYTHON_VERSION:
5311
+ raise OSError(f'Requires python {LITE_REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
5312
5312
 
5313
5313
 
5314
5314
  ########################################
@@ -5760,6 +5760,7 @@ TODO:
5760
5760
  - structured
5761
5761
  - prefixed
5762
5762
  - debug
5763
+ - optional noisy? noisy will never be lite - some kinda configure_standard callback mechanism?
5763
5764
  """
5764
5765
 
5765
5766
 
@@ -5796,8 +5797,9 @@ class StandardLogFormatter(logging.Formatter):
5796
5797
  ##
5797
5798
 
5798
5799
 
5799
- class StandardLogHandler(ProxyLogHandler):
5800
- pass
5800
+ class StandardConfiguredLogHandler(ProxyLogHandler):
5801
+ def __init_subclass__(cls, **kwargs):
5802
+ raise TypeError('This class serves only as a marker and should not be subclassed.')
5801
5803
 
5802
5804
 
5803
5805
  ##
@@ -5828,7 +5830,7 @@ def configure_standard_logging(
5828
5830
  target: ta.Optional[logging.Logger] = None,
5829
5831
  force: bool = False,
5830
5832
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
5831
- ) -> ta.Optional[StandardLogHandler]:
5833
+ ) -> ta.Optional[StandardConfiguredLogHandler]:
5832
5834
  with _locking_logging_module_lock():
5833
5835
  if target is None:
5834
5836
  target = logging.root
@@ -5836,7 +5838,7 @@ def configure_standard_logging(
5836
5838
  #
5837
5839
 
5838
5840
  if not force:
5839
- if any(isinstance(h, StandardLogHandler) for h in list(target.handlers)):
5841
+ if any(isinstance(h, StandardConfiguredLogHandler) for h in list(target.handlers)):
5840
5842
  return None
5841
5843
 
5842
5844
  #
@@ -5870,7 +5872,7 @@ def configure_standard_logging(
5870
5872
 
5871
5873
  #
5872
5874
 
5873
- return StandardLogHandler(handler)
5875
+ return StandardConfiguredLogHandler(handler)
5874
5876
 
5875
5877
 
5876
5878
  ########################################
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ominfra
3
- Version: 0.0.0.dev158
3
+ Version: 0.0.0.dev159
4
4
  Summary: ominfra
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -12,8 +12,8 @@ Classifier: Operating System :: OS Independent
12
12
  Classifier: Operating System :: POSIX
13
13
  Requires-Python: >=3.12
14
14
  License-File: LICENSE
15
- Requires-Dist: omdev==0.0.0.dev158
16
- Requires-Dist: omlish==0.0.0.dev158
15
+ Requires-Dist: omdev==0.0.0.dev159
16
+ Requires-Dist: omlish==0.0.0.dev159
17
17
  Provides-Extra: all
18
18
  Requires-Dist: paramiko~=3.5; extra == "all"
19
19
  Requires-Dist: asyncssh~=2.18; extra == "all"