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.
@@ -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 _install_uv_and_pip() -> None:
9
- global _uv_and_pip_installation_needed
10
- if not _uv_and_pip_installation_needed:
11
- return
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
- if package_name == "pip":
32
- assert use_uv
33
- elif package_name == "uv":
34
- assert not use_uv
35
- else:
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
- subprocess.run(command, check=True, stdout=subprocess.PIPE
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
- assert package_name not in ["pip", "uv"]
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
- subprocess.run(command, check=True, stdout=subprocess.PIPE
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
- except:
146
+ raise RuntimeError(
147
+ f"Package '{package_name}' still importable after uninstallation")
148
+ except ModuleNotFoundError:
72
149
  pass
73
- else:
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
- """Returns the amount of available RAM in MB. """
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
- """Returns the free (logical) CPU capacity"""
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
- """Checks if a process with the given PID is active."""
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
- """Returns the start time of the process with the given PID."""
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
- """Returns the current process ID."""
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
- """Returns the start time of the current process."""
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
- """Returns the total unused GPU capacity as a float value.
56
-
57
- The function calculates the sum of unused capacity across all available NVIDIA GPUs.
58
- For example, 2.0 means two completely unused GPUs
59
- 0 is returned if no GPUs or on error.
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 as e:
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
- class ValidationSuccessFlag:
2
- """Singleton class to represent a successful validation."""
1
+ from persidict.singletons import Singleton
3
2
 
4
- _instance = None
5
3
 
6
- def __new__(cls):
7
- if cls._instance is None:
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
- base32_alphabet = '0123456789abcdefghijklmnopqrstuv'
3
- base32_alphabet_map = {char:index for index,char in enumerate(base32_alphabet)}
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
- Convert a hexadecimal (base 16) string to a base 32 string.
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
- :name hexdigest: A string representing a hexadecimal number.
11
- :return: A string representing the equivalent number in base 32.
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
- if not hexdigest:
15
- return '0'
16
- num = int(hexdigest,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
- Convert an integer to a base 32 string.
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
- :name n: An integer.
27
- :return: A string representing the equivalent number in base 32.
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 = base32_alphabet[n & 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
- :name digest: A string representing a number in base 32.
41
- :return: An integer representing the equivalent number.
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
- for char in digest:
46
- num = num * 32 + base32_alphabet_map[char]
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
- def current_date_gmt_string():
5
- """ Return the current date and time in the format '2024_12Jan_22_utc'
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
- date_str = utc_now.strftime("%Y_%m%b_%d_utc")
10
-
11
- return date_str
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
- hash_type: str = "sha256"
10
- max_signature_length: int = 22
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
- """Return base16 hash signature of an object.
13
+ """Compute a hexadecimal (base16) hash for an arbitrary Python object.
14
14
 
15
- Uses joblib's Hasher (or NumpyHasher). It uses Pickle for serialization,
16
- except for NumPy arrays, which use optimized custom routines.
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=hash_type)
33
+ hasher = joblib.hashing.NumpyHasher(hash_name=_HASH_TYPE)
20
34
  else:
21
- hasher = joblib.hashing.Hasher(hash_name=hash_type)
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
- """Return base32 hash signature of an object"""
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
- return get_base32_hash_signature(x)[:max_signature_length]
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