edsl 0.1.33.dev2__py3-none-any.whl → 0.1.34__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/Base.py +24 -14
- edsl/__init__.py +1 -0
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +6 -6
- edsl/agents/Invigilator.py +28 -6
- edsl/agents/InvigilatorBase.py +8 -27
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +150 -182
- edsl/agents/prompt_helpers.py +129 -0
- edsl/config.py +26 -34
- edsl/coop/coop.py +14 -4
- edsl/data_transfer_models.py +26 -73
- edsl/enums.py +2 -0
- edsl/inference_services/AnthropicService.py +5 -2
- edsl/inference_services/AwsBedrock.py +5 -2
- edsl/inference_services/AzureAI.py +5 -2
- edsl/inference_services/GoogleService.py +108 -33
- edsl/inference_services/InferenceServiceABC.py +44 -13
- edsl/inference_services/MistralAIService.py +5 -2
- edsl/inference_services/OpenAIService.py +10 -6
- edsl/inference_services/TestService.py +34 -16
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/registry.py +2 -0
- edsl/jobs/Jobs.py +109 -18
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +64 -10
- edsl/jobs/interviews/Interview.py +130 -49
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +16 -0
- edsl/jobs/interviews/InterviewExceptionEntry.py +2 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +119 -173
- edsl/jobs/runners/JobsRunnerStatus.py +332 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +1 -13
- edsl/jobs/tasks/TaskHistory.py +17 -0
- edsl/language_models/LanguageModel.py +36 -38
- edsl/language_models/registry.py +13 -9
- edsl/language_models/utilities.py +5 -2
- edsl/questions/QuestionBase.py +74 -16
- edsl/questions/QuestionBaseGenMixin.py +28 -0
- edsl/questions/QuestionBudget.py +93 -41
- edsl/questions/QuestionCheckBox.py +1 -1
- edsl/questions/QuestionFreeText.py +6 -0
- edsl/questions/QuestionMultipleChoice.py +13 -24
- edsl/questions/QuestionNumerical.py +5 -4
- edsl/questions/Quick.py +41 -0
- edsl/questions/ResponseValidatorABC.py +11 -6
- edsl/questions/derived/QuestionLinearScale.py +4 -1
- edsl/questions/derived/QuestionTopK.py +4 -1
- edsl/questions/derived/QuestionYesNo.py +8 -2
- edsl/questions/descriptors.py +12 -11
- edsl/questions/templates/budget/__init__.py +0 -0
- edsl/questions/templates/budget/answering_instructions.jinja +7 -0
- edsl/questions/templates/budget/question_presentation.jinja +7 -0
- edsl/questions/templates/extract/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +0 -1
- edsl/questions/templates/rank/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +2 -2
- edsl/results/DatasetExportMixin.py +5 -1
- edsl/results/Result.py +1 -1
- edsl/results/Results.py +4 -1
- edsl/scenarios/FileStore.py +178 -34
- edsl/scenarios/Scenario.py +76 -37
- edsl/scenarios/ScenarioList.py +19 -2
- edsl/scenarios/ScenarioListPdfMixin.py +150 -4
- edsl/study/Study.py +32 -0
- edsl/surveys/DAG.py +62 -0
- edsl/surveys/MemoryPlan.py +26 -0
- edsl/surveys/Rule.py +34 -1
- edsl/surveys/RuleCollection.py +55 -5
- edsl/surveys/Survey.py +189 -10
- edsl/surveys/base.py +4 -0
- edsl/templates/error_reporting/interview_details.html +6 -1
- edsl/utilities/utilities.py +9 -1
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/METADATA +3 -1
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/RECORD +75 -69
- edsl/jobs/interviews/retry_management.py +0 -39
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -333
- edsl/scenarios/ScenarioImageMixin.py +0 -100
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dev2.dist-info → edsl-0.1.34.dist-info}/WHEEL +0 -0
@@ -1,39 +0,0 @@
|
|
1
|
-
from edsl import CONFIG
|
2
|
-
|
3
|
-
from tenacity import (
|
4
|
-
retry,
|
5
|
-
wait_exponential,
|
6
|
-
stop_after_attempt,
|
7
|
-
retry_if_exception_type,
|
8
|
-
before_sleep,
|
9
|
-
)
|
10
|
-
|
11
|
-
EDSL_BACKOFF_START_SEC = float(CONFIG.get("EDSL_BACKOFF_START_SEC"))
|
12
|
-
EDSL_MAX_BACKOFF_SEC = float(CONFIG.get("EDSL_MAX_BACKOFF_SEC"))
|
13
|
-
EDSL_MAX_ATTEMPTS = int(CONFIG.get("EDSL_MAX_ATTEMPTS"))
|
14
|
-
|
15
|
-
|
16
|
-
def print_retry(retry_state, print_to_terminal=True):
|
17
|
-
"Prints details on tenacity retries."
|
18
|
-
attempt_number = retry_state.attempt_number
|
19
|
-
exception = retry_state.outcome.exception()
|
20
|
-
wait_time = retry_state.next_action.sleep
|
21
|
-
exception_name = type(exception).__name__
|
22
|
-
if print_to_terminal:
|
23
|
-
print(
|
24
|
-
f"Attempt {attempt_number} failed with exception '{exception_name}':"
|
25
|
-
f"{exception}",
|
26
|
-
f"now waiting {wait_time:.2f} seconds before retrying."
|
27
|
-
f"Parameters: start={EDSL_BACKOFF_START_SEC}, max={EDSL_MAX_BACKOFF_SEC}, max_attempts={EDSL_MAX_ATTEMPTS}."
|
28
|
-
"\n\n",
|
29
|
-
)
|
30
|
-
|
31
|
-
|
32
|
-
retry_strategy = retry(
|
33
|
-
wait=wait_exponential(
|
34
|
-
multiplier=EDSL_BACKOFF_START_SEC, max=EDSL_MAX_BACKOFF_SEC
|
35
|
-
), # Exponential back-off starting at 1s, doubling, maxing out at 60s
|
36
|
-
stop=stop_after_attempt(EDSL_MAX_ATTEMPTS), # Stop after 5 attempts
|
37
|
-
# retry=retry_if_exception_type(Exception), # Customize this as per your specific retry-able exception
|
38
|
-
before_sleep=print_retry, # Use custom print function for retries
|
39
|
-
)
|
@@ -1,333 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
from typing import List, DefaultDict
|
3
|
-
import asyncio
|
4
|
-
from typing import Type
|
5
|
-
from collections import defaultdict
|
6
|
-
|
7
|
-
from typing import Literal, List, Type, DefaultDict
|
8
|
-
from collections import UserDict, defaultdict
|
9
|
-
|
10
|
-
from edsl.jobs.interviews.InterviewStatusDictionary import InterviewStatusDictionary
|
11
|
-
from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
|
12
|
-
from edsl.jobs.tokens.TokenUsage import TokenUsage
|
13
|
-
from edsl.enums import get_token_pricing
|
14
|
-
from edsl.jobs.tasks.task_status_enum import TaskStatus
|
15
|
-
|
16
|
-
InterviewTokenUsageMapping = DefaultDict[str, InterviewTokenUsage]
|
17
|
-
|
18
|
-
from edsl.jobs.interviews.InterviewStatistic import InterviewStatistic
|
19
|
-
from edsl.jobs.interviews.InterviewStatisticsCollection import (
|
20
|
-
InterviewStatisticsCollection,
|
21
|
-
)
|
22
|
-
from edsl.jobs.tokens.InterviewTokenUsage import InterviewTokenUsage
|
23
|
-
|
24
|
-
|
25
|
-
# return {"cache_status": token_usage_type, "details": details, "cost": f"${token_usage.cost(prices):.5f}"}
|
26
|
-
|
27
|
-
from dataclasses import dataclass, asdict
|
28
|
-
|
29
|
-
from rich.text import Text
|
30
|
-
from rich.box import SIMPLE
|
31
|
-
from rich.table import Table
|
32
|
-
|
33
|
-
|
34
|
-
@dataclass
|
35
|
-
class ModelInfo:
|
36
|
-
model_name: str
|
37
|
-
TPM_limit_k: float
|
38
|
-
RPM_limit_k: float
|
39
|
-
num_tasks_waiting: int
|
40
|
-
token_usage_info: dict
|
41
|
-
|
42
|
-
|
43
|
-
@dataclass
|
44
|
-
class ModelTokenUsageStats:
|
45
|
-
token_usage_type: str
|
46
|
-
details: List[dict]
|
47
|
-
cost: str
|
48
|
-
|
49
|
-
|
50
|
-
class Stats:
|
51
|
-
def elapsed_time(self):
|
52
|
-
InterviewStatistic("elapsed_time", value=elapsed_time, digits=1, units="sec.")
|
53
|
-
|
54
|
-
|
55
|
-
class JobsRunnerStatusMixin:
|
56
|
-
# @staticmethod
|
57
|
-
# def status_dict(interviews: List[Type["Interview"]]) -> List[Type[InterviewStatusDictionary]]:
|
58
|
-
# """
|
59
|
-
# >>> from edsl.jobs.interviews.Interview import Interview
|
60
|
-
# >>> interviews = [Interview.example()]
|
61
|
-
# >>> JobsRunnerStatusMixin().status_dict(interviews)
|
62
|
-
# [InterviewStatusDictionary({<TaskStatus.NOT_STARTED: 1>: 0, <TaskStatus.WAITING_FOR_DEPENDENCIES: 2>: 0, <TaskStatus.CANCELLED: 3>: 0, <TaskStatus.PARENT_FAILED: 4>: 0, <TaskStatus.WAITING_FOR_REQUEST_CAPACITY: 5>: 0, <TaskStatus.WAITING_FOR_TOKEN_CAPACITY: 6>: 0, <TaskStatus.API_CALL_IN_PROGRESS: 7>: 0, <TaskStatus.SUCCESS: 8>: 0, <TaskStatus.FAILED: 9>: 0, 'number_from_cache': 0})]
|
63
|
-
# """
|
64
|
-
# return [interview.interview_status for interview in interviews]
|
65
|
-
|
66
|
-
def _compute_statistic(stat_name: str, completed_tasks, elapsed_time, interviews):
|
67
|
-
stat_definitions = {
|
68
|
-
"elapsed_time": lambda: InterviewStatistic(
|
69
|
-
"elapsed_time", value=elapsed_time, digits=1, units="sec."
|
70
|
-
),
|
71
|
-
"total_interviews_requested": lambda: InterviewStatistic(
|
72
|
-
"total_interviews_requested", value=len(interviews), units=""
|
73
|
-
),
|
74
|
-
"completed_interviews": lambda: InterviewStatistic(
|
75
|
-
"completed_interviews", value=len(completed_tasks), units=""
|
76
|
-
),
|
77
|
-
"percent_complete": lambda: InterviewStatistic(
|
78
|
-
"percent_complete",
|
79
|
-
value=(
|
80
|
-
len(completed_tasks) / len(interviews) * 100
|
81
|
-
if len(interviews) > 0
|
82
|
-
else "NA"
|
83
|
-
),
|
84
|
-
digits=0,
|
85
|
-
units="%",
|
86
|
-
),
|
87
|
-
"average_time_per_interview": lambda: InterviewStatistic(
|
88
|
-
"average_time_per_interview",
|
89
|
-
value=elapsed_time / len(completed_tasks) if completed_tasks else "NA",
|
90
|
-
digits=1,
|
91
|
-
units="sec.",
|
92
|
-
),
|
93
|
-
"task_remaining": lambda: InterviewStatistic(
|
94
|
-
"task_remaining", value=len(interviews) - len(completed_tasks), units=""
|
95
|
-
),
|
96
|
-
"estimated_time_remaining": lambda: InterviewStatistic(
|
97
|
-
"estimated_time_remaining",
|
98
|
-
value=(
|
99
|
-
(len(interviews) - len(completed_tasks))
|
100
|
-
* (elapsed_time / len(completed_tasks))
|
101
|
-
if len(completed_tasks) > 0
|
102
|
-
else "NA"
|
103
|
-
),
|
104
|
-
digits=1,
|
105
|
-
units="sec.",
|
106
|
-
),
|
107
|
-
}
|
108
|
-
if stat_name not in stat_definitions:
|
109
|
-
raise ValueError(
|
110
|
-
f"Invalid stat_name: {stat_name}. The valid stat_names are: {list(stat_definitions.keys())}"
|
111
|
-
)
|
112
|
-
return stat_definitions[stat_name]()
|
113
|
-
|
114
|
-
@staticmethod
|
115
|
-
def _job_level_info(
|
116
|
-
completed_tasks: List[Type[asyncio.Task]],
|
117
|
-
elapsed_time: float,
|
118
|
-
interviews: List[Type["Interview"]],
|
119
|
-
) -> InterviewStatisticsCollection:
|
120
|
-
interview_statistics = InterviewStatisticsCollection()
|
121
|
-
|
122
|
-
default_statistics = [
|
123
|
-
"elapsed_time",
|
124
|
-
"total_interviews_requested",
|
125
|
-
"completed_interviews",
|
126
|
-
"percent_complete",
|
127
|
-
"average_time_per_interview",
|
128
|
-
"task_remaining",
|
129
|
-
"estimated_time_remaining",
|
130
|
-
]
|
131
|
-
for stat_name in default_statistics:
|
132
|
-
interview_statistics.add_stat(
|
133
|
-
JobsRunnerStatusMixin._compute_statistic(
|
134
|
-
stat_name, completed_tasks, elapsed_time, interviews
|
135
|
-
)
|
136
|
-
)
|
137
|
-
|
138
|
-
return interview_statistics
|
139
|
-
|
140
|
-
@staticmethod
|
141
|
-
def _get_model_queues_info(interviews):
|
142
|
-
models_to_tokens = defaultdict(InterviewTokenUsage)
|
143
|
-
model_to_status = defaultdict(InterviewStatusDictionary)
|
144
|
-
waiting_dict = defaultdict(int)
|
145
|
-
|
146
|
-
for interview in interviews:
|
147
|
-
models_to_tokens[interview.model] += interview.token_usage
|
148
|
-
model_to_status[interview.model] += interview.interview_status
|
149
|
-
waiting_dict[interview.model] += interview.interview_status.waiting
|
150
|
-
|
151
|
-
for model, num_waiting in waiting_dict.items():
|
152
|
-
yield JobsRunnerStatusMixin._get_model_info(
|
153
|
-
model, num_waiting, models_to_tokens
|
154
|
-
)
|
155
|
-
|
156
|
-
@staticmethod
|
157
|
-
def generate_status_summary(
|
158
|
-
completed_tasks: List[Type[asyncio.Task]],
|
159
|
-
elapsed_time: float,
|
160
|
-
interviews: List[Type["Interview"]],
|
161
|
-
include_model_queues=False,
|
162
|
-
) -> InterviewStatisticsCollection:
|
163
|
-
"""Generate a summary of the status of the job runner.
|
164
|
-
|
165
|
-
:param completed_tasks: list of completed tasks
|
166
|
-
:param elapsed_time: time elapsed since the start of the job
|
167
|
-
:param interviews: list of interviews to be conducted
|
168
|
-
|
169
|
-
>>> from edsl.jobs.interviews.Interview import Interview
|
170
|
-
>>> interviews = [Interview.example()]
|
171
|
-
>>> completed_tasks = []
|
172
|
-
>>> elapsed_time = 0
|
173
|
-
>>> JobsRunnerStatusMixin().generate_status_summary(completed_tasks, elapsed_time, interviews)
|
174
|
-
{'Elapsed time': '0.0 sec.', 'Total interviews requested': '1 ', 'Completed interviews': '0 ', 'Percent complete': '0 %', 'Average time per interview': 'NA', 'Task remaining': '1 ', 'Estimated time remaining': 'NA'}
|
175
|
-
"""
|
176
|
-
|
177
|
-
interview_status_summary: InterviewStatisticsCollection = (
|
178
|
-
JobsRunnerStatusMixin._job_level_info(
|
179
|
-
completed_tasks=completed_tasks,
|
180
|
-
elapsed_time=elapsed_time,
|
181
|
-
interviews=interviews,
|
182
|
-
)
|
183
|
-
)
|
184
|
-
if include_model_queues:
|
185
|
-
interview_status_summary.model_queues = list(
|
186
|
-
JobsRunnerStatusMixin._get_model_queues_info(interviews)
|
187
|
-
)
|
188
|
-
else:
|
189
|
-
interview_status_summary.model_queues = None
|
190
|
-
|
191
|
-
return interview_status_summary
|
192
|
-
|
193
|
-
@staticmethod
|
194
|
-
def _get_model_info(
|
195
|
-
model: str,
|
196
|
-
num_waiting: int,
|
197
|
-
models_to_tokens: InterviewTokenUsageMapping,
|
198
|
-
) -> dict:
|
199
|
-
"""Get the status of a model.
|
200
|
-
|
201
|
-
:param model: the model name
|
202
|
-
:param num_waiting: the number of tasks waiting for capacity
|
203
|
-
:param models_to_tokens: a mapping of models to token usage
|
204
|
-
|
205
|
-
>>> from edsl.jobs.interviews.Interview import Interview
|
206
|
-
>>> interviews = [Interview.example()]
|
207
|
-
>>> models_to_tokens = defaultdict(InterviewTokenUsage)
|
208
|
-
>>> model = interviews[0].model
|
209
|
-
>>> num_waiting = 0
|
210
|
-
>>> JobsRunnerStatusMixin()._get_model_info(model, num_waiting, models_to_tokens)
|
211
|
-
ModelInfo(model_name='...', TPM_limit_k=..., RPM_limit_k=..., num_tasks_waiting=0, token_usage_info=[ModelTokenUsageStats(token_usage_type='new_token_usage', details=[{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], cost='$0.00000'), ModelTokenUsageStats(token_usage_type='cached_token_usage', details=[{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], cost='$0.00000')])
|
212
|
-
"""
|
213
|
-
|
214
|
-
## TODO: This should probably be a coop method
|
215
|
-
prices = get_token_pricing(model.model)
|
216
|
-
|
217
|
-
token_usage_info = []
|
218
|
-
for token_usage_type in ["new_token_usage", "cached_token_usage"]:
|
219
|
-
token_usage_info.append(
|
220
|
-
JobsRunnerStatusMixin._get_token_usage_info(
|
221
|
-
token_usage_type, models_to_tokens, model, prices
|
222
|
-
)
|
223
|
-
)
|
224
|
-
|
225
|
-
return ModelInfo(
|
226
|
-
**{
|
227
|
-
"model_name": model.model,
|
228
|
-
"TPM_limit_k": model.TPM / 1000,
|
229
|
-
"RPM_limit_k": model.RPM / 1000,
|
230
|
-
"num_tasks_waiting": num_waiting,
|
231
|
-
"token_usage_info": token_usage_info,
|
232
|
-
}
|
233
|
-
)
|
234
|
-
|
235
|
-
@staticmethod
|
236
|
-
def _get_token_usage_info(
|
237
|
-
token_usage_type: Literal["new_token_usage", "cached_token_usage"],
|
238
|
-
models_to_tokens: InterviewTokenUsageMapping,
|
239
|
-
model: str,
|
240
|
-
prices: "TokenPricing",
|
241
|
-
) -> ModelTokenUsageStats:
|
242
|
-
"""Get the token usage info for a model.
|
243
|
-
|
244
|
-
>>> from edsl.jobs.interviews.Interview import Interview
|
245
|
-
>>> interviews = [Interview.example()]
|
246
|
-
>>> models_to_tokens = defaultdict(InterviewTokenUsage)
|
247
|
-
>>> model = interviews[0].model
|
248
|
-
>>> prices = get_token_pricing(model.model)
|
249
|
-
>>> cache_status = "new_token_usage"
|
250
|
-
>>> JobsRunnerStatusMixin()._get_token_usage_info(cache_status, models_to_tokens, model, prices)
|
251
|
-
ModelTokenUsageStats(token_usage_type='new_token_usage', details=[{'type': 'prompt_tokens', 'tokens': 0}, {'type': 'completion_tokens', 'tokens': 0}], cost='$0.00000')
|
252
|
-
|
253
|
-
"""
|
254
|
-
all_token_usage: InterviewTokenUsage = models_to_tokens[model]
|
255
|
-
token_usage: TokenUsage = getattr(all_token_usage, token_usage_type)
|
256
|
-
|
257
|
-
details = [
|
258
|
-
{"type": token_type, "tokens": getattr(token_usage, token_type)}
|
259
|
-
for token_type in ["prompt_tokens", "completion_tokens"]
|
260
|
-
]
|
261
|
-
|
262
|
-
return ModelTokenUsageStats(
|
263
|
-
token_usage_type=token_usage_type,
|
264
|
-
details=details,
|
265
|
-
cost=f"${token_usage.cost(prices):.5f}",
|
266
|
-
)
|
267
|
-
|
268
|
-
@staticmethod
|
269
|
-
def _add_statistics_to_table(table, status_summary):
|
270
|
-
table.add_column("Statistic", style="dim", no_wrap=True, width=50)
|
271
|
-
table.add_column("Value", width=10)
|
272
|
-
|
273
|
-
for key, value in status_summary.items():
|
274
|
-
if key != "model_queues":
|
275
|
-
table.add_row(key, value)
|
276
|
-
|
277
|
-
@staticmethod
|
278
|
-
def display_status_table(status_summary: InterviewStatisticsCollection) -> "Table":
|
279
|
-
table = Table(
|
280
|
-
title="Job Status",
|
281
|
-
show_header=True,
|
282
|
-
header_style="bold magenta",
|
283
|
-
box=SIMPLE,
|
284
|
-
)
|
285
|
-
|
286
|
-
### Job-level statistics
|
287
|
-
JobsRunnerStatusMixin._add_statistics_to_table(table, status_summary)
|
288
|
-
|
289
|
-
## Model-level statistics
|
290
|
-
spacing = " "
|
291
|
-
|
292
|
-
if status_summary.model_queues is not None:
|
293
|
-
table.add_row(Text("Model Queues", style="bold red"), "")
|
294
|
-
for model_info in status_summary.model_queues:
|
295
|
-
model_name = model_info.model_name
|
296
|
-
tpm = f"TPM (k)={model_info.TPM_limit_k}"
|
297
|
-
rpm = f"RPM (k)= {model_info.RPM_limit_k}"
|
298
|
-
pretty_model_name = model_name + ";" + tpm + ";" + rpm
|
299
|
-
table.add_row(Text(pretty_model_name, style="blue"), "")
|
300
|
-
table.add_row(
|
301
|
-
"Number question tasks waiting for capacity",
|
302
|
-
str(model_info.num_tasks_waiting),
|
303
|
-
)
|
304
|
-
# Token usage and cost info
|
305
|
-
for token_usage_info in model_info.token_usage_info:
|
306
|
-
token_usage_type = token_usage_info.token_usage_type
|
307
|
-
table.add_row(
|
308
|
-
Text(
|
309
|
-
spacing + token_usage_type.replace("_", " "), style="bold"
|
310
|
-
),
|
311
|
-
"",
|
312
|
-
)
|
313
|
-
for detail in token_usage_info.details:
|
314
|
-
token_type = detail["type"]
|
315
|
-
tokens = detail["tokens"]
|
316
|
-
table.add_row(spacing + f"{token_type}", f"{tokens:,}")
|
317
|
-
# table.add_row(spacing + "cost", cache_info["cost"])
|
318
|
-
|
319
|
-
return table
|
320
|
-
|
321
|
-
def status_table(self, completed_tasks: List[asyncio.Task], elapsed_time: float):
|
322
|
-
summary_data = JobsRunnerStatusMixin.generate_status_summary(
|
323
|
-
completed_tasks=completed_tasks,
|
324
|
-
elapsed_time=elapsed_time,
|
325
|
-
interviews=self.total_interviews,
|
326
|
-
)
|
327
|
-
return self.display_status_table(summary_data)
|
328
|
-
|
329
|
-
|
330
|
-
if __name__ == "__main__":
|
331
|
-
import doctest
|
332
|
-
|
333
|
-
doctest.testmod(optionflags=doctest.ELLIPSIS)
|
@@ -1,100 +0,0 @@
|
|
1
|
-
import base64
|
2
|
-
import os
|
3
|
-
import requests
|
4
|
-
import tempfile
|
5
|
-
import mimetypes
|
6
|
-
from urllib.parse import urlparse
|
7
|
-
|
8
|
-
|
9
|
-
class ScenarioImageMixin:
|
10
|
-
def add_image(self, image_path: str):
|
11
|
-
"""Add an image to a scenario.
|
12
|
-
|
13
|
-
>>> from edsl.scenarios.Scenario import Scenario
|
14
|
-
>>> s = Scenario({"food": "wood chips"})
|
15
|
-
>>> s.add_image(Scenario.example_image())
|
16
|
-
Scenario({'food': 'wood chips', 'file_path': '...', 'encoded_image': '...'})
|
17
|
-
"""
|
18
|
-
new_scenario = self.from_image(image_path)
|
19
|
-
return self + new_scenario
|
20
|
-
|
21
|
-
@staticmethod
|
22
|
-
def example_image():
|
23
|
-
"""Return an example image path."""
|
24
|
-
import os
|
25
|
-
|
26
|
-
base_path = os.path.dirname(os.path.abspath(__file__))
|
27
|
-
return os.path.join(base_path, "../../static/logo.png")
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def from_image(cls, image_path: str) -> "Scenario":
|
31
|
-
"""Creates a scenario with a base64 encoding of an image.
|
32
|
-
|
33
|
-
>>> from edsl.scenarios.Scenario import Scenario
|
34
|
-
>>> s = Scenario.from_image(Scenario.example_image())
|
35
|
-
>>> s
|
36
|
-
Scenario({'file_path': '...', 'encoded_image': '...'})
|
37
|
-
"""
|
38
|
-
|
39
|
-
if image_path.startswith("http://") or image_path.startswith("https://"):
|
40
|
-
return cls._from_url_image(image_path)
|
41
|
-
else:
|
42
|
-
return cls._from_filepath_image(image_path)
|
43
|
-
|
44
|
-
@classmethod
|
45
|
-
def _from_url_image(cls, image_url: str) -> "Scenario":
|
46
|
-
"""Handles downloading and encoding an image from a URL."""
|
47
|
-
response = requests.get(image_url)
|
48
|
-
if response.status_code == 200:
|
49
|
-
# Try to extract the file extension from the URL
|
50
|
-
parsed_url = urlparse(image_url)
|
51
|
-
file_name = parsed_url.path.split("/")[-1]
|
52
|
-
file_extension = file_name.split(".")[-1] if "." in file_name else None
|
53
|
-
|
54
|
-
# If the file extension is not found in the URL, use the content type
|
55
|
-
if not file_extension:
|
56
|
-
content_type = response.headers.get("Content-Type")
|
57
|
-
file_extension = mimetypes.guess_extension(content_type)
|
58
|
-
|
59
|
-
# If still no file extension, use a generic binary extension
|
60
|
-
if not file_extension:
|
61
|
-
file_extension = ".bin"
|
62
|
-
|
63
|
-
# Create a temporary file with the appropriate extension
|
64
|
-
with tempfile.NamedTemporaryFile(
|
65
|
-
delete=False, suffix=file_extension
|
66
|
-
) as temp_file:
|
67
|
-
# Write the image content to the temporary file
|
68
|
-
temp_file.write(response.content)
|
69
|
-
temp_file_name = temp_file.name
|
70
|
-
else:
|
71
|
-
raise ValueError("Failed to download the image.")
|
72
|
-
|
73
|
-
scenario = cls._from_filepath_image(temp_file_name)
|
74
|
-
os.remove(temp_file_name)
|
75
|
-
return scenario
|
76
|
-
|
77
|
-
@classmethod
|
78
|
-
def _from_filepath_image(cls, image_path: str) -> "Scenario":
|
79
|
-
"""Handles encoding an image from a local file path."""
|
80
|
-
with open(image_path, "rb") as image_file:
|
81
|
-
s = cls(
|
82
|
-
{
|
83
|
-
"file_path": image_path,
|
84
|
-
"encoded_image": base64.b64encode(image_file.read()).decode(
|
85
|
-
"utf-8"
|
86
|
-
),
|
87
|
-
}
|
88
|
-
)
|
89
|
-
s._has_image = True
|
90
|
-
return s
|
91
|
-
|
92
|
-
def __repr__(self):
|
93
|
-
return f"Scenario({self.data})"
|
94
|
-
|
95
|
-
|
96
|
-
if __name__ == "__main__":
|
97
|
-
import doctest
|
98
|
-
from edsl.scenarios.Scenario import Scenario
|
99
|
-
|
100
|
-
doctest.testmod(extraglobs={"Scenario": Scenario}, optionflags=doctest.ELLIPSIS)
|
File without changes
|
File without changes
|