edsl 0.1.49__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 +120 -29
  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.49.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.49.dist-info/RECORD +0 -347
  238. {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
  239. {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
edsl/plugins/cli.py ADDED
@@ -0,0 +1,372 @@
1
+ """
2
+ Command-line interface for managing EDSL plugins.
3
+
4
+ This module provides a text-based interface for listing, installing,
5
+ updating, and removing EDSL plugins.
6
+ """
7
+
8
+ import sys
9
+ import argparse
10
+ import textwrap
11
+ from typing import Optional, List, Dict, Any, Tuple
12
+ from dataclasses import asdict
13
+ import os
14
+ import re
15
+
16
+ from .plugin_host import PluginHost, get_plugin_manager
17
+ from .exceptions import (
18
+ PluginException,
19
+ PluginNotFoundError,
20
+ PluginInstallationError,
21
+ GitHubRepoError,
22
+ InvalidPluginError
23
+ )
24
+ from .plugins_registry import (
25
+ AvailablePlugin,
26
+ get_available_plugins,
27
+ search_plugins,
28
+ get_plugin_details,
29
+ get_github_url_by_name
30
+ )
31
+
32
+
33
+ class PluginCLI:
34
+ """Command-line interface for managing EDSL plugins."""
35
+
36
+ def __init__(self):
37
+ """Initialize the CLI."""
38
+ self.plugin_manager = get_plugin_manager()
39
+ self.parser = self._create_parser()
40
+
41
+ def _create_parser(self) -> argparse.ArgumentParser:
42
+ """Create the argument parser for the CLI."""
43
+ parser = argparse.ArgumentParser(
44
+ description="EDSL Plugin Manager",
45
+ formatter_class=argparse.RawDescriptionHelpFormatter,
46
+ epilog=textwrap.dedent("""
47
+ Examples:
48
+ edsl plugins list # List all installed plugins
49
+ edsl plugins available # List available plugins from repository
50
+ edsl plugins search text # Search for plugins related to text
51
+ edsl plugins info text_analysis # Get detailed info about a plugin
52
+ edsl plugins install text_analysis # Install a plugin by name
53
+ edsl plugins install text_analysis --url https://github.com/example/repo # Install with explicit URL
54
+ edsl plugins uninstall text_analysis
55
+ """)
56
+ )
57
+
58
+ subparsers = parser.add_subparsers(dest="command", help="Command to execute")
59
+
60
+ # List command
61
+ list_parser = subparsers.add_parser("list", help="List installed plugins")
62
+
63
+ # Available command
64
+ available_parser = subparsers.add_parser("available", help="List available plugins")
65
+
66
+ # Search command
67
+ search_parser = subparsers.add_parser("search", help="Search for plugins")
68
+ search_parser.add_argument("query", help="Search query")
69
+ search_parser.add_argument("--tags", nargs="+", help="Filter by tags")
70
+
71
+ # Info command
72
+ info_parser = subparsers.add_parser("info", help="Get detailed info about a plugin")
73
+ info_parser.add_argument("name", help="Plugin name")
74
+
75
+ # Install command
76
+ install_parser = subparsers.add_parser("install", help="Install a plugin")
77
+ install_parser.add_argument("name", help="Name of the plugin to install")
78
+ install_parser.add_argument("--branch", help="Branch to install from")
79
+ install_parser.add_argument("--url", help="Directly specify GitHub URL instead of using the registry")
80
+
81
+ # Uninstall command
82
+ uninstall_parser = subparsers.add_parser("uninstall", help="Uninstall a plugin")
83
+ uninstall_parser.add_argument("name", help="Plugin name")
84
+
85
+ return parser
86
+
87
+ def run(self, args: Optional[List[str]] = None) -> int:
88
+ """
89
+ Run the CLI with the given arguments.
90
+
91
+ Args:
92
+ args: Command-line arguments (defaults to sys.argv[1:])
93
+
94
+ Returns:
95
+ Exit code (0 for success, non-zero for error)
96
+ """
97
+ if args is None:
98
+ args = sys.argv[1:]
99
+
100
+ # Parse arguments
101
+ args = self.parser.parse_args(args)
102
+
103
+ try:
104
+ # Execute the requested command
105
+ if args.command == "list":
106
+ self._list_plugins()
107
+ elif args.command == "available":
108
+ self._list_available_plugins()
109
+ elif args.command == "search":
110
+ self._search_plugins(args.query, args.tags)
111
+ elif args.command == "info":
112
+ self._show_plugin_info(args.name)
113
+ elif args.command == "install":
114
+ self._install_plugin(args.name, args.branch, args.url)
115
+ elif args.command == "uninstall":
116
+ self._uninstall_plugin(args.name)
117
+ else:
118
+ self.parser.print_help()
119
+ return 1
120
+
121
+ return 0
122
+
123
+ except PluginException as e:
124
+ print(f"Error: {str(e)}")
125
+ return 1
126
+ except Exception as e:
127
+ print(f"Unexpected error: {str(e)}")
128
+ return 1
129
+
130
+ def _list_plugins(self) -> None:
131
+ """List all installed plugins."""
132
+ plugins_info = self.plugin_manager.list_plugins()
133
+
134
+ if not plugins_info:
135
+ print("No plugins installed.")
136
+ return
137
+
138
+ # Print a table of installed plugins
139
+ print("\nInstalled Plugins:")
140
+ print("-" * 80)
141
+ print(f"{'Name':<20} {'Version':<10} {'Description':<50}")
142
+ print("-" * 80)
143
+
144
+ for name, info in plugins_info.items():
145
+ # Truncate description if too long
146
+ description = info.get('description', '')
147
+ if description and len(description) > 47:
148
+ description = description[:47] + "..."
149
+
150
+ print(f"{name:<20} {'unknown':<10} {description:<50}")
151
+
152
+ print("\nUse 'edsl plugins info <name>' for more details about a specific plugin.")
153
+
154
+ def _list_available_plugins(self) -> None:
155
+ """List all available plugins from the repository."""
156
+ try:
157
+ plugins = get_available_plugins()
158
+
159
+ if not plugins:
160
+ print("No plugins available.")
161
+ return
162
+
163
+ # Print a table of available plugins
164
+ print("\nAvailable Plugins:")
165
+ print("-" * 100)
166
+ print(f"{'Name':<20} {'Version':<10} {'Rating':<8} {'Downloads':<10} {'Description':<50}")
167
+ print("-" * 100)
168
+
169
+ for plugin in plugins:
170
+ # Truncate description if too long
171
+ description = plugin.description
172
+ if len(description) > 47:
173
+ description = description[:47] + "..."
174
+
175
+ print(f"{plugin.name:<20} {plugin.version:<10} {plugin.rating:<8.1f} {plugin.downloads:<10} {description:<50}")
176
+
177
+ print("\nUse 'edsl plugins info <name>' for more details about a specific plugin.")
178
+ print("Use 'edsl plugins install <name>' to install a plugin.")
179
+
180
+ except Exception as e:
181
+ print(f"Error fetching available plugins: {str(e)}")
182
+
183
+ def _search_plugins(self, query: str, tags: Optional[List[str]] = None) -> None:
184
+ """
185
+ Search for plugins matching a query string or tags.
186
+
187
+ Args:
188
+ query: Search query string
189
+ tags: Optional list of tags to filter by
190
+ """
191
+ try:
192
+ results = search_plugins(query, tags)
193
+
194
+ if not results:
195
+ print(f"No plugins found matching '{query}'.")
196
+ return
197
+
198
+ # Print a table of search results
199
+ print(f"\nSearch Results for '{query}':")
200
+ if tags:
201
+ print(f"Filtered by tags: {', '.join(tags)}")
202
+
203
+ print("-" * 100)
204
+ print(f"{'Name':<20} {'Version':<10} {'Rating':<8} {'Tags':<25} {'Description':<35}")
205
+ print("-" * 100)
206
+
207
+ for plugin in results:
208
+ # Truncate description if too long
209
+ description = plugin.description
210
+ if len(description) > 32:
211
+ description = description[:32] + "..."
212
+
213
+ # Format tags
214
+ tag_str = ", ".join(plugin.tags[:3])
215
+ if len(plugin.tags) > 3:
216
+ tag_str += "..."
217
+ if len(tag_str) > 22:
218
+ tag_str = tag_str[:22] + "..."
219
+
220
+ print(f"{plugin.name:<20} {plugin.version:<10} {plugin.rating:<8.1f} {tag_str:<25} {description:<35}")
221
+
222
+ print("\nUse 'edsl plugins info <name>' for more details about a specific plugin.")
223
+
224
+ except Exception as e:
225
+ print(f"Error searching for plugins: {str(e)}")
226
+
227
+ def _show_plugin_info(self, name: str) -> None:
228
+ """
229
+ Show detailed information about a specific plugin.
230
+
231
+ Args:
232
+ name: Name of the plugin to show info for
233
+ """
234
+ # First check if it's an installed plugin
235
+ local_plugins = self.plugin_manager.list_plugins()
236
+
237
+ if name in local_plugins:
238
+ info = local_plugins[name]
239
+ print(f"\nPlugin: {name} (Installed)")
240
+ print("-" * 80)
241
+ print(f"Description: {info.get('description', 'No description available')}")
242
+ print(f"Methods: {', '.join(info.get('methods', []))}")
243
+ print(f"Installed from: {info.get('installed_from', 'Unknown')}")
244
+ print("\nUse 'edsl plugins uninstall {name}' to uninstall this plugin.")
245
+ return
246
+
247
+ # If not installed, check if it's available
248
+ try:
249
+ info = get_plugin_details(name)
250
+
251
+ if not info:
252
+ print(f"Plugin '{name}' not found.")
253
+ return
254
+
255
+ print(f"\nPlugin: {info['name']}")
256
+ print("-" * 80)
257
+ print(f"Description: {info['description']}")
258
+ print(f"Version: {info['version']}")
259
+ print(f"Author: {info['author']}")
260
+ print(f"Tags: {', '.join(info['tags'])}")
261
+ print(f"GitHub URL: {info['github_url']}")
262
+ print(f"Created: {info['created_at']}")
263
+ print(f"Last Updated: {info.get('last_update', 'Unknown')}")
264
+ print(f"Downloads: {info['downloads']}")
265
+ print(f"Rating: {info['rating']:.1f}/5.0")
266
+ print(f"License: {info.get('license', 'Unknown')}")
267
+ print(f"Dependencies: {', '.join(info.get('dependencies', ['None']))}")
268
+ print(f"Compatible EDSL versions: {', '.join(info.get('compatible_edsl_versions', ['Unknown']))}")
269
+ print("\nDocumentation:")
270
+ print(f" Homepage: {info.get('homepage', 'N/A')}")
271
+ print(f" Docs: {info.get('documentation', 'N/A')}")
272
+
273
+ if 'examples' in info and info['examples']:
274
+ print("\nExamples:")
275
+ for example in info['examples']:
276
+ print(f" - {example}")
277
+
278
+ print(f"\nUse 'edsl plugins install {info['github_url']}' to install this plugin.")
279
+
280
+ except Exception as e:
281
+ print(f"Error retrieving plugin information: {str(e)}")
282
+
283
+ def _install_plugin(self, plugin_name: str, branch: Optional[str] = None, url: Optional[str] = None) -> None:
284
+ """
285
+ Install a plugin by name or URL.
286
+
287
+ Args:
288
+ plugin_name: Name of the plugin to install
289
+ branch: Optional branch to checkout (defaults to main/master)
290
+ url: Optional GitHub URL to use instead of registry lookup
291
+ """
292
+ try:
293
+ github_url = url
294
+
295
+ # If URL not provided, look up in registry by name
296
+ if not github_url:
297
+ github_url = get_github_url_by_name(plugin_name)
298
+ if not github_url:
299
+ print(f"Plugin '{plugin_name}' not found in registry.")
300
+ print("Use 'edsl plugins available' to see available plugins.")
301
+ print("Or provide the GitHub URL with '--url' if you know it.")
302
+ return
303
+
304
+ print(f"Installing plugin '{plugin_name}' from {github_url}...")
305
+ if branch:
306
+ print(f"Using branch: {branch}")
307
+
308
+ # Install the plugin
309
+ installed_plugins = PluginHost.install_from_github(github_url, branch)
310
+
311
+ print(f"Successfully installed plugin(s): {', '.join(installed_plugins)}")
312
+ print("\nUse 'edsl plugins list' to see all installed plugins.")
313
+
314
+ except PluginException as e:
315
+ print(f"Error installing plugin: {str(e)}")
316
+ raise
317
+ except Exception as e:
318
+ print(f"Unexpected error during installation: {str(e)}")
319
+ raise
320
+
321
+ def _uninstall_plugin(self, name: str) -> None:
322
+ """
323
+ Uninstall a plugin by name.
324
+
325
+ Args:
326
+ name: Name of the plugin to uninstall
327
+ """
328
+ try:
329
+ print(f"Uninstalling plugin '{name}'...")
330
+
331
+ # Uninstall the plugin
332
+ PluginHost.uninstall_plugin(name)
333
+
334
+ print(f"Successfully uninstalled plugin '{name}'.")
335
+
336
+ except PluginException as e:
337
+ print(f"Error uninstalling plugin: {str(e)}")
338
+ raise
339
+ except Exception as e:
340
+ print(f"Unexpected error during uninstallation: {str(e)}")
341
+ raise
342
+
343
+ def _get_plugin_name_from_url(self, url: str) -> str:
344
+ """
345
+ Extract a plugin name from a GitHub URL.
346
+
347
+ Args:
348
+ url: GitHub URL
349
+
350
+ Returns:
351
+ Extracted plugin name or a placeholder
352
+ """
353
+ # Try to extract the repository name from the URL
354
+ match = re.search(r"github\.com/[^/]+/([^/]+)", url)
355
+ if match:
356
+ repo_name = match.group(1)
357
+ # Convert repo name to plugin name
358
+ if repo_name.startswith("plugin-"):
359
+ return repo_name[7:] # Remove "plugin-" prefix
360
+ elif repo_name.startswith("edsl-plugin-"):
361
+ return repo_name[12:] # Remove "edsl-plugin-" prefix
362
+ else:
363
+ return repo_name
364
+
365
+ # If we can't extract a name, return a placeholder
366
+ return "plugin"
367
+
368
+
369
+ def main():
370
+ """Main entry point for the CLI."""
371
+ cli = PluginCLI()
372
+ sys.exit(cli.run())
@@ -0,0 +1,283 @@
1
+ """
2
+ Command-line interface for managing EDSL plugins using Typer.
3
+
4
+ This module provides a Typer-based CLI for listing, installing,
5
+ updating, and removing EDSL plugins.
6
+ """
7
+
8
+ import sys
9
+ from typing import Optional, List, Dict, Any
10
+ import re
11
+ from pathlib import Path
12
+
13
+ import typer
14
+ from rich.console import Console
15
+ from rich.table import Table
16
+ from rich.panel import Panel
17
+ from rich.text import Text
18
+ from rich import box
19
+
20
+ from .plugin_host import PluginHost, get_plugin_manager
21
+ from .exceptions import (
22
+ PluginException,
23
+ PluginNotFoundError,
24
+ PluginInstallationError,
25
+ GitHubRepoError,
26
+ InvalidPluginError
27
+ )
28
+ from .plugins_registry import (
29
+ AvailablePlugin,
30
+ get_available_plugins,
31
+ search_plugins,
32
+ get_plugin_details,
33
+ get_github_url_by_name
34
+ )
35
+
36
+ # Create the Typer app
37
+ app = typer.Typer(help="Manage EDSL plugins")
38
+ console = Console()
39
+
40
+ @app.command("list")
41
+ def list_plugins():
42
+ """List all installed plugins."""
43
+ plugin_manager = get_plugin_manager()
44
+ plugins_info = plugin_manager.list_plugins()
45
+
46
+ if not plugins_info:
47
+ console.print("[yellow]No plugins installed.[/yellow]")
48
+ return
49
+
50
+ # Create a rich table
51
+ table = Table(title="Installed Plugins", box=box.ROUNDED)
52
+ table.add_column("Name", style="cyan")
53
+ table.add_column("Version", style="green")
54
+ table.add_column("Description")
55
+
56
+ for name, info in plugins_info.items():
57
+ description = info.get('description', '')
58
+ table.add_row(
59
+ name,
60
+ info.get('version', 'unknown'),
61
+ description
62
+ )
63
+
64
+ console.print(table)
65
+ console.print("\nUse [bold cyan]edsl plugins info <name>[/bold cyan] for more details about a specific plugin.")
66
+
67
+ @app.command("available")
68
+ def list_available_plugins():
69
+ """List all available plugins from the repository."""
70
+ try:
71
+ plugins = get_available_plugins()
72
+
73
+ if not plugins:
74
+ console.print("[yellow]No plugins available.[/yellow]")
75
+ return
76
+
77
+ # Create a rich table
78
+ table = Table(title="Available Plugins", box=box.ROUNDED)
79
+ table.add_column("Name", style="cyan")
80
+ table.add_column("Version", style="green")
81
+ table.add_column("Description")
82
+
83
+ for plugin in plugins:
84
+ table.add_row(
85
+ plugin.name,
86
+ plugin.version,
87
+ plugin.description
88
+ )
89
+
90
+ console.print(table)
91
+ console.print("\nUse [bold cyan]edsl plugins info <name>[/bold cyan] for more details about a specific plugin.")
92
+ console.print("Use [bold cyan]edsl plugins install <name>[/bold cyan] to install a plugin.")
93
+
94
+ except Exception as e:
95
+ console.print(f"[red]Error fetching available plugins: {str(e)}[/red]")
96
+
97
+ @app.command("search")
98
+ def search_for_plugins(
99
+ query: str = typer.Argument(..., help="Search query string"),
100
+ tags: Optional[List[str]] = typer.Option(None, help="Filter by tags")
101
+ ):
102
+ """Search for plugins matching a query string or tags."""
103
+ try:
104
+ results = search_plugins(query, tags)
105
+
106
+ if not results:
107
+ console.print(f"[yellow]No plugins found matching '{query}'.[/yellow]")
108
+ return
109
+
110
+ # Create a rich table
111
+ title = f"Search Results for '{query}'"
112
+ if tags:
113
+ title += f" (Tags: {', '.join(tags)})"
114
+
115
+ table = Table(title=title, box=box.ROUNDED)
116
+ table.add_column("Name", style="cyan")
117
+ table.add_column("Version", style="green")
118
+ table.add_column("Tags")
119
+ table.add_column("Description")
120
+
121
+ for plugin in results:
122
+ # Format tags
123
+ tag_str = ", ".join(plugin.tags[:3])
124
+ if len(plugin.tags) > 3:
125
+ tag_str += "..."
126
+
127
+ table.add_row(
128
+ plugin.name,
129
+ plugin.version,
130
+ tag_str,
131
+ plugin.description
132
+ )
133
+
134
+ console.print(table)
135
+ console.print("\nUse [bold cyan]edsl plugins info <name>[/bold cyan] for more details about a specific plugin.")
136
+
137
+ except Exception as e:
138
+ console.print(f"[red]Error searching for plugins: {str(e)}[/red]")
139
+
140
+ @app.command("info")
141
+ def show_plugin_info(name: str = typer.Argument(..., help="Plugin name")):
142
+ """Get detailed information about a specific plugin."""
143
+ plugin_manager = get_plugin_manager()
144
+ local_plugins = plugin_manager.list_plugins()
145
+
146
+ if name in local_plugins:
147
+ info = local_plugins[name]
148
+
149
+ panel_content = []
150
+ panel_content.append(f"[bold cyan]Description:[/bold cyan] {info.get('description', 'No description available')}")
151
+ panel_content.append(f"[bold cyan]Methods:[/bold cyan] {', '.join(info.get('methods', []))}")
152
+ panel_content.append(f"[bold cyan]Installed from:[/bold cyan] {info.get('installed_from', 'Unknown')}")
153
+
154
+ panel = Panel(
155
+ "\n".join(panel_content),
156
+ title=f"[bold]Plugin: {name} (Installed)[/bold]",
157
+ expand=False
158
+ )
159
+ console.print(panel)
160
+ console.print(f"\nUse [bold cyan]edsl plugins uninstall {name}[/bold cyan] to uninstall this plugin.")
161
+ return
162
+
163
+ # If not installed, check if it's available
164
+ try:
165
+ info = get_plugin_details(name)
166
+
167
+ if not info:
168
+ console.print(f"[yellow]Plugin '{name}' not found.[/yellow]")
169
+ return
170
+
171
+ # Create a rich panel with detailed information
172
+ panel_content = []
173
+ panel_content.append(f"[bold cyan]Description:[/bold cyan] {info['description']}")
174
+ panel_content.append(f"[bold cyan]Version:[/bold cyan] {info['version']}")
175
+ panel_content.append(f"[bold cyan]Author:[/bold cyan] {info['author']}")
176
+ panel_content.append(f"[bold cyan]Tags:[/bold cyan] {', '.join(info['tags'])}")
177
+ panel_content.append(f"[bold cyan]GitHub URL:[/bold cyan] {info['github_url']}")
178
+ panel_content.append(f"[bold cyan]Created:[/bold cyan] {info['created_at']}")
179
+ panel_content.append(f"[bold cyan]Last Updated:[/bold cyan] {info.get('last_update', 'Unknown')}")
180
+ panel_content.append(f"[bold cyan]License:[/bold cyan] {info.get('license', 'Unknown')}")
181
+ panel_content.append(f"[bold cyan]Dependencies:[/bold cyan] {', '.join(info.get('dependencies', ['None']))}")
182
+ panel_content.append(f"[bold cyan]Compatible EDSL versions:[/bold cyan] {', '.join(info.get('compatible_edsl_versions', ['Unknown']))}")
183
+
184
+ panel_content.append("\n[bold cyan]Documentation:[/bold cyan]")
185
+ panel_content.append(f" Homepage: {info.get('homepage', 'N/A')}")
186
+ panel_content.append(f" Docs: {info.get('documentation', 'N/A')}")
187
+
188
+ if 'examples' in info and info['examples']:
189
+ panel_content.append("\n[bold cyan]Examples:[/bold cyan]")
190
+ for example in info['examples']:
191
+ panel_content.append(f" • {example}")
192
+
193
+ panel = Panel(
194
+ "\n".join(panel_content),
195
+ title=f"[bold]Plugin: {info['name']}[/bold]",
196
+ expand=False
197
+ )
198
+ console.print(panel)
199
+ console.print(f"\nUse [bold cyan]edsl plugins install {info['name']}[/bold cyan] to install this plugin.")
200
+
201
+ except Exception as e:
202
+ console.print(f"[red]Error retrieving plugin information: {str(e)}[/red]")
203
+
204
+ @app.command("install")
205
+ def install_plugin(
206
+ name: str = typer.Argument(..., help="Name of the plugin to install"),
207
+ branch: Optional[str] = typer.Option(None, help="Branch to install from"),
208
+ url: Optional[str] = typer.Option(None, help="Directly specify GitHub URL instead of using the registry")
209
+ ):
210
+ """Install a plugin by name or URL."""
211
+ try:
212
+ github_url = url
213
+
214
+ # If URL not provided, look up in registry by name
215
+ if not github_url:
216
+ github_url = get_github_url_by_name(name)
217
+ if not github_url:
218
+ console.print(f"[yellow]Plugin '{name}' not found in registry.[/yellow]")
219
+ console.print("Use [bold cyan]edsl plugins available[/bold cyan] to see available plugins.")
220
+ console.print("Or provide the GitHub URL with '--url' if you know it.")
221
+ raise typer.Exit(code=1)
222
+
223
+ with console.status(f"Installing plugin '{name}' from {github_url}...", spinner="dots"):
224
+ # Install the plugin
225
+ installed_plugins = PluginHost.install_from_github(github_url, branch)
226
+
227
+ console.print(f"[green]Successfully installed plugin(s): {', '.join(installed_plugins)}[/green]")
228
+ console.print("\nUse [bold cyan]edsl plugins list[/bold cyan] to see all installed plugins.")
229
+
230
+ except PluginException as e:
231
+ console.print(f"[red]Error installing plugin: {str(e)}[/red]")
232
+ raise typer.Exit(code=1)
233
+ except Exception as e:
234
+ console.print(f"[red]Unexpected error during installation: {str(e)}[/red]")
235
+ raise typer.Exit(code=1)
236
+
237
+ @app.command("uninstall")
238
+ def uninstall_plugin(
239
+ name: str = typer.Argument(..., help="Plugin name")
240
+ ):
241
+ """Uninstall a plugin by name."""
242
+ try:
243
+ with console.status(f"Uninstalling plugin '{name}'...", spinner="dots"):
244
+ # Uninstall the plugin
245
+ PluginHost.uninstall_plugin(name)
246
+
247
+ console.print(f"[green]Successfully uninstalled plugin '{name}'.[/green]")
248
+
249
+ except PluginException as e:
250
+ console.print(f"[red]Error uninstalling plugin: {str(e)}[/red]")
251
+ raise typer.Exit(code=1)
252
+ except Exception as e:
253
+ console.print(f"[red]Unexpected error during uninstallation: {str(e)}[/red]")
254
+ raise typer.Exit(code=1)
255
+
256
+ def _get_plugin_name_from_url(url: str) -> str:
257
+ """
258
+ Extract a plugin name from a GitHub URL.
259
+
260
+ Args:
261
+ url: GitHub URL
262
+
263
+ Returns:
264
+ Extracted plugin name or a placeholder
265
+ """
266
+ # Try to extract the repository name from the URL
267
+ match = re.search(r"github\.com/[^/]+/([^/]+)", url)
268
+ if match:
269
+ repo_name = match.group(1)
270
+ # Convert repo name to plugin name
271
+ if repo_name.startswith("plugin-"):
272
+ return repo_name[7:] # Remove "plugin-" prefix
273
+ elif repo_name.startswith("edsl-plugin-"):
274
+ return repo_name[12:] # Remove "edsl-plugin-" prefix
275
+ else:
276
+ return repo_name
277
+
278
+ # If we can't extract a name, return a placeholder
279
+ return "plugin"
280
+
281
+ def run():
282
+ """Run the plugin CLI."""
283
+ app()
@@ -0,0 +1,31 @@
1
+ """Exceptions for the plugins module."""
2
+
3
+ from ..base.base_exception import BaseException
4
+
5
+ class PluginException(BaseException):
6
+ """Base exception for all plugin-related errors."""
7
+ relevant_doc = "https://docs.expectedparrot.com/plugins.html"
8
+
9
+ class PluginNotFoundError(PluginException):
10
+ """Raised when a requested plugin is not found."""
11
+ pass
12
+
13
+ class PluginInstallationError(PluginException):
14
+ """Raised when a plugin installation fails."""
15
+ pass
16
+
17
+ class GitHubRepoError(PluginInstallationError):
18
+ """Raised when there's an error with a GitHub repository."""
19
+ pass
20
+
21
+ class InvalidPluginError(PluginException):
22
+ """Raised when a plugin is invalid or does not implement required hooks."""
23
+ pass
24
+
25
+ class PluginDependencyError(PluginInstallationError):
26
+ """Raised when plugin dependencies cannot be satisfied."""
27
+ pass
28
+
29
+ class PluginMethodError(PluginException):
30
+ """Raised when there is an error with a plugin method."""
31
+ pass