lockss-pybasic 0.2.0.dev6__py3-none-any.whl → 0.2.0.dev8__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 +37 -22
- lockss/pybasic/cliutil.py +82 -16
- {lockss_pybasic-0.2.0.dev6.dist-info → lockss_pybasic-0.2.0.dev8.dist-info}/METADATA +6 -5
- lockss_pybasic-0.2.0.dev8.dist-info/RECORD +10 -0
- lockss_pybasic-0.2.0.dev6.dist-info/RECORD +0 -10
- {lockss_pybasic-0.2.0.dev6.dist-info → lockss_pybasic-0.2.0.dev8.dist-info}/WHEEL +0 -0
- {lockss_pybasic-0.2.0.dev6.dist-info → lockss_pybasic-0.2.0.dev8.dist-info}/licenses/LICENSE +0 -0
lockss/pybasic/__init__.py
CHANGED
lockss/pybasic/auidutil.py
CHANGED
|
@@ -36,7 +36,7 @@ and related utility classes in the LOCKSS lockss-core library.
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
import urllib.parse
|
|
39
|
-
from typing import Dict
|
|
39
|
+
from typing import Dict
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class InvalidAuidError(ValueError):
|
|
@@ -60,7 +60,7 @@ class AuidGenerator:
|
|
|
60
60
|
>>> params = {"base_url": "http://example.com/", "year": "2023"}
|
|
61
61
|
>>> auid = AuidGenerator.generate_auid(plugin_id, params)
|
|
62
62
|
>>> print(auid)
|
|
63
|
-
org|lockss|plugin|simulated|SimulatedPlugin&base_url~http%3A%2F%2Fexample
|
|
63
|
+
org|lockss|plugin|simulated|SimulatedPlugin&base_url~http%3A%2F%2Fexample%2Ecom%2F&year~2023
|
|
64
64
|
"""
|
|
65
65
|
|
|
66
66
|
@staticmethod
|
|
@@ -103,6 +103,18 @@ class AuidGenerator:
|
|
|
103
103
|
raise ValueError("plugin_key cannot be empty")
|
|
104
104
|
return plugin_key.replace("|", ".")
|
|
105
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
|
+
|
|
106
118
|
@staticmethod
|
|
107
119
|
def encode_component(s: str) -> str:
|
|
108
120
|
"""
|
|
@@ -111,9 +123,13 @@ class AuidGenerator:
|
|
|
111
123
|
Port of PropKeyEncoder.encode() from lockss-core.
|
|
112
124
|
|
|
113
125
|
This method encodes strings using URL encoding with the following rules:
|
|
114
|
-
- Alphanumeric characters
|
|
126
|
+
- Alphanumeric characters (a-z, A-Z, 0-9) are not encoded
|
|
127
|
+
- Hyphens (-), underscores (_), and asterisks (*) are not encoded
|
|
115
128
|
- Spaces are encoded as '+'
|
|
116
|
-
- All other characters are percent-encoded with uppercase hex digits
|
|
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.
|
|
117
133
|
|
|
118
134
|
Args:
|
|
119
135
|
s: String to encode
|
|
@@ -123,28 +139,25 @@ class AuidGenerator:
|
|
|
123
139
|
|
|
124
140
|
Example:
|
|
125
141
|
>>> AuidGenerator.encode_component("http://example.com/")
|
|
126
|
-
'http%3A%2F%2Fexample
|
|
142
|
+
'http%3A%2F%2Fexample%2Ecom%2F'
|
|
127
143
|
"""
|
|
128
144
|
if not s:
|
|
129
145
|
return ""
|
|
130
146
|
|
|
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
147
|
result = []
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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)
|
|
145
155
|
else:
|
|
146
|
-
|
|
147
|
-
|
|
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'))
|
|
148
161
|
|
|
149
162
|
return ''.join(result)
|
|
150
163
|
|
|
@@ -165,7 +178,7 @@ class AuidGenerator:
|
|
|
165
178
|
"""
|
|
166
179
|
if not s:
|
|
167
180
|
return ""
|
|
168
|
-
return urllib.parse.unquote_plus(s)
|
|
181
|
+
return urllib.parse.unquote_plus(s, errors="strict")
|
|
169
182
|
|
|
170
183
|
@staticmethod
|
|
171
184
|
def props_to_canonical_encoded_string(props: Dict[str, str]) -> str:
|
|
@@ -231,8 +244,10 @@ class AuidGenerator:
|
|
|
231
244
|
|
|
232
245
|
for pair in pairs:
|
|
233
246
|
if "~" not in pair:
|
|
234
|
-
|
|
247
|
+
raise ValueError("Missing tilde in key-value pair")
|
|
235
248
|
key_encoded, val_encoded = pair.split("~", 1)
|
|
249
|
+
if "~" in val_encoded:
|
|
250
|
+
raise ValueError("Additional tilde in key-value pair")
|
|
236
251
|
key = AuidGenerator.decode_component(key_encoded)
|
|
237
252
|
val = AuidGenerator.decode_component(val_encoded)
|
|
238
253
|
props[key] = val
|
lockss/pybasic/cliutil.py
CHANGED
|
@@ -31,18 +31,21 @@
|
|
|
31
31
|
"""
|
|
32
32
|
Command line utilities.
|
|
33
33
|
"""
|
|
34
|
-
|
|
34
|
+
import pathlib
|
|
35
35
|
from collections.abc import Callable
|
|
36
36
|
import sys
|
|
37
37
|
from typing import Any, Dict, Generic, Optional, TypeVar
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
import click
|
|
40
|
+
from pydantic.v1 import BaseModel as BaseModel1
|
|
40
41
|
from pydantic_argparse import ArgumentParser
|
|
41
42
|
from pydantic_argparse.argparse.actions import SubParsersAction
|
|
42
43
|
from rich_argparse import RichHelpFormatter
|
|
43
44
|
|
|
45
|
+
from .fileutil import path
|
|
46
|
+
|
|
44
47
|
|
|
45
|
-
class ActionCommand(Callable,
|
|
48
|
+
class ActionCommand(Callable, BaseModel1):
|
|
46
49
|
"""
|
|
47
50
|
Base class for a pydantic-argparse style command.
|
|
48
51
|
"""
|
|
@@ -77,7 +80,7 @@ LICENSE_DESCRIPTION = 'print the software license and exit'
|
|
|
77
80
|
VERSION_DESCRIPTION = 'print the version number and exit'
|
|
78
81
|
|
|
79
82
|
|
|
80
|
-
BaseModelT = TypeVar('BaseModelT', bound=
|
|
83
|
+
BaseModelT = TypeVar('BaseModelT', bound=BaseModel1)
|
|
81
84
|
|
|
82
85
|
|
|
83
86
|
class BaseCli(Generic[BaseModelT]):
|
|
@@ -129,22 +132,25 @@ class BaseCli(Generic[BaseModelT]):
|
|
|
129
132
|
|
|
130
133
|
def dispatch(self) -> None:
|
|
131
134
|
"""
|
|
132
|
-
Dispatches from the first field ``x_y_z`` in ``self.
|
|
135
|
+
Dispatches from the first field ``x_y_z`` in ``self._args`` that is a
|
|
133
136
|
command (i.e. whose value derives from ``BaseModel``) to a method
|
|
134
137
|
called ``_x_y_z``.
|
|
135
138
|
"""
|
|
136
|
-
|
|
139
|
+
self._dispatch_recursive(self._args, [])
|
|
140
|
+
|
|
141
|
+
def _dispatch_recursive(self, base_model: BaseModel1, subcommands: list[str]) -> None:
|
|
142
|
+
field_names = base_model.__class__.__fields__.keys()
|
|
137
143
|
for field_name in field_names:
|
|
138
|
-
field_value = getattr(
|
|
139
|
-
if issubclass(type(field_value),
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
field_value = getattr(base_model, field_name)
|
|
145
|
+
if issubclass(type(field_value), BaseModel1):
|
|
146
|
+
self._dispatch_recursive(field_value, [*subcommands, field_name])
|
|
147
|
+
return
|
|
148
|
+
func_name = ''.join(f'_{sub}' for sub in subcommands)
|
|
149
|
+
func = getattr(self, func_name)
|
|
150
|
+
if callable(func):
|
|
151
|
+
func(base_model) # FIXME?
|
|
146
152
|
else:
|
|
147
|
-
self._parser.
|
|
153
|
+
self._parser.exit(1, f'internal error: no {func_name} callable for the {" ".join(sub for sub in subcommands)} command')
|
|
148
154
|
|
|
149
155
|
def _initialize_rich_argparse(self) -> None:
|
|
150
156
|
"""
|
|
@@ -234,7 +240,7 @@ def one_or_more(values: Dict[str, Any], *names: str):
|
|
|
234
240
|
return values
|
|
235
241
|
|
|
236
242
|
|
|
237
|
-
def option_name(model_cls: type[
|
|
243
|
+
def option_name(model_cls: type[BaseModel1], name: str) -> str:
|
|
238
244
|
if (info := model_cls.__fields__.get(name)) is None:
|
|
239
245
|
raise RuntimeError(f'invalid name: {name}')
|
|
240
246
|
if alias := info.alias:
|
|
@@ -244,3 +250,63 @@ def option_name(model_cls: type[BaseModel], name: str) -> str:
|
|
|
244
250
|
|
|
245
251
|
def _matchy_length(values: Dict[str, Any], *names: str) -> int:
|
|
246
252
|
return len([name for name in names if values.get(name)])
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def click_path(spec: Optional[str]) -> click.Path:
|
|
256
|
+
if spec is None:
|
|
257
|
+
spec = ''
|
|
258
|
+
allow_dash = False
|
|
259
|
+
dir_okay = True
|
|
260
|
+
executable = False
|
|
261
|
+
exists = False
|
|
262
|
+
file_okay = True
|
|
263
|
+
path_type = path
|
|
264
|
+
readable = True
|
|
265
|
+
resolve_path = False
|
|
266
|
+
writable = False
|
|
267
|
+
for char in spec:
|
|
268
|
+
if char == 'd':
|
|
269
|
+
if 'f' in spec:
|
|
270
|
+
raise ValueError(f'"d" and "f" are mutually exclusive: {spec}')
|
|
271
|
+
dir_okay = True
|
|
272
|
+
file_okay = False
|
|
273
|
+
elif char == 'e':
|
|
274
|
+
exists = True
|
|
275
|
+
elif char == 'f':
|
|
276
|
+
if 'd' in spec:
|
|
277
|
+
raise ValueError(f'"f" and "d" are mutually exclusive: {spec}')
|
|
278
|
+
dir_okay = False
|
|
279
|
+
file_okay = True
|
|
280
|
+
elif char == 'p':
|
|
281
|
+
if 'P' in spec or 's' in spec:
|
|
282
|
+
raise ValueError(f'"p", "P", and "s" are mutually exclusive: {spec}')
|
|
283
|
+
path_type = path
|
|
284
|
+
elif char == 'P':
|
|
285
|
+
if 'p' in spec or 's' in spec:
|
|
286
|
+
raise ValueError(f'"P", "p", and "s" are mutually exclusive: {spec}')
|
|
287
|
+
path_type = pathlib.Path
|
|
288
|
+
elif char == 'r':
|
|
289
|
+
readable = True
|
|
290
|
+
elif char == 's':
|
|
291
|
+
if 'p' in spec or 'P' in spec:
|
|
292
|
+
raise ValueError(f'"s", "p", and "P" are mutually exclusive: {spec}')
|
|
293
|
+
path_type = str
|
|
294
|
+
elif char == 'w':
|
|
295
|
+
writable = True
|
|
296
|
+
elif char == 'x':
|
|
297
|
+
executable = True
|
|
298
|
+
elif char == 'z':
|
|
299
|
+
resolve_path = True
|
|
300
|
+
elif char == '-':
|
|
301
|
+
allow_dash = True
|
|
302
|
+
else:
|
|
303
|
+
raise ValueError(f'unknown specification character "{char}": {spec}')
|
|
304
|
+
return click.Path(allow_dash=allow_dash,
|
|
305
|
+
dir_okay=dir_okay,
|
|
306
|
+
executable=executable,
|
|
307
|
+
exists=exists,
|
|
308
|
+
file_okay=file_okay,
|
|
309
|
+
path_type=path_type,
|
|
310
|
+
readable=readable,
|
|
311
|
+
resolve_path=resolve_path,
|
|
312
|
+
writable=writable)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lockss-pybasic
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev8
|
|
4
4
|
Summary: Basic Python utilities
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE
|
|
@@ -8,7 +8,7 @@ Author: Thib Guicherd-Callin
|
|
|
8
8
|
Author-email: thib@cs.stanford.edu
|
|
9
9
|
Maintainer: Thib Guicherd-Callin
|
|
10
10
|
Maintainer-email: thib@cs.stanford.edu
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.10,<4.0
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Framework :: Pydantic :: 2
|
|
@@ -16,7 +16,8 @@ Classifier: Intended Audience :: Developers
|
|
|
16
16
|
Classifier: License :: OSI Approved :: BSD License
|
|
17
17
|
Classifier: Programming Language :: Python
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
-
Requires-Dist:
|
|
19
|
+
Requires-Dist: click-extra (>=7.5.0,<7.6.0)
|
|
20
|
+
Requires-Dist: pydantic (>=2.11.0,<2.12.0)
|
|
20
21
|
Requires-Dist: pydantic-argparse (>=0.10.0,<0.11.0)
|
|
21
22
|
Requires-Dist: rich-argparse (>=1.7.0,<1.8.0)
|
|
22
23
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
@@ -27,8 +28,8 @@ Description-Content-Type: text/x-rst
|
|
|
27
28
|
lockss-pybasic
|
|
28
29
|
==============
|
|
29
30
|
|
|
30
|
-
.. |RELEASE| replace:: 0.
|
|
31
|
-
.. |RELEASE_DATE| replace::
|
|
31
|
+
.. |RELEASE| replace:: 0.2.0-dev8
|
|
32
|
+
.. |RELEASE_DATE| replace:: NOT YET RELEASED
|
|
32
33
|
|
|
33
34
|
**Latest release:** |RELEASE| (|RELEASE_DATE|)
|
|
34
35
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
lockss/pybasic/__init__.py,sha256=A-Y9xGDnzeI7ZG2J4KT3DCP7lDVCAzlvJ1dJD87420E,1678
|
|
2
|
+
lockss/pybasic/auidutil.py,sha256=o5IsRLEYROXRVS6oTO1VFtdzw7SImYSR5VcqAMHY4To,13921
|
|
3
|
+
lockss/pybasic/cliutil.py,sha256=vf8lxc-m6p6if5D7uWIBynIxvCiG6uZg44pHCMSiSzs,12866
|
|
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.dev8.dist-info/METADATA,sha256=xucIusdQQSQBwgcbNN2uS-6nb4hHDzBMxh_0m1_B7rs,4326
|
|
8
|
+
lockss_pybasic-0.2.0.dev8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
9
|
+
lockss_pybasic-0.2.0.dev8.dist-info/licenses/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
|
|
10
|
+
lockss_pybasic-0.2.0.dev8.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
{lockss_pybasic-0.2.0.dev6.dist-info → lockss_pybasic-0.2.0.dev8.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|