edsl 0.1.33.dev1__py3-none-any.whl → 0.1.33.dev2__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/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -4
- edsl/agents/Agent.py +46 -14
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +125 -212
- edsl/agents/InvigilatorBase.py +140 -32
- edsl/agents/PromptConstructionMixin.py +43 -66
- edsl/agents/__init__.py +1 -0
- edsl/auto/AutoStudy.py +117 -0
- edsl/auto/StageBase.py +230 -0
- edsl/auto/StageGenerateSurvey.py +178 -0
- edsl/auto/StageLabelQuestions.py +125 -0
- edsl/auto/StagePersona.py +61 -0
- edsl/auto/StagePersonaDimensionValueRanges.py +88 -0
- edsl/auto/StagePersonaDimensionValues.py +74 -0
- edsl/auto/StagePersonaDimensions.py +69 -0
- edsl/auto/StageQuestions.py +73 -0
- edsl/auto/SurveyCreatorPipeline.py +21 -0
- edsl/auto/utilities.py +224 -0
- edsl/config.py +38 -39
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +39 -5
- edsl/data/Cache.py +35 -1
- edsl/data_transfer_models.py +120 -38
- edsl/enums.py +2 -0
- edsl/exceptions/language_models.py +25 -1
- edsl/exceptions/questions.py +62 -5
- edsl/exceptions/results.py +4 -0
- edsl/inference_services/AnthropicService.py +13 -11
- edsl/inference_services/AwsBedrock.py +19 -17
- edsl/inference_services/AzureAI.py +37 -20
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +2 -0
- edsl/inference_services/InferenceServiceABC.py +24 -0
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OpenAIService.py +41 -50
- edsl/inference_services/TestService.py +71 -0
- edsl/inference_services/models_available_cache.py +0 -6
- edsl/inference_services/registry.py +4 -0
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +18 -13
- edsl/jobs/buckets/TokenBucket.py +39 -14
- edsl/jobs/interviews/Interview.py +297 -77
- edsl/jobs/interviews/InterviewExceptionEntry.py +83 -19
- edsl/jobs/interviews/interview_exception_tracking.py +0 -70
- edsl/jobs/interviews/retry_management.py +3 -1
- edsl/jobs/runners/JobsRunnerAsyncio.py +116 -70
- edsl/jobs/runners/JobsRunnerStatusMixin.py +1 -1
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskHistory.py +131 -213
- edsl/language_models/LanguageModel.py +239 -129
- edsl/language_models/ModelList.py +2 -2
- edsl/language_models/RegisterLanguageModelsMeta.py +14 -29
- edsl/language_models/fake_openai_call.py +15 -0
- edsl/language_models/fake_openai_service.py +61 -0
- edsl/language_models/registry.py +15 -2
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +273 -242
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +6 -0
- edsl/questions/QuestionCheckBox.py +227 -35
- edsl/questions/QuestionExtract.py +98 -27
- edsl/questions/QuestionFreeText.py +46 -29
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -22
- edsl/questions/QuestionMultipleChoice.py +173 -64
- edsl/questions/QuestionNumerical.py +87 -46
- edsl/questions/QuestionRank.py +182 -24
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +169 -0
- edsl/questions/__init__.py +3 -4
- edsl/questions/decorators.py +21 -0
- edsl/questions/derived/QuestionLikertFive.py +10 -5
- edsl/questions/derived/QuestionLinearScale.py +11 -1
- edsl/questions/derived/QuestionTopK.py +6 -0
- edsl/questions/derived/QuestionYesNo.py +16 -1
- edsl/questions/descriptors.py +43 -7
- edsl/questions/prompt_templates/question_budget.jinja +13 -0
- edsl/questions/prompt_templates/question_checkbox.jinja +32 -0
- edsl/questions/prompt_templates/question_extract.jinja +11 -0
- edsl/questions/prompt_templates/question_free_text.jinja +3 -0
- edsl/questions/prompt_templates/question_linear_scale.jinja +11 -0
- edsl/questions/prompt_templates/question_list.jinja +17 -0
- edsl/questions/prompt_templates/question_multiple_choice.jinja +33 -0
- edsl/questions/prompt_templates/question_numerical.jinja +37 -0
- edsl/questions/question_registry.py +6 -2
- edsl/questions/templates/__init__.py +0 -0
- edsl/questions/templates/checkbox/__init__.py +0 -0
- edsl/questions/templates/checkbox/answering_instructions.jinja +10 -0
- edsl/questions/templates/checkbox/question_presentation.jinja +22 -0
- edsl/questions/templates/extract/answering_instructions.jinja +7 -0
- edsl/questions/templates/extract/question_presentation.jinja +1 -0
- edsl/questions/templates/free_text/__init__.py +0 -0
- edsl/questions/templates/free_text/answering_instructions.jinja +0 -0
- edsl/questions/templates/free_text/question_presentation.jinja +1 -0
- edsl/questions/templates/likert_five/__init__.py +0 -0
- edsl/questions/templates/likert_five/answering_instructions.jinja +10 -0
- edsl/questions/templates/likert_five/question_presentation.jinja +12 -0
- edsl/questions/templates/linear_scale/__init__.py +0 -0
- edsl/questions/templates/linear_scale/answering_instructions.jinja +5 -0
- edsl/questions/templates/linear_scale/question_presentation.jinja +5 -0
- edsl/questions/templates/list/__init__.py +0 -0
- edsl/questions/templates/list/answering_instructions.jinja +4 -0
- edsl/questions/templates/list/question_presentation.jinja +5 -0
- edsl/questions/templates/multiple_choice/__init__.py +0 -0
- edsl/questions/templates/multiple_choice/answering_instructions.jinja +9 -0
- edsl/questions/templates/multiple_choice/html.jinja +0 -0
- edsl/questions/templates/multiple_choice/question_presentation.jinja +12 -0
- edsl/questions/templates/numerical/__init__.py +0 -0
- edsl/questions/templates/numerical/answering_instructions.jinja +8 -0
- edsl/questions/templates/numerical/question_presentation.jinja +7 -0
- edsl/questions/templates/rank/answering_instructions.jinja +11 -0
- edsl/questions/templates/rank/question_presentation.jinja +15 -0
- edsl/questions/templates/top_k/__init__.py +0 -0
- edsl/questions/templates/top_k/answering_instructions.jinja +8 -0
- edsl/questions/templates/top_k/question_presentation.jinja +22 -0
- edsl/questions/templates/yes_no/__init__.py +0 -0
- edsl/questions/templates/yes_no/answering_instructions.jinja +6 -0
- edsl/questions/templates/yes_no/question_presentation.jinja +12 -0
- edsl/results/Dataset.py +20 -0
- edsl/results/DatasetExportMixin.py +41 -47
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +131 -45
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/Scenario.py +10 -4
- edsl/scenarios/ScenarioList.py +348 -39
- edsl/scenarios/ScenarioListExportMixin.py +9 -0
- edsl/study/SnapShot.py +8 -1
- edsl/surveys/RuleCollection.py +2 -2
- edsl/surveys/Survey.py +634 -315
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +75 -4
- edsl/surveys/instructions/ChangeInstruction.py +47 -0
- edsl/surveys/instructions/Instruction.py +34 -0
- edsl/surveys/instructions/InstructionCollection.py +77 -0
- edsl/surveys/instructions/__init__.py +0 -0
- edsl/templates/error_reporting/base.html +24 -0
- edsl/templates/error_reporting/exceptions_by_model.html +35 -0
- edsl/templates/error_reporting/exceptions_by_question_name.html +17 -0
- edsl/templates/error_reporting/exceptions_by_type.html +17 -0
- edsl/templates/error_reporting/interview_details.html +111 -0
- edsl/templates/error_reporting/interviews.html +10 -0
- edsl/templates/error_reporting/overview.html +5 -0
- edsl/templates/error_reporting/performance_plot.html +2 -0
- edsl/templates/error_reporting/report.css +74 -0
- edsl/templates/error_reporting/report.html +118 -0
- edsl/templates/error_reporting/report.js +25 -0
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/METADATA +4 -2
- edsl-0.1.33.dev2.dist-info/RECORD +289 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -286
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.33.dev1.dist-info/RECORD +0 -209
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/LICENSE +0 -0
- {edsl-0.1.33.dev1.dist-info → edsl-0.1.33.dev2.dist-info}/WHEEL +0 -0
edsl/results/Results.py
CHANGED
@@ -17,6 +17,7 @@ from edsl.exceptions.results import (
|
|
17
17
|
ResultsInvalidNameError,
|
18
18
|
ResultsMutateError,
|
19
19
|
ResultsFilterError,
|
20
|
+
ResultsDeserializationError,
|
20
21
|
)
|
21
22
|
|
22
23
|
from edsl.results.ResultsExportMixin import ResultsExportMixin
|
@@ -77,6 +78,7 @@ class Results(UserList, Mixins, Base):
|
|
77
78
|
"question_options",
|
78
79
|
"question_type",
|
79
80
|
"comment",
|
81
|
+
"generated_tokens",
|
80
82
|
]
|
81
83
|
|
82
84
|
def __init__(
|
@@ -108,6 +110,81 @@ class Results(UserList, Mixins, Base):
|
|
108
110
|
if hasattr(self, "_add_output_functions"):
|
109
111
|
self._add_output_functions()
|
110
112
|
|
113
|
+
def leaves(self):
|
114
|
+
leaves = []
|
115
|
+
for result in self:
|
116
|
+
leaves.extend(result.leaves())
|
117
|
+
return leaves
|
118
|
+
|
119
|
+
def tree(
|
120
|
+
self,
|
121
|
+
fold_attributes: Optional[List[str]] = None,
|
122
|
+
drop: Optional[List[str]] = None,
|
123
|
+
open_file=True,
|
124
|
+
) -> dict:
|
125
|
+
"""Return the results as a tree."""
|
126
|
+
from edsl.results.tree_explore import FoldableHTMLTableGenerator
|
127
|
+
|
128
|
+
if drop is None:
|
129
|
+
drop = []
|
130
|
+
|
131
|
+
valid_attributes = [
|
132
|
+
"model",
|
133
|
+
"scenario",
|
134
|
+
"agent",
|
135
|
+
"answer",
|
136
|
+
"question",
|
137
|
+
"iteration",
|
138
|
+
]
|
139
|
+
if fold_attributes is None:
|
140
|
+
fold_attributes = []
|
141
|
+
|
142
|
+
for attribute in fold_attributes:
|
143
|
+
if attribute not in valid_attributes:
|
144
|
+
raise ValueError(
|
145
|
+
f"Invalid fold attribute: {attribute}; must be in {valid_attributes}"
|
146
|
+
)
|
147
|
+
data = self.leaves()
|
148
|
+
generator = FoldableHTMLTableGenerator(data)
|
149
|
+
tree = generator.tree(fold_attributes=fold_attributes, drop=drop)
|
150
|
+
html_content = generator.generate_html(tree, fold_attributes)
|
151
|
+
import tempfile
|
152
|
+
from edsl.utilities.utilities import is_notebook
|
153
|
+
|
154
|
+
from IPython.display import display, HTML
|
155
|
+
|
156
|
+
if is_notebook():
|
157
|
+
import html
|
158
|
+
from IPython.display import display, HTML
|
159
|
+
|
160
|
+
height = 1000
|
161
|
+
width = 1000
|
162
|
+
escaped_output = html.escape(html_content)
|
163
|
+
# escaped_output = rendered_html
|
164
|
+
iframe = f""""
|
165
|
+
<iframe srcdoc="{ escaped_output }" style="width: {width}px; height: {height}px;"></iframe>
|
166
|
+
"""
|
167
|
+
display(HTML(iframe))
|
168
|
+
return None
|
169
|
+
|
170
|
+
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f:
|
171
|
+
f.write(html_content.encode())
|
172
|
+
print(f"HTML file has been generated: {f.name}")
|
173
|
+
|
174
|
+
if open_file:
|
175
|
+
import webbrowser
|
176
|
+
import time
|
177
|
+
|
178
|
+
time.sleep(1) # Wait for 1 second
|
179
|
+
# webbrowser.open(f.name)
|
180
|
+
import os
|
181
|
+
|
182
|
+
filename = f.name
|
183
|
+
webbrowser.open(f"file://{os.path.abspath(filename)}")
|
184
|
+
|
185
|
+
else:
|
186
|
+
return html_content
|
187
|
+
|
111
188
|
def code(self):
|
112
189
|
raise NotImplementedError
|
113
190
|
|
@@ -290,8 +367,7 @@ class Results(UserList, Mixins, Base):
|
|
290
367
|
),
|
291
368
|
)
|
292
369
|
except Exception as e:
|
293
|
-
|
294
|
-
# breakpoint()
|
370
|
+
raise ResultsDeserializationError(f"Error in Results.from_dict: {e}")
|
295
371
|
return results
|
296
372
|
|
297
373
|
######################
|
@@ -395,7 +471,7 @@ class Results(UserList, Mixins, Base):
|
|
395
471
|
|
396
472
|
>>> r = Results.example()
|
397
473
|
>>> r.models[0]
|
398
|
-
Model(model_name =
|
474
|
+
Model(model_name = ...)
|
399
475
|
"""
|
400
476
|
return [r.model for r in self.data]
|
401
477
|
|
@@ -477,39 +553,6 @@ class Results(UserList, Mixins, Base):
|
|
477
553
|
)
|
478
554
|
return sorted(list(all_keys))
|
479
555
|
|
480
|
-
def _parse_column(self, column: str) -> tuple[str, str]:
|
481
|
-
"""
|
482
|
-
Parses a column name into a tuple containing a data type and a key.
|
483
|
-
|
484
|
-
>>> r = Results.example()
|
485
|
-
>>> r._parse_column("answer.how_feeling")
|
486
|
-
('answer', 'how_feeling')
|
487
|
-
|
488
|
-
The standard way a column is specified is with a dot-separated string, e.g. _parse_column("agent.status")
|
489
|
-
But you can also specify a single key, e.g. "status", in which case it will look up the data type.
|
490
|
-
"""
|
491
|
-
if "." in column:
|
492
|
-
data_type, key = column.split(".")
|
493
|
-
else:
|
494
|
-
try:
|
495
|
-
data_type, key = self._key_to_data_type[column], column
|
496
|
-
except KeyError:
|
497
|
-
import difflib
|
498
|
-
|
499
|
-
close_matches = difflib.get_close_matches(
|
500
|
-
column, self._key_to_data_type.keys()
|
501
|
-
)
|
502
|
-
if close_matches:
|
503
|
-
suggestions = ", ".join(close_matches)
|
504
|
-
raise ResultsColumnNotFoundError(
|
505
|
-
f"Column '{column}' not found in data. Did you mean: {suggestions}?"
|
506
|
-
)
|
507
|
-
else:
|
508
|
-
raise ResultsColumnNotFoundError(
|
509
|
-
f"Column {column} not found in data"
|
510
|
-
)
|
511
|
-
return data_type, key
|
512
|
-
|
513
556
|
def first(self) -> "Result":
|
514
557
|
"""Return the first observation in the results.
|
515
558
|
|
@@ -632,9 +675,11 @@ class Results(UserList, Mixins, Base):
|
|
632
675
|
"""
|
633
676
|
if functions_dict is None:
|
634
677
|
functions_dict = {}
|
635
|
-
|
678
|
+
evaluator = EvalWithCompoundTypes(
|
636
679
|
names=result.combined_dict, functions=functions_dict
|
637
680
|
)
|
681
|
+
evaluator.functions.update(int=int, float=float)
|
682
|
+
return evaluator
|
638
683
|
|
639
684
|
def mutate(
|
640
685
|
self, new_var_string: str, functions_dict: Optional[dict] = None
|
@@ -721,8 +766,8 @@ class Results(UserList, Mixins, Base):
|
|
721
766
|
|
722
767
|
def sample(
|
723
768
|
self,
|
724
|
-
n: int = None,
|
725
|
-
frac: float = None,
|
769
|
+
n: Optional[int] = None,
|
770
|
+
frac: Optional[float] = None,
|
726
771
|
with_replacement: bool = True,
|
727
772
|
seed: Optional[str] = "edsl",
|
728
773
|
) -> Results:
|
@@ -771,13 +816,17 @@ class Results(UserList, Mixins, Base):
|
|
771
816
|
Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
|
772
817
|
|
773
818
|
>>> results.select('how_feeling', 'model', 'how_feeling')
|
774
|
-
Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'model.model': ['
|
819
|
+
Dataset([{'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}, {'model.model': ['...', '...', '...', '...']}, {'answer.how_feeling': ['OK', 'Great', 'Terrible', 'OK']}])
|
820
|
+
|
821
|
+
>>> from edsl import Results; r = Results.example(); r.select('answer.how_feeling_y')
|
822
|
+
Dataset([{'answer.how_feeling_yesterday': ['Great', 'Good', 'OK', 'Terrible']}])
|
775
823
|
"""
|
776
824
|
|
777
|
-
if len(self) == 0:
|
778
|
-
|
825
|
+
# if len(self) == 0:
|
826
|
+
# raise Exception("No data to select from---the Results object is empty.")
|
779
827
|
|
780
828
|
if not columns or columns == ("*",) or columns == (None,):
|
829
|
+
# is the users passes nothing, then we'll return all the columns
|
781
830
|
columns = ("*.*",)
|
782
831
|
|
783
832
|
if isinstance(columns[0], list):
|
@@ -801,6 +850,16 @@ class Results(UserList, Mixins, Base):
|
|
801
850
|
# iterate through the passed columns
|
802
851
|
for column in columns:
|
803
852
|
# a user could pass 'result.how_feeling' or just 'how_feeling'
|
853
|
+
matches = self._matching_columns(column)
|
854
|
+
if len(matches) > 1:
|
855
|
+
raise Exception(
|
856
|
+
f"Column '{column}' is ambiguous. Did you mean one of {matches}?"
|
857
|
+
)
|
858
|
+
if len(matches) == 0 and ".*" not in column:
|
859
|
+
raise Exception(f"Column '{column}' not found in data.")
|
860
|
+
if len(matches) == 1:
|
861
|
+
column = matches[0]
|
862
|
+
|
804
863
|
parsed_data_type, parsed_key = self._parse_column(column)
|
805
864
|
data_types = get_data_types_to_return(parsed_data_type)
|
806
865
|
found_once = False # we need to track this to make sure we found the key at least once
|
@@ -843,6 +902,21 @@ class Results(UserList, Mixins, Base):
|
|
843
902
|
|
844
903
|
return Dataset(sorted_new_data)
|
845
904
|
|
905
|
+
def select(self, *columns: Union[str, list[str]]) -> "Results":
|
906
|
+
from edsl.results.Selector import Selector
|
907
|
+
|
908
|
+
if len(self) == 0:
|
909
|
+
raise Exception("No data to select from---the Results object is empty.")
|
910
|
+
|
911
|
+
selector = Selector(
|
912
|
+
known_data_types=self.known_data_types,
|
913
|
+
data_type_to_keys=self._data_type_to_keys,
|
914
|
+
key_to_data_type=self._key_to_data_type,
|
915
|
+
fetch_list_func=self._fetch_list,
|
916
|
+
columns=self.columns,
|
917
|
+
)
|
918
|
+
return selector.select(*columns)
|
919
|
+
|
846
920
|
def sort_by(self, *columns: str, reverse: bool = False) -> Results:
|
847
921
|
import warnings
|
848
922
|
|
@@ -851,6 +925,11 @@ class Results(UserList, Mixins, Base):
|
|
851
925
|
)
|
852
926
|
return self.order_by(*columns, reverse=reverse)
|
853
927
|
|
928
|
+
def _parse_column(self, column: str) -> tuple[str, str]:
|
929
|
+
if "." in column:
|
930
|
+
return column.split(".")
|
931
|
+
return self._key_to_data_type[column], column
|
932
|
+
|
854
933
|
def order_by(self, *columns: str, reverse: bool = False) -> Results:
|
855
934
|
"""Sort the results by one or more columns.
|
856
935
|
|
@@ -948,7 +1027,9 @@ class Results(UserList, Mixins, Base):
|
|
948
1027
|
def has_single_equals(string):
|
949
1028
|
if "!=" in string:
|
950
1029
|
return False
|
951
|
-
if "=" in string and not
|
1030
|
+
if "=" in string and not (
|
1031
|
+
"==" in string or "<=" in string or ">=" in string
|
1032
|
+
):
|
952
1033
|
return True
|
953
1034
|
|
954
1035
|
if has_single_equals(expression):
|
@@ -989,7 +1070,7 @@ class Results(UserList, Mixins, Base):
|
|
989
1070
|
return Results(survey=self.survey, data=new_data, created_columns=None)
|
990
1071
|
|
991
1072
|
@classmethod
|
992
|
-
def example(cls,
|
1073
|
+
def example(cls, randomize: bool = False) -> Results:
|
993
1074
|
"""Return an example `Results` object.
|
994
1075
|
|
995
1076
|
Example usage:
|
@@ -1003,7 +1084,12 @@ class Results(UserList, Mixins, Base):
|
|
1003
1084
|
|
1004
1085
|
c = Cache()
|
1005
1086
|
job = Jobs.example(randomize=randomize)
|
1006
|
-
results = job.run(
|
1087
|
+
results = job.run(
|
1088
|
+
cache=c,
|
1089
|
+
stop_on_exception=True,
|
1090
|
+
skip_retry=True,
|
1091
|
+
raise_validation_errors=True,
|
1092
|
+
)
|
1007
1093
|
return results
|
1008
1094
|
|
1009
1095
|
def rich_print(self):
|
edsl/results/ResultsDBMixin.py
CHANGED
@@ -136,9 +136,9 @@ class ResultsDBMixin:
|
|
136
136
|
|
137
137
|
>>> from edsl.results import Results
|
138
138
|
>>> r = Results.example()
|
139
|
-
>>> d = r.sql("select data_type, key, value from self where data_type = 'answer' limit 3", shape="long")
|
140
|
-
>>> list(d['value'])
|
141
|
-
['
|
139
|
+
>>> d = r.sql("select data_type, key, value from self where data_type = 'answer' order by value limit 3", shape="long")
|
140
|
+
>>> sorted(list(d['value']))
|
141
|
+
['Good', 'Great', 'Great']
|
142
142
|
|
143
143
|
We can also return the data in wide format.
|
144
144
|
Note the use of single quotes to escape the column names, as required by sql.
|
edsl/results/Selector.py
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
from typing import Union, List, Dict, Any
|
2
|
+
from collections import defaultdict
|
3
|
+
from edsl.results.Dataset import Dataset
|
4
|
+
|
5
|
+
|
6
|
+
class Selector:
|
7
|
+
def __init__(
|
8
|
+
self,
|
9
|
+
known_data_types: List[str],
|
10
|
+
data_type_to_keys: Dict[str, List[str]],
|
11
|
+
key_to_data_type: Dict[str, str],
|
12
|
+
fetch_list_func,
|
13
|
+
columns: List[str],
|
14
|
+
):
|
15
|
+
self.known_data_types = known_data_types
|
16
|
+
self._data_type_to_keys = data_type_to_keys
|
17
|
+
self._key_to_data_type = key_to_data_type
|
18
|
+
self._fetch_list = fetch_list_func
|
19
|
+
self.columns = columns
|
20
|
+
|
21
|
+
def select(self, *columns: Union[str, List[str]]) -> "Dataset":
|
22
|
+
columns = self._normalize_columns(columns)
|
23
|
+
to_fetch = self._get_columns_to_fetch(columns)
|
24
|
+
new_data = self._fetch_data(to_fetch)
|
25
|
+
return Dataset(new_data)
|
26
|
+
|
27
|
+
def _normalize_columns(self, columns: Union[str, List[str]]) -> tuple:
|
28
|
+
if not columns or columns == ("*",) or columns == (None,):
|
29
|
+
return ("*.*",)
|
30
|
+
if isinstance(columns[0], list):
|
31
|
+
return tuple(columns[0])
|
32
|
+
return columns
|
33
|
+
|
34
|
+
def _get_columns_to_fetch(self, columns: tuple) -> Dict[str, List[str]]:
|
35
|
+
to_fetch = defaultdict(list)
|
36
|
+
self.items_in_order = []
|
37
|
+
|
38
|
+
for column in columns:
|
39
|
+
matches = self._find_matching_columns(column)
|
40
|
+
self._validate_matches(column, matches)
|
41
|
+
|
42
|
+
if len(matches) == 1:
|
43
|
+
column = matches[0]
|
44
|
+
|
45
|
+
data_type, key = self._parse_column(column)
|
46
|
+
self._process_column(data_type, key, to_fetch)
|
47
|
+
|
48
|
+
return to_fetch
|
49
|
+
|
50
|
+
def _find_matching_columns(self, partial_name: str) -> list[str]:
|
51
|
+
if "." in partial_name:
|
52
|
+
search_in_list = self.columns
|
53
|
+
else:
|
54
|
+
search_in_list = [s.split(".")[1] for s in self.columns]
|
55
|
+
|
56
|
+
matches = [s for s in search_in_list if s.startswith(partial_name)]
|
57
|
+
return [partial_name] if partial_name in matches else matches
|
58
|
+
|
59
|
+
def _validate_matches(self, column: str, matches: List[str]):
|
60
|
+
if len(matches) > 1:
|
61
|
+
raise ValueError(
|
62
|
+
f"Column '{column}' is ambiguous. Did you mean one of {matches}?"
|
63
|
+
)
|
64
|
+
if len(matches) == 0 and ".*" not in column:
|
65
|
+
raise ValueError(f"Column '{column}' not found in data.")
|
66
|
+
|
67
|
+
def _parse_column(self, column: str) -> tuple[str, str]:
|
68
|
+
if "." in column:
|
69
|
+
return column.split(".")
|
70
|
+
try:
|
71
|
+
return self._key_to_data_type[column], column
|
72
|
+
except KeyError:
|
73
|
+
self._raise_key_error(column)
|
74
|
+
|
75
|
+
def _raise_key_error(self, column: str):
|
76
|
+
import difflib
|
77
|
+
|
78
|
+
close_matches = difflib.get_close_matches(column, self._key_to_data_type.keys())
|
79
|
+
if close_matches:
|
80
|
+
suggestions = ", ".join(close_matches)
|
81
|
+
raise KeyError(
|
82
|
+
f"Column '{column}' not found in data. Did you mean: {suggestions}?"
|
83
|
+
)
|
84
|
+
else:
|
85
|
+
raise KeyError(f"Column {column} not found in data")
|
86
|
+
|
87
|
+
def _process_column(self, data_type: str, key: str, to_fetch: Dict[str, List[str]]):
|
88
|
+
data_types = self._get_data_types_to_return(data_type)
|
89
|
+
found_once = False
|
90
|
+
|
91
|
+
for dt in data_types:
|
92
|
+
relevant_keys = self._data_type_to_keys[dt]
|
93
|
+
for k in relevant_keys:
|
94
|
+
if k == key or key == "*":
|
95
|
+
found_once = True
|
96
|
+
to_fetch[dt].append(k)
|
97
|
+
self.items_in_order.append(f"{dt}.{k}")
|
98
|
+
|
99
|
+
if not found_once:
|
100
|
+
raise ValueError(f"Key {key} not found in data.")
|
101
|
+
|
102
|
+
def _get_data_types_to_return(self, parsed_data_type: str) -> List[str]:
|
103
|
+
if parsed_data_type == "*":
|
104
|
+
return self.known_data_types
|
105
|
+
if parsed_data_type not in self.known_data_types:
|
106
|
+
raise ValueError(
|
107
|
+
f"Data type {parsed_data_type} not found in data. Did you mean one of {self.known_data_types}"
|
108
|
+
)
|
109
|
+
return [parsed_data_type]
|
110
|
+
|
111
|
+
def _fetch_data(self, to_fetch: Dict[str, List[str]]) -> List[Dict[str, Any]]:
|
112
|
+
new_data = []
|
113
|
+
for data_type, keys in to_fetch.items():
|
114
|
+
for key in keys:
|
115
|
+
entries = self._fetch_list(data_type, key)
|
116
|
+
new_data.append({f"{data_type}.{key}": entries})
|
117
|
+
|
118
|
+
return [d for key in self.items_in_order for d in new_data if key in d]
|
@@ -0,0 +1,115 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from typing import List, Dict, Any
|
3
|
+
import json
|
4
|
+
|
5
|
+
|
6
|
+
class FoldableHTMLTableGenerator:
|
7
|
+
def __init__(self, data: List[Dict[str, Any]]):
|
8
|
+
self.data = data
|
9
|
+
|
10
|
+
def tree(self, fold_attributes: List[str], drop: List[str] = None) -> Dict:
|
11
|
+
def nested_dict():
|
12
|
+
return defaultdict(nested_dict)
|
13
|
+
|
14
|
+
result = nested_dict()
|
15
|
+
drop = drop or [] # Use an empty list if drop is None
|
16
|
+
|
17
|
+
for item in self.data:
|
18
|
+
current = result
|
19
|
+
for attr in fold_attributes:
|
20
|
+
current = current[item[attr]]
|
21
|
+
|
22
|
+
row = {
|
23
|
+
k: v
|
24
|
+
for k, v in item.items()
|
25
|
+
if k not in fold_attributes and k not in drop
|
26
|
+
}
|
27
|
+
if "_rows" not in current:
|
28
|
+
current["_rows"] = []
|
29
|
+
current["_rows"].append(row)
|
30
|
+
|
31
|
+
return result
|
32
|
+
|
33
|
+
def generate_html(self, tree, fold_attributes: List[str]) -> str:
|
34
|
+
html_content = """
|
35
|
+
<!DOCTYPE html>
|
36
|
+
<html lang="en">
|
37
|
+
<head>
|
38
|
+
<meta charset="UTF-8">
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
40
|
+
<title>Foldable Nested Table</title>
|
41
|
+
<style>
|
42
|
+
.folding-section { margin-left: 20px; }
|
43
|
+
.fold-button { cursor: pointer; margin: 5px 0; }
|
44
|
+
table { border-collapse: collapse; margin-top: 10px; }
|
45
|
+
th, td { border: 1px solid black; padding: 5px; }
|
46
|
+
.attribute-label { font-weight: bold; }
|
47
|
+
</style>
|
48
|
+
</head>
|
49
|
+
<body>
|
50
|
+
<div id="root"></div>
|
51
|
+
<script>
|
52
|
+
function toggleFold(id) {
|
53
|
+
const element = document.getElementById(id);
|
54
|
+
element.style.display = element.style.display === 'none' ? 'block' : 'none';
|
55
|
+
}
|
56
|
+
|
57
|
+
function createFoldableSection(data, path = [], attributes = %s) {
|
58
|
+
const container = document.createElement('div');
|
59
|
+
container.className = 'folding-section';
|
60
|
+
|
61
|
+
for (const [key, value] of Object.entries(data)) {
|
62
|
+
if (key === '_rows') {
|
63
|
+
const table = document.createElement('table');
|
64
|
+
const headerRow = table.insertRow();
|
65
|
+
const headers = Object.keys(value[0]);
|
66
|
+
headers.forEach(header => {
|
67
|
+
const th = document.createElement('th');
|
68
|
+
th.textContent = header;
|
69
|
+
headerRow.appendChild(th);
|
70
|
+
});
|
71
|
+
value.forEach(row => {
|
72
|
+
const tableRow = table.insertRow();
|
73
|
+
headers.forEach(header => {
|
74
|
+
const cell = tableRow.insertCell();
|
75
|
+
cell.textContent = row[header];
|
76
|
+
});
|
77
|
+
});
|
78
|
+
container.appendChild(table);
|
79
|
+
} else {
|
80
|
+
const button = document.createElement('button');
|
81
|
+
const attributeType = attributes[path.length];
|
82
|
+
button.innerHTML = `<span class="attribute-label">${attributeType}:</span> ${key}`;
|
83
|
+
button.className = 'fold-button';
|
84
|
+
const sectionId = `section-${path.join('-')}-${key}`;
|
85
|
+
button.onclick = () => toggleFold(sectionId);
|
86
|
+
container.appendChild(button);
|
87
|
+
|
88
|
+
const section = document.createElement('div');
|
89
|
+
section.id = sectionId;
|
90
|
+
section.style.display = 'none';
|
91
|
+
section.appendChild(createFoldableSection(value, [...path, key], attributes));
|
92
|
+
container.appendChild(section);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
return container;
|
97
|
+
}
|
98
|
+
|
99
|
+
const treeData = %s;
|
100
|
+
document.getElementById('root').appendChild(createFoldableSection(treeData));
|
101
|
+
</script>
|
102
|
+
</body>
|
103
|
+
</html>
|
104
|
+
"""
|
105
|
+
|
106
|
+
return html_content % (json.dumps(fold_attributes), json.dumps(tree))
|
107
|
+
|
108
|
+
def save_html(self, fold_attributes: List[str], filename: str = "output.html"):
|
109
|
+
tree = self.tree(fold_attributes)
|
110
|
+
html_content = self.generate_html(tree, fold_attributes)
|
111
|
+
|
112
|
+
with open(filename, "w", encoding="utf-8") as f:
|
113
|
+
f.write(html_content)
|
114
|
+
|
115
|
+
print(f"HTML file has been generated: {filename}")
|
edsl/scenarios/Scenario.py
CHANGED
@@ -219,10 +219,9 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
219
219
|
|
220
220
|
@classmethod
|
221
221
|
def from_pdf(cls, pdf_path):
|
222
|
-
import fitz # PyMuPDF
|
223
|
-
from edsl import Scenario
|
224
|
-
|
225
222
|
# Ensure the file exists
|
223
|
+
import fitz
|
224
|
+
|
226
225
|
if not os.path.exists(pdf_path):
|
227
226
|
raise FileNotFoundError(f"The file {pdf_path} does not exist.")
|
228
227
|
|
@@ -236,7 +235,14 @@ class Scenario(Base, UserDict, ScenarioImageMixin, ScenarioHtmlMixin):
|
|
236
235
|
text = ""
|
237
236
|
for page_num in range(len(document)):
|
238
237
|
page = document.load_page(page_num)
|
239
|
-
|
238
|
+
blocks = page.get_text("blocks") # Extract text blocks
|
239
|
+
|
240
|
+
# Sort blocks by their vertical position (y0) to maintain reading order
|
241
|
+
blocks.sort(key=lambda b: (b[1], b[0])) # Sort by y0 first, then x0
|
242
|
+
|
243
|
+
# Combine the text blocks in order
|
244
|
+
for block in blocks:
|
245
|
+
text += block[4] + "\n"
|
240
246
|
|
241
247
|
# Create a dictionary for the combined text
|
242
248
|
page_info = {"filename": filename, "text": text}
|