lockss-turtles 0.6.0.dev24__py3-none-any.whl → 0.6.0.dev26__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.
- lockss/turtles/__init__.py +1 -1
- lockss/turtles/app.py +346 -53
- lockss/turtles/cli.py +370 -99
- lockss/turtles/plugin.py +2 -3
- lockss/turtles/plugin_registry.py +5 -6
- lockss/turtles/plugin_set.py +24 -24
- lockss/turtles/plugin_signing_credentials.py +84 -0
- lockss/turtles/util.py +2 -3
- {lockss_turtles-0.6.0.dev24.dist-info → lockss_turtles-0.6.0.dev26.dist-info}/METADATA +4 -3
- lockss_turtles-0.6.0.dev26.dist-info/RECORD +18 -0
- {lockss_turtles-0.6.0.dev24.dist-info → lockss_turtles-0.6.0.dev26.dist-info}/WHEEL +1 -1
- unittest/lockss/turtles/__init__.py +53 -12
- unittest/lockss/turtles/test_plugin_registry.py +411 -0
- unittest/lockss/turtles/test_plugin_set.py +235 -38
- unittest/lockss/turtles/test_plugin_signing_credentials.py +102 -0
- lockss_turtles-0.6.0.dev24.dist-info/RECORD +0 -15
- {lockss_turtles-0.6.0.dev24.dist-info → lockss_turtles-0.6.0.dev26.dist-info}/entry_points.txt +0 -0
- {lockss_turtles-0.6.0.dev24.dist-info → lockss_turtles-0.6.0.dev26.dist-info/licenses}/LICENSE +0 -0
lockss/turtles/plugin.py
CHANGED
|
@@ -29,11 +29,10 @@
|
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
31
|
"""
|
|
32
|
-
|
|
32
|
+
Module to represent a LOCKSS plugin.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
# Remove in Python 3.14
|
|
36
|
-
# See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
37
36
|
from __future__ import annotations
|
|
38
37
|
|
|
39
38
|
from collections.abc import Callable
|
|
@@ -29,11 +29,10 @@
|
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
31
|
"""
|
|
32
|
-
|
|
32
|
+
Module to represent plugin registries and plugin registry catalogs.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
# Remove in Python 3.14
|
|
36
|
-
# See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
37
36
|
from __future__ import annotations
|
|
38
37
|
|
|
39
38
|
from abc import ABC, abstractmethod
|
|
@@ -318,8 +317,8 @@ class DirectoryPluginRegistryLayout(BasePluginRegistryLayout):
|
|
|
318
317
|
type: Literal['directory'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
|
|
319
318
|
|
|
320
319
|
#: This plugin registry layout's file naming convention.
|
|
321
|
-
file_naming_convention:
|
|
322
|
-
|
|
320
|
+
file_naming_convention: PluginRegistryLayoutFileNamingConvention = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT,
|
|
321
|
+
**BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
|
|
323
322
|
|
|
324
323
|
def _copy_jar(self,
|
|
325
324
|
src_path: Path,
|
|
@@ -478,7 +477,7 @@ class PluginRegistryLayer(BaseModel):
|
|
|
478
477
|
:return: This plugin registry layer's path.
|
|
479
478
|
:rtype: Path
|
|
480
479
|
"""
|
|
481
|
-
return
|
|
480
|
+
return self.get_plugin_registry().get_root().joinpath(self.path)
|
|
482
481
|
|
|
483
482
|
def get_plugin_registry(self) -> PluginRegistry:
|
|
484
483
|
"""
|
lockss/turtles/plugin_set.py
CHANGED
|
@@ -29,11 +29,10 @@
|
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
31
|
"""
|
|
32
|
-
|
|
32
|
+
Module to represent plugin sets and plugin set catalogs.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
# Remove in Python 3.14
|
|
36
|
-
# See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
37
36
|
from __future__ import annotations
|
|
38
37
|
|
|
39
38
|
from abc import ABC, abstractmethod
|
|
@@ -42,7 +41,7 @@ from pathlib import Path
|
|
|
42
41
|
import shlex
|
|
43
42
|
import subprocess
|
|
44
43
|
import sys
|
|
45
|
-
from typing import Annotated, Any, ClassVar, Literal, Optional, Union
|
|
44
|
+
from typing import Annotated, Any, Callable, ClassVar, Literal, Optional, Union
|
|
46
45
|
|
|
47
46
|
from pydantic import BaseModel, Field
|
|
48
47
|
|
|
@@ -234,12 +233,12 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
|
|
|
234
233
|
type: Literal['maven'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
|
|
235
234
|
|
|
236
235
|
#: This plugin set builder's main code path.
|
|
237
|
-
main:
|
|
238
|
-
|
|
236
|
+
main: str = Field(DEFAULT_MAIN,
|
|
237
|
+
**BasePluginSetBuilder.MAIN_FIELD)
|
|
239
238
|
|
|
240
239
|
#: This plugin set builder's unit test path.
|
|
241
|
-
test:
|
|
242
|
-
|
|
240
|
+
test: str = Field(DEFAULT_TEST,
|
|
241
|
+
**BasePluginSetBuilder.TEST_FIELD)
|
|
243
242
|
|
|
244
243
|
#: An internal flag to remember if a build has occurred.
|
|
245
244
|
_built: bool
|
|
@@ -248,7 +247,7 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
|
|
|
248
247
|
plugin_id: PluginIdentifier,
|
|
249
248
|
keystore_path: Path,
|
|
250
249
|
keystore_alias: str,
|
|
251
|
-
keystore_password=None) -> tuple[Path, Plugin]:
|
|
250
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
252
251
|
"""
|
|
253
252
|
Builds the given plugin with the supplied plugin signing credentials.
|
|
254
253
|
|
|
@@ -285,7 +284,7 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
|
|
|
285
284
|
def _big_build(self,
|
|
286
285
|
keystore_path: Path,
|
|
287
286
|
keystore_alias: str,
|
|
288
|
-
keystore_password: str=None) -> None:
|
|
287
|
+
keystore_password: Optional[Callable[[], str]]=None) -> None:
|
|
289
288
|
"""
|
|
290
289
|
Runs ``mvn package`` on the project if the ``_built`` flag is False.
|
|
291
290
|
|
|
@@ -301,8 +300,9 @@ class MavenPluginSetBuilder(BasePluginSetBuilder):
|
|
|
301
300
|
# Do build
|
|
302
301
|
cmd = ['mvn', 'package',
|
|
303
302
|
f'-Dkeystore.file={keystore_path!s}',
|
|
304
|
-
f'-Dkeystore.alias={keystore_alias}'
|
|
305
|
-
|
|
303
|
+
f'-Dkeystore.alias={keystore_alias}']
|
|
304
|
+
if keystore_password:
|
|
305
|
+
cmd.extend(f'-Dkeystore.password={keystore_password()}')
|
|
306
306
|
try:
|
|
307
307
|
subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
|
|
308
308
|
except subprocess.CalledProcessError as cpe:
|
|
@@ -363,12 +363,12 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
|
363
363
|
type: Literal['ant'] = Field(**BasePluginSetBuilder.TYPE_FIELD)
|
|
364
364
|
|
|
365
365
|
#: This plugin set builder's main code path.
|
|
366
|
-
main:
|
|
367
|
-
|
|
366
|
+
main: str = Field(DEFAULT_MAIN,
|
|
367
|
+
**BasePluginSetBuilder.MAIN_FIELD)
|
|
368
368
|
|
|
369
369
|
#: This plugin set builder's unit test path.
|
|
370
|
-
test:
|
|
371
|
-
|
|
370
|
+
test: str = Field(DEFAULT_TEST,
|
|
371
|
+
**BasePluginSetBuilder.TEST_FIELD)
|
|
372
372
|
|
|
373
373
|
#: An internal flag to remember if a build has occurred.
|
|
374
374
|
_built: bool
|
|
@@ -377,7 +377,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
|
377
377
|
plugin_id: PluginIdentifier,
|
|
378
378
|
keystore_path: Path,
|
|
379
379
|
keystore_alias: str,
|
|
380
|
-
keystore_password=None) -> tuple[Path, Plugin]:
|
|
380
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
381
381
|
"""
|
|
382
382
|
Builds the given plugin with the supplied plugin signing credentials.
|
|
383
383
|
|
|
@@ -388,7 +388,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
|
388
388
|
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
389
389
|
:type keystore_alias: str
|
|
390
390
|
:param keystore_password: The plugin signing password.
|
|
391
|
-
:type keystore_password:
|
|
391
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
392
392
|
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
393
393
|
instantiated from it.
|
|
394
394
|
:rtype: tuple[Path, Plugin]
|
|
@@ -433,7 +433,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
|
433
433
|
plugin_id: PluginIdentifier,
|
|
434
434
|
keystore_path: Path,
|
|
435
435
|
keystore_alias: str,
|
|
436
|
-
keystore_password: str=None) -> tuple[Path, Plugin]:
|
|
436
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
437
437
|
"""
|
|
438
438
|
Performs the "little build" of the given plugin.
|
|
439
439
|
|
|
@@ -444,7 +444,7 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
|
444
444
|
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
445
445
|
:type keystore_alias: str
|
|
446
446
|
:param keystore_password: The plugin signing password.
|
|
447
|
-
:type keystore_password:
|
|
447
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
448
448
|
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
449
449
|
instantiated from it.
|
|
450
450
|
:rtype: tuple[Path, Plugin]
|
|
@@ -483,8 +483,8 @@ class AntPluginSetBuilder(BasePluginSetBuilder):
|
|
|
483
483
|
'--jar', str(jar_path),
|
|
484
484
|
'--alias', keystore_alias,
|
|
485
485
|
'--keystore', str(keystore_path)]
|
|
486
|
-
if keystore_password
|
|
487
|
-
cmd.extend(['--password', keystore_password])
|
|
486
|
+
if keystore_password:
|
|
487
|
+
cmd.extend(['--password', keystore_password()])
|
|
488
488
|
try:
|
|
489
489
|
subprocess.run(cmd, cwd=self.get_root(), check=True, stdout=sys.stdout, stderr=sys.stderr)
|
|
490
490
|
except subprocess.CalledProcessError as cpe:
|
|
@@ -552,7 +552,7 @@ class PluginSet(BaseModel):
|
|
|
552
552
|
plugin_id: PluginIdentifier,
|
|
553
553
|
keystore_path: Path,
|
|
554
554
|
keystore_alias: str,
|
|
555
|
-
keystore_password=None) -> tuple[Path, Plugin]:
|
|
555
|
+
keystore_password: Optional[Callable[[], str]]=None) -> tuple[Path, Plugin]:
|
|
556
556
|
"""
|
|
557
557
|
Builds the given plugin with the supplied plugin signing credentials.
|
|
558
558
|
|
|
@@ -563,7 +563,7 @@ class PluginSet(BaseModel):
|
|
|
563
563
|
:param keystore_alias: The alias to use in the plugin signing keystore.
|
|
564
564
|
:type keystore_alias: str
|
|
565
565
|
:param keystore_password: The plugin signing password.
|
|
566
|
-
:type keystore_password:
|
|
566
|
+
:type keystore_password: Optional[Callable[[], str]]
|
|
567
567
|
:return: A tuple of the built and signed JAR file and a Plugin object
|
|
568
568
|
instantiated from it.
|
|
569
569
|
"""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software without
|
|
17
|
+
# specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
Module to represent plugin signing credentials.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Literal
|
|
37
|
+
|
|
38
|
+
from pydantic import Field
|
|
39
|
+
|
|
40
|
+
from .util import BaseModelWithRoot
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
#: A type alias for the plugin signing credentials kind.
|
|
44
|
+
PluginSigningCredentialsKind = Literal['PluginSigningCredentials']
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PluginSigningCredentials(BaseModelWithRoot):
|
|
48
|
+
"""
|
|
49
|
+
A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent
|
|
50
|
+
plugin signing credentials.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
#: This object's kind.
|
|
54
|
+
kind: PluginSigningCredentialsKind = Field(title='Kind',
|
|
55
|
+
description="This object's kind")
|
|
56
|
+
|
|
57
|
+
#: Path to the plugin signing keystore.
|
|
58
|
+
plugin_signing_keystore: str = Field(alias='plugin-signing-keystore',
|
|
59
|
+
title='Plugin Signing Keystore',
|
|
60
|
+
description='A path to the plugin signing keystore')
|
|
61
|
+
|
|
62
|
+
#: Alias to use from the plugin signing keystore.
|
|
63
|
+
plugin_signing_alias: str = Field(alias='plugin-signing-alias',
|
|
64
|
+
title='Plugin Signing Alias',
|
|
65
|
+
description='The plugin signing alias to use')
|
|
66
|
+
|
|
67
|
+
def get_plugin_signing_alias(self) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Returns the alias to use from the plugin signing keystore.
|
|
70
|
+
|
|
71
|
+
:return: The plugin signing alias.
|
|
72
|
+
:rtype: str
|
|
73
|
+
"""
|
|
74
|
+
return self.plugin_signing_alias
|
|
75
|
+
|
|
76
|
+
def get_plugin_signing_keystore(self) -> Path:
|
|
77
|
+
"""
|
|
78
|
+
The file path for the plugin signing keystore; a relative path is
|
|
79
|
+
understood to be relative to the file containing the definition.
|
|
80
|
+
|
|
81
|
+
:return: The file path for the plugin signing keystore.
|
|
82
|
+
:rtype: Path
|
|
83
|
+
"""
|
|
84
|
+
return self.get_root().joinpath(self.plugin_signing_keystore)
|
lockss/turtles/util.py
CHANGED
|
@@ -29,11 +29,10 @@
|
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
31
|
"""
|
|
32
|
-
|
|
32
|
+
Utility module.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
# Remove in Python 3.14
|
|
36
|
-
# See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
37
36
|
from __future__ import annotations
|
|
38
37
|
|
|
39
38
|
from collections.abc import Iterable
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lockss-turtles
|
|
3
|
-
Version: 0.6.0.
|
|
3
|
+
Version: 0.6.0.dev26
|
|
4
4
|
Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
|
|
5
5
|
License: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Thib Guicherd-Callin
|
|
7
8
|
Author-email: thib@cs.stanford.edu
|
|
8
9
|
Maintainer: Thib Guicherd-Callin
|
|
@@ -34,7 +35,7 @@ Description-Content-Type: text/x-rst
|
|
|
34
35
|
Turtles
|
|
35
36
|
=======
|
|
36
37
|
|
|
37
|
-
.. |RELEASE| replace:: 0.6.0-
|
|
38
|
+
.. |RELEASE| replace:: 0.6.0-dev26 NOT YET RELEASED
|
|
38
39
|
.. |RELEASE_DATE| replace:: NOT YET RELEASED
|
|
39
40
|
.. |TURTLES| replace:: **Turtles**
|
|
40
41
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
lockss/turtles/__init__.py,sha256=DFU1_qgdgSb_zxc9Lw6YgvadqEQQ4qAwx_0x1hX-cQU,1827
|
|
2
|
+
lockss/turtles/__main__.py,sha256=BqRTG3dejCgLh87UJ1Os9DD9unxnQkOdUQM6eqWS3Sg,1680
|
|
3
|
+
lockss/turtles/app.py,sha256=XnBF91r-D3YGSfgIP0v9C064fH8Rc6y4I5y1arEweCQ,30520
|
|
4
|
+
lockss/turtles/cli.py,sha256=x4Ciba5fcDSNeYAhhauOeCP1FcMy5ptOts32CWG3la4,30546
|
|
5
|
+
lockss/turtles/plugin.py,sha256=fxIAaSL06uv9sjYCz2wTQjn_92qPK3EvGNnbkpeSZcA,10972
|
|
6
|
+
lockss/turtles/plugin_registry.py,sha256=8BR6CrqY8hh4zpy13B8gZ9qpCMASlOxvX6g8VmpWeMc,26864
|
|
7
|
+
lockss/turtles/plugin_set.py,sha256=4fUmpp_GF_WY06gyhm9tVofYINoTXYUqAvKik3Z4bE4,24640
|
|
8
|
+
lockss/turtles/plugin_signing_credentials.py,sha256=EekUJeXvbfRcrVHCsvvbGBO07bSBzEgVL1Ck4TUbe8k,3415
|
|
9
|
+
lockss/turtles/util.py,sha256=sBvB96BBED_yVZOybcf-sIwldNm9wstgWssnhugpww0,3536
|
|
10
|
+
unittest/lockss/turtles/__init__.py,sha256=ude-nT_uHF0lCD9qKrzedm8QvwqTiNjogpyz8SJudOQ,5598
|
|
11
|
+
unittest/lockss/turtles/test_plugin_registry.py,sha256=PIBeAKoDudInJOGhwTSug_b4AugGtK5yFmapUtQYTNw,18372
|
|
12
|
+
unittest/lockss/turtles/test_plugin_set.py,sha256=SJ2IO3sVOkm1zRr4a9SbyMhKpyKZAzoDV1oVfya3eEU,10949
|
|
13
|
+
unittest/lockss/turtles/test_plugin_signing_credentials.py,sha256=oxAKxeIEATNu3dIajcAFy-gEChMDCHSSDjZotuAB49o,5269
|
|
14
|
+
lockss_turtles-0.6.0.dev26.dist-info/METADATA,sha256=PqR_im9Se6s6-2gDrojNn07LUi2CKESgngN4UTmX8YM,2362
|
|
15
|
+
lockss_turtles-0.6.0.dev26.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
16
|
+
lockss_turtles-0.6.0.dev26.dist-info/entry_points.txt,sha256=25BAVFSBRKWAWiXIGZgcr1ypt2mV7nj31Jl8WcNZZOk,51
|
|
17
|
+
lockss_turtles-0.6.0.dev26.dist-info/licenses/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
|
|
18
|
+
lockss_turtles-0.6.0.dev26.dist-info/RECORD,,
|
|
@@ -29,37 +29,78 @@
|
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
31
|
from collections.abc import Callable
|
|
32
|
+
from pathlib import Path
|
|
32
33
|
from pydantic import ValidationError
|
|
33
34
|
from pydantic_core import ErrorDetails
|
|
34
35
|
from typing import Any, Optional, Tuple, Union
|
|
35
36
|
from unittest import TestCase
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
ROOT: Path = Path('.').absolute()
|
|
40
|
+
|
|
41
|
+
|
|
38
42
|
Loc = Union[Tuple[str], str]
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
class PydanticTestCase(TestCase):
|
|
42
46
|
|
|
47
|
+
def assertPydanticListType(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
|
|
48
|
+
self._assertPydanticValidationError(func,
|
|
49
|
+
lambda e: e.get('type') == 'list_type' \
|
|
50
|
+
and e.get('loc') == self._loc(loc_tuple_or_str),
|
|
51
|
+
msg=msg)
|
|
52
|
+
|
|
53
|
+
def assertPydanticLiteralError(self, func: Callable[[], Any], loc_tuple_or_str: Loc, expected: Union[Tuple[str], str], msg=None) -> None:
|
|
54
|
+
if isinstance(expected, str):
|
|
55
|
+
exp = f"'{expected}'"
|
|
56
|
+
elif len(expected) == 0:
|
|
57
|
+
raise RuntimeError(f"'expected' cannot be an empty tuple")
|
|
58
|
+
elif len(expected) == 1:
|
|
59
|
+
exp = f"'{expected[0]}'"
|
|
60
|
+
else:
|
|
61
|
+
exp = f'''{", ".join(f"'{e}'" for e in expected[:-1])} or '{expected[-1]}\''''
|
|
62
|
+
self._assertPydanticValidationError(func,
|
|
63
|
+
lambda e: e.get('type') == 'literal_error' \
|
|
64
|
+
and e.get('loc') == self._loc(loc_tuple_or_str) \
|
|
65
|
+
and (ctx := e.get('ctx')) \
|
|
66
|
+
and ctx.get('expected') == exp,
|
|
67
|
+
msg=msg)
|
|
68
|
+
|
|
69
|
+
def assertPydanticModelAttributesType(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
|
|
70
|
+
self._assertPydanticValidationError(func,
|
|
71
|
+
lambda e: e.get('type') == 'model_attributes_type' \
|
|
72
|
+
and e.get('loc') == self._loc(loc_tuple_or_str),
|
|
73
|
+
msg=msg)
|
|
74
|
+
|
|
75
|
+
def assertPydanticMissing(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
|
|
76
|
+
self._assertPydanticValidationError(func,
|
|
77
|
+
lambda e: e.get('type') == 'missing' \
|
|
78
|
+
and e.get('loc') == self._loc(loc_tuple_or_str),
|
|
79
|
+
msg=msg)
|
|
80
|
+
|
|
81
|
+
def assertPydanticStringType(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
|
|
82
|
+
self._assertPydanticValidationError(func,
|
|
83
|
+
lambda e: e.get('type') == 'string_type' \
|
|
84
|
+
and e.get('loc') == self._loc(loc_tuple_or_str),
|
|
85
|
+
msg=msg)
|
|
86
|
+
|
|
87
|
+
def assertPydanticTooShort(self, func: Callable[[], Any], loc_tuple_or_str: Loc, msg=None) -> None:
|
|
88
|
+
self._assertPydanticValidationError(func,
|
|
89
|
+
lambda e: e.get('type') == 'too_short' \
|
|
90
|
+
and e.get('loc') == self._loc(loc_tuple_or_str),
|
|
91
|
+
msg=msg)
|
|
92
|
+
|
|
43
93
|
def _assertPydanticValidationError(self,
|
|
44
94
|
func: Callable[[], Any],
|
|
45
95
|
matcher: Callable[[ErrorDetails], bool],
|
|
46
|
-
msg: Optional[str]=None):
|
|
96
|
+
msg: Optional[str]=None) -> None:
|
|
47
97
|
with self.assertRaises(ValidationError) as cm:
|
|
48
98
|
func()
|
|
49
|
-
self.fail('Expected ValidationError but did not get one')
|
|
50
|
-
self.assertIsInstance(cm.exception, ValidationError)
|
|
51
99
|
ve: ValidationError = cm.exception
|
|
52
100
|
for e in ve.errors():
|
|
53
101
|
if matcher(e):
|
|
54
102
|
return
|
|
55
103
|
self.fail(msg or f'Did not get a matching ValidationError; got:\n{"\n".join([str(e) for e in ve.errors()])}\n{ve!s}')
|
|
56
104
|
|
|
57
|
-
def
|
|
58
|
-
if isinstance(
|
|
59
|
-
loc = (loc,)
|
|
60
|
-
self._assertPydanticValidationError(func, lambda e: e.get('type') == 'missing' and e.get('loc') == loc)
|
|
61
|
-
|
|
62
|
-
def assertPydanticLiteralError(self, func: Callable[[], Any], loc: Loc, expected: str, msg=None) -> None:
|
|
63
|
-
if isinstance(loc, str):
|
|
64
|
-
loc = (loc,)
|
|
65
|
-
self._assertPydanticValidationError(func, lambda e: e.get('type') == 'literal_error' and e.get('loc') == loc and (ctx := e.get('ctx')) and ctx.get('expected') == repr(expected))
|
|
105
|
+
def _loc(self, loc_tuple_or_str: Loc) -> tuple[str]:
|
|
106
|
+
return (loc_tuple_or_str,) if isinstance(loc_tuple_or_str, str) else loc_tuple_or_str
|