ominfra 0.0.0.dev158__py3-none-any.whl → 0.0.0.dev159__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
ominfra/scripts/manage.py CHANGED
@@ -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"