edsl 0.1.49__py3-none-any.whl → 0.1.51__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.
Files changed (257) hide show
  1. edsl/__init__.py +124 -53
  2. edsl/__version__.py +1 -1
  3. edsl/agents/agent.py +21 -21
  4. edsl/agents/agent_list.py +2 -5
  5. edsl/agents/exceptions.py +119 -5
  6. edsl/base/__init__.py +10 -35
  7. edsl/base/base_class.py +71 -36
  8. edsl/base/base_exception.py +204 -0
  9. edsl/base/data_transfer_models.py +1 -1
  10. edsl/base/exceptions.py +94 -0
  11. edsl/buckets/__init__.py +15 -1
  12. edsl/buckets/bucket_collection.py +3 -4
  13. edsl/buckets/exceptions.py +107 -0
  14. edsl/buckets/model_buckets.py +1 -2
  15. edsl/buckets/token_bucket.py +11 -6
  16. edsl/buckets/token_bucket_api.py +27 -12
  17. edsl/buckets/token_bucket_client.py +9 -7
  18. edsl/caching/cache.py +12 -4
  19. edsl/caching/cache_entry.py +10 -9
  20. edsl/caching/exceptions.py +113 -7
  21. edsl/caching/remote_cache_sync.py +6 -7
  22. edsl/caching/sql_dict.py +20 -14
  23. edsl/cli.py +43 -0
  24. edsl/config/__init__.py +1 -1
  25. edsl/config/config_class.py +32 -6
  26. edsl/conversation/Conversation.py +8 -4
  27. edsl/conversation/car_buying.py +1 -3
  28. edsl/conversation/exceptions.py +58 -0
  29. edsl/conversation/mug_negotiation.py +2 -8
  30. edsl/coop/__init__.py +28 -6
  31. edsl/coop/coop.py +120 -29
  32. edsl/coop/coop_functions.py +1 -1
  33. edsl/coop/ep_key_handling.py +1 -1
  34. edsl/coop/exceptions.py +188 -9
  35. edsl/coop/price_fetcher.py +5 -8
  36. edsl/coop/utils.py +4 -6
  37. edsl/dataset/__init__.py +5 -4
  38. edsl/dataset/dataset.py +177 -86
  39. edsl/dataset/dataset_operations_mixin.py +98 -76
  40. edsl/dataset/dataset_tree.py +11 -7
  41. edsl/dataset/display/table_display.py +0 -2
  42. edsl/dataset/display/table_renderers.py +6 -4
  43. edsl/dataset/exceptions.py +125 -0
  44. edsl/dataset/file_exports.py +18 -11
  45. edsl/dataset/r/ggplot.py +13 -6
  46. edsl/display/__init__.py +27 -0
  47. edsl/display/core.py +147 -0
  48. edsl/display/plugin.py +189 -0
  49. edsl/display/utils.py +52 -0
  50. edsl/inference_services/__init__.py +9 -1
  51. edsl/inference_services/available_model_cache_handler.py +1 -1
  52. edsl/inference_services/available_model_fetcher.py +5 -6
  53. edsl/inference_services/data_structures.py +10 -7
  54. edsl/inference_services/exceptions.py +132 -1
  55. edsl/inference_services/inference_service_abc.py +2 -2
  56. edsl/inference_services/inference_services_collection.py +2 -6
  57. edsl/inference_services/registry.py +4 -3
  58. edsl/inference_services/service_availability.py +4 -3
  59. edsl/inference_services/services/anthropic_service.py +4 -1
  60. edsl/inference_services/services/aws_bedrock.py +13 -12
  61. edsl/inference_services/services/azure_ai.py +12 -10
  62. edsl/inference_services/services/deep_infra_service.py +1 -4
  63. edsl/inference_services/services/deep_seek_service.py +1 -5
  64. edsl/inference_services/services/google_service.py +7 -3
  65. edsl/inference_services/services/groq_service.py +1 -1
  66. edsl/inference_services/services/mistral_ai_service.py +4 -2
  67. edsl/inference_services/services/ollama_service.py +1 -1
  68. edsl/inference_services/services/open_ai_service.py +7 -5
  69. edsl/inference_services/services/perplexity_service.py +6 -2
  70. edsl/inference_services/services/test_service.py +8 -7
  71. edsl/inference_services/services/together_ai_service.py +2 -3
  72. edsl/inference_services/services/xai_service.py +1 -1
  73. edsl/instructions/__init__.py +1 -1
  74. edsl/instructions/change_instruction.py +7 -5
  75. edsl/instructions/exceptions.py +61 -0
  76. edsl/instructions/instruction.py +6 -2
  77. edsl/instructions/instruction_collection.py +6 -4
  78. edsl/instructions/instruction_handler.py +12 -15
  79. edsl/interviews/ReportErrors.py +0 -3
  80. edsl/interviews/__init__.py +9 -2
  81. edsl/interviews/answering_function.py +11 -13
  82. edsl/interviews/exception_tracking.py +15 -8
  83. edsl/interviews/exceptions.py +79 -0
  84. edsl/interviews/interview.py +33 -30
  85. edsl/interviews/interview_status_dictionary.py +4 -2
  86. edsl/interviews/interview_status_log.py +2 -1
  87. edsl/interviews/interview_task_manager.py +5 -5
  88. edsl/interviews/request_token_estimator.py +5 -2
  89. edsl/interviews/statistics.py +3 -4
  90. edsl/invigilators/__init__.py +7 -1
  91. edsl/invigilators/exceptions.py +79 -0
  92. edsl/invigilators/invigilator_base.py +0 -1
  93. edsl/invigilators/invigilators.py +9 -13
  94. edsl/invigilators/prompt_constructor.py +1 -5
  95. edsl/invigilators/prompt_helpers.py +8 -4
  96. edsl/invigilators/question_instructions_prompt_builder.py +1 -1
  97. edsl/invigilators/question_option_processor.py +9 -5
  98. edsl/invigilators/question_template_replacements_builder.py +3 -2
  99. edsl/jobs/__init__.py +42 -5
  100. edsl/jobs/async_interview_runner.py +25 -23
  101. edsl/jobs/check_survey_scenario_compatibility.py +11 -10
  102. edsl/jobs/data_structures.py +8 -5
  103. edsl/jobs/exceptions.py +177 -8
  104. edsl/jobs/fetch_invigilator.py +1 -1
  105. edsl/jobs/jobs.py +74 -69
  106. edsl/jobs/jobs_checks.py +6 -7
  107. edsl/jobs/jobs_component_constructor.py +4 -4
  108. edsl/jobs/jobs_pricing_estimation.py +4 -3
  109. edsl/jobs/jobs_remote_inference_logger.py +5 -4
  110. edsl/jobs/jobs_runner_asyncio.py +3 -4
  111. edsl/jobs/jobs_runner_status.py +8 -9
  112. edsl/jobs/remote_inference.py +27 -24
  113. edsl/jobs/results_exceptions_handler.py +10 -7
  114. edsl/key_management/__init__.py +3 -1
  115. edsl/key_management/exceptions.py +62 -0
  116. edsl/key_management/key_lookup.py +1 -1
  117. edsl/key_management/key_lookup_builder.py +37 -14
  118. edsl/key_management/key_lookup_collection.py +2 -0
  119. edsl/language_models/__init__.py +1 -1
  120. edsl/language_models/exceptions.py +302 -14
  121. edsl/language_models/language_model.py +9 -8
  122. edsl/language_models/model.py +4 -4
  123. edsl/language_models/model_list.py +1 -1
  124. edsl/language_models/price_manager.py +1 -1
  125. edsl/language_models/raw_response_handler.py +14 -9
  126. edsl/language_models/registry.py +17 -21
  127. edsl/language_models/repair.py +0 -6
  128. edsl/language_models/unused/fake_openai_service.py +0 -1
  129. edsl/load_plugins.py +69 -0
  130. edsl/logger.py +146 -0
  131. edsl/notebooks/__init__.py +24 -1
  132. edsl/notebooks/exceptions.py +82 -0
  133. edsl/notebooks/notebook.py +7 -3
  134. edsl/notebooks/notebook_to_latex.py +1 -2
  135. edsl/plugins/__init__.py +63 -0
  136. edsl/plugins/built_in/export_example.py +50 -0
  137. edsl/plugins/built_in/pig_latin.py +67 -0
  138. edsl/plugins/cli.py +372 -0
  139. edsl/plugins/cli_typer.py +283 -0
  140. edsl/plugins/exceptions.py +31 -0
  141. edsl/plugins/hookspec.py +51 -0
  142. edsl/plugins/plugin_host.py +128 -0
  143. edsl/plugins/plugin_manager.py +633 -0
  144. edsl/plugins/plugins_registry.py +168 -0
  145. edsl/prompts/__init__.py +24 -1
  146. edsl/prompts/exceptions.py +107 -5
  147. edsl/prompts/prompt.py +15 -7
  148. edsl/questions/HTMLQuestion.py +5 -11
  149. edsl/questions/Quick.py +0 -1
  150. edsl/questions/__init__.py +6 -4
  151. edsl/questions/answer_validator_mixin.py +318 -323
  152. edsl/questions/compose_questions.py +3 -3
  153. edsl/questions/descriptors.py +11 -50
  154. edsl/questions/exceptions.py +278 -22
  155. edsl/questions/loop_processor.py +7 -5
  156. edsl/questions/prompt_templates/question_list.jinja +3 -0
  157. edsl/questions/question_base.py +46 -19
  158. edsl/questions/question_base_gen_mixin.py +2 -2
  159. edsl/questions/question_base_prompts_mixin.py +13 -7
  160. edsl/questions/question_budget.py +503 -98
  161. edsl/questions/question_check_box.py +660 -160
  162. edsl/questions/question_dict.py +345 -194
  163. edsl/questions/question_extract.py +401 -61
  164. edsl/questions/question_free_text.py +80 -14
  165. edsl/questions/question_functional.py +119 -9
  166. edsl/questions/{derived/question_likert_five.py → question_likert_five.py} +2 -2
  167. edsl/questions/{derived/question_linear_scale.py → question_linear_scale.py} +3 -4
  168. edsl/questions/question_list.py +275 -28
  169. edsl/questions/question_matrix.py +643 -96
  170. edsl/questions/question_multiple_choice.py +219 -51
  171. edsl/questions/question_numerical.py +361 -32
  172. edsl/questions/question_rank.py +401 -124
  173. edsl/questions/question_registry.py +7 -5
  174. edsl/questions/{derived/question_top_k.py → question_top_k.py} +3 -3
  175. edsl/questions/{derived/question_yes_no.py → question_yes_no.py} +3 -4
  176. edsl/questions/register_questions_meta.py +2 -2
  177. edsl/questions/response_validator_abc.py +13 -15
  178. edsl/questions/response_validator_factory.py +10 -12
  179. edsl/questions/templates/dict/answering_instructions.jinja +1 -0
  180. edsl/questions/templates/rank/question_presentation.jinja +1 -1
  181. edsl/results/__init__.py +1 -1
  182. edsl/results/exceptions.py +141 -7
  183. edsl/results/report.py +1 -2
  184. edsl/results/result.py +11 -9
  185. edsl/results/results.py +480 -321
  186. edsl/results/results_selector.py +8 -4
  187. edsl/scenarios/PdfExtractor.py +2 -2
  188. edsl/scenarios/construct_download_link.py +69 -35
  189. edsl/scenarios/directory_scanner.py +33 -14
  190. edsl/scenarios/document_chunker.py +1 -1
  191. edsl/scenarios/exceptions.py +238 -14
  192. edsl/scenarios/file_methods.py +1 -1
  193. edsl/scenarios/file_store.py +7 -3
  194. edsl/scenarios/handlers/__init__.py +17 -0
  195. edsl/scenarios/handlers/docx_file_store.py +0 -5
  196. edsl/scenarios/handlers/pdf_file_store.py +0 -1
  197. edsl/scenarios/handlers/pptx_file_store.py +0 -5
  198. edsl/scenarios/handlers/py_file_store.py +0 -1
  199. edsl/scenarios/handlers/sql_file_store.py +1 -4
  200. edsl/scenarios/handlers/sqlite_file_store.py +0 -1
  201. edsl/scenarios/handlers/txt_file_store.py +1 -1
  202. edsl/scenarios/scenario.py +1 -3
  203. edsl/scenarios/scenario_list.py +179 -27
  204. edsl/scenarios/scenario_list_pdf_tools.py +1 -0
  205. edsl/scenarios/scenario_selector.py +0 -1
  206. edsl/surveys/__init__.py +3 -4
  207. edsl/surveys/dag/__init__.py +4 -2
  208. edsl/surveys/descriptors.py +1 -1
  209. edsl/surveys/edit_survey.py +1 -0
  210. edsl/surveys/exceptions.py +165 -9
  211. edsl/surveys/memory/__init__.py +5 -3
  212. edsl/surveys/memory/memory_management.py +1 -0
  213. edsl/surveys/memory/memory_plan.py +6 -15
  214. edsl/surveys/rules/__init__.py +5 -3
  215. edsl/surveys/rules/rule.py +1 -2
  216. edsl/surveys/rules/rule_collection.py +1 -1
  217. edsl/surveys/survey.py +12 -24
  218. edsl/surveys/survey_css.py +3 -3
  219. edsl/surveys/survey_export.py +6 -3
  220. edsl/surveys/survey_flow_visualization.py +10 -1
  221. edsl/surveys/survey_simulator.py +2 -1
  222. edsl/tasks/__init__.py +23 -1
  223. edsl/tasks/exceptions.py +72 -0
  224. edsl/tasks/question_task_creator.py +3 -3
  225. edsl/tasks/task_creators.py +1 -3
  226. edsl/tasks/task_history.py +8 -10
  227. edsl/tasks/task_status_log.py +1 -2
  228. edsl/tokens/__init__.py +29 -1
  229. edsl/tokens/exceptions.py +37 -0
  230. edsl/tokens/interview_token_usage.py +3 -2
  231. edsl/tokens/token_usage.py +4 -3
  232. edsl/utilities/__init__.py +21 -1
  233. edsl/utilities/decorators.py +1 -2
  234. edsl/utilities/markdown_to_docx.py +2 -2
  235. edsl/utilities/markdown_to_pdf.py +1 -1
  236. edsl/utilities/repair_functions.py +0 -1
  237. edsl/utilities/restricted_python.py +0 -1
  238. edsl/utilities/template_loader.py +2 -3
  239. edsl/utilities/utilities.py +8 -29
  240. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/METADATA +32 -2
  241. edsl-0.1.51.dist-info/RECORD +365 -0
  242. edsl-0.1.51.dist-info/entry_points.txt +3 -0
  243. edsl/dataset/smart_objects.py +0 -96
  244. edsl/exceptions/BaseException.py +0 -21
  245. edsl/exceptions/__init__.py +0 -54
  246. edsl/exceptions/configuration.py +0 -16
  247. edsl/exceptions/general.py +0 -34
  248. edsl/questions/derived/__init__.py +0 -0
  249. edsl/study/ObjectEntry.py +0 -173
  250. edsl/study/ProofOfWork.py +0 -113
  251. edsl/study/SnapShot.py +0 -80
  252. edsl/study/Study.py +0 -520
  253. edsl/study/__init__.py +0 -6
  254. edsl/utilities/interface.py +0 -135
  255. edsl-0.1.49.dist-info/RECORD +0 -347
  256. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/LICENSE +0 -0
  257. {edsl-0.1.49.dist-info → edsl-0.1.51.dist-info}/WHEEL +0 -0
@@ -35,7 +35,6 @@ class PyMethods(FileMethods):
35
35
  import pygments
36
36
  from pygments.lexers import PythonLexer
37
37
  from pygments.formatters import HtmlFormatter
38
- from pygments.styles import get_style_by_name
39
38
 
40
39
  try:
41
40
  with open(self.path, "r", encoding="utf-8") as f:
@@ -1,6 +1,6 @@
1
1
  import tempfile
2
2
  import re
3
- from typing import List, Optional
3
+ from typing import List
4
4
  import textwrap
5
5
 
6
6
 
@@ -217,9 +217,6 @@ class SqlMethods(FileMethods):
217
217
  This is a simple check and doesn't replace proper SQL parsing.
218
218
  """
219
219
  try:
220
- with open(self.path, "r", encoding="utf-8") as f:
221
- content = f.read()
222
-
223
220
  statements = self.split_statements()
224
221
  for stmt in statements:
225
222
  # Check for basic SQL keywords
@@ -50,7 +50,6 @@ class SQLiteMethods(FileMethods):
50
50
  """
51
51
  Opens the database with the system's default SQLite viewer if available.
52
52
  """
53
- import os
54
53
  import subprocess
55
54
 
56
55
  if os.path.exists(self.path):
@@ -23,7 +23,7 @@ class TxtMethods(FileMethods):
23
23
  print("TXT file was not found.")
24
24
 
25
25
  def view_notebook(self):
26
- from IPython.display import FileLink, display
26
+ from ...display import FileLink, display
27
27
 
28
28
  display(FileLink(self.path))
29
29
 
@@ -20,7 +20,6 @@ information to questions in surveys.
20
20
  from __future__ import annotations
21
21
  import copy
22
22
  import os
23
- import json
24
23
  from collections import UserDict
25
24
  from typing import Union, List, Optional, TYPE_CHECKING, Collection
26
25
  from uuid import uuid4
@@ -97,8 +96,7 @@ class Scenario(Base, UserDict):
97
96
  data = dict(data)
98
97
  except Exception as e:
99
98
  raise ScenarioError(
100
- f"You must pass in a dictionary to initialize a Scenario. You passed in {data}",
101
- "Exception message:" + str(e),
99
+ f"You must pass in a dictionary to initialize a Scenario. You passed in {data}" + "Exception message:" + str(e),
102
100
  )
103
101
 
104
102
  super().__init__()
@@ -30,6 +30,7 @@ from typing import (
30
30
  import warnings
31
31
  import csv
32
32
  import random
33
+ import os
33
34
  from io import StringIO
34
35
  import inspect
35
36
  from collections import UserList, defaultdict
@@ -57,8 +58,9 @@ from ..dataset import ScenarioListOperationsMixin
57
58
 
58
59
  from .exceptions import ScenarioError
59
60
  from .scenario import Scenario
60
- from .directory_scanner import DirectoryScanner
61
61
  from .scenario_list_pdf_tools import PdfTools
62
+ from .directory_scanner import DirectoryScanner
63
+ from .file_store import FileStore
62
64
 
63
65
 
64
66
  if TYPE_CHECKING:
@@ -485,7 +487,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
485
487
  if isinstance(other, Scenario):
486
488
  other = ScenarioList([other])
487
489
  elif not isinstance(other, ScenarioList):
488
- raise TypeError(f"Cannot multiply ScenarioList with {type(other)}")
490
+ from .exceptions import TypeScenarioError
491
+ raise TypeScenarioError(f"Cannot multiply ScenarioList with {type(other)}")
489
492
 
490
493
  new_sl = []
491
494
  for s1, s2 in list(product(self, other)):
@@ -596,7 +599,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
596
599
  # Convert to a set (removes duplicates)
597
600
  new_scenario[field_name] = set(values)
598
601
  else:
599
- raise ValueError(f"Invalid output_type: {output_type}. Must be 'string', 'list', or 'set'.")
602
+ from .exceptions import ValueScenarioError
603
+ raise ValueScenarioError(f"Invalid output_type: {output_type}. Must be 'string', 'list', or 'set'.")
600
604
 
601
605
  new_scenarios.append(new_scenario)
602
606
 
@@ -887,6 +891,127 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
887
891
  sl = self.duplicate()
888
892
  return ScenarioList([scenario.keep(fields) for scenario in sl])
889
893
 
894
+ @classmethod
895
+ def from_directory(
896
+ cls,
897
+ path: Optional[str] = None,
898
+ recursive: bool = False,
899
+ key_name: str = "content",
900
+ ) -> "ScenarioList":
901
+ """Create a ScenarioList of Scenario objects from files in a directory.
902
+
903
+ This method scans a directory and creates a Scenario object for each file found,
904
+ where each Scenario contains a FileStore object under the specified key.
905
+ Optionally filters files based on a wildcard pattern. If no path is provided,
906
+ the current working directory is used.
907
+
908
+ Args:
909
+ path: The directory path to scan, optionally including a wildcard pattern.
910
+ If None, uses the current working directory.
911
+ Examples:
912
+ - "/path/to/directory" - scans all files in the directory
913
+ - "/path/to/directory/*.py" - scans only Python files in the directory
914
+ - "*.txt" - scans only text files in the current working directory
915
+ recursive: Whether to scan subdirectories recursively. Defaults to False.
916
+ key_name: The key to use for the FileStore object in each Scenario. Defaults to "content".
917
+
918
+ Returns:
919
+ A ScenarioList containing Scenario objects for all matching files, where each Scenario
920
+ has a FileStore object under the specified key.
921
+
922
+ Raises:
923
+ FileNotFoundError: If the specified directory does not exist.
924
+
925
+ Examples:
926
+ # Get all files in the current directory with default key "content"
927
+ sl = ScenarioList.from_directory()
928
+
929
+ # Get all Python files in a specific directory with custom key "python_file"
930
+ sl = ScenarioList.from_directory('*.py', key_name="python_file")
931
+
932
+ # Get all image files in the current directory
933
+ sl = ScenarioList.from_directory('*.png', key_name="image")
934
+
935
+ # Get all files recursively including subdirectories
936
+ sl = ScenarioList.from_directory(recursive=True, key_name="document")
937
+ """
938
+ # Handle default case - use current directory
939
+ if path is None:
940
+ directory_path = os.getcwd()
941
+ pattern = None
942
+ else:
943
+ # Special handling for "**" pattern which indicates recursive scanning
944
+ has_recursive_pattern = '**' in path if path else False
945
+
946
+ # Check if path contains any wildcard
947
+ if path and ('*' in path):
948
+ # Handle "**/*.ext" pattern - find the directory part before the **
949
+ if has_recursive_pattern:
950
+ # Extract the base directory by finding the part before **
951
+ parts = path.split('**')
952
+ if parts and parts[0]:
953
+ # Remove trailing slash if any
954
+ directory_path = parts[0].rstrip('/')
955
+ if not directory_path:
956
+ directory_path = os.getcwd()
957
+ # Get the pattern after **
958
+ pattern = parts[1] if len(parts) > 1 else None
959
+ if pattern and pattern.startswith('/'):
960
+ pattern = pattern[1:] # Remove leading slash
961
+ else:
962
+ directory_path = os.getcwd()
963
+ pattern = None
964
+ # Handle case where path is just a pattern (e.g., "*.py")
965
+ elif os.path.dirname(path) == '':
966
+ directory_path = os.getcwd()
967
+ pattern = os.path.basename(path)
968
+ else:
969
+ # Split into directory and pattern
970
+ directory_path = os.path.dirname(path)
971
+ if not directory_path:
972
+ directory_path = os.getcwd()
973
+ pattern = os.path.basename(path)
974
+ else:
975
+ # Path is a directory with no pattern
976
+ directory_path = path
977
+ pattern = None
978
+
979
+ # Ensure directory exists
980
+ if not os.path.isdir(directory_path):
981
+ from .exceptions import FileNotFoundScenarioError
982
+ raise FileNotFoundScenarioError(f"Directory not found: {directory_path}")
983
+
984
+ # Create a DirectoryScanner for the directory
985
+ scanner = DirectoryScanner(directory_path)
986
+
987
+ # Configure wildcard pattern filtering
988
+ suffix_allow_list = None
989
+ example_suffix = None
990
+
991
+ if pattern:
992
+ if pattern.startswith('*.'):
993
+ # Simple extension filter (e.g., "*.py")
994
+ suffix_allow_list = [pattern[2:]]
995
+ elif '*' in pattern:
996
+ # Other wildcard patterns
997
+ example_suffix = pattern
998
+ else:
999
+ # Handle simple non-wildcard pattern (exact match)
1000
+ example_suffix = pattern
1001
+
1002
+ # Use scanner to find files and create FileStore objects
1003
+ file_stores = scanner.scan(
1004
+ factory=lambda path: FileStore(path),
1005
+ recursive=recursive,
1006
+ suffix_allow_list=suffix_allow_list,
1007
+ example_suffix=example_suffix
1008
+ )
1009
+
1010
+ # Convert FileStore objects to Scenario objects with the specified key
1011
+ scenarios = [Scenario({key_name: file_store}) for file_store in file_stores]
1012
+
1013
+ return cls(scenarios)
1014
+
890
1015
  @classmethod
891
1016
  def from_list(
892
1017
  cls, name: str, values: list, func: Optional[Callable] = None
@@ -903,7 +1028,9 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
903
1028
  ScenarioList([Scenario({'name': 'Alice'}), Scenario({'name': 'Bob'})])
904
1029
  """
905
1030
  if not func:
906
- func = lambda x: x
1031
+ def identity(x):
1032
+ return x
1033
+ func = identity
907
1034
  return cls([Scenario({name: func(value)}) for value in values])
908
1035
 
909
1036
  def table(
@@ -914,7 +1041,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
914
1041
  ) -> str:
915
1042
  """Return the ScenarioList as a table."""
916
1043
 
917
- from tabulate import tabulate_formats
918
1044
 
919
1045
  if tablefmt is not None and tablefmt not in tabulate_formats:
920
1046
  raise ValueError(
@@ -951,13 +1077,10 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
951
1077
 
952
1078
  Example:
953
1079
 
954
- >>> s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 3, 'b': 4})])
955
- >>> s.reorder_keys(['b', 'a'])
956
- ScenarioList([Scenario({'b': 2, 'a': 1}), Scenario({'b': 4, 'a': 3})])
957
- >>> s.reorder_keys(['a', 'b', 'c'])
958
- Traceback (most recent call last):
959
- ...
960
- AssertionError
1080
+ # Example:
1081
+ # s = ScenarioList([Scenario({'a': 1, 'b': 2}), Scenario({'a': 3, 'b': 4})])
1082
+ # s.reorder_keys(['b', 'a']) # Returns a new ScenarioList with reordered keys
1083
+ # Attempting s.reorder_keys(['a', 'b', 'c']) would fail as 'c' is not a valid key
961
1084
  """
962
1085
  assert set(new_order) == set(self.parameters)
963
1086
 
@@ -1020,7 +1143,7 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1020
1143
  return ScenarioList(new_scenarios)
1021
1144
 
1022
1145
  @classmethod
1023
- def from_list_of_tuples(self, *names: str, values: List[Tuple]) -> ScenarioList:
1146
+ def from_list_of_tuples(self, *names: str, values: List[tuple]) -> ScenarioList:
1024
1147
  sl = ScenarioList.from_list(names[0], [value[0] for value in values])
1025
1148
  for index, name in enumerate(names[1:]):
1026
1149
  sl = sl.add_list(name, [value[index + 1] for value in values])
@@ -1148,7 +1271,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1148
1271
  import sqlite3
1149
1272
 
1150
1273
  if table is None and sql_query is None:
1151
- raise ValueError("Either table or sql_query must be provided")
1274
+ from .exceptions import ValueScenarioError
1275
+ raise ValueScenarioError("Either table or sql_query must be provided")
1152
1276
 
1153
1277
  try:
1154
1278
  with sqlite3.connect(filepath) as conn:
@@ -1210,12 +1334,12 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1210
1334
  """
1211
1335
  import tempfile
1212
1336
  import requests
1213
- from docx import Document
1214
1337
 
1215
1338
  if "/edit" in url:
1216
1339
  doc_id = url.split("/d/")[1].split("/edit")[0]
1217
1340
  else:
1218
- raise ValueError("Invalid Google Doc URL format.")
1341
+ from .exceptions import ValueScenarioError
1342
+ raise ValueScenarioError("Invalid Google Doc URL format.")
1219
1343
 
1220
1344
  export_url = f"https://docs.google.com/document/d/{doc_id}/export?format=docx"
1221
1345
 
@@ -1419,7 +1543,8 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1419
1543
  print("The Excel file contains multiple sheets:")
1420
1544
  for name in all_sheets.keys():
1421
1545
  print(f"- {name}")
1422
- raise ValueError("Please provide a sheet name to load data from.")
1546
+ from .exceptions import ValueScenarioError
1547
+ raise ValueScenarioError("Please provide a sheet name to load data from.")
1423
1548
  else:
1424
1549
  # If there is only one sheet, use it
1425
1550
  sheet_name = list(all_sheets.keys())[0]
@@ -1468,14 +1593,14 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1468
1593
  ScenarioList: An instance of the ScenarioList class.
1469
1594
 
1470
1595
  """
1471
- import pandas as pd
1472
1596
  import tempfile
1473
1597
  import requests
1474
1598
 
1475
1599
  if "/edit" in url:
1476
1600
  sheet_id = url.split("/d/")[1].split("/edit")[0]
1477
1601
  else:
1478
- raise ValueError("Invalid Google Sheet URL format.")
1602
+ from .exceptions import ValueScenarioError
1603
+ raise ValueScenarioError("Invalid Google Sheet URL format.")
1479
1604
 
1480
1605
  export_url = (
1481
1606
  f"https://docs.google.com/spreadsheets/d/{sheet_id}/export?format=xlsx"
@@ -1542,14 +1667,39 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1542
1667
  response.raise_for_status()
1543
1668
  file_obj = StringIO(response.text)
1544
1669
  else:
1545
- file_obj = open(source, "r")
1670
+ # Try different encodings if the default fails
1671
+ encodings_to_try = ["utf-8", "latin-1", "cp1252", "ISO-8859-1"]
1672
+ last_exception = None
1673
+ file_obj = None
1674
+
1675
+ for encoding in encodings_to_try:
1676
+ try:
1677
+ file_obj = open(source, "r", encoding=encoding)
1678
+ # Test reading a bit to verify encoding
1679
+ file_obj.readline()
1680
+ file_obj.seek(0) # Reset file position
1681
+ break
1682
+ except UnicodeDecodeError as e:
1683
+ last_exception = e
1684
+ if file_obj:
1685
+ file_obj.close()
1686
+ file_obj = None
1687
+
1688
+ if file_obj is None:
1689
+ from .exceptions import ValueScenarioError
1690
+ raise ValueScenarioError(f"Could not decode file {source} with any of the attempted encodings. Original error: {last_exception}")
1546
1691
 
1547
1692
  reader = csv.reader(file_obj, delimiter=delimiter)
1548
- header = next(reader)
1549
- observations = [Scenario(dict(zip(header, row))) for row in reader]
1693
+ try:
1694
+ header = next(reader)
1695
+ observations = [Scenario(dict(zip(header, row))) for row in reader]
1696
+ except StopIteration:
1697
+ from .exceptions import ValueScenarioError
1698
+ raise ValueScenarioError(f"File {source} appears to be empty or has an invalid format")
1550
1699
 
1551
1700
  finally:
1552
- file_obj.close()
1701
+ if file_obj:
1702
+ file_obj.close()
1553
1703
 
1554
1704
  return cls(observations)
1555
1705
 
@@ -1614,7 +1764,6 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1614
1764
  """
1615
1765
  from ..surveys import Survey
1616
1766
  from ..questions import QuestionBase
1617
- from ..jobs import Jobs
1618
1767
 
1619
1768
  if isinstance(survey, QuestionBase):
1620
1769
  return Survey([survey]).by(self)
@@ -1862,13 +2011,16 @@ class ScenarioList(Base, UserList, ScenarioListOperationsMixin):
1862
2011
  import string
1863
2012
 
1864
2013
  if num_options < 2:
1865
- raise ValueError("num_options must be at least 2")
2014
+ from .exceptions import ValueScenarioError
2015
+ raise ValueScenarioError("num_options must be at least 2")
1866
2016
 
1867
2017
  if num_options > len(self):
1868
- raise ValueError(f"num_options ({num_options}) cannot exceed the number of scenarios ({len(self)})")
2018
+ from .exceptions import ValueScenarioError
2019
+ raise ValueScenarioError(f"num_options ({num_options}) cannot exceed the number of scenarios ({len(self)})")
1869
2020
 
1870
2021
  if use_alphabet and num_options > 26:
1871
- raise ValueError("When using alphabet labels, num_options cannot exceed 26 (the number of letters in the English alphabet)")
2022
+ from .exceptions import ValueScenarioError
2023
+ raise ValueScenarioError("When using alphabet labels, num_options cannot exceed 26 (the number of letters in the English alphabet)")
1872
2024
 
1873
2025
  # Convert each scenario to a dictionary
1874
2026
  scenario_dicts = [scenario.to_dict(add_edsl_version=False) for scenario in self]
@@ -175,6 +175,7 @@ class PdfTools:
175
175
  image_path = os.path.join(output_folder, f"page_{i+1}.{image_format}")
176
176
  image.save(image_path, image_format.upper())
177
177
 
178
+ from ..file_store import FileStore
178
179
  scenario = Scenario({
179
180
  "filepath":image_path,
180
181
  "page":i,
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
3
3
 
4
4
  if TYPE_CHECKING:
5
5
  from .scenario_list import ScenarioList
6
- from .scenario import Scenario
7
6
 
8
7
  class ScenarioSelector:
9
8
  """
edsl/surveys/__init__.py CHANGED
@@ -1,8 +1,7 @@
1
1
  from .survey import Survey
2
- from .survey_flow_visualization import SurveyFlowVisualization
3
-
4
- from .rules import Rule, RuleCollection
5
- from .base import EndOfSurvey, RulePriority
2
+ from .survey_flow_visualization import SurveyFlowVisualization # noqa: F401
3
+ from .rules import Rule, RuleCollection # noqa: F401
4
+ from .base import EndOfSurvey, RulePriority # noqa: F401
6
5
 
7
6
  __all__ = ["Survey"]
8
7
  ## , "SurveyFlowVisualization", "Rule", "RuleCollection", "EndOfSurvey", "RulePriority"]
@@ -1,2 +1,4 @@
1
- from .dag import DAG
2
- from .construct_dag import ConstructDAG
1
+ from .dag import DAG # noqa: F401
2
+ from .construct_dag import ConstructDAG # noqa: F401
3
+
4
+ __all__ = []
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
4
4
  from typing import Any, TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
- from edsl.questions.QuestionBase import QuestionBase
7
+ pass # Not using any imported types in this file
8
8
 
9
9
 
10
10
  class BaseDescriptor(ABC):
@@ -5,6 +5,7 @@ from typing import Union, Optional, TYPE_CHECKING
5
5
  if TYPE_CHECKING:
6
6
  from ..questions import QuestionBase
7
7
  from .survey import Survey
8
+ from ..instructions import Instruction, ChangeInstruction
8
9
 
9
10
  from .exceptions import SurveyError, SurveyCreationError
10
11
  from .rules.rule import Rule
@@ -1,36 +1,192 @@
1
1
  from ..base import BaseException
2
2
 
3
3
  class SurveyError(BaseException):
4
- relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html"
4
+ """
5
+ Base exception class for all survey-related errors.
6
+
7
+ This exception is the parent class for all exceptions related to Survey operations,
8
+ including creation, validation, and navigation. It provides a common type
9
+ for catching any survey-specific error.
10
+
11
+ This exception is raised directly when:
12
+ - Question names don't meet validation requirements
13
+ - Survey operations encounter general errors not covered by more specific exceptions
14
+ """
15
+ doc_page = "surveys"
5
16
 
6
17
 
7
18
  class SurveyCreationError(SurveyError):
8
- pass
19
+ """
20
+ Exception raised when there's an error creating or modifying a survey.
21
+
22
+ This exception occurs when:
23
+ - Adding skip rules to EndOfSurvey (which isn't allowed)
24
+ - Combining surveys with non-default rules
25
+ - Adding questions with duplicate names
26
+ - Creating invalid question groups
27
+ - Validating group names and boundaries
28
+
29
+ To fix this error:
30
+ 1. Ensure all question names in a survey are unique
31
+ 2. Don't add skip rules to EndOfSurvey
32
+ 3. Check that question groups are properly defined
33
+ 4. When combining surveys, ensure they're compatible
34
+
35
+ Examples:
36
+ ```python
37
+ survey.add(question) # Raises SurveyCreationError if question's name already exists
38
+ survey.add_rules_to_question(EndOfSurvey) # Raises SurveyCreationError
39
+ ```
40
+ """
41
+ doc_anchor = "creating-surveys"
9
42
 
10
43
 
11
44
  class SurveyHasNoRulesError(SurveyError):
12
- pass
45
+ """
46
+ Exception raised when rules are required but not found for a question.
47
+
48
+ This exception occurs when:
49
+ - The survey's next_question method is called but no rules exist for the current question
50
+ - Navigation can't proceed because there's no defined path forward
51
+
52
+ To fix this error:
53
+ 1. Add appropriate rules to all questions in the survey
54
+ 2. Ensure the survey has a complete navigation path from start to finish
55
+ 3. Use default rules where appropriate (add_default_rules method)
56
+
57
+ Examples:
58
+ ```python
59
+ survey.next_question(0) # Raises SurveyHasNoRulesError if question 0 has no rules
60
+ ```
61
+ """
62
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
13
63
 
14
64
 
15
65
  class SurveyRuleSendsYouBackwardsError(SurveyError):
16
- pass
66
+ """
67
+ Exception raised when a rule would navigate backward in the survey flow.
68
+
69
+ This exception occurs during rule initialization to prevent rules that
70
+ would navigate backward in the survey flow, which is not allowed.
71
+
72
+ Backward navigation creates potential loops and is generally considered
73
+ poor survey design. EDSL enforces forward-only navigation.
74
+
75
+ To fix this error:
76
+ 1. Redesign your survey to avoid backward navigation
77
+ 2. Use forward-only rules with proper branching logic
78
+ 3. Consider using memory to carry forward information if needed
79
+
80
+ Examples:
81
+ ```python
82
+ survey.add_rule(question_index=2, rule=Rule(lambda x: True, 1)) # Raises SurveyRuleSendsYouBackwardsError
83
+ ```
84
+ """
85
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
17
86
 
18
87
 
19
88
  class SurveyRuleSkipLogicSyntaxError(SurveyError):
20
- pass
89
+ """
90
+ Exception raised when a rule's expression has invalid syntax.
91
+
92
+ This exception occurs when:
93
+ - The expression in a rule is not valid Python syntax
94
+ - The expression can't be parsed or compiled
95
+
96
+ To fix this error:
97
+ 1. Check the syntax of your rule expression
98
+ 2. Ensure all variables in the expression are properly referenced
99
+ 3. Test the expression in isolation to verify it's valid Python
100
+
101
+ Examples:
102
+ ```python
103
+ Rule(lambda x: x[question] ==, 1) # Raises SurveyRuleSkipLogicSyntaxError (invalid syntax)
104
+ ```
105
+ """
106
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
21
107
 
22
108
 
23
109
  class SurveyRuleReferenceInRuleToUnknownQuestionError(SurveyError):
24
- pass
110
+ """
111
+ Exception raised when a rule references an unknown question.
112
+
113
+ This exception is designed to catch cases where a rule's condition
114
+ references a question that doesn't exist in the survey.
115
+
116
+ To fix this error:
117
+ 1. Ensure all questions referenced in rule conditions exist in the survey
118
+ 2. Check for typos in question names or indices
119
+
120
+ Note: This exception is defined but not actively used in the codebase.
121
+ It raises Exception("not used") to indicate this state.
122
+ """
123
+ def __init__(self, message="Rule references an unknown question", **kwargs):
124
+ super().__init__(message, **kwargs)
25
125
 
26
126
 
27
127
  class SurveyRuleRefersToFutureStateError(SurveyError):
28
- pass
128
+ """
129
+ Exception raised when a rule references questions that come later in the survey.
130
+
131
+ This exception occurs when:
132
+ - A rule condition refers to questions that haven't been presented yet
133
+ - Rule evaluation would require information not yet collected
134
+
135
+ To fix this error:
136
+ 1. Redesign your rules to only reference current or previous questions
137
+ 2. Ensure rule conditions only depend on information already collected
138
+ 3. Restructure your survey if you need different branching logic
139
+
140
+ Examples:
141
+ ```python
142
+ # If question 3 hasn't been asked yet:
143
+ Rule(lambda x: x['q3_answer'] == 'Yes', next_question=4) # Raises SurveyRuleRefersToFutureStateError
144
+ ```
145
+ """
146
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
29
147
 
30
148
 
31
149
  class SurveyRuleCollectionHasNoRulesAtNodeError(SurveyError):
32
- pass
150
+ """
151
+ Exception raised when no rules are found for a specific question during navigation.
152
+
153
+ This exception occurs when:
154
+ - The RuleCollection's next_question method can't find applicable rules
155
+ - A survey is trying to determine the next question but has no rule for the current state
156
+
157
+ To fix this error:
158
+ 1. Add rules for all questions in your survey
159
+ 2. Ensure rules are properly added to the RuleCollection
160
+ 3. Add default rules where appropriate
161
+
162
+ Examples:
163
+ ```python
164
+ # If rule_collection has no rules for question 2:
165
+ rule_collection.next_question(2, {}) # Raises SurveyRuleCollectionHasNoRulesAtNodeError
166
+ ```
167
+ """
168
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rule-collections"
33
169
 
34
170
 
35
171
  class SurveyRuleCannotEvaluateError(SurveyError):
36
- pass
172
+ """
173
+ Exception raised when a rule expression cannot be evaluated.
174
+
175
+ This exception occurs when:
176
+ - The rule's expression fails to evaluate with the provided data
177
+ - Required variables are missing in the evaluation context
178
+ - The expression contains errors that only appear at runtime
179
+
180
+ To fix this error:
181
+ 1. Check that your rule expression is valid
182
+ 2. Ensure all referenced variables are available in the context
183
+ 3. Add error handling in complex expressions
184
+ 4. Test rules with sample data before using in production
185
+
186
+ Examples:
187
+ ```python
188
+ # If 'q1_answer' is not in the data dictionary:
189
+ Rule(lambda x: x['q1_answer'] == 'Yes', 2).evaluate({}) # Raises SurveyRuleCannotEvaluateError
190
+ ```
191
+ """
192
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/surveys.html#rules"
@@ -1,3 +1,5 @@
1
- from .memory_plan import MemoryPlan
2
- from .memory_management import MemoryManagement
3
- from .memory import Memory
1
+ from .memory_plan import MemoryPlan # noqa: F401
2
+ from .memory_management import MemoryManagement # noqa: F401
3
+ from .memory import Memory # noqa: F401
4
+
5
+ __all__ = []