pythagoras 0.24.4__py3-none-any.whl → 0.24.7__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/basic_pre_validators.py +130 -15
- pythagoras/_070_protected_code_portals/fn_arg_names_checker.py +20 -18
- 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_decorators.py +59 -1
- pythagoras/_070_protected_code_portals/protected_portal_core_classes.py +239 -4
- pythagoras/_070_protected_code_portals/system_utils.py +85 -12
- pythagoras/_070_protected_code_portals/validation_succesful_const.py +12 -7
- pythagoras/_080_pure_code_portals/pure_core_classes.py +178 -25
- pythagoras/_080_pure_code_portals/pure_decorator.py +37 -0
- pythagoras/_080_pure_code_portals/recursion_pre_validator.py +39 -0
- pythagoras/_090_swarming_portals/output_suppressor.py +32 -3
- pythagoras/_090_swarming_portals/swarming_portals.py +165 -19
- pythagoras/_100_top_level_API/__init__.py +11 -0
- pythagoras/_800_signatures_and_converters/__init__.py +17 -0
- 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/core/__init__.py +54 -0
- {pythagoras-0.24.4.dist-info → pythagoras-0.24.7.dist-info}/METADATA +1 -1
- {pythagoras-0.24.4.dist-info → pythagoras-0.24.7.dist-info}/RECORD +27 -27
- {pythagoras-0.24.4.dist-info → pythagoras-0.24.7.dist-info}/WHEEL +0 -0
|
@@ -3,15 +3,21 @@ from typing import List, Set
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def check_if_fn_accepts_args(required_arg_names: List[str]|Set[str], fn: str) -> bool:
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"""Determine whether a function can accept specific keyword argument names.
|
|
7
|
+
|
|
8
|
+
Analyzes the source code of a single Python function and checks whether
|
|
9
|
+
all required names could be passed as keyword arguments.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Args:
|
|
12
|
+
required_arg_names: Iterable of parameter names that must be accepted.
|
|
13
|
+
fn: Source code string containing exactly one function definition.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
Returns:
|
|
16
|
+
True if the function has **kwargs or explicitly defines all required names
|
|
17
|
+
as keyword-acceptable parameters; otherwise False.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If no function definition is found or if multiple functions are present.
|
|
15
21
|
"""
|
|
16
22
|
|
|
17
23
|
tree = ast.parse(fn)
|
|
@@ -19,6 +25,8 @@ def check_if_fn_accepts_args(required_arg_names: List[str]|Set[str], fn: str) ->
|
|
|
19
25
|
func_def_nodes = [node for node in tree.body if isinstance(node, ast.FunctionDef)]
|
|
20
26
|
if not func_def_nodes:
|
|
21
27
|
raise ValueError("No function definition found in the provided source code.")
|
|
28
|
+
if not len(func_def_nodes) == 1:
|
|
29
|
+
raise ValueError("Multiple function definitions found in the provided source code.")
|
|
22
30
|
func_def = func_def_nodes[0]
|
|
23
31
|
args = func_def.args
|
|
24
32
|
|
|
@@ -26,14 +34,8 @@ def check_if_fn_accepts_args(required_arg_names: List[str]|Set[str], fn: str) ->
|
|
|
26
34
|
if args.kwarg is not None:
|
|
27
35
|
return True
|
|
28
36
|
|
|
29
|
-
# Collect all explicitly named parameters
|
|
30
|
-
|
|
31
|
-
for arg in args.args
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
param_names.add(kwarg.arg)
|
|
35
|
-
|
|
36
|
-
for name in required_arg_names:
|
|
37
|
-
if name not in param_names:
|
|
38
|
-
return False
|
|
39
|
-
return True
|
|
37
|
+
# Collect all explicitly named parameters (excluding positional-only)
|
|
38
|
+
# Note: args.posonlyargs are NOT included as they cannot be passed by keyword
|
|
39
|
+
param_names = {arg.arg for arg in args.args + args.kwonlyargs}
|
|
40
|
+
|
|
41
|
+
return set(required_arg_names).issubset(param_names)
|
|
@@ -1,12 +1,50 @@
|
|
|
1
|
+
from typing import List, Any
|
|
1
2
|
|
|
2
3
|
|
|
3
|
-
def flatten_list(nested_list):
|
|
4
|
-
"""Flatten a list
|
|
5
|
-
|
|
4
|
+
def flatten_list(nested_list: List[Any]) -> List[Any]:
|
|
5
|
+
"""Flatten a nested list into a single-level list.
|
|
6
|
+
|
|
7
|
+
This function flattens lists of arbitrary depth using an iterative
|
|
8
|
+
(non-recursive) algorithm. Only values of type ``list`` are treated as
|
|
9
|
+
containers to be expanded. Other iterable types (e.g., tuples, sets,
|
|
10
|
+
strings, generators) are considered atomic values and are not traversed.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
- nested_list: list
|
|
14
|
+
A possibly nested Python list. Must be an instance of ``list``.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
- list
|
|
18
|
+
A new list containing all elements from ``nested_list`` in their
|
|
19
|
+
original left-to-right order, but with one level of nesting (flat).
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
- TypeError: If ``nested_list`` is not a ``list`` instance.
|
|
23
|
+
|
|
24
|
+
Notes:
|
|
25
|
+
- Preserves the order of elements.
|
|
26
|
+
- Supports unlimited nesting depth without recursion.
|
|
27
|
+
- Cyclic references (e.g., a list that contains itself, directly or
|
|
28
|
+
indirectly) will lead to an infinite loop.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> flatten_list([1, [2, 3, [4]], 5])
|
|
32
|
+
[1, 2, 3, 4, 5]
|
|
33
|
+
>>> flatten_list([["a", ["b"]], "c"])
|
|
34
|
+
['a', 'b', 'c']
|
|
35
|
+
>>> flatten_list([(1, 2), [3, 4]])
|
|
36
|
+
[(1, 2), 3, 4]
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(nested_list, list):
|
|
39
|
+
raise TypeError(f"Expected list, got {type(nested_list).__name__}")
|
|
6
40
|
flattened = []
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
41
|
+
stack = [nested_list]
|
|
42
|
+
|
|
43
|
+
while stack:
|
|
44
|
+
current = stack.pop()
|
|
45
|
+
if isinstance(current, list):
|
|
46
|
+
stack.extend(reversed(current))
|
|
10
47
|
else:
|
|
11
|
-
flattened.append(
|
|
48
|
+
flattened.append(current)
|
|
49
|
+
|
|
12
50
|
return flattened
|
|
@@ -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
|
+
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Decorators for building protected functions.
|
|
2
|
+
|
|
3
|
+
This module provides the protected decorator which wraps callables
|
|
4
|
+
into ProtectedFn objects. A ProtectedFn coordinates pre- and post-execution
|
|
5
|
+
validation using ValidatorFn instances and executes within a
|
|
6
|
+
ProtectedCodePortal context.
|
|
7
|
+
|
|
8
|
+
The decorator is a thin, declarative layer over the underlying core classes,
|
|
9
|
+
allowing you to attach validators at definition
|
|
10
|
+
time while keeping function logic clean and focused.
|
|
11
|
+
"""
|
|
2
12
|
|
|
3
13
|
from typing import Callable, Any
|
|
4
14
|
|
|
@@ -8,6 +18,27 @@ from .protected_portal_core_classes import *
|
|
|
8
18
|
from persidict import Joker, KEEP_CURRENT
|
|
9
19
|
|
|
10
20
|
class protected(autonomous):
|
|
21
|
+
"""Decorator for protected functions with pre/post validation.
|
|
22
|
+
|
|
23
|
+
This decorator wraps a target callable into a ProtectedFn that enforces
|
|
24
|
+
a sequence of pre- and post-execution validators. It builds on the
|
|
25
|
+
autonomous decorator, adding validator support to it.
|
|
26
|
+
|
|
27
|
+
Typical usage:
|
|
28
|
+
@protected(pre_validators=[...], post_validators=[...])
|
|
29
|
+
def fn(...):
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
See Also:
|
|
33
|
+
ProtectedFn: The runtime wrapper that performs validation and execution.
|
|
34
|
+
ProtectedCodePortal: Portal coordinating protected function execution.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
_pre_validators (list[ValidatorFn] | None): Validators executed before
|
|
38
|
+
the target function.
|
|
39
|
+
_post_validators (list[ValidatorFn] | None): Validators executed after
|
|
40
|
+
the target function.
|
|
41
|
+
"""
|
|
11
42
|
|
|
12
43
|
_pre_validators: list[ValidatorFn] | None
|
|
13
44
|
_post_validators: list[ValidatorFn] | None
|
|
@@ -19,6 +50,24 @@ class protected(autonomous):
|
|
|
19
50
|
, excessive_logging: bool|Joker = KEEP_CURRENT
|
|
20
51
|
, portal: ProtectedCodePortal | None = None
|
|
21
52
|
):
|
|
53
|
+
"""Initialize the protected decorator.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
pre_validators (list[ValidatorFn] | None): Pre-execution validators
|
|
57
|
+
to apply. Each item is either a ValidatorFn or a callable that
|
|
58
|
+
can be wrapped into a PreValidatorFn by ProtectedFn.
|
|
59
|
+
post_validators (list[ValidatorFn] | None): Post-execution validators
|
|
60
|
+
to apply. Each item is either a ValidatorFn or a callable that
|
|
61
|
+
can be wrapped into a PostValidatorFn by ProtectedFn.
|
|
62
|
+
fixed_kwargs (dict[str, Any] | None): Keyword arguments to pre-bind
|
|
63
|
+
to the wrapped function for every call.
|
|
64
|
+
excessive_logging (bool | Joker): Enables verbose logging for the
|
|
65
|
+
wrapped function and its validators. Use KEEP_CURRENT to inherit
|
|
66
|
+
the current setting from the portal/context.
|
|
67
|
+
portal (ProtectedCodePortal | None): Optional portal instance to
|
|
68
|
+
bind the wrapped function to. If None, a suitable portal will be
|
|
69
|
+
inferred when fuction is called.
|
|
70
|
+
"""
|
|
22
71
|
assert isinstance(portal, ProtectedCodePortal) or portal is None
|
|
23
72
|
assert isinstance(fixed_kwargs, dict) or fixed_kwargs is None
|
|
24
73
|
autonomous.__init__(self=self
|
|
@@ -30,6 +79,15 @@ class protected(autonomous):
|
|
|
30
79
|
|
|
31
80
|
|
|
32
81
|
def __call__(self, fn: Callable|str) -> ProtectedFn:
|
|
82
|
+
"""Wrap the given function into a ProtectedFn.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
fn (Callable | str): The target function or its source code string.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
ProtectedFn: A wrapper that performs pre/post validation and then
|
|
89
|
+
executes the function.
|
|
90
|
+
"""
|
|
33
91
|
wrapper = ProtectedFn(fn
|
|
34
92
|
, portal=self._portal
|
|
35
93
|
, pre_validators=self._pre_validators
|