lockss-pybasic 0.2.0.dev4__py3-none-any.whl → 0.2.0.dev6__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 +1 -1
- lockss/pybasic/auidutil.py +402 -0
- lockss/pybasic/cliutil.py +44 -52
- lockss/pybasic/outpututil.py +2 -2
- {lockss_pybasic-0.2.0.dev4.dist-info → lockss_pybasic-0.2.0.dev6.dist-info}/METADATA +5 -4
- lockss_pybasic-0.2.0.dev6.dist-info/RECORD +10 -0
- {lockss_pybasic-0.2.0.dev4.dist-info → lockss_pybasic-0.2.0.dev6.dist-info}/WHEEL +1 -1
- lockss_pybasic-0.2.0.dev4.dist-info/RECORD +0 -9
- {lockss_pybasic-0.2.0.dev4.dist-info → lockss_pybasic-0.2.0.dev6.dist-info/licenses}/LICENSE +0 -0
lockss/pybasic/__init__.py
CHANGED
|
@@ -0,0 +1,402 @@
|
|
|
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
|
+
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, Optional
|
|
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.com%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
|
+
@staticmethod
|
|
107
|
+
def encode_component(s: str) -> str:
|
|
108
|
+
"""
|
|
109
|
+
URL-encode a string component for use in AUID.
|
|
110
|
+
|
|
111
|
+
Port of PropKeyEncoder.encode() from lockss-core.
|
|
112
|
+
|
|
113
|
+
This method encodes strings using URL encoding with the following rules:
|
|
114
|
+
- Alphanumeric characters, hyphens, underscores, periods, and asterisks are not encoded
|
|
115
|
+
- Spaces are encoded as '+'
|
|
116
|
+
- All other characters are percent-encoded with uppercase hex digits
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
s: String to encode
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
URL-encoded string with spaces as '+' and uppercase hex digits
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> AuidGenerator.encode_component("http://example.com/")
|
|
126
|
+
'http%3A%2F%2Fexample.com%2F'
|
|
127
|
+
"""
|
|
128
|
+
if not s:
|
|
129
|
+
return ""
|
|
130
|
+
|
|
131
|
+
# Use quote_plus which converts spaces to '+'
|
|
132
|
+
# safe='-_.~' preserves unreserved characters per RFC 3986
|
|
133
|
+
# We use safe='' to match Java's PropKeyEncoder behavior more closely
|
|
134
|
+
encoded = urllib.parse.quote_plus(s, safe='')
|
|
135
|
+
|
|
136
|
+
# Convert to uppercase hex (urllib uses lowercase by default)
|
|
137
|
+
result = []
|
|
138
|
+
i = 0
|
|
139
|
+
while i < len(encoded):
|
|
140
|
+
if encoded[i] == '%' and i + 2 < len(encoded):
|
|
141
|
+
result.append('%')
|
|
142
|
+
result.append(encoded[i+1].upper())
|
|
143
|
+
result.append(encoded[i+2].upper())
|
|
144
|
+
i += 3
|
|
145
|
+
else:
|
|
146
|
+
result.append(encoded[i])
|
|
147
|
+
i += 1
|
|
148
|
+
|
|
149
|
+
return ''.join(result)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def decode_component(s: str) -> str:
|
|
153
|
+
"""
|
|
154
|
+
URL-decode a string component from an AUID.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
s: URL-encoded string
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Decoded string
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
>>> AuidGenerator.decode_component('http%3A%2F%2Fexample.com%2F')
|
|
164
|
+
'http://example.com/'
|
|
165
|
+
"""
|
|
166
|
+
if not s:
|
|
167
|
+
return ""
|
|
168
|
+
return urllib.parse.unquote_plus(s)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def props_to_canonical_encoded_string(props: Dict[str, str]) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Convert properties dictionary to canonical encoded string.
|
|
174
|
+
|
|
175
|
+
Port of PropUtil.propsToCanonicalEncodedString() from lockss-core.
|
|
176
|
+
|
|
177
|
+
The canonical form is created by:
|
|
178
|
+
1. Sorting keys alphabetically
|
|
179
|
+
2. Encoding each key and value
|
|
180
|
+
3. Joining with '~' between key and value, '&' between pairs
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
props: Dictionary of AU definitional parameters
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Canonical encoded string (e.g., "key1~val1&key2~val2")
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> props = {"year": "2023", "base_url": "http://example.com/"}
|
|
190
|
+
>>> AuidGenerator.props_to_canonical_encoded_string(props)
|
|
191
|
+
'base_url~http%3A%2F%2Fexample.com%2F&year~2023'
|
|
192
|
+
"""
|
|
193
|
+
if not props:
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
# Sort keys for canonical ordering (case-sensitive, like Java TreeSet)
|
|
197
|
+
sorted_keys = sorted(props.keys())
|
|
198
|
+
|
|
199
|
+
parts = []
|
|
200
|
+
for key in sorted_keys:
|
|
201
|
+
val = props[key]
|
|
202
|
+
if val is None:
|
|
203
|
+
val = ""
|
|
204
|
+
encoded_key = AuidGenerator.encode_component(str(key))
|
|
205
|
+
encoded_val = AuidGenerator.encode_component(str(val))
|
|
206
|
+
parts.append(f"{encoded_key}~{encoded_val}")
|
|
207
|
+
|
|
208
|
+
return "&".join(parts)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def canonical_encoded_string_to_props(encoded: str) -> Dict[str, str]:
|
|
212
|
+
"""
|
|
213
|
+
Decode a canonical encoded string back to properties dictionary.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
encoded: Canonical encoded string (e.g., "key1~val1&key2~val2")
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Dictionary of decoded parameters
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
>>> encoded = 'base_url~http%3A%2F%2Fexample.com%2F&year~2023'
|
|
223
|
+
>>> AuidGenerator.canonical_encoded_string_to_props(encoded)
|
|
224
|
+
{'base_url': 'http://example.com/', 'year': '2023'}
|
|
225
|
+
"""
|
|
226
|
+
if not encoded:
|
|
227
|
+
return {}
|
|
228
|
+
|
|
229
|
+
props = {}
|
|
230
|
+
pairs = encoded.split("&")
|
|
231
|
+
|
|
232
|
+
for pair in pairs:
|
|
233
|
+
if "~" not in pair:
|
|
234
|
+
continue
|
|
235
|
+
key_encoded, val_encoded = pair.split("~", 1)
|
|
236
|
+
key = AuidGenerator.decode_component(key_encoded)
|
|
237
|
+
val = AuidGenerator.decode_component(val_encoded)
|
|
238
|
+
props[key] = val
|
|
239
|
+
|
|
240
|
+
return props
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def generate_auid(plugin_id: str, au_def_props: Dict[str, str]) -> str:
|
|
244
|
+
"""
|
|
245
|
+
Generate an AUID from plugin ID and definitional properties.
|
|
246
|
+
|
|
247
|
+
Port of PluginManager.generateAuId() from lockss-core.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
plugin_id: Plugin class name (e.g., "org.lockss.plugin.simulated.SimulatedPlugin")
|
|
251
|
+
au_def_props: Dictionary of AU definitional parameters
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
AUID string (e.g., "org|lockss|plugin|simulated|SimulatedPlugin&base_url~...")
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError: If plugin_id is empty
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
>>> plugin_id = "org.lockss.plugin.simulated.SimulatedPlugin"
|
|
261
|
+
>>> params = {"base_url": "http://example.com/", "year": "2023"}
|
|
262
|
+
>>> AuidGenerator.generate_auid(plugin_id, params)
|
|
263
|
+
'org|lockss|plugin|simulated|SimulatedPlugin&base_url~http%3A%2F%2Fexample.com%2F&year~2023'
|
|
264
|
+
"""
|
|
265
|
+
if not plugin_id:
|
|
266
|
+
raise ValueError("plugin_id cannot be empty")
|
|
267
|
+
|
|
268
|
+
plugin_key = AuidGenerator.plugin_key_from_id(plugin_id)
|
|
269
|
+
au_key = AuidGenerator.props_to_canonical_encoded_string(au_def_props)
|
|
270
|
+
return f"{plugin_key}&{au_key}"
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def plugin_key_from_auid(auid: str) -> str:
|
|
274
|
+
"""
|
|
275
|
+
Extract plugin key from AUID.
|
|
276
|
+
|
|
277
|
+
Port of PluginManager.pluginKeyFromAuId() from lockss-core.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
auid: AUID string
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Plugin key portion
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
InvalidAuidError: If AUID format is invalid
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F"
|
|
290
|
+
>>> AuidGenerator.plugin_key_from_auid(auid)
|
|
291
|
+
'org|lockss|plugin|TestPlugin'
|
|
292
|
+
"""
|
|
293
|
+
if not auid:
|
|
294
|
+
raise InvalidAuidError("AUID cannot be empty")
|
|
295
|
+
|
|
296
|
+
idx = auid.find("&")
|
|
297
|
+
if idx < 0:
|
|
298
|
+
raise InvalidAuidError(f"AUID missing '&' separator: {auid}")
|
|
299
|
+
|
|
300
|
+
return auid[:idx]
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def au_key_from_auid(auid: str) -> str:
|
|
304
|
+
"""
|
|
305
|
+
Extract AU key from AUID.
|
|
306
|
+
|
|
307
|
+
Port of PluginManager.auKeyFromAuId() from lockss-core.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
auid: AUID string
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
AU key portion (may be empty string)
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
InvalidAuidError: If AUID format is invalid
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F"
|
|
320
|
+
>>> AuidGenerator.au_key_from_auid(auid)
|
|
321
|
+
'base_url~http%3A%2F%2Fexample.com%2F'
|
|
322
|
+
"""
|
|
323
|
+
if not auid:
|
|
324
|
+
raise InvalidAuidError("AUID cannot be empty")
|
|
325
|
+
|
|
326
|
+
idx = auid.find("&")
|
|
327
|
+
if idx < 0:
|
|
328
|
+
raise InvalidAuidError(f"AUID missing '&' separator: {auid}")
|
|
329
|
+
|
|
330
|
+
return auid[idx + 1:]
|
|
331
|
+
|
|
332
|
+
@staticmethod
|
|
333
|
+
def plugin_id_from_auid(auid: str) -> str:
|
|
334
|
+
"""
|
|
335
|
+
Extract plugin ID from AUID.
|
|
336
|
+
|
|
337
|
+
Port of PluginManager.pluginIdFromAuId() from lockss-core.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
auid: AUID string
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Plugin ID (with dots restored)
|
|
344
|
+
|
|
345
|
+
Raises:
|
|
346
|
+
InvalidAuidError: If AUID format is invalid
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F"
|
|
350
|
+
>>> AuidGenerator.plugin_id_from_auid(auid)
|
|
351
|
+
'org.lockss.plugin.TestPlugin'
|
|
352
|
+
"""
|
|
353
|
+
plugin_key = AuidGenerator.plugin_key_from_auid(auid)
|
|
354
|
+
return AuidGenerator.plugin_id_from_key(plugin_key)
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def decode_auid(auid: str) -> Dict[str, str]:
|
|
358
|
+
"""
|
|
359
|
+
Decode an AUID into its definitional parameters.
|
|
360
|
+
|
|
361
|
+
This is a convenience method that extracts the AU key and decodes it.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
auid: AUID string
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dictionary of decoded AU parameters
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
InvalidAuidError: If AUID format is invalid
|
|
371
|
+
|
|
372
|
+
Example:
|
|
373
|
+
>>> auid = "org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F&year~2023"
|
|
374
|
+
>>> AuidGenerator.decode_auid(auid)
|
|
375
|
+
{'base_url': 'http://example.com/', 'year': '2023'}
|
|
376
|
+
"""
|
|
377
|
+
au_key = AuidGenerator.au_key_from_auid(auid)
|
|
378
|
+
return AuidGenerator.canonical_encoded_string_to_props(au_key)
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def validate_auid(auid: str) -> bool:
|
|
382
|
+
"""
|
|
383
|
+
Check if an AUID string is valid.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
auid: AUID string to validate
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
True if valid, False otherwise
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
>>> AuidGenerator.validate_auid("org|lockss|plugin|TestPlugin&base_url~http%3A%2F%2Fexample.com%2F")
|
|
393
|
+
True
|
|
394
|
+
>>> AuidGenerator.validate_auid("invalid-auid")
|
|
395
|
+
False
|
|
396
|
+
"""
|
|
397
|
+
try:
|
|
398
|
+
AuidGenerator.plugin_key_from_auid(auid)
|
|
399
|
+
AuidGenerator.au_key_from_auid(auid)
|
|
400
|
+
return True
|
|
401
|
+
except (InvalidAuidError, ValueError):
|
|
402
|
+
return False
|
lockss/pybasic/cliutil.py
CHANGED
|
@@ -34,52 +34,47 @@ Command line utilities.
|
|
|
34
34
|
|
|
35
35
|
from collections.abc import Callable
|
|
36
36
|
import sys
|
|
37
|
-
from typing import Any,
|
|
37
|
+
from typing import Any, Dict, Generic, Optional, TypeVar
|
|
38
38
|
|
|
39
|
-
from pydantic.v1 import BaseModel
|
|
40
|
-
from pydantic.v1.fields import FieldInfo
|
|
39
|
+
from pydantic.v1 import BaseModel
|
|
41
40
|
from pydantic_argparse import ArgumentParser
|
|
42
41
|
from pydantic_argparse.argparse.actions import SubParsersAction
|
|
43
42
|
from rich_argparse import RichHelpFormatter
|
|
44
43
|
|
|
45
|
-
if TYPE_CHECKING:
|
|
46
|
-
from _typeshed import SupportsWrite as SupportsWriteStr
|
|
47
|
-
else:
|
|
48
|
-
SupportsWriteStr = Any
|
|
49
44
|
|
|
45
|
+
class ActionCommand(Callable, BaseModel):
|
|
46
|
+
"""
|
|
47
|
+
Base class for a pydantic-argparse style command.
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
50
50
|
|
|
51
|
-
class StringCommand(BaseModel):
|
|
52
|
-
|
|
53
|
-
def display(self, file: SupportsWriteStr=sys.stdout) -> None:
|
|
54
|
-
print(getattr(self, 'display_string'), file=file)
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
__base__=StringCommand,
|
|
60
|
-
**{option_name: (Optional[bool], FieldInfo(False, description=description)),
|
|
61
|
-
display_string: PrivateAttr(display_string)})
|
|
52
|
+
class StringCommand(ActionCommand):
|
|
53
|
+
"""
|
|
54
|
+
A pydantic-argparse style command that prints a string.
|
|
62
55
|
|
|
56
|
+
Example of use:
|
|
63
57
|
|
|
64
|
-
|
|
58
|
+
.. code-block:: python
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return StringCommand.make('CopyrightCommand', 'copyright', 'print the copyright and exit', display_string)
|
|
60
|
+
class MyCliModel(BaseModel):
|
|
61
|
+
copyright: Optional[StringCommand.type(my_copyright_string)] = Field(description=COPYRIGHT_DESCRIPTION)
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
See also the convenience constants ``COPYRIGHT_DESCRIPTION``,
|
|
64
|
+
``LICENSE_DESCRIPTION``, and ``VERSION_DESCRIPTION``.
|
|
65
|
+
"""
|
|
72
66
|
|
|
73
67
|
@staticmethod
|
|
74
|
-
def
|
|
75
|
-
|
|
68
|
+
def type(display_str: str):
|
|
69
|
+
class _StringCommand(StringCommand):
|
|
70
|
+
def __call__(self, file=sys.stdout, **kwargs):
|
|
71
|
+
print(display_str, file=file)
|
|
72
|
+
return _StringCommand
|
|
76
73
|
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def make(display_string: str):
|
|
82
|
-
return StringCommand.make('VersionCommand', 'version', 'print the version number and exit', display_string)
|
|
75
|
+
COPYRIGHT_DESCRIPTION = 'print the copyright and exit'
|
|
76
|
+
LICENSE_DESCRIPTION = 'print the software license and exit'
|
|
77
|
+
VERSION_DESCRIPTION = 'print the version number and exit'
|
|
83
78
|
|
|
84
79
|
|
|
85
80
|
BaseModelT = TypeVar('BaseModelT', bound=BaseModel)
|
|
@@ -134,25 +129,22 @@ class BaseCli(Generic[BaseModelT]):
|
|
|
134
129
|
|
|
135
130
|
def dispatch(self) -> None:
|
|
136
131
|
"""
|
|
137
|
-
Dispatches from the first field ``x_y_z`` in ``self.
|
|
132
|
+
Dispatches from the first field ``x_y_z`` in ``self.args`` that is a
|
|
138
133
|
command (i.e. whose value derives from ``BaseModel``) to a method
|
|
139
134
|
called ``_x_y_z``.
|
|
140
135
|
"""
|
|
141
|
-
self.
|
|
142
|
-
|
|
143
|
-
def _dispatch_recursive(self, base_model: BaseModel, subcommands: list[str]) -> None:
|
|
144
|
-
field_names = base_model.__class__.__fields__.keys()
|
|
136
|
+
field_names = self._args.__class__.__fields__.keys()
|
|
145
137
|
for field_name in field_names:
|
|
146
|
-
field_value = getattr(
|
|
138
|
+
field_value = getattr(self._args, field_name)
|
|
147
139
|
if issubclass(type(field_value), BaseModel):
|
|
148
|
-
self
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
154
146
|
else:
|
|
155
|
-
self._parser.
|
|
147
|
+
self._parser.error(f'unknown command; expected one of {", ".join(field_names)}')
|
|
156
148
|
|
|
157
149
|
def _initialize_rich_argparse(self) -> None:
|
|
158
150
|
"""
|
|
@@ -184,7 +176,7 @@ class BaseCli(Generic[BaseModelT]):
|
|
|
184
176
|
})
|
|
185
177
|
|
|
186
178
|
|
|
187
|
-
def at_most_one_from_enum(model_cls
|
|
179
|
+
def at_most_one_from_enum(model_cls, values: Dict[str, Any], enum_cls) -> Dict[str, Any]:
|
|
188
180
|
"""
|
|
189
181
|
Among the fields of a Pydantic-Argparse model whose ``Field`` definition is
|
|
190
182
|
tagged with the ``enum`` keyword set to the given ``Enum`` type, ensures
|
|
@@ -200,7 +192,7 @@ def at_most_one_from_enum(model_cls: type[BaseModel], values: Dict[str, Any], en
|
|
|
200
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]
|
|
201
193
|
ret = [field_name for field_name in enum_names if values.get(field_name)]
|
|
202
194
|
if (length := len(ret)) > 1:
|
|
203
|
-
raise ValueError(f'at most one of {
|
|
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])})')
|
|
204
196
|
return values
|
|
205
197
|
|
|
206
198
|
|
|
@@ -224,21 +216,21 @@ def get_from_enum(model_inst, enum_cls, default=None):
|
|
|
224
216
|
return default
|
|
225
217
|
|
|
226
218
|
|
|
227
|
-
def at_most_one(
|
|
219
|
+
def at_most_one(values: Dict[str, Any], *names: str):
|
|
228
220
|
if (length := _matchy_length(values, *names)) > 1:
|
|
229
|
-
raise ValueError(f'at most one of {
|
|
221
|
+
raise ValueError(f'at most one of {", ".join([option_name(name) for name in names])} is allowed, got {length}')
|
|
230
222
|
return values
|
|
231
223
|
|
|
232
224
|
|
|
233
|
-
def exactly_one(
|
|
225
|
+
def exactly_one(values: Dict[str, Any], *names: str):
|
|
234
226
|
if (length := _matchy_length(values, *names)) != 1:
|
|
235
|
-
raise ValueError(f'exactly one of {
|
|
227
|
+
raise ValueError(f'exactly one of {", ".join([option_name(name) for name in names])} is required, got {length}')
|
|
236
228
|
return values
|
|
237
229
|
|
|
238
230
|
|
|
239
|
-
def one_or_more(
|
|
231
|
+
def one_or_more(values: Dict[str, Any], *names: str):
|
|
240
232
|
if _matchy_length(values, *names) == 0:
|
|
241
|
-
raise ValueError(f'one or more of {
|
|
233
|
+
raise ValueError(f'one or more of {", ".join([option_name(name) for name in names])} is required')
|
|
242
234
|
return values
|
|
243
235
|
|
|
244
236
|
|
|
@@ -247,7 +239,7 @@ def option_name(model_cls: type[BaseModel], name: str) -> str:
|
|
|
247
239
|
raise RuntimeError(f'invalid name: {name}')
|
|
248
240
|
if alias := info.alias:
|
|
249
241
|
name = alias
|
|
250
|
-
return f'{(
|
|
242
|
+
return f'{("-" if len(name) == 1 else "--")}{name.replace("_", "-")}'
|
|
251
243
|
|
|
252
244
|
|
|
253
245
|
def _matchy_length(values: Dict[str, Any], *names: str) -> int:
|
lockss/pybasic/outpututil.py
CHANGED
|
@@ -46,7 +46,7 @@ DEFAULT_OUTPUT_FORMAT = 'simple' # from tabulate
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class OutputFormatOptions(BaseModel):
|
|
49
|
-
output_format: Optional[str] = Field(DEFAULT_OUTPUT_FORMAT, description=f'[output] set the output format; choices: {
|
|
49
|
+
output_format: Optional[str] = Field(DEFAULT_OUTPUT_FORMAT, description=f'[output] set the output format; choices: {", ".join(OutputFormat.__members__.keys())}')
|
|
50
50
|
|
|
51
51
|
@validator('output_format')
|
|
52
52
|
def _validate_output_format(cls, val: str):
|
|
@@ -54,4 +54,4 @@ class OutputFormatOptions(BaseModel):
|
|
|
54
54
|
_ = OutputFormat[val]
|
|
55
55
|
return val
|
|
56
56
|
except KeyError:
|
|
57
|
-
raise ValueError(f'must be one of {
|
|
57
|
+
raise ValueError(f'must be one of {", ".join(OutputFormat.__members__.keys())}; got {val}')
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lockss-pybasic
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev6
|
|
4
4
|
Summary: Basic Python utilities
|
|
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
|
|
@@ -26,8 +27,8 @@ Description-Content-Type: text/x-rst
|
|
|
26
27
|
lockss-pybasic
|
|
27
28
|
==============
|
|
28
29
|
|
|
29
|
-
.. |RELEASE| replace:: 0.1.
|
|
30
|
-
.. |RELEASE_DATE| replace:: 2025-
|
|
30
|
+
.. |RELEASE| replace:: 0.1.1
|
|
31
|
+
.. |RELEASE_DATE| replace:: 2025-10-02
|
|
31
32
|
|
|
32
33
|
**Latest release:** |RELEASE| (|RELEASE_DATE|)
|
|
33
34
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
lockss/pybasic/__init__.py,sha256=36qFj2gOmi2s3h_V6wwYbwaqcj9bSd41ZEes0hGtRFE,1678
|
|
2
|
+
lockss/pybasic/auidutil.py,sha256=Udqompf_F4li3pfzQPgzjfp_A2s_litsZ7uucBQDHEg,13283
|
|
3
|
+
lockss/pybasic/cliutil.py,sha256=tW6ojnvu2B_GNTmWS6ElV2xHJ3E2YGRDnQ4zQ9dU6uY,10533
|
|
4
|
+
lockss/pybasic/errorutil.py,sha256=XI84PScZ851_-gfoazivJ8ceieMYWaxQr7qih5ltga0,1951
|
|
5
|
+
lockss/pybasic/fileutil.py,sha256=BpdoPWL70xYTuhyQRBEurScRVnPQg0mX-XW8yyKPGjw,2958
|
|
6
|
+
lockss/pybasic/outpututil.py,sha256=8naQEZ1rM6vOFNL-9mWoK4dMBWokHmzQ0FkHaz8dyuM,2345
|
|
7
|
+
lockss_pybasic-0.2.0.dev6.dist-info/METADATA,sha256=1R9WSiC304TcXF7EtMxlVqMLFQAU_bvZ9SH1NjMhSmg,4269
|
|
8
|
+
lockss_pybasic-0.2.0.dev6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
9
|
+
lockss_pybasic-0.2.0.dev6.dist-info/licenses/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
|
|
10
|
+
lockss_pybasic-0.2.0.dev6.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
lockss/pybasic/__init__.py,sha256=9aaQqxV5mxDgrIUSS4yRxzuDC_DZ5l7eUmSMkILhkUs,1678
|
|
2
|
-
lockss/pybasic/cliutil.py,sha256=_jHcYa2ccW94fXb4zdxGFyV3BXgcqdlEOYhIbun-DJ4,11236
|
|
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.2.0.dev4.dist-info/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
|
|
7
|
-
lockss_pybasic-0.2.0.dev4.dist-info/METADATA,sha256=8dba_8a1HH-v4-ObNFtcYPLb1X37hEkpQ3aMbl0OTgI,4247
|
|
8
|
-
lockss_pybasic-0.2.0.dev4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
9
|
-
lockss_pybasic-0.2.0.dev4.dist-info/RECORD,,
|
{lockss_pybasic-0.2.0.dev4.dist-info → lockss_pybasic-0.2.0.dev6.dist-info/licenses}/LICENSE
RENAMED
|
File without changes
|