edsl 0.1.30.dev5__py3-none-any.whl → 0.1.31__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.
- edsl/__version__.py +1 -1
- edsl/agents/Invigilator.py +7 -2
- edsl/agents/PromptConstructionMixin.py +18 -1
- edsl/config.py +4 -0
- edsl/conjure/Conjure.py +6 -0
- edsl/coop/coop.py +4 -0
- edsl/coop/utils.py +9 -1
- edsl/data/CacheHandler.py +3 -4
- edsl/enums.py +2 -0
- edsl/inference_services/DeepInfraService.py +6 -91
- edsl/inference_services/GroqService.py +18 -0
- edsl/inference_services/InferenceServicesCollection.py +13 -5
- edsl/inference_services/OpenAIService.py +64 -21
- edsl/inference_services/registry.py +2 -1
- edsl/jobs/Jobs.py +80 -33
- edsl/jobs/buckets/TokenBucket.py +15 -7
- edsl/jobs/interviews/Interview.py +41 -19
- edsl/jobs/interviews/InterviewExceptionEntry.py +101 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +58 -40
- edsl/jobs/interviews/interview_exception_tracking.py +68 -10
- edsl/jobs/runners/JobsRunnerAsyncio.py +112 -81
- edsl/jobs/runners/JobsRunnerStatusData.py +0 -237
- edsl/jobs/runners/JobsRunnerStatusMixin.py +291 -35
- edsl/jobs/tasks/QuestionTaskCreator.py +2 -3
- edsl/jobs/tasks/TaskCreators.py +8 -2
- edsl/jobs/tasks/TaskHistory.py +145 -1
- edsl/language_models/LanguageModel.py +133 -75
- edsl/language_models/ModelList.py +8 -2
- edsl/language_models/registry.py +16 -0
- edsl/questions/QuestionFunctional.py +8 -7
- edsl/questions/QuestionMultipleChoice.py +15 -12
- edsl/questions/QuestionNumerical.py +0 -1
- edsl/questions/descriptors.py +6 -4
- edsl/results/DatasetExportMixin.py +185 -78
- edsl/results/Result.py +13 -11
- edsl/results/Results.py +19 -16
- edsl/results/ResultsToolsMixin.py +1 -1
- edsl/scenarios/Scenario.py +14 -0
- edsl/scenarios/ScenarioList.py +59 -21
- edsl/scenarios/ScenarioListExportMixin.py +16 -5
- edsl/scenarios/ScenarioListPdfMixin.py +3 -0
- edsl/surveys/Survey.py +11 -8
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dist-info}/METADATA +4 -2
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dist-info}/RECORD +46 -44
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dist-info}/LICENSE +0 -0
- {edsl-0.1.30.dev5.dist-info → edsl-0.1.31.dist-info}/WHEEL +0 -0
edsl/jobs/Jobs.py
CHANGED
@@ -3,9 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import warnings
|
4
4
|
from itertools import product
|
5
5
|
from typing import Optional, Union, Sequence, Generator
|
6
|
-
|
7
6
|
from edsl.Base import Base
|
8
|
-
|
9
7
|
from edsl.exceptions import MissingAPIKeyError
|
10
8
|
from edsl.jobs.buckets.BucketCollection import BucketCollection
|
11
9
|
from edsl.jobs.interviews.Interview import Interview
|
@@ -321,7 +319,11 @@ class Jobs(Base):
|
|
321
319
|
self.scenarios = self.scenarios or [Scenario()]
|
322
320
|
for agent, scenario, model in product(self.agents, self.scenarios, self.models):
|
323
321
|
yield Interview(
|
324
|
-
survey=self.survey,
|
322
|
+
survey=self.survey,
|
323
|
+
agent=agent,
|
324
|
+
scenario=scenario,
|
325
|
+
model=model,
|
326
|
+
skip_retry=self.skip_retry,
|
325
327
|
)
|
326
328
|
|
327
329
|
def create_bucket_collection(self) -> BucketCollection:
|
@@ -411,6 +413,12 @@ class Jobs(Base):
|
|
411
413
|
if warn:
|
412
414
|
warnings.warn(message)
|
413
415
|
|
416
|
+
@property
|
417
|
+
def skip_retry(self):
|
418
|
+
if not hasattr(self, "_skip_retry"):
|
419
|
+
return False
|
420
|
+
return self._skip_retry
|
421
|
+
|
414
422
|
def run(
|
415
423
|
self,
|
416
424
|
n: int = 1,
|
@@ -425,6 +433,7 @@ class Jobs(Base):
|
|
425
433
|
print_exceptions=True,
|
426
434
|
remote_cache_description: Optional[str] = None,
|
427
435
|
remote_inference_description: Optional[str] = None,
|
436
|
+
skip_retry: bool = False,
|
428
437
|
) -> Results:
|
429
438
|
"""
|
430
439
|
Runs the Job: conducts Interviews and returns their results.
|
@@ -443,6 +452,7 @@ class Jobs(Base):
|
|
443
452
|
from edsl.coop.coop import Coop
|
444
453
|
|
445
454
|
self._check_parameters()
|
455
|
+
self._skip_retry = skip_retry
|
446
456
|
|
447
457
|
if batch_mode is not None:
|
448
458
|
raise NotImplementedError(
|
@@ -461,12 +471,11 @@ class Jobs(Base):
|
|
461
471
|
remote_inference = False
|
462
472
|
|
463
473
|
if remote_inference:
|
464
|
-
|
465
|
-
from
|
466
|
-
from edsl.
|
467
|
-
|
468
|
-
|
469
|
-
from edsl.surveys.Survey import Survey
|
474
|
+
import time
|
475
|
+
from datetime import datetime
|
476
|
+
from edsl.config import CONFIG
|
477
|
+
|
478
|
+
expected_parrot_url = CONFIG.get("EXPECTED_PARROT_URL")
|
470
479
|
|
471
480
|
self._output("Remote inference activated. Sending job to server...")
|
472
481
|
if remote_cache:
|
@@ -474,33 +483,60 @@ class Jobs(Base):
|
|
474
483
|
"Remote caching activated. The remote cache will be used for this job."
|
475
484
|
)
|
476
485
|
|
477
|
-
|
486
|
+
remote_job_creation_data = coop.remote_inference_create(
|
478
487
|
self,
|
479
488
|
description=remote_inference_description,
|
480
489
|
status="queued",
|
490
|
+
iterations=n,
|
481
491
|
)
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
492
|
+
time_queued = datetime.now().strftime("%m/%d/%Y %I:%M:%S %p")
|
493
|
+
job_uuid = remote_job_creation_data.get("uuid")
|
494
|
+
print(f"Remote inference started (Job uuid={job_uuid}).")
|
495
|
+
# print(f"Job queued at {time_queued}.")
|
496
|
+
job_in_queue = True
|
497
|
+
while job_in_queue:
|
498
|
+
remote_job_data = coop.remote_inference_get(job_uuid)
|
499
|
+
status = remote_job_data.get("status")
|
500
|
+
if status == "cancelled":
|
501
|
+
print("\r" + " " * 80 + "\r", end="")
|
502
|
+
print("Job cancelled by the user.")
|
503
|
+
print(
|
504
|
+
f"See {expected_parrot_url}/home/remote-inference for more details."
|
493
505
|
)
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
506
|
+
return None
|
507
|
+
elif status == "failed":
|
508
|
+
print("\r" + " " * 80 + "\r", end="")
|
509
|
+
print("Job failed.")
|
510
|
+
print(
|
511
|
+
f"See {expected_parrot_url}/home/remote-inference for more details."
|
512
|
+
)
|
513
|
+
return None
|
514
|
+
elif status == "completed":
|
515
|
+
results_uuid = remote_job_data.get("results_uuid")
|
516
|
+
results = coop.get(results_uuid, expected_object_type="results")
|
517
|
+
print("\r" + " " * 80 + "\r", end="")
|
518
|
+
print(
|
519
|
+
f"Job completed and Results stored on Coop (Results uuid={results_uuid})."
|
520
|
+
)
|
521
|
+
return results
|
522
|
+
else:
|
523
|
+
duration = 10 if len(self) < 10 else 60
|
524
|
+
time_checked = datetime.now().strftime("%Y-%m-%d %I:%M:%S %p")
|
525
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
526
|
+
start_time = time.time()
|
527
|
+
i = 0
|
528
|
+
while time.time() - start_time < duration:
|
529
|
+
print(
|
530
|
+
f"\r{frames[i % len(frames)]} Job status: {status} - last update: {time_checked}",
|
531
|
+
end="",
|
532
|
+
flush=True,
|
533
|
+
)
|
534
|
+
time.sleep(0.1)
|
535
|
+
i += 1
|
502
536
|
else:
|
503
537
|
if check_api_keys:
|
538
|
+
from edsl import Model
|
539
|
+
|
504
540
|
for model in self.models + [Model()]:
|
505
541
|
if not model.has_valid_api_key():
|
506
542
|
raise MissingAPIKeyError(
|
@@ -606,9 +642,9 @@ class Jobs(Base):
|
|
606
642
|
results = JobsRunnerAsyncio(self).run(*args, **kwargs)
|
607
643
|
return results
|
608
644
|
|
609
|
-
async def run_async(self, cache=None, **kwargs):
|
645
|
+
async def run_async(self, cache=None, n=1, **kwargs):
|
610
646
|
"""Run the job asynchronously."""
|
611
|
-
results = await JobsRunnerAsyncio(self).run_async(cache=cache, **kwargs)
|
647
|
+
results = await JobsRunnerAsyncio(self).run_async(cache=cache, n=n, **kwargs)
|
612
648
|
return results
|
613
649
|
|
614
650
|
def all_question_parameters(self):
|
@@ -688,7 +724,10 @@ class Jobs(Base):
|
|
688
724
|
#######################
|
689
725
|
@classmethod
|
690
726
|
def example(
|
691
|
-
cls,
|
727
|
+
cls,
|
728
|
+
throw_exception_probability: int = 0,
|
729
|
+
randomize: bool = False,
|
730
|
+
test_model=False,
|
692
731
|
) -> Jobs:
|
693
732
|
"""Return an example Jobs instance.
|
694
733
|
|
@@ -706,6 +745,11 @@ class Jobs(Base):
|
|
706
745
|
|
707
746
|
addition = "" if not randomize else str(uuid4())
|
708
747
|
|
748
|
+
if test_model:
|
749
|
+
from edsl.language_models import LanguageModel
|
750
|
+
|
751
|
+
m = LanguageModel.example(test_model=True)
|
752
|
+
|
709
753
|
# (status, question, period)
|
710
754
|
agent_answers = {
|
711
755
|
("Joyful", "how_feeling", "morning"): "OK",
|
@@ -753,7 +797,10 @@ class Jobs(Base):
|
|
753
797
|
Scenario({"period": "afternoon"}),
|
754
798
|
]
|
755
799
|
)
|
756
|
-
|
800
|
+
if test_model:
|
801
|
+
job = base_survey.by(m).by(scenario_list).by(joy_agent, sad_agent)
|
802
|
+
else:
|
803
|
+
job = base_survey.by(scenario_list).by(joy_agent, sad_agent)
|
757
804
|
|
758
805
|
return job
|
759
806
|
|
edsl/jobs/buckets/TokenBucket.py
CHANGED
@@ -30,7 +30,7 @@ class TokenBucket:
|
|
30
30
|
if self.turbo_mode:
|
31
31
|
pass
|
32
32
|
else:
|
33
|
-
#pass
|
33
|
+
# pass
|
34
34
|
self.turbo_mode = True
|
35
35
|
self.capacity = float("inf")
|
36
36
|
self.refill_rate = float("inf")
|
@@ -74,7 +74,7 @@ class TokenBucket:
|
|
74
74
|
|
75
75
|
def refill(self) -> None:
|
76
76
|
"""Refill the bucket with new tokens based on elapsed time.
|
77
|
-
|
77
|
+
|
78
78
|
|
79
79
|
|
80
80
|
>>> bucket = TokenBucket(bucket_name="test", bucket_type="test", capacity=10, refill_rate=1)
|
@@ -82,7 +82,7 @@ class TokenBucket:
|
|
82
82
|
>>> bucket.refill()
|
83
83
|
>>> bucket.tokens > 0
|
84
84
|
True
|
85
|
-
|
85
|
+
|
86
86
|
"""
|
87
87
|
now = time.monotonic()
|
88
88
|
elapsed = now - self.last_refill
|
@@ -100,7 +100,9 @@ class TokenBucket:
|
|
100
100
|
available_tokens = min(self.capacity, self.tokens + refill_amount)
|
101
101
|
return max(0, requested_tokens - available_tokens) / self.refill_rate
|
102
102
|
|
103
|
-
async def get_tokens(
|
103
|
+
async def get_tokens(
|
104
|
+
self, amount: Union[int, float] = 1, cheat_bucket_capacity=True
|
105
|
+
) -> None:
|
104
106
|
"""Wait for the specified number of tokens to become available.
|
105
107
|
|
106
108
|
|
@@ -116,14 +118,20 @@ class TokenBucket:
|
|
116
118
|
True
|
117
119
|
|
118
120
|
>>> bucket = TokenBucket(bucket_name="test", bucket_type="test", capacity=10, refill_rate=1)
|
119
|
-
>>> asyncio.run(bucket.get_tokens(11))
|
121
|
+
>>> asyncio.run(bucket.get_tokens(11, cheat_bucket_capacity=False))
|
120
122
|
Traceback (most recent call last):
|
121
123
|
...
|
122
124
|
ValueError: Requested amount exceeds bucket capacity. Bucket capacity: 10, requested amount: 11. As the bucket never overflows, the requested amount will never be available.
|
125
|
+
>>> asyncio.run(bucket.get_tokens(11, cheat_bucket_capacity=True))
|
123
126
|
"""
|
124
127
|
if amount > self.capacity:
|
125
|
-
|
126
|
-
|
128
|
+
if not cheat_bucket_capacity:
|
129
|
+
msg = f"Requested amount exceeds bucket capacity. Bucket capacity: {self.capacity}, requested amount: {amount}. As the bucket never overflows, the requested amount will never be available."
|
130
|
+
raise ValueError(msg)
|
131
|
+
else:
|
132
|
+
self.tokens = 0 # clear the bucket but let it go through
|
133
|
+
return
|
134
|
+
|
127
135
|
while self.tokens < amount:
|
128
136
|
self.refill()
|
129
137
|
await asyncio.sleep(0.01) # Sleep briefly to prevent busy waiting
|
@@ -14,17 +14,19 @@ from edsl.jobs.tasks.TaskCreators import TaskCreators
|
|
14
14
|
from edsl.jobs.interviews.InterviewStatusLog import InterviewStatusLog
|
15
15
|
from edsl.jobs.interviews.interview_exception_tracking import (
|
16
16
|
InterviewExceptionCollection,
|
17
|
-
InterviewExceptionEntry,
|
18
17
|
)
|
18
|
+
from edsl.jobs.interviews.InterviewExceptionEntry import InterviewExceptionEntry
|
19
19
|
from edsl.jobs.interviews.retry_management import retry_strategy
|
20
20
|
from edsl.jobs.interviews.InterviewTaskBuildingMixin import InterviewTaskBuildingMixin
|
21
21
|
from edsl.jobs.interviews.InterviewStatusMixin import InterviewStatusMixin
|
22
22
|
|
23
23
|
import asyncio
|
24
24
|
|
25
|
+
|
25
26
|
def run_async(coro):
|
26
27
|
return asyncio.run(coro)
|
27
28
|
|
29
|
+
|
28
30
|
class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
29
31
|
"""
|
30
32
|
An 'interview' is one agent answering one survey, with one language model, for a given scenario.
|
@@ -41,7 +43,8 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
41
43
|
debug: Optional[bool] = False,
|
42
44
|
iteration: int = 0,
|
43
45
|
cache: Optional["Cache"] = None,
|
44
|
-
sidecar_model: Optional[
|
46
|
+
sidecar_model: Optional["LanguageModel"] = None,
|
47
|
+
skip_retry=False,
|
45
48
|
):
|
46
49
|
"""Initialize the Interview instance.
|
47
50
|
|
@@ -85,6 +88,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
85
88
|
self.task_creators = TaskCreators() # tracks the task creators
|
86
89
|
self.exceptions = InterviewExceptionCollection()
|
87
90
|
self._task_status_log_dict = InterviewStatusLog()
|
91
|
+
self.skip_retry = skip_retry
|
88
92
|
|
89
93
|
# dictionary mapping question names to their index in the survey.
|
90
94
|
self.to_index = {
|
@@ -92,13 +96,37 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
92
96
|
for index, question_name in enumerate(self.survey.question_names)
|
93
97
|
}
|
94
98
|
|
99
|
+
def _to_dict(self, include_exceptions=False) -> dict[str, Any]:
|
100
|
+
"""Return a dictionary representation of the Interview instance.
|
101
|
+
This is just for hashing purposes.
|
102
|
+
|
103
|
+
>>> i = Interview.example()
|
104
|
+
>>> hash(i)
|
105
|
+
1646262796627658719
|
106
|
+
"""
|
107
|
+
d = {
|
108
|
+
"agent": self.agent._to_dict(),
|
109
|
+
"survey": self.survey._to_dict(),
|
110
|
+
"scenario": self.scenario._to_dict(),
|
111
|
+
"model": self.model._to_dict(),
|
112
|
+
"iteration": self.iteration,
|
113
|
+
"exceptions": {},
|
114
|
+
}
|
115
|
+
if include_exceptions:
|
116
|
+
d["exceptions"] = self.exceptions.to_dict()
|
117
|
+
|
118
|
+
def __hash__(self) -> int:
|
119
|
+
from edsl.utilities.utilities import dict_hash
|
120
|
+
|
121
|
+
return dict_hash(self._to_dict())
|
122
|
+
|
95
123
|
async def async_conduct_interview(
|
96
124
|
self,
|
97
125
|
*,
|
98
126
|
model_buckets: ModelBuckets = None,
|
99
127
|
debug: bool = False,
|
100
128
|
stop_on_exception: bool = False,
|
101
|
-
sidecar_model: Optional[
|
129
|
+
sidecar_model: Optional["LanguageModel"] = None,
|
102
130
|
) -> tuple["Answers", List[dict[str, Any]]]:
|
103
131
|
"""
|
104
132
|
Conduct an Interview asynchronously.
|
@@ -132,8 +160,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
132
160
|
<BLANKLINE>
|
133
161
|
|
134
162
|
>>> i.exceptions
|
135
|
-
{'q0':
|
136
|
-
|
163
|
+
{'q0': ...
|
137
164
|
>>> i = Interview.example()
|
138
165
|
>>> result, _ = asyncio.run(i.async_conduct_interview(stop_on_exception = True))
|
139
166
|
Traceback (most recent call last):
|
@@ -146,13 +173,11 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
146
173
|
if model_buckets is None or hasattr(self.agent, "answer_question_directly"):
|
147
174
|
model_buckets = ModelBuckets.infinity_bucket()
|
148
175
|
|
149
|
-
|
150
176
|
## build the tasks using the InterviewTaskBuildingMixin
|
151
177
|
## This is the key part---it creates a task for each question,
|
152
178
|
## with dependencies on the questions that must be answered before this one can be answered.
|
153
179
|
self.tasks = self._build_question_tasks(
|
154
|
-
debug=debug,
|
155
|
-
model_buckets=model_buckets
|
180
|
+
debug=debug, model_buckets=model_buckets
|
156
181
|
)
|
157
182
|
|
158
183
|
## 'Invigilators' are used to administer the survey
|
@@ -195,7 +220,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
195
220
|
|
196
221
|
def _record_exception(self, task, exception: Exception) -> None:
|
197
222
|
"""Record an exception in the Interview instance.
|
198
|
-
|
223
|
+
|
199
224
|
It records the exception in the Interview instance, with the task name and the exception entry.
|
200
225
|
|
201
226
|
>>> i = Interview.example()
|
@@ -204,13 +229,9 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
204
229
|
{}
|
205
230
|
>>> i._record_exception(i.tasks[0], Exception("An exception occurred."))
|
206
231
|
>>> i.exceptions
|
207
|
-
{'q0':
|
232
|
+
{'q0': ...
|
208
233
|
"""
|
209
|
-
exception_entry = InterviewExceptionEntry(
|
210
|
-
exception=repr(exception),
|
211
|
-
time=time.time(),
|
212
|
-
traceback=traceback.format_exc(),
|
213
|
-
)
|
234
|
+
exception_entry = InterviewExceptionEntry(exception)
|
214
235
|
self.exceptions.add(task.get_name(), exception_entry)
|
215
236
|
|
216
237
|
@property
|
@@ -235,14 +256,14 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
235
256
|
"""Return a string representation of the Interview instance."""
|
236
257
|
return f"Interview(agent = {repr(self.agent)}, survey = {repr(self.survey)}, scenario = {repr(self.scenario)}, model = {repr(self.model)})"
|
237
258
|
|
238
|
-
def duplicate(self, iteration: int, cache:
|
259
|
+
def duplicate(self, iteration: int, cache: "Cache") -> Interview:
|
239
260
|
"""Duplicate the interview, but with a new iteration number and cache.
|
240
|
-
|
261
|
+
|
241
262
|
>>> i = Interview.example()
|
242
263
|
>>> i2 = i.duplicate(1, None)
|
243
264
|
>>> i.iteration + 1 == i2.iteration
|
244
265
|
True
|
245
|
-
|
266
|
+
|
246
267
|
"""
|
247
268
|
return Interview(
|
248
269
|
agent=self.agent,
|
@@ -251,6 +272,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
251
272
|
model=self.model,
|
252
273
|
iteration=iteration,
|
253
274
|
cache=cache,
|
275
|
+
skip_retry=self.skip_retry,
|
254
276
|
)
|
255
277
|
|
256
278
|
@classmethod
|
@@ -270,7 +292,7 @@ class Interview(InterviewStatusMixin, InterviewTaskBuildingMixin):
|
|
270
292
|
scenario = Scenario.example()
|
271
293
|
model = LanguageModel.example()
|
272
294
|
if throw_exception:
|
273
|
-
model = LanguageModel.example(test_model
|
295
|
+
model = LanguageModel.example(test_model=True, throw_exception=True)
|
274
296
|
agent = Agent.example()
|
275
297
|
return Interview(agent=agent, survey=survey, scenario=scenario, model=model)
|
276
298
|
return Interview(agent=agent, survey=survey, scenario=scenario, model=model)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import traceback
|
2
|
+
import datetime
|
3
|
+
import time
|
4
|
+
from collections import UserDict
|
5
|
+
|
6
|
+
# traceback=traceback.format_exc(),
|
7
|
+
# traceback = frame_summary_to_dict(traceback.extract_tb(e.__traceback__))
|
8
|
+
# traceback = [frame_summary_to_dict(f) for f in traceback.extract_tb(e.__traceback__)]
|
9
|
+
|
10
|
+
|
11
|
+
class InterviewExceptionEntry:
|
12
|
+
"""Class to record an exception that occurred during the interview.
|
13
|
+
|
14
|
+
>>> entry = InterviewExceptionEntry.example()
|
15
|
+
>>> entry.to_dict()['exception']
|
16
|
+
"ValueError('An error occurred.')"
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, exception: Exception, traceback_format="html"):
|
20
|
+
self.time = datetime.datetime.now().isoformat()
|
21
|
+
self.exception = exception
|
22
|
+
self.traceback_format = traceback_format
|
23
|
+
|
24
|
+
def __getitem__(self, key):
|
25
|
+
# Support dict-like access obj['a']
|
26
|
+
return str(getattr(self, key))
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def example(cls):
|
30
|
+
try:
|
31
|
+
raise ValueError("An error occurred.")
|
32
|
+
except Exception as e:
|
33
|
+
entry = InterviewExceptionEntry(e)
|
34
|
+
return entry
|
35
|
+
|
36
|
+
@property
|
37
|
+
def traceback(self):
|
38
|
+
"""Return the exception as HTML."""
|
39
|
+
if self.traceback_format == "html":
|
40
|
+
return self.html_traceback
|
41
|
+
else:
|
42
|
+
return self.text_traceback
|
43
|
+
|
44
|
+
@property
|
45
|
+
def text_traceback(self):
|
46
|
+
"""
|
47
|
+
>>> entry = InterviewExceptionEntry.example()
|
48
|
+
>>> entry.text_traceback
|
49
|
+
'Traceback (most recent call last):...'
|
50
|
+
"""
|
51
|
+
e = self.exception
|
52
|
+
tb_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
53
|
+
return tb_str
|
54
|
+
|
55
|
+
@property
|
56
|
+
def html_traceback(self):
|
57
|
+
from rich.console import Console
|
58
|
+
from rich.table import Table
|
59
|
+
from rich.traceback import Traceback
|
60
|
+
|
61
|
+
from io import StringIO
|
62
|
+
|
63
|
+
html_output = StringIO()
|
64
|
+
|
65
|
+
console = Console(file=html_output, record=True)
|
66
|
+
|
67
|
+
tb = Traceback.from_exception(
|
68
|
+
type(self.exception),
|
69
|
+
self.exception,
|
70
|
+
self.exception.__traceback__,
|
71
|
+
show_locals=True,
|
72
|
+
)
|
73
|
+
console.print(tb)
|
74
|
+
return html_output.getvalue()
|
75
|
+
|
76
|
+
def to_dict(self) -> dict:
|
77
|
+
"""Return the exception as a dictionary.
|
78
|
+
|
79
|
+
>>> entry = InterviewExceptionEntry.example()
|
80
|
+
>>> entry.to_dict()['exception']
|
81
|
+
"ValueError('An error occurred.')"
|
82
|
+
|
83
|
+
"""
|
84
|
+
return {
|
85
|
+
"exception": repr(self.exception),
|
86
|
+
"time": self.time,
|
87
|
+
"traceback": self.traceback,
|
88
|
+
}
|
89
|
+
|
90
|
+
def push(self):
|
91
|
+
from edsl import Coop
|
92
|
+
|
93
|
+
coop = Coop()
|
94
|
+
results = coop.error_create(self.to_dict())
|
95
|
+
return results
|
96
|
+
|
97
|
+
|
98
|
+
if __name__ == "__main__":
|
99
|
+
import doctest
|
100
|
+
|
101
|
+
doctest.testmod(optionflags=doctest.ELLIPSIS)
|