lockss-turtles 0.5.0.dev4__py3-none-any.whl → 0.6.0__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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # Copyright (c) 2000-2023, Board of Trustees of Leland Stanford Jr. University
3
+ # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
4
4
  #
5
5
  # Redistribution and use in source and binary forms, with or without
6
6
  # modification, are permitted provided that the following conditions are met:
@@ -28,28 +28,77 @@
28
28
  # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
29
  # POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
- from pathlib import Path, PurePath
32
- import json
31
+ """
32
+ Utility module.
33
+ """
33
34
 
34
- import jsonschema
35
- import jsonschema.exceptions
36
- import yaml
35
+ # Remove in Python 3.14; see https://stackoverflow.com/a/33533514
36
+ from __future__ import annotations
37
37
 
38
+ from collections.abc import Iterable
39
+ from pathlib import Path
40
+ from typing import Any, Optional, Union
38
41
 
39
- def _load_and_validate(schema_path, instance_path, multiple=False):
40
- with schema_path.open('r') as f:
41
- schema = json.load(f)
42
- with instance_path.open('r') as f:
43
- ret = list(yaml.safe_load_all(f) if multiple else [yaml.safe_load(f)])
44
- for instance in ret:
45
- try:
46
- jsonschema.validate(instance, schema)
47
- except jsonschema.exceptions.ValidationError as validation_exception:
48
- raise Exception(validation_exception.message) from validation_exception
49
- return ret if multiple else ret[0]
42
+ from lockss.pybasic.fileutil import path
43
+ from pydantic import BaseModel
50
44
 
51
45
 
52
- def _path(purepath_or_string):
53
- if not issubclass(type(purepath_or_string), PurePath):
54
- purepath_or_string = Path(purepath_or_string)
55
- return purepath_or_string.expanduser().resolve()
46
+ #: Type alias for the union of ``Path`` and ``str``.
47
+ PathOrStr = Union[Path, str]
48
+
49
+
50
+ class BaseModelWithRoot(BaseModel):
51
+ """
52
+ A Pydantic model with a root path which can be used to resolve relative
53
+ paths.
54
+ """
55
+
56
+ #: An internal root path.
57
+ _root: Optional[Path]
58
+
59
+ def model_post_init(self, context: Any) -> None:
60
+ """
61
+ Pydantic post-initialization method to create the ``_root`` field.
62
+ """
63
+ self._root = None
64
+
65
+ def get_root(self) -> Path:
66
+ """
67
+ Returns this object's root path.
68
+
69
+ See ``initialize``.
70
+
71
+ :return: This object's root path.
72
+ :rtype: Path
73
+ :raises ValueError: If this object's ``initialize`` method was not
74
+ called.
75
+ """
76
+ if self._root is None:
77
+ raise ValueError('Uninitialized root')
78
+ return self._root
79
+
80
+ def initialize(self,
81
+ root_path_or_str: PathOrStr) -> BaseModelWithRoot:
82
+ """
83
+ Mandatory initialization of the root path.
84
+
85
+ :param root_path_or_str: This object's root path.
86
+ :type root_path_or_str: PathOrStr
87
+ :return: This object, for chaining.
88
+ :rtype: BaseModelWithRoot
89
+ """
90
+ self._root = path(root_path_or_str)
91
+ return self
92
+
93
+
94
+ def file_or(paths: Iterable[Path]) -> str:
95
+ """
96
+ Turns an iterable of file paths into a ``" or "``-separated string suitable
97
+ for CLI messages.
98
+
99
+ :param paths: A non-null list of file paths.
100
+ :type paths: Iterable[Path]
101
+ :return: A ``" or "``-separated string of the given file paths.
102
+ :rtype: str
103
+ """
104
+ return ' or '.join(map(str, paths))
@@ -1,4 +1,4 @@
1
- Copyright (c) 2000-2023, Board of Trustees of Leland Stanford Jr. University
1
+ Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
2
2
 
3
3
  Redistribution and use in source and binary forms, with or without
4
4
  modification, are permitted provided that the following conditions are met:
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.3
2
+ Name: lockss-turtles
3
+ Version: 0.6.0
4
+ Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
5
+ License: BSD-3-Clause
6
+ Author: Thib Guicherd-Callin
7
+ Author-email: thib@cs.stanford.edu
8
+ Maintainer: Thib Guicherd-Callin
9
+ Maintainer-email: thib@cs.stanford.edu
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Framework :: Pydantic :: 2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: BSD License
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: System :: Archiving
20
+ Classifier: Topic :: Utilities
21
+ Requires-Dist: exceptiongroup (>=1.3.0,<1.4.0)
22
+ Requires-Dist: java-manifest (>=1.1.0,<1.2.0)
23
+ Requires-Dist: lockss-pybasic (>=0.1.1,<0.2.0)
24
+ Requires-Dist: pydantic (>=2.11.0,<2.12.0)
25
+ Requires-Dist: pyyaml (>=6.0.0,<6.1.0)
26
+ Requires-Dist: xdg (>=6.0.0,<6.1.0)
27
+ Project-URL: Documentation, https://docs.lockss.org/en/latest/software/turtles
28
+ Project-URL: Repository, https://github.com/lockss/lockss-turtles
29
+ Project-URL: changelog, https://github.com/lockss/lockss-turtles/blob/main/CHANGELOG.rst
30
+ Project-URL: issues, https://github.com/lockss/lockss-turtles/issues
31
+ Description-Content-Type: text/x-rst
32
+
33
+ =======
34
+ Turtles
35
+ =======
36
+
37
+ .. |RELEASE| replace:: 0.6.0
38
+ .. |RELEASE_DATE| replace:: 2025-10-23
39
+ .. |TURTLES| replace:: **Turtles**
40
+
41
+ .. image:: https://assets.lockss.org/images/logos/turtles/turtles_128x128.png
42
+ :alt: Turtles logo
43
+ :align: right
44
+
45
+ |TURTLES| is a command line tool and Python library to manage LOCKSS plugin sets and LOCKSS plugin registries.
46
+
47
+ :Latest release: |RELEASE| (|RELEASE_DATE|)
48
+ :Documentation: https://docs.lockss.org/en/latest/software/turtles
49
+ :Release notes: `CHANGELOG.rst <https://github.com/lockss/lockss-turtles/blob/main/CHANGELOG.rst>`_
50
+ :License: `LICENSE <https://github.com/lockss/lockss-turtles/blob/main/LICENSE>`_
51
+ :Repository: https://github.com/lockss/lockss-turtles
52
+ :Issues: https://github.com/lockss/lockss-turtles/issues
53
+
54
+ ----
55
+
56
+ Quick Start::
57
+
58
+ # Install with pipx
59
+ pipx install lockss-turtles
60
+
61
+ # Verify installation and discover all the commands
62
+ turtles --help
63
+
64
+
@@ -0,0 +1,18 @@
1
+ lockss/turtles/__init__.py,sha256=6aGkk-abZwTPYy_tMY72hKjNJjtoDCcQvnBtILsTEsk,1821
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=QCUWh56NV5YMQmZIz3-9DYHZI42BC3Aain5mxax7x9o,24668
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=NXQyvvV3u1Pk3h5e-W7F93NIL6rYrv3bN_N4TFuZ0_U,18644
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.dist-info/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
15
+ lockss_turtles-0.6.0.dist-info/METADATA,sha256=AUSE2g09EKcSqA34-m-TOwdsmjxXuBMesC_sBicF6Lo,2305
16
+ lockss_turtles-0.6.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ lockss_turtles-0.6.0.dist-info/entry_points.txt,sha256=25BAVFSBRKWAWiXIGZgcr1ypt2mV7nj31Jl8WcNZZOk,51
18
+ lockss_turtles-0.6.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,106 @@
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
+ from collections.abc import Callable
32
+ from pathlib import Path
33
+ from pydantic import ValidationError
34
+ from pydantic_core import ErrorDetails
35
+ from typing import Any, Optional, Tuple, Union
36
+ from unittest import TestCase
37
+
38
+
39
+ ROOT: Path = Path('.').absolute()
40
+
41
+
42
+ Loc = Union[Tuple[str], str]
43
+
44
+
45
+ class PydanticTestCase(TestCase):
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
+
93
+ def _assertPydanticValidationError(self,
94
+ func: Callable[[], Any],
95
+ matcher: Callable[[ErrorDetails], bool],
96
+ msg: Optional[str]=None) -> None:
97
+ with self.assertRaises(ValidationError) as cm:
98
+ func()
99
+ ve: ValidationError = cm.exception
100
+ for e in ve.errors():
101
+ if matcher(e):
102
+ return
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}')
104
+
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