lockss-debugpanel 0.9.0.dev7__tar.gz → 0.10.0.dev2__tar.gz

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.
@@ -2,24 +2,36 @@
2
2
  Release Notes
3
3
  =============
4
4
 
5
+ ------
6
+ 0.10.0
7
+ ------
8
+
9
+ Released: NOT YET RELEASED
10
+
11
+ Requires Python 3.10 or greater.
12
+
5
13
  -----
6
14
  0.9.0
7
15
  -----
8
16
 
9
- Released: NOT YET RELEASED
17
+ Released: 2026-03-18
10
18
 
11
- Requires Python 3.10.
19
+ Requires Python 3.10 or greater.
12
20
 
13
21
  * **Features**
14
22
 
15
23
  * New command line infrastructure based on `Click Extra <https://kdeldycke.github.io/click-extra>`_, `Cloup <https://cloup.readthedocs.io/>`_ and `Click <https://click.palletsprojects.com/>`_, including expanded tabular output styles, progress bar, command sections and aliases.
16
24
 
25
+ * New ``--headings``//``--no-headings``, ``--progress``//``--no-progress`` output styles.
26
+
17
27
  * **Changes**
18
28
 
19
29
  * The alias ``-u`` of ``--username`` and ``-p`` of ``--password`` are deprecated in favor of ``-U`` and ``-P`` respectively.
20
30
 
21
31
  * ``--process-pool`` and ``--thread-pool`` are deprecated in favor of ``--pool-type=process-pool`` and ``--pool-type=thread-pool`` respectively.
22
32
 
33
+ * ``--output-format`` has been renamed to ``--table-format``/``-T``.
34
+
23
35
  -----
24
36
  0.8.2
25
37
  -----
@@ -1,4 +1,4 @@
1
- Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
1
+ Copyright (c) 2000-2026, 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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockss-debugpanel
3
- Version: 0.9.0.dev7
3
+ Version: 0.10.0.dev2
4
4
  Summary: Command line tool and Python library to interact with the LOCKSS 1.x DebugPanel servlet
5
5
  License: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -19,8 +19,13 @@ Classifier: Programming Language :: Python
19
19
  Classifier: Topic :: Software Development :: Libraries
20
20
  Classifier: Topic :: System :: Archiving
21
21
  Classifier: Topic :: Utilities
22
- Requires-Dist: click-extra (>=7.5.0,<7.6.0)
23
- Requires-Dist: lockss-pybasic (==0.2.0-dev16)
22
+ Requires-Dist: PyYAML (>=6.0.0,<6.1.0)
23
+ Requires-Dist: click-command-tree (>=1.2.0,<1.3.0)
24
+ Requires-Dist: click-extra[hjson,toml,xml,yaml] (>=8.1.1,<8.2.0)
25
+ Requires-Dist: click-plugins (>=1.1.1.2,<1.2.0)
26
+ Requires-Dist: cloup (>=3.0.7)
27
+ Requires-Dist: lockss-pybasic (==0.3.0.dev11)
28
+ Requires-Dist: pydantic (>=2.13.0,<2.14.0)
24
29
  Project-URL: Documentation, https://docs.lockss.org/en/latest/software/debugpanel
25
30
  Project-URL: Repository, https://github.com/lockss/lockss-debugpanel
26
31
  Project-URL: changelog, https://github.com/lockss/lockss-debugpanel/blob/main/CHANGELOG.rst
@@ -31,7 +36,7 @@ Description-Content-Type: text/x-rst
31
36
  Debugpanel
32
37
  ==========
33
38
 
34
- .. |RELEASE| replace:: 0.9.0-dev7
39
+ .. |RELEASE| replace:: 0.10.0-dev2
35
40
  .. |RELEASE_DATE| replace:: NOT YET RELEASED
36
41
  .. |DEBUGPANEL| replace:: **Debugpanel**
37
42
 
@@ -52,7 +57,7 @@ Debugpanel
52
57
 
53
58
  Quick Start::
54
59
 
55
- # Requires Python 3.9-3.13
60
+ # Requires Python 3.10 or greater
56
61
  python --version
57
62
 
58
63
  # Install with pipx
@@ -65,14 +70,9 @@ Quick Start::
65
70
  debugpanel reload-config -n lockss1.example.edu:8081
66
71
 
67
72
  # Crawl AUIDs from list.txt on lockss1.example.edu:8081 and lockss2.example.edu:8081
68
-
69
- # ...First alternative: each node gets a -n option
70
73
  debugpanel crawl -A list.txt -n lockss1.example.edu:8081 -n lockss2.example.edu:8081
71
74
 
72
- # ...Second alternative: each -n option can have arguments
73
- debugpanel crawl -A list.txt -n lockss1.example.edu:8081 lockss2.example.edu:8081
74
-
75
- # ...Third alternative: list lockss1.example.edu:8081 and lockss2.example.edu:8081 in nodes.txt
75
+ # Alternatively, list lockss1.example.edu:8081 and lockss2.example.edu:8081 in nodes.txt
76
76
  debugpanel crawl -A list.txt -N nodes.txt
77
77
 
78
78
 
@@ -2,7 +2,7 @@
2
2
  Debugpanel
3
3
  ==========
4
4
 
5
- .. |RELEASE| replace:: 0.9.0-dev7
5
+ .. |RELEASE| replace:: 0.10.0-dev2
6
6
  .. |RELEASE_DATE| replace:: NOT YET RELEASED
7
7
  .. |DEBUGPANEL| replace:: **Debugpanel**
8
8
 
@@ -23,7 +23,7 @@ Debugpanel
23
23
 
24
24
  Quick Start::
25
25
 
26
- # Requires Python 3.9-3.13
26
+ # Requires Python 3.10 or greater
27
27
  python --version
28
28
 
29
29
  # Install with pipx
@@ -36,13 +36,8 @@ Quick Start::
36
36
  debugpanel reload-config -n lockss1.example.edu:8081
37
37
 
38
38
  # Crawl AUIDs from list.txt on lockss1.example.edu:8081 and lockss2.example.edu:8081
39
-
40
- # ...First alternative: each node gets a -n option
41
39
  debugpanel crawl -A list.txt -n lockss1.example.edu:8081 -n lockss2.example.edu:8081
42
40
 
43
- # ...Second alternative: each -n option can have arguments
44
- debugpanel crawl -A list.txt -n lockss1.example.edu:8081 lockss2.example.edu:8081
45
-
46
- # ...Third alternative: list lockss1.example.edu:8081 and lockss2.example.edu:8081 in nodes.txt
41
+ # Alternatively, list lockss1.example.edu:8081 and lockss2.example.edu:8081 in nodes.txt
47
42
  debugpanel crawl -A list.txt -N nodes.txt
48
43
 
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
1
+ # Copyright (c) 2000-2026, 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:
@@ -28,7 +28,7 @@
28
28
 
29
29
  [project]
30
30
  name = "lockss-debugpanel"
31
- version = "0.9.0-dev7" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
31
+ version = "0.10.0-dev2" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
32
32
  description = "Command line tool and Python library to interact with the LOCKSS 1.x DebugPanel servlet"
33
33
  license = { text = "BSD-3-Clause" }
34
34
  readme = "README.rst"
@@ -40,8 +40,13 @@ maintainers = [
40
40
  { name = "Thib Guicherd-Callin", email = "thib@cs.stanford.edu" }
41
41
  ]
42
42
  dependencies = [
43
- "click-extra (>=7.5.0,<7.6.0)",
44
- "lockss-pybasic (==0.2.0-dev16)",
43
+ "click-command-tree (>=1.2.0,<1.3.0)",
44
+ "click-extra[hjson,toml,xml,yaml] (>=8.1.1,<8.2.0)",
45
+ "click-plugins (>=1.1.1.2,<1.2.0)",
46
+ "cloup (>=3.0.7)", # from Click Extra 8.2.1
47
+ "lockss-pybasic (==0.3.0-dev11)",
48
+ "pydantic (>=2.13.0,<2.14.0)", # from lockss-pybasic 0.3.0-dev11
49
+ "PyYAML (>=6.0.0,<6.1.0)",
45
50
  ]
46
51
  classifiers = [
47
52
  "Development Status :: 5 - Production/Stable",
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
5
+ """
6
+
7
+ __version__ = '0.10.0-dev2'
8
+
9
+ __copyright__ = '''
10
+ Copyright (c) 2000-2026, Board of Trustees of Leland Stanford Jr. University
11
+ '''.strip()
12
+
13
+ __license__ = __copyright__ + '\n\n' + '''
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice,
18
+ this list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ 3. Neither the name of the copyright holder nor the names of its contributors
25
+ may be used to endorse or promote products derived from this software without
26
+ specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
31
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
32
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
33
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
34
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
35
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
36
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
37
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
38
+ POSSIBILITY OF SUCH DAMAGE.
39
+ '''.strip()
40
+
41
+ from ._core import *
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
3
+ # Copyright (c) 2000-2026, 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:
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright (c) 2000-2026, 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 abc import ABC, abstractmethod
32
+ from base64 import b64encode
33
+ from typing import Any, Optional, TypeAlias
34
+ from urllib.request import Request, urlopen
35
+
36
+ from lockss.pybasic.errorutil import InternalError
37
+ from lockss.pybasic.nodeutil import NodeIdentifier, NodeSpec, NodeTypeEnum
38
+
39
+
40
+ #: A type alias for what ``urllib.request.urlopen`` returns.
41
+ UrlOpenT: TypeAlias = Any
42
+
43
+
44
+ #: A default depth for the deep crawl operation.
45
+ DEFAULT_DEPTH: int = 123
46
+
47
+
48
+ class _DebugPanelClientInterface(ABC):
49
+ """
50
+ Abstract base class for DebugPanel servlet clients.
51
+ """
52
+
53
+ @abstractmethod
54
+ def authenticate(self, u: str, p: str) -> _DebugPanelClientInterface:
55
+ """
56
+ Stores authentication information for this node.
57
+
58
+ :param u: The UI username.
59
+ :type u: str
60
+ :param p: The UI password.
61
+ :type p: str
62
+ :return: This instance, for chaining.
63
+ :rtype: _DebugPanelClientInterface
64
+ """
65
+ raise NotImplementedError
66
+
67
+ @abstractmethod
68
+ def check_substance(self, auid: str) -> UrlOpenT:
69
+ """
70
+ Performs the DebugPanel servlet "Check Substance" operation on this node
71
+ for the given AUID.
72
+
73
+ :param auid: An AUID.
74
+ :type auid: str
75
+ :return: The result of ``urllib.request.urlopen``.
76
+ :rtype: UrlOpenT
77
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
78
+ """
79
+ raise NotImplementedError
80
+
81
+ @abstractmethod
82
+ def crawl(self, auid: str) -> UrlOpenT:
83
+ """
84
+ Performs the DebugPanel servlet "Force Start Crawl" operation on this
85
+ node for the given AUID.
86
+
87
+ :param auid: An AUID.
88
+ :type auid: str
89
+ :return: The result of ``urllib.request.urlopen``.
90
+ :rtype: UrlOpenT
91
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
92
+ """
93
+ raise NotImplementedError
94
+
95
+ @abstractmethod
96
+ def crawl_plugins(self) -> UrlOpenT:
97
+ """
98
+ Performs the DebugPanel servlet "Crawl Plugins" operation on this node.
99
+
100
+ :return: The result of ``urllib.request.urlopen``.
101
+ :rtype: UrlOpenT
102
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
103
+ """
104
+ raise NotImplementedError
105
+
106
+ @abstractmethod
107
+ def deep_crawl(self, auid: str, depth: int = DEFAULT_DEPTH) -> UrlOpenT:
108
+ """
109
+ Performs the DebugPanel servlet "Force Deep Crawl" operation on this
110
+ node for the given AUID, with the given depth (default
111
+ ``DEFAULT_DEPTH``).
112
+
113
+ :param auid: An AUID.
114
+ :type auid: str
115
+ :param depth: A strictly positive refetch depth.
116
+ :type auid: int
117
+ :return: The result of ``urllib.request.urlopen``.
118
+ :rtype: UrlOpenT
119
+ :raises ValueError: If depth is negative or zero.
120
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
121
+ """
122
+ raise NotImplementedError
123
+
124
+ @abstractmethod
125
+ def disable_indexing(self, auid: str) -> UrlOpenT:
126
+ """
127
+ Performs the DebugPanel servlet "Disable Indexing" operation on this
128
+ node for the given AUID.
129
+
130
+ :param auid: An AUID.
131
+ :type auid: str
132
+ :return: The result of ``urllib.request.urlopen``.
133
+ :rtype: UrlOpenT
134
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
135
+ """
136
+ raise NotImplementedError
137
+
138
+ @abstractmethod
139
+ def poll(self, auid: str) -> UrlOpenT:
140
+ """
141
+ Performs the DebugPanel servlet "Start V3 Poll" operation on this node
142
+ for the given AUID.
143
+
144
+ :param auid: An AUID.
145
+ :type auid: str
146
+ :return: The result of ``urllib.request.urlopen``.
147
+ :rtype: UrlOpenT
148
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
149
+ """
150
+ raise NotImplementedError
151
+
152
+ @abstractmethod
153
+ def reindex_metadata(self, auid: str) -> UrlOpenT:
154
+ """
155
+ Performs the DebugPanel servlet "Force Reindex Metadata" operation on
156
+ this node for the given AUID.
157
+
158
+ :param auid: An AUID.
159
+ :type auid: str
160
+ :return: The result of ``urllib.request.urlopen``.
161
+ :rtype: UrlOpenT
162
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
163
+ """
164
+ raise NotImplementedError
165
+
166
+ @abstractmethod
167
+ def reload_config(self) -> UrlOpenT:
168
+ """
169
+ Performs the DebugPanel servlet "Reload Config" operation on this node.
170
+
171
+ :return: The result of ``urllib.request.urlopen``.
172
+ :rtype: UrlOpenT
173
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
174
+ """
175
+ raise NotImplementedError
176
+
177
+ @abstractmethod
178
+ def validate_files(self, auid: str) -> UrlOpenT:
179
+ """
180
+ Performs the DebugPanel servlet "Validate Files" operation on this node
181
+ for the given AUID.
182
+
183
+ :param auid: An AUID.
184
+ :type auid: str
185
+ :return: The result of ``urllib.request.urlopen``.
186
+ :rtype: UrlOpenT
187
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
188
+ """
189
+ raise NotImplementedError
190
+
191
+
192
+ class _DebugPanelClient1(_DebugPanelClientInterface):
193
+ """
194
+ _DebugPanelAdapter implementation for LOCKSS 1.x.
195
+ """
196
+
197
+ def __init__(self, client: DebugPanelClient) -> None:
198
+ super().__init__()
199
+ self._client: DebugPanelClient = client
200
+ self._basic: Optional[str] = None
201
+
202
+ def authenticate(self, u: str, p: str) -> _DebugPanelClient1:
203
+ self._basic: str = b64encode(f'{u}:{p}'.encode('utf-8')).decode('utf-8')
204
+ return self
205
+
206
+ def check_substance(self, auid: str) -> UrlOpenT:
207
+ return self._auid_action(auid, 'Check Substance')
208
+
209
+ def crawl(self, auid: str) -> UrlOpenT:
210
+ return self._auid_action(auid, 'Force Start Crawl')
211
+
212
+ def crawl_plugins(self) -> UrlOpenT:
213
+ return self._node_action('Crawl Plugins')
214
+
215
+ def deep_crawl(self, auid: str, depth: int = DEFAULT_DEPTH) -> UrlOpenT:
216
+ if depth < 1:
217
+ raise ValueError(f'depth must be a strictly positive integer, got {depth}')
218
+ return self._auid_action(auid, 'Force Deep Crawl', depth=depth)
219
+
220
+ def disable_indexing(self, auid: str) -> UrlOpenT:
221
+ return self._auid_action(auid, 'Disable Indexing')
222
+
223
+ def poll(self, auid: str) -> UrlOpenT:
224
+ return self._auid_action(auid, 'Start V3 Poll')
225
+
226
+ def reindex_metadata(self, auid: str) -> UrlOpenT:
227
+ return self._auid_action(auid, 'Force Reindex Metadata')
228
+
229
+ def reload_config(self) -> UrlOpenT:
230
+ return self._node_action('Reload Config')
231
+
232
+ def validate_files(self, auid: str) -> UrlOpenT:
233
+ return self._auid_action(auid, 'Validate Files')
234
+
235
+ def _auid_action(self, auid: str, action: str, **kwargs) -> UrlOpenT:
236
+ """
237
+ Performs one AUID-centric action.
238
+
239
+ :param auid: An AUID.
240
+ :type auid: str
241
+ :param action: An AUID-oriented DebugPanel servlet action string, e.g.
242
+ ``Force Deep Crawl``.
243
+ :type action: str
244
+ :param kwargs: Key-value pairs of additional query string arguments.
245
+ :type kwargs: dict[str, Any]
246
+ :return: The result of calling `urllib.request.urlopen`` on an appropriate
247
+ URL.
248
+ :rtype: RequestUrlOpenT
249
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
250
+ """
251
+ action_encoded = action.replace(" ", "%20")
252
+ auid_encoded = auid.replace('%', '%25').replace('|', '%7C').replace('&', '%26').replace('~', '%7E')
253
+ req = self._make_request(f'action={action_encoded}&auid={auid_encoded}', **kwargs)
254
+ return urlopen(req)
255
+
256
+ def _make_request(self, query: str, **kwargs) -> Request:
257
+ """
258
+ Constructs and authenticates an HTTP request.
259
+
260
+ :param query: A primary ampersand-separated query string, e.g.
261
+ ``"action=MyAction&auid=MyAuid"``.
262
+ :type query: str
263
+ :param kwargs: Key-value pairs of additional query string arguments, e.g.
264
+ ``(..., depth=99)`` to add ``"&depth=99"``.
265
+ :type kwargs: dict[str, Any]
266
+ :return: An authenticated ``Request`` instance (before
267
+ ``urllib.request.urlopen`` is called).
268
+ :rtype: Request
269
+ """
270
+ for key, val in kwargs.items():
271
+ query = f'{query}&{key}={val}'
272
+ url: str = f'{(ns := self._client.get_node_spec()).protocol.value}://{ns.host}:{ns.ui}/DebugPanel?{query}'
273
+ req: Request = Request(url)
274
+ req.add_header('Authorization', f'Basic {self._basic}')
275
+ return req
276
+
277
+ def _node_action(self, action: str, **kwargs) -> UrlOpenT:
278
+ """
279
+ Performs one node-centric action.
280
+
281
+ :param action: A node-oriented DebugPanel servlet action string, e.g.
282
+ ``Reload Config``.
283
+ :type action: str
284
+ :param kwargs: Key-value pairs of additional query string arguments, e.g.
285
+ ``(..., depth=99)`` to add ``"&depth=99"``.
286
+ :type kwargs: dict[str, Any]
287
+ :return: The result of calling `urllib.request.urlopen`` on an appropriate
288
+ URL.
289
+ :rtype: RequestUrlOpenT
290
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
291
+ """
292
+ action_encoded: str = action.replace(" ", "%20")
293
+ req: Request = self._make_request(f'action={action_encoded}', **kwargs)
294
+ return urlopen(req)
295
+
296
+
297
+ class _DebugPanelClient2(_DebugPanelClientInterface):
298
+ """
299
+ _DebugPanelAdapter implementation for LOCKSS 2.x, which raises
300
+ ``NotImplementedError`` for everything.
301
+ """
302
+
303
+ def authenticate(self, u: str, p: str) -> _DebugPanelClient2:
304
+ raise NotImplementedError
305
+
306
+ def check_substance(self, auid: str) -> UrlOpenT:
307
+ raise NotImplementedError
308
+
309
+ def crawl(self, auid: str) -> UrlOpenT:
310
+ raise NotImplementedError
311
+
312
+ def crawl_plugins(self) -> UrlOpenT:
313
+ raise NotImplementedError
314
+
315
+ def deep_crawl(self, auid: str, depth: int = DEFAULT_DEPTH) -> UrlOpenT:
316
+ raise NotImplementedError
317
+
318
+ def disable_indexing(self, auid: str) -> UrlOpenT:
319
+ raise NotImplementedError
320
+
321
+ def poll(self, auid: str) -> UrlOpenT:
322
+ raise NotImplementedError
323
+
324
+ def reindex_metadata(self, auid: str) -> UrlOpenT:
325
+ raise NotImplementedError
326
+
327
+ def reload_config(self) -> UrlOpenT:
328
+ raise NotImplementedError
329
+
330
+ def validate_files(self, auid: str) -> UrlOpenT:
331
+ raise NotImplementedError
332
+
333
+
334
+ class DebugPanelClient(_DebugPanelClientInterface):
335
+ """
336
+ A DebugPanel servlet client for either LOCKSS 1.x or 2.x.
337
+ """
338
+
339
+ def __init__(self, node_spec: NodeSpec):
340
+ self._node_spec: NodeSpec = node_spec
341
+ match typ := node_spec.type:
342
+ case NodeTypeEnum.V1.value:
343
+ self._impl: _DebugPanelClientInterface = _DebugPanelClient1(self)
344
+ case NodeTypeEnum.V2.value:
345
+ self._impl: _DebugPanelClientInterface = _DebugPanelClient2()
346
+ case _:
347
+ raise InternalError from ValueError(typ)
348
+
349
+ def authenticate(self, u: str, p: str) -> DebugPanelClient:
350
+ self._impl.authenticate(u, p)
351
+ return self
352
+
353
+ def check_substance(self, auid: str) -> UrlOpenT:
354
+ return self._impl.check_substance(auid)
355
+
356
+ def crawl(self, auid: str) -> UrlOpenT:
357
+ return self._impl.crawl(auid)
358
+
359
+ def crawl_plugins(self) -> UrlOpenT:
360
+ return self._impl.crawl_plugins()
361
+
362
+ def deep_crawl(self, auid: str, depth: int = DEFAULT_DEPTH) -> UrlOpenT:
363
+ return self._impl.deep_crawl(auid, depth=depth)
364
+
365
+ def disable_indexing(self, auid: str) -> UrlOpenT:
366
+ return self._impl.disable_indexing(auid)
367
+
368
+ def get_id(self) -> NodeIdentifier:
369
+ """
370
+ Returns this client's node identifier, from the node spec.
371
+
372
+ :return: This client's node identifier.
373
+ :rtype: NodeIdentifier
374
+ """
375
+ return self.get_node_spec().id
376
+
377
+ def get_node_spec(self) -> NodeSpec:
378
+ """
379
+ Returns this client's node spec.
380
+
381
+ :return: This client's node spec.
382
+ :rtype: NodeSpec
383
+ """
384
+ return self._node_spec.model_copy()
385
+
386
+ def poll(self, auid: str) -> UrlOpenT:
387
+ return self._impl.poll(auid)
388
+
389
+ def reindex_metadata(self, auid: str) -> UrlOpenT:
390
+ return self._impl.reindex_metadata(auid)
391
+
392
+ def reload_config(self) -> UrlOpenT:
393
+ return self._impl.reload_config()
394
+
395
+ def validate_files(self, auid: str) -> UrlOpenT:
396
+ return self._impl.validate_files(auid)