pythagoras 0.24.4__py3-none-any.whl → 0.24.6__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.
- pythagoras/_060_autonomous_code_portals/autonomous_decorators.py +31 -4
- pythagoras/_060_autonomous_code_portals/autonomous_portal_core_classes.py +94 -14
- pythagoras/_060_autonomous_code_portals/names_usage_analyzer.py +133 -4
- pythagoras/_070_protected_code_portals/list_flattener.py +45 -7
- pythagoras/_070_protected_code_portals/package_manager.py +99 -24
- pythagoras/_070_protected_code_portals/protected_portal_core_classes.py +70 -0
- pythagoras/_070_protected_code_portals/system_utils.py +85 -12
- pythagoras/_070_protected_code_portals/validation_succesful_const.py +12 -7
- pythagoras/_800_signatures_and_converters/base_16_32_convertors.py +55 -20
- pythagoras/_800_signatures_and_converters/current_date_gmt_str.py +20 -5
- pythagoras/_800_signatures_and_converters/hash_signatures.py +46 -10
- pythagoras/_800_signatures_and_converters/node_signature.py +27 -12
- pythagoras/_800_signatures_and_converters/random_signatures.py +14 -3
- {pythagoras-0.24.4.dist-info → pythagoras-0.24.6.dist-info}/METADATA +1 -1
- {pythagoras-0.24.4.dist-info → pythagoras-0.24.6.dist-info}/RECORD +16 -16
- {pythagoras-0.24.4.dist-info → pythagoras-0.24.6.dist-info}/WHEEL +0 -0
|
@@ -1,23 +1,55 @@
|
|
|
1
|
+
"""Utilities to install and uninstall Python packages at runtime.
|
|
2
|
+
|
|
3
|
+
This module provides a thin wrapper around pip and the uv tool to install
|
|
4
|
+
and uninstall packages from within the running Python process.
|
|
5
|
+
|
|
6
|
+
Key points:
|
|
7
|
+
- By default, uv is preferred as the installer frontend (uv pip ...). If uv
|
|
8
|
+
or pip is not available, the module will attempt to install the missing
|
|
9
|
+
tool as needed.
|
|
10
|
+
- For safety, uninstall_package refuses to operate on 'pip' or 'uv' directly.
|
|
11
|
+
- Calls are synchronous and raise on non-zero exit status.
|
|
12
|
+
"""
|
|
13
|
+
|
|
1
14
|
import subprocess
|
|
2
15
|
import importlib
|
|
3
16
|
import sys
|
|
17
|
+
from functools import lru_cache
|
|
4
18
|
from typing import Optional
|
|
5
19
|
|
|
6
|
-
_uv_and_pip_installation_needed:bool = True
|
|
7
20
|
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
def _run(command: list[str]) -> str:
|
|
22
|
+
"""Run command; raise RuntimeError on failure."""
|
|
23
|
+
try:
|
|
24
|
+
subprocess.run(command, check=True, stdout=subprocess.PIPE
|
|
25
|
+
, stderr=subprocess.STDOUT, text=True)
|
|
26
|
+
except subprocess.CalledProcessError as e:
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
f"Command failed: {' '.join(command)}\n{e.stdout}") from e
|
|
29
|
+
|
|
12
30
|
|
|
31
|
+
@lru_cache(maxsize=1) # ensure only one call to _install_uv_and_pip
|
|
32
|
+
def _install_uv_and_pip() -> None:
|
|
33
|
+
"""Ensure the 'uv' and 'pip' frontends are available.
|
|
34
|
+
|
|
35
|
+
Behavior:
|
|
36
|
+
- If this helper has already run in the current process and determined
|
|
37
|
+
the tools are present, it returns immediately.
|
|
38
|
+
- Tries to import 'uv'; if missing, installs it using system pip
|
|
39
|
+
(use_uv=False).
|
|
40
|
+
- Tries to import 'pip'; if missing, installs it using uv (use_uv=True).
|
|
41
|
+
|
|
42
|
+
This function is an internal helper and is called implicitly by
|
|
43
|
+
install_package() for any package other than 'pip' or 'uv'.
|
|
44
|
+
"""
|
|
13
45
|
try:
|
|
14
46
|
importlib.import_module("uv")
|
|
15
|
-
except:
|
|
47
|
+
except ModuleNotFoundError:
|
|
16
48
|
install_package("uv", use_uv=False)
|
|
17
49
|
|
|
18
50
|
try:
|
|
19
51
|
importlib.import_module("pip")
|
|
20
|
-
except:
|
|
52
|
+
except ModuleNotFoundError:
|
|
21
53
|
install_package("pip", use_uv=True)
|
|
22
54
|
|
|
23
55
|
|
|
@@ -26,13 +58,40 @@ def install_package(package_name:str
|
|
|
26
58
|
, version:Optional[str]=None
|
|
27
59
|
, use_uv:bool = True
|
|
28
60
|
) -> None:
|
|
29
|
-
"""Install package using pip.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
"""Install a Python package using uv (default) or pip.
|
|
62
|
+
|
|
63
|
+
Parameters:
|
|
64
|
+
- package_name: Name of the package to install. Special cases:
|
|
65
|
+
- 'pip': must be installed using uv (use_uv=True).
|
|
66
|
+
- 'uv' : must be installed using pip (use_uv=False).
|
|
67
|
+
- upgrade: If True, pass "--upgrade" to the installer.
|
|
68
|
+
- version: Optional version pin, e.g. "1.2.3". If provided, constructs
|
|
69
|
+
"package_name==version".
|
|
70
|
+
- use_uv: If True, run as `python -m uv pip install ...`; otherwise use pip.
|
|
71
|
+
|
|
72
|
+
Behavior:
|
|
73
|
+
- Ensures both uv and pip are available unless installing one of them.
|
|
74
|
+
- Runs the installer in a subprocess with check=True (raises on failure).
|
|
75
|
+
- Imports the package after installation to verify it is importable.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
- RuntimeError: if the installation command fails.
|
|
79
|
+
- ValueError: if package_name or version are invalid, or if attempting
|
|
80
|
+
to install pip with use_uv=False or uv with use_uv=True.
|
|
81
|
+
- ModuleNotFoundError: if the package cannot be imported after installation.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if not package_name or not isinstance(package_name, str):
|
|
85
|
+
raise ValueError("package_name must be a non-empty string")
|
|
86
|
+
|
|
87
|
+
if version and not isinstance(version, str):
|
|
88
|
+
raise ValueError("version must be a string")
|
|
89
|
+
|
|
90
|
+
if package_name == "pip" and not use_uv:
|
|
91
|
+
raise ValueError("pip must be installed using uv (use_uv=True)")
|
|
92
|
+
elif package_name == "uv" and use_uv:
|
|
93
|
+
raise ValueError("uv must be installed using pip (use_uv=False)")
|
|
94
|
+
elif package_name not in ("pip", "uv"):
|
|
36
95
|
_install_uv_and_pip()
|
|
37
96
|
|
|
38
97
|
if use_uv:
|
|
@@ -46,30 +105,46 @@ def install_package(package_name:str
|
|
|
46
105
|
package_spec = f"{package_name}=={version}" if version else package_name
|
|
47
106
|
command += [package_spec]
|
|
48
107
|
|
|
49
|
-
|
|
50
|
-
, stderr=subprocess.STDOUT, text=True)
|
|
108
|
+
_run(command)
|
|
51
109
|
|
|
110
|
+
# Verify import. Note: assumes package name matches importable module name.
|
|
52
111
|
importlib.import_module(package_name)
|
|
53
112
|
|
|
54
113
|
|
|
55
114
|
def uninstall_package(package_name:str, use_uv:bool=True)->None:
|
|
56
|
-
"""Uninstall package using uv or pip.
|
|
115
|
+
"""Uninstall a Python package using uv (default) or pip.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
- package_name: Name of the package to uninstall. Must not be 'pip' or 'uv'.
|
|
119
|
+
- use_uv: If True, run `python -m uv pip uninstall <name>`; otherwise use pip with "-y".
|
|
120
|
+
|
|
121
|
+
Behavior:
|
|
122
|
+
- Runs the uninstaller in a subprocess with check=True.
|
|
123
|
+
- Attempts to import and reload the package after uninstallation. If that
|
|
124
|
+
succeeds, raises an Exception to indicate the package still appears installed.
|
|
57
125
|
|
|
58
|
-
|
|
126
|
+
Raises:
|
|
127
|
+
- ValueError: if package_name is 'pip' or 'uv'.
|
|
128
|
+
- RuntimeError: if the uninstall command fails, or if post-uninstall
|
|
129
|
+
validation indicates the package is still importable.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
if package_name in ["pip", "uv"]:
|
|
133
|
+
raise ValueError(f"Cannot uninstall '{package_name}' "
|
|
134
|
+
"- it's a protected package")
|
|
59
135
|
|
|
60
136
|
if use_uv:
|
|
61
137
|
command = [sys.executable, "-m", "uv", "pip", "uninstall", package_name]
|
|
62
138
|
else:
|
|
63
139
|
command = [sys.executable, "-m", "pip", "uninstall", "-y", package_name]
|
|
64
140
|
|
|
65
|
-
|
|
66
|
-
, stderr=subprocess.STDOUT, text=True)
|
|
141
|
+
_run(command)
|
|
67
142
|
|
|
68
143
|
try:
|
|
69
144
|
package = importlib.import_module(package_name)
|
|
70
145
|
importlib.reload(package)
|
|
71
|
-
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
f"Package '{package_name}' still importable after uninstallation")
|
|
148
|
+
except ModuleNotFoundError:
|
|
72
149
|
pass
|
|
73
|
-
|
|
74
|
-
raise Exception(
|
|
75
|
-
f"Failed to validate package uninstallation for '{package_name}'. ")
|
|
150
|
+
|
|
@@ -30,6 +30,24 @@ from .._060_autonomous_code_portals import *
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class ProtectedCodePortal(AutonomousCodePortal):
|
|
33
|
+
"""Portal for protected code execution.
|
|
34
|
+
|
|
35
|
+
This portal specializes the AutonomousCodePortal to coordinate execution of
|
|
36
|
+
ProtectedFn instances. It carries configuration and storage
|
|
37
|
+
required by validators (e.g., retry throttling) and by protected function
|
|
38
|
+
orchestration.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
root_dict (PersiDict | str | None): Optional persistent dictionary or a
|
|
42
|
+
path/identifier to initialize the portal's storage. If None, a
|
|
43
|
+
default in-memory storage may be used.
|
|
44
|
+
p_consistency_checks (float | Joker): Probability or flag controlling
|
|
45
|
+
internal consistency checks performed by the portal. Use
|
|
46
|
+
KEEP_CURRENT to inherit the current setting.
|
|
47
|
+
excessive_logging (bool | Joker): Enables verbose logging of portal and
|
|
48
|
+
function operations. Use KEEP_CURRENT to inherit the current
|
|
49
|
+
setting.
|
|
50
|
+
"""
|
|
33
51
|
|
|
34
52
|
def __init__(self
|
|
35
53
|
, root_dict: PersiDict|str|None = None
|
|
@@ -42,6 +60,14 @@ class ProtectedCodePortal(AutonomousCodePortal):
|
|
|
42
60
|
|
|
43
61
|
|
|
44
62
|
class ProtectedFn(AutonomousFn):
|
|
63
|
+
"""Function wrapper that enforces pre/post validation around execution.
|
|
64
|
+
|
|
65
|
+
A ProtectedFn evaluates a sequence of pre-validators before executing the
|
|
66
|
+
underlying function and a sequence of post-validators after execution. If a
|
|
67
|
+
pre-validator returns a ProtectedFnCallSignature, that signature will be
|
|
68
|
+
executed first (allowing validators to perform prerequisite actions) before
|
|
69
|
+
re-attempting the validation/execution loop.
|
|
70
|
+
"""
|
|
45
71
|
|
|
46
72
|
_pre_validators_cache: list[ValidatorFn] | None
|
|
47
73
|
_post_validators_cache: list[ValidatorFn] | None
|
|
@@ -57,6 +83,25 @@ class ProtectedFn(AutonomousFn):
|
|
|
57
83
|
, excessive_logging: bool | Joker = KEEP_CURRENT
|
|
58
84
|
, fixed_kwargs: dict[str,Any] | None = None
|
|
59
85
|
, portal: ProtectedCodePortal | None = None):
|
|
86
|
+
"""Construct a ProtectedFn.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
fn (Callable | str): The underlying Python function or its source
|
|
90
|
+
code string.
|
|
91
|
+
pre_validators (list[ValidatorFn] | list[Callable] | ValidatorFn | Callable | None):
|
|
92
|
+
Pre-execution validators. Callables are wrapped into
|
|
93
|
+
PreValidatorFn. Lists can be nested and will
|
|
94
|
+
be flattened.
|
|
95
|
+
post_validators (list[ValidatorFn] | list[Callable] | ValidatorFn | Callable | None):
|
|
96
|
+
Post-execution validators. Callables are wrapped into
|
|
97
|
+
PostValidatorFn. Lists can be nested and will be flattened.
|
|
98
|
+
excessive_logging (bool | Joker): Enable verbose logging or inherit
|
|
99
|
+
current setting with KEEP_CURRENT.
|
|
100
|
+
fixed_kwargs (dict[str, Any] | None): Keyword arguments to be fixed
|
|
101
|
+
(bound) for every execution of the function.
|
|
102
|
+
portal (ProtectedCodePortal | None): Portal instance to bind the
|
|
103
|
+
function to.
|
|
104
|
+
"""
|
|
60
105
|
super().__init__(fn=fn
|
|
61
106
|
, portal = portal
|
|
62
107
|
, fixed_kwargs=fixed_kwargs
|
|
@@ -114,6 +159,11 @@ class ProtectedFn(AutonomousFn):
|
|
|
114
159
|
|
|
115
160
|
@property
|
|
116
161
|
def pre_validators(self) -> list[AutonomousFn]:
|
|
162
|
+
"""List of pre-validator functions for this protected function.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
list[AutonomousFn]: A cached list of PreValidatorFn instances.
|
|
166
|
+
"""
|
|
117
167
|
if not hasattr(self, "_pre_validators_cache"):
|
|
118
168
|
self._pre_validators_cache = [
|
|
119
169
|
addr.get() for addr in self._pre_validators_addrs]
|
|
@@ -122,6 +172,11 @@ class ProtectedFn(AutonomousFn):
|
|
|
122
172
|
|
|
123
173
|
@property
|
|
124
174
|
def post_validators(self) -> list[AutonomousFn]:
|
|
175
|
+
"""List of post-validator functions for this protected function.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list[AutonomousFn]: A cached list of PostValidatorFn instances.
|
|
179
|
+
"""
|
|
125
180
|
if not hasattr(self, "_post_validators_cache"):
|
|
126
181
|
self._post_validators_cache = [
|
|
127
182
|
addr.get() for addr in self._post_validators_addrs]
|
|
@@ -131,6 +186,21 @@ class ProtectedFn(AutonomousFn):
|
|
|
131
186
|
def can_be_executed(self
|
|
132
187
|
, kw_args: KwArgs
|
|
133
188
|
) -> ProtectedFnCallSignature|ValidationSuccessFlag|None:
|
|
189
|
+
"""Run pre-validators to determine if execution can proceed.
|
|
190
|
+
|
|
191
|
+
The portal will shuffle the order of pre-validators. If any validator
|
|
192
|
+
returns a ProtectedFnCallSignature, that signature should be executed by
|
|
193
|
+
the caller prior to executing the protected function (this method simply
|
|
194
|
+
returns it). If any validator fails, None is returned. If all succeed,
|
|
195
|
+
VALIDATION_SUCCESSFUL is returned.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
kw_args (KwArgs): Arguments intended for the wrapped function.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
ProtectedFnCallSignature | ValidationSuccessFlag | None: Either a
|
|
202
|
+
signature to execute first, the success flag, or None on failure.
|
|
203
|
+
"""
|
|
134
204
|
with self.portal as portal:
|
|
135
205
|
kw_args = kw_args.pack()
|
|
136
206
|
pre_validators = copy(self.pre_validators)
|
|
@@ -3,13 +3,41 @@ import psutil
|
|
|
3
3
|
import pynvml
|
|
4
4
|
|
|
5
5
|
def get_unused_ram_mb() -> int:
|
|
6
|
-
"""
|
|
6
|
+
"""Get the currently available RAM on the system in megabytes (MB).
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
int: Integer number of megabytes of RAM that are currently available
|
|
10
|
+
to user processes as reported by psutil.virtual_memory().available.
|
|
11
|
+
|
|
12
|
+
Notes:
|
|
13
|
+
- The value is rounded down to the nearest integer.
|
|
14
|
+
- Uses powers-of-two conversion (1 MB = 1024^2 bytes).
|
|
15
|
+
- On systems with memory compression or overcommit, this value is an
|
|
16
|
+
approximation provided by the OS.
|
|
17
|
+
"""
|
|
7
18
|
free_ram = psutil.virtual_memory().available / (1024 * 1024)
|
|
8
19
|
return int(free_ram)
|
|
9
20
|
|
|
10
21
|
|
|
11
22
|
def get_unused_cpu_cores() -> float:
|
|
12
|
-
"""
|
|
23
|
+
"""Estimate currently unused logical CPU capacity in units of CPU cores.
|
|
24
|
+
|
|
25
|
+
On POSIX systems with load average support, this uses the 1-minute load
|
|
26
|
+
average to estimate remaining capacity: max(logical_cores - load1, 0).
|
|
27
|
+
On other systems, it falls back to instantaneous CPU percent usage as
|
|
28
|
+
reported by psutil and computes: logical_cores * (1 - usage/100).
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
float: A non-negative float representing approximate available logical
|
|
32
|
+
CPU cores. For example, 2.5 means roughly two and a half cores free.
|
|
33
|
+
|
|
34
|
+
Notes:
|
|
35
|
+
- The number of logical cores (with SMT/Hyper-Threading) is used.
|
|
36
|
+
- If psutil reports near-zero usage, a small default (0.5%) is assumed
|
|
37
|
+
to avoid transient 0.0 readings.
|
|
38
|
+
- This is a heuristic; short spikes and scheduling nuances may cause
|
|
39
|
+
deviations from actual availability.
|
|
40
|
+
"""
|
|
13
41
|
|
|
14
42
|
cnt = psutil.cpu_count(logical=True) or 1
|
|
15
43
|
|
|
@@ -24,7 +52,20 @@ def get_unused_cpu_cores() -> float:
|
|
|
24
52
|
|
|
25
53
|
|
|
26
54
|
def process_is_active(pid: int) -> bool:
|
|
27
|
-
"""
|
|
55
|
+
"""Check whether a process with the given PID is currently active.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
pid (int): Operating system process identifier (PID).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
bool: True if the process exists and is running; False if it does not
|
|
62
|
+
exist, has exited, or cannot be inspected due to permissions or other
|
|
63
|
+
errors.
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
- Any exception from psutil (e.g., NoSuchProcess, AccessDenied) results
|
|
67
|
+
in a False return value for safety.
|
|
68
|
+
"""
|
|
28
69
|
try:
|
|
29
70
|
process = psutil.Process(pid)
|
|
30
71
|
return process.is_running()
|
|
@@ -33,7 +74,19 @@ def process_is_active(pid: int) -> bool:
|
|
|
33
74
|
|
|
34
75
|
|
|
35
76
|
def get_process_start_time(pid: int) -> int:
|
|
36
|
-
"""
|
|
77
|
+
"""Get the UNIX timestamp of when a process started.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
pid (int): Operating system process identifier (PID).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
int: Start time as a UNIX timestamp (seconds since epoch). Returns 0 if
|
|
84
|
+
the process does not exist or cannot be accessed.
|
|
85
|
+
|
|
86
|
+
Notes:
|
|
87
|
+
- Any exception from psutil (e.g., NoSuchProcess, AccessDenied) results
|
|
88
|
+
in a 0 return value for safety.
|
|
89
|
+
"""
|
|
37
90
|
try:
|
|
38
91
|
process = psutil.Process(pid)
|
|
39
92
|
return int(process.create_time())
|
|
@@ -42,21 +95,41 @@ def get_process_start_time(pid: int) -> int:
|
|
|
42
95
|
|
|
43
96
|
|
|
44
97
|
def get_current_process_id() -> int:
|
|
45
|
-
"""
|
|
98
|
+
"""Get the current process ID (PID).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
int: The PID of the running Python process.
|
|
102
|
+
"""
|
|
46
103
|
return psutil.Process().pid
|
|
47
104
|
|
|
48
105
|
|
|
49
106
|
def get_current_process_start_time() -> int:
|
|
50
|
-
"""
|
|
107
|
+
"""Get the UNIX timestamp for when the current Python process started.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
int: Start time as a UNIX timestamp (seconds since epoch). Returns 0 on
|
|
111
|
+
unexpected error.
|
|
112
|
+
"""
|
|
51
113
|
return get_process_start_time(get_current_process_id())
|
|
52
114
|
|
|
53
115
|
|
|
54
116
|
def get_unused_nvidia_gpus() -> float:
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
For example, 2.0 means
|
|
59
|
-
|
|
117
|
+
"""Estimate the total unused NVIDIA GPU capacity across all devices.
|
|
118
|
+
|
|
119
|
+
This aggregates the per-GPU unused utilization percentage (100 - gpu%) and
|
|
120
|
+
returns the sum in "GPU units". For example, 2.0 means capacity equivalent
|
|
121
|
+
to two fully idle GPUs. If no NVIDIA GPUs are present or NVML is unavailable,
|
|
122
|
+
the function returns 0.0.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
float: Sum of unused GPU capacity across all NVIDIA GPUs in GPU units.
|
|
126
|
+
|
|
127
|
+
Notes:
|
|
128
|
+
- Requires NVIDIA Management Library (pynvml) to be installed and the
|
|
129
|
+
NVIDIA driver to be available.
|
|
130
|
+
- Utilization is based on instantaneous NVML readings and may fluctuate.
|
|
131
|
+
- Any NVML error (e.g., no devices, driver issues) results in 0.0 for
|
|
132
|
+
safety.
|
|
60
133
|
"""
|
|
61
134
|
try:
|
|
62
135
|
pynvml.nvmlInit()
|
|
@@ -70,7 +143,7 @@ def get_unused_nvidia_gpus() -> float:
|
|
|
70
143
|
|
|
71
144
|
return unused_capacity / 100.0
|
|
72
145
|
|
|
73
|
-
except pynvml.NVMLError
|
|
146
|
+
except pynvml.NVMLError:
|
|
74
147
|
# Return 0.0 on any NVML error (no GPUs, driver issues, etc.)
|
|
75
148
|
return 0.0
|
|
76
149
|
finally:
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
"""Singleton class to represent a successful validation."""
|
|
1
|
+
from persidict.singletons import Singleton
|
|
3
2
|
|
|
4
|
-
_instance = None
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
cls._instance = super().__new__(cls)
|
|
9
|
-
return cls._instance
|
|
4
|
+
class ValidationSuccessFlag(Singleton):
|
|
5
|
+
"""Marker singleton indicating that validation has succeeded.
|
|
10
6
|
|
|
7
|
+
This lightweight class is used as a unique sentinel object that signals a
|
|
8
|
+
successful validation outcome in protected code portals. Using a singleton
|
|
9
|
+
avoids ambiguity with other truthy values.
|
|
10
|
+
"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# A canonical, importable singleton value representing a successful validation.
|
|
15
|
+
# Use identity checks (``is VALIDATION_SUCCESSFUL``) rather than equality.
|
|
11
16
|
VALIDATION_SUCCESSFUL = ValidationSuccessFlag()
|
|
@@ -1,48 +1,83 @@
|
|
|
1
|
+
from typing import Final
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
_BASE32_ALPHABET: Final[str] = '0123456789abcdefghijklmnopqrstuv'
|
|
4
|
+
_BASE32_ALPHABET_MAP: Final[dict[str, int]] = {
|
|
5
|
+
char:index for index,char in enumerate(_BASE32_ALPHABET)}
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def convert_base16_to_base32(hexdigest: str) -> str:
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
+
"""Convert a hexadecimal (base16) string to this project's base32.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
hexdigest (str): A hexadecimal string (case-insensitive). May be an
|
|
13
|
+
empty string or "0" to represent zero.
|
|
9
14
|
|
|
10
|
-
:
|
|
11
|
-
|
|
15
|
+
Returns:
|
|
16
|
+
str: The corresponding value encoded with the custom base32 alphabet
|
|
17
|
+
(digits 0-9 then letters a-v).
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
>>> convert_base16_to_base32("ff")
|
|
21
|
+
'7v'
|
|
12
22
|
"""
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
24
|
+
try:
|
|
25
|
+
num = int(hexdigest, 16)
|
|
26
|
+
except ValueError as e:
|
|
27
|
+
raise ValueError(f"Invalid hexadecimal string: {hexdigest}") from e
|
|
28
|
+
|
|
17
29
|
base32_str = convert_int_to_base32(num)
|
|
18
30
|
|
|
19
31
|
return base32_str
|
|
20
32
|
|
|
21
33
|
|
|
22
34
|
def convert_int_to_base32(n: int) -> str:
|
|
23
|
-
"""
|
|
24
|
-
|
|
35
|
+
"""Convert a non-negative integer to Pythagoras' base32 string.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
n (int): Non-negative integer to encode.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: The base32 representation.
|
|
25
42
|
|
|
26
|
-
:
|
|
27
|
-
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If n is negative.
|
|
28
45
|
"""
|
|
46
|
+
if n < 0:
|
|
47
|
+
raise ValueError("n must be non-negative")
|
|
48
|
+
|
|
49
|
+
if n == 0:
|
|
50
|
+
return "0"
|
|
51
|
+
|
|
29
52
|
base32_str = ''
|
|
30
53
|
while n > 0:
|
|
31
|
-
base32_str =
|
|
54
|
+
base32_str = _BASE32_ALPHABET[n & 31] + base32_str
|
|
32
55
|
n >>= 5
|
|
33
56
|
|
|
34
57
|
return base32_str
|
|
35
58
|
|
|
36
59
|
def convert_base_32_to_int(digest: str) -> int:
|
|
37
|
-
"""
|
|
38
|
-
Convert a base 32 string to an integer.
|
|
60
|
+
"""Convert a base32 string (custom alphabet) to an integer.
|
|
39
61
|
|
|
40
|
-
:
|
|
41
|
-
|
|
62
|
+
Args:
|
|
63
|
+
digest (str): String encoded with Pythagoras' base32 alphabet.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
int: The decoded non-negative integer value.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
KeyError: If digest contains a character outside the supported
|
|
70
|
+
base32 alphabet (0-9, a-v).
|
|
42
71
|
"""
|
|
72
|
+
if not digest:
|
|
73
|
+
raise ValueError("Digest cannot be empty")
|
|
74
|
+
|
|
43
75
|
digest = digest.lower()
|
|
44
76
|
num = 0
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
try:
|
|
78
|
+
for char in digest:
|
|
79
|
+
num = num * 32 + _BASE32_ALPHABET_MAP[char]
|
|
80
|
+
except KeyError as e:
|
|
81
|
+
raise ValueError(f"Invalid character '{e.args[0]}' in base32 digest: {digest}") from e
|
|
47
82
|
return num
|
|
48
83
|
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
|
+
from typing import Final
|
|
2
3
|
|
|
4
|
+
_MONTH_ABBREVIATIONS: Final[tuple[str, ...]] = (
|
|
5
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
6
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
|
|
9
|
+
def current_date_gmt_string() -> str:
|
|
10
|
+
"""Get the current UTC date as a compact string.
|
|
11
|
+
|
|
12
|
+
Produces an underscore-delimited UTC date string suitable for
|
|
13
|
+
stable file names and log records.
|
|
14
|
+
|
|
15
|
+
The format is: "YYYY_MMMonAbbrev_dd_utc" (e.g., "2024_12Dec_11_utc").
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: The formatted UTC date string, for the current moment.
|
|
6
19
|
"""
|
|
7
20
|
|
|
8
21
|
utc_now = datetime.now(timezone.utc)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
month_abbrev = _MONTH_ABBREVIATIONS[utc_now.month - 1]
|
|
23
|
+
# locale-dependent month abbreviation
|
|
24
|
+
result = (f"{utc_now.year}_{utc_now.month:02d}{month_abbrev}" +
|
|
25
|
+
f"_{utc_now.day:02d}_utc")
|
|
26
|
+
return result
|
|
@@ -1,33 +1,69 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, Final
|
|
3
3
|
|
|
4
4
|
import joblib.hashing
|
|
5
5
|
|
|
6
6
|
from .base_16_32_convertors import convert_base16_to_base32
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
_HASH_TYPE: Final[str] = "sha256"
|
|
10
|
+
_MAX_SIGNATURE_LENGTH: Final[int] = 22
|
|
11
11
|
|
|
12
12
|
def get_base16_hash_signature(x:Any) -> str:
|
|
13
|
-
"""
|
|
13
|
+
"""Compute a hexadecimal (base16) hash for an arbitrary Python object.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
This function delegates to joblib's hashing utilities. If NumPy is
|
|
16
|
+
imported in the current process, it uses NumpyHasher for efficient and
|
|
17
|
+
stable hashing of NumPy arrays; otherwise it uses the generic Hasher.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
x (Any): The object to hash. Must be picklable by joblib unless a
|
|
21
|
+
specialized routine (e.g., for NumPy arrays) is available.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
str: A hexadecimal string digest computed with the configured
|
|
25
|
+
algorithm (sha256 by default).
|
|
26
|
+
|
|
27
|
+
Notes:
|
|
28
|
+
- joblib relies on pickle for most Python objects; ensure that custom
|
|
29
|
+
objects are picklable for stable results.
|
|
30
|
+
- The digest is deterministic for the same object content.
|
|
17
31
|
"""
|
|
18
32
|
if 'numpy' in sys.modules:
|
|
19
|
-
hasher = joblib.hashing.NumpyHasher(hash_name=
|
|
33
|
+
hasher = joblib.hashing.NumpyHasher(hash_name=_HASH_TYPE)
|
|
20
34
|
else:
|
|
21
|
-
hasher = joblib.hashing.Hasher(hash_name=
|
|
35
|
+
hasher = joblib.hashing.Hasher(hash_name=_HASH_TYPE)
|
|
22
36
|
hash_signature = hasher.hash(x)
|
|
23
37
|
return str(hash_signature)
|
|
24
38
|
|
|
25
39
|
def get_base32_hash_signature(x:Any) -> str:
|
|
26
|
-
"""
|
|
40
|
+
"""Compute a base32-encoded hash for an arbitrary Python object.
|
|
41
|
+
|
|
42
|
+
Internally computes a hexadecimal digest first, then converts it to the
|
|
43
|
+
custom base32 alphabet used by Pythagoras.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
x (Any): The object to hash.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: The full-length base32 digest string (not truncated).
|
|
50
|
+
"""
|
|
27
51
|
base_16_hash = get_base16_hash_signature(x)
|
|
28
52
|
base_32_hash = convert_base16_to_base32(base_16_hash)
|
|
29
53
|
return base_32_hash
|
|
30
54
|
|
|
31
55
|
def get_hash_signature(x:Any) -> str:
|
|
32
|
-
|
|
56
|
+
"""Compute a short, URL-safe hash signature for an object.
|
|
57
|
+
|
|
58
|
+
This is a convenience wrapper that returns the first max_signature_length
|
|
59
|
+
characters of the base32 digest, which is typically sufficient for
|
|
60
|
+
collision-resistant identifiers in logs and filenames.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
x (Any): The object to hash.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: The truncated base32 digest string.
|
|
67
|
+
"""
|
|
68
|
+
return get_base32_hash_signature(x)[:_MAX_SIGNATURE_LENGTH]
|
|
33
69
|
|