lockss-pybasic 0.2.0.dev6__py3-none-any.whl → 0.2.0.dev7__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.
@@ -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.2.0-dev6'
39
+ __version__ = '0.2.0-dev7'
@@ -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, Optional
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.com%2F&year~2023
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, hyphens, underscores, periods, and asterisks are not encoded
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.com%2F'
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
- 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
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
- result.append(encoded[i])
147
- i += 1
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
- continue
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
@@ -129,22 +129,25 @@ class BaseCli(Generic[BaseModelT]):
129
129
 
130
130
  def dispatch(self) -> None:
131
131
  """
132
- Dispatches from the first field ``x_y_z`` in ``self.args`` that is a
132
+ Dispatches from the first field ``x_y_z`` in ``self._args`` that is a
133
133
  command (i.e. whose value derives from ``BaseModel``) to a method
134
134
  called ``_x_y_z``.
135
135
  """
136
- field_names = self._args.__class__.__fields__.keys()
136
+ self._dispatch_recursive(self._args, [])
137
+
138
+ def _dispatch_recursive(self, base_model: BaseModel, subcommands: list[str]) -> None:
139
+ field_names = base_model.__class__.__fields__.keys()
137
140
  for field_name in field_names:
138
- field_value = getattr(self._args, field_name)
141
+ field_value = getattr(base_model, field_name)
139
142
  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
143
+ self._dispatch_recursive(field_value, [*subcommands, field_name])
144
+ return
145
+ func_name = ''.join(f'_{sub}' for sub in subcommands)
146
+ func = getattr(self, func_name)
147
+ if callable(func):
148
+ func(base_model) # FIXME?
146
149
  else:
147
- self._parser.error(f'unknown command; expected one of {", ".join(field_names)}')
150
+ self._parser.exit(1, f'internal error: no {func_name} callable for the {" ".join(sub for sub in subcommands)} command')
148
151
 
149
152
  def _initialize_rich_argparse(self) -> None:
150
153
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockss-pybasic
3
- Version: 0.2.0.dev6
3
+ Version: 0.2.0.dev7
4
4
  Summary: Basic Python utilities
5
5
  License: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -0,0 +1,10 @@
1
+ lockss/pybasic/__init__.py,sha256=zBCFbOy0mzMxX4ofmKT5pq7i2s5bTNX5ECUCi02W2M4,1678
2
+ lockss/pybasic/auidutil.py,sha256=o5IsRLEYROXRVS6oTO1VFtdzw7SImYSR5VcqAMHY4To,13921
3
+ lockss/pybasic/cliutil.py,sha256=BFOijSrTByMHno3bbDmtICLQfREJpD7lVGUAuzvrHBY,10697
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.dev7.dist-info/METADATA,sha256=ReyRdrU3vbGjOcdjkz4r2nqNCHXHWLK0tsN7g_2b2_s,4269
8
+ lockss_pybasic-0.2.0.dev7.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
9
+ lockss_pybasic-0.2.0.dev7.dist-info/licenses/LICENSE,sha256=O9ONND4uDxY_jucI4jZDf2liAk05ScEJaYu-Al7EOdQ,1506
10
+ lockss_pybasic-0.2.0.dev7.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,,