lockss-pybasic 0.1.0.dev23__py3-none-any.whl → 0.2.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.
- lockss/pybasic/__init__.py +2 -2
- lockss/pybasic/auidutil.py +417 -0
- lockss/pybasic/cliutil.py +180 -186
- lockss/pybasic/errorutil.py +1 -1
- lockss/pybasic/fileutil.py +1 -1
- lockss_pybasic-0.2.0.dist-info/METADATA +68 -0
- lockss_pybasic-0.2.0.dist-info/RECORD +9 -0
- {lockss_pybasic-0.1.0.dev23.dist-info → lockss_pybasic-0.2.0.dist-info}/WHEEL +1 -1
- {lockss_pybasic-0.1.0.dev23.dist-info → lockss_pybasic-0.2.0.dist-info/licenses}/LICENSE +1 -1
- lockss/pybasic/outpututil.py +0 -57
- lockss_pybasic-0.1.0.dev23.dist-info/METADATA +0 -102
- lockss_pybasic-0.1.0.dev23.dist-info/RECORD +0 -9
lockss/pybasic/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ Basic Python utilities.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
__copyright__ = '''
|
|
8
|
-
Copyright (c) 2000-
|
|
8
|
+
Copyright (c) 2000-2026, Board of Trustees of Leland Stanford Jr. University
|
|
9
9
|
'''.strip()
|
|
10
10
|
|
|
11
11
|
__license__ = __copyright__ + '\n\n' + '''
|
|
@@ -36,4 +36,4 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
36
36
|
POSSIBILITY OF SUCH DAMAGE.
|
|
37
37
|
'''.strip()
|
|
38
38
|
|
|
39
|
-
__version__ = '0.
|
|
39
|
+
__version__ = '0.2.0'
|
|
@@ -0,0 +1,417 @@
|
|
|
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
|
+
"""
|
|
32
|
+
LOCKSS AUID Generator
|
|
33
|
+
|
|
34
|
+
Port of the AUID generation logic from org.lockss.plugin.PluginManager
|
|
35
|
+
and related utility classes in the LOCKSS lockss-core library.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import urllib.parse
|
|
39
|
+
from typing import Dict
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvalidAuidError(ValueError):
|
|
43
|
+
"""Raised when an AUID string is malformed or invalid."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AuidGenerator:
|
|
48
|
+
"""
|
|
49
|
+
Generator for LOCKSS Archival Unit Identifiers (AUIDs).
|
|
50
|
+
|
|
51
|
+
This class provides static methods for generating and parsing AUIDs,
|
|
52
|
+
which uniquely identify archival units in the LOCKSS system.
|
|
53
|
+
|
|
54
|
+
AUID Format: pluginKey&auKey
|
|
55
|
+
- pluginKey: Plugin class name with dots replaced by pipes
|
|
56
|
+
- auKey: Canonically encoded definitional parameters
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
>>> plugin_id = "org.lockss.plugin.simulated.SimulatedPlugin"
|
|
60
|
+
>>> params = {"base_url": "http://example.com/", "year": "2023"}
|
|
61
|
+
>>> auid = AuidGenerator.generate_auid(plugin_id, params)
|
|
62
|
+
>>> print(auid)
|
|
63
|
+
org|lockss|plugin|simulated|SimulatedPlugin&base_url~http%3A%2F%2Fexample%2Ecom%2F&year~2023
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def plugin_key_from_id(plugin_id: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Convert plugin ID to plugin key by replacing dots with pipes.
|
|
70
|
+
|
|
71
|
+
Port of PluginManager.pluginKeyFromId()
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
plugin_id: Plugin class name (e.g., "org.lockss.plugin.simulated.SimulatedPlugin")
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Plugin key (e.g., "org|lockss|plugin|simulated|SimulatedPlugin")
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> AuidGenerator.plugin_key_from_id("org.lockss.plugin.TestPlugin")
|
|
81
|
+
'org|lockss|plugin|TestPlugin'
|
|
82
|
+
"""
|
|
83
|
+
if not plugin_id:
|
|
84
|
+
raise ValueError("plugin_id cannot be empty")
|
|
85
|
+
return plugin_id.replace(".", "|")
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def plugin_id_from_key(plugin_key: str) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Convert plugin key back to plugin ID by replacing pipes with dots.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
plugin_key: Plugin key (e.g., "org|lockss|plugin|simulated|SimulatedPlugin")
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Plugin ID (e.g., "org.lockss.plugin.simulated.SimulatedPlugin")
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
>>> AuidGenerator.plugin_id_from_key("org|lockss|plugin|TestPlugin")
|
|
100
|
+
'org.lockss.plugin.TestPlugin'
|
|
101
|
+
"""
|
|
102
|
+
if not plugin_key:
|
|
103
|
+
raise ValueError("plugin_key cannot be empty")
|
|
104
|
+
return plugin_key.replace("|", ".")
|
|
105
|
+
|
|
106
|
+
# Characters that don't need encoding - matches Java PropKeyEncoder exactly
|
|
107
|
+
# See lockss-core PropKeyEncoder.java lines 46-62
|
|
108
|
+
_DONT_NEED_ENCODING = set(
|
|
109
|
+
'abcdefghijklmnopqrstuvwxyz'
|
|
110
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
111
|
+
'0123456789'
|
|
112
|
+
' ' # Space is converted to '+' in encode()
|
|
113
|
+
'-'
|
|
114
|
+
'_'
|
|
115
|
+
'*'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def encode_component(s: str) -> str:
|
|
120
|
+
"""
|
|
121
|
+
URL-encode a string component for use in AUID.
|
|
122
|
+
|
|
123
|
+
Port of PropKeyEncoder.encode() from lockss-core.
|
|
124
|
+
|
|
125
|
+
This method encodes strings using URL encoding with the following rules:
|
|
126
|
+
- Alphanumeric characters (a-z, A-Z, 0-9) are not encoded
|
|
127
|
+
- Hyphens (-), underscores (_), and asterisks (*) are not encoded
|
|
128
|
+
- Spaces are encoded as '+'
|
|
129
|
+
- All other characters (including periods) are percent-encoded with uppercase hex digits
|
|
130
|
+
|
|
131
|
+
Note: This differs from standard URL encoding (RFC 3986) which treats
|
|
132
|
+
periods (.) as unreserved. Java's PropKeyEncoder encodes periods.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
s: String to encode
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
URL-encoded string with spaces as '+' and uppercase hex digits
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> AuidGenerator.encode_component("http://example.com/")
|
|
142
|
+
'http%3A%2F%2Fexample%2Ecom%2F'
|
|
143
|
+
"""
|
|
144
|
+
if not s:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
result = []
|
|
148
|
+
# Encode string to UTF-8 bytes, matching Java's OutputStreamWriter behavior
|
|
149
|
+
for char in s:
|
|
150
|
+
if char in AuidGenerator._DONT_NEED_ENCODING:
|
|
151
|
+
if char == ' ':
|
|
152
|
+
result.append('+')
|
|
153
|
+
else:
|
|
154
|
+
result.append(char)
|
|
155
|
+
else:
|
|
156
|
+
# Encode character to UTF-8 bytes and percent-encode each byte
|
|
157
|
+
char_bytes = char.encode('utf-8')
|
|
158
|
+
for byte in char_bytes:
|
|
159
|
+
result.append('%')
|
|
160
|
+
result.append(format(byte, '02X'))
|
|
161
|
+
|
|
162
|
+
return ''.join(result)
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def decode_component(s: str) -> str:
|
|
166
|
+
"""
|
|
167
|
+
URL-decode a string component from an AUID.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
s: URL-encoded string
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Decoded string
|
|
174
|
+
|
|
175
|
+
Example:
|
|
176
|
+
>>> AuidGenerator.decode_component('http%3A%2F%2Fexample.com%2F')
|
|
177
|
+
'http://example.com/'
|
|
178
|
+
"""
|
|
179
|
+
if not s:
|
|
180
|
+
return ""
|
|
181
|
+
return urllib.parse.unquote_plus(s, errors="strict")
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
def props_to_canonical_encoded_string(props: Dict[str, str]) -> str:
|
|
185
|
+
"""
|
|
186
|
+
Convert properties dictionary to canonical encoded string.
|
|
187
|
+
|
|
188
|
+
Port of PropUtil.propsToCanonicalEncodedString() from lockss-core.
|
|
189
|
+
|
|
190
|
+
The canonical form is created by:
|
|
191
|
+
1. Sorting keys alphabetically
|
|
192
|
+
2. Encoding each key and value
|
|
193
|
+
3. Joining with '~' between key and value, '&' between pairs
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
props: Dictionary of AU definitional parameters
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Canonical encoded string (e.g., "key1~val1&key2~val2")
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
>>> props = {"year": "2023", "base_url": "http://example.com/"}
|
|
203
|
+
>>> AuidGenerator.props_to_canonical_encoded_string(props)
|
|
204
|
+
'base_url~http%3A%2F%2Fexample.com%2F&year~2023'
|
|
205
|
+
"""
|
|
206
|
+
if not props:
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
# Sort keys for canonical ordering (case-sensitive, like Java TreeSet)
|
|
210
|
+
sorted_keys = sorted(props.keys())
|
|
211
|
+
|
|
212
|
+
parts = []
|
|
213
|
+
for key in sorted_keys:
|
|
214
|
+
val = props[key]
|
|
215
|
+
if val is None:
|
|
216
|
+
val = ""
|
|
217
|
+
encoded_key = AuidGenerator.encode_component(str(key))
|
|
218
|
+
encoded_val = AuidGenerator.encode_component(str(val))
|
|
219
|
+
parts.append(f"{encoded_key}~{encoded_val}")
|
|
220
|
+
|
|
221
|
+
return "&".join(parts)
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def canonical_encoded_string_to_props(encoded: str) -> Dict[str, str]:
|
|
225
|
+
"""
|
|
226
|
+
Decode a canonical encoded string back to properties dictionary.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
encoded: Canonical encoded string (e.g., "key1~val1&key2~val2")
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Dictionary of decoded parameters
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
>>> encoded = 'base_url~http%3A%2F%2Fexample.com%2F&year~2023'
|
|
236
|
+
>>> AuidGenerator.canonical_encoded_string_to_props(encoded)
|
|
237
|
+
{'base_url': 'http://example.com/', 'year': '2023'}
|
|
238
|
+
"""
|
|
239
|
+
if not encoded:
|
|
240
|
+
return {}
|
|
241
|
+
|
|
242
|
+
props = {}
|
|
243
|
+
pairs = encoded.split("&")
|
|
244
|
+
|
|
245
|
+
for pair in pairs:
|
|
246
|
+
if "~" not in pair:
|
|
247
|
+
raise ValueError("Missing tilde in key-value pair")
|
|
248
|
+
key_encoded, val_encoded = pair.split("~", 1)
|
|
249
|
+
if "~" in val_encoded:
|
|
250
|
+
raise ValueError("Additional tilde in key-value pair")
|
|
251
|
+
key = AuidGenerator.decode_component(key_encoded)
|
|
252
|
+
val = AuidGenerator.decode_component(val_encoded)
|
|
253
|
+
props[key] = val
|
|
254
|
+
|
|
255
|
+
return props
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def generate_auid(plugin_id: str, au_def_props: Dict[str, str]) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Generate an AUID from plugin ID and definitional properties.
|
|
261
|
+
|
|
262
|
+
Port of PluginManager.generateAuId() from lockss-core.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
plugin_id: Plugin class name (e.g., "org.lockss.plugin.simulated.SimulatedPlugin")
|
|
266
|
+
au_def_props: Dictionary of AU definitional parameters
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
AUID string (e.g., "org|lockss|plugin|simulated|SimulatedPlugin&base_url~...")
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If plugin_id is empty
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> plugin_id = "org.lockss.plugin.simulated.SimulatedPlugin"
|
|
276
|
+
>>> params = {"base_url": "http://example.com/", "year": "2023"}
|
|
277
|
+
>>> AuidGenerator.generate_auid(plugin_id, params)
|
|
278
|
+
'org|lockss|plugin|simulated|SimulatedPlugin&base_url~http%3A%2F%2Fexample.com%2F&year~2023'
|
|
279
|
+
"""
|
|
280
|
+
if not plugin_id:
|
|
281
|
+
raise ValueError("plugin_id cannot be empty")
|
|
282
|
+
|
|
283
|
+
plugin_key = AuidGenerator.plugin_key_from_id(plugin_id)
|
|
284
|
+
au_key = AuidGenerator.props_to_canonical_encoded_string(au_def_props)
|
|
285
|
+
return f"{plugin_key}&{au_key}"
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def plugin_key_from_auid(auid: str) -> str:
|
|
289
|
+
"""
|
|
290
|
+
Extract plugin key from AUID.
|
|
291
|
+
|
|
292
|
+
Port of PluginManager.pluginKeyFromAuId() from lockss-core.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
auid: AUID string
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Plugin key portion
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
InvalidAuidError: If AUID format is invalid
|
|
302
|
+
|
|
303
|
+
Example:
|
|
304
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F"
|
|
305
|
+
>>> AuidGenerator.plugin_key_from_auid(auid)
|
|
306
|
+
'org|lockss|plugin|TestPlugin'
|
|
307
|
+
"""
|
|
308
|
+
if not auid:
|
|
309
|
+
raise InvalidAuidError("AUID cannot be empty")
|
|
310
|
+
|
|
311
|
+
idx = auid.find("&")
|
|
312
|
+
if idx < 0:
|
|
313
|
+
raise InvalidAuidError(f"AUID missing '&' separator: {auid}")
|
|
314
|
+
|
|
315
|
+
return auid[:idx]
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def au_key_from_auid(auid: str) -> str:
|
|
319
|
+
"""
|
|
320
|
+
Extract AU key from AUID.
|
|
321
|
+
|
|
322
|
+
Port of PluginManager.auKeyFromAuId() from lockss-core.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
auid: AUID string
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
AU key portion (may be empty string)
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
InvalidAuidError: If AUID format is invalid
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F"
|
|
335
|
+
>>> AuidGenerator.au_key_from_auid(auid)
|
|
336
|
+
'base_url~http%3A%2F%2Fexample.com%2F'
|
|
337
|
+
"""
|
|
338
|
+
if not auid:
|
|
339
|
+
raise InvalidAuidError("AUID cannot be empty")
|
|
340
|
+
|
|
341
|
+
idx = auid.find("&")
|
|
342
|
+
if idx < 0:
|
|
343
|
+
raise InvalidAuidError(f"AUID missing '&' separator: {auid}")
|
|
344
|
+
|
|
345
|
+
return auid[idx + 1:]
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def plugin_id_from_auid(auid: str) -> str:
|
|
349
|
+
"""
|
|
350
|
+
Extract plugin ID from AUID.
|
|
351
|
+
|
|
352
|
+
Port of PluginManager.pluginIdFromAuId() from lockss-core.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
auid: AUID string
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Plugin ID (with dots restored)
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
InvalidAuidError: If AUID format is invalid
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F"
|
|
365
|
+
>>> AuidGenerator.plugin_id_from_auid(auid)
|
|
366
|
+
'org.lockss.plugin.TestPlugin'
|
|
367
|
+
"""
|
|
368
|
+
plugin_key = AuidGenerator.plugin_key_from_auid(auid)
|
|
369
|
+
return AuidGenerator.plugin_id_from_key(plugin_key)
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def decode_auid(auid: str) -> Dict[str, str]:
|
|
373
|
+
"""
|
|
374
|
+
Decode an AUID into its definitional parameters.
|
|
375
|
+
|
|
376
|
+
This is a convenience method that extracts the AU key and decodes it.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
auid: AUID string
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Dictionary of decoded AU parameters
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
InvalidAuidError: If AUID format is invalid
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F&year~2023"
|
|
389
|
+
>>> AuidGenerator.decode_auid(auid)
|
|
390
|
+
{'base_url': 'http://example.com/', 'year': '2023'}
|
|
391
|
+
"""
|
|
392
|
+
au_key = AuidGenerator.au_key_from_auid(auid)
|
|
393
|
+
return AuidGenerator.canonical_encoded_string_to_props(au_key)
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def validate_auid(auid: str) -> bool:
|
|
397
|
+
"""
|
|
398
|
+
Check if an AUID string is valid.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
auid: AUID string to validate
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
True if valid, False otherwise
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
>>> AuidGenerator.validate_auid("org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F")
|
|
408
|
+
True
|
|
409
|
+
>>> AuidGenerator.validate_auid("invalid-auid")
|
|
410
|
+
False
|
|
411
|
+
"""
|
|
412
|
+
try:
|
|
413
|
+
AuidGenerator.plugin_key_from_auid(auid)
|
|
414
|
+
AuidGenerator.au_key_from_auid(auid)
|
|
415
|
+
return True
|
|
416
|
+
except (InvalidAuidError, ValueError):
|
|
417
|
+
return False
|
lockss/pybasic/cliutil.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-
|
|
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:
|
|
@@ -32,211 +32,205 @@
|
|
|
32
32
|
Command line utilities.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
from
|
|
36
|
-
import
|
|
37
|
-
from typing import Any, Dict, Generic, Optional, TypeVar
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Optional, Union
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
from
|
|
41
|
-
from
|
|
42
|
-
from
|
|
38
|
+
import click
|
|
39
|
+
from click.types import ParamType, IntRange
|
|
40
|
+
from click_extra import ChoiceSource, EnumChoice, ExtraContext, HelpExtraFormatter, Style, TableFormat, option
|
|
41
|
+
from click_extra.colorize import default_theme
|
|
43
42
|
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
def click_path(spec: Optional[str]) -> click.Path:
|
|
46
45
|
"""
|
|
47
|
-
|
|
46
|
+
Generates a ``click.Path`` based on a specification string.
|
|
47
|
+
|
|
48
|
+
The specification string can contain the following specifier characters:
|
|
49
|
+
|
|
50
|
+
.. list-table::
|
|
51
|
+
:header-rows: 1
|
|
52
|
+
|
|
53
|
+
* * Specifier
|
|
54
|
+
* Present
|
|
55
|
+
* Mutually exclusive with
|
|
56
|
+
* * ``f``
|
|
57
|
+
* Must be a file
|
|
58
|
+
* ``d`` [*]
|
|
59
|
+
* * ``d``
|
|
60
|
+
* Must be a directory
|
|
61
|
+
* ``f`` [*]
|
|
62
|
+
* * ``e``
|
|
63
|
+
* File or directory must exist
|
|
64
|
+
* ``E``
|
|
65
|
+
* * ``E``
|
|
66
|
+
* File or directory may or may not exist, but if it does not exist,
|
|
67
|
+
other checks are skipped (default)
|
|
68
|
+
* ``e``
|
|
69
|
+
* * ``r``
|
|
70
|
+
* File or directory must be readable
|
|
71
|
+
*
|
|
72
|
+
* * ``w``
|
|
73
|
+
* File or directory must be writable
|
|
74
|
+
*
|
|
75
|
+
* * ``x``
|
|
76
|
+
* File or directory must be executable
|
|
77
|
+
*
|
|
78
|
+
* * ``p``
|
|
79
|
+
* Resulting path will be ``pathlib.Path`` (default)
|
|
80
|
+
* ``s``
|
|
81
|
+
* * ``s``
|
|
82
|
+
* Resulting path will be ``str``
|
|
83
|
+
* ``p``
|
|
84
|
+
* * ``-``
|
|
85
|
+
* Path is allowed to be ``-``
|
|
86
|
+
*
|
|
87
|
+
* * ``z``
|
|
88
|
+
* Path will be absolute and resolved, with ``pathlib.Path.resolve``
|
|
89
|
+
*
|
|
90
|
+
|
|
91
|
+
When two mutual exclusive specifiers are present, ``ValueError`` is raised.
|
|
92
|
+
|
|
93
|
+
:param spec: A specification string.
|
|
94
|
+
:type spec: str
|
|
95
|
+
:return: A ``click.Path``.
|
|
96
|
+
:rtype: click.Path
|
|
97
|
+
:raises ValueError: If two mutually exclusive specifiers are present in the
|
|
98
|
+
specification string.
|
|
48
99
|
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
100
|
+
if spec is None:
|
|
101
|
+
spec = ''
|
|
102
|
+
allow_dash = False
|
|
103
|
+
dir_okay = True
|
|
104
|
+
executable = False
|
|
105
|
+
exists = False
|
|
106
|
+
file_okay = True
|
|
107
|
+
path_type = Path
|
|
108
|
+
readable = True
|
|
109
|
+
resolve_path = False
|
|
110
|
+
writable = False
|
|
111
|
+
for char in spec:
|
|
112
|
+
if char == 'd':
|
|
113
|
+
if 'f' in spec:
|
|
114
|
+
raise ValueError(f'"d" and "f" are mutually exclusive: {spec}')
|
|
115
|
+
dir_okay = True
|
|
116
|
+
file_okay = False
|
|
117
|
+
elif char == 'e':
|
|
118
|
+
if 'E' in spec:
|
|
119
|
+
raise ValueError(f'"e" and "E" are mutually exclusive: {spec}')
|
|
120
|
+
exists = True
|
|
121
|
+
elif char == 'E':
|
|
122
|
+
if 'e' in spec:
|
|
123
|
+
raise ValueError(f'"E" and "e" are mutually exclusive: {spec}')
|
|
124
|
+
exists = False
|
|
125
|
+
elif char == 'f':
|
|
126
|
+
if 'd' in spec:
|
|
127
|
+
raise ValueError(f'"f" and "d" are mutually exclusive: {spec}')
|
|
128
|
+
dir_okay = False
|
|
129
|
+
file_okay = True
|
|
130
|
+
elif char == 'p':
|
|
131
|
+
if 's' in spec:
|
|
132
|
+
raise ValueError(f'"p" and "s" are mutually exclusive: {spec}')
|
|
133
|
+
path_type = Path
|
|
134
|
+
elif char == 'r':
|
|
135
|
+
readable = True
|
|
136
|
+
elif char == 's':
|
|
137
|
+
if 'p' in spec:
|
|
138
|
+
raise ValueError(f'"s" and "p" are mutually exclusive: {spec}')
|
|
139
|
+
path_type = str
|
|
140
|
+
elif char == 'w':
|
|
141
|
+
writable = True
|
|
142
|
+
elif char == 'x':
|
|
143
|
+
executable = True
|
|
144
|
+
elif char == 'z':
|
|
145
|
+
resolve_path = True
|
|
146
|
+
elif char == '-':
|
|
147
|
+
allow_dash = True
|
|
148
|
+
else:
|
|
149
|
+
raise ValueError(f'unknown specification character "{char}": {spec}')
|
|
150
|
+
return click.Path(allow_dash=allow_dash,
|
|
151
|
+
dir_okay=dir_okay,
|
|
152
|
+
executable=executable,
|
|
153
|
+
exists=exists,
|
|
154
|
+
file_okay=file_okay,
|
|
155
|
+
path_type=path_type,
|
|
156
|
+
readable=readable,
|
|
157
|
+
resolve_path=resolve_path,
|
|
158
|
+
writable=writable)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
#: Composes the given decorators, so that
|
|
162
|
+
#: @compose_decorators(f, g, h)
|
|
163
|
+
#: def foo():
|
|
164
|
+
#: pass
|
|
165
|
+
#: is equivalent to:
|
|
166
|
+
#: @f
|
|
167
|
+
# @g
|
|
168
|
+
# @h
|
|
169
|
+
#: def foo():
|
|
170
|
+
#: pass
|
|
171
|
+
def compose_decorators(*decorators):
|
|
172
|
+
def wrapped(decorated):
|
|
173
|
+
for dec in reversed(decorators):
|
|
174
|
+
decorated = dec(decorated)
|
|
175
|
+
return decorated
|
|
176
|
+
return wrapped
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def make_table_format_option(switches: Union[str, tuple[str, ...]] = ('--table-format', '-T'),
|
|
180
|
+
default: TableFormat = TableFormat.SIMPLE):
|
|
65
181
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
BaseModelT = TypeVar('BaseModelT', bound=BaseModel)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class BaseCli(Generic[BaseModelT]):
|
|
182
|
+
Makes an equivalent of ``click_Extra.table_format_option`` with the given
|
|
183
|
+
command line switches and the given table format default.
|
|
184
|
+
|
|
185
|
+
The standard ``click_Extra.table_format_option`` attaches to the top-level
|
|
186
|
+
command only.
|
|
187
|
+
|
|
188
|
+
:param switches: A string or tuple of strings for the command line switches.
|
|
189
|
+
:type switches: Union[str, tuple[str, ...]]
|
|
190
|
+
:param default: A ``click_extra.TableFormat`` default.
|
|
191
|
+
:type default: TableFormat
|
|
192
|
+
:return: A remixed ``click_Extra.table_format_option``.
|
|
193
|
+
:rtype:
|
|
84
194
|
"""
|
|
85
|
-
|
|
195
|
+
if type(switches) == str:
|
|
196
|
+
switches = (switches,)
|
|
197
|
+
return option(*switches, type=EnumChoice(TableFormat, choice_source=ChoiceSource.VALUE), default=default, show_default=True, help='Set the rendering of tables to the given style.')
|
|
86
198
|
|
|
87
|
-
``BaseModelT`` represents a Pydantic-Argparse model type deriving from
|
|
88
|
-
``BaseModel``.
|
|
89
199
|
|
|
90
|
-
|
|
91
|
-
``x_y_z`` deriving from ``BaseModel`` , the ``dispatch()`` method expects a
|
|
92
|
-
method named ``_x_y_z``.
|
|
200
|
+
def make_extra_context_settings() -> dict[str, Any]:
|
|
93
201
|
"""
|
|
202
|
+
Makes a custom ``click_Extra.ExtraContext`` with essential changes.
|
|
94
203
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Constructs a new ``BaseCli`` instance.
|
|
98
|
-
|
|
99
|
-
:param kwargs: Keyword arguments. Must include ``model``, the
|
|
100
|
-
``BaseModel`` type corresponding to ``BaseModelT``. Must
|
|
101
|
-
include ``prog``, the command-line name of the program.
|
|
102
|
-
Must include ``description``, the description string of
|
|
103
|
-
the program.
|
|
104
|
-
:type kwargs: Dict[str, Any]
|
|
105
|
-
"""
|
|
106
|
-
super().__init__()
|
|
107
|
-
self._args: Optional[BaseModelT] = None
|
|
108
|
-
self._parser: Optional[ArgumentParser] = None
|
|
109
|
-
self.extra: Dict[str, Any] = dict(**kwargs)
|
|
110
|
-
|
|
111
|
-
def run(self) -> None:
|
|
112
|
-
"""
|
|
113
|
-
Runs the command line tool:
|
|
114
|
-
|
|
115
|
-
* Creates a Pydantic-Argparse ``ArgumentParser`` with ``model``,
|
|
116
|
-
``prog`` and ``description`` from the constructor keyword
|
|
117
|
-
arguments in ``self.parser``.
|
|
118
|
-
|
|
119
|
-
* Stores the Pydantic-Argparse parsed arguments in ``self.args``.
|
|
120
|
-
|
|
121
|
-
* Calls ``dispatch()``.
|
|
122
|
-
"""
|
|
123
|
-
self._parser: ArgumentParser = ArgumentParser(model=self.extra.get('model'),
|
|
124
|
-
prog=self.extra.get('prog'),
|
|
125
|
-
description=self.extra.get('description'))
|
|
126
|
-
self._initialize_rich_argparse()
|
|
127
|
-
self._args = self._parser.parse_typed_args()
|
|
128
|
-
self.dispatch()
|
|
129
|
-
|
|
130
|
-
def dispatch(self) -> None:
|
|
131
|
-
"""
|
|
132
|
-
Dispatches from the first field ``x_y_z`` in ``self.args`` that is a
|
|
133
|
-
command (i.e. whose value derives from ``BaseModel``) to a method
|
|
134
|
-
called ``_x_y_z``.
|
|
135
|
-
"""
|
|
136
|
-
field_names = self._args.__class__.__fields__.keys()
|
|
137
|
-
for field_name in field_names:
|
|
138
|
-
field_value = getattr(self._args, field_name)
|
|
139
|
-
if issubclass(type(field_value), BaseModel):
|
|
140
|
-
func = getattr(self, f'_{field_name}')
|
|
141
|
-
if callable(func):
|
|
142
|
-
func(field_value)
|
|
143
|
-
else:
|
|
144
|
-
self._parser.exit(1, f'internal error: no _{field_name} callable for the {field_name} command')
|
|
145
|
-
break
|
|
146
|
-
else:
|
|
147
|
-
self._parser.error(f'unknown command; expected one of {', '.join(field_names)}')
|
|
148
|
-
|
|
149
|
-
def _initialize_rich_argparse(self) -> None:
|
|
150
|
-
"""
|
|
151
|
-
Initializes `rich-argparse <https://pypi.org/project/rich-argparse/>`_
|
|
152
|
-
for this instance.
|
|
153
|
-
"""
|
|
154
|
-
self._initialize_rich_argparse_styles()
|
|
155
|
-
def __add_formatter_class(container):
|
|
156
|
-
container.formatter_class = RichHelpFormatter
|
|
157
|
-
if hasattr(container, '_actions'):
|
|
158
|
-
for action in container._actions:
|
|
159
|
-
if issubclass(type(action), SubParsersAction):
|
|
160
|
-
for subaction in action.choices.values():
|
|
161
|
-
__add_formatter_class(subaction)
|
|
162
|
-
__add_formatter_class(self._parser)
|
|
163
|
-
|
|
164
|
-
def _initialize_rich_argparse_styles(self) -> None:
|
|
165
|
-
# See https://github.com/hamdanal/rich-argparse#customize-the-colors
|
|
166
|
-
for cls in [RichHelpFormatter]:
|
|
167
|
-
cls.styles.update({
|
|
168
|
-
'argparse.args': 'bold cyan', # for positional-arguments and --options (e.g "--help")
|
|
169
|
-
'argparse.groups': 'underline dark_orange', # for group names (e.g. "positional arguments")
|
|
170
|
-
'argparse.help': 'default', # for argument's help text (e.g. "show this help message and exit")
|
|
171
|
-
'argparse.metavar': 'italic dark_cyan', # for metavariables (e.g. "FILE" in "--file FILE")
|
|
172
|
-
'argparse.prog': 'bold grey50', # for %(prog)s in the usage (e.g. "foo" in "Usage: foo [options]")
|
|
173
|
-
'argparse.syntax': 'bold', # for highlights of back-tick quoted text (e.g. "`some text`")
|
|
174
|
-
'argparse.text': 'default', # for descriptions, epilog, and --version (e.g. "A program to foo")
|
|
175
|
-
'argparse.default': 'italic', # for %(default)s in the help (e.g. "Value" in "(default: Value)")
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def at_most_one_from_enum(model_cls, values: Dict[str, Any], enum_cls) -> Dict[str, Any]:
|
|
180
|
-
"""
|
|
181
|
-
Among the fields of a Pydantic-Argparse model whose ``Field`` definition is
|
|
182
|
-
tagged with the ``enum`` keyword set to the given ``Enum`` type, ensures
|
|
183
|
-
that at most one of them has a true value in the given Pydantic-Argparse
|
|
184
|
-
validator ``values``, or raises a ``ValueError`` otherwise.
|
|
185
|
-
|
|
186
|
-
:param model_cls: A Pydantic-Argparse model class.
|
|
187
|
-
:param values: The Pydantic-Argparse validator ``values``.
|
|
188
|
-
:param enum_cls: The ``Enum`` class the fields of the Pydantic-Argparse
|
|
189
|
-
model are tagged with (using the ``enum`` keyword).
|
|
190
|
-
:return: The ``values`` argument, if no ``ValueError`` has been raised.
|
|
191
|
-
"""
|
|
192
|
-
enum_names = [field_name for field_name, model_field in model_cls.__fields__.items() if model_field.field_info.extra.get('enum') == enum_cls]
|
|
193
|
-
ret = [field_name for field_name in enum_names if values.get(field_name)]
|
|
194
|
-
if (length := len(ret)) > 1:
|
|
195
|
-
raise ValueError(f'at most one of {', '.join([option_name(enum_name) for enum_name in enum_names])} is allowed, got {length} ({', '.join([option_name(enum_name) for enum_name in ret])})')
|
|
196
|
-
return values
|
|
204
|
+
Currently, the only change is that the help formatter styles the invoked
|
|
205
|
+
command in bold.
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"""
|
|
201
|
-
Among the fields of a Pydantic-Argparse model whose ``Field`` definition is
|
|
202
|
-
tagged with the ``enum`` keyword set to the given ``Enum`` type, gets the
|
|
203
|
-
corresponding enum value of the first with a true value in the model, or
|
|
204
|
-
returns the given default value. Assumes the existence of a
|
|
205
|
-
``from_member()`` static method in the ``Enum`` class.
|
|
206
|
-
|
|
207
|
-
:param model_inst:
|
|
208
|
-
:param enum_cls:
|
|
209
|
-
:param default:
|
|
210
|
-
:return:
|
|
207
|
+
:return: A custom ``click_Extra.ExtraContext``.
|
|
208
|
+
:rtype: dict[str, Any]
|
|
211
209
|
"""
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
210
|
+
return ExtraContext.settings(
|
|
211
|
+
formatter_settings=HelpExtraFormatter.settings(
|
|
212
|
+
theme=default_theme.with_(
|
|
213
|
+
invoked_command=Style(bold=True)
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
217
|
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
raise ValueError(f'at most one of {', '.join([option_name(name) for name in names])} is allowed, got {length}')
|
|
222
|
-
return values
|
|
219
|
+
#: A ``click.ParamType`` for strictly positive integers (1 to infinity).
|
|
220
|
+
PositiveInt: ParamType = IntRange(min=1, max=None)
|
|
223
221
|
|
|
224
222
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
raise ValueError(f'exactly one of {', '.join([option_name(name) for name in names])} is required, got {length}')
|
|
228
|
-
return values
|
|
223
|
+
#: A ``click.ParamType`` for non-negative integers (0 to infinity).
|
|
224
|
+
NonNegativeInt: ParamType = IntRange(min=0, max=None)
|
|
229
225
|
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
raise ValueError(f'one or more of {', '.join([option_name(name) for name in names])} is required')
|
|
234
|
-
return values
|
|
227
|
+
#: A ``click.ParamType`` for strictly negative integers (negative infinity to -1).
|
|
228
|
+
NegativeInt: ParamType = IntRange(min=None, max=-1)
|
|
235
229
|
|
|
236
230
|
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
#: A ``click.ParamType`` for non-positive integers (negative infinity to 0).
|
|
232
|
+
NonPositiveInt: ParamType = IntRange(min=None, max=0)
|
|
239
233
|
|
|
240
234
|
|
|
241
|
-
|
|
242
|
-
|
|
235
|
+
#: A ``click.ParamType`` for unsigned 16-bit integers (0 to 65535).
|
|
236
|
+
UInt16: ParamType = IntRange(min=0, max=65535)
|
lockss/pybasic/errorutil.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-
|
|
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:
|
lockss/pybasic/fileutil.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-
|
|
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,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lockss-pybasic
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Basic Python utilities
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Thib Guicherd-Callin
|
|
8
|
+
Author-email: thib@cs.stanford.edu
|
|
9
|
+
Maintainer: Thib Guicherd-Callin
|
|
10
|
+
Maintainer-email: thib@cs.stanford.edu
|
|
11
|
+
Requires-Python: >=3.10,<4.0
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Framework :: Pydantic :: 2
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Dist: click-extra (>=7.5.0,<7.6.0)
|
|
20
|
+
Project-URL: Repository, https://github.com/lockss/lockss-pybasic
|
|
21
|
+
Description-Content-Type: text/x-rst
|
|
22
|
+
|
|
23
|
+
==============
|
|
24
|
+
lockss-pybasic
|
|
25
|
+
==============
|
|
26
|
+
|
|
27
|
+
.. |RELEASE| replace:: 0.2.0
|
|
28
|
+
.. |RELEASE_DATE| replace:: 2026-03-18
|
|
29
|
+
|
|
30
|
+
**Latest release:** |RELEASE| (|RELEASE_DATE|)
|
|
31
|
+
|
|
32
|
+
``lockss-pybasic`` provides basic utilities for various LOCKSS projects written in Python.
|
|
33
|
+
|
|
34
|
+
-------
|
|
35
|
+
Modules
|
|
36
|
+
-------
|
|
37
|
+
|
|
38
|
+
``lockss.pybasic.cliutil``
|
|
39
|
+
Command line utilities based on `Click Extra <https://kdeldycke.github.io/click-extra>`_, `Cloup <https://cloup.readthedocs.io/>`_ and `Click <https://click.palletsprojects.com/>`_.
|
|
40
|
+
|
|
41
|
+
* ``click_path()``: a ``click.Path`` utility.
|
|
42
|
+
|
|
43
|
+
* ``PositiveInt``, ``NonNegativeInt``, ``NegativeInt``, ``NonPositiveInt``, ``UInt16``: ``click.ParamType`` integer types.
|
|
44
|
+
|
|
45
|
+
* ``compose_decorators()``: a decorator utility.
|
|
46
|
+
|
|
47
|
+
* ``make_table_format_option()``: a remix of ``click_extra.table_format_option`` that is not attached to the top-level command.
|
|
48
|
+
|
|
49
|
+
* ``make_extra_context_settings()``: a custom ``click_extra.ExtraContext``.
|
|
50
|
+
|
|
51
|
+
``lockss.pybasic.errorutil``
|
|
52
|
+
Error and exception utilities.
|
|
53
|
+
|
|
54
|
+
* ``InternalError`` is a no-arg subclass of ``RuntimeError``.
|
|
55
|
+
|
|
56
|
+
``lockss.pybasic.fileutil``
|
|
57
|
+
File and path utilities.
|
|
58
|
+
|
|
59
|
+
* ``file_lines`` returns the non-empty lines of a file stripped of comments that begin with ``#`` and run to the end of a line.
|
|
60
|
+
|
|
61
|
+
* ``path`` takes a string or ``PurePath`` and returns a ``Path`` for which ``Path.expanduser()`` and ``Path.resolve()`` have been called.
|
|
62
|
+
|
|
63
|
+
-------------
|
|
64
|
+
Release Notes
|
|
65
|
+
-------------
|
|
66
|
+
|
|
67
|
+
See `<CHANGELOG.rst>`_.
|
|
68
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
lockss/pybasic/__init__.py,sha256=mfug98GqR17NB7OSpPd9mj_-YSAf_0Ij2Ek8fpOSMqc,1673
|
|
2
|
+
lockss/pybasic/auidutil.py,sha256=Q4vjjGfymiXVwPu35RyyLZBnViv8mDJKCjOyJb-sbS8,13921
|
|
3
|
+
lockss/pybasic/cliutil.py,sha256=F970MhLcQCYS3INLQT1Ij8nKt_ICmttiS3nSnuWuN7s,8106
|
|
4
|
+
lockss/pybasic/errorutil.py,sha256=4EaO0a1yIG1DbWltASeT15bg1bGg5kOYspsW0iJdVLc,1951
|
|
5
|
+
lockss/pybasic/fileutil.py,sha256=IIS2AFDgYtmBLPVHqQi1AkIFj6da04b6NQtjX9bIVqQ,2958
|
|
6
|
+
lockss_pybasic-0.2.0.dist-info/METADATA,sha256=UzEx0lIH3DPlaXzpAVJ5AjTZFNqByz_tdaol1vEp3dc,2229
|
|
7
|
+
lockss_pybasic-0.2.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
8
|
+
lockss_pybasic-0.2.0.dist-info/licenses/LICENSE,sha256=EOxPunNz3XP6AjgbPFolu-d9BS_AF9TtKn1WXgeYPsE,1506
|
|
9
|
+
lockss_pybasic-0.2.0.dist-info/RECORD,,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2000-
|
|
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:
|
lockss/pybasic/outpututil.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
Utilities to work with 'tabulate'.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
from enum import Enum
|
|
36
|
-
|
|
37
|
-
from pydantic.v1 import BaseModel, Field, validator
|
|
38
|
-
import tabulate
|
|
39
|
-
from typing import Optional
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
OutputFormat = Enum('OutputFormat', tabulate.tabulate_formats)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
DEFAULT_OUTPUT_FORMAT = 'simple' # from tabulate
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class OutputFormatOptions(BaseModel):
|
|
49
|
-
output_format: Optional[str] = Field(DEFAULT_OUTPUT_FORMAT, description=f'[output] set the output format; choices: {', '.join(OutputFormat.__members__.keys())}')
|
|
50
|
-
|
|
51
|
-
@validator('output_format')
|
|
52
|
-
def _validate_output_format(cls, val: str):
|
|
53
|
-
try:
|
|
54
|
-
_ = OutputFormat[val]
|
|
55
|
-
return val
|
|
56
|
-
except KeyError:
|
|
57
|
-
raise ValueError(f'must be one of {', '.join(OutputFormat.__members__.keys())}; got {val}')
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: lockss-pybasic
|
|
3
|
-
Version: 0.1.0.dev23
|
|
4
|
-
Summary: Basic Python utilities
|
|
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 :: 4 - Beta
|
|
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: pydantic (>=2.11.0,<3.0.0)
|
|
22
|
-
Requires-Dist: pydantic-argparse (>=0.10.0,<0.11.0)
|
|
23
|
-
Requires-Dist: rich-argparse (>=1.7.0,<1.8.0)
|
|
24
|
-
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
25
|
-
Project-URL: Repository, https://github.com/lockss/lockss-pybasic
|
|
26
|
-
Description-Content-Type: text/x-rst
|
|
27
|
-
|
|
28
|
-
==============
|
|
29
|
-
lockss-pybasic
|
|
30
|
-
==============
|
|
31
|
-
|
|
32
|
-
.. |RELEASE| replace:: 0.1.0-dev23
|
|
33
|
-
.. |RELEASE_DATE| replace:: ?
|
|
34
|
-
|
|
35
|
-
**Latest release:** |RELEASE| (|RELEASE_DATE|)
|
|
36
|
-
|
|
37
|
-
``lockss-pybasic`` provides basic utilities for various LOCKSS projects written in Python.
|
|
38
|
-
|
|
39
|
-
-------
|
|
40
|
-
Modules
|
|
41
|
-
-------
|
|
42
|
-
|
|
43
|
-
``lockss.pybasic.cliutil``
|
|
44
|
-
Command line utilities.
|
|
45
|
-
|
|
46
|
-
* ``BaseCli`` is a base class for command line interfaces that uses `pydantic-argparse <https://pypi.org/project/pydantic-argparse/>`_ to define arguments and subcommands, and `rich-argparse <https://pypi.org/project/rich-argparse/>`_ to display help messages. For each command ``x-y-z`` induced by a ``pydantic-argparse`` field named ``x_y_z`` deriving from ``BaseModel`` , the ``dispatch()`` method expects a method named ``_x_y_z``.
|
|
47
|
-
|
|
48
|
-
* ``StringCommand`` provides a `pydantic-argparse <https://pypi.org/project/pydantic-argparse/>`_ way to define a subcommand whose only purpose is printing a string, with ``COPYRIGHT_DESCRIPTION``, ``LICENSE_DESCRIPTION`` and ``VERSION_DESCRIPTION`` constants provided for convenience. Example:
|
|
49
|
-
|
|
50
|
-
.. code-block:: python
|
|
51
|
-
|
|
52
|
-
class MyCliModel(BaseModel):
|
|
53
|
-
copyright: Optional[StringCommand.type(my_copyright_string)] = Field(description=COPYRIGHT_DESCRIPTION)
|
|
54
|
-
|
|
55
|
-
* ``at_most_one_from_enum`` and ``get_from_enum`` provide a facility for defining a `pydantic-argparse <https://pypi.org/project/pydantic-argparse/>`_ model that defines one command line option per constant of an ``Enum``, using an ``enum`` keyword argument in the ``Field`` definition. Example:
|
|
56
|
-
|
|
57
|
-
.. code-block:: python
|
|
58
|
-
|
|
59
|
-
class Orientation(Enum):
|
|
60
|
-
horizontal = 1
|
|
61
|
-
vertical = 2
|
|
62
|
-
diagonal = 3
|
|
63
|
-
|
|
64
|
-
DEFAULT_ORIENTATION = Orientation.horizontal
|
|
65
|
-
|
|
66
|
-
class MyCliModel(BaseModel):
|
|
67
|
-
diagonal: Optional[bool] = Field(False, description='display diagonally', enum=Orientation)
|
|
68
|
-
horizontal: Optional[bool] = Field(False, description='display horizontally', enum=Orientation)
|
|
69
|
-
unrelated: Optional[bool] = Field(...)
|
|
70
|
-
vertical: Optional[bool] = Field(False, description='display vertically', enum=Orientation)
|
|
71
|
-
|
|
72
|
-
@root_validator
|
|
73
|
-
def _at_most_one_orientation(cls, values):
|
|
74
|
-
return at_most_one_from_enum(cls, values, Orientation)
|
|
75
|
-
|
|
76
|
-
def get_orientation(self) -> Orientation:
|
|
77
|
-
return get_from_enum(self, Orientation, DEFAULT_ORIENTATION)
|
|
78
|
-
|
|
79
|
-
``lockss.pybasic.errorutil``
|
|
80
|
-
Error and exception utilities.
|
|
81
|
-
|
|
82
|
-
* ``InternalError`` is a no-arg subclass of ``RuntimeError``.
|
|
83
|
-
|
|
84
|
-
``lockss.pybasic.fileutil``
|
|
85
|
-
File and path utilities.
|
|
86
|
-
|
|
87
|
-
* ``file_lines`` returns the non-empty lines of a file stripped of comments that begin with ``#`` and run to the end of a line.
|
|
88
|
-
|
|
89
|
-
* ``path`` takes a string or ``PurePath`` and returns a ``Path`` for which ``Path.expanduser()`` and ``Path.resolve()`` have been called.
|
|
90
|
-
|
|
91
|
-
``lockss.pybasic.outpututil``
|
|
92
|
-
Utilities to work with `tabulate <https://pypi.org/project/tabulate/>`_.
|
|
93
|
-
|
|
94
|
-
* ``OutputFormat`` is an ``Enum`` of all the output formats from ``tabulate.tabulate_formats``.
|
|
95
|
-
|
|
96
|
-
* ``OutputFormatOptions`` defines a `pydantic-argparse <https://pypi.org/project/pydantic-argparse/>`_ model for setting an output format from ``OutputFormat``.
|
|
97
|
-
|
|
98
|
-
-------------
|
|
99
|
-
Release Notes
|
|
100
|
-
-------------
|
|
101
|
-
|
|
102
|
-
See `<CHANGELOG.rst>`_.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
lockss/pybasic/__init__.py,sha256=jmOhJI5PiH3jLMWLGSCBYvv8hV2yG9PTPUfvYGtP1oU,1679
|
|
2
|
-
lockss/pybasic/cliutil.py,sha256=Q7WNFbtZzMBfEXjhqsUJlYOdsKYQm0Mgerf7aVe5VK8,10347
|
|
3
|
-
lockss/pybasic/errorutil.py,sha256=XI84PScZ851_-gfoazivJ8ceieMYWaxQr7qih5ltga0,1951
|
|
4
|
-
lockss/pybasic/fileutil.py,sha256=BpdoPWL70xYTuhyQRBEurScRVnPQg0mX-XW8yyKPGjw,2958
|
|
5
|
-
lockss/pybasic/outpututil.py,sha256=JyXKXlkaCcCGvonUvmDGRdI6PxwN5t7DPVIk64S8-2g,2345
|
|
6
|
-
lockss_pybasic-0.1.0.dev23.dist-info/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
|
|
7
|
-
lockss_pybasic-0.1.0.dev23.dist-info/METADATA,sha256=viCY_etc3kouRMBzEAcl44-zfDWo2Fkzk767I91jBKM,4372
|
|
8
|
-
lockss_pybasic-0.1.0.dev23.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
9
|
-
lockss_pybasic-0.1.0.dev23.dist-info/RECORD,,
|