pythagoras 0.24.2__py3-none-any.whl → 0.24.4__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/.DS_Store +0 -0
- pythagoras/_010_basic_portals/__init__.py +0 -1
- pythagoras/_010_basic_portals/basic_portal_core_classes.py +2 -14
- pythagoras/_020_ordinary_code_portals/code_normalizer.py +37 -9
- pythagoras/_020_ordinary_code_portals/function_processing.py +58 -15
- pythagoras/_020_ordinary_code_portals/ordinary_decorator.py +14 -0
- pythagoras/_020_ordinary_code_portals/ordinary_portal_core_classes.py +196 -24
- pythagoras/_030_data_portals/data_portal_core_classes.py +74 -22
- pythagoras/_030_data_portals/ready_and_get.py +45 -4
- pythagoras/_030_data_portals/storable_decorator.py +18 -1
- pythagoras/_040_logging_code_portals/exception_processing_tracking.py +30 -2
- pythagoras/_040_logging_code_portals/execution_environment_summary.py +60 -24
- pythagoras/_040_logging_code_portals/kw_args.py +74 -12
- pythagoras/_040_logging_code_portals/logging_decorator.py +23 -1
- pythagoras/_040_logging_code_portals/logging_portal_core_classes.py +368 -15
- pythagoras/_040_logging_code_portals/notebook_checker.py +9 -1
- pythagoras/_040_logging_code_portals/uncaught_exceptions.py +40 -0
- pythagoras/_050_safe_code_portals/safe_decorator.py +27 -1
- pythagoras/_050_safe_code_portals/safe_portal_core_classes.py +87 -11
- pythagoras/_090_swarming_portals/swarming_portals.py +4 -6
- {pythagoras-0.24.2.dist-info → pythagoras-0.24.4.dist-info}/METADATA +3 -3
- {pythagoras-0.24.2.dist-info → pythagoras-0.24.4.dist-info}/RECORD +23 -24
- pythagoras/_010_basic_portals/not_picklable_class.py +0 -36
- {pythagoras-0.24.2.dist-info → pythagoras-0.24.4.dist-info}/WHEEL +0 -0
|
@@ -14,10 +14,7 @@ from .._800_signatures_and_converters import get_hash_signature
|
|
|
14
14
|
from .._010_basic_portals.basic_portal_core_classes import (
|
|
15
15
|
_describe_persistent_characteristic
|
|
16
16
|
, _describe_runtime_characteristic)
|
|
17
|
-
from .._020_ordinary_code_portals import
|
|
18
|
-
get_normalized_function_source
|
|
19
|
-
,OrdinaryCodePortal
|
|
20
|
-
,OrdinaryFn)
|
|
17
|
+
from .._020_ordinary_code_portals import OrdinaryCodePortal ,OrdinaryFn
|
|
21
18
|
from persidict import WriteOnceDict
|
|
22
19
|
|
|
23
20
|
TOTAL_VALUES_TXT = "Values, total"
|
|
@@ -25,36 +22,63 @@ PROBABILITY_OF_CHECKS_TXT = "Probability of consistency checks"
|
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
def get_active_data_portal() -> DataPortal:
|
|
25
|
+
"""Return the currently active DataPortal.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
DataPortal: The portal that is active in the current context ("with" block).
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
AssertionError: If the active portal is not an instance of DataPortal.
|
|
32
|
+
"""
|
|
28
33
|
portal = get_active_portal()
|
|
29
34
|
assert isinstance(portal, DataPortal)
|
|
30
35
|
return portal
|
|
31
36
|
|
|
32
37
|
|
|
33
38
|
def get_nonactive_data_portals() -> list[DataPortal]:
|
|
34
|
-
"""
|
|
39
|
+
"""Return all known DataPortals that are not currently active.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
list[DataPortal]: A list of DataPortal instances that are available to
|
|
43
|
+
the runtime but are not the current active portal stack.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
AssertionError: If any returned portal is not an instance of DataPortal.
|
|
47
|
+
"""
|
|
35
48
|
portals = get_nonactive_portals()
|
|
36
49
|
assert all(isinstance(p, DataPortal) for p in portals)
|
|
37
50
|
return portals
|
|
38
51
|
|
|
39
52
|
|
|
40
53
|
class DataPortal(OrdinaryCodePortal):
|
|
41
|
-
"""A portal that persistently stores values.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
"""A portal that persistently stores and retrieves immutable values.
|
|
55
|
+
|
|
56
|
+
A DataPortal is responsible for addressing, storing, and retrieving
|
|
57
|
+
values by their content-derived addresses. It exposes a context manager
|
|
58
|
+
interface so that code running within a "with portal:" block treats that
|
|
59
|
+
portal as the active one.
|
|
60
|
+
|
|
61
|
+
Behavior overview:
|
|
62
|
+
- Content-addressed storage: immutable values are referenced by a
|
|
63
|
+
HashAddr/ValueAddr that is derived from the value's bytes and a
|
|
64
|
+
human-readable descriptor.
|
|
65
|
+
- Transparent fetch and replication: if a value is not present in the
|
|
66
|
+
active portal but exists in any other known portal, it is fetched
|
|
67
|
+
and copied into the active portal on demand.
|
|
68
|
+
- Config settings: portal-specific and function-specific settings are
|
|
69
|
+
persisted in a dedicated config store.
|
|
70
|
+
- Consistency checks: the underlying persistent dictionary can perform
|
|
71
|
+
random, probabilistic consistency checks controlled by the
|
|
72
|
+
p_consistency_checks parameter.
|
|
73
|
+
|
|
74
|
+
Note:
|
|
75
|
+
Use the portal as a context manager whenever code performs I/O with
|
|
76
|
+
the portal (reading or storing values):
|
|
45
77
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
the value will be automatically copied to the current portal.
|
|
50
|
-
|
|
51
|
-
A portal can serve as a context manager, enabling the use of the
|
|
52
|
-
'with' statement to support portal-aware code blocks. If some code is
|
|
53
|
-
supposed to explicitly read anything from (or save to) a portal,
|
|
54
|
-
it should be wrapped in a 'with' statement that
|
|
55
|
-
marks the portal as active for the duration of the code block.
|
|
78
|
+
with portal:
|
|
79
|
+
addr = ValueAddr(data)
|
|
80
|
+
value = addr.get()
|
|
56
81
|
|
|
57
|
-
DataPortal also supports random consistency checks.
|
|
58
82
|
"""
|
|
59
83
|
|
|
60
84
|
_value_store: WriteOnceDict | None
|
|
@@ -67,6 +91,18 @@ class DataPortal(OrdinaryCodePortal):
|
|
|
67
91
|
, root_dict: PersiDict|str|None = None
|
|
68
92
|
, p_consistency_checks: float|Joker = KEEP_CURRENT
|
|
69
93
|
):
|
|
94
|
+
"""Initialize a DataPortal.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
root_dict: Prototype PersiDict or a path/URI used to create
|
|
98
|
+
a persistent dictionary for internal stores. If None, uses
|
|
99
|
+
the parent's default.
|
|
100
|
+
p_consistency_checks: Probability in [0, 1] or KEEP_CURRENT Joker
|
|
101
|
+
that controls random consistency checks of the value store.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If p_consistency_checks is not in [0, 1] and not a Joker.
|
|
105
|
+
"""
|
|
70
106
|
OrdinaryCodePortal.__init__(self, root_dict = root_dict)
|
|
71
107
|
del root_dict
|
|
72
108
|
self._auxiliary_config_params_at_init = dict()
|
|
@@ -102,14 +138,24 @@ class DataPortal(OrdinaryCodePortal):
|
|
|
102
138
|
|
|
103
139
|
|
|
104
140
|
def _post_init_hook(self) -> None:
|
|
105
|
-
"""
|
|
141
|
+
"""Finalize initialization after __init__ completes across the MRO.
|
|
142
|
+
|
|
143
|
+
Ensures that auxiliary configuration parameters are persisted and that
|
|
144
|
+
the value store is configured according to the portal's
|
|
145
|
+
p_consistency_checks setting.
|
|
146
|
+
"""
|
|
106
147
|
super()._post_init_hook()
|
|
107
148
|
self._persist_initial_config_params()
|
|
108
149
|
self._value_store.p_consistency_checks = self.p_consistency_checks
|
|
109
150
|
|
|
110
151
|
|
|
111
152
|
def get_params(self) -> dict:
|
|
112
|
-
"""
|
|
153
|
+
"""Return the portal's configuration parameters.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
dict: A sorted dictionary of base parameters augmented with
|
|
157
|
+
auxiliary config entries defined at initialization.
|
|
158
|
+
"""
|
|
113
159
|
params = super().get_params()
|
|
114
160
|
params.update(self._auxiliary_config_params_at_init)
|
|
115
161
|
sorted_params = sort_dict_by_keys(params)
|
|
@@ -201,6 +247,12 @@ class StorableFn(OrdinaryFn):
|
|
|
201
247
|
, fn: Callable | str
|
|
202
248
|
, portal: DataPortal | None = None
|
|
203
249
|
):
|
|
250
|
+
"""Create a storable wrapper around an ordinary function.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
fn: A Python function or its source code (normalized) as a string.
|
|
254
|
+
portal: Optional DataPortal to bind the function to.
|
|
255
|
+
"""
|
|
204
256
|
OrdinaryFn.__init__(self, fn=fn, portal=portal)
|
|
205
257
|
self._auxiliary_config_params_at_init = dict()
|
|
206
258
|
|
|
@@ -5,11 +5,32 @@ from .data_portal_core_classes import HashAddr
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def ready(obj) -> bool:
|
|
8
|
-
"""
|
|
8
|
+
"""Check readiness of a possibly nested data structure.
|
|
9
|
+
|
|
10
|
+
Recursively inspects the structure and returns True only if every
|
|
11
|
+
HashAddr/ValueAddr contained is ready. Strings and bytes-like objects are
|
|
12
|
+
treated as atomic. Lists, tuples, and dicts are traversed recursively.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
obj: Any Python object, possibly a nested combination of lists, tuples,
|
|
16
|
+
and dicts containing HashAddr/ValueAddr objects.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
bool: True if all contained addresses are ready; False otherwise.
|
|
20
|
+
"""
|
|
9
21
|
return _ready_impl(obj)
|
|
10
22
|
|
|
11
23
|
def _ready_impl(obj, seen=None):
|
|
12
|
-
"""
|
|
24
|
+
"""Implementation helper for ready() with cycle protection.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
obj: Object under inspection.
|
|
28
|
+
seen: A set of object ids already visited to prevent infinite loops
|
|
29
|
+
on cyclic graphs.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if obj and all nested addresses are ready.
|
|
33
|
+
"""
|
|
13
34
|
if seen is None:
|
|
14
35
|
seen = set()
|
|
15
36
|
elif id(obj) in seen:
|
|
@@ -36,12 +57,32 @@ def _ready_impl(obj, seen=None):
|
|
|
36
57
|
|
|
37
58
|
|
|
38
59
|
def get(obj:Any) -> Any:
|
|
39
|
-
"""
|
|
60
|
+
"""Deep-copy a structure, replacing HashAddr/ValueAddr with values.
|
|
61
|
+
|
|
62
|
+
Traverses the structure and replaces every address with the value
|
|
63
|
+
obtained via .get(), preserving container topology. Handles cycles by
|
|
64
|
+
memoizing objects during traversal.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
obj: Any Python object or nested structure containing addresses.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Any: A copy of obj with all addresses resolved into concrete values.
|
|
71
|
+
"""
|
|
40
72
|
return _get_impl(obj)
|
|
41
73
|
|
|
42
74
|
|
|
43
75
|
def _get_impl(obj:Any, seen=None)->Any:
|
|
44
|
-
"""
|
|
76
|
+
"""Implementation helper for get() with cycle protection and memoization.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
obj: Object to resolve.
|
|
80
|
+
seen: A dict mapping visited object ids to their resolved copies,
|
|
81
|
+
used to preserve identities and break cycles.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Any: The resolved object or a structure containing resolved values.
|
|
85
|
+
"""
|
|
45
86
|
if seen is None:
|
|
46
87
|
seen = dict()
|
|
47
88
|
|
|
@@ -4,15 +4,32 @@ from .._020_ordinary_code_portals import ordinary
|
|
|
4
4
|
from .data_portal_core_classes import DataPortal, StorableFn
|
|
5
5
|
|
|
6
6
|
class storable(ordinary):
|
|
7
|
-
"""
|
|
7
|
+
"""Decorator that converts a Python function into a StorableFn.
|
|
8
|
+
|
|
9
|
+
When applied, the function is wrapped into a StorableFn that can be
|
|
10
|
+
addressed and stored in a DataPortal.
|
|
8
11
|
"""
|
|
9
12
|
|
|
10
13
|
def __init__(self
|
|
11
14
|
, portal: DataPortal | None = None):
|
|
15
|
+
"""Create a storable decorator bound to an optional DataPortal.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
portal: The DataPortal to use as default when wrapping functions.
|
|
19
|
+
If None, the active portal at call time will be used.
|
|
20
|
+
"""
|
|
12
21
|
assert isinstance(portal, DataPortal) or portal is None
|
|
13
22
|
ordinary.__init__(self=self, portal=portal)
|
|
14
23
|
|
|
15
24
|
def __call__(self,fn:Callable)->StorableFn:
|
|
25
|
+
"""Wrap the given function into a StorableFn.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
fn: The ordinary Python function to wrap.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
StorableFn: A storable function bound to the configured portal.
|
|
32
|
+
"""
|
|
16
33
|
wrapper = StorableFn(fn
|
|
17
34
|
, portal=self._portal)
|
|
18
35
|
return wrapper
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
def _exception_needs_to_be_processed(exc_type, exc_value, trace_back) -> bool:
|
|
2
|
-
"""
|
|
2
|
+
"""Determine whether an exception should be logged by Pythagoras.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
exc_type: The exception class (type of the raised exception). May be None.
|
|
6
|
+
exc_value: The exception instance.
|
|
7
|
+
trace_back: The traceback object associated with the exception.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
bool: True if the exception has not yet been marked as processed by
|
|
11
|
+
Pythagoras and should be logged; False otherwise.
|
|
12
|
+
|
|
13
|
+
Notes:
|
|
14
|
+
Pythagoras marks exceptions it already handled to avoid duplicate
|
|
15
|
+
logging. This function checks for such marks using either
|
|
16
|
+
Exception.add_note (Python 3.11+) or a fallback attribute.
|
|
17
|
+
"""
|
|
3
18
|
if exc_type is None:
|
|
4
19
|
return False
|
|
5
20
|
|
|
@@ -16,7 +31,20 @@ def _exception_needs_to_be_processed(exc_type, exc_value, trace_back) -> bool:
|
|
|
16
31
|
|
|
17
32
|
|
|
18
33
|
def _mark_exception_as_processed(exc_type, exc_value, trace_back) -> None:
|
|
19
|
-
"""Mark
|
|
34
|
+
"""Mark an exception as already processed by Pythagoras.
|
|
35
|
+
|
|
36
|
+
This mark prevents duplicate logging of the same exception by subsequent
|
|
37
|
+
handlers.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
exc_type: The exception class (type of the raised exception). Unused.
|
|
41
|
+
exc_value: The exception instance to mark.
|
|
42
|
+
trace_back: The traceback object associated with the exception. Unused.
|
|
43
|
+
|
|
44
|
+
Side Effects:
|
|
45
|
+
- Mutates the exception by adding a note (preferred) or by setting
|
|
46
|
+
an attribute `__suppress_pythagoras_logging__ = True`.
|
|
47
|
+
"""
|
|
20
48
|
if hasattr(exc_value, "add_note"):
|
|
21
49
|
exc_value.add_note(
|
|
22
50
|
"__suppress_pythagoras_logging__")
|
|
@@ -9,46 +9,82 @@ from .notebook_checker import is_executed_in_notebook
|
|
|
9
9
|
from .._010_basic_portals import BasicPortal
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def build_execution_environment_summary()-> Dict:
|
|
13
|
-
"""
|
|
12
|
+
def build_execution_environment_summary() -> Dict:
|
|
13
|
+
"""Build a snapshot of the current execution environment.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Gathers system, process, and Python runtime metadata useful for diagnosing
|
|
16
|
+
issues, particularly in distributed or long-running applications.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dict: A dictionary containing environment details such as hostname,
|
|
20
|
+
user, process ID, OS/platform, Python version, CPU/memory stats,
|
|
21
|
+
working directory, local timezone, and whether execution is inside a
|
|
22
|
+
Jupyter notebook.
|
|
17
23
|
"""
|
|
18
24
|
cwd = os.getcwd()
|
|
19
25
|
|
|
20
26
|
execution_environment_summary = dict(
|
|
21
|
-
hostname
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
hostname=socket.gethostname(),
|
|
28
|
+
user=getuser(),
|
|
29
|
+
pid=os.getpid(),
|
|
30
|
+
platform=platform.platform(),
|
|
31
|
+
python_implementation=platform.python_implementation(),
|
|
32
|
+
python_version=platform.python_version(),
|
|
33
|
+
processor=platform.processor(),
|
|
34
|
+
cpu_count=psutil.cpu_count(),
|
|
35
|
+
cpu_load_avg=psutil.getloadavg(),
|
|
36
|
+
# cuda_gpu_count=torch.cuda.device_count(),
|
|
37
|
+
disk_usage=psutil.disk_usage(cwd),
|
|
38
|
+
virtual_memory=psutil.virtual_memory(),
|
|
39
|
+
working_directory=cwd,
|
|
40
|
+
local_timezone=datetime.now().astimezone().tzname(),
|
|
41
|
+
is_in_notebook=is_executed_in_notebook(),
|
|
42
|
+
)
|
|
37
43
|
|
|
38
44
|
return execution_environment_summary
|
|
39
45
|
|
|
40
|
-
def make_unique_name(suggested_name:str, existing_names) -> str:
|
|
41
|
-
"""
|
|
46
|
+
def make_unique_name(suggested_name: str, existing_names) -> str:
|
|
47
|
+
"""Return a unique name based on the suggestion and a set of existing names.
|
|
48
|
+
|
|
49
|
+
If the suggested name already exists, appends a random numeric
|
|
50
|
+
suffix until a unique candidate is found.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
suggested_name: Preferred base name.
|
|
54
|
+
existing_names: A collection supporting membership check (e.g., dict,
|
|
55
|
+
set, list) used to determine collisions.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: A name guaranteed to not be present in existing_names at the
|
|
59
|
+
time of checking.
|
|
60
|
+
"""
|
|
42
61
|
candidate = suggested_name
|
|
43
62
|
entropy_infuser = BasicPortal._entropy_infuser
|
|
44
63
|
while candidate in existing_names:
|
|
45
64
|
candidate = suggested_name + "_"
|
|
46
|
-
random_number = entropy_infuser.randint(1,10_000_000_000)
|
|
65
|
+
random_number = entropy_infuser.randint(1, 10_000_000_000)
|
|
47
66
|
candidate += str(random_number)
|
|
48
67
|
return candidate
|
|
49
68
|
|
|
50
69
|
def add_execution_environment_summary(*args, **kwargs):
|
|
51
|
-
"""
|
|
70
|
+
"""Augment keyword arguments with an execution environment summary.
|
|
71
|
+
|
|
72
|
+
Optionally also adds positional messages under a unique `message_list` key
|
|
73
|
+
when args are provided. This is primarily used to enrich logged events with
|
|
74
|
+
contextual runtime information.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
*args: Optional messages or payloads to attach under `message_list`.
|
|
78
|
+
**kwargs: The keyword arguments dictionary to be augmented. Mutated in
|
|
79
|
+
place by adding a unique key for the environment summary.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict: The same kwargs dict, with additional keys:
|
|
83
|
+
- execution_environment_summary*: A unique key containing the env
|
|
84
|
+
summary built by build_execution_environment_summary().
|
|
85
|
+
- message_list*: A unique key containing the provided args (if any).
|
|
86
|
+
Asterisks denote keys may be suffixed to ensure uniqueness.
|
|
87
|
+
"""
|
|
52
88
|
context_param_name = "execution_environment_summary"
|
|
53
89
|
context_param_name = make_unique_name(
|
|
54
90
|
suggested_name=context_param_name, existing_names=kwargs)
|
|
@@ -6,24 +6,37 @@ from .._030_data_portals import DataPortal, ValueAddr
|
|
|
6
6
|
from parameterizable import sort_dict_by_keys
|
|
7
7
|
|
|
8
8
|
class KwArgs(dict):
|
|
9
|
-
"""
|
|
9
|
+
"""Container for keyword arguments with deterministic ordering and packing.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
and
|
|
13
|
-
|
|
14
|
-
for the same lists of arguments.
|
|
11
|
+
Provides utilities to sort keys deterministically and to pack/unpack values
|
|
12
|
+
to and from ValueAddr instances so that argument sets can be compared and
|
|
13
|
+
hashed reliably across runs.
|
|
15
14
|
"""
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def __init__(self, *args, **kargs):
|
|
18
|
+
"""Create a KwArgs mapping with deterministically sorted keys.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
*args: Positional arguments accepted by dict().
|
|
22
|
+
**kargs: Keyword arguments accepted by dict().
|
|
23
|
+
"""
|
|
19
24
|
dict.__init__(self)
|
|
20
25
|
tmp_dict = dict(*args, **kargs)
|
|
21
26
|
tmp_dict = sort_dict_by_keys(tmp_dict)
|
|
22
27
|
self.update(tmp_dict)
|
|
23
28
|
|
|
24
29
|
|
|
25
|
-
def sort(self, inplace:bool) -> KwArgs:
|
|
26
|
-
"""
|
|
30
|
+
def sort(self, inplace: bool) -> KwArgs:
|
|
31
|
+
"""Return a version with keys sorted, optionally in place.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
inplace: If True, sorts this instance and returns it. If False,
|
|
35
|
+
returns a new KwArgs instance with sorted keys.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
KwArgs: The sorted KwArgs (self when inplace=True, otherwise a new instance).
|
|
39
|
+
"""
|
|
27
40
|
if inplace:
|
|
28
41
|
sorted_dict = sort_dict_by_keys(self)
|
|
29
42
|
self.clear()
|
|
@@ -34,7 +47,18 @@ class KwArgs(dict):
|
|
|
34
47
|
|
|
35
48
|
|
|
36
49
|
def __setitem__(self, key, value):
|
|
37
|
-
"""
|
|
50
|
+
"""Set an item enforcing KwArgs invariants.
|
|
51
|
+
|
|
52
|
+
Enforces that keys are strings and values are not KwArgs themselves.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
key: The key to set; must be a str.
|
|
56
|
+
value: The value to associate with the key.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
KeyError: If the key is not a string.
|
|
60
|
+
ValueError: If the value is a KwArgs (nested KwArgs are disallowed).
|
|
61
|
+
"""
|
|
38
62
|
if not isinstance(key, str):
|
|
39
63
|
raise KeyError("Keys must be strings in KwArgs.")
|
|
40
64
|
if isinstance(value, KwArgs):
|
|
@@ -43,15 +67,24 @@ class KwArgs(dict):
|
|
|
43
67
|
|
|
44
68
|
|
|
45
69
|
def __reduce__(self):
|
|
46
|
-
"""
|
|
70
|
+
"""Support pickling by sorting keys first for stable serialization.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
tuple: Standard pickle reduce tuple from dict.__reduce__().
|
|
74
|
+
"""
|
|
47
75
|
self.sort(inplace=True)
|
|
48
76
|
return super().__reduce__()
|
|
49
77
|
|
|
50
78
|
|
|
51
79
|
def unpack(self) -> UnpackedKwArgs:
|
|
52
|
-
"""
|
|
80
|
+
"""Return a copy with all ValueAddr values resolved to raw values.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
UnpackedKwArgs: A new mapping where each ValueAddr is replaced with
|
|
84
|
+
its underlying value via ValueAddr.get().
|
|
85
|
+
"""
|
|
53
86
|
unpacked_copy = dict()
|
|
54
|
-
for k,v in self.items():
|
|
87
|
+
for k, v in self.items():
|
|
55
88
|
if isinstance(v, ValueAddr):
|
|
56
89
|
unpacked_copy[k] = v.get()
|
|
57
90
|
else:
|
|
@@ -61,7 +94,20 @@ class KwArgs(dict):
|
|
|
61
94
|
|
|
62
95
|
|
|
63
96
|
def pack(self, store = True) -> PackedKwArgs:
|
|
64
|
-
"""
|
|
97
|
+
"""Pack values into ValueAddr handles, optionally storing them.
|
|
98
|
+
|
|
99
|
+
Each argument value is replaced by a ValueAddr pointing to either the
|
|
100
|
+
stored value (when store=True) or a non-stored address (when store=False)
|
|
101
|
+
that can still serve as a stable identifier for hashing/equality.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
store: If True, values are stored in the active portal and the
|
|
105
|
+
returned addresses persist across sessions. If False, produces
|
|
106
|
+
non-stored addresses suitable for transient signatures.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
PackedKwArgs: A new mapping with values converted to ValueAddr.
|
|
110
|
+
"""
|
|
65
111
|
packed_copy = dict()
|
|
66
112
|
if store:
|
|
67
113
|
portal = get_active_portal()
|
|
@@ -77,10 +123,26 @@ class KwArgs(dict):
|
|
|
77
123
|
|
|
78
124
|
|
|
79
125
|
class PackedKwArgs(KwArgs):
|
|
126
|
+
"""KwArgs where all values are ValueAddr instances.
|
|
127
|
+
|
|
128
|
+
This is the packed form produced by KwArgs.pack().
|
|
129
|
+
"""
|
|
80
130
|
def __init__(self,*args, **kargs):
|
|
131
|
+
"""Construct a PackedKwArgs mapping.
|
|
132
|
+
|
|
133
|
+
Accepts the same arguments as dict/KwArgs.
|
|
134
|
+
"""
|
|
81
135
|
super().__init__(*args, **kargs)
|
|
82
136
|
|
|
83
137
|
|
|
84
138
|
class UnpackedKwArgs(KwArgs):
|
|
139
|
+
"""KwArgs where all values are raw (non-ValueAddr) objects.
|
|
140
|
+
|
|
141
|
+
This is the unpacked form produced by KwArgs.unpack().
|
|
142
|
+
"""
|
|
85
143
|
def __init__(self,*args, **kargs):
|
|
144
|
+
"""Construct an UnpackedKwArgs mapping.
|
|
145
|
+
|
|
146
|
+
Accepts the same arguments as dict/KwArgs.
|
|
147
|
+
"""
|
|
86
148
|
super().__init__(*args, **kargs)
|
|
@@ -6,19 +6,41 @@ from .._030_data_portals import storable
|
|
|
6
6
|
from .logging_portal_core_classes import LoggingCodePortal, LoggingFn
|
|
7
7
|
|
|
8
8
|
class logging(storable):
|
|
9
|
-
"""
|
|
9
|
+
"""Decorator that converts a Python function into a LoggingFn.
|
|
10
|
+
|
|
11
|
+
When applied, the target function is wrapped into a LoggingFn that records
|
|
12
|
+
execution attempts, results, outputs, crashes, and events via a
|
|
13
|
+
LoggingCodePortal.
|
|
10
14
|
"""
|
|
11
15
|
_excessive_logging: bool|Joker
|
|
12
16
|
|
|
13
17
|
def __init__(self
|
|
14
18
|
, excessive_logging:bool|Joker = KEEP_CURRENT
|
|
15
19
|
, portal: LoggingCodePortal | None = None):
|
|
20
|
+
"""Initialize the logging decorator.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
excessive_logging: If True, the wrapped function will capture
|
|
24
|
+
detailed per-execution artifacts (attempt context, outputs,
|
|
25
|
+
and results). If KEEP_CURRENT, inherits from the wrapped
|
|
26
|
+
LoggingFn or the active portal.
|
|
27
|
+
portal: Optional LoggingCodePortal to bind the wrapped function to.
|
|
28
|
+
If None, the active portal at execution time is used.
|
|
29
|
+
"""
|
|
16
30
|
assert isinstance(excessive_logging, (bool,Joker))
|
|
17
31
|
assert isinstance(portal, LoggingCodePortal) or portal is None
|
|
18
32
|
storable.__init__(self=self, portal=portal)
|
|
19
33
|
self._excessive_logging = excessive_logging
|
|
20
34
|
|
|
21
35
|
def __call__(self,fn:Callable)->LoggingFn:
|
|
36
|
+
"""Wrap the function into a LoggingFn.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
fn: A plain Python callable to decorate.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
LoggingFn: The logging-enabled wrapper for the given function.
|
|
43
|
+
"""
|
|
22
44
|
wrapper = LoggingFn(fn
|
|
23
45
|
, excessive_logging=self._excessive_logging
|
|
24
46
|
, portal=self._portal)
|