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.
- edsl/__init__.py +124 -53
- edsl/__version__.py +1 -1
- edsl/agents/agent.py +21 -21
- edsl/agents/agent_list.py +2 -5
- edsl/agents/exceptions.py +119 -5
- edsl/base/__init__.py +10 -35
- edsl/base/base_class.py +71 -36
- edsl/base/base_exception.py +204 -0
- edsl/base/data_transfer_models.py +1 -1
- edsl/base/exceptions.py +94 -0
- edsl/buckets/__init__.py +15 -1
- edsl/buckets/bucket_collection.py +3 -4
- edsl/buckets/exceptions.py +75 -0
- edsl/buckets/model_buckets.py +1 -2
- edsl/buckets/token_bucket.py +11 -6
- edsl/buckets/token_bucket_api.py +1 -2
- edsl/buckets/token_bucket_client.py +9 -7
- edsl/caching/cache.py +7 -2
- edsl/caching/cache_entry.py +10 -9
- edsl/caching/exceptions.py +113 -7
- edsl/caching/remote_cache_sync.py +1 -2
- edsl/caching/sql_dict.py +17 -12
- edsl/cli.py +43 -0
- edsl/config/config_class.py +30 -6
- edsl/conversation/Conversation.py +3 -2
- edsl/conversation/exceptions.py +58 -0
- edsl/conversation/mug_negotiation.py +0 -2
- edsl/coop/__init__.py +20 -1
- edsl/coop/coop.py +120 -29
- edsl/coop/exceptions.py +188 -9
- edsl/coop/price_fetcher.py +3 -6
- edsl/coop/utils.py +4 -6
- edsl/dataset/__init__.py +5 -4
- edsl/dataset/dataset.py +53 -43
- edsl/dataset/dataset_operations_mixin.py +86 -72
- edsl/dataset/dataset_tree.py +9 -5
- edsl/dataset/display/table_display.py +0 -2
- edsl/dataset/display/table_renderers.py +0 -1
- edsl/dataset/exceptions.py +125 -0
- edsl/dataset/file_exports.py +18 -11
- edsl/dataset/r/ggplot.py +13 -6
- edsl/display/__init__.py +27 -0
- edsl/display/core.py +147 -0
- edsl/display/plugin.py +189 -0
- edsl/display/utils.py +52 -0
- edsl/inference_services/__init__.py +9 -1
- edsl/inference_services/available_model_cache_handler.py +1 -1
- edsl/inference_services/available_model_fetcher.py +4 -5
- edsl/inference_services/data_structures.py +9 -6
- edsl/inference_services/exceptions.py +132 -1
- edsl/inference_services/inference_service_abc.py +2 -2
- edsl/inference_services/inference_services_collection.py +2 -6
- edsl/inference_services/registry.py +4 -3
- edsl/inference_services/service_availability.py +2 -1
- edsl/inference_services/services/anthropic_service.py +4 -1
- edsl/inference_services/services/aws_bedrock.py +13 -12
- edsl/inference_services/services/azure_ai.py +12 -10
- edsl/inference_services/services/deep_infra_service.py +1 -4
- edsl/inference_services/services/deep_seek_service.py +1 -5
- edsl/inference_services/services/google_service.py +6 -2
- edsl/inference_services/services/groq_service.py +1 -1
- edsl/inference_services/services/mistral_ai_service.py +4 -2
- edsl/inference_services/services/ollama_service.py +1 -1
- edsl/inference_services/services/open_ai_service.py +7 -5
- edsl/inference_services/services/perplexity_service.py +6 -2
- edsl/inference_services/services/test_service.py +8 -7
- edsl/inference_services/services/together_ai_service.py +2 -3
- edsl/inference_services/services/xai_service.py +1 -1
- edsl/instructions/__init__.py +1 -1
- edsl/instructions/change_instruction.py +3 -2
- edsl/instructions/exceptions.py +61 -0
- edsl/instructions/instruction.py +5 -2
- edsl/instructions/instruction_collection.py +2 -1
- edsl/instructions/instruction_handler.py +4 -9
- edsl/interviews/ReportErrors.py +0 -3
- edsl/interviews/__init__.py +9 -2
- edsl/interviews/answering_function.py +11 -13
- edsl/interviews/exception_tracking.py +14 -7
- edsl/interviews/exceptions.py +79 -0
- edsl/interviews/interview.py +32 -29
- edsl/interviews/interview_status_dictionary.py +4 -2
- edsl/interviews/interview_status_log.py +2 -1
- edsl/interviews/interview_task_manager.py +3 -3
- edsl/interviews/request_token_estimator.py +3 -1
- edsl/interviews/statistics.py +2 -3
- edsl/invigilators/__init__.py +7 -1
- edsl/invigilators/exceptions.py +79 -0
- edsl/invigilators/invigilator_base.py +0 -1
- edsl/invigilators/invigilators.py +8 -12
- edsl/invigilators/prompt_constructor.py +1 -5
- edsl/invigilators/prompt_helpers.py +8 -4
- edsl/invigilators/question_instructions_prompt_builder.py +1 -1
- edsl/invigilators/question_option_processor.py +9 -5
- edsl/invigilators/question_template_replacements_builder.py +3 -2
- edsl/jobs/__init__.py +3 -3
- edsl/jobs/async_interview_runner.py +24 -22
- edsl/jobs/check_survey_scenario_compatibility.py +7 -6
- edsl/jobs/data_structures.py +7 -4
- edsl/jobs/exceptions.py +177 -8
- edsl/jobs/fetch_invigilator.py +1 -1
- edsl/jobs/jobs.py +72 -67
- edsl/jobs/jobs_checks.py +2 -3
- edsl/jobs/jobs_component_constructor.py +2 -2
- edsl/jobs/jobs_pricing_estimation.py +3 -2
- edsl/jobs/jobs_remote_inference_logger.py +5 -4
- edsl/jobs/jobs_runner_asyncio.py +1 -2
- edsl/jobs/jobs_runner_status.py +8 -9
- edsl/jobs/remote_inference.py +26 -23
- edsl/jobs/results_exceptions_handler.py +8 -5
- edsl/key_management/__init__.py +3 -1
- edsl/key_management/exceptions.py +62 -0
- edsl/key_management/key_lookup.py +1 -1
- edsl/key_management/key_lookup_builder.py +37 -14
- edsl/key_management/key_lookup_collection.py +2 -0
- edsl/language_models/__init__.py +1 -1
- edsl/language_models/exceptions.py +302 -14
- edsl/language_models/language_model.py +4 -7
- edsl/language_models/model.py +4 -4
- edsl/language_models/model_list.py +1 -1
- edsl/language_models/price_manager.py +1 -1
- edsl/language_models/raw_response_handler.py +14 -9
- edsl/language_models/registry.py +17 -21
- edsl/language_models/repair.py +0 -6
- edsl/language_models/unused/fake_openai_service.py +0 -1
- edsl/load_plugins.py +69 -0
- edsl/logger.py +146 -0
- edsl/notebooks/notebook.py +1 -1
- edsl/notebooks/notebook_to_latex.py +0 -1
- edsl/plugins/__init__.py +63 -0
- edsl/plugins/built_in/export_example.py +50 -0
- edsl/plugins/built_in/pig_latin.py +67 -0
- edsl/plugins/cli.py +372 -0
- edsl/plugins/cli_typer.py +283 -0
- edsl/plugins/exceptions.py +31 -0
- edsl/plugins/hookspec.py +51 -0
- edsl/plugins/plugin_host.py +128 -0
- edsl/plugins/plugin_manager.py +633 -0
- edsl/plugins/plugins_registry.py +168 -0
- edsl/prompts/__init__.py +2 -0
- edsl/prompts/exceptions.py +107 -5
- edsl/prompts/prompt.py +14 -6
- edsl/questions/HTMLQuestion.py +5 -11
- edsl/questions/Quick.py +0 -1
- edsl/questions/__init__.py +2 -0
- edsl/questions/answer_validator_mixin.py +318 -318
- edsl/questions/compose_questions.py +2 -2
- edsl/questions/descriptors.py +10 -49
- edsl/questions/exceptions.py +278 -22
- edsl/questions/loop_processor.py +7 -5
- edsl/questions/prompt_templates/question_list.jinja +3 -0
- edsl/questions/question_base.py +14 -16
- edsl/questions/question_base_gen_mixin.py +2 -2
- edsl/questions/question_base_prompts_mixin.py +9 -3
- edsl/questions/question_budget.py +9 -5
- edsl/questions/question_check_box.py +3 -5
- edsl/questions/question_dict.py +171 -194
- edsl/questions/question_extract.py +1 -1
- edsl/questions/question_free_text.py +4 -6
- edsl/questions/question_functional.py +4 -3
- edsl/questions/question_list.py +36 -9
- edsl/questions/question_matrix.py +95 -61
- edsl/questions/question_multiple_choice.py +6 -4
- edsl/questions/question_numerical.py +2 -4
- edsl/questions/question_registry.py +4 -2
- edsl/questions/register_questions_meta.py +0 -1
- edsl/questions/response_validator_abc.py +7 -13
- edsl/questions/templates/dict/answering_instructions.jinja +1 -0
- edsl/questions/templates/rank/question_presentation.jinja +1 -1
- edsl/results/__init__.py +1 -1
- edsl/results/exceptions.py +141 -7
- edsl/results/report.py +0 -1
- edsl/results/result.py +4 -5
- edsl/results/results.py +10 -51
- edsl/results/results_selector.py +8 -4
- edsl/scenarios/PdfExtractor.py +2 -2
- edsl/scenarios/construct_download_link.py +69 -35
- edsl/scenarios/directory_scanner.py +33 -14
- edsl/scenarios/document_chunker.py +1 -1
- edsl/scenarios/exceptions.py +238 -14
- edsl/scenarios/file_methods.py +1 -1
- edsl/scenarios/file_store.py +7 -3
- edsl/scenarios/handlers/__init__.py +17 -0
- edsl/scenarios/handlers/docx_file_store.py +0 -5
- edsl/scenarios/handlers/pdf_file_store.py +0 -1
- edsl/scenarios/handlers/pptx_file_store.py +0 -5
- edsl/scenarios/handlers/py_file_store.py +0 -1
- edsl/scenarios/handlers/sql_file_store.py +1 -4
- edsl/scenarios/handlers/sqlite_file_store.py +0 -1
- edsl/scenarios/handlers/txt_file_store.py +1 -1
- edsl/scenarios/scenario.py +0 -1
- edsl/scenarios/scenario_list.py +152 -18
- edsl/scenarios/scenario_list_pdf_tools.py +1 -0
- edsl/scenarios/scenario_selector.py +0 -1
- edsl/surveys/__init__.py +3 -4
- edsl/surveys/dag/__init__.py +4 -2
- edsl/surveys/descriptors.py +1 -1
- edsl/surveys/edit_survey.py +1 -0
- edsl/surveys/exceptions.py +165 -9
- edsl/surveys/memory/__init__.py +5 -3
- edsl/surveys/memory/memory_management.py +1 -0
- edsl/surveys/memory/memory_plan.py +6 -15
- edsl/surveys/rules/__init__.py +5 -3
- edsl/surveys/rules/rule.py +1 -2
- edsl/surveys/rules/rule_collection.py +1 -1
- edsl/surveys/survey.py +12 -24
- edsl/surveys/survey_export.py +6 -3
- edsl/surveys/survey_flow_visualization.py +10 -1
- edsl/tasks/__init__.py +2 -0
- edsl/tasks/question_task_creator.py +3 -3
- edsl/tasks/task_creators.py +1 -3
- edsl/tasks/task_history.py +5 -7
- edsl/tasks/task_status_log.py +1 -2
- edsl/tokens/__init__.py +3 -1
- edsl/tokens/token_usage.py +1 -1
- edsl/utilities/__init__.py +21 -1
- edsl/utilities/decorators.py +1 -2
- edsl/utilities/markdown_to_docx.py +2 -2
- edsl/utilities/markdown_to_pdf.py +1 -1
- edsl/utilities/repair_functions.py +0 -1
- edsl/utilities/restricted_python.py +0 -1
- edsl/utilities/template_loader.py +2 -3
- edsl/utilities/utilities.py +8 -29
- {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/METADATA +32 -2
- edsl-0.1.50.dist-info/RECORD +363 -0
- edsl-0.1.50.dist-info/entry_points.txt +3 -0
- edsl/dataset/smart_objects.py +0 -96
- edsl/exceptions/BaseException.py +0 -21
- edsl/exceptions/__init__.py +0 -54
- edsl/exceptions/configuration.py +0 -16
- edsl/exceptions/general.py +0 -34
- edsl/study/ObjectEntry.py +0 -173
- edsl/study/ProofOfWork.py +0 -113
- edsl/study/SnapShot.py +0 -80
- edsl/study/Study.py +0 -520
- edsl/study/__init__.py +0 -6
- edsl/utilities/interface.py +0 -135
- edsl-0.1.49.dist-info/RECORD +0 -347
- {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/LICENSE +0 -0
- {edsl-0.1.49.dist-info → edsl-0.1.50.dist-info}/WHEEL +0 -0
@@ -0,0 +1,633 @@
|
|
1
|
+
# edsl/plugins/plugin_manager.py
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import subprocess
|
5
|
+
import tempfile
|
6
|
+
import importlib
|
7
|
+
import importlib.util
|
8
|
+
import shutil
|
9
|
+
import re
|
10
|
+
import json
|
11
|
+
from typing import Optional, List, Dict, Any
|
12
|
+
import pluggy
|
13
|
+
import platformdirs
|
14
|
+
from urllib.parse import urlparse
|
15
|
+
|
16
|
+
from .hookspec import EDSLPluginSpec
|
17
|
+
from .exceptions import (
|
18
|
+
PluginException,
|
19
|
+
PluginNotFoundError,
|
20
|
+
PluginInstallationError,
|
21
|
+
GitHubRepoError,
|
22
|
+
InvalidPluginError,
|
23
|
+
PluginDependencyError,
|
24
|
+
PluginMethodError
|
25
|
+
)
|
26
|
+
from .. import logger
|
27
|
+
|
28
|
+
class EDSLPluginManager:
|
29
|
+
"""Manage EDSL plugins using pluggy."""
|
30
|
+
|
31
|
+
# Define paths for persistent data
|
32
|
+
PLUGINS_DATA_DIR = platformdirs.user_data_dir("edsl")
|
33
|
+
PLUGINS_DATA_FILE = os.path.join(PLUGINS_DATA_DIR, "installed_plugins.json")
|
34
|
+
|
35
|
+
# Ensure the directory exists
|
36
|
+
os.makedirs(PLUGINS_DATA_DIR, exist_ok=True)
|
37
|
+
|
38
|
+
def __init__(self):
|
39
|
+
# Create a plugin manager for the "edsl" project
|
40
|
+
self.manager = pluggy.PluginManager("edsl")
|
41
|
+
# Register the hook specifications
|
42
|
+
self.manager.add_hookspecs(EDSLPluginSpec)
|
43
|
+
# Load all plugins that are installed
|
44
|
+
self.manager.load_setuptools_entrypoints("edsl_plugins")
|
45
|
+
# Dictionary to store plugin methods
|
46
|
+
self.methods = {}
|
47
|
+
# Dictionary to track installed plugins
|
48
|
+
self.installed_plugins = self._load_installed_plugins()
|
49
|
+
# Dictionary to store objects exported to the global namespace
|
50
|
+
self.exports = {}
|
51
|
+
# Register built-in plugins
|
52
|
+
self._register_builtin_plugins()
|
53
|
+
# Discover and register methods
|
54
|
+
self._discover_methods()
|
55
|
+
# Gather exports from plugins
|
56
|
+
self._gather_exports()
|
57
|
+
|
58
|
+
def _load_installed_plugins(self) -> Dict[str, str]:
|
59
|
+
"""
|
60
|
+
Load the list of installed plugins from the data file.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
Dictionary mapping plugin names to installation directories
|
64
|
+
"""
|
65
|
+
# Create the directory if it doesn't exist
|
66
|
+
os.makedirs(os.path.dirname(self.PLUGINS_DATA_FILE), exist_ok=True)
|
67
|
+
|
68
|
+
# Load the plugins data if it exists
|
69
|
+
if os.path.exists(self.PLUGINS_DATA_FILE):
|
70
|
+
try:
|
71
|
+
with open(self.PLUGINS_DATA_FILE, 'r') as f:
|
72
|
+
return json.load(f)
|
73
|
+
except (json.JSONDecodeError, IOError) as e:
|
74
|
+
logger.warning(f"Failed to load installed plugins data: {str(e)}")
|
75
|
+
|
76
|
+
return {}
|
77
|
+
|
78
|
+
def _save_installed_plugins(self):
|
79
|
+
"""
|
80
|
+
Save the list of installed plugins to the data file.
|
81
|
+
"""
|
82
|
+
try:
|
83
|
+
with open(self.PLUGINS_DATA_FILE, 'w') as f:
|
84
|
+
json.dump(self.installed_plugins, f)
|
85
|
+
except IOError as e:
|
86
|
+
logger.warning(f"Failed to save installed plugins data: {str(e)}")
|
87
|
+
|
88
|
+
def install_plugin_from_github(self, github_url: str, branch: Optional[str] = None) -> List[str]:
|
89
|
+
"""
|
90
|
+
Install a plugin from a GitHub repository.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
github_url: URL to the GitHub repository
|
94
|
+
branch: Optional branch to checkout (defaults to main/master)
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
List of installed plugin names
|
98
|
+
|
99
|
+
Raises:
|
100
|
+
GitHubRepoError: If the URL is invalid or the repository cannot be accessed
|
101
|
+
PluginInstallationError: If the installation fails
|
102
|
+
InvalidPluginError: If the repository does not contain valid plugins
|
103
|
+
"""
|
104
|
+
# Validate GitHub URL
|
105
|
+
self._validate_github_url(github_url)
|
106
|
+
|
107
|
+
# Extract plugin name from URL
|
108
|
+
repo_name = github_url.split('/')[-1]
|
109
|
+
if repo_name.endswith('.git'):
|
110
|
+
repo_name = repo_name[:-4]
|
111
|
+
|
112
|
+
# Create a persistent directory for the plugin
|
113
|
+
plugins_install_dir = os.path.join(self.PLUGINS_DATA_DIR, "plugins")
|
114
|
+
os.makedirs(plugins_install_dir, exist_ok=True)
|
115
|
+
plugin_dir = os.path.join(plugins_install_dir, repo_name)
|
116
|
+
|
117
|
+
# Remove existing installation if it exists
|
118
|
+
if os.path.exists(plugin_dir):
|
119
|
+
shutil.rmtree(plugin_dir)
|
120
|
+
|
121
|
+
try:
|
122
|
+
# Clone the repository to the persistent directory
|
123
|
+
self._clone_repository(github_url, plugin_dir, branch)
|
124
|
+
|
125
|
+
# Check for setup.py or pyproject.toml
|
126
|
+
if not self._has_package_files(plugin_dir):
|
127
|
+
raise InvalidPluginError(
|
128
|
+
f"Repository at {github_url} does not contain required setup.py or pyproject.toml"
|
129
|
+
)
|
130
|
+
|
131
|
+
# Install the package in development mode
|
132
|
+
installed_plugins = self._install_package(plugin_dir)
|
133
|
+
|
134
|
+
# Reload plugins and discover methods
|
135
|
+
self._reload_plugins()
|
136
|
+
|
137
|
+
return installed_plugins
|
138
|
+
|
139
|
+
except GitHubRepoError as e:
|
140
|
+
# Re-raise with more context
|
141
|
+
raise GitHubRepoError(f"Failed to access GitHub repository: {str(e)}")
|
142
|
+
except subprocess.CalledProcessError as e:
|
143
|
+
raise PluginInstallationError(f"Installation command failed: {e.output.decode() if hasattr(e, 'output') else str(e)}")
|
144
|
+
except Exception as e:
|
145
|
+
raise PluginInstallationError(f"Plugin installation failed: {str(e)}")
|
146
|
+
|
147
|
+
def _validate_github_url(self, url: str) -> None:
|
148
|
+
"""
|
149
|
+
Validate that a URL is a valid GitHub repository URL.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
url: URL to validate
|
153
|
+
|
154
|
+
Raises:
|
155
|
+
GitHubRepoError: If the URL is invalid
|
156
|
+
"""
|
157
|
+
parsed_url = urlparse(url)
|
158
|
+
|
159
|
+
# Check that it's a valid URL with https scheme
|
160
|
+
if not parsed_url.scheme or not parsed_url.netloc:
|
161
|
+
raise GitHubRepoError(f"Invalid URL: {url}")
|
162
|
+
|
163
|
+
# Check that it's a GitHub URL
|
164
|
+
if not parsed_url.netloc.endswith('github.com'):
|
165
|
+
raise GitHubRepoError(f"Not a GitHub URL: {url}")
|
166
|
+
|
167
|
+
# Check that it has a path (username/repo)
|
168
|
+
if not parsed_url.path or parsed_url.path.count('/') < 2:
|
169
|
+
raise GitHubRepoError(f"Invalid GitHub repository path: {url}")
|
170
|
+
|
171
|
+
def _clone_repository(self, url: str, target_dir: str, branch: Optional[str] = None) -> None:
|
172
|
+
"""
|
173
|
+
Clone a GitHub repository to a local directory.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
url: URL of the GitHub repository
|
177
|
+
target_dir: Directory to clone into
|
178
|
+
branch: Optional branch to checkout
|
179
|
+
|
180
|
+
Raises:
|
181
|
+
GitHubRepoError: If the clone fails
|
182
|
+
"""
|
183
|
+
try:
|
184
|
+
# Basic clone command
|
185
|
+
cmd = ['git', 'clone', url, target_dir]
|
186
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
187
|
+
|
188
|
+
# Checkout specific branch if requested
|
189
|
+
if branch:
|
190
|
+
subprocess.run(
|
191
|
+
['git', 'checkout', branch],
|
192
|
+
check=True,
|
193
|
+
capture_output=True,
|
194
|
+
cwd=target_dir
|
195
|
+
)
|
196
|
+
except subprocess.CalledProcessError as e:
|
197
|
+
error_msg = e.stderr.decode() if hasattr(e, 'stderr') else str(e)
|
198
|
+
raise GitHubRepoError(f"Failed to clone repository: {error_msg}")
|
199
|
+
|
200
|
+
def _has_package_files(self, directory: str) -> bool:
|
201
|
+
"""
|
202
|
+
Check if a directory contains Python package files.
|
203
|
+
|
204
|
+
Args:
|
205
|
+
directory: Directory to check
|
206
|
+
|
207
|
+
Returns:
|
208
|
+
True if setup.py or pyproject.toml exist
|
209
|
+
"""
|
210
|
+
return (
|
211
|
+
os.path.exists(os.path.join(directory, 'setup.py')) or
|
212
|
+
os.path.exists(os.path.join(directory, 'pyproject.toml'))
|
213
|
+
)
|
214
|
+
|
215
|
+
def _install_package(self, package_dir: str) -> List[str]:
|
216
|
+
"""
|
217
|
+
Install a Python package from a local directory.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
package_dir: Directory containing the package
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
List of installed plugin names
|
224
|
+
|
225
|
+
Raises:
|
226
|
+
PluginInstallationError: If installation fails
|
227
|
+
"""
|
228
|
+
try:
|
229
|
+
# Install in development mode
|
230
|
+
cmd = [sys.executable, '-m', 'pip', 'install', '-e', package_dir]
|
231
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
232
|
+
|
233
|
+
# Try to determine the package name from setup.py or pyproject.toml
|
234
|
+
package_name = self._get_package_name(package_dir)
|
235
|
+
|
236
|
+
# Record successful installation
|
237
|
+
self.installed_plugins[package_name] = package_dir
|
238
|
+
# Save the updated list of installed plugins
|
239
|
+
self._save_installed_plugins()
|
240
|
+
|
241
|
+
# Return names of plugins in this package
|
242
|
+
# For now just return the package name as a placeholder
|
243
|
+
return [package_name]
|
244
|
+
|
245
|
+
except subprocess.CalledProcessError as e:
|
246
|
+
error_msg = e.stderr.decode() if hasattr(e, 'stderr') else str(e)
|
247
|
+
raise PluginInstallationError(f"Failed to install package: {error_msg}")
|
248
|
+
|
249
|
+
def _get_package_name(self, package_dir: str) -> str:
|
250
|
+
"""
|
251
|
+
Extract the package name from setup.py or pyproject.toml.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
package_dir: Directory containing the package
|
255
|
+
|
256
|
+
Returns:
|
257
|
+
Package name (defaults to directory name if can't be determined)
|
258
|
+
"""
|
259
|
+
# Try to extract from setup.py
|
260
|
+
setup_py = os.path.join(package_dir, 'setup.py')
|
261
|
+
if os.path.exists(setup_py):
|
262
|
+
with open(setup_py, 'r') as f:
|
263
|
+
content = f.read()
|
264
|
+
# Look for name='...' pattern
|
265
|
+
match = re.search(r"name=['\"]([^'\"]+)['\"]", content)
|
266
|
+
if match:
|
267
|
+
return match.group(1)
|
268
|
+
|
269
|
+
# Try to extract from pyproject.toml
|
270
|
+
pyproject_toml = os.path.join(package_dir, 'pyproject.toml')
|
271
|
+
if os.path.exists(pyproject_toml):
|
272
|
+
with open(pyproject_toml, 'r') as f:
|
273
|
+
content = f.read()
|
274
|
+
# Look for name = "..." pattern
|
275
|
+
match = re.search(r"name\s*=\s*['\"]([^'\"]+)['\"]", content)
|
276
|
+
if match:
|
277
|
+
return match.group(1)
|
278
|
+
|
279
|
+
# Look for name in plugins registry to normalize name
|
280
|
+
try:
|
281
|
+
# Get the GitHub URL from setup.py or repo directory
|
282
|
+
git_config = os.path.join(package_dir, '.git', 'config')
|
283
|
+
if os.path.exists(git_config):
|
284
|
+
with open(git_config, 'r') as f:
|
285
|
+
content = f.read()
|
286
|
+
url_match = re.search(r"url\s*=\s*(.+)", content)
|
287
|
+
if url_match:
|
288
|
+
url = url_match.group(1).strip()
|
289
|
+
# Extract repo name
|
290
|
+
repo_name = url.split('/')[-1]
|
291
|
+
# Remove .git suffix if present
|
292
|
+
if repo_name.endswith('.git'):
|
293
|
+
repo_name = repo_name[:-4]
|
294
|
+
|
295
|
+
# If repo name follows common patterns
|
296
|
+
if repo_name.startswith('edsl-'):
|
297
|
+
return repo_name[5:] # Remove 'edsl-' prefix
|
298
|
+
if repo_name.startswith('plugin-'):
|
299
|
+
return repo_name[7:] # Remove 'plugin-' prefix
|
300
|
+
if repo_name.endswith('-plugin'):
|
301
|
+
return repo_name[:-7] # Remove '-plugin' suffix
|
302
|
+
except Exception as e:
|
303
|
+
logger.debug(f"Error extracting name from git config: {str(e)}")
|
304
|
+
|
305
|
+
# Default to directory name
|
306
|
+
dir_name = os.path.basename(os.path.normpath(package_dir))
|
307
|
+
# Clean up common prefixes/suffixes in directory names too
|
308
|
+
if dir_name.startswith('edsl-'):
|
309
|
+
return dir_name[5:]
|
310
|
+
if dir_name.startswith('plugin-'):
|
311
|
+
return dir_name[7:]
|
312
|
+
if dir_name.endswith('-plugin'):
|
313
|
+
return dir_name[:-7]
|
314
|
+
|
315
|
+
return dir_name
|
316
|
+
|
317
|
+
def _gather_exports(self):
|
318
|
+
"""Gather objects from plugins that should be exported to the global namespace."""
|
319
|
+
# Clear existing exports
|
320
|
+
self.exports = {}
|
321
|
+
|
322
|
+
# Get all plugins
|
323
|
+
for plugin in self.manager.get_plugins():
|
324
|
+
try:
|
325
|
+
# Check if the plugin implements the exports_to_namespace hook
|
326
|
+
if hasattr(plugin, "exports_to_namespace"):
|
327
|
+
exports = plugin.exports_to_namespace()
|
328
|
+
if exports:
|
329
|
+
# If plugin provides exports, add them to the exports dictionary
|
330
|
+
plugin_name = plugin.plugin_name()
|
331
|
+
for name, obj in exports.items():
|
332
|
+
# Log the export
|
333
|
+
logger.info(f"Plugin '{plugin_name}' exports '{name}' to global namespace")
|
334
|
+
self.exports[name] = obj
|
335
|
+
except Exception as e:
|
336
|
+
logger.warning(f"Error gathering exports from plugin: {str(e)}")
|
337
|
+
|
338
|
+
def get_exports(self) -> Dict[str, Any]:
|
339
|
+
"""
|
340
|
+
Get all objects that plugins export to the global namespace.
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
Dictionary mapping names to exported objects
|
344
|
+
"""
|
345
|
+
return self.exports
|
346
|
+
|
347
|
+
def _reload_plugins(self) -> None:
|
348
|
+
"""Reload plugins and discover new methods."""
|
349
|
+
# Reload setuptools entry points
|
350
|
+
self.manager.load_setuptools_entrypoints("edsl_plugins")
|
351
|
+
|
352
|
+
# Also check for directly installed plugins
|
353
|
+
for plugin_name, plugin_dir in self.installed_plugins.items():
|
354
|
+
try:
|
355
|
+
if os.path.exists(plugin_dir):
|
356
|
+
# Try to find the main plugin module
|
357
|
+
plugin_module = os.path.join(plugin_dir, plugin_name.lower())
|
358
|
+
if os.path.exists(f"{plugin_module}.py"):
|
359
|
+
spec = importlib.util.spec_from_file_location(plugin_name, f"{plugin_module}.py")
|
360
|
+
if spec:
|
361
|
+
module = importlib.util.module_from_spec(spec)
|
362
|
+
spec.loader.exec_module(module)
|
363
|
+
# Try to register the plugin class
|
364
|
+
if hasattr(module, plugin_name):
|
365
|
+
plugin_class = getattr(module, plugin_name)
|
366
|
+
self.manager.register(plugin_class())
|
367
|
+
logger.info(f"Registered plugin {plugin_name} from {plugin_dir}")
|
368
|
+
except Exception as e:
|
369
|
+
logger.warning(f"Error loading plugin {plugin_name} from {plugin_dir}: {str(e)}")
|
370
|
+
|
371
|
+
# Rediscover methods
|
372
|
+
self._discover_methods()
|
373
|
+
|
374
|
+
# Gather exports from plugins
|
375
|
+
self._gather_exports()
|
376
|
+
|
377
|
+
def uninstall_plugin(self, plugin_name: str) -> bool:
|
378
|
+
"""
|
379
|
+
Uninstall a plugin by name.
|
380
|
+
|
381
|
+
Args:
|
382
|
+
plugin_name: Name of the plugin to uninstall
|
383
|
+
|
384
|
+
Returns:
|
385
|
+
True if uninstallation was successful
|
386
|
+
|
387
|
+
Raises:
|
388
|
+
PluginNotFoundError: If the plugin is not installed
|
389
|
+
PluginInstallationError: If uninstallation fails
|
390
|
+
"""
|
391
|
+
# Check if plugin is installed directly
|
392
|
+
if plugin_name in self.installed_plugins:
|
393
|
+
actual_name = plugin_name
|
394
|
+
else:
|
395
|
+
# Try to find the plugin using case-insensitive matching and pattern recognition
|
396
|
+
actual_name = None
|
397
|
+
for installed_name in self.installed_plugins:
|
398
|
+
# Check for exact match ignoring case
|
399
|
+
if installed_name.lower() == plugin_name.lower():
|
400
|
+
actual_name = installed_name
|
401
|
+
break
|
402
|
+
|
403
|
+
# Check for matches with common prefix/suffix variations
|
404
|
+
normalized_installed = installed_name.lower()
|
405
|
+
normalized_requested = plugin_name.lower()
|
406
|
+
|
407
|
+
# Strip common prefixes/suffixes for comparison
|
408
|
+
for prefix in ["edsl-", "plugin-"]:
|
409
|
+
if normalized_installed.startswith(prefix):
|
410
|
+
normalized_installed = normalized_installed[len(prefix):]
|
411
|
+
if normalized_requested.startswith(prefix):
|
412
|
+
normalized_requested = normalized_requested[len(prefix):]
|
413
|
+
|
414
|
+
for suffix in ["-plugin"]:
|
415
|
+
if normalized_installed.endswith(suffix):
|
416
|
+
normalized_installed = normalized_installed[:-len(suffix)]
|
417
|
+
if normalized_requested.endswith(suffix):
|
418
|
+
normalized_requested = normalized_requested[:-len(suffix)]
|
419
|
+
|
420
|
+
# Compare normalized names
|
421
|
+
if normalized_installed == normalized_requested:
|
422
|
+
actual_name = installed_name
|
423
|
+
break
|
424
|
+
|
425
|
+
if actual_name is None:
|
426
|
+
raise PluginNotFoundError(f"Plugin '{plugin_name}' is not installed")
|
427
|
+
|
428
|
+
try:
|
429
|
+
package_dir = self.installed_plugins[actual_name]
|
430
|
+
logger.debug(f"Uninstalling plugin '{actual_name}' from {package_dir}")
|
431
|
+
|
432
|
+
# Determine pip package name
|
433
|
+
pip_package_name = actual_name
|
434
|
+
|
435
|
+
# Try to get the actual package name from setup.py or pyproject.toml
|
436
|
+
setup_py = os.path.join(package_dir, 'setup.py')
|
437
|
+
if os.path.exists(setup_py):
|
438
|
+
with open(setup_py, 'r') as f:
|
439
|
+
content = f.read()
|
440
|
+
match = re.search(r"name=['\"]([^'\"]+)['\"]", content)
|
441
|
+
if match:
|
442
|
+
pip_package_name = match.group(1)
|
443
|
+
|
444
|
+
pyproject_toml = os.path.join(package_dir, 'pyproject.toml')
|
445
|
+
if os.path.exists(pyproject_toml):
|
446
|
+
with open(pyproject_toml, 'r') as f:
|
447
|
+
content = f.read()
|
448
|
+
match = re.search(r"name\s*=\s*['\"]([^'\"]+)['\"]", content)
|
449
|
+
if match:
|
450
|
+
pip_package_name = match.group(1)
|
451
|
+
|
452
|
+
# Uninstall the package using pip
|
453
|
+
cmd = [sys.executable, '-m', 'pip', 'uninstall', '-y', pip_package_name]
|
454
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
455
|
+
|
456
|
+
# Remove from installed plugins
|
457
|
+
del self.installed_plugins[actual_name]
|
458
|
+
# Save the updated list of installed plugins
|
459
|
+
self._save_installed_plugins()
|
460
|
+
|
461
|
+
# Reload plugins
|
462
|
+
self._reload_plugins()
|
463
|
+
|
464
|
+
return True
|
465
|
+
|
466
|
+
except subprocess.CalledProcessError as e:
|
467
|
+
error_msg = e.stderr.decode() if hasattr(e, 'stderr') else str(e)
|
468
|
+
raise PluginInstallationError(f"Failed to uninstall plugin: {error_msg}")
|
469
|
+
|
470
|
+
def _register_builtin_plugins(self):
|
471
|
+
"""Register built-in plugins."""
|
472
|
+
# Import and register internal plugins
|
473
|
+
from .built_in.pig_latin import PigLatin
|
474
|
+
self.manager.register(PigLatin())
|
475
|
+
|
476
|
+
# Register the export example plugin
|
477
|
+
try:
|
478
|
+
from .built_in.export_example import ExportExample
|
479
|
+
self.manager.register(ExportExample())
|
480
|
+
except ImportError:
|
481
|
+
# In case the file isn't there or has issues
|
482
|
+
logger.warning("Failed to load export_example plugin")
|
483
|
+
|
484
|
+
def _discover_methods(self):
|
485
|
+
"""Discover and register all plugin methods."""
|
486
|
+
# Clear existing methods
|
487
|
+
self.methods = {}
|
488
|
+
|
489
|
+
# Get all plugin names
|
490
|
+
for plugin in self.manager.get_plugins():
|
491
|
+
try:
|
492
|
+
# Call the hook method directly on the plugin instance
|
493
|
+
plugin_name = plugin.plugin_name()
|
494
|
+
|
495
|
+
# Get methods from this plugin
|
496
|
+
methods = plugin.get_plugin_methods()
|
497
|
+
|
498
|
+
# Register methods with their full names and shortcuts
|
499
|
+
if methods:
|
500
|
+
for method_name, method in methods.items():
|
501
|
+
# Create a bound method that takes the plugin instance as the first argument
|
502
|
+
bound_method = lambda *args, m=method, **kwargs: m(*args, **kwargs)
|
503
|
+
|
504
|
+
# Full qualified name
|
505
|
+
full_name = f"{plugin_name}.{method_name}"
|
506
|
+
self.methods[full_name] = bound_method
|
507
|
+
|
508
|
+
# Register shorthand if not already taken
|
509
|
+
if method_name not in self.methods:
|
510
|
+
self.methods[method_name] = bound_method
|
511
|
+
except Exception as e:
|
512
|
+
logger.warning(f"Error discovering methods for plugin: {str(e)}")
|
513
|
+
|
514
|
+
def get_method(self, name: str) -> Optional[callable]:
|
515
|
+
"""
|
516
|
+
Get a method by name.
|
517
|
+
|
518
|
+
Args:
|
519
|
+
name: Method name, can be shorthand or fully qualified
|
520
|
+
|
521
|
+
Returns:
|
522
|
+
Method function or None if not found
|
523
|
+
|
524
|
+
Raises:
|
525
|
+
PluginMethodError: If the method is not found but a plugin is
|
526
|
+
"""
|
527
|
+
if name in self.methods:
|
528
|
+
return self.methods[name]
|
529
|
+
|
530
|
+
# If the name looks like a fully qualified name (plugin.method)
|
531
|
+
# but the plugin exists and the method doesn't, give a more helpful error
|
532
|
+
if '.' in name:
|
533
|
+
plugin_name = name.split('.')[0]
|
534
|
+
for plugin in self.manager.get_plugins():
|
535
|
+
if plugin.plugin_name() == plugin_name:
|
536
|
+
method_name = name.split('.')[1]
|
537
|
+
raise PluginMethodError(
|
538
|
+
f"Plugin '{plugin_name}' exists but method '{method_name}' was not found"
|
539
|
+
)
|
540
|
+
|
541
|
+
# Method not found
|
542
|
+
return None
|
543
|
+
|
544
|
+
def list_methods(self) -> List[str]:
|
545
|
+
"""
|
546
|
+
List all available methods.
|
547
|
+
|
548
|
+
Returns:
|
549
|
+
Sorted list of method names
|
550
|
+
"""
|
551
|
+
return sorted(self.methods.keys())
|
552
|
+
|
553
|
+
def list_plugins(self) -> Dict[str, Dict[str, Any]]:
|
554
|
+
"""
|
555
|
+
List all installed plugins with their details.
|
556
|
+
|
557
|
+
Returns:
|
558
|
+
Dictionary mapping plugin names to details
|
559
|
+
"""
|
560
|
+
plugins_info = {}
|
561
|
+
|
562
|
+
# Get plugins from the plugin manager
|
563
|
+
for plugin in self.manager.get_plugins():
|
564
|
+
try:
|
565
|
+
name = plugin.plugin_name()
|
566
|
+
|
567
|
+
# Gather details
|
568
|
+
plugin_info = {
|
569
|
+
"name": name,
|
570
|
+
"description": plugin.plugin_description() if hasattr(plugin, "plugin_description") else None,
|
571
|
+
"methods": list(plugin.get_plugin_methods().keys()) if hasattr(plugin, "get_plugin_methods") else [],
|
572
|
+
"installed_from": self.installed_plugins.get(name, "built-in")
|
573
|
+
}
|
574
|
+
|
575
|
+
plugins_info[name] = plugin_info
|
576
|
+
|
577
|
+
except Exception as e:
|
578
|
+
logger.warning(f"Error getting info for plugin: {str(e)}")
|
579
|
+
|
580
|
+
# Also include all plugins from installed_plugins.json even if they're not loaded
|
581
|
+
for name, install_dir in self.installed_plugins.items():
|
582
|
+
if name not in plugins_info:
|
583
|
+
# Try to find description from standard paths
|
584
|
+
description = None
|
585
|
+
try:
|
586
|
+
# First check if we can find a description in setup.py or pyproject.toml
|
587
|
+
setup_py = os.path.join(install_dir, 'setup.py')
|
588
|
+
if os.path.exists(setup_py):
|
589
|
+
with open(setup_py, 'r') as f:
|
590
|
+
content = f.read()
|
591
|
+
# Look for description="..." pattern
|
592
|
+
match = re.search(r"description=['\"]([^'\"]+)['\"]", content)
|
593
|
+
if match:
|
594
|
+
description = match.group(1)
|
595
|
+
|
596
|
+
pyproject_toml = os.path.join(install_dir, 'pyproject.toml')
|
597
|
+
if not description and os.path.exists(pyproject_toml):
|
598
|
+
with open(pyproject_toml, 'r') as f:
|
599
|
+
content = f.read()
|
600
|
+
# Look for description = "..." pattern
|
601
|
+
match = re.search(r"description\s*=\s*['\"]([^'\"]+)['\"]", content)
|
602
|
+
if match:
|
603
|
+
description = match.group(1)
|
604
|
+
|
605
|
+
# If still no description, check for README files
|
606
|
+
if not description:
|
607
|
+
for readme_file in ["README.md", "README.rst", "README.txt"]:
|
608
|
+
readme_path = os.path.join(install_dir, readme_file)
|
609
|
+
if os.path.exists(readme_path):
|
610
|
+
with open(readme_path, 'r') as f:
|
611
|
+
# Skip any markdown headers
|
612
|
+
first_line = f.readline().strip()
|
613
|
+
if first_line.startswith('#'):
|
614
|
+
# Take the next line that's not empty
|
615
|
+
for line in f:
|
616
|
+
if line.strip():
|
617
|
+
first_line = line.strip()
|
618
|
+
break
|
619
|
+
description = first_line[:100] + "..." if len(first_line) > 100 else first_line
|
620
|
+
break
|
621
|
+
except Exception as e:
|
622
|
+
logger.debug(f"Error extracting description for {name}: {e}")
|
623
|
+
|
624
|
+
# Add basic info for installed but not loaded plugins
|
625
|
+
plugins_info[name] = {
|
626
|
+
"name": name,
|
627
|
+
"description": description or f"Plugin installed at {install_dir}",
|
628
|
+
"methods": [],
|
629
|
+
"installed_from": install_dir,
|
630
|
+
"version": "unknown"
|
631
|
+
}
|
632
|
+
|
633
|
+
return plugins_info
|