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
@@ -4,6 +4,7 @@ import time
4
4
  from threading import RLock
5
5
 
6
6
  from ..jobs.decorators import synchronized_class
7
+ from .exceptions import TokenLimitError
7
8
 
8
9
 
9
10
  @synchronized_class
@@ -350,6 +351,7 @@ class TokenBucket:
350
351
  - Usage statistics and token levels are logged for tracking purposes
351
352
 
352
353
  Example:
354
+ >>> from edsl.buckets.token_bucket import TokenBucket
353
355
  >>> bucket = TokenBucket(bucket_name="api", bucket_type="test", capacity=100, refill_rate=10)
354
356
  >>> bucket.tokens = 100
355
357
  >>> import asyncio
@@ -358,29 +360,32 @@ class TokenBucket:
358
360
  70
359
361
 
360
362
  >>> # Example with capacity cheating
363
+ >>> from edsl.buckets.token_bucket import TokenBucket
361
364
  >>> bucket = TokenBucket(bucket_name="api", bucket_type="test", capacity=10, refill_rate=1)
362
365
  >>> asyncio.run(bucket.get_tokens(15, cheat_bucket_capacity=True))
363
366
  >>> bucket.capacity > 15 # Capacity should have been increased
364
367
  True
365
368
 
366
- >>> # Example raising ValueError
369
+ >>> # Example raising TokenLimitError
370
+ >>> from edsl.buckets.token_bucket import TokenBucket
371
+ >>> from edsl.buckets.exceptions import TokenLimitError
367
372
  >>> bucket = TokenBucket(bucket_name="api", bucket_type="test", capacity=10, refill_rate=1)
368
373
  >>> try:
369
374
  ... asyncio.run(bucket.get_tokens(15, cheat_bucket_capacity=False))
370
- ... except ValueError as e:
371
- ... print("ValueError raised")
372
- ValueError raised
375
+ ... except TokenLimitError as e:
376
+ ... print("TokenLimitError raised")
377
+ TokenLimitError raised
373
378
  """
374
379
  self.num_requests += amount
375
380
  if amount >= self.capacity:
376
381
  if not cheat_bucket_capacity:
377
382
  msg = f"Requested amount exceeds bucket capacity. Bucket capacity: {self.capacity}, requested amount: {amount}. As the bucket never overflows, the requested amount will never be available."
378
- raise ValueError(msg)
383
+ raise TokenLimitError(msg)
379
384
  else:
380
385
  self.capacity = amount * 1.10
381
386
  self._old_capacity = self.capacity
382
387
 
383
- start_time = time.monotonic()
388
+ # Loop until we have enough tokens
384
389
  while True:
385
390
  self.refill() # Refill based on elapsed time
386
391
  if self.tokens >= amount:
@@ -1,10 +1,11 @@
1
- from fastapi import FastAPI, HTTPException
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import JSONResponse
2
3
  from pydantic import BaseModel
3
4
  from typing import Union, Dict
4
- from typing import Union, List, Any, Optional
5
- from threading import RLock
5
+ from typing import Optional
6
6
 
7
7
  from .token_bucket import TokenBucket # Original implementation
8
+ from .exceptions import BucketNotFoundError, InvalidBucketParameterError
8
9
 
9
10
  def safe_float_for_json(value: float) -> Union[float, str]:
10
11
  """Convert float('inf') to 'infinity' for JSON serialization.
@@ -25,6 +26,20 @@ app = FastAPI()
25
26
  # In-memory storage for TokenBucket instances
26
27
  buckets: Dict[str, TokenBucket] = {}
27
28
 
29
+ @app.exception_handler(BucketNotFoundError)
30
+ async def bucket_not_found_handler(request, exc):
31
+ return JSONResponse(
32
+ status_code=404,
33
+ content={"detail": str(exc)},
34
+ )
35
+
36
+ @app.exception_handler(InvalidBucketParameterError)
37
+ async def invalid_parameter_handler(request, exc):
38
+ return JSONResponse(
39
+ status_code=400,
40
+ content={"detail": str(exc)},
41
+ )
42
+
28
43
 
29
44
  class TokenBucketCreate(BaseModel):
30
45
  bucket_name: str
@@ -84,13 +99,13 @@ async def list_buckets(
84
99
  async def add_tokens(bucket_id: str, amount: float):
85
100
  """Add tokens to an existing bucket."""
86
101
  if bucket_id not in buckets:
87
- raise HTTPException(status_code=404, detail="Bucket not found")
102
+ raise BucketNotFoundError(f"Bucket with ID '{bucket_id}' not found")
88
103
 
89
104
  if not isinstance(amount, (int, float)) or amount != amount: # Check for NaN
90
- raise HTTPException(status_code=400, detail="Invalid amount specified")
105
+ raise InvalidBucketParameterError("Invalid amount specified")
91
106
 
92
107
  if amount == float("inf") or amount == float("-inf"):
93
- raise HTTPException(status_code=400, detail="Amount cannot be infinite")
108
+ raise InvalidBucketParameterError("Amount cannot be infinite")
94
109
 
95
110
  bucket = buckets[bucket_id]
96
111
  bucket.add_tokens(amount)
@@ -125,14 +140,14 @@ async def create_bucket(bucket: TokenBucketCreate):
125
140
  not isinstance(bucket.capacity, (int, float))
126
141
  or bucket.capacity != bucket.capacity
127
142
  ): # Check for NaN
128
- raise HTTPException(status_code=400, detail="Invalid capacity value")
143
+ raise InvalidBucketParameterError("Invalid capacity value")
129
144
  if (
130
145
  not isinstance(bucket.refill_rate, (int, float))
131
146
  or bucket.refill_rate != bucket.refill_rate
132
147
  ): # Check for NaN
133
- raise HTTPException(status_code=400, detail="Invalid refill rate value")
148
+ raise InvalidBucketParameterError("Invalid refill rate value")
134
149
  if bucket.capacity == float("inf") or bucket.refill_rate == float("inf"):
135
- raise HTTPException(status_code=400, detail="Values cannot be infinite")
150
+ raise InvalidBucketParameterError("Values cannot be infinite")
136
151
  bucket_id = f"{bucket.bucket_name}_{bucket.bucket_type}"
137
152
  if bucket_id in buckets:
138
153
  # Instead of error, return success with "existing" status
@@ -157,7 +172,7 @@ async def create_bucket(bucket: TokenBucketCreate):
157
172
  @app.post("/bucket/{bucket_id}/get_tokens")
158
173
  async def get_tokens(bucket_id: str, amount: float, cheat_bucket_capacity: bool = True):
159
174
  if bucket_id not in buckets:
160
- raise HTTPException(status_code=404, detail="Bucket not found")
175
+ raise BucketNotFoundError(f"Bucket with ID '{bucket_id}' not found")
161
176
 
162
177
  bucket = buckets[bucket_id]
163
178
  await bucket.get_tokens(amount, cheat_bucket_capacity)
@@ -167,7 +182,7 @@ async def get_tokens(bucket_id: str, amount: float, cheat_bucket_capacity: bool
167
182
  @app.post("/bucket/{bucket_id}/turbo_mode/{state}")
168
183
  async def set_turbo_mode(bucket_id: str, state: bool):
169
184
  if bucket_id not in buckets:
170
- raise HTTPException(status_code=404, detail="Bucket not found")
185
+ raise BucketNotFoundError(f"Bucket with ID '{bucket_id}' not found")
171
186
 
172
187
  bucket = buckets[bucket_id]
173
188
  if state:
@@ -180,7 +195,7 @@ async def set_turbo_mode(bucket_id: str, state: bool):
180
195
  @app.get("/bucket/{bucket_id}/status")
181
196
  async def get_bucket_status(bucket_id: str):
182
197
  if bucket_id not in buckets:
183
- raise HTTPException(status_code=404, detail="Bucket not found")
198
+ raise BucketNotFoundError(f"Bucket with ID '{bucket_id}' not found")
184
199
 
185
200
  bucket = buckets[bucket_id]
186
201
  status = {
@@ -7,13 +7,15 @@ operations to a remote server, enabling distributed rate limiting across
7
7
  multiple processes or machines.
8
8
  """
9
9
 
10
- from typing import Union, Optional, Dict, List, Any, Tuple
10
+ from typing import Union, Optional, Dict, Any
11
11
  import asyncio
12
12
  import time
13
13
  import aiohttp
14
14
  from matplotlib import pyplot as plt
15
15
  from matplotlib.figure import Figure
16
16
 
17
+ from .exceptions import BucketError, TokenBucketClientError
18
+
17
19
 
18
20
  class TokenBucketClient:
19
21
  """
@@ -125,7 +127,7 @@ class TokenBucketClient:
125
127
  json=payload,
126
128
  ) as response:
127
129
  if response.status != 200:
128
- raise ValueError(f"Unexpected error: {await response.text()}")
130
+ raise TokenBucketClientError(f"Unexpected error: {await response.text()}")
129
131
 
130
132
  # Process server response
131
133
  result = await response.json()
@@ -200,7 +202,7 @@ class TokenBucketClient:
200
202
  params={"amount": amount},
201
203
  ) as response:
202
204
  if response.status != 200:
203
- raise ValueError(f"Failed to add tokens: {await response.text()}")
205
+ raise TokenBucketClientError(f"Failed to add tokens: {await response.text()}")
204
206
 
205
207
  async def _set_turbo_mode(self, state: bool) -> None:
206
208
  """
@@ -220,7 +222,7 @@ class TokenBucketClient:
220
222
  f"{self.api_base_url}/bucket/{self.bucket_id}/turbo_mode/{str(state).lower()}"
221
223
  ) as response:
222
224
  if response.status != 200:
223
- raise ValueError(
225
+ raise TokenBucketClientError(
224
226
  f"Failed to set turbo mode: {await response.text()}"
225
227
  )
226
228
 
@@ -259,7 +261,7 @@ class TokenBucketClient:
259
261
  },
260
262
  ) as response:
261
263
  if response.status != 200:
262
- raise ValueError(f"Failed to get tokens: {await response.text()}")
264
+ raise TokenBucketClientError(f"Failed to get tokens: {await response.text()}")
263
265
 
264
266
  def get_throughput(self, time_window: Optional[float] = None) -> float:
265
267
  """
@@ -324,7 +326,7 @@ class TokenBucketClient:
324
326
  f"{self.api_base_url}/bucket/{self.bucket_id}/status"
325
327
  ) as response:
326
328
  if response.status != 200:
327
- raise ValueError(
329
+ raise TokenBucketClientError(
328
330
  f"Failed to get bucket status: {await response.text()}"
329
331
  )
330
332
  return await response.json()
@@ -408,7 +410,7 @@ class TokenBucketClient:
408
410
  # Calculate time needed to accumulate the required tokens
409
411
  return (requested_tokens - self.tokens) / self.refill_rate
410
412
  except Exception as e:
411
- raise ValueError(f"Error calculating wait time: {e}")
413
+ raise BucketError(f"Error calculating wait time: {e}")
412
414
 
413
415
  # Note: The commented out method below is a reminder for future implementation
414
416
  # def wait_time(self, num_tokens: Union[int, float]) -> float:
edsl/caching/cache.py CHANGED
@@ -32,10 +32,14 @@ import json
32
32
  import os
33
33
  import warnings
34
34
  from typing import Optional, Union, TYPE_CHECKING
35
- from ..base import Base
36
35
 
36
+ from ..base import Base
37
37
  from ..utilities import remove_edsl_version, dict_hash
38
38
  from .exceptions import CacheError
39
+ from .sql_dict import SQLiteDict
40
+
41
+ if TYPE_CHECKING:
42
+ from .cache_entry import CacheEntry
39
43
 
40
44
  class Cache(Base):
41
45
  """Cache for storing and retrieving language model responses.
@@ -172,8 +176,11 @@ class Cache(Base):
172
176
 
173
177
  Examples:
174
178
  >>> from edsl import Cache
175
- >>> Cache.example().values()
176
- [CacheEntry(...)]
179
+ >>> entries = Cache.example().values()
180
+ >>> len(entries)
181
+ 1
182
+ >>> entries[0] # doctest: +ELLIPSIS
183
+ CacheEntry(model='gpt-3.5-turbo', parameters={'temperature': 0.5}, ...)
177
184
  """
178
185
  return list(self.data.values())
179
186
 
@@ -418,9 +425,10 @@ class Cache(Base):
418
425
  """
419
426
  # if a file doesn't exist at jsonfile, throw an error
420
427
  from .sql_dict import SQLiteDict
428
+ from .exceptions import CacheFileNotFoundError
421
429
 
422
430
  if not os.path.exists(jsonlfile):
423
- raise FileNotFoundError(f"File {jsonlfile} not found")
431
+ raise CacheFileNotFoundError(f"File {jsonlfile} not found")
424
432
 
425
433
  if db_path is None:
426
434
  data = {}
@@ -11,10 +11,11 @@ from __future__ import annotations
11
11
  import json
12
12
  import datetime
13
13
  import hashlib
14
- from typing import Optional, Dict, List, Any, Union
14
+ from typing import Optional, Dict, List, Any
15
15
  from uuid import uuid4
16
16
 
17
17
  from ..base import RepresentationMixin
18
+ from .exceptions import CacheError
18
19
 
19
20
 
20
21
  class CacheEntry(RepresentationMixin):
@@ -81,22 +82,22 @@ class CacheEntry(RepresentationMixin):
81
82
  TypeError: If any attribute has an incorrect type
82
83
  """
83
84
  if not isinstance(self.model, str):
84
- raise TypeError("`model` should be a string.")
85
+ raise CacheError("`model` should be a string.")
85
86
  if not isinstance(self.parameters, dict):
86
- raise TypeError("`parameters` should be a dictionary.")
87
+ raise CacheError("`parameters` should be a dictionary.")
87
88
  if not isinstance(self.system_prompt, str):
88
- raise TypeError("`system_prompt` should be a string.")
89
+ raise CacheError("`system_prompt` should be a string.")
89
90
  if not isinstance(self.user_prompt, str):
90
- raise TypeError("`user_prompt` should be a string")
91
+ raise CacheError("`user_prompt` should be a string")
91
92
  if not isinstance(self.output, str):
92
- raise TypeError("`output` should be a string")
93
+ raise CacheError("`output` should be a string")
93
94
  if not isinstance(self.iteration, int):
94
- raise TypeError("`iteration` should be an integer")
95
+ raise CacheError("`iteration` should be an integer")
95
96
  # Note: timestamp is stored as int for compatibility, but could be float in future
96
97
  if not isinstance(self.timestamp, int):
97
- raise TypeError(f"`timestamp` should be an integer")
98
+ raise CacheError("`timestamp` should be an integer")
98
99
  if self.service is not None and not isinstance(self.service, str):
99
- raise TypeError("`service` should be either a string or None")
100
+ raise CacheError("`service` should be either a string or None")
100
101
 
101
102
  @classmethod
102
103
  def gen_key(
@@ -1,5 +1,5 @@
1
1
  """
2
- Custom exceptions for the data module in the EDSL framework.
2
+ Custom exceptions for the caching module in the EDSL framework.
3
3
 
4
4
  This module defines exceptions that are raised during cache operations,
5
5
  such as when entries cannot be stored, retrieved, or synchronized.
@@ -13,12 +13,118 @@ class CacheError(BaseException):
13
13
  Exception raised for errors related to cache operations.
14
14
 
15
15
  This exception is raised when cache operations fail, such as when:
16
- - A cache key cannot be generated
17
- - A cache entry cannot be stored or retrieved
18
- - A cache synchronization operation fails
19
- - Cache migration encounters an error
16
+ - A cache key cannot be generated due to invalid or non-serializable inputs
17
+ - A cache entry cannot be stored or retrieved due to database access issues
18
+ - A cache synchronization operation fails when syncing with remote cache
19
+ - Cache migration encounters an error when upgrading between versions
20
+
21
+ To resolve this error:
22
+ 1. Check that your inputs to cached functions are serializable
23
+ 2. Ensure you have proper file system permissions for the SQLite cache file
24
+ 3. For remote cache issues, verify your network connection and API credentials
25
+ 4. For migration issues, you may need to clear the cache by calling cache.clear()
26
+
27
+ Examples:
28
+ ```python
29
+ # Cache key generation failure
30
+ @cache
31
+ def my_function(non_serializable_object): # Raises CacheError
32
+ return result
33
+
34
+ # Storage failure
35
+ cache.set("key", "value") # May raise CacheError if DB is locked
36
+ ```
20
37
 
21
38
  Attributes:
22
- message (str): Explanation of the error
39
+ message (str): Detailed explanation of the error
40
+ """
41
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/remote_caching.html"
42
+
43
+
44
+ class CacheFileNotFoundError(CacheError):
45
+ """
46
+ Exception raised when a cache file cannot be found.
47
+
48
+ This exception is raised when attempting to load or access a cache file
49
+ that does not exist on the filesystem.
50
+
51
+ To resolve this error:
52
+ 1. Check that the file path is correct
53
+ 2. Verify that the file exists and has proper permissions
54
+ 3. If you're loading a serialized cache, make sure it was previously saved
55
+
56
+ Examples:
57
+ ```python
58
+ # Attempting to load a non-existent cache file
59
+ cache.load_from_file("non_existent_file.json") # Raises CacheFileNotFoundError
60
+ ```
61
+ """
62
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/remote_caching.html"
63
+
64
+
65
+ class CacheValueError(CacheError):
66
+ """
67
+ Exception raised when an invalid value is provided to a cache operation.
68
+
69
+ This exception is raised when:
70
+ - An incorrect type is provided for a cache entry
71
+ - A value cannot be serialized for storage
72
+ - A validation check fails during cache operations
73
+
74
+ To resolve this error:
75
+ 1. Ensure the value has the expected type and structure
76
+ 2. Verify that values being stored are serializable
77
+ 3. Check for any special requirements in the cache implementation
78
+
79
+ Examples:
80
+ ```python
81
+ # Attempting to store an invalid value type
82
+ cache_dict["key"] = non_cache_entry_object # Raises CacheValueError
83
+ ```
84
+ """
85
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/remote_caching.html"
86
+
87
+
88
+ class CacheKeyError(CacheError):
89
+ """
90
+ Exception raised when a key cannot be found in the cache.
91
+
92
+ This exception is raised when attempting to access a cache entry that
93
+ does not exist in the cache.
94
+
95
+ To resolve this error:
96
+ 1. Check that the key exists before accessing it
97
+ 2. Use cache.get() with a default value to handle missing keys gracefully
98
+ 3. Consider initializing the key-value pair before accessing
99
+
100
+ Examples:
101
+ ```python
102
+ # Attempting to access a non-existent key
103
+ value = cache["non_existent_key"] # Raises CacheKeyError
104
+
105
+ # Safer alternative
106
+ value = cache.get("non_existent_key", default=None) # Returns None instead of raising
107
+ ```
108
+ """
109
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/remote_caching.html"
110
+
111
+
112
+ class CacheNotImplementedError(CacheError):
113
+ """
114
+ Exception raised when a cache method is not implemented.
115
+
116
+ This exception is raised when attempting to use a method that is
117
+ defined in the interface but not implemented in the concrete class.
118
+
119
+ To resolve this error:
120
+ 1. Check if the operation is supported by the specific cache implementation
121
+ 2. Consider using an alternative approach that is supported
122
+ 3. If you're implementing a custom cache, implement the required method
123
+
124
+ Examples:
125
+ ```python
126
+ # Attempting to use an unimplemented method
127
+ cache.some_unimplemented_method() # Raises CacheNotImplementedError
128
+ ```
23
129
  """
24
- relevant_doc = "https://docs.expectedparrot.com/en/latest/data.html#cache"
130
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/remote_caching.html"
@@ -1,14 +1,13 @@
1
- from typing import List, Dict, Any, Optional, TYPE_CHECKING, Callable
1
+ from typing import List, TYPE_CHECKING, Callable
2
2
  from dataclasses import dataclass
3
3
  from contextlib import AbstractContextManager
4
4
  from collections import UserList
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from .cache import Cache
8
- from edsl.coop.coop import Coop
8
+ from ..coop.coop import Coop
9
9
  from .cache_entry import CacheEntry
10
10
 
11
- from logging import Logger
12
11
 
13
12
 
14
13
  class CacheKeyList(UserList):
@@ -35,7 +34,7 @@ class CacheEntriesList(UserList):
35
34
  return f"CacheEntries({entries_repr})"
36
35
 
37
36
  def to_cache(self) -> "Cache":
38
- from edsl.caching.cache import Cache
37
+ from .cache import Cache
39
38
 
40
39
  return Cache({entry.key: entry for entry in self.data})
41
40
 
@@ -179,9 +178,9 @@ if __name__ == "__main__":
179
178
 
180
179
  doctest.testmod()
181
180
 
182
- from edsl.coop.coop import Coop
183
- from edsl.data.Cache import Cache
184
- from edsl.data.CacheEntry import CacheEntry
181
+ from ..coop.coop import Coop
182
+ from .cache import Cache
183
+ from .cache_entry import CacheEntry
185
184
 
186
185
  r = RemoteCacheSync(Coop(), Cache(), print)
187
186
  diff = r._get_cache_difference()
edsl/caching/sql_dict.py CHANGED
@@ -9,11 +9,11 @@ making it a drop-in replacement for regular dictionaries but with database persi
9
9
 
10
10
  from __future__ import annotations
11
11
  import json
12
- from typing import Any, Generator, Optional, Union, Dict, List, Tuple, TypeVar
12
+ from typing import Any, Generator, Optional, Union, Dict, TypeVar
13
13
 
14
14
  from ..config import CONFIG
15
15
  from .cache_entry import CacheEntry
16
- from .orm import Base, Data
16
+ from .orm import Data
17
17
 
18
18
  T = TypeVar('T')
19
19
 
@@ -64,8 +64,9 @@ class SQLiteDict:
64
64
 
65
65
  Example:
66
66
  >>> temp_db_path = SQLiteDict._get_temp_path()
67
- >>> SQLiteDict(f"sqlite:///{temp_db_path}") # Use the temp file for SQLite
68
- SQLiteDict(db_path='...')
67
+ >>> db = SQLiteDict(f"sqlite:///{temp_db_path}") # Use the temp file for SQLite
68
+ >>> isinstance(db, SQLiteDict)
69
+ True
69
70
  >>> import os; os.unlink(temp_db_path) # Clean up the temp file after the test
70
71
  """
71
72
  from sqlalchemy.exc import SQLAlchemyError
@@ -76,13 +77,14 @@ class SQLiteDict:
76
77
  if not self.db_path.startswith("sqlite:///"):
77
78
  self.db_path = f"sqlite:///{self.db_path}"
78
79
  try:
79
- from edsl.caching.orm import Base, Data
80
+ from .orm import Base
80
81
 
81
82
  self.engine = create_engine(self.db_path, echo=False, future=True)
82
83
  Base.metadata.create_all(self.engine)
83
84
  self.Session = sessionmaker(bind=self.engine)
84
85
  except SQLAlchemyError as e:
85
- raise Exception(
86
+ from .exceptions import CacheError
87
+ raise CacheError(
86
88
  f"""Database initialization error: {e}. The attempted DB path was {db_path}"""
87
89
  ) from e
88
90
 
@@ -99,7 +101,6 @@ class SQLiteDict:
99
101
  Path to a temporary file location
100
102
  """
101
103
  import tempfile
102
- import os
103
104
 
104
105
  _, temp_db_path = tempfile.mkstemp(suffix=".db")
105
106
  return temp_db_path
@@ -123,9 +124,10 @@ class SQLiteDict:
123
124
  >>> d["foo"] = CacheEntry.example()
124
125
  """
125
126
  if not isinstance(value, CacheEntry):
126
- raise ValueError(f"Value must be a CacheEntry object (got {type(value)}).")
127
+ from .exceptions import CacheValueError
128
+ raise CacheValueError(f"Value must be a CacheEntry object (got {type(value)}).")
127
129
  with self.Session() as db:
128
- from edsl.caching.orm import Base, Data
130
+ from .orm import Data
129
131
 
130
132
  db.merge(Data(key=key, value=json.dumps(value.to_dict())))
131
133
  db.commit()
@@ -154,11 +156,12 @@ class SQLiteDict:
154
156
  True
155
157
  """
156
158
  with self.Session() as db:
157
- from edsl.caching.orm import Base, Data
159
+ from .orm import Data
158
160
 
159
161
  value = db.query(Data).filter_by(key=key).first()
160
162
  if not value:
161
- raise KeyError(f"Key '{key}' not found.")
163
+ from .exceptions import CacheKeyError
164
+ raise CacheKeyError(f"Key '{key}' not found.")
162
165
  return CacheEntry.from_dict(json.loads(value.value))
163
166
 
164
167
  def get(self, key: str, default: Optional[Any] = None) -> Union[CacheEntry, Any]:
@@ -181,9 +184,10 @@ class SQLiteDict:
181
184
  >>> d.get("foo", "bar")
182
185
  'bar'
183
186
  """
187
+ from .exceptions import CacheKeyError
184
188
  try:
185
189
  return self[key]
186
- except KeyError:
190
+ except (KeyError, CacheKeyError):
187
191
  return default
188
192
 
189
193
  def __bool__(self) -> bool:
@@ -233,7 +237,8 @@ class SQLiteDict:
233
237
  the database from being locked for too long.
234
238
  """
235
239
  if not (isinstance(new_d, dict) or isinstance(new_d, SQLiteDict)):
236
- raise ValueError(
240
+ from .exceptions import CacheValueError
241
+ raise CacheValueError(
237
242
  f"new_d must be a dict or SQLiteDict object (got {type(new_d)})"
238
243
  )
239
244
  current_batch = 0
@@ -301,7 +306,8 @@ class SQLiteDict:
301
306
  db.delete(instance)
302
307
  db.commit()
303
308
  else:
304
- raise KeyError(f"Key '{key}' not found.")
309
+ from .exceptions import CacheKeyError
310
+ raise CacheKeyError(f"Key '{key}' not found.")
305
311
 
306
312
  def __contains__(self, key: str) -> bool:
307
313
  """
edsl/cli.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ EDSL Command Line Interface.
3
+
4
+ This module provides the main entry point for the EDSL command-line tool.
5
+ """
6
+
7
+ import sys
8
+ import typer
9
+ from typing import Optional
10
+ from rich.console import Console
11
+ from importlib import metadata
12
+
13
+ # Create the main Typer app
14
+ app = typer.Typer(help="Expected Parrot EDSL Command Line Interface")
15
+ console = Console()
16
+
17
+ # Import the plugins app
18
+ from .plugins.cli_typer import app as plugins_app
19
+
20
+ # Add the plugins subcommand
21
+ app.add_typer(plugins_app, name="plugins")
22
+
23
+ @app.callback()
24
+ def callback():
25
+ """
26
+ Expected Parrot EDSL Command Line Interface.
27
+
28
+ A toolkit for creating, managing, and running surveys with language models.
29
+ """
30
+ pass
31
+
32
+ @app.command()
33
+ def version():
34
+ """Show the EDSL version."""
35
+ try:
36
+ version = metadata.version("edsl")
37
+ console.print(f"[bold cyan]EDSL version:[/bold cyan] {version}")
38
+ except metadata.PackageNotFoundError:
39
+ console.print("[yellow]EDSL package not installed or version not available.[/yellow]")
40
+
41
+ def main():
42
+ """Main entry point for the EDSL CLI."""
43
+ app()
edsl/config/__init__.py CHANGED
@@ -3,6 +3,6 @@
3
3
  This module provides a Config class that loads environment variables from a .env file and sets them as class attributes.
4
4
  """
5
5
 
6
- from edsl.config.config_class import Config, CONFIG, CONFIG_MAP, EDSL_RUN_MODES, cache_dir
6
+ from .config_class import Config, CONFIG, CONFIG_MAP, EDSL_RUN_MODES, cache_dir
7
7
 
8
8
  __all__ = ["Config", "CONFIG", "CONFIG_MAP", "EDSL_RUN_MODES", "cache_dir"]