transcrypto 1.7.0__py3-none-any.whl → 1.8.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.
- transcrypto/__init__.py +1 -1
- transcrypto/base.py +58 -339
- transcrypto/cli/__init__.py +3 -0
- transcrypto/cli/aeshash.py +368 -0
- transcrypto/cli/bidsecret.py +334 -0
- transcrypto/cli/clibase.py +303 -0
- transcrypto/cli/intmath.py +427 -0
- transcrypto/cli/publicalgos.py +877 -0
- transcrypto/profiler.py +10 -7
- transcrypto/transcrypto.py +40 -1986
- {transcrypto-1.7.0.dist-info → transcrypto-1.8.0.dist-info}/METADATA +12 -10
- transcrypto-1.8.0.dist-info/RECORD +23 -0
- transcrypto-1.7.0.dist-info/RECORD +0 -17
- {transcrypto-1.7.0.dist-info → transcrypto-1.8.0.dist-info}/WHEEL +0 -0
- {transcrypto-1.7.0.dist-info → transcrypto-1.8.0.dist-info}/entry_points.txt +0 -0
- {transcrypto-1.7.0.dist-info → transcrypto-1.8.0.dist-info}/licenses/LICENSE +0 -0
transcrypto/__init__.py
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"""Basic cryptography primitives implementation."""
|
|
4
4
|
|
|
5
5
|
__all__: list[str] = ['__author__', '__version__']
|
|
6
|
-
__version__ = '1.
|
|
6
|
+
__version__ = '1.8.0' # remember to also update pyproject.toml
|
|
7
7
|
__author__ = 'Daniel Balparda <balparda@github.com>'
|
transcrypto/base.py
CHANGED
|
@@ -15,12 +15,10 @@ import hashlib
|
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
17
|
import math
|
|
18
|
-
import os
|
|
19
18
|
import pathlib
|
|
20
19
|
import pickle # noqa: S403
|
|
21
20
|
import secrets
|
|
22
21
|
import sys
|
|
23
|
-
import threading
|
|
24
22
|
import time
|
|
25
23
|
from collections import abc
|
|
26
24
|
from types import TracebackType
|
|
@@ -28,23 +26,39 @@ from typing import (
|
|
|
28
26
|
Any,
|
|
29
27
|
Protocol,
|
|
30
28
|
Self,
|
|
31
|
-
TypeVar,
|
|
32
29
|
cast,
|
|
33
30
|
final,
|
|
34
31
|
runtime_checkable,
|
|
35
32
|
)
|
|
36
33
|
|
|
37
|
-
import click
|
|
38
34
|
import numpy as np
|
|
39
|
-
import typer
|
|
40
35
|
import zstandard
|
|
41
|
-
from click import testing as click_testing
|
|
42
|
-
from rich import console as rich_console
|
|
43
|
-
from rich import logging as rich_logging
|
|
44
36
|
from scipy import stats
|
|
45
37
|
|
|
46
38
|
# Data conversion utils
|
|
47
39
|
|
|
40
|
+
# JSON types
|
|
41
|
+
type JSONValue = bool | int | float | str | list[JSONValue] | dict[str, JSONValue] | None
|
|
42
|
+
type JSONDict = dict[str, JSONValue]
|
|
43
|
+
|
|
44
|
+
# Crypto types: add bytes for cryptographic data; has to be encoded for JSON serialization
|
|
45
|
+
type CryptValue = bool | int | float | str | bytes | list[CryptValue] | dict[str, CryptValue] | None
|
|
46
|
+
type CryptDict = dict[str, CryptValue]
|
|
47
|
+
_JSON_DATACLASS_TYPES: set[str] = {
|
|
48
|
+
# native support
|
|
49
|
+
'int',
|
|
50
|
+
'float',
|
|
51
|
+
'str',
|
|
52
|
+
'bool',
|
|
53
|
+
# support for lists for now, but no nested lists or dicts yet
|
|
54
|
+
'list[int]',
|
|
55
|
+
'list[float]',
|
|
56
|
+
'list[str]',
|
|
57
|
+
'list[bool]',
|
|
58
|
+
# need conversion/encoding: see CryptValue/CryptDict
|
|
59
|
+
'bytes',
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
BytesToHex: abc.Callable[[bytes], str] = lambda b: b.hex()
|
|
49
63
|
BytesToInt: abc.Callable[[bytes], int] = lambda b: int.from_bytes(b, 'big', signed=False)
|
|
50
64
|
BytesToEncoded: abc.Callable[[bytes], str] = lambda b: base64.urlsafe_b64encode(b).decode('ascii')
|
|
@@ -67,26 +81,6 @@ TimeStr: abc.Callable[[int | float | None], str] = lambda tm: (
|
|
|
67
81
|
Now: abc.Callable[[], int] = lambda: int(time.time())
|
|
68
82
|
StrNow: abc.Callable[[], str] = lambda: TimeStr(Now())
|
|
69
83
|
|
|
70
|
-
# Logging
|
|
71
|
-
_LOG_FORMAT_NO_PROCESS: str = '%(funcName)s: %(message)s'
|
|
72
|
-
_LOG_FORMAT_WITH_PROCESS: str = '%(processName)s/' + _LOG_FORMAT_NO_PROCESS
|
|
73
|
-
_LOG_FORMAT_DATETIME: str = '[%Y%m%d-%H:%M:%S]' # e.g., [20240131-13:45:30]
|
|
74
|
-
_LOG_LEVELS: dict[int, int] = {
|
|
75
|
-
0: logging.ERROR,
|
|
76
|
-
1: logging.WARNING,
|
|
77
|
-
2: logging.INFO,
|
|
78
|
-
3: logging.DEBUG,
|
|
79
|
-
}
|
|
80
|
-
_LOG_COMMON_PROVIDERS: set[str] = {
|
|
81
|
-
'werkzeug',
|
|
82
|
-
'gunicorn.error',
|
|
83
|
-
'gunicorn.access',
|
|
84
|
-
'uvicorn',
|
|
85
|
-
'uvicorn.error',
|
|
86
|
-
'uvicorn.access',
|
|
87
|
-
'django.server',
|
|
88
|
-
}
|
|
89
|
-
|
|
90
84
|
# SI prefix table, powers of 1000
|
|
91
85
|
_SI_PREFIXES: dict[int, str] = {
|
|
92
86
|
-6: 'a', # atto
|
|
@@ -109,29 +103,15 @@ _SI_PREFIXES: dict[int, str] = {
|
|
|
109
103
|
_PICKLE_PROTOCOL = 4 # protocol 4 available since python v3.8 # do NOT ever change!
|
|
110
104
|
PickleGeneric: abc.Callable[[Any], bytes] = lambda o: pickle.dumps(o, protocol=_PICKLE_PROTOCOL)
|
|
111
105
|
UnpickleGeneric: abc.Callable[[bytes], Any] = pickle.loads # noqa: S301
|
|
112
|
-
PickleJSON: abc.Callable[[
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
UnpickleJSON: abc.Callable[[bytes],
|
|
106
|
+
PickleJSON: abc.Callable[[JSONDict], bytes] = lambda d: json.dumps(d, separators=(',', ':')).encode(
|
|
107
|
+
'utf-8'
|
|
108
|
+
)
|
|
109
|
+
UnpickleJSON: abc.Callable[[bytes], JSONDict] = lambda b: json.loads(b.decode('utf-8'))
|
|
116
110
|
_PICKLE_AAD = b'transcrypto.base.Serialize.1.0' # do NOT ever change!
|
|
117
111
|
# these help find compressed files, do NOT change unless zstandard changes
|
|
118
112
|
_ZSTD_MAGIC_FRAME = 0xFD2FB528
|
|
119
113
|
_ZSTD_MAGIC_SKIPPABLE_MIN = 0x184D2A50
|
|
120
114
|
_ZSTD_MAGIC_SKIPPABLE_MAX = 0x184D2A5F
|
|
121
|
-
# JSON
|
|
122
|
-
_JSON_DATACLASS_TYPES: set[str] = {
|
|
123
|
-
# native support
|
|
124
|
-
'int',
|
|
125
|
-
'float',
|
|
126
|
-
'str',
|
|
127
|
-
'bool',
|
|
128
|
-
'list[int]',
|
|
129
|
-
'list[float]',
|
|
130
|
-
'list[str]',
|
|
131
|
-
'list[bool]',
|
|
132
|
-
# need conversion/encoding
|
|
133
|
-
'bytes',
|
|
134
|
-
}
|
|
135
115
|
|
|
136
116
|
|
|
137
117
|
class Error(Exception):
|
|
@@ -150,112 +130,6 @@ class ImplementationError(Error, NotImplementedError):
|
|
|
150
130
|
"""Feature is not implemented yet (TransCrypto)."""
|
|
151
131
|
|
|
152
132
|
|
|
153
|
-
__console_lock: threading.RLock = threading.RLock()
|
|
154
|
-
__console_singleton: rich_console.Console | None = None
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def Console() -> rich_console.Console:
|
|
158
|
-
"""Get the global console instance.
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
rich.console.Console: The global console instance.
|
|
162
|
-
|
|
163
|
-
"""
|
|
164
|
-
with __console_lock:
|
|
165
|
-
if __console_singleton is None:
|
|
166
|
-
return rich_console.Console() # fallback console if InitLogging hasn't been called yet
|
|
167
|
-
return __console_singleton
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def ResetConsole() -> None:
|
|
171
|
-
"""Reset the global console instance."""
|
|
172
|
-
global __console_singleton # noqa: PLW0603
|
|
173
|
-
with __console_lock:
|
|
174
|
-
__console_singleton = None
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def InitLogging(
|
|
178
|
-
verbosity: int,
|
|
179
|
-
/,
|
|
180
|
-
*,
|
|
181
|
-
include_process: bool = False,
|
|
182
|
-
soft_wrap: bool = False,
|
|
183
|
-
color: bool | None = False,
|
|
184
|
-
) -> tuple[rich_console.Console, int, bool]:
|
|
185
|
-
"""Initialize logger (with RichHandler) and get a rich.console.Console singleton.
|
|
186
|
-
|
|
187
|
-
This method will also return the actual decided values for verbosity and color use.
|
|
188
|
-
If you have a CLI app that uses this, its pytests should call `ResetConsole()` in a fixture, like:
|
|
189
|
-
|
|
190
|
-
from transcrypto import logging
|
|
191
|
-
@pytest.fixture(autouse=True)
|
|
192
|
-
def _reset_base_logging() -> Generator[None, None, None]: # type: ignore
|
|
193
|
-
logging.ResetConsole()
|
|
194
|
-
yield # stop
|
|
195
|
-
|
|
196
|
-
Args:
|
|
197
|
-
verbosity (int): Logging verbosity level: 0==ERROR, 1==WARNING, 2==INFO, 3==DEBUG
|
|
198
|
-
include_process (bool, optional): Whether to include process name in log output.
|
|
199
|
-
soft_wrap (bool, optional): Whether to enable soft wrapping in the console.
|
|
200
|
-
Default is False, and it means rich will hard-wrap long lines (by adding line breaks).
|
|
201
|
-
color (bool | None, optional): Whether to enable/disable color output in the console.
|
|
202
|
-
If None, respects NO_COLOR env var.
|
|
203
|
-
|
|
204
|
-
Returns:
|
|
205
|
-
tuple[rich_console.Console, int, bool]:
|
|
206
|
-
(The initialized console instance, actual log level, actual color use)
|
|
207
|
-
|
|
208
|
-
Raises:
|
|
209
|
-
RuntimeError: if you call this more than once
|
|
210
|
-
|
|
211
|
-
"""
|
|
212
|
-
global __console_singleton # noqa: PLW0603
|
|
213
|
-
with __console_lock:
|
|
214
|
-
if __console_singleton is not None:
|
|
215
|
-
raise RuntimeError(
|
|
216
|
-
'calling InitLogging() more than once is forbidden; '
|
|
217
|
-
'use Console() to get a console after first creation'
|
|
218
|
-
)
|
|
219
|
-
# set level
|
|
220
|
-
logging_level: int = _LOG_LEVELS.get(min(verbosity, 3), logging.ERROR)
|
|
221
|
-
# respect NO_COLOR unless the caller has already decided (treat env presence as "disable color")
|
|
222
|
-
no_color: bool = (
|
|
223
|
-
False
|
|
224
|
-
if (os.getenv('NO_COLOR') is None and color is None)
|
|
225
|
-
else ((os.getenv('NO_COLOR') is not None) if color is None else (not color))
|
|
226
|
-
)
|
|
227
|
-
# create console and configure logging
|
|
228
|
-
console = rich_console.Console(soft_wrap=soft_wrap, no_color=no_color)
|
|
229
|
-
logging.basicConfig(
|
|
230
|
-
level=logging_level,
|
|
231
|
-
format=_LOG_FORMAT_WITH_PROCESS if include_process else _LOG_FORMAT_NO_PROCESS,
|
|
232
|
-
datefmt=_LOG_FORMAT_DATETIME,
|
|
233
|
-
handlers=[
|
|
234
|
-
rich_logging.RichHandler( # we show name/line, but want time & level
|
|
235
|
-
console=console,
|
|
236
|
-
rich_tracebacks=True,
|
|
237
|
-
show_time=True,
|
|
238
|
-
show_level=True,
|
|
239
|
-
show_path=True,
|
|
240
|
-
),
|
|
241
|
-
],
|
|
242
|
-
force=True, # force=True to override any previous logging config
|
|
243
|
-
)
|
|
244
|
-
# configure common loggers
|
|
245
|
-
logging.captureWarnings(True)
|
|
246
|
-
for name in _LOG_COMMON_PROVIDERS:
|
|
247
|
-
log: logging.Logger = logging.getLogger(name)
|
|
248
|
-
log.handlers.clear()
|
|
249
|
-
log.propagate = True
|
|
250
|
-
log.setLevel(logging_level)
|
|
251
|
-
__console_singleton = console # need a global statement to re-bind this one
|
|
252
|
-
logging.info(
|
|
253
|
-
f'Logging initialized at level {logging.getLevelName(logging_level)} / '
|
|
254
|
-
f'{"NO " if no_color else ""}COLOR'
|
|
255
|
-
)
|
|
256
|
-
return (console, logging_level, not no_color)
|
|
257
|
-
|
|
258
|
-
|
|
259
133
|
def HumanizedBytes(inp_sz: float, /) -> str: # noqa: PLR0911
|
|
260
134
|
"""Convert a byte count into a human-readable string using binary prefixes (powers of 1024).
|
|
261
135
|
|
|
@@ -562,18 +436,24 @@ class Timer:
|
|
|
562
436
|
"""
|
|
563
437
|
|
|
564
438
|
def __init__(
|
|
565
|
-
self,
|
|
439
|
+
self,
|
|
440
|
+
label: str = '',
|
|
441
|
+
/,
|
|
442
|
+
*,
|
|
443
|
+
emit_log: bool = True,
|
|
444
|
+
emit_print: abc.Callable[[str], None] | None = None,
|
|
566
445
|
) -> None:
|
|
567
446
|
"""Initialize the Timer.
|
|
568
447
|
|
|
569
448
|
Args:
|
|
570
449
|
label (str, optional): A description or name for the timed block or function
|
|
571
450
|
emit_log (bool, optional): Emit a log message when finished; default is True
|
|
572
|
-
emit_print (
|
|
451
|
+
emit_print (Callable[[str], None] | None, optional): Emit a print() message when
|
|
452
|
+
finished using the provided callable; default is None
|
|
573
453
|
|
|
574
454
|
"""
|
|
575
455
|
self.emit_log: bool = emit_log
|
|
576
|
-
self.emit_print:
|
|
456
|
+
self.emit_print: abc.Callable[[str], None] | None = emit_print
|
|
577
457
|
self.label: str = label.strip()
|
|
578
458
|
self.start: float | None = None
|
|
579
459
|
self.end: float | None = None
|
|
@@ -647,8 +527,8 @@ class Timer:
|
|
|
647
527
|
message: str = str(self)
|
|
648
528
|
if self.emit_log:
|
|
649
529
|
logging.info(message)
|
|
650
|
-
if self.emit_print:
|
|
651
|
-
|
|
530
|
+
if self.emit_print is not None:
|
|
531
|
+
self.emit_print(message)
|
|
652
532
|
|
|
653
533
|
def __exit__(
|
|
654
534
|
self,
|
|
@@ -659,9 +539,7 @@ class Timer:
|
|
|
659
539
|
"""Stop the timer when exiting the context."""
|
|
660
540
|
self.Stop()
|
|
661
541
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
def __call__(self, func: Timer._F) -> Timer._F:
|
|
542
|
+
def __call__[**F, R](self, func: abc.Callable[F, R]) -> abc.Callable[F, R]:
|
|
665
543
|
"""Allow the Timer to be used as a decorator.
|
|
666
544
|
|
|
667
545
|
Args:
|
|
@@ -673,11 +551,11 @@ class Timer:
|
|
|
673
551
|
"""
|
|
674
552
|
|
|
675
553
|
@functools.wraps(func)
|
|
676
|
-
def _Wrapper(*args:
|
|
554
|
+
def _Wrapper(*args: F.args, **kwargs: F.kwargs) -> R:
|
|
677
555
|
with self.__class__(self.label, emit_log=self.emit_log, emit_print=self.emit_print):
|
|
678
556
|
return func(*args, **kwargs)
|
|
679
557
|
|
|
680
|
-
return _Wrapper
|
|
558
|
+
return _Wrapper
|
|
681
559
|
|
|
682
560
|
|
|
683
561
|
def RandBits(n_bits: int, /) -> int:
|
|
@@ -731,7 +609,7 @@ def RandInt(min_int: int, max_int: int, /) -> int:
|
|
|
731
609
|
return n
|
|
732
610
|
|
|
733
611
|
|
|
734
|
-
def RandShuffle[T
|
|
612
|
+
def RandShuffle[T](seq: abc.MutableSequence[T], /) -> None:
|
|
735
613
|
"""In-place Crypto-random shuffle order for `seq` mutable sequence.
|
|
736
614
|
|
|
737
615
|
Args:
|
|
@@ -1108,17 +986,17 @@ class CryptoKey(abstract.ABC):
|
|
|
1108
986
|
|
|
1109
987
|
@final
|
|
1110
988
|
@property
|
|
1111
|
-
def _json_dict(self) ->
|
|
989
|
+
def _json_dict(self) -> JSONDict:
|
|
1112
990
|
"""Dictionary representation of the object suitable for JSON conversion.
|
|
1113
991
|
|
|
1114
992
|
Returns:
|
|
1115
|
-
|
|
993
|
+
JSONDict: representation of the object suitable for JSON conversion
|
|
1116
994
|
|
|
1117
995
|
Raises:
|
|
1118
996
|
ImplementationError: object has types that are not supported in JSON
|
|
1119
997
|
|
|
1120
998
|
"""
|
|
1121
|
-
self_dict:
|
|
999
|
+
self_dict: CryptDict = dataclasses.asdict(self)
|
|
1122
1000
|
for field in dataclasses.fields(self):
|
|
1123
1001
|
# check the type is OK
|
|
1124
1002
|
if field.type not in _JSON_DATACLASS_TYPES:
|
|
@@ -1127,8 +1005,8 @@ class CryptoKey(abstract.ABC):
|
|
|
1127
1005
|
)
|
|
1128
1006
|
# convert types that we accept but JSON does not
|
|
1129
1007
|
if field.type == 'bytes':
|
|
1130
|
-
self_dict[field.name] = BytesToEncoded(self_dict[field.name])
|
|
1131
|
-
return self_dict
|
|
1008
|
+
self_dict[field.name] = BytesToEncoded(cast('bytes', self_dict[field.name]))
|
|
1009
|
+
return cast('JSONDict', self_dict)
|
|
1132
1010
|
|
|
1133
1011
|
@final
|
|
1134
1012
|
@property
|
|
@@ -1154,11 +1032,11 @@ class CryptoKey(abstract.ABC):
|
|
|
1154
1032
|
|
|
1155
1033
|
@final
|
|
1156
1034
|
@classmethod
|
|
1157
|
-
def _FromJSONDict(cls, json_dict:
|
|
1035
|
+
def _FromJSONDict(cls, json_dict: JSONDict, /) -> Self:
|
|
1158
1036
|
"""Create object from JSON representation.
|
|
1159
1037
|
|
|
1160
1038
|
Args:
|
|
1161
|
-
json_dict (
|
|
1039
|
+
json_dict (JSONDict): JSON dict
|
|
1162
1040
|
|
|
1163
1041
|
Returns:
|
|
1164
1042
|
a CryptoKey object ready for use
|
|
@@ -1180,7 +1058,7 @@ class CryptoKey(abstract.ABC):
|
|
|
1180
1058
|
f'Unsupported JSON field {field.name!r}/{field.type} not in {_JSON_DATACLASS_TYPES}'
|
|
1181
1059
|
)
|
|
1182
1060
|
if field.type == 'bytes':
|
|
1183
|
-
json_dict[field.name] = EncodedToBytes(json_dict[field.name])
|
|
1061
|
+
json_dict[field.name] = EncodedToBytes(json_dict[field.name]) # type: ignore[assignment, arg-type]
|
|
1184
1062
|
# build the object
|
|
1185
1063
|
return cls(**json_dict)
|
|
1186
1064
|
|
|
@@ -1200,7 +1078,7 @@ class CryptoKey(abstract.ABC):
|
|
|
1200
1078
|
|
|
1201
1079
|
"""
|
|
1202
1080
|
# get the dict back
|
|
1203
|
-
json_dict:
|
|
1081
|
+
json_dict: JSONDict = json.loads(json_data)
|
|
1204
1082
|
if not isinstance(json_dict, dict): # pyright: ignore[reportUnnecessaryIsInstance]
|
|
1205
1083
|
raise InputError(f'JSON data decoded to unexpected type: {type(json_dict)}')
|
|
1206
1084
|
return cls._FromJSONDict(json_dict)
|
|
@@ -1328,9 +1206,7 @@ class CryptoKey(abstract.ABC):
|
|
|
1328
1206
|
data = BytesFromInput(data)
|
|
1329
1207
|
# we now have bytes and we suppose it came from CryptoKey.blob()/CryptoKey.CryptoBlob()
|
|
1330
1208
|
try:
|
|
1331
|
-
json_dict:
|
|
1332
|
-
data=data, key=key, silent=silent, unpickler=UnpickleJSON
|
|
1333
|
-
)
|
|
1209
|
+
json_dict: JSONDict = DeSerialize(data=data, key=key, silent=silent, unpickler=UnpickleJSON)
|
|
1334
1210
|
return cls._FromJSONDict(json_dict)
|
|
1335
1211
|
except Exception as err:
|
|
1336
1212
|
raise InputError(f'input decode error: {err}') from err
|
|
@@ -1445,15 +1321,15 @@ class Signer(Protocol):
|
|
|
1445
1321
|
"""
|
|
1446
1322
|
|
|
1447
1323
|
|
|
1448
|
-
def Serialize(
|
|
1449
|
-
python_obj:
|
|
1324
|
+
def Serialize[T](
|
|
1325
|
+
python_obj: T,
|
|
1450
1326
|
/,
|
|
1451
1327
|
*,
|
|
1452
1328
|
file_path: str | None = None,
|
|
1453
1329
|
compress: int | None = 3,
|
|
1454
1330
|
key: Encryptor | None = None,
|
|
1455
1331
|
silent: bool = False,
|
|
1456
|
-
pickler: abc.Callable[[
|
|
1332
|
+
pickler: abc.Callable[[T], bytes] = PickleGeneric,
|
|
1457
1333
|
) -> bytes:
|
|
1458
1334
|
"""Serialize a Python object into a BLOB, optionally compress / encrypt / save to disk.
|
|
1459
1335
|
|
|
@@ -1523,14 +1399,14 @@ def Serialize(
|
|
|
1523
1399
|
return obj
|
|
1524
1400
|
|
|
1525
1401
|
|
|
1526
|
-
def DeSerialize( # noqa: C901
|
|
1402
|
+
def DeSerialize[T]( # noqa: C901
|
|
1527
1403
|
*,
|
|
1528
1404
|
data: bytes | None = None,
|
|
1529
1405
|
file_path: str | None = None,
|
|
1530
1406
|
key: Decryptor | None = None,
|
|
1531
1407
|
silent: bool = False,
|
|
1532
|
-
unpickler: abc.Callable[[bytes],
|
|
1533
|
-
) ->
|
|
1408
|
+
unpickler: abc.Callable[[bytes], T] = UnpickleGeneric,
|
|
1409
|
+
) -> T:
|
|
1534
1410
|
"""Load (de-serializes) a BLOB back to a Python object, optionally decrypting / decompressing.
|
|
1535
1411
|
|
|
1536
1412
|
Data path is:
|
|
@@ -1602,7 +1478,7 @@ def DeSerialize( # noqa: C901
|
|
|
1602
1478
|
messages.append(' (no compression detected)')
|
|
1603
1479
|
# create the actual object = unpickle
|
|
1604
1480
|
with Timer('UNPICKLE', emit_log=False) as tm_unpickle:
|
|
1605
|
-
python_obj:
|
|
1481
|
+
python_obj: T = unpickler(obj)
|
|
1606
1482
|
if not silent:
|
|
1607
1483
|
messages.append(f' {tm_unpickle}')
|
|
1608
1484
|
# log and return
|
|
@@ -1759,160 +1635,3 @@ class PrivateBid512(PublicBid512):
|
|
|
1759
1635
|
private_key=private_key,
|
|
1760
1636
|
secret_bid=secret,
|
|
1761
1637
|
)
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
1765
|
-
class CLIConfig:
|
|
1766
|
-
"""CLI global context, storing the configuration."""
|
|
1767
|
-
|
|
1768
|
-
console: rich_console.Console
|
|
1769
|
-
verbose: int
|
|
1770
|
-
color: bool | None
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
def CLIErrorGuard[**P](fn: abc.Callable[P, None], /) -> abc.Callable[P, None]:
|
|
1774
|
-
"""Guard CLI command functions.
|
|
1775
|
-
|
|
1776
|
-
Returns:
|
|
1777
|
-
A wrapped function that catches expected user-facing errors and prints them consistently.
|
|
1778
|
-
|
|
1779
|
-
"""
|
|
1780
|
-
|
|
1781
|
-
@functools.wraps(fn)
|
|
1782
|
-
def _Wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
|
|
1783
|
-
try:
|
|
1784
|
-
# call the actual function
|
|
1785
|
-
fn(*args, **kwargs)
|
|
1786
|
-
except (Error, ValueError) as err:
|
|
1787
|
-
# get context
|
|
1788
|
-
ctx: object | None = dict(kwargs).get('ctx')
|
|
1789
|
-
if not isinstance(ctx, typer.Context):
|
|
1790
|
-
ctx = next((a for a in args if isinstance(a, typer.Context)), None)
|
|
1791
|
-
# print error nicely
|
|
1792
|
-
if isinstance(ctx, typer.Context):
|
|
1793
|
-
# we have context
|
|
1794
|
-
obj: CLIConfig = cast('CLIConfig', ctx.obj)
|
|
1795
|
-
if obj.verbose >= 2: # verbose >= 2 means INFO level or more verbose # noqa: PLR2004
|
|
1796
|
-
obj.console.print_exception() # print full traceback
|
|
1797
|
-
else:
|
|
1798
|
-
obj.console.print(str(err)) # print only error message
|
|
1799
|
-
# no context
|
|
1800
|
-
elif logging.getLogger().getEffectiveLevel() < logging.INFO:
|
|
1801
|
-
Console().print(str(err)) # print only error message (DEBUG level is verbose already)
|
|
1802
|
-
else:
|
|
1803
|
-
Console().print_exception() # print full traceback (less verbose mode needs it)
|
|
1804
|
-
|
|
1805
|
-
return _Wrapper
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
def _ClickWalk(
|
|
1809
|
-
command: click.Command,
|
|
1810
|
-
ctx: typer.Context,
|
|
1811
|
-
path: list[str],
|
|
1812
|
-
/,
|
|
1813
|
-
) -> abc.Iterator[tuple[list[str], click.Command, typer.Context]]:
|
|
1814
|
-
"""Recursively walk Click commands/groups.
|
|
1815
|
-
|
|
1816
|
-
Yields:
|
|
1817
|
-
tuple[list[str], click.Command, typer.Context]: path, command, ctx
|
|
1818
|
-
|
|
1819
|
-
"""
|
|
1820
|
-
yield (path, command, ctx) # yield self
|
|
1821
|
-
# now walk subcommands, if any
|
|
1822
|
-
sub_cmd: click.Command | None
|
|
1823
|
-
sub_ctx: typer.Context
|
|
1824
|
-
# prefer the explicit `.commands` mapping when present; otherwise fall back to
|
|
1825
|
-
# click's `list_commands()`/`get_command()` for dynamic groups
|
|
1826
|
-
if not isinstance(command, click.Group):
|
|
1827
|
-
return
|
|
1828
|
-
# explicit commands mapping
|
|
1829
|
-
if command.commands:
|
|
1830
|
-
for name, sub_cmd in sorted(command.commands.items()):
|
|
1831
|
-
sub_ctx = typer.Context(sub_cmd, info_name=name, parent=ctx)
|
|
1832
|
-
yield from _ClickWalk(sub_cmd, sub_ctx, [*path, name])
|
|
1833
|
-
return
|
|
1834
|
-
# dynamic commands
|
|
1835
|
-
for name in sorted(command.list_commands(ctx)):
|
|
1836
|
-
sub_cmd = command.get_command(ctx, name)
|
|
1837
|
-
if sub_cmd is None:
|
|
1838
|
-
continue # skip invalid subcommands
|
|
1839
|
-
sub_ctx = typer.Context(sub_cmd, info_name=name, parent=ctx)
|
|
1840
|
-
yield from _ClickWalk(sub_cmd, sub_ctx, [*path, name])
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
def GenerateTyperHelpMarkdown(
|
|
1844
|
-
typer_app: typer.Typer,
|
|
1845
|
-
/,
|
|
1846
|
-
*,
|
|
1847
|
-
prog_name: str,
|
|
1848
|
-
heading_level: int = 1,
|
|
1849
|
-
code_fence_language: str = 'text',
|
|
1850
|
-
) -> str:
|
|
1851
|
-
"""Capture `--help` for a Typer CLI and all subcommands as Markdown.
|
|
1852
|
-
|
|
1853
|
-
This function converts a Typer app to its underlying Click command tree and then:
|
|
1854
|
-
- invokes `--help` for the root ("Main") command
|
|
1855
|
-
- walks commands/subcommands recursively
|
|
1856
|
-
- invokes `--help` for each command path
|
|
1857
|
-
|
|
1858
|
-
It emits a Markdown document with a heading per command and a fenced block
|
|
1859
|
-
containing the exact `--help` output.
|
|
1860
|
-
|
|
1861
|
-
Notes:
|
|
1862
|
-
- This uses Click's `CliRunner().invoke(...)` for faithful output.
|
|
1863
|
-
- The walk is generic over Click `MultiCommand`/`Group` structures.
|
|
1864
|
-
- If a command cannot be loaded, it is skipped.
|
|
1865
|
-
|
|
1866
|
-
Args:
|
|
1867
|
-
typer_app: The Typer app (e.g. `app`).
|
|
1868
|
-
prog_name: Program name used in usage strings (e.g. "profiler").
|
|
1869
|
-
heading_level: Markdown heading level for each command section.
|
|
1870
|
-
code_fence_language: Language tag for fenced blocks (default: "text").
|
|
1871
|
-
|
|
1872
|
-
Returns:
|
|
1873
|
-
Markdown string.
|
|
1874
|
-
|
|
1875
|
-
"""
|
|
1876
|
-
# prepare Click root command and context
|
|
1877
|
-
click_root: click.Command = typer.main.get_command(typer_app)
|
|
1878
|
-
root_ctx: typer.Context = typer.Context(click_root, info_name=prog_name)
|
|
1879
|
-
runner = click_testing.CliRunner()
|
|
1880
|
-
parts: list[str] = []
|
|
1881
|
-
for path, _, _ in _ClickWalk(click_root, root_ctx, []):
|
|
1882
|
-
# build command path
|
|
1883
|
-
command_path: str = ' '.join([prog_name, *path]).strip()
|
|
1884
|
-
heading_prefix: str = '#' * max(1, heading_level + len(path))
|
|
1885
|
-
ResetConsole() # ensure clean state for each command (also it raises on duplicate loggers)
|
|
1886
|
-
# invoke --help for this command path
|
|
1887
|
-
result: click_testing.Result = runner.invoke(
|
|
1888
|
-
click_root,
|
|
1889
|
-
[*path, '--help'],
|
|
1890
|
-
prog_name=prog_name,
|
|
1891
|
-
color=False,
|
|
1892
|
-
)
|
|
1893
|
-
if result.exit_code != 0 and not result.output:
|
|
1894
|
-
continue # skip invalid commands
|
|
1895
|
-
# build markdown section
|
|
1896
|
-
global_prefix: str = ( # only for the top-level command
|
|
1897
|
-
(
|
|
1898
|
-
'<!-- cspell:disable -->\n'
|
|
1899
|
-
'<!-- auto-generated; DO NOT EDIT! see base.GenerateTyperHelpMarkdown() -->\n\n'
|
|
1900
|
-
)
|
|
1901
|
-
if not path
|
|
1902
|
-
else ''
|
|
1903
|
-
)
|
|
1904
|
-
extras: str = ( # type of command, by level
|
|
1905
|
-
('Command-Line Interface' if not path else 'Command') if len(path) <= 1 else 'Sub-Command'
|
|
1906
|
-
)
|
|
1907
|
-
parts.extend(
|
|
1908
|
-
(
|
|
1909
|
-
f'{global_prefix}{heading_prefix} `{command_path}` {extras}',
|
|
1910
|
-
'',
|
|
1911
|
-
f'```{code_fence_language}',
|
|
1912
|
-
result.output.strip(),
|
|
1913
|
-
'```',
|
|
1914
|
-
'',
|
|
1915
|
-
)
|
|
1916
|
-
)
|
|
1917
|
-
# join all parts and return
|
|
1918
|
-
return '\n'.join(parts).rstrip()
|