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.
@@ -12,6 +12,7 @@ from .paths import DeployPathOwner
12
12
  from .specs import DeploySpec
13
13
  from .types import DeployAppTag
14
14
  from .types import DeployHome
15
+ from .types import DeployKey
15
16
  from .types import DeployRev
16
17
  from .types import DeployTag
17
18
  from .venvs import DeployVenvManager
@@ -19,13 +20,15 @@ from .venvs import DeployVenvManager
19
20
 
20
21
  def make_deploy_tag(
21
22
  rev: DeployRev,
22
- now: ta.Optional[datetime.datetime] = None,
23
+ key: DeployKey,
24
+ *,
25
+ utcnow: ta.Optional[datetime.datetime] = None,
23
26
  ) -> DeployTag:
24
- if now is None:
25
- now = datetime.datetime.utcnow() # noqa
26
- now_fmt = '%Y%m%dT%H%M%S'
27
- now_str = now.strftime(now_fmt)
28
- return DeployTag('-'.join([now_str, rev]))
27
+ if utcnow is None:
28
+ utcnow = datetime.datetime.now(tz=datetime.timezone.utc) # noqa
29
+ now_fmt = '%Y%m%dT%H%M%SZ'
30
+ now_str = utcnow.strftime(now_fmt)
31
+ return DeployTag('-'.join([now_str, rev, key]))
29
32
 
30
33
 
31
34
  class DeployAppManager(DeployPathOwner):
@@ -46,7 +49,7 @@ class DeployAppManager(DeployPathOwner):
46
49
  def _dir(self) -> str:
47
50
  return os.path.join(check.non_empty_str(self._deploy_home), 'apps')
48
51
 
49
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
52
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
50
53
  return {
51
54
  DeployPath.parse('apps/@app/@tag'),
52
55
  }
@@ -55,7 +58,7 @@ class DeployAppManager(DeployPathOwner):
55
58
  self,
56
59
  spec: DeploySpec,
57
60
  ):
58
- app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev))
61
+ app_tag = DeployAppTag(spec.app, make_deploy_tag(spec.rev, spec.key()))
59
62
  app_dir = os.path.join(self._dir(), spec.app, app_tag.tag)
60
63
 
61
64
  #
@@ -0,0 +1,207 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import os
4
+ import shutil
5
+ import tempfile
6
+ import typing as ta
7
+
8
+ from omlish.lite.check import check
9
+ from omlish.lite.strings import attr_repr
10
+
11
+
12
+ DeployAtomicPathSwapKind = ta.Literal['dir', 'file']
13
+ DeployAtomicPathSwapState = ta.Literal['open', 'committed', 'aborted'] # ta.TypeAlias
14
+
15
+
16
+ ##
17
+
18
+
19
+ class DeployAtomicPathSwap(abc.ABC):
20
+ def __init__(
21
+ self,
22
+ kind: DeployAtomicPathSwapKind,
23
+ dst_path: str,
24
+ *,
25
+ auto_commit: bool = False,
26
+ ) -> None:
27
+ super().__init__()
28
+
29
+ self._kind = kind
30
+ self._dst_path = dst_path
31
+ self._auto_commit = auto_commit
32
+
33
+ self._state: DeployAtomicPathSwapState = 'open'
34
+
35
+ def __repr__(self) -> str:
36
+ return attr_repr(self, 'kind', 'dst_path', 'tmp_path')
37
+
38
+ @property
39
+ def kind(self) -> DeployAtomicPathSwapKind:
40
+ return self._kind
41
+
42
+ @property
43
+ def dst_path(self) -> str:
44
+ return self._dst_path
45
+
46
+ @property
47
+ @abc.abstractmethod
48
+ def tmp_path(self) -> str:
49
+ raise NotImplementedError
50
+
51
+ #
52
+
53
+ @property
54
+ def state(self) -> DeployAtomicPathSwapState:
55
+ return self._state
56
+
57
+ def _check_state(self, *states: DeployAtomicPathSwapState) -> None:
58
+ if self._state not in states:
59
+ raise RuntimeError(f'Atomic path swap not in correct state: {self._state}, {states}')
60
+
61
+ #
62
+
63
+ @abc.abstractmethod
64
+ def _commit(self) -> None:
65
+ raise NotImplementedError
66
+
67
+ def commit(self) -> None:
68
+ if self._state == 'committed':
69
+ return
70
+ self._check_state('open')
71
+ try:
72
+ self._commit()
73
+ except Exception: # noqa
74
+ self._abort()
75
+ raise
76
+ else:
77
+ self._state = 'committed'
78
+
79
+ #
80
+
81
+ @abc.abstractmethod
82
+ def _abort(self) -> None:
83
+ raise NotImplementedError
84
+
85
+ def abort(self) -> None:
86
+ if self._state == 'aborted':
87
+ return
88
+ self._abort()
89
+ self._state = 'aborted'
90
+
91
+ #
92
+
93
+ def __enter__(self) -> 'DeployAtomicPathSwap':
94
+ return self
95
+
96
+ def __exit__(self, exc_type, exc_val, exc_tb):
97
+ if (
98
+ exc_type is None and
99
+ self._auto_commit and
100
+ self._state == 'open'
101
+ ):
102
+ self.commit()
103
+ else:
104
+ self.abort()
105
+
106
+
107
+ #
108
+
109
+
110
+ class DeployAtomicPathSwapping(abc.ABC):
111
+ @abc.abstractmethod
112
+ def begin_atomic_path_swap(
113
+ self,
114
+ kind: DeployAtomicPathSwapKind,
115
+ dst_path: str,
116
+ *,
117
+ name_hint: ta.Optional[str] = None,
118
+ make_dirs: bool = False,
119
+ **kwargs: ta.Any,
120
+ ) -> DeployAtomicPathSwap:
121
+ raise NotImplementedError
122
+
123
+
124
+ ##
125
+
126
+
127
+ class OsRenameDeployAtomicPathSwap(DeployAtomicPathSwap):
128
+ def __init__(
129
+ self,
130
+ kind: DeployAtomicPathSwapKind,
131
+ dst_path: str,
132
+ tmp_path: str,
133
+ **kwargs: ta.Any,
134
+ ) -> None:
135
+ if kind == 'dir':
136
+ check.state(os.path.isdir(tmp_path))
137
+ elif kind == 'file':
138
+ check.state(os.path.isfile(tmp_path))
139
+ else:
140
+ raise TypeError(kind)
141
+
142
+ super().__init__(
143
+ kind,
144
+ dst_path,
145
+ **kwargs,
146
+ )
147
+
148
+ self._tmp_path = tmp_path
149
+
150
+ @property
151
+ def tmp_path(self) -> str:
152
+ return self._tmp_path
153
+
154
+ def _commit(self) -> None:
155
+ os.rename(self._tmp_path, self._dst_path)
156
+
157
+ def _abort(self) -> None:
158
+ shutil.rmtree(self._tmp_path, ignore_errors=True)
159
+
160
+
161
+ class TempDirDeployAtomicPathSwapping(DeployAtomicPathSwapping):
162
+ def __init__(
163
+ self,
164
+ *,
165
+ temp_dir: ta.Optional[str] = None,
166
+ root_dir: ta.Optional[str] = None,
167
+ ) -> None:
168
+ super().__init__()
169
+
170
+ if root_dir is not None:
171
+ root_dir = os.path.abspath(root_dir)
172
+ self._root_dir = root_dir
173
+ self._temp_dir = temp_dir
174
+
175
+ def begin_atomic_path_swap(
176
+ self,
177
+ kind: DeployAtomicPathSwapKind,
178
+ dst_path: str,
179
+ *,
180
+ name_hint: ta.Optional[str] = None,
181
+ make_dirs: bool = False,
182
+ **kwargs: ta.Any,
183
+ ) -> DeployAtomicPathSwap:
184
+ dst_path = os.path.abspath(dst_path)
185
+ if self._root_dir is not None and not dst_path.startswith(check.non_empty_str(self._root_dir)):
186
+ raise RuntimeError(f'Atomic path swap dst must be in root dir: {dst_path}, {self._root_dir}')
187
+
188
+ dst_dir = os.path.dirname(dst_path)
189
+ if make_dirs:
190
+ os.makedirs(dst_dir, exist_ok=True)
191
+ if not os.path.isdir(dst_dir):
192
+ raise RuntimeError(f'Atomic path swap dst dir does not exist: {dst_dir}')
193
+
194
+ if kind == 'dir':
195
+ tmp_path = tempfile.mkdtemp(prefix=name_hint, dir=self._temp_dir)
196
+ elif kind == 'file':
197
+ fd, tmp_path = tempfile.mkstemp(prefix=name_hint, dir=self._temp_dir)
198
+ os.close(fd)
199
+ else:
200
+ raise TypeError(kind)
201
+
202
+ return OsRenameDeployAtomicPathSwap(
203
+ kind,
204
+ dst_path,
205
+ tmp_path,
206
+ **kwargs,
207
+ )
@@ -14,11 +14,10 @@ import typing as ta
14
14
 
15
15
  from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
16
16
  from omlish.lite.cached import async_cached_nullary
17
- from omlish.lite.cached import cached_nullary
18
17
  from omlish.lite.check import check
19
18
 
20
- from .paths import DeployPath
21
- from .paths import DeployPathOwner
19
+ from .atomics import DeployAtomicPathSwapping
20
+ from .paths import SingleDirDeployPathOwner
22
21
  from .specs import DeployGitRepo
23
22
  from .types import DeployHome
24
23
  from .types import DeployRev
@@ -27,27 +26,22 @@ from .types import DeployRev
27
26
  ##
28
27
 
29
28
 
30
- class DeployGitManager(DeployPathOwner):
29
+ class DeployGitManager(SingleDirDeployPathOwner):
31
30
  def __init__(
32
31
  self,
33
32
  *,
34
33
  deploy_home: ta.Optional[DeployHome] = None,
34
+ atomics: DeployAtomicPathSwapping,
35
35
  ) -> None:
36
- super().__init__()
36
+ super().__init__(
37
+ owned_dir='git',
38
+ deploy_home=deploy_home,
39
+ )
37
40
 
38
- self._deploy_home = deploy_home
41
+ self._atomics = atomics
39
42
 
40
43
  self._repo_dirs: ta.Dict[DeployGitRepo, DeployGitManager.RepoDir] = {}
41
44
 
42
- @cached_nullary
43
- def _dir(self) -> str:
44
- return os.path.join(check.non_empty_str(self._deploy_home), 'git')
45
-
46
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
47
- return {
48
- DeployPath.parse('git'),
49
- }
50
-
51
45
  class RepoDir:
52
46
  def __init__(
53
47
  self,
@@ -59,7 +53,7 @@ class DeployGitManager(DeployPathOwner):
59
53
  self._git = git
60
54
  self._repo = repo
61
55
  self._dir = os.path.join(
62
- self._git._dir(), # noqa
56
+ self._git._make_dir(), # noqa
63
57
  check.non_empty_str(repo.host),
64
58
  check.non_empty_str(repo.path),
65
59
  )
@@ -96,18 +90,20 @@ class DeployGitManager(DeployPathOwner):
96
90
 
97
91
  async def checkout(self, rev: DeployRev, dst_dir: str) -> None:
98
92
  check.state(not os.path.exists(dst_dir))
99
-
100
- await self.fetch(rev)
101
-
102
- # FIXME: temp dir swap
103
- os.makedirs(dst_dir)
104
-
105
- dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_dir)
106
- await dst_call('git', 'init')
107
-
108
- await dst_call('git', 'remote', 'add', 'local', self._dir)
109
- await dst_call('git', 'fetch', '--depth=1', 'local', rev)
110
- await dst_call('git', 'checkout', rev)
93
+ with self._git._atomics.begin_atomic_path_swap( # noqa
94
+ 'dir',
95
+ dst_dir,
96
+ auto_commit=True,
97
+ make_dirs=True,
98
+ ) as dst_swap:
99
+ await self.fetch(rev)
100
+
101
+ dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_swap.tmp_path)
102
+ await dst_call('git', 'init')
103
+
104
+ await dst_call('git', 'remote', 'add', 'local', self._dir)
105
+ await dst_call('git', 'fetch', '--depth=1', 'local', rev)
106
+ await dst_call('git', 'checkout', rev)
111
107
 
112
108
  def get_repo_dir(self, repo: DeployGitRepo) -> RepoDir:
113
109
  try:
@@ -8,12 +8,14 @@ from omlish.lite.inject import inj
8
8
 
9
9
  from ..commands.inject import bind_command
10
10
  from .apps import DeployAppManager
11
+ from .atomics import DeployAtomicPathSwapping
11
12
  from .commands import DeployCommand
12
13
  from .commands import DeployCommandExecutor
13
14
  from .config import DeployConfig
14
15
  from .git import DeployGitManager
15
16
  from .interp import InterpCommand
16
17
  from .interp import InterpCommandExecutor
18
+ from .tmp import DeployTmpManager
17
19
  from .types import DeployHome
18
20
  from .venvs import DeployVenvManager
19
21
 
@@ -25,10 +27,19 @@ def bind_deploy(
25
27
  lst: ta.List[InjectorBindingOrBindings] = [
26
28
  inj.bind(deploy_config),
27
29
 
30
+ #
31
+
28
32
  inj.bind(DeployAppManager, singleton=True),
33
+
29
34
  inj.bind(DeployGitManager, singleton=True),
35
+
36
+ inj.bind(DeployTmpManager, singleton=True),
37
+ inj.bind(DeployAtomicPathSwapping, to_key=DeployTmpManager),
38
+
30
39
  inj.bind(DeployVenvManager, singleton=True),
31
40
 
41
+ #
42
+
32
43
  bind_command(DeployCommand, DeployCommandExecutor),
33
44
  bind_command(InterpCommand, InterpCommandExecutor),
34
45
  ]
@@ -14,6 +14,8 @@
14
14
  /venv
15
15
  /<appplaceholder>
16
16
 
17
+ /tmp
18
+
17
19
  ?
18
20
  /logs
19
21
  /wrmsr--omlish--<placeholder>
@@ -39,8 +41,11 @@ import dataclasses as dc
39
41
  import os.path
40
42
  import typing as ta
41
43
 
44
+ from omlish.lite.cached import cached_nullary
42
45
  from omlish.lite.check import check
43
46
 
47
+ from .types import DeployHome
48
+
44
49
 
45
50
  DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
46
51
  DeployPathPlaceholder = ta.Literal['app', 'tag'] # ta.TypeAlias
@@ -181,6 +186,8 @@ class DeployPath:
181
186
  parts: ta.Sequence[DeployPathPart]
182
187
 
183
188
  def __post_init__(self) -> None:
189
+ hash(self)
190
+
184
191
  check.not_empty(self.parts)
185
192
  for p in self.parts[:-1]:
186
193
  check.equal(p.kind, 'dir')
@@ -215,10 +222,10 @@ class DeployPath:
215
222
  else:
216
223
  tail_parse = FileDeployPathPart.parse
217
224
  ps = check.non_empty_str(s).split('/')
218
- return cls([
225
+ return cls((
219
226
  *([DirDeployPathPart.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
220
227
  tail_parse(ps[-1]),
221
- ])
228
+ ))
222
229
 
223
230
 
224
231
  ##
@@ -226,5 +233,36 @@ class DeployPath:
226
233
 
227
234
  class DeployPathOwner(abc.ABC):
228
235
  @abc.abstractmethod
229
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
236
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
230
237
  raise NotImplementedError
238
+
239
+
240
+ class SingleDirDeployPathOwner(DeployPathOwner, abc.ABC):
241
+ def __init__(
242
+ self,
243
+ *args: ta.Any,
244
+ owned_dir: str,
245
+ deploy_home: ta.Optional[DeployHome],
246
+ **kwargs: ta.Any,
247
+ ) -> None:
248
+ super().__init__(*args, **kwargs)
249
+
250
+ check.not_in('/', owned_dir)
251
+ self._owned_dir: str = check.non_empty_str(owned_dir)
252
+
253
+ self._deploy_home = deploy_home
254
+
255
+ self._owned_deploy_paths = frozenset([DeployPath.parse(self._owned_dir + '/')])
256
+
257
+ @cached_nullary
258
+ def _dir(self) -> str:
259
+ return os.path.join(check.non_empty_str(self._deploy_home), self._owned_dir)
260
+
261
+ @cached_nullary
262
+ def _make_dir(self) -> str:
263
+ if not os.path.isdir(d := self._dir()):
264
+ os.makedirs(d, exist_ok=True)
265
+ return d
266
+
267
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
268
+ return self._owned_deploy_paths
@@ -1,10 +1,13 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import dataclasses as dc
3
+ import hashlib
3
4
  import typing as ta
4
5
 
6
+ from omlish.lite.cached import cached_nullary
5
7
  from omlish.lite.check import check
6
8
 
7
9
  from .types import DeployApp
10
+ from .types import DeployKey
8
11
  from .types import DeployRev
9
12
 
10
13
 
@@ -30,3 +33,10 @@ class DeploySpec:
30
33
  app: DeployApp
31
34
  repo: DeployGitRepo
32
35
  rev: DeployRev
36
+
37
+ def __post_init__(self) -> None:
38
+ hash(self)
39
+
40
+ @cached_nullary
41
+ def key(self) -> DeployKey:
42
+ return DeployKey(hashlib.sha256(repr(self).encode('utf-8')).hexdigest()[:8])
@@ -0,0 +1,46 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.cached import cached_nullary
5
+ from omlish.lite.check import check
6
+
7
+ from .atomics import DeployAtomicPathSwap
8
+ from .atomics import DeployAtomicPathSwapKind
9
+ from .atomics import DeployAtomicPathSwapping
10
+ from .atomics import TempDirDeployAtomicPathSwapping
11
+ from .paths import SingleDirDeployPathOwner
12
+ from .types import DeployHome
13
+
14
+
15
+ class DeployTmpManager(
16
+ SingleDirDeployPathOwner,
17
+ DeployAtomicPathSwapping,
18
+ ):
19
+ def __init__(
20
+ self,
21
+ *,
22
+ deploy_home: ta.Optional[DeployHome] = None,
23
+ ) -> None:
24
+ super().__init__(
25
+ owned_dir='tmp',
26
+ deploy_home=deploy_home,
27
+ )
28
+
29
+ @cached_nullary
30
+ def _swapping(self) -> DeployAtomicPathSwapping:
31
+ return TempDirDeployAtomicPathSwapping(
32
+ temp_dir=self._make_dir(),
33
+ root_dir=check.non_empty_str(self._deploy_home),
34
+ )
35
+
36
+ def begin_atomic_path_swap(
37
+ self,
38
+ kind: DeployAtomicPathSwapKind,
39
+ dst_path: str,
40
+ **kwargs: ta.Any,
41
+ ) -> DeployAtomicPathSwap:
42
+ return self._swapping().begin_atomic_path_swap(
43
+ kind,
44
+ dst_path,
45
+ **kwargs,
46
+ )
@@ -6,6 +6,7 @@ DeployHome = ta.NewType('DeployHome', str)
6
6
  DeployApp = ta.NewType('DeployApp', str)
7
7
  DeployTag = ta.NewType('DeployTag', str)
8
8
  DeployRev = ta.NewType('DeployRev', str)
9
+ DeployKey = ta.NewType('DeployKey', str)
9
10
 
10
11
 
11
12
  class DeployAppTag(ta.NamedTuple):
@@ -11,6 +11,7 @@ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
11
11
  from omlish.lite.cached import cached_nullary
12
12
  from omlish.lite.check import check
13
13
 
14
+ from .atomics import DeployAtomicPathSwapping
14
15
  from .paths import DeployPath
15
16
  from .paths import DeployPathOwner
16
17
  from .types import DeployAppTag
@@ -22,16 +23,18 @@ class DeployVenvManager(DeployPathOwner):
22
23
  self,
23
24
  *,
24
25
  deploy_home: ta.Optional[DeployHome] = None,
26
+ atomics: DeployAtomicPathSwapping,
25
27
  ) -> None:
26
28
  super().__init__()
27
29
 
28
30
  self._deploy_home = deploy_home
31
+ self._atomics = atomics
29
32
 
30
33
  @cached_nullary
31
34
  def _dir(self) -> str:
32
35
  return os.path.join(check.non_empty_str(self._deploy_home), 'venvs')
33
36
 
34
- def get_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
37
+ def get_owned_deploy_paths(self) -> ta.AbstractSet[DeployPath]:
35
38
  return {
36
39
  DeployPath.parse('venvs/@app/@tag/'),
37
40
  }
@@ -45,6 +48,8 @@ class DeployVenvManager(DeployPathOwner):
45
48
  ) -> None:
46
49
  sys_exe = 'python3'
47
50
 
51
+ # !! NOTE: (most) venvs cannot be relocated, so an atomic swap can't be used. it's up to the path manager to
52
+ # garbage collect orphaned dirs.
48
53
  await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
49
54
 
50
55
  #
@@ -2899,12 +2899,12 @@ def is_debugger_attached() -> bool:
2899
2899
  return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
2900
2900
 
2901
2901
 
2902
- REQUIRED_PYTHON_VERSION = (3, 8)
2902
+ LITE_REQUIRED_PYTHON_VERSION = (3, 8)
2903
2903
 
2904
2904
 
2905
- def check_runtime_version() -> None:
2906
- if sys.version_info < REQUIRED_PYTHON_VERSION:
2907
- raise OSError(f'Requires python {REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
2905
+ def check_lite_runtime_version() -> None:
2906
+ if sys.version_info < LITE_REQUIRED_PYTHON_VERSION:
2907
+ raise OSError(f'Requires python {LITE_REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
2908
2908
 
2909
2909
 
2910
2910
  ########################################
@@ -3482,6 +3482,7 @@ TODO:
3482
3482
  - structured
3483
3483
  - prefixed
3484
3484
  - debug
3485
+ - optional noisy? noisy will never be lite - some kinda configure_standard callback mechanism?
3485
3486
  """
3486
3487
 
3487
3488
 
@@ -3518,8 +3519,9 @@ class StandardLogFormatter(logging.Formatter):
3518
3519
  ##
3519
3520
 
3520
3521
 
3521
- class StandardLogHandler(ProxyLogHandler):
3522
- pass
3522
+ class StandardConfiguredLogHandler(ProxyLogHandler):
3523
+ def __init_subclass__(cls, **kwargs):
3524
+ raise TypeError('This class serves only as a marker and should not be subclassed.')
3523
3525
 
3524
3526
 
3525
3527
  ##
@@ -3550,7 +3552,7 @@ def configure_standard_logging(
3550
3552
  target: ta.Optional[logging.Logger] = None,
3551
3553
  force: bool = False,
3552
3554
  handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
3553
- ) -> ta.Optional[StandardLogHandler]:
3555
+ ) -> ta.Optional[StandardConfiguredLogHandler]:
3554
3556
  with _locking_logging_module_lock():
3555
3557
  if target is None:
3556
3558
  target = logging.root
@@ -3558,7 +3560,7 @@ def configure_standard_logging(
3558
3560
  #
3559
3561
 
3560
3562
  if not force:
3561
- if any(isinstance(h, StandardLogHandler) for h in list(target.handlers)):
3563
+ if any(isinstance(h, StandardConfiguredLogHandler) for h in list(target.handlers)):
3562
3564
  return None
3563
3565
 
3564
3566
  #
@@ -3592,7 +3594,7 @@ def configure_standard_logging(
3592
3594
 
3593
3595
  #
3594
3596
 
3595
- return StandardLogHandler(handler)
3597
+ return StandardConfiguredLogHandler(handler)
3596
3598
 
3597
3599
 
3598
3600
  ########################################