edsl 0.1.48__py3-none-any.whl → 0.1.50__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 (239) 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 +75 -0
  14. edsl/buckets/model_buckets.py +1 -2
  15. edsl/buckets/token_bucket.py +11 -6
  16. edsl/buckets/token_bucket_api.py +1 -2
  17. edsl/buckets/token_bucket_client.py +9 -7
  18. edsl/caching/cache.py +7 -2
  19. edsl/caching/cache_entry.py +10 -9
  20. edsl/caching/exceptions.py +113 -7
  21. edsl/caching/remote_cache_sync.py +1 -2
  22. edsl/caching/sql_dict.py +17 -12
  23. edsl/cli.py +43 -0
  24. edsl/config/config_class.py +30 -6
  25. edsl/conversation/Conversation.py +3 -2
  26. edsl/conversation/exceptions.py +58 -0
  27. edsl/conversation/mug_negotiation.py +0 -2
  28. edsl/coop/__init__.py +20 -1
  29. edsl/coop/coop.py +129 -38
  30. edsl/coop/exceptions.py +188 -9
  31. edsl/coop/price_fetcher.py +3 -6
  32. edsl/coop/utils.py +4 -6
  33. edsl/dataset/__init__.py +5 -4
  34. edsl/dataset/dataset.py +53 -43
  35. edsl/dataset/dataset_operations_mixin.py +86 -72
  36. edsl/dataset/dataset_tree.py +9 -5
  37. edsl/dataset/display/table_display.py +0 -2
  38. edsl/dataset/display/table_renderers.py +0 -1
  39. edsl/dataset/exceptions.py +125 -0
  40. edsl/dataset/file_exports.py +18 -11
  41. edsl/dataset/r/ggplot.py +13 -6
  42. edsl/display/__init__.py +27 -0
  43. edsl/display/core.py +147 -0
  44. edsl/display/plugin.py +189 -0
  45. edsl/display/utils.py +52 -0
  46. edsl/inference_services/__init__.py +9 -1
  47. edsl/inference_services/available_model_cache_handler.py +1 -1
  48. edsl/inference_services/available_model_fetcher.py +4 -5
  49. edsl/inference_services/data_structures.py +9 -6
  50. edsl/inference_services/exceptions.py +132 -1
  51. edsl/inference_services/inference_service_abc.py +2 -2
  52. edsl/inference_services/inference_services_collection.py +2 -6
  53. edsl/inference_services/registry.py +4 -3
  54. edsl/inference_services/service_availability.py +2 -1
  55. edsl/inference_services/services/anthropic_service.py +4 -1
  56. edsl/inference_services/services/aws_bedrock.py +13 -12
  57. edsl/inference_services/services/azure_ai.py +12 -10
  58. edsl/inference_services/services/deep_infra_service.py +1 -4
  59. edsl/inference_services/services/deep_seek_service.py +1 -5
  60. edsl/inference_services/services/google_service.py +6 -2
  61. edsl/inference_services/services/groq_service.py +1 -1
  62. edsl/inference_services/services/mistral_ai_service.py +4 -2
  63. edsl/inference_services/services/ollama_service.py +1 -1
  64. edsl/inference_services/services/open_ai_service.py +7 -5
  65. edsl/inference_services/services/perplexity_service.py +6 -2
  66. edsl/inference_services/services/test_service.py +8 -7
  67. edsl/inference_services/services/together_ai_service.py +2 -3
  68. edsl/inference_services/services/xai_service.py +1 -1
  69. edsl/instructions/__init__.py +1 -1
  70. edsl/instructions/change_instruction.py +3 -2
  71. edsl/instructions/exceptions.py +61 -0
  72. edsl/instructions/instruction.py +5 -2
  73. edsl/instructions/instruction_collection.py +2 -1
  74. edsl/instructions/instruction_handler.py +4 -9
  75. edsl/interviews/ReportErrors.py +0 -3
  76. edsl/interviews/__init__.py +9 -2
  77. edsl/interviews/answering_function.py +11 -13
  78. edsl/interviews/exception_tracking.py +14 -7
  79. edsl/interviews/exceptions.py +79 -0
  80. edsl/interviews/interview.py +32 -29
  81. edsl/interviews/interview_status_dictionary.py +4 -2
  82. edsl/interviews/interview_status_log.py +2 -1
  83. edsl/interviews/interview_task_manager.py +3 -3
  84. edsl/interviews/request_token_estimator.py +3 -1
  85. edsl/interviews/statistics.py +2 -3
  86. edsl/invigilators/__init__.py +7 -1
  87. edsl/invigilators/exceptions.py +79 -0
  88. edsl/invigilators/invigilator_base.py +0 -1
  89. edsl/invigilators/invigilators.py +8 -12
  90. edsl/invigilators/prompt_constructor.py +1 -5
  91. edsl/invigilators/prompt_helpers.py +8 -4
  92. edsl/invigilators/question_instructions_prompt_builder.py +1 -1
  93. edsl/invigilators/question_option_processor.py +9 -5
  94. edsl/invigilators/question_template_replacements_builder.py +3 -2
  95. edsl/jobs/__init__.py +3 -3
  96. edsl/jobs/async_interview_runner.py +24 -22
  97. edsl/jobs/check_survey_scenario_compatibility.py +7 -6
  98. edsl/jobs/data_structures.py +7 -4
  99. edsl/jobs/exceptions.py +177 -8
  100. edsl/jobs/fetch_invigilator.py +1 -1
  101. edsl/jobs/jobs.py +72 -67
  102. edsl/jobs/jobs_checks.py +2 -3
  103. edsl/jobs/jobs_component_constructor.py +2 -2
  104. edsl/jobs/jobs_pricing_estimation.py +3 -2
  105. edsl/jobs/jobs_remote_inference_logger.py +5 -4
  106. edsl/jobs/jobs_runner_asyncio.py +1 -2
  107. edsl/jobs/jobs_runner_status.py +8 -9
  108. edsl/jobs/remote_inference.py +26 -23
  109. edsl/jobs/results_exceptions_handler.py +8 -5
  110. edsl/key_management/__init__.py +3 -1
  111. edsl/key_management/exceptions.py +62 -0
  112. edsl/key_management/key_lookup.py +1 -1
  113. edsl/key_management/key_lookup_builder.py +37 -14
  114. edsl/key_management/key_lookup_collection.py +2 -0
  115. edsl/language_models/__init__.py +1 -1
  116. edsl/language_models/exceptions.py +302 -14
  117. edsl/language_models/language_model.py +4 -7
  118. edsl/language_models/model.py +4 -4
  119. edsl/language_models/model_list.py +1 -1
  120. edsl/language_models/price_manager.py +1 -1
  121. edsl/language_models/raw_response_handler.py +14 -9
  122. edsl/language_models/registry.py +17 -21
  123. edsl/language_models/repair.py +0 -6
  124. edsl/language_models/unused/fake_openai_service.py +0 -1
  125. edsl/load_plugins.py +69 -0
  126. edsl/logger.py +146 -0
  127. edsl/notebooks/notebook.py +1 -1
  128. edsl/notebooks/notebook_to_latex.py +0 -1
  129. edsl/plugins/__init__.py +63 -0
  130. edsl/plugins/built_in/export_example.py +50 -0
  131. edsl/plugins/built_in/pig_latin.py +67 -0
  132. edsl/plugins/cli.py +372 -0
  133. edsl/plugins/cli_typer.py +283 -0
  134. edsl/plugins/exceptions.py +31 -0
  135. edsl/plugins/hookspec.py +51 -0
  136. edsl/plugins/plugin_host.py +128 -0
  137. edsl/plugins/plugin_manager.py +633 -0
  138. edsl/plugins/plugins_registry.py +168 -0
  139. edsl/prompts/__init__.py +2 -0
  140. edsl/prompts/exceptions.py +107 -5
  141. edsl/prompts/prompt.py +14 -6
  142. edsl/questions/HTMLQuestion.py +5 -11
  143. edsl/questions/Quick.py +0 -1
  144. edsl/questions/__init__.py +2 -0
  145. edsl/questions/answer_validator_mixin.py +318 -318
  146. edsl/questions/compose_questions.py +2 -2
  147. edsl/questions/descriptors.py +10 -49
  148. edsl/questions/exceptions.py +278 -22
  149. edsl/questions/loop_processor.py +7 -5
  150. edsl/questions/prompt_templates/question_list.jinja +3 -0
  151. edsl/questions/question_base.py +14 -16
  152. edsl/questions/question_base_gen_mixin.py +2 -2
  153. edsl/questions/question_base_prompts_mixin.py +9 -3
  154. edsl/questions/question_budget.py +9 -5
  155. edsl/questions/question_check_box.py +3 -5
  156. edsl/questions/question_dict.py +171 -194
  157. edsl/questions/question_extract.py +1 -1
  158. edsl/questions/question_free_text.py +4 -6
  159. edsl/questions/question_functional.py +4 -3
  160. edsl/questions/question_list.py +36 -9
  161. edsl/questions/question_matrix.py +95 -61
  162. edsl/questions/question_multiple_choice.py +6 -4
  163. edsl/questions/question_numerical.py +2 -4
  164. edsl/questions/question_registry.py +4 -2
  165. edsl/questions/register_questions_meta.py +0 -1
  166. edsl/questions/response_validator_abc.py +7 -13
  167. edsl/questions/templates/dict/answering_instructions.jinja +1 -0
  168. edsl/questions/templates/rank/question_presentation.jinja +1 -1
  169. edsl/results/__init__.py +1 -1
  170. edsl/results/exceptions.py +141 -7
  171. edsl/results/report.py +0 -1
  172. edsl/results/result.py +4 -5
  173. edsl/results/results.py +10 -51
  174. edsl/results/results_selector.py +8 -4
  175. edsl/scenarios/PdfExtractor.py +2 -2
  176. edsl/scenarios/construct_download_link.py +69 -35
  177. edsl/scenarios/directory_scanner.py +33 -14
  178. edsl/scenarios/document_chunker.py +1 -1
  179. edsl/scenarios/exceptions.py +238 -14
  180. edsl/scenarios/file_methods.py +1 -1
  181. edsl/scenarios/file_store.py +7 -3
  182. edsl/scenarios/handlers/__init__.py +17 -0
  183. edsl/scenarios/handlers/docx_file_store.py +0 -5
  184. edsl/scenarios/handlers/pdf_file_store.py +0 -1
  185. edsl/scenarios/handlers/pptx_file_store.py +0 -5
  186. edsl/scenarios/handlers/py_file_store.py +0 -1
  187. edsl/scenarios/handlers/sql_file_store.py +1 -4
  188. edsl/scenarios/handlers/sqlite_file_store.py +0 -1
  189. edsl/scenarios/handlers/txt_file_store.py +1 -1
  190. edsl/scenarios/scenario.py +0 -1
  191. edsl/scenarios/scenario_list.py +152 -18
  192. edsl/scenarios/scenario_list_pdf_tools.py +1 -0
  193. edsl/scenarios/scenario_selector.py +0 -1
  194. edsl/surveys/__init__.py +3 -4
  195. edsl/surveys/dag/__init__.py +4 -2
  196. edsl/surveys/descriptors.py +1 -1
  197. edsl/surveys/edit_survey.py +1 -0
  198. edsl/surveys/exceptions.py +165 -9
  199. edsl/surveys/memory/__init__.py +5 -3
  200. edsl/surveys/memory/memory_management.py +1 -0
  201. edsl/surveys/memory/memory_plan.py +6 -15
  202. edsl/surveys/rules/__init__.py +5 -3
  203. edsl/surveys/rules/rule.py +1 -2
  204. edsl/surveys/rules/rule_collection.py +1 -1
  205. edsl/surveys/survey.py +12 -24
  206. edsl/surveys/survey_export.py +6 -3
  207. edsl/surveys/survey_flow_visualization.py +10 -1
  208. edsl/tasks/__init__.py +2 -0
  209. edsl/tasks/question_task_creator.py +3 -3
  210. edsl/tasks/task_creators.py +1 -3
  211. edsl/tasks/task_history.py +5 -7
  212. edsl/tasks/task_status_log.py +1 -2
  213. edsl/tokens/__init__.py +3 -1
  214. edsl/tokens/token_usage.py +1 -1
  215. edsl/utilities/__init__.py +21 -1
  216. edsl/utilities/decorators.py +1 -2
  217. edsl/utilities/markdown_to_docx.py +2 -2
  218. edsl/utilities/markdown_to_pdf.py +1 -1
  219. edsl/utilities/repair_functions.py +0 -1
  220. edsl/utilities/restricted_python.py +0 -1
  221. edsl/utilities/template_loader.py +2 -3
  222. edsl/utilities/utilities.py +8 -29
  223. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/METADATA +32 -2
  224. edsl-0.1.50.dist-info/RECORD +363 -0
  225. edsl-0.1.50.dist-info/entry_points.txt +3 -0
  226. edsl/dataset/smart_objects.py +0 -96
  227. edsl/exceptions/BaseException.py +0 -21
  228. edsl/exceptions/__init__.py +0 -54
  229. edsl/exceptions/configuration.py +0 -16
  230. edsl/exceptions/general.py +0 -34
  231. edsl/study/ObjectEntry.py +0 -173
  232. edsl/study/ProofOfWork.py +0 -113
  233. edsl/study/SnapShot.py +0 -80
  234. edsl/study/Study.py +0 -520
  235. edsl/study/__init__.py +0 -6
  236. edsl/utilities/interface.py +0 -135
  237. edsl-0.1.48.dist-info/RECORD +0 -347
  238. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
  239. {edsl-0.1.48.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
edsl/coop/__init__.py CHANGED
@@ -8,6 +8,7 @@ This module enables EDSL to interact with cloud-based resources for enhanced fun
8
8
  3. Caching of interview results for improved performance and cost savings
9
9
  4. API key management and authentication
10
10
  5. Price and model availability information
11
+ 6. Plugin registry and discovery
11
12
 
12
13
  The primary interface is the Coop class, which serves as a client for the
13
14
  Expected Parrot API. Most users will only need to interact with the Coop class directly.
@@ -17,9 +18,27 @@ Example:
17
18
  >>> coop = Coop() # Uses API key from environment or stored location
18
19
  >>> survey = my_survey.push() # Uploads survey to Expected Parrot
19
20
  >>> job_info = coop.remote_inference_create(my_job) # Creates remote job
21
+
22
+ # Working with plugins
23
+ >>> from edsl.coop import get_available_plugins
24
+ >>> plugins = get_available_plugins()
25
+ >>> plugin_names = [p.name for p in plugins]
20
26
  """
21
27
 
22
28
  from .utils import EDSLObject, ObjectType, VisibilityType, ObjectRegistry
23
29
  from .coop import Coop
24
30
  from .exceptions import CoopServerResponseError
25
- __all__ = ["Coop"]
31
+
32
+ __all__ = [
33
+ "Coop",
34
+ "EDSLObject",
35
+ "ObjectType",
36
+ "VisibilityType",
37
+ "ObjectRegistry",
38
+ "CoopServerResponseError",
39
+ "AvailablePlugin",
40
+ "get_available_plugins",
41
+ "search_plugins",
42
+ "get_plugin_details",
43
+ "PluginRegistryError"
44
+ ]
edsl/coop/coop.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import aiohttp
2
+ import base64
2
3
  import json
3
4
  import requests
4
5
 
@@ -140,7 +141,7 @@ class Coop(CoopFunctionsMixin):
140
141
  if self.api_key:
141
142
  headers["Authorization"] = f"Bearer {self.api_key}"
142
143
  else:
143
- headers["Authorization"] = f"Bearer None"
144
+ headers["Authorization"] = "Bearer None"
144
145
  return headers
145
146
 
146
147
  def _send_server_request(
@@ -149,7 +150,7 @@ class Coop(CoopFunctionsMixin):
149
150
  method: str,
150
151
  payload: Optional[dict[str, Any]] = None,
151
152
  params: Optional[dict[str, Any]] = None,
152
- timeout: Optional[float] = 5,
153
+ timeout: Optional[float] = 10,
153
154
  ) -> requests.Response:
154
155
  """
155
156
  Send a request to the server and return the response.
@@ -159,7 +160,7 @@ class Coop(CoopFunctionsMixin):
159
160
  if payload is None:
160
161
  timeout = 40
161
162
  elif (
162
- method.upper() == "POST"
163
+ (method.upper() == "POST" or method.upper() == "PATCH")
163
164
  and "json_string" in payload
164
165
  and payload.get("json_string") is not None
165
166
  ):
@@ -179,7 +180,9 @@ class Coop(CoopFunctionsMixin):
179
180
  timeout=timeout,
180
181
  )
181
182
  else:
182
- raise Exception(f"Invalid {method=}.")
183
+ from edsl.coop.exceptions import CoopInvalidMethodError
184
+
185
+ raise CoopInvalidMethodError(f"Invalid {method=}.")
183
186
  except requests.ConnectionError:
184
187
  raise requests.ConnectionError(f"Could not connect to the server at {url}.")
185
188
 
@@ -226,16 +229,17 @@ class Coop(CoopFunctionsMixin):
226
229
  """
227
230
  # Get EDSL version from header
228
231
  # breakpoint()
229
- server_edsl_version = response.headers.get("X-EDSL-Version")
230
-
231
- if server_edsl_version:
232
- if self._user_version_is_outdated(
233
- user_version_str=self._edsl_version,
234
- server_version_str=server_edsl_version,
235
- ):
236
- print(
237
- "Please upgrade your EDSL version to access our latest features. Open your terminal and run `pip install --upgrade edsl`"
238
- )
232
+ # Commented out as currently unused
233
+ # server_edsl_version = response.headers.get("X-EDSL-Version")
234
+
235
+ # if server_edsl_version:
236
+ # if self._user_version_is_outdated(
237
+ # user_version_str=self._edsl_version,
238
+ # server_version_str=server_edsl_version,
239
+ # ):
240
+ # print(
241
+ # "Please upgrade your EDSL version to access our latest features. Open your terminal and run `pip install --upgrade edsl`"
242
+ # )
239
243
 
240
244
  if response.status_code >= 400:
241
245
  try:
@@ -266,7 +270,7 @@ class Coop(CoopFunctionsMixin):
266
270
 
267
271
  print("\n✨ API key retrieved.")
268
272
 
269
- if stored_in_user_space := self.ep_key_handler.ask_to_store(api_key):
273
+ if self.ep_key_handler.ask_to_store(api_key):
270
274
  pass
271
275
  else:
272
276
  path_to_env = write_api_key_to_env(api_key)
@@ -299,13 +303,19 @@ class Coop(CoopFunctionsMixin):
299
303
  message = root.find("Message").text
300
304
  details = root.find("Details").text
301
305
  except Exception:
302
- raise Exception(
303
- f"Server returned status code {response.status_code}",
304
- "XML response could not be decoded.",
305
- "The server response was: " + response.text,
306
+ from edsl.coop.exceptions import CoopServerResponseError
307
+
308
+ raise CoopServerResponseError(
309
+ f"Server returned status code {response.status_code}. "
310
+ f"XML response could not be decoded. "
311
+ f"The server response was: {response.text}"
306
312
  )
307
313
 
308
- raise Exception(f"An error occurred: {code} - {message} - {details}")
314
+ from edsl.coop.exceptions import CoopServerResponseError
315
+
316
+ raise CoopServerResponseError(
317
+ f"An error occurred: {code} - {message} - {details}"
318
+ )
309
319
 
310
320
  def _poll_for_api_key(
311
321
  self, edsl_auth_token: str, timeout: int = 120
@@ -432,6 +442,23 @@ class Coop(CoopFunctionsMixin):
432
442
  else:
433
443
  return None
434
444
 
445
+ def _scenario_is_file_store(self, scenario_dict: dict) -> bool:
446
+ """
447
+ Check if the scenario object is a valid FileStore.
448
+
449
+ Matches keys in the scenario dict against the expected keys for a FileStore.
450
+ """
451
+ file_store_keys = [
452
+ "path",
453
+ "base64_string",
454
+ "binary",
455
+ "suffix",
456
+ "mime_type",
457
+ "external_locations",
458
+ "extracted_text",
459
+ ]
460
+ return all(key in scenario_dict.keys() for key in file_store_keys)
461
+
435
462
  def create(
436
463
  self,
437
464
  object: EDSLObject,
@@ -471,21 +498,30 @@ class Coop(CoopFunctionsMixin):
471
498
  >>> print(result["url"]) # URL to access the survey
472
499
  """
473
500
  object_type = ObjectRegistry.get_object_type_by_edsl_class(object)
501
+ object_dict = object.to_dict()
502
+ if object_type == "scenario" and self._scenario_is_file_store(object_dict):
503
+ file_store_metadata = {
504
+ "suffix": object_dict["suffix"],
505
+ "mime_type": object_dict["mime_type"],
506
+ }
507
+ else:
508
+ file_store_metadata = None
474
509
  response = self._send_server_request(
475
- uri=f"api/v0/object",
510
+ uri="api/v0/object",
476
511
  method="POST",
477
512
  payload={
478
513
  "description": description,
479
514
  "alias": alias,
480
515
  "json_string": (
481
516
  json.dumps(
482
- object.to_dict(),
517
+ object_dict,
483
518
  default=self._json_handle_none,
484
519
  )
485
520
  if object_type != "scenario"
486
521
  else ""
487
522
  ),
488
523
  "object_type": object_type,
524
+ "file_store_metadata": file_store_metadata,
489
525
  "visibility": visibility,
490
526
  "version": self._edsl_version,
491
527
  },
@@ -495,19 +531,57 @@ class Coop(CoopFunctionsMixin):
495
531
 
496
532
  if object_type == "scenario":
497
533
  json_data = json.dumps(
498
- object.to_dict(),
534
+ object_dict,
499
535
  default=self._json_handle_none,
500
536
  )
501
537
  headers = {"Content-Type": "application/json"}
502
538
  if response_json.get("upload_signed_url"):
503
539
  signed_url = response_json.get("upload_signed_url")
504
540
  else:
505
- raise Exception("No signed url provided received")
541
+ from edsl.coop.exceptions import CoopResponseError
542
+
543
+ raise CoopResponseError("No signed url was provided received")
506
544
 
507
545
  response = requests.put(
508
546
  signed_url, data=json_data.encode(), headers=headers
509
547
  )
510
548
  self._resolve_gcs_response(response)
549
+
550
+ file_store_upload_signed_url = response_json.get(
551
+ "file_store_upload_signed_url"
552
+ )
553
+ if file_store_metadata and not file_store_upload_signed_url:
554
+ from edsl.coop.exceptions import CoopResponseError
555
+
556
+ raise CoopResponseError("No file store signed url provided.")
557
+ elif file_store_metadata:
558
+ headers = {"Content-Type": file_store_metadata["mime_type"]}
559
+ # Lint json files prior to upload
560
+ if file_store_metadata["suffix"] == "json":
561
+ file_store_bytes = base64.b64decode(object_dict["base64_string"])
562
+ pretty_json_string = json.dumps(
563
+ json.loads(file_store_bytes), indent=4
564
+ )
565
+ byte_data = pretty_json_string.encode("utf-8")
566
+ # Lint python files prior to upload
567
+ elif file_store_metadata["suffix"] == "py":
568
+ import black
569
+
570
+ file_store_bytes = base64.b64decode(object_dict["base64_string"])
571
+ python_string = file_store_bytes.decode("utf-8")
572
+ formatted_python_string = black.format_str(
573
+ python_string, mode=black.Mode()
574
+ )
575
+ byte_data = formatted_python_string.encode("utf-8")
576
+ else:
577
+ byte_data = base64.b64decode(object_dict["base64_string"])
578
+ response = requests.put(
579
+ file_store_upload_signed_url,
580
+ data=byte_data,
581
+ headers=headers,
582
+ )
583
+ self._resolve_gcs_response(response)
584
+
511
585
  owner_username = response_json.get("owner_username")
512
586
  object_alias = response_json.get("alias")
513
587
 
@@ -519,7 +593,6 @@ class Coop(CoopFunctionsMixin):
519
593
  "uuid": response_json.get("uuid"),
520
594
  "version": self._edsl_version,
521
595
  "visibility": response_json.get("visibility"),
522
- "upload_signed_url": response_json.get("upload_signed_url", None),
523
596
  }
524
597
 
525
598
  def get(
@@ -566,13 +639,13 @@ class Coop(CoopFunctionsMixin):
566
639
 
567
640
  if obj_uuid:
568
641
  response = self._send_server_request(
569
- uri=f"api/v0/object",
642
+ uri="api/v0/object",
570
643
  method="GET",
571
644
  params={"uuid": obj_uuid},
572
645
  )
573
646
  else:
574
647
  response = self._send_server_request(
575
- uri=f"api/v0/object/alias",
648
+ uri="api/v0/object/alias",
576
649
  method="GET",
577
650
  params={"owner_username": owner_username, "alias": alias},
578
651
  )
@@ -586,7 +659,11 @@ class Coop(CoopFunctionsMixin):
586
659
  json_string = object_data.text
587
660
  object_type = response.json().get("object_type")
588
661
  if expected_object_type and object_type != expected_object_type:
589
- raise Exception(f"Expected {expected_object_type=} but got {object_type=}")
662
+ from edsl.coop.exceptions import CoopObjectTypeError
663
+
664
+ raise CoopObjectTypeError(
665
+ f"Expected {expected_object_type=} but got {object_type=}"
666
+ )
590
667
  edsl_class = ObjectRegistry.object_type_to_edsl_class.get(object_type)
591
668
  object = edsl_class.from_dict(json.loads(json_string))
592
669
  return object
@@ -597,7 +674,7 @@ class Coop(CoopFunctionsMixin):
597
674
  """
598
675
  edsl_class = ObjectRegistry.object_type_to_edsl_class.get(object_type)
599
676
  response = self._send_server_request(
600
- uri=f"api/v0/objects",
677
+ uri="api/v0/objects",
601
678
  method="GET",
602
679
  params={"type": object_type},
603
680
  )
@@ -677,7 +754,9 @@ class Coop(CoopFunctionsMixin):
677
754
  and value is None
678
755
  and alias is None
679
756
  ):
680
- raise Exception("Nothing to patch.")
757
+ from edsl.coop.exceptions import CoopPatchError
758
+
759
+ raise CoopPatchError("Nothing to patch.")
681
760
 
682
761
  obj_uuid, owner_username, obj_alias = self._resolve_uuid_or_alias(url_or_uuid)
683
762
 
@@ -808,7 +887,9 @@ class Coop(CoopFunctionsMixin):
808
887
  [CacheEntry(...), CacheEntry(...), ...]
809
888
  """
810
889
  if job_uuid is None:
811
- raise ValueError("Must provide a job_uuid.")
890
+ from edsl.coop.exceptions import CoopValueError
891
+
892
+ raise CoopValueError("Must provide a job_uuid.")
812
893
  response = self._send_server_request(
813
894
  uri="api/v0/remote-cache/get-many-by-job",
814
895
  method="POST",
@@ -836,7 +917,9 @@ class Coop(CoopFunctionsMixin):
836
917
  [CacheEntry(...), CacheEntry(...), ...]
837
918
  """
838
919
  if select_keys is None or len(select_keys) == 0:
839
- raise ValueError("Must provide a non-empty list of select_keys.")
920
+ from edsl.coop.exceptions import CoopValueError
921
+
922
+ raise CoopValueError("Must provide a non-empty list of select_keys.")
840
923
  response = self._send_server_request(
841
924
  uri="api/v0/remote-cache/get-many-by-key",
842
925
  method="POST",
@@ -1099,7 +1182,9 @@ class Coop(CoopFunctionsMixin):
1099
1182
  ... print(f"Results available at: {job_status['results_url']}")
1100
1183
  """
1101
1184
  if job_uuid is None and results_uuid is None:
1102
- raise ValueError("Either job_uuid or results_uuid must be provided.")
1185
+ from edsl.coop.exceptions import CoopValueError
1186
+
1187
+ raise CoopValueError("Either job_uuid or results_uuid must be provided.")
1103
1188
  elif job_uuid is not None:
1104
1189
  params = {"job_uuid": job_uuid}
1105
1190
  else:
@@ -1136,7 +1221,7 @@ class Coop(CoopFunctionsMixin):
1136
1221
  "latest_error_report_uuid": latest_error_report_uuid,
1137
1222
  "latest_error_report_url": latest_error_report_url,
1138
1223
  "status": data.get("status"),
1139
- "reason": data.get("reason"),
1224
+ "reason": data.get("latest_failure_reason"),
1140
1225
  "credits_consumed": data.get("price"),
1141
1226
  "version": data.get("version"),
1142
1227
  }
@@ -1173,7 +1258,9 @@ class Coop(CoopFunctionsMixin):
1173
1258
  elif isinstance(input, Survey):
1174
1259
  job = Jobs(survey=input)
1175
1260
  else:
1176
- raise TypeError("Input must be either a Job or a Survey.")
1261
+ from edsl.coop.exceptions import CoopTypeError
1262
+
1263
+ raise CoopTypeError("Input must be either a Job or a Survey.")
1177
1264
 
1178
1265
  response = self._send_server_request(
1179
1266
  uri="api/v0/remote-inference/cost",
@@ -1215,7 +1302,7 @@ class Coop(CoopFunctionsMixin):
1215
1302
  )
1216
1303
  survey_uuid = survey_details.get("uuid")
1217
1304
  response = self._send_server_request(
1218
- uri=f"api/v0/projects/create-from-survey",
1305
+ uri="api/v0/projects/create-from-survey",
1219
1306
  method="POST",
1220
1307
  payload={"project_name": project_name, "survey_uuid": str(survey_uuid)},
1221
1308
  )
@@ -1308,7 +1395,9 @@ class Coop(CoopFunctionsMixin):
1308
1395
  elif CONFIG.get("EDSL_FETCH_TOKEN_PRICES") == "False":
1309
1396
  return {}
1310
1397
  else:
1311
- raise ValueError(
1398
+ from edsl.coop.exceptions import CoopValueError
1399
+
1400
+ raise CoopValueError(
1312
1401
  "Invalid EDSL_FETCH_TOKEN_PRICES value---should be 'True' or 'False'."
1313
1402
  )
1314
1403
 
@@ -1464,7 +1553,9 @@ class Coop(CoopFunctionsMixin):
1464
1553
  api_key = self._poll_for_api_key(edsl_auth_token)
1465
1554
 
1466
1555
  if api_key is None:
1467
- raise Exception("Timed out waiting for login. Please try again.")
1556
+ from edsl.coop.exceptions import CoopTimeoutError
1557
+
1558
+ raise CoopTimeoutError("Timed out waiting for login. Please try again.")
1468
1559
 
1469
1560
  path_to_env = write_api_key_to_env(api_key)
1470
1561
  print("\n✨ API key retrieved and written to .env file at the following path:")
edsl/coop/exceptions.py CHANGED
@@ -16,8 +16,11 @@ class CoopErrors(BaseException):
16
16
  This is the parent class for all exceptions raised by the Coop module.
17
17
  It inherits from EDSL's BaseException to maintain consistency with
18
18
  the library's exception hierarchy.
19
+
20
+ When catching errors from Coop operations, you can catch this exception
21
+ to handle all Coop-related errors in a single exception handler.
19
22
  """
20
- pass
23
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html"
21
24
 
22
25
 
23
26
  class CoopInvalidURLError(CoopErrors):
@@ -27,12 +30,21 @@ class CoopInvalidURLError(CoopErrors):
27
30
  This exception is raised when a URL provided to the Coop client
28
31
  does not match the expected format for object or resource URLs.
29
32
 
33
+ To fix this error:
34
+ 1. Ensure the URL follows the correct pattern:
35
+ - https://expectedparrot.com/content/{uuid}
36
+ - https://expectedparrot.com/content/{username}/{alias}
37
+ 2. Check for typos in the domain name or path structure
38
+ 3. Verify that you're using a complete URL rather than just a UUID or path
39
+
30
40
  Example:
31
- When a URL doesn't follow the pattern:
32
- - https://expectedparrot.com/content/{uuid}
33
- - https://expectedparrot.com/content/{username}/{alias}
41
+ ```python
42
+ coop = Coop()
43
+ coop.get("https://wrongdomain.com/content/123") # Raises CoopInvalidURLError
44
+ coop.get("https://expectedparrot.com/wrong-path/123") # Raises CoopInvalidURLError
45
+ ```
34
46
  """
35
- pass
47
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html#accessing-content"
36
48
 
37
49
 
38
50
  class CoopNoUUIDError(CoopErrors):
@@ -42,10 +54,18 @@ class CoopNoUUIDError(CoopErrors):
42
54
  This exception is raised when an operation requires a UUID or other
43
55
  identifier, but none was provided.
44
56
 
57
+ To fix this error:
58
+ 1. Ensure you're providing either a valid UUID or a complete URL to the operation
59
+ 2. If using an alias instead of a UUID, make sure the alias exists and is formatted correctly
60
+
45
61
  Example:
46
- When calling get() without providing a UUID or URL
62
+ ```python
63
+ coop = Coop()
64
+ coop.get() # Raises CoopNoUUIDError - missing required UUID or URL
65
+ coop.get("") # Raises CoopNoUUIDError - empty string is not a valid UUID
66
+ ```
47
67
  """
48
- pass
68
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html#accessing-content"
49
69
 
50
70
 
51
71
  class CoopServerResponseError(CoopErrors):
@@ -56,7 +76,166 @@ class CoopServerResponseError(CoopErrors):
56
76
  response, such as authentication failures, rate limits, or server errors.
57
77
  The exception message typically includes the error details from the server.
58
78
 
79
+ To fix this error:
80
+ 1. Check the exception message for specific error details from the server
81
+ 2. For authentication errors (401), verify your API key is correct and not expired
82
+ 3. For rate limit errors (429), reduce the frequency of your requests
83
+ 4. For server errors (500+), the issue may be temporary - wait and try again
84
+ 5. Check your network connection if you're getting connection timeout errors
85
+
86
+ Example:
87
+ ```python
88
+ coop = Coop(api_key="invalid-key")
89
+ coop.get("valid-uuid") # Raises CoopServerResponseError with 401 Unauthorized
90
+ ```
91
+ """
92
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
93
+
94
+
95
+ class CoopInvalidMethodError(CoopErrors):
96
+ """
97
+ Exception raised when an invalid method is requested.
98
+
99
+ This exception is raised when attempting to use an HTTP method that is not
100
+ supported by the Coop API client or by a specific endpoint.
101
+
102
+ To fix this error:
103
+ 1. Check that you're using a valid HTTP method (GET, POST, PUT, DELETE, PATCH)
104
+ 2. Verify that the endpoint you're accessing supports the method you're using
105
+ 3. Consider using a higher-level method in the Coop class rather than direct HTTP methods
106
+
107
+ Example:
108
+ ```python
109
+ coop = Coop()
110
+ coop._request("INVALID_METHOD", "/endpoint") # Raises CoopInvalidMethodError
111
+ ```
112
+ """
113
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html"
114
+
115
+
116
+ class CoopResponseError(CoopErrors):
117
+ """
118
+ Exception raised when there's an issue with the server response.
119
+
120
+ This exception is raised when the server response cannot be processed or
121
+ is missing expected data, even though the HTTP status code might be successful.
122
+
123
+ To fix this error:
124
+ 1. Check the exception message for details about what data was missing
125
+ 2. Verify that your request contains all required parameters
126
+ 3. Ensure you're using the correct endpoint for your intended operation
127
+
128
+ Example:
129
+ ```python
130
+ coop = Coop()
131
+ # Assuming an endpoint that returns incomplete data
132
+ coop.get("some-resource") # Might raise CoopResponseError if response is malformed
133
+ ```
134
+ """
135
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html"
136
+
137
+
138
+ class CoopObjectTypeError(CoopErrors):
139
+ """
140
+ Exception raised when an object has an unexpected type.
141
+
142
+ This exception is raised when an object retrieved from the server has a type
143
+ that doesn't match what was expected, particularly when using typed methods.
144
+
145
+ To fix this error:
146
+ 1. Make sure you're using the correct method for the object type you want
147
+ 2. Verify that the UUID or URL points to the expected object type
148
+ 3. Use the generic get() method if you're uncertain about the object type
149
+
150
+ Example:
151
+ ```python
152
+ coop = Coop()
153
+ # Trying to get a Survey object but the UUID points to a Job
154
+ coop.get_survey("job-uuid") # Raises CoopObjectTypeError
155
+ ```
156
+ """
157
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html#accessing-content"
158
+
159
+
160
+ class CoopPatchError(CoopErrors):
161
+ """
162
+ Exception raised when a patch operation cannot be performed.
163
+
164
+ This exception is raised when attempting to update an object but not
165
+ providing any fields to update, or if the patch operation fails.
166
+
167
+ To fix this error:
168
+ 1. Ensure you're providing at least one field to update
169
+ 2. Verify that you have permission to update the object
170
+ 3. Check that the object still exists and hasn't been deleted
171
+
172
+ Example:
173
+ ```python
174
+ coop = Coop()
175
+ coop.patch("object-uuid") # Raises CoopPatchError if no update fields provided
176
+ ```
177
+ """
178
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html#updating-content"
179
+
180
+
181
+ class CoopValueError(CoopErrors):
182
+ """
183
+ Exception raised when invalid parameters are provided to a Coop operation.
184
+
185
+ This exception is raised when required parameters are missing or when
186
+ parameters have invalid values.
187
+
188
+ To fix this error:
189
+ 1. Check that all required parameters are provided
190
+ 2. Verify that parameter values meet the expected format or constraints
191
+ 3. Consult the API documentation for the correct parameter usage
192
+
193
+ Example:
194
+ ```python
195
+ coop = Coop()
196
+ coop.get_job_results() # Raises CoopValueError if job_uuid is not provided
197
+ ```
198
+ """
199
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html"
200
+
201
+
202
+ class CoopTypeError(CoopErrors):
203
+ """
204
+ Exception raised when a parameter has an incorrect type.
205
+
206
+ This exception is raised when a parameter provided to a Coop method
207
+ has a type that doesn't match what's expected.
208
+
209
+ To fix this error:
210
+ 1. Check the expected types of method parameters
211
+ 2. Convert parameters to the correct type before passing them
212
+ 3. Ensure you're using EDSL objects where required
213
+
214
+ Example:
215
+ ```python
216
+ coop = Coop()
217
+ coop.create("not_an_edsl_object") # Raises CoopTypeError
218
+ ```
219
+ """
220
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html"
221
+
222
+
223
+ class CoopTimeoutError(CoopErrors):
224
+ """
225
+ Exception raised when a Coop operation times out.
226
+
227
+ This exception is raised when an operation takes longer than the
228
+ specified timeout period to complete.
229
+
230
+ To fix this error:
231
+ 1. Increase the timeout value for long-running operations
232
+ 2. Check your network connection if timeouts occur frequently
233
+ 3. For login operations, try again or use API key authentication instead
234
+
59
235
  Example:
60
- When the server returns a 401 Unauthorized response due to an invalid API key
236
+ ```python
237
+ coop = Coop()
238
+ coop.login(timeout=1) # Raises CoopTimeoutError if login takes longer than 1 second
239
+ ```
61
240
  """
62
- pass
241
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/using_coop.html"
@@ -7,9 +7,7 @@ that price information is only fetched once and then cached for efficiency.
7
7
  """
8
8
 
9
9
  import requests
10
- import csv
11
- from io import StringIO
12
- from typing import Dict, Tuple, Optional, Any
10
+ from typing import Dict, Tuple, Any
13
11
 
14
12
 
15
13
  class PriceFetcher:
@@ -85,7 +83,6 @@ class PriceFetcher:
85
83
  return self._cached_prices
86
84
 
87
85
  import os
88
- import requests
89
86
  from ..config import CONFIG
90
87
 
91
88
  try:
@@ -96,7 +93,7 @@ class PriceFetcher:
96
93
  if api_key:
97
94
  headers["Authorization"] = f"Bearer {api_key}"
98
95
  else:
99
- headers["Authorization"] = f"Bearer None"
96
+ headers["Authorization"] = "Bearer None"
100
97
 
101
98
  response = requests.get(url, headers=headers, timeout=20)
102
99
  response.raise_for_status() # Raise an exception for bad responses
@@ -120,7 +117,7 @@ class PriceFetcher:
120
117
  self._cached_prices = price_lookup
121
118
  return self._cached_prices
122
119
 
123
- except requests.RequestException as e:
120
+ except requests.RequestException:
124
121
  # Silently handle errors and return empty dict
125
122
  # print(f"An error occurred: {e}")
126
123
  return {}