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
@@ -1,6 +1,11 @@
1
1
  from typing import Optional, Union, Literal, TYPE_CHECKING, NewType, Callable, Any
2
-
3
2
  from dataclasses import dataclass
3
+ from ..coop import CoopServerResponseError
4
+ from ..coop.utils import VisibilityType
5
+ from ..coop.coop import RemoteInferenceResponse, RemoteInferenceCreationInfo
6
+ from .jobs_status_enums import JobsStatus
7
+ from .jobs_remote_inference_logger import JobLogger
8
+ from .exceptions import RemoteInferenceError
4
9
 
5
10
 
6
11
  Seconds = NewType("Seconds", float)
@@ -9,14 +14,7 @@ JobUUID = NewType("JobUUID", str)
9
14
  if TYPE_CHECKING:
10
15
  from ..results import Results
11
16
  from .jobs import Jobs
12
- from .jobs_remote_inference_logger import JobLogger
13
-
14
- from ..coop import CoopServerResponseError
15
- from ..coop.utils import VisibilityType
16
- from ..coop.coop import RemoteInferenceResponse, RemoteInferenceCreationInfo
17
17
 
18
- from .jobs_status_enums import JobsStatus
19
- from .jobs_remote_inference_logger import JobLogger
20
18
 
21
19
  class RemoteJobConstants:
22
20
  """Constants for remote job handling."""
@@ -53,7 +51,6 @@ class JobsRemoteInferenceHandler:
53
51
  def _create_logger(self) -> JobLogger:
54
52
  from ..utilities import is_notebook
55
53
  from .jobs_remote_inference_logger import (
56
- JupyterJobLogger,
57
54
  StdOutJobLogger,
58
55
  )
59
56
  from .html_table_job_logger import HTMLTableJobLogger
@@ -75,7 +72,7 @@ class JobsRemoteInferenceHandler:
75
72
  return user_edsl_settings.get("remote_inference", False)
76
73
  except requests.ConnectionError:
77
74
  pass
78
- except CoopServerResponseError as e:
75
+ except CoopServerResponseError:
79
76
  pass
80
77
 
81
78
  return False
@@ -84,10 +81,9 @@ class JobsRemoteInferenceHandler:
84
81
  self,
85
82
  iterations: int = 1,
86
83
  remote_inference_description: Optional[str] = None,
87
- remote_inference_results_visibility: Optional['VisibilityType'] = "unlisted",
84
+ remote_inference_results_visibility: Optional["VisibilityType"] = "unlisted",
88
85
  fresh: Optional[bool] = False,
89
86
  ) -> RemoteJobInfo:
90
- from ..config import CONFIG
91
87
  from ..coop import Coop
92
88
 
93
89
  logger = self._create_logger()
@@ -137,7 +133,7 @@ class JobsRemoteInferenceHandler:
137
133
  @staticmethod
138
134
  def check_status(
139
135
  job_uuid: JobUUID,
140
- ) -> 'RemoteInferenceResponse':
136
+ ) -> "RemoteInferenceResponse":
141
137
  from ..coop import Coop
142
138
 
143
139
  coop = Coop()
@@ -182,6 +178,13 @@ class JobsRemoteInferenceHandler:
182
178
  ) -> None:
183
179
  "Handles a failed job by logging the error and updating the job status."
184
180
  latest_error_report_url = remote_job_data.get("latest_error_report_url")
181
+
182
+ reason = remote_job_data.get("reason")
183
+
184
+ if reason == "insufficient funds":
185
+ latest_error_report_url = "Error: Insufficient balance to start the job"
186
+ print("❌ Error: Insufficient balance to start the job")
187
+
185
188
  if latest_error_report_url:
186
189
  job_info.logger.add_info("error_report_url", latest_error_report_url)
187
190
 
@@ -235,13 +238,13 @@ class JobsRemoteInferenceHandler:
235
238
  """Makes one attempt to fetch and process a remote job's status and results."""
236
239
  remote_job_data = remote_job_data_fetcher(job_info.job_uuid)
237
240
  status = remote_job_data.get("status")
238
-
241
+ reason = remote_job_data.get("reason")
239
242
  if status == "cancelled":
240
243
  self._handle_cancelled_job(job_info)
241
- return None
244
+ return None, reason
242
245
 
243
- elif status == "failed" or status == "completed":
244
- if status == "failed":
246
+ elif status == "failed" or status == "completed" or status == "partial_failed":
247
+ if status == "failed" or status == "partial_failed":
245
248
  self._handle_failed_job(job_info, remote_job_data)
246
249
 
247
250
  results_uuid = remote_job_data.get("results_uuid")
@@ -252,13 +255,13 @@ class JobsRemoteInferenceHandler:
252
255
  remote_job_data=remote_job_data,
253
256
  object_fetcher=object_fetcher,
254
257
  )
255
- return results
258
+ return results, reason
256
259
  else:
257
- return None
260
+ return None, reason
258
261
 
259
262
  else:
260
263
  self._sleep_for_a_bit(job_info, status)
261
- return "continue"
264
+ return "continue", reason
262
265
 
263
266
  def poll_remote_inference_job(
264
267
  self,
@@ -274,11 +277,11 @@ class JobsRemoteInferenceHandler:
274
277
 
275
278
  job_in_queue = True
276
279
  while job_in_queue:
277
- result = self._attempt_fetch_job(
280
+ result, reason = self._attempt_fetch_job(
278
281
  job_info, remote_job_data_fetcher, object_fetcher
279
282
  )
280
283
  if result != "continue":
281
- return result
284
+ return result, reason
282
285
 
283
286
  async def create_and_poll_remote_job(
284
287
  self,
@@ -310,7 +313,7 @@ class JobsRemoteInferenceHandler:
310
313
  ),
311
314
  )
312
315
  if job_info is None:
313
- raise ValueError("Remote job creation failed.")
316
+ raise RemoteInferenceError("Remote job creation failed.")
314
317
 
315
318
  return await loop.run_in_executor(
316
319
  None,
@@ -1,8 +1,10 @@
1
- from typing import Optional, TYPE_CHECKING, Protocol
1
+ from typing import Protocol
2
2
  import sys
3
3
  #from edsl.scenarios.FileStore import HTMLFileStore
4
4
  from edsl.config import CONFIG
5
5
  from edsl.coop.coop import Coop
6
+ from ..scenarios import FileStore
7
+ from .exceptions import JobsErrors
6
8
 
7
9
 
8
10
  class ResultsProtocol(Protocol):
@@ -41,7 +43,8 @@ class ResultsExceptionsHandler:
41
43
  self.parameters = parameters
42
44
 
43
45
  self.open_in_browser = self._get_browser_setting()
44
- self.remote_logging = self._get_remote_logging_setting()
46
+ #self.remote_logging = self._get_remote_logging_setting()
47
+ self.remote_logging = False
45
48
 
46
49
  def _get_browser_setting(self) -> bool:
47
50
  """Determine if exceptions should be opened in browser based on config."""
@@ -51,7 +54,7 @@ class ResultsExceptionsHandler:
51
54
  elif setting == "False":
52
55
  return False
53
56
  else:
54
- raise Exception(
57
+ raise JobsErrors(
55
58
  "EDSL_OPEN_EXCEPTION_REPORT_URL must be either True or False"
56
59
  )
57
60
 
@@ -60,13 +63,13 @@ class ResultsExceptionsHandler:
60
63
  try:
61
64
  coop = Coop()
62
65
  return coop.edsl_settings["remote_logging"]
63
- except Exception as e:
66
+ except Exception:
64
67
  # print(e)
65
68
  return False
66
69
 
67
70
  def _generate_error_message(self, indices) -> str:
68
71
  """Generate appropriate error message based on number of exceptions."""
69
- msg = f"Exceptions were raised.\n"
72
+ msg = "Exceptions were raised.\n"
70
73
  return msg
71
74
 
72
75
  def handle_exceptions(self) -> None:
@@ -25,4 +25,6 @@ openai_key = keys['openai'].api_token
25
25
 
26
26
  from .key_lookup import KeyLookup
27
27
  from .key_lookup_collection import KeyLookupCollection
28
- from .key_lookup_builder import KeyLookupBuilder
28
+ from .key_lookup_builder import KeyLookupBuilder
29
+
30
+ __all__ = ["KeyLookup", "KeyLookupCollection", "KeyLookupBuilder"]
@@ -0,0 +1,62 @@
1
+ """
2
+ Exceptions specific to the key_management module.
3
+
4
+ This module defines custom exception classes for all key management-related errors
5
+ in the EDSL framework, ensuring consistent error handling and user feedback.
6
+ """
7
+
8
+ from ..base import BaseException
9
+
10
+
11
+ class KeyManagementError(BaseException):
12
+ """
13
+ Base exception class for all key management-related errors.
14
+
15
+ This is the parent class for all exceptions related to API key management,
16
+ including key lookup, validation, and configuration.
17
+
18
+ Examples:
19
+ ```python
20
+ # Usually not raised directly, but through subclasses
21
+ # For example, when attempting to use an invalid key
22
+ key_lookup.add_key(service="invalid_service", key="api_key") # Would raise KeyManagementServiceError
23
+ ```
24
+ """
25
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
26
+
27
+
28
+ class KeyManagementValueError(KeyManagementError):
29
+ """
30
+ Exception raised when invalid values are provided to key management operations.
31
+
32
+ This exception occurs when:
33
+ - Invalid fetch orders or configuration parameters are provided
34
+ - API key format is invalid
35
+ - Service names don't match expected values
36
+
37
+ Examples:
38
+ ```python
39
+ # Invalid fetch order
40
+ key_lookup_builder.set_fetch_order("invalid_order") # Raises KeyManagementValueError
41
+ ```
42
+ """
43
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
44
+
45
+
46
+ class KeyManagementDuplicateError(KeyManagementError):
47
+ """
48
+ Exception raised when duplicate keys or services are encountered.
49
+
50
+ This exception occurs when:
51
+ - Attempting to add a duplicate service ID
52
+ - Adding a key that already exists for a service
53
+ - Registering multiple handlers for the same service
54
+
55
+ Examples:
56
+ ```python
57
+ # Adding a duplicate service
58
+ builder.register_env_var(service_id="openai", env_var="OPENAI_API_KEY")
59
+ builder.register_env_var(service_id="openai", env_var="ANOTHER_KEY") # Raises KeyManagementDuplicateError
60
+ ```
61
+ """
62
+ relevant_doc = "https://docs.expectedparrot.com/en/latest/api_keys.html"
@@ -19,7 +19,7 @@ class KeyLookup(UserDict):
19
19
  - Creating example instances for testing
20
20
 
21
21
  Typical usage:
22
- >>> from edsl.key_management.models import LanguageModelInput
22
+ >>> from .models import LanguageModelInput # Import for doctest
23
23
  >>> lookup = KeyLookup()
24
24
  >>> lookup['openai'] = LanguageModelInput(api_token='sk-key123', rpm=60, tpm=100000)
25
25
  >>> openai_config = lookup['openai']
@@ -1,9 +1,25 @@
1
- from typing import Optional, List
1
+ from typing import Optional, TYPE_CHECKING
2
2
  import os
3
3
  from functools import lru_cache
4
+ import textwrap
5
+
6
+ if TYPE_CHECKING:
7
+ from ..coop import Coop
4
8
 
5
9
  from ..enums import service_to_api_keyname
6
- from ..exceptions.general import MissingAPIKeyError
10
+ from ..base import BaseException
11
+
12
+ class MissingAPIKeyError(BaseException):
13
+ def __init__(self, full_message=None, model_name=None, inference_service=None, silent=False):
14
+ if model_name and inference_service:
15
+ full_message = textwrap.dedent(
16
+ f"""
17
+ An API Key for model `{model_name}` is missing from the .env file.
18
+ This key is associated with the inference service `{inference_service}`.
19
+ Please see https://docs.expectedparrot.com/en/latest/api_keys.html for more information.
20
+ """
21
+ )
22
+ super().__init__(full_message, show_docs=False, silent=silent)
7
23
 
8
24
  from .key_lookup import KeyLookup
9
25
  from .models import (
@@ -31,6 +47,8 @@ api_id_to_service = {"AWS_ACCESS_KEY_ID": "bedrock"}
31
47
  class KeyLookupBuilder:
32
48
  """Factory class for building KeyLookup objects by gathering credentials from multiple sources.
33
49
 
50
+ >>> from edsl.key_management.exceptions import KeyManagementValueError
51
+
34
52
  KeyLookupBuilder is responsible for discovering, organizing, and consolidating API keys
35
53
  and rate limits from various sources. It can pull credentials from:
36
54
 
@@ -67,9 +85,9 @@ class KeyLookupBuilder:
67
85
  Validation examples:
68
86
  >>> try:
69
87
  ... KeyLookupBuilder(fetch_order=["config", "env"]) # Should be tuple
70
- ... except ValueError as e:
71
- ... str(e)
72
- 'fetch_order must be a tuple'
88
+ ... except KeyManagementValueError as e:
89
+ ... "fetch_order must be a tuple" in str(e)
90
+ True
73
91
 
74
92
  >>> builder = KeyLookupBuilder()
75
93
  >>> builder.extract_service("EDSL_SERVICE_RPM_OPENAI")
@@ -93,7 +111,8 @@ class KeyLookupBuilder:
93
111
  fetch_order: Optional[tuple[str]] = None,
94
112
  coop: Optional["Coop"] = None,
95
113
  ):
96
- from ..coop import Coop
114
+ # Import here to avoid circular import issues
115
+ from ..coop import Coop # Import Coop type for type hinting
97
116
 
98
117
  # Fetch order goes from lowest priority to highest priority
99
118
  if fetch_order is None:
@@ -102,7 +121,8 @@ class KeyLookupBuilder:
102
121
  self.fetch_order = fetch_order
103
122
 
104
123
  if not isinstance(self.fetch_order, tuple):
105
- raise ValueError("fetch_order must be a tuple")
124
+ from edsl.key_management.exceptions import KeyManagementValueError
125
+ raise KeyManagementValueError("fetch_order must be a tuple")
106
126
 
107
127
  if coop is None:
108
128
  self.coop = Coop()
@@ -141,8 +161,8 @@ class KeyLookupBuilder:
141
161
  >>> lookup = builder.build()
142
162
  >>> isinstance(lookup, KeyLookup)
143
163
  True
144
- >>> lookup['test'].api_token # Test service should always exist
145
- 'test'
164
+ >>> lookup['test'].api_token == 'test' # Test service should always exist
165
+ True
146
166
 
147
167
  Technical Notes:
148
168
  - Skips services with missing API keys
@@ -194,7 +214,7 @@ class KeyLookupBuilder:
194
214
  - Supports services that require both API key and API ID
195
215
  """
196
216
  if (key_entries := self.key_data.get(service)) is None:
197
- raise MissingAPIKeyError(f"No key found for service '{service}'")
217
+ raise MissingAPIKeyError(f"No key found for service '{service}'", silent=True)
198
218
 
199
219
  if len(key_entries) == 1:
200
220
  api_key_entry = key_entries[0]
@@ -286,6 +306,8 @@ class KeyLookupBuilder:
286
306
 
287
307
  def _add_id(self, key: str, value: str, source: str) -> None:
288
308
  """Add an API ID to the id_data dictionary.
309
+
310
+ >>> from edsl.key_management.exceptions import KeyManagementDuplicateError
289
311
 
290
312
  >>> builder = KeyLookupBuilder()
291
313
  >>> builder._add_id("AWS_ACCESS_KEY_ID", "AKIA1234", "env")
@@ -293,9 +315,9 @@ class KeyLookupBuilder:
293
315
  'AKIA1234'
294
316
  >>> try:
295
317
  ... builder._add_id("AWS_ACCESS_KEY_ID", "AKIA5678", "env")
296
- ... except ValueError as e:
297
- ... str(e)
298
- 'Duplicate ID for service bedrock'
318
+ ... except KeyManagementDuplicateError as e:
319
+ ... "Duplicate ID for service bedrock" in str(e)
320
+ True
299
321
  """
300
322
  service = api_id_to_service[key]
301
323
  if service not in self.id_data:
@@ -303,7 +325,8 @@ class KeyLookupBuilder:
303
325
  service=service, name=key, value=value, source=source
304
326
  )
305
327
  else:
306
- raise ValueError(f"Duplicate ID for service {service}")
328
+ from edsl.key_management.exceptions import KeyManagementDuplicateError
329
+ raise KeyManagementDuplicateError(f"Duplicate ID for service {service}")
307
330
 
308
331
  def _add_limit(self, key: str, value: str, source: str) -> None:
309
332
  """Add a rate limit entry to the limit_data dictionary.
@@ -1,5 +1,6 @@
1
1
  from collections import UserDict
2
2
 
3
+ # Import for doctest and type hints
3
4
  from .key_lookup_builder import KeyLookupBuilder
4
5
  from .key_lookup import KeyLookup
5
6
 
@@ -25,6 +26,7 @@ class KeyLookupCollection(UserDict):
25
26
  True
26
27
 
27
28
  Basic usage:
29
+ >>> from edsl.key_management import KeyLookup
28
30
  >>> collection = KeyLookupCollection()
29
31
  >>> collection.add_key_lookup(("config", "env"))
30
32
  >>> lookup = collection[("config", "env")] # Get the stored KeyLookup
@@ -4,4 +4,4 @@ from .model_list import ModelList
4
4
 
5
5
  from .exceptions import LanguageModelBadResponseError
6
6
 
7
- __all__ = ["Model", "ModelList", "LanguageModelBadResponseError"]
7
+ __all__ = ["Model", "ModelList", "LanguageModelBadResponseError", "LanguageModel"]