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
|
@@ -5,10 +5,10 @@ import traceback
|
|
|
5
5
|
from typing import Callable, Any
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
|
-
|
|
8
|
+
from parameterizable import NotPicklableClass
|
|
9
9
|
from persidict import PersiDict, KEEP_CURRENT, Joker
|
|
10
10
|
|
|
11
|
-
from .._010_basic_portals import
|
|
11
|
+
from .._010_basic_portals import get_active_portal
|
|
12
12
|
from .._010_basic_portals.basic_portal_core_classes import (
|
|
13
13
|
_describe_persistent_characteristic, _describe_runtime_characteristic)
|
|
14
14
|
|
|
@@ -32,6 +32,18 @@ from .._800_signatures_and_converters.random_signatures import (
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
class LoggingFn(StorableFn):
|
|
35
|
+
"""A function wrapper that logs executions, outputs, events, and crashes.
|
|
36
|
+
|
|
37
|
+
LoggingFn wraps a callable and, when executed within a LoggingCodePortal,
|
|
38
|
+
records execution attempts, results, captured stdout/stderr, raised
|
|
39
|
+
exceptions, and custom events. It also supports an excessive_logging mode
|
|
40
|
+
that enables storing rich per-call artifacts.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
_auxiliary_config_params_at_init (dict): Internal configuration store
|
|
44
|
+
inherited from StorableFn. Includes the 'excessive_logging' flag
|
|
45
|
+
when provided.
|
|
46
|
+
"""
|
|
35
47
|
|
|
36
48
|
# _excessive_logging_at_init: bool | Joker
|
|
37
49
|
|
|
@@ -40,6 +52,19 @@ class LoggingFn(StorableFn):
|
|
|
40
52
|
, excessive_logging: bool|Joker = KEEP_CURRENT
|
|
41
53
|
, portal: LoggingCodePortal | None = None
|
|
42
54
|
):
|
|
55
|
+
"""Initialize a LoggingFn wrapper.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
fn: A callable to wrap or a string with a function's source code.
|
|
59
|
+
excessive_logging: If True, capture detailed per-execution
|
|
60
|
+
artifacts (attempt context, outputs, results). If KEEP_CURRENT,
|
|
61
|
+
will inherit the setting from another LoggingFn when cloning.
|
|
62
|
+
portal: Optional LoggingCodePortal to bind this function to. If
|
|
63
|
+
omitted, uses the active portal during execution.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
TypeError: If excessive_logging is not a bool or Joker.
|
|
67
|
+
"""
|
|
43
68
|
super().__init__(fn=fn, portal=portal)
|
|
44
69
|
|
|
45
70
|
if not isinstance(excessive_logging, (bool, Joker)):
|
|
@@ -56,15 +81,43 @@ class LoggingFn(StorableFn):
|
|
|
56
81
|
|
|
57
82
|
@property
|
|
58
83
|
def excessive_logging(self) -> bool:
|
|
84
|
+
"""Whether rich per-execution logging is enabled for this function.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
bool: True if excessive logging is enabled for this function (from
|
|
88
|
+
its own config or inherited via the portal); False otherwise.
|
|
89
|
+
"""
|
|
59
90
|
value = self._get_config_setting("excessive_logging", self.portal)
|
|
60
91
|
return bool(value)
|
|
61
92
|
|
|
62
93
|
|
|
63
94
|
def get_signature(self, arguments:dict) -> LoggingFnCallSignature:
|
|
95
|
+
"""Create a call signature for this function and the given arguments.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
arguments: A mapping of keyword arguments for the call. Values may
|
|
99
|
+
be raw or ValueAddr; they will be normalized and packed.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
LoggingFnCallSignature: A signature object uniquely identifying
|
|
103
|
+
the combination of function and arguments.
|
|
104
|
+
"""
|
|
64
105
|
return LoggingFnCallSignature(self, arguments)
|
|
65
106
|
|
|
66
107
|
|
|
67
108
|
def execute(self,**kwargs):
|
|
109
|
+
"""Execute the wrapped function and log artifacts via the portal.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
**kwargs: Keyword arguments to pass to the wrapped function.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Any: The result returned by the wrapped function.
|
|
116
|
+
|
|
117
|
+
Side Effects:
|
|
118
|
+
- Registers an execution attempt and, if enabled, captures
|
|
119
|
+
stdout/stderr and stores the result and output.
|
|
120
|
+
"""
|
|
68
121
|
with self.portal:
|
|
69
122
|
packed_kwargs = KwArgs(**kwargs).pack()
|
|
70
123
|
fn_call_signature = self.get_signature(packed_kwargs)
|
|
@@ -76,6 +129,12 @@ class LoggingFn(StorableFn):
|
|
|
76
129
|
|
|
77
130
|
@property
|
|
78
131
|
def portal(self) -> LoggingCodePortal:
|
|
132
|
+
"""The LoggingCodePortal associated with this function.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
LoggingCodePortal: The portal used for storage and logging during
|
|
136
|
+
execution.
|
|
137
|
+
"""
|
|
79
138
|
return super().portal
|
|
80
139
|
|
|
81
140
|
|
|
@@ -109,6 +168,12 @@ class LoggingFnCallSignature:
|
|
|
109
168
|
|
|
110
169
|
@property
|
|
111
170
|
def portal(self):
|
|
171
|
+
"""Portal associated with the underlying function.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
LoggingCodePortal: The portal used to store and retrieve logging
|
|
175
|
+
artifacts for this call signature.
|
|
176
|
+
"""
|
|
112
177
|
return self.fn.portal
|
|
113
178
|
|
|
114
179
|
|
|
@@ -146,6 +211,11 @@ class LoggingFnCallSignature:
|
|
|
146
211
|
|
|
147
212
|
@property
|
|
148
213
|
def fn(self) -> LoggingFn:
|
|
214
|
+
"""Resolve and cache the wrapped LoggingFn instance.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
LoggingFn: The function associated with this call signature.
|
|
218
|
+
"""
|
|
149
219
|
if not hasattr(self, "_fn_cache") or self._fn_cache is None:
|
|
150
220
|
self._fn_cache = self.fn_addr.get(expected_type=LoggingFn)
|
|
151
221
|
return self._fn_cache
|
|
@@ -153,6 +223,11 @@ class LoggingFnCallSignature:
|
|
|
153
223
|
|
|
154
224
|
@property
|
|
155
225
|
def fn_name(self) -> str:
|
|
226
|
+
"""Name of the wrapped function.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
str: The function's name, cached after first access.
|
|
230
|
+
"""
|
|
156
231
|
if not hasattr(self, "_fn_name_cache") or self._fn_name_cache is None:
|
|
157
232
|
self._fn_name_cache = self.fn.name
|
|
158
233
|
return self._fn_name_cache
|
|
@@ -160,16 +235,31 @@ class LoggingFnCallSignature:
|
|
|
160
235
|
|
|
161
236
|
@property
|
|
162
237
|
def fn_addr(self) -> ValueAddr:
|
|
238
|
+
"""Address of the wrapped LoggingFn in the portal storage.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
ValueAddr: The persisted address pointing to the LoggingFn.
|
|
242
|
+
"""
|
|
163
243
|
return self._fn_addr
|
|
164
244
|
|
|
165
245
|
|
|
166
246
|
@property
|
|
167
247
|
def kwargs_addr(self) -> ValueAddr:
|
|
248
|
+
"""Address of the packed keyword arguments in portal storage.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
ValueAddr: The persisted address pointing to the packed kwargs.
|
|
252
|
+
"""
|
|
168
253
|
return self._kwargs_addr
|
|
169
254
|
|
|
170
255
|
|
|
171
256
|
@property
|
|
172
257
|
def packed_kwargs(self) -> PackedKwArgs:
|
|
258
|
+
"""Packed keyword arguments for this call signature.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
PackedKwArgs: The packed kwargs, fetched from storage if not cached.
|
|
262
|
+
"""
|
|
173
263
|
if (not hasattr(self,"_packed_kwargs_cache")
|
|
174
264
|
or self._packed_kwargs_cache is None):
|
|
175
265
|
with self.portal:
|
|
@@ -180,10 +270,21 @@ class LoggingFnCallSignature:
|
|
|
180
270
|
|
|
181
271
|
@property
|
|
182
272
|
def excessive_logging(self) -> bool:
|
|
273
|
+
"""Whether excessive logging is enabled for the underlying function.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
bool: True if excessive logging is enabled, False otherwise.
|
|
277
|
+
"""
|
|
183
278
|
return self.fn.excessive_logging
|
|
184
279
|
|
|
185
280
|
|
|
186
281
|
def __hash_signature_descriptor__(self) -> str:
|
|
282
|
+
"""Descriptor string contributing to the address hash.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
str: A lowercase string combining the function name and this class
|
|
286
|
+
name, used as part of the address hashing scheme.
|
|
287
|
+
"""
|
|
187
288
|
descriptor = self.fn_name
|
|
188
289
|
descriptor += "_" + self.__class__.__name__
|
|
189
290
|
descriptor = descriptor.lower()
|
|
@@ -191,11 +292,21 @@ class LoggingFnCallSignature:
|
|
|
191
292
|
|
|
192
293
|
|
|
193
294
|
def execute(self) -> Any:
|
|
295
|
+
"""Execute the underlying function with stored arguments.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Any: The function's return value.
|
|
299
|
+
"""
|
|
194
300
|
return self.fn.execute(**self.packed_kwargs.unpack())
|
|
195
301
|
|
|
196
302
|
|
|
197
303
|
@property
|
|
198
304
|
def addr(self) -> ValueAddr:
|
|
305
|
+
"""Address uniquely identifying this call signature.
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
ValueAddr: Address of the LoggingFnCallSignature persisted in portal.
|
|
309
|
+
"""
|
|
199
310
|
if hasattr(self, "_addr_cache") and self._addr_cache is not None:
|
|
200
311
|
return self._addr_cache
|
|
201
312
|
with self.portal:
|
|
@@ -205,6 +316,11 @@ class LoggingFnCallSignature:
|
|
|
205
316
|
|
|
206
317
|
@property
|
|
207
318
|
def execution_attempts(self) -> PersiDict:
|
|
319
|
+
"""Timeline of execution attempts metadata for this call.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
PersiDict: Append-only JSON sub-dictionary keyed by timestamped IDs.
|
|
323
|
+
"""
|
|
208
324
|
with self.portal as portal:
|
|
209
325
|
attempts_path = self.addr + ["attempts"]
|
|
210
326
|
attempts = portal._run_history.json.get_subdict(attempts_path)
|
|
@@ -213,6 +329,11 @@ class LoggingFnCallSignature:
|
|
|
213
329
|
|
|
214
330
|
@property
|
|
215
331
|
def last_execution_attempt(self) -> Any:
|
|
332
|
+
"""Most recent execution attempt metadata, if any.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
Any: Latest attempt metadata dict, or None if no attempts exist.
|
|
336
|
+
"""
|
|
216
337
|
with self.portal:
|
|
217
338
|
attempts = self.execution_attempts
|
|
218
339
|
timeline = attempts.newest_values(1)
|
|
@@ -225,6 +346,11 @@ class LoggingFnCallSignature:
|
|
|
225
346
|
|
|
226
347
|
@property
|
|
227
348
|
def execution_results(self) -> PersiDict:
|
|
349
|
+
"""Timeline of execution results for this call.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
PersiDict: Append-only PKL sub-dictionary storing returned values.
|
|
353
|
+
"""
|
|
228
354
|
with self.portal as portal:
|
|
229
355
|
results_path = self.addr + ["results"]
|
|
230
356
|
results = portal._run_history.pkl.get_subdict(results_path)
|
|
@@ -233,6 +359,11 @@ class LoggingFnCallSignature:
|
|
|
233
359
|
|
|
234
360
|
@property
|
|
235
361
|
def last_execution_result(self) -> Any:
|
|
362
|
+
"""Most recent execution result, if any.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Any: Latest return value, or None if no results exist.
|
|
366
|
+
"""
|
|
236
367
|
with self.portal:
|
|
237
368
|
results = self.execution_results
|
|
238
369
|
timeline = results.newest_values(1)
|
|
@@ -245,6 +376,11 @@ class LoggingFnCallSignature:
|
|
|
245
376
|
|
|
246
377
|
@property
|
|
247
378
|
def execution_outputs(self) -> PersiDict:
|
|
379
|
+
"""Timeline of captured stdout/stderr/logging output for this call.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
PersiDict: Append-only TXT sub-dictionary of captured output strings.
|
|
383
|
+
"""
|
|
248
384
|
with self.portal as portal:
|
|
249
385
|
outputs_path = self.addr + ["outputs"]
|
|
250
386
|
outputs = portal._run_history.txt.get_subdict(outputs_path)
|
|
@@ -253,6 +389,11 @@ class LoggingFnCallSignature:
|
|
|
253
389
|
|
|
254
390
|
@property
|
|
255
391
|
def last_execution_output(self) -> Any:
|
|
392
|
+
"""Most recent captured combined output, if any.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Any: Latest captured output string, or None if no outputs exist.
|
|
396
|
+
"""
|
|
256
397
|
with self.portal:
|
|
257
398
|
outputs = self.execution_outputs
|
|
258
399
|
timeline = outputs.newest_values(1)
|
|
@@ -265,6 +406,11 @@ class LoggingFnCallSignature:
|
|
|
265
406
|
|
|
266
407
|
@property
|
|
267
408
|
def crashes(self) -> PersiDict:
|
|
409
|
+
"""Timeline of crashes (exceptions) observed during this call.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
PersiDict: Append-only JSON sub-dictionary of exception payloads.
|
|
413
|
+
"""
|
|
268
414
|
with self.portal as portal:
|
|
269
415
|
crashes_path = self.addr + ["crashes"]
|
|
270
416
|
crashes = portal._run_history.json.get_subdict(crashes_path)
|
|
@@ -273,6 +419,11 @@ class LoggingFnCallSignature:
|
|
|
273
419
|
|
|
274
420
|
@property
|
|
275
421
|
def last_crash(self) -> Any:
|
|
422
|
+
"""Most recent crash payload, if any.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Any: Latest exception payload dict, or None if no crashes exist.
|
|
426
|
+
"""
|
|
276
427
|
with self.portal as portal:
|
|
277
428
|
crashes = self.crashes
|
|
278
429
|
timeline = crashes.newest_values(1)
|
|
@@ -285,6 +436,11 @@ class LoggingFnCallSignature:
|
|
|
285
436
|
|
|
286
437
|
@property
|
|
287
438
|
def events(self) -> PersiDict:
|
|
439
|
+
"""Timeline of user events emitted during this call.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
PersiDict: Append-only JSON sub-dictionary of event payloads.
|
|
443
|
+
"""
|
|
288
444
|
with self.portal as portal:
|
|
289
445
|
events_path = self.addr + ["events"]
|
|
290
446
|
events = portal._run_history.json.get_subdict(events_path)
|
|
@@ -293,6 +449,11 @@ class LoggingFnCallSignature:
|
|
|
293
449
|
|
|
294
450
|
@property
|
|
295
451
|
def last_event(self) -> Any:
|
|
452
|
+
"""Most recent user event payload, if any.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Any: Latest event payload dict, or None if no events exist.
|
|
456
|
+
"""
|
|
296
457
|
with self.portal:
|
|
297
458
|
events = self.events
|
|
298
459
|
timeline = events.newest_values(1)
|
|
@@ -305,6 +466,12 @@ class LoggingFnCallSignature:
|
|
|
305
466
|
|
|
306
467
|
@property
|
|
307
468
|
def execution_records(self) -> list[LoggingFnExecutionRecord]:
|
|
469
|
+
"""All execution sessions derived from attempts for this call.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
list[LoggingFnExecutionRecord]: Records constructed from stored
|
|
473
|
+
attempt IDs for convenient access to artifacts per run session.
|
|
474
|
+
"""
|
|
308
475
|
with self.portal:
|
|
309
476
|
result = []
|
|
310
477
|
for k in self.execution_attempts:
|
|
@@ -313,15 +480,18 @@ class LoggingFnCallSignature:
|
|
|
313
480
|
return result
|
|
314
481
|
|
|
315
482
|
|
|
316
|
-
class LoggingFnExecutionRecord(
|
|
317
|
-
"""
|
|
483
|
+
class LoggingFnExecutionRecord(NotPicklableClass):
|
|
484
|
+
"""Record of a single function execution session.
|
|
318
485
|
|
|
319
|
-
|
|
320
|
-
execution
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
486
|
+
Provides convenient accessors to all artifacts logged for one particular
|
|
487
|
+
execution of a LoggingFn: environment context, captured output, crashes,
|
|
488
|
+
events, and the returned result value.
|
|
489
|
+
|
|
490
|
+
Attributes:
|
|
491
|
+
call_signature (LoggingFnCallSignature): The function-call identity
|
|
492
|
+
this record belongs to.
|
|
493
|
+
session_id (str): Unique identifier for the execution session ("run_*"),
|
|
494
|
+
shared by all artifacts emitted during this run.
|
|
325
495
|
"""
|
|
326
496
|
call_signature: LoggingFnCallSignature
|
|
327
497
|
session_id: str
|
|
@@ -329,17 +499,34 @@ class LoggingFnExecutionRecord(NotPicklable):
|
|
|
329
499
|
self
|
|
330
500
|
, call_signature: LoggingFnCallSignature
|
|
331
501
|
, session_id: str):
|
|
502
|
+
"""Construct an execution record.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
call_signature: The call signature the record is associated with.
|
|
506
|
+
session_id: The unique ID of the execution session.
|
|
507
|
+
"""
|
|
332
508
|
self.call_signature = call_signature
|
|
333
509
|
self.session_id = session_id
|
|
334
510
|
|
|
335
511
|
|
|
336
512
|
@property
|
|
337
513
|
def portal(self):
|
|
514
|
+
"""LoggingCodePortal used to resolve underlying artifacts.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
LoggingCodePortal: The portal associated with the call signature.
|
|
518
|
+
"""
|
|
338
519
|
return self.call_signature.portal
|
|
339
520
|
|
|
340
521
|
|
|
341
522
|
@property
|
|
342
523
|
def output(self) -> str|None:
|
|
524
|
+
"""Combined stdout/stderr/logging output captured for the session.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
str | None: The captured text output, or None when no output
|
|
528
|
+
was recorded for this session.
|
|
529
|
+
"""
|
|
343
530
|
with self.portal:
|
|
344
531
|
execution_outputs = self.call_signature.execution_outputs
|
|
345
532
|
for k in execution_outputs:
|
|
@@ -350,6 +537,12 @@ class LoggingFnExecutionRecord(NotPicklable):
|
|
|
350
537
|
|
|
351
538
|
@property
|
|
352
539
|
def attempt_context(self)-> dict|None:
|
|
540
|
+
"""Environment/context snapshot captured at attempt start.
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
dict | None: The environment summary dict for this session, or
|
|
544
|
+
None if not present (e.g., excessive logging disabled).
|
|
545
|
+
"""
|
|
353
546
|
with self.portal:
|
|
354
547
|
execution_attempts = self.call_signature.execution_attempts
|
|
355
548
|
for k in execution_attempts:
|
|
@@ -360,6 +553,11 @@ class LoggingFnExecutionRecord(NotPicklable):
|
|
|
360
553
|
|
|
361
554
|
@property
|
|
362
555
|
def crashes(self) -> list[dict]:
|
|
556
|
+
"""All exceptions recorded during the session.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
list[dict]: A list of exception payload dicts in chronological order.
|
|
560
|
+
"""
|
|
363
561
|
result = []
|
|
364
562
|
with self.portal:
|
|
365
563
|
crashes = self.call_signature.crashes
|
|
@@ -371,6 +569,11 @@ class LoggingFnExecutionRecord(NotPicklable):
|
|
|
371
569
|
|
|
372
570
|
@property
|
|
373
571
|
def events(self) -> list[dict]:
|
|
572
|
+
"""All events recorded during the session.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
list[dict]: A list of event payload dicts in chronological order.
|
|
576
|
+
"""
|
|
374
577
|
result = []
|
|
375
578
|
with self.portal:
|
|
376
579
|
events = self.call_signature.events
|
|
@@ -382,6 +585,14 @@ class LoggingFnExecutionRecord(NotPicklable):
|
|
|
382
585
|
|
|
383
586
|
@property
|
|
384
587
|
def result(self)->Any:
|
|
588
|
+
"""Return value produced by the function in this session.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
Any: The object returned by the wrapped function for the run.
|
|
592
|
+
|
|
593
|
+
Raises:
|
|
594
|
+
ValueError: If there is no stored result for this session ID.
|
|
595
|
+
"""
|
|
385
596
|
with self.portal:
|
|
386
597
|
execution_results = self.call_signature.execution_results
|
|
387
598
|
for k in execution_results:
|
|
@@ -392,7 +603,17 @@ class LoggingFnExecutionRecord(NotPicklable):
|
|
|
392
603
|
f"{self.call_signature.fn_name} execution results.")
|
|
393
604
|
|
|
394
605
|
|
|
395
|
-
class LoggingFnExecutionFrame(
|
|
606
|
+
class LoggingFnExecutionFrame(NotPicklableClass):
|
|
607
|
+
"""Context manager managing a single function execution with logging.
|
|
608
|
+
|
|
609
|
+
When used as a context, optionally captures stdout/stderr/logging,
|
|
610
|
+
registers attempt metadata, stores results and output, and routes
|
|
611
|
+
exceptions/events to both per-call artifacts and portal-level histories.
|
|
612
|
+
|
|
613
|
+
Attributes:
|
|
614
|
+
call_stack (list[LoggingFnExecutionFrame]): Class-level stack of active
|
|
615
|
+
frames (last item is the current execution frame).
|
|
616
|
+
"""
|
|
396
617
|
call_stack: list[LoggingFnExecutionFrame] = []
|
|
397
618
|
|
|
398
619
|
session_id: str
|
|
@@ -404,6 +625,12 @@ class LoggingFnExecutionFrame(NotPicklable):
|
|
|
404
625
|
context_used: bool
|
|
405
626
|
|
|
406
627
|
def __init__(self, fn_call_signature: LoggingFnCallSignature):
|
|
628
|
+
"""Initialize the execution frame for a specific function call.
|
|
629
|
+
|
|
630
|
+
Args:
|
|
631
|
+
fn_call_signature: The call signature identifying the function and
|
|
632
|
+
its (packed) arguments.
|
|
633
|
+
"""
|
|
407
634
|
with fn_call_signature.portal:
|
|
408
635
|
self.session_id = "run_"+get_random_signature()
|
|
409
636
|
self.fn_call_signature = fn_call_signature
|
|
@@ -421,40 +648,84 @@ class LoggingFnExecutionFrame(NotPicklable):
|
|
|
421
648
|
|
|
422
649
|
@property
|
|
423
650
|
def portal(self) -> LoggingCodePortal:
|
|
651
|
+
"""Portal associated with the current execution frame.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
LoggingCodePortal: The portal used for logging within this frame.
|
|
655
|
+
"""
|
|
424
656
|
return self.fn.portal
|
|
425
657
|
|
|
426
658
|
|
|
427
659
|
@property
|
|
428
660
|
def fn(self) -> LoggingFn:
|
|
661
|
+
"""The LoggingFn being executed in this frame.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
LoggingFn: The wrapped function object.
|
|
665
|
+
"""
|
|
429
666
|
return self.fn_call_signature.fn
|
|
430
667
|
|
|
431
668
|
|
|
432
669
|
@property
|
|
433
670
|
def fn_name(self) -> str:
|
|
671
|
+
"""Name of the function being executed.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
str: The wrapped function's name.
|
|
675
|
+
"""
|
|
434
676
|
return self.fn_call_signature.fn_name
|
|
435
677
|
|
|
436
678
|
|
|
437
679
|
@property
|
|
438
680
|
def excessive_logging(self) -> bool:
|
|
681
|
+
"""Whether the frame should capture detailed artifacts.
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
bool: True if excessive logging is enabled for the function.
|
|
685
|
+
"""
|
|
439
686
|
return self.fn.excessive_logging
|
|
440
687
|
|
|
441
688
|
|
|
442
689
|
@property
|
|
443
690
|
def fn_addr(self) -> ValueAddr:
|
|
691
|
+
"""Address of the wrapped function in storage.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
ValueAddr: The persisted address of the LoggingFn.
|
|
695
|
+
"""
|
|
444
696
|
return self.fn_call_signature.fn_addr
|
|
445
697
|
|
|
446
698
|
|
|
447
699
|
@property
|
|
448
700
|
def packed_args(self) -> PackedKwArgs:
|
|
701
|
+
"""Packed keyword arguments used for this execution.
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
PackedKwArgs: The packed argument mapping.
|
|
705
|
+
"""
|
|
449
706
|
return self.fn_call_signature.packed_kwargs
|
|
450
707
|
|
|
451
708
|
|
|
452
709
|
@property
|
|
453
710
|
def kwargs_addr(self) -> ValueAddr:
|
|
711
|
+
"""Address of the packed arguments in storage.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
ValueAddr: The persisted address of the packed kwargs.
|
|
715
|
+
"""
|
|
454
716
|
return self.fn_call_signature.kwargs_addr
|
|
455
717
|
|
|
456
718
|
|
|
457
719
|
def __enter__(self):
|
|
720
|
+
"""Enter the execution frame context.
|
|
721
|
+
|
|
722
|
+
Performs sanity checks, enters the portal context, optionally starts
|
|
723
|
+
capturing output, pushes the frame onto the call stack, and registers
|
|
724
|
+
an execution attempt if excessive logging is enabled.
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
LoggingFnExecutionFrame: This frame instance for use as a context var.
|
|
728
|
+
"""
|
|
458
729
|
assert not self.context_used, (
|
|
459
730
|
"An instance of PureFnExecutionFrame can be used only once.")
|
|
460
731
|
self.context_used = True
|
|
@@ -471,6 +742,10 @@ class LoggingFnExecutionFrame(NotPicklable):
|
|
|
471
742
|
|
|
472
743
|
|
|
473
744
|
def _register_execution_attempt(self):
|
|
745
|
+
"""Record an execution attempt with environment and source snapshot.
|
|
746
|
+
|
|
747
|
+
No-op when excessive logging is disabled.
|
|
748
|
+
"""
|
|
474
749
|
if not self.excessive_logging:
|
|
475
750
|
return
|
|
476
751
|
execution_attempts = self.fn_call_signature.execution_attempts
|
|
@@ -481,6 +756,14 @@ class LoggingFnExecutionFrame(NotPicklable):
|
|
|
481
756
|
|
|
482
757
|
|
|
483
758
|
def _register_execution_result(self, result: Any):
|
|
759
|
+
"""Store the function's return value into the results timeline.
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
result: The value returned by the wrapped function.
|
|
763
|
+
|
|
764
|
+
Notes:
|
|
765
|
+
No-op when excessive logging is disabled.
|
|
766
|
+
"""
|
|
484
767
|
if not self.excessive_logging:
|
|
485
768
|
return
|
|
486
769
|
execution_results = self.fn_call_signature.execution_results
|
|
@@ -489,6 +772,17 @@ class LoggingFnExecutionFrame(NotPicklable):
|
|
|
489
772
|
|
|
490
773
|
|
|
491
774
|
def __exit__(self, exc_type, exc_value, trace_back):
|
|
775
|
+
"""Exit the execution frame context.
|
|
776
|
+
|
|
777
|
+
Ensures the current exception (if any) is logged, finalizes output
|
|
778
|
+
capture and stores it, pops the frame from the call stack, and exits
|
|
779
|
+
the underlying portal context.
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
exc_type: Exception class raised within the context, if any.
|
|
783
|
+
exc_value: Exception instance raised within the context, if any.
|
|
784
|
+
trace_back: Traceback object associated with the exception, if any.
|
|
785
|
+
"""
|
|
492
786
|
log_exception()
|
|
493
787
|
if isinstance(self.output_capturer, OutputCapturer):
|
|
494
788
|
self.output_capturer.__exit__(exc_type, exc_value, traceback)
|
|
@@ -536,6 +830,22 @@ class LoggingCodePortal(DataPortal):
|
|
|
536
830
|
, p_consistency_checks: float|Joker = KEEP_CURRENT
|
|
537
831
|
, excessive_logging: bool|Joker = KEEP_CURRENT
|
|
538
832
|
):
|
|
833
|
+
"""Construct a LoggingCodePortal.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
root_dict: PersiDict instance or filesystem path serving as the
|
|
837
|
+
storage root. When None, a default in-memory or configured
|
|
838
|
+
PersiDict is used by the base DataPortal.
|
|
839
|
+
p_consistency_checks: Probability [0..1] to run consistency checks
|
|
840
|
+
on storage operations; KEEP_CURRENT inherits existing setting.
|
|
841
|
+
excessive_logging: If True, functions executed via this portal will
|
|
842
|
+
store detailed artifacts (attempts/results/outputs). If
|
|
843
|
+
KEEP_CURRENT, the setting is inherited when cloning or
|
|
844
|
+
otherwise unspecified.
|
|
845
|
+
|
|
846
|
+
Raises:
|
|
847
|
+
TypeError: If excessive_logging is not a bool or Joker.
|
|
848
|
+
"""
|
|
539
849
|
super().__init__(root_dict=root_dict
|
|
540
850
|
, p_consistency_checks=p_consistency_checks)
|
|
541
851
|
del root_dict
|
|
@@ -577,17 +887,35 @@ class LoggingCodePortal(DataPortal):
|
|
|
577
887
|
|
|
578
888
|
|
|
579
889
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
890
|
+
"""Exit the portal context, ensuring any active exception is logged.
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
exc_type: Exception class raised within the portal context, if any.
|
|
894
|
+
exc_val: Exception instance, if any.
|
|
895
|
+
exc_tb: Traceback object, if any.
|
|
896
|
+
"""
|
|
580
897
|
log_exception()
|
|
581
898
|
super().__exit__(exc_type, exc_val, exc_tb)
|
|
582
899
|
|
|
583
900
|
|
|
584
901
|
@property
|
|
585
902
|
def excessive_logging(self) -> bool:
|
|
903
|
+
"""Whether this portal captures detailed per-call artifacts.
|
|
904
|
+
|
|
905
|
+
Returns:
|
|
906
|
+
bool: True if excessive logging is enabled, False otherwise.
|
|
907
|
+
"""
|
|
586
908
|
return bool(self._get_config_setting("excessive_logging"))
|
|
587
909
|
|
|
588
910
|
|
|
589
911
|
def describe(self) -> pd.DataFrame:
|
|
590
|
-
"""
|
|
912
|
+
"""Summarize the portal's current persistent and runtime state.
|
|
913
|
+
|
|
914
|
+
Returns:
|
|
915
|
+
pandas.DataFrame: A table with key characteristics, including
|
|
916
|
+
total crashes logged, today's crashes, and whether excessive
|
|
917
|
+
logging is enabled, combined with the base DataPortal summary.
|
|
918
|
+
"""
|
|
591
919
|
all_params = [super().describe()]
|
|
592
920
|
all_params.append(_describe_persistent_characteristic(
|
|
593
921
|
EXCEPTIONS_TOTAL_TXT, len(self._crash_history)))
|
|
@@ -603,7 +931,12 @@ class LoggingCodePortal(DataPortal):
|
|
|
603
931
|
|
|
604
932
|
|
|
605
933
|
def _clear(self) -> None:
|
|
606
|
-
"""Clear the portal's state
|
|
934
|
+
"""Clear the portal's internal state and unregister handlers.
|
|
935
|
+
|
|
936
|
+
Side Effects:
|
|
937
|
+
- Drops references to crash/event/run histories.
|
|
938
|
+
- Unregisters global uncaught exception handlers.
|
|
939
|
+
"""
|
|
607
940
|
self._crash_history = None
|
|
608
941
|
self._event_log = None
|
|
609
942
|
self._run_history = None
|
|
@@ -611,10 +944,17 @@ class LoggingCodePortal(DataPortal):
|
|
|
611
944
|
super()._clear()
|
|
612
945
|
|
|
613
946
|
|
|
614
|
-
|
|
947
|
+
def log_exception() -> None:
|
|
948
|
+
"""Log the currently handled exception to the active LoggingCodePortal.
|
|
615
949
|
|
|
950
|
+
Captures the exception from sys.exc_info(), enriches it with execution
|
|
951
|
+
environment context, and stores it in the portal-level crash history. If
|
|
952
|
+
called during a function execution with excessive logging enabled, also
|
|
953
|
+
stores the event under the function's per-call crash log.
|
|
616
954
|
|
|
617
|
-
|
|
955
|
+
Returns:
|
|
956
|
+
None
|
|
957
|
+
"""
|
|
618
958
|
exc_type, exc_value, trace_back = sys.exc_info()
|
|
619
959
|
if not _exception_needs_to_be_processed(
|
|
620
960
|
exc_type, exc_value, trace_back):
|
|
@@ -642,6 +982,19 @@ def log_exception() -> None:
|
|
|
642
982
|
|
|
643
983
|
|
|
644
984
|
def log_event(*args, **kwargs):
|
|
985
|
+
"""Record an application event to the active LoggingCodePortal.
|
|
986
|
+
|
|
987
|
+
Adds environment context and optional positional messages to the event
|
|
988
|
+
payload. When called during a function execution, also attaches the event
|
|
989
|
+
to that call's event log.
|
|
990
|
+
|
|
991
|
+
Args:
|
|
992
|
+
*args: Optional positional messages to include in the event payload.
|
|
993
|
+
**kwargs: Key-value pairs forming the body of the event.
|
|
994
|
+
|
|
995
|
+
Returns:
|
|
996
|
+
None
|
|
997
|
+
"""
|
|
645
998
|
if len(LoggingFnExecutionFrame.call_stack):
|
|
646
999
|
frame = LoggingFnExecutionFrame.call_stack[-1]
|
|
647
1000
|
event_id = frame.session_id + "_event_" + str(frame.event_counter)
|