edsl 0.1.31.dev4__py3-none-any.whl → 0.1.33__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 +9 -3
- edsl/TemplateLoader.py +24 -0
- edsl/__init__.py +8 -3
- edsl/__version__.py +1 -1
- edsl/agents/Agent.py +40 -8
- edsl/agents/AgentList.py +43 -0
- edsl/agents/Invigilator.py +136 -221
- edsl/agents/InvigilatorBase.py +148 -59
- edsl/agents/{PromptConstructionMixin.py → PromptConstructor.py} +154 -85
- 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 +48 -47
- edsl/conjure/Conjure.py +6 -0
- edsl/coop/PriceFetcher.py +58 -0
- edsl/coop/coop.py +50 -7
- edsl/data/Cache.py +35 -1
- edsl/data/CacheHandler.py +3 -4
- edsl/data_transfer_models.py +73 -38
- edsl/enums.py +8 -0
- edsl/exceptions/general.py +10 -8
- 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 +112 -0
- edsl/inference_services/AzureAI.py +214 -0
- edsl/inference_services/DeepInfraService.py +4 -3
- edsl/inference_services/GoogleService.py +16 -12
- edsl/inference_services/GroqService.py +5 -4
- edsl/inference_services/InferenceServiceABC.py +58 -3
- edsl/inference_services/InferenceServicesCollection.py +13 -8
- edsl/inference_services/MistralAIService.py +120 -0
- edsl/inference_services/OllamaService.py +18 -0
- edsl/inference_services/OpenAIService.py +55 -56
- edsl/inference_services/TestService.py +80 -0
- edsl/inference_services/TogetherAIService.py +170 -0
- edsl/inference_services/models_available_cache.py +25 -0
- edsl/inference_services/registry.py +19 -1
- edsl/jobs/Answers.py +10 -12
- edsl/jobs/FailedQuestion.py +78 -0
- edsl/jobs/Jobs.py +137 -41
- edsl/jobs/buckets/BucketCollection.py +24 -15
- edsl/jobs/buckets/TokenBucket.py +105 -18
- edsl/jobs/interviews/Interview.py +393 -83
- edsl/jobs/interviews/{interview_exception_tracking.py → InterviewExceptionCollection.py} +22 -18
- edsl/jobs/interviews/InterviewExceptionEntry.py +167 -0
- edsl/jobs/runners/JobsRunnerAsyncio.py +152 -160
- edsl/jobs/runners/JobsRunnerStatus.py +331 -0
- edsl/jobs/tasks/QuestionTaskCreator.py +30 -23
- edsl/jobs/tasks/TaskCreators.py +1 -1
- edsl/jobs/tasks/TaskHistory.py +205 -126
- edsl/language_models/LanguageModel.py +297 -177
- 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 +25 -8
- edsl/language_models/repair.py +0 -19
- edsl/language_models/utilities.py +61 -0
- edsl/notebooks/Notebook.py +20 -2
- edsl/prompts/Prompt.py +52 -2
- edsl/questions/AnswerValidatorMixin.py +23 -26
- edsl/questions/QuestionBase.py +330 -249
- edsl/questions/QuestionBaseGenMixin.py +133 -0
- edsl/questions/QuestionBasePromptsMixin.py +266 -0
- edsl/questions/QuestionBudget.py +99 -42
- edsl/questions/QuestionCheckBox.py +227 -36
- edsl/questions/QuestionExtract.py +98 -28
- edsl/questions/QuestionFreeText.py +47 -31
- edsl/questions/QuestionFunctional.py +7 -0
- edsl/questions/QuestionList.py +141 -23
- edsl/questions/QuestionMultipleChoice.py +159 -66
- edsl/questions/QuestionNumerical.py +88 -47
- edsl/questions/QuestionRank.py +182 -25
- edsl/questions/Quick.py +41 -0
- edsl/questions/RegisterQuestionsMeta.py +31 -12
- edsl/questions/ResponseValidatorABC.py +170 -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 +15 -2
- edsl/questions/derived/QuestionTopK.py +10 -1
- edsl/questions/derived/QuestionYesNo.py +24 -3
- 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/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/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/__init__.py +0 -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/__init__.py +0 -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 +58 -30
- edsl/results/DatasetTree.py +145 -0
- edsl/results/Result.py +32 -5
- edsl/results/Results.py +135 -46
- edsl/results/ResultsDBMixin.py +3 -3
- edsl/results/Selector.py +118 -0
- edsl/results/tree_explore.py +115 -0
- edsl/scenarios/FileStore.py +71 -10
- edsl/scenarios/Scenario.py +109 -24
- edsl/scenarios/ScenarioImageMixin.py +2 -2
- edsl/scenarios/ScenarioList.py +546 -21
- edsl/scenarios/ScenarioListExportMixin.py +24 -4
- edsl/scenarios/ScenarioListPdfMixin.py +153 -4
- edsl/study/SnapShot.py +8 -1
- edsl/study/Study.py +32 -0
- edsl/surveys/Rule.py +15 -3
- edsl/surveys/RuleCollection.py +21 -5
- edsl/surveys/Survey.py +707 -298
- edsl/surveys/SurveyExportMixin.py +71 -9
- edsl/surveys/SurveyFlowVisualizationMixin.py +2 -1
- edsl/surveys/SurveyQualtricsImport.py +284 -0
- 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 +116 -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/utilities/utilities.py +40 -1
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/METADATA +8 -2
- edsl-0.1.33.dist-info/RECORD +295 -0
- edsl/jobs/interviews/InterviewTaskBuildingMixin.py +0 -271
- edsl/jobs/interviews/retry_management.py +0 -37
- edsl/jobs/runners/JobsRunnerStatusMixin.py +0 -303
- edsl/utilities/gcp_bucket/simple_example.py +0 -9
- edsl-0.1.31.dev4.dist-info/RECORD +0 -204
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/LICENSE +0 -0
- {edsl-0.1.31.dev4.dist-info → edsl-0.1.33.dist-info}/WHEEL +0 -0
@@ -2,6 +2,27 @@
|
|
2
2
|
|
3
3
|
from typing import Union, Optional
|
4
4
|
|
5
|
+
import subprocess
|
6
|
+
import platform
|
7
|
+
import os
|
8
|
+
import tempfile
|
9
|
+
|
10
|
+
|
11
|
+
def open_docx(file_path):
|
12
|
+
"""
|
13
|
+
Open a docx file using the default application in a cross-platform manner.
|
14
|
+
|
15
|
+
:param file_path: str, path to the docx file
|
16
|
+
"""
|
17
|
+
file_path = os.path.abspath(file_path)
|
18
|
+
|
19
|
+
if platform.system() == "Darwin": # macOS
|
20
|
+
subprocess.call(("open", file_path))
|
21
|
+
elif platform.system() == "Windows": # Windows
|
22
|
+
os.startfile(file_path)
|
23
|
+
else: # linux variants
|
24
|
+
subprocess.call(("xdg-open", file_path))
|
25
|
+
|
5
26
|
|
6
27
|
class SurveyExportMixin:
|
7
28
|
"""A mixin class for exporting surveys to different formats."""
|
@@ -25,7 +46,12 @@ class SurveyExportMixin:
|
|
25
46
|
)
|
26
47
|
return q.run().select("description").first()
|
27
48
|
|
28
|
-
def docx(
|
49
|
+
def docx(
|
50
|
+
self,
|
51
|
+
return_document_object: bool = False,
|
52
|
+
filename: Optional[str] = None,
|
53
|
+
open_file: bool = False,
|
54
|
+
) -> Union["Document", None]:
|
29
55
|
"""Generate a docx document for the survey."""
|
30
56
|
from docx import Document
|
31
57
|
|
@@ -45,19 +71,55 @@ class SurveyExportMixin:
|
|
45
71
|
if hasattr(question, "question_options"):
|
46
72
|
for option in getattr(question, "question_options", []):
|
47
73
|
doc.add_paragraph(str(option), style="ListBullet")
|
48
|
-
if filename:
|
49
|
-
doc.save(filename)
|
50
|
-
print("The survey has been saved to", filename)
|
51
|
-
return
|
52
|
-
return doc
|
53
74
|
|
54
|
-
|
75
|
+
if return_document_object and filename is None:
|
76
|
+
return doc
|
77
|
+
|
78
|
+
if filename is None:
|
79
|
+
with tempfile.NamedTemporaryFile(
|
80
|
+
"w", delete=False, suffix=".docx", dir=os.getcwd()
|
81
|
+
) as f:
|
82
|
+
filename = f.name
|
83
|
+
|
84
|
+
doc.save(filename)
|
85
|
+
print("The survey has been saved to", filename)
|
86
|
+
if open_file:
|
87
|
+
open_docx(filename)
|
88
|
+
return
|
89
|
+
|
90
|
+
def show(self):
|
91
|
+
self.to_scenario_list(questions_only=False, rename=True).print(format="rich")
|
92
|
+
|
93
|
+
def to_scenario_list(
|
94
|
+
self, questions_only: bool = True, rename=False
|
95
|
+
) -> "ScenarioList":
|
55
96
|
from edsl import ScenarioList, Scenario
|
56
97
|
|
98
|
+
# from edsl.questions import QuestionBase
|
99
|
+
|
100
|
+
if questions_only:
|
101
|
+
to_iterate_over = self._questions
|
102
|
+
else:
|
103
|
+
to_iterate_over = self.recombined_questions_and_instructions()
|
104
|
+
|
105
|
+
if rename:
|
106
|
+
renaming_dict = {
|
107
|
+
"name": "identifier",
|
108
|
+
"question_name": "identifier",
|
109
|
+
"question_text": "text",
|
110
|
+
}
|
111
|
+
else:
|
112
|
+
renaming_dict = {}
|
113
|
+
|
57
114
|
all_keys = set([])
|
58
115
|
scenarios = ScenarioList()
|
59
|
-
for
|
60
|
-
d =
|
116
|
+
for item in to_iterate_over:
|
117
|
+
d = item.to_dict()
|
118
|
+
if item.__class__.__name__ == "Instruction":
|
119
|
+
d["question_type"] = "NA / instruction"
|
120
|
+
for key in renaming_dict:
|
121
|
+
if key in d:
|
122
|
+
d[renaming_dict[key]] = d.pop(key)
|
61
123
|
all_keys.update(d.keys())
|
62
124
|
scenarios.append(Scenario(d))
|
63
125
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
"""A mixin for visualizing the flow of a survey."""
|
2
2
|
|
3
|
+
from typing import Optional
|
3
4
|
from edsl.surveys.base import RulePriority, EndOfSurvey
|
4
5
|
import tempfile
|
5
6
|
|
@@ -7,7 +8,7 @@ import tempfile
|
|
7
8
|
class SurveyFlowVisualizationMixin:
|
8
9
|
"""A mixin for visualizing the flow of a survey."""
|
9
10
|
|
10
|
-
def show_flow(self, filename: str = None):
|
11
|
+
def show_flow(self, filename: Optional[str] = None):
|
11
12
|
"""Create an image showing the flow of users through the survey."""
|
12
13
|
# Create a graph object
|
13
14
|
import pydot
|
@@ -0,0 +1,284 @@
|
|
1
|
+
import json
|
2
|
+
import html
|
3
|
+
import re
|
4
|
+
|
5
|
+
from edsl import Question
|
6
|
+
from edsl import Survey
|
7
|
+
|
8
|
+
qualtrics_codes = {
|
9
|
+
"TE": "free_text",
|
10
|
+
"MC": "multiple_choice",
|
11
|
+
"Matrix": "matrix",
|
12
|
+
"DB": "instruction", # not quite right, but for now
|
13
|
+
"Timing": "free_text", # not quite right, but for now
|
14
|
+
}
|
15
|
+
# TE (Text Entry): Allows respondents to input a text response.
|
16
|
+
# MC (Multiple Choice): Provides respondents with a list of options to choose from.
|
17
|
+
# DB (Descriptive Text or Information): Displays text or information without requiring a response.
|
18
|
+
# Matrix: A grid-style question where respondents can evaluate multiple items using the same set of response options.
|
19
|
+
|
20
|
+
|
21
|
+
def clean_html(raw_html):
|
22
|
+
# Unescape HTML entities
|
23
|
+
clean_text = html.unescape(raw_html)
|
24
|
+
# Remove HTML tags
|
25
|
+
clean_text = re.sub(r"<.*?>", "", clean_text)
|
26
|
+
# Replace non-breaking spaces with regular spaces
|
27
|
+
clean_text = clean_text.replace("\xa0", " ")
|
28
|
+
# Optionally, strip leading/trailing spaces
|
29
|
+
clean_text = clean_text.strip()
|
30
|
+
return clean_text
|
31
|
+
|
32
|
+
|
33
|
+
class QualtricsQuestion:
|
34
|
+
def __init__(self, question_json, debug=False):
|
35
|
+
self.debug = debug
|
36
|
+
self.question_json = question_json
|
37
|
+
if self.element != "SQ":
|
38
|
+
raise ValueError("Invalid question element type")
|
39
|
+
|
40
|
+
@property
|
41
|
+
def element(self):
|
42
|
+
return self.question_json["Element"]
|
43
|
+
|
44
|
+
@property
|
45
|
+
def selector(self):
|
46
|
+
return self.question_json.get("Selector", None)
|
47
|
+
|
48
|
+
@property
|
49
|
+
def question_name(self):
|
50
|
+
return self.question_json["PrimaryAttribute"]
|
51
|
+
|
52
|
+
@property
|
53
|
+
def question_text(self):
|
54
|
+
return clean_html(self.question_json["Payload"]["QuestionText"])
|
55
|
+
|
56
|
+
@property
|
57
|
+
def raw_question_type(self):
|
58
|
+
return self.question_json["Payload"]["QuestionType"]
|
59
|
+
|
60
|
+
@property
|
61
|
+
def question_type(self):
|
62
|
+
q_type = qualtrics_codes.get(self.raw_question_type, None)
|
63
|
+
if q_type is None:
|
64
|
+
print(f"Unknown question type: {self.raw_question_type}")
|
65
|
+
return None
|
66
|
+
return q_type
|
67
|
+
|
68
|
+
@property
|
69
|
+
def choices(self):
|
70
|
+
if "Choices" in self.question_json["Payload"]:
|
71
|
+
return [
|
72
|
+
choice["Display"]
|
73
|
+
for choice in self.question_json["Payload"]["Choices"].values()
|
74
|
+
]
|
75
|
+
return None
|
76
|
+
|
77
|
+
@property
|
78
|
+
def answers(self):
|
79
|
+
if "Answers" in self.question_json["Payload"]:
|
80
|
+
return [
|
81
|
+
choice["Display"]
|
82
|
+
for choice in self.question_json["Payload"]["Choices"].values()
|
83
|
+
]
|
84
|
+
return None
|
85
|
+
|
86
|
+
def to_edsl(self):
|
87
|
+
if self.question_type == "instruction":
|
88
|
+
from edsl import Instruction
|
89
|
+
|
90
|
+
return [Instruction(text=self.question_text, name=self.question_name)]
|
91
|
+
|
92
|
+
if self.question_type == "free_text":
|
93
|
+
try:
|
94
|
+
q = Question(
|
95
|
+
**{
|
96
|
+
"question_type": self.question_type,
|
97
|
+
"question_text": self.question_text,
|
98
|
+
"question_name": self.question_name,
|
99
|
+
}
|
100
|
+
)
|
101
|
+
return [q]
|
102
|
+
except Exception as e:
|
103
|
+
return []
|
104
|
+
|
105
|
+
if self.question_type == "multiple_choice":
|
106
|
+
# Let's figure of it it's actually a checkbox question
|
107
|
+
if self.selector == "MAVR" or self.selector == "MULTIPLE":
|
108
|
+
try:
|
109
|
+
q = Question(
|
110
|
+
**{
|
111
|
+
"question_type": "checkbox",
|
112
|
+
"question_text": self.question_text,
|
113
|
+
"question_name": self.question_name,
|
114
|
+
"question_options": self.choices,
|
115
|
+
}
|
116
|
+
)
|
117
|
+
return [q]
|
118
|
+
except Exception as e:
|
119
|
+
return []
|
120
|
+
|
121
|
+
# maybe it's a linear scale!
|
122
|
+
if "<br>" in self.choices[0]:
|
123
|
+
option_labels = {}
|
124
|
+
question_options = []
|
125
|
+
for choice in self.choices:
|
126
|
+
if "<br>" in choice:
|
127
|
+
option_label, question_option = choice.split("<br>")
|
128
|
+
option_labels[int(question_option)] = option_label
|
129
|
+
question_options.append(int(question_option))
|
130
|
+
else:
|
131
|
+
question_options.append(int(choice))
|
132
|
+
try:
|
133
|
+
q = Question(
|
134
|
+
**{
|
135
|
+
"question_type": "linear_scale",
|
136
|
+
"question_text": self.question_text,
|
137
|
+
"question_name": self.question_name,
|
138
|
+
"question_options": question_options,
|
139
|
+
"option_labels": option_labels,
|
140
|
+
}
|
141
|
+
)
|
142
|
+
return [q]
|
143
|
+
except Exception as e:
|
144
|
+
if self.debug:
|
145
|
+
raise e
|
146
|
+
else:
|
147
|
+
print(e)
|
148
|
+
return []
|
149
|
+
|
150
|
+
try:
|
151
|
+
q = Question(
|
152
|
+
**{
|
153
|
+
"question_type": self.question_type,
|
154
|
+
"question_text": self.question_text,
|
155
|
+
"question_name": self.question_name,
|
156
|
+
"question_options": self.choices,
|
157
|
+
}
|
158
|
+
)
|
159
|
+
return [q]
|
160
|
+
except Exception as e:
|
161
|
+
return []
|
162
|
+
|
163
|
+
if self.question_type == "matrix":
|
164
|
+
questions = []
|
165
|
+
for index, choice in enumerate(self.choices):
|
166
|
+
try:
|
167
|
+
q = Question(
|
168
|
+
**{
|
169
|
+
"question_type": "multiple_choice",
|
170
|
+
"question_text": self.question_text + f" ({choice})",
|
171
|
+
"question_name": self.question_name + f"_{index}",
|
172
|
+
"question_options": self.answers,
|
173
|
+
}
|
174
|
+
)
|
175
|
+
questions.append(q)
|
176
|
+
except Exception as e:
|
177
|
+
continue
|
178
|
+
|
179
|
+
return questions
|
180
|
+
|
181
|
+
raise ValueError(f"Invalid question type: {self.question_type}")
|
182
|
+
|
183
|
+
|
184
|
+
class SurveyQualtricsImport:
|
185
|
+
def __init__(self, qsf_file_name: str):
|
186
|
+
self.qsf_file_name = qsf_file_name
|
187
|
+
self.question_data = self.extract_questions_from_json()
|
188
|
+
|
189
|
+
def create_survey(self):
|
190
|
+
questions = []
|
191
|
+
for qualtrics_questions in self.question_data:
|
192
|
+
questions.extend(qualtrics_questions.to_edsl())
|
193
|
+
return Survey(questions)
|
194
|
+
|
195
|
+
@property
|
196
|
+
def survey_data(self):
|
197
|
+
with open(self.qsf_file_name, "r") as f:
|
198
|
+
survey_data = json.load(f)
|
199
|
+
return survey_data
|
200
|
+
|
201
|
+
def extract_questions_from_json(self):
|
202
|
+
questions = self.survey_data["SurveyElements"]
|
203
|
+
|
204
|
+
extracted_questions = []
|
205
|
+
|
206
|
+
for question in questions:
|
207
|
+
if question["Element"] == "SQ":
|
208
|
+
extracted_questions.append(QualtricsQuestion(question))
|
209
|
+
|
210
|
+
return extracted_questions
|
211
|
+
|
212
|
+
def extract_blocks_from_json(self):
|
213
|
+
blocks = []
|
214
|
+
|
215
|
+
for element in self.survey_data["SurveyElements"]:
|
216
|
+
if element["Element"] == "BL":
|
217
|
+
for block_payload in element["Payload"]:
|
218
|
+
block_elements = [
|
219
|
+
BlockElement(be["Type"], be["QuestionID"])
|
220
|
+
for be in block_payload["BlockElements"]
|
221
|
+
]
|
222
|
+
options_data = block_payload.get("Options", {})
|
223
|
+
options = BlockOptions(
|
224
|
+
options_data.get("BlockLocking", "false"),
|
225
|
+
options_data.get("RandomizeQuestions", "false"),
|
226
|
+
options_data.get("BlockVisibility", "Collapsed"),
|
227
|
+
)
|
228
|
+
|
229
|
+
block = SurveyBlock(
|
230
|
+
block_payload["Type"],
|
231
|
+
block_payload["Description"],
|
232
|
+
block_payload["ID"],
|
233
|
+
block_elements,
|
234
|
+
options,
|
235
|
+
)
|
236
|
+
blocks.append(block)
|
237
|
+
|
238
|
+
return blocks
|
239
|
+
|
240
|
+
|
241
|
+
class SurveyBlock:
|
242
|
+
def __init__(self, block_type, description, block_id, block_elements, options):
|
243
|
+
self.block_type = block_type
|
244
|
+
self.description = description
|
245
|
+
self.block_id = block_id
|
246
|
+
self.block_elements = block_elements
|
247
|
+
self.options = options
|
248
|
+
|
249
|
+
def __repr__(self):
|
250
|
+
return f"SurveyBlock(type={self.block_type}, description={self.description}, id={self.block_id})"
|
251
|
+
|
252
|
+
|
253
|
+
class BlockElement:
|
254
|
+
def __init__(self, element_type, question_id):
|
255
|
+
self.element_type = element_type
|
256
|
+
self.question_id = question_id
|
257
|
+
|
258
|
+
def __repr__(self):
|
259
|
+
return f"BlockElement(type={self.element_type}, question_id={self.question_id})"
|
260
|
+
|
261
|
+
|
262
|
+
class BlockOptions:
|
263
|
+
def __init__(self, block_locking, randomize_questions, block_visibility):
|
264
|
+
self.block_locking = block_locking
|
265
|
+
self.randomize_questions = randomize_questions
|
266
|
+
self.block_visibility = block_visibility
|
267
|
+
|
268
|
+
def __repr__(self):
|
269
|
+
return (
|
270
|
+
f"BlockOptions(block_locking={self.block_locking}, "
|
271
|
+
f"randomize_questions={self.randomize_questions}, "
|
272
|
+
f"block_visibility={self.block_visibility})"
|
273
|
+
)
|
274
|
+
|
275
|
+
|
276
|
+
if __name__ == "__main__":
|
277
|
+
survey_creator = SurveyQualtricsImport("example.qsf")
|
278
|
+
# print(survey_creator.question_data)
|
279
|
+
# survey = survey_creator.create_survey()
|
280
|
+
# info = survey.push()
|
281
|
+
# print(info)
|
282
|
+
# questions = survey.extract_questions_from_json()
|
283
|
+
# for question in questions:
|
284
|
+
# print(question)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from typing import List, Optional
|
2
|
+
|
3
|
+
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
4
|
+
|
5
|
+
|
6
|
+
class ChangeInstruction:
|
7
|
+
def __init__(
|
8
|
+
self,
|
9
|
+
keep: Optional[List[str]] = None,
|
10
|
+
drop: Optional[List[str]] = None,
|
11
|
+
):
|
12
|
+
if keep is None and drop is None:
|
13
|
+
raise ValueError("Keep and drop cannot both be None")
|
14
|
+
|
15
|
+
self.keep = keep or []
|
16
|
+
self.drop = drop or []
|
17
|
+
|
18
|
+
def include_instruction(self, instruction_name) -> bool:
|
19
|
+
return (instruction_name in self.keep) or (not instruction_name in self.drop)
|
20
|
+
|
21
|
+
def add_name(self, index) -> None:
|
22
|
+
self.name = "change_instruction_{}".format(index)
|
23
|
+
|
24
|
+
def __str__(self):
|
25
|
+
return self.text
|
26
|
+
|
27
|
+
def _to_dict(self):
|
28
|
+
return {
|
29
|
+
"keep": self.keep,
|
30
|
+
"drop": self.drop,
|
31
|
+
"edsl_class_name": "ChangeInstruction",
|
32
|
+
}
|
33
|
+
|
34
|
+
@add_edsl_version
|
35
|
+
def to_dict(self):
|
36
|
+
return self._to_dict()
|
37
|
+
|
38
|
+
def __hash__(self) -> int:
|
39
|
+
"""Return a hash of the question."""
|
40
|
+
from edsl.utilities.utilities import dict_hash
|
41
|
+
|
42
|
+
return dict_hash(self._to_dict())
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
@remove_edsl_version
|
46
|
+
def from_dict(cls, data):
|
47
|
+
return cls(data["keep"], data["drop"])
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from typing import Union, Optional, List, Generator, Dict
|
2
|
+
from edsl.questions import QuestionBase
|
3
|
+
|
4
|
+
from edsl.utilities.decorators import add_edsl_version, remove_edsl_version
|
5
|
+
|
6
|
+
|
7
|
+
class Instruction:
|
8
|
+
def __init__(self, name, text):
|
9
|
+
self.name = name
|
10
|
+
self.text = text
|
11
|
+
|
12
|
+
def __str__(self):
|
13
|
+
return self.text
|
14
|
+
|
15
|
+
def __repr__(self):
|
16
|
+
return """Instruction(name="{}", text="{}")""".format(self.name, self.text)
|
17
|
+
|
18
|
+
def _to_dict(self):
|
19
|
+
return {"name": self.name, "text": self.text, "edsl_class_name": "Instruction"}
|
20
|
+
|
21
|
+
@add_edsl_version
|
22
|
+
def to_dict(self):
|
23
|
+
return self._to_dict()
|
24
|
+
|
25
|
+
def __hash__(self) -> int:
|
26
|
+
"""Return a hash of the question."""
|
27
|
+
from edsl.utilities.utilities import dict_hash
|
28
|
+
|
29
|
+
return dict_hash(self._to_dict())
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
@remove_edsl_version
|
33
|
+
def from_dict(cls, data):
|
34
|
+
return cls(data["name"], data["text"])
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from edsl.surveys.instructions.Instruction import Instruction
|
2
|
+
from edsl.surveys.instructions.ChangeInstruction import ChangeInstruction
|
3
|
+
from edsl.questions import QuestionBase
|
4
|
+
from typing import Union, Optional, List, Generator, Dict
|
5
|
+
|
6
|
+
|
7
|
+
from collections import UserDict
|
8
|
+
|
9
|
+
|
10
|
+
class InstructionCollection(UserDict):
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
instruction_names_to_instruction: Dict[str, Instruction],
|
14
|
+
questions: List[QuestionBase],
|
15
|
+
):
|
16
|
+
self.instruction_names_to_instruction = instruction_names_to_instruction
|
17
|
+
self.questions = questions
|
18
|
+
data = {}
|
19
|
+
for question in self.questions:
|
20
|
+
# just add both the question and the question name
|
21
|
+
data[question.name] = list(self.relevant_instructions(question))
|
22
|
+
# data[question] = list(self.relevant_instructions(question))
|
23
|
+
super().__init__(data)
|
24
|
+
|
25
|
+
def __getitem__(self, key):
|
26
|
+
# in case the person uses question instead of the name
|
27
|
+
if isinstance(key, QuestionBase):
|
28
|
+
key = key.name
|
29
|
+
return self.data[key]
|
30
|
+
|
31
|
+
@property
|
32
|
+
def question_names(self):
|
33
|
+
return [q.name for q in self.questions]
|
34
|
+
|
35
|
+
def question_index(self, question_name):
|
36
|
+
return self.question_names.index(question_name)
|
37
|
+
|
38
|
+
def _entries_before(
|
39
|
+
self, question_name
|
40
|
+
) -> tuple[List[Instruction], List[ChangeInstruction]]:
|
41
|
+
if question_name not in self.question_names:
|
42
|
+
raise ValueError(
|
43
|
+
f"Question name not found in the list of questions: got '{question_name}'; list is {self.question_names}"
|
44
|
+
)
|
45
|
+
instructions, changes = [], []
|
46
|
+
|
47
|
+
index = self.question_index(question_name)
|
48
|
+
for instruction in self.instruction_names_to_instruction.values():
|
49
|
+
if instruction.pseudo_index < index:
|
50
|
+
if isinstance(instruction, Instruction):
|
51
|
+
instructions.append(instruction)
|
52
|
+
elif isinstance(instruction, ChangeInstruction):
|
53
|
+
changes.append(instruction)
|
54
|
+
return instructions, changes
|
55
|
+
|
56
|
+
def relevant_instructions(
|
57
|
+
self, question: Union[str, QuestionBase]
|
58
|
+
) -> Generator[Instruction, None, None]:
|
59
|
+
## Find all the questions that are after a given instruction
|
60
|
+
if isinstance(question, str):
|
61
|
+
question_name = question
|
62
|
+
elif isinstance(question, QuestionBase):
|
63
|
+
question_name = question.name
|
64
|
+
|
65
|
+
instructions_before, changes_before = self._entries_before(question_name)
|
66
|
+
keep_list = []
|
67
|
+
drop_list = []
|
68
|
+
for change in changes_before:
|
69
|
+
keep_list.extend(change.keep)
|
70
|
+
drop_list.extend(change.drop)
|
71
|
+
|
72
|
+
for instruction in instructions_before:
|
73
|
+
if instruction.name in keep_list or instruction.name not in drop_list:
|
74
|
+
yield instruction
|
75
|
+
|
76
|
+
def __len__(self):
|
77
|
+
return len(self.instruction_names_to_instruction)
|
File without changes
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Exception Details</title>
|
7
|
+
<style>
|
8
|
+
{{ css }}
|
9
|
+
</style>
|
10
|
+
|
11
|
+
<script>
|
12
|
+
{{ javascript }}
|
13
|
+
</script>
|
14
|
+
|
15
|
+
</head>
|
16
|
+
<body>
|
17
|
+
{% include 'overview.html' %}
|
18
|
+
{% include 'exceptions_by_type.html' %}
|
19
|
+
{% include 'exceptions_by_model.html' %}
|
20
|
+
{% include 'exceptions_by_question_name.html' %}
|
21
|
+
{% include 'interviews.html' %}
|
22
|
+
{% include 'performance_plot.html' %}
|
23
|
+
</body>
|
24
|
+
</html>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<h2>Exceptions by Model</h2>
|
2
|
+
<table border="1">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th>Model</th>
|
6
|
+
<th>Number</th>
|
7
|
+
</tr>
|
8
|
+
</thead>
|
9
|
+
<tbody>
|
10
|
+
{% for model, exceptions in exceptions_by_model.items() %}
|
11
|
+
<tr>
|
12
|
+
<td>{{ model }}</td>
|
13
|
+
<td>{{ exceptions }}</td>
|
14
|
+
</tr>
|
15
|
+
{% endfor %}
|
16
|
+
</tbody>
|
17
|
+
</table>
|
18
|
+
|
19
|
+
<h2>Exceptions by Service</h2>
|
20
|
+
<table border="1">
|
21
|
+
<thead>
|
22
|
+
<tr>
|
23
|
+
<th>Service</th>
|
24
|
+
<th>Number</th>
|
25
|
+
</tr>
|
26
|
+
</thead>
|
27
|
+
<tbody>
|
28
|
+
{% for service, exceptions in exceptions_by_service.items() %}
|
29
|
+
<tr>
|
30
|
+
<td>{{ service }}</td>
|
31
|
+
<td>{{ exceptions }}</td>
|
32
|
+
</tr>
|
33
|
+
{% endfor %}
|
34
|
+
</tbody>
|
35
|
+
</table>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<h2>Exceptions by Question Name</h2>
|
2
|
+
<table border="1">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th>Question Name</th>
|
6
|
+
<th>Number of Exceptions</th>
|
7
|
+
</tr>
|
8
|
+
</thead>
|
9
|
+
<tbody>
|
10
|
+
{% for question_name, exception_count in exceptions_by_question_name.items() %}
|
11
|
+
<tr>
|
12
|
+
<td>{{ question_name }}</td>
|
13
|
+
<td>{{ exception_count }}</td>
|
14
|
+
</tr>
|
15
|
+
{% endfor %}
|
16
|
+
</tbody>
|
17
|
+
</table>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<h2>Exceptions by Type</h2>
|
2
|
+
<table border="1">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th>Exception Type</th>
|
6
|
+
<th>Number</th>
|
7
|
+
</tr>
|
8
|
+
</thead>
|
9
|
+
<tbody>
|
10
|
+
{% for exception_type, exceptions in exceptions_by_type.items() %}
|
11
|
+
<tr>
|
12
|
+
<td>{{ exception_type }}</td>
|
13
|
+
<td>{{ exceptions }}</td>
|
14
|
+
</tr>
|
15
|
+
{% endfor %}
|
16
|
+
</tbody>
|
17
|
+
</table>
|