o
    6i=+                     @   s   U d Z ddlZddlZddlZddlmZ ddlmZm	Z	m
Z
 ddlmZ eG dd dZeG dd	 d	ZG d
d dZdae
e ed< defddZddedefddZdS )z
Plugin Discovery and Loading System

Automatically discovers and loads plugins from the plugins/ directory.
Each plugin must be in a subdirectory with a plugin.py file containing a *Plugin class.
    N)Path)DictAnyOptional)	dataclassc                   @   s   e Zd ZU dZeed< eed< eed< dZeed< dZe	ed< d	Z
eed
< dZee ed< d	Zeed< dZeed< dZeed< dZeed< dZeed< dZeed< dZeed< dd ZdS )PluginParameterzODefines a parameter for the plugin, used for both validation and UI generation.nametypelabelFrequiredNdefault placeholderoptions	help_textminmax
min_length
max_lengthpatternpattern_descriptionc                 C   s&   | j d u r| jdkrg | _ d S d S d S )Nselect)r   r	   self r   6/home/byschii/byschiidev/penelope/lib/plugin_loader.py__post_init__%   s   
zPluginParameter.__post_init__)__name__
__module____qualname____doc__str__annotations__r   boolr   r   r   r   listr   r   floatr   r   intr   r   r   r   r   r   r   r   r      s"   
 r   c                   @   sJ   e Zd ZU dZeed< eed< dZee ed< dZee	 ed< dd Z
dS )	PluginMetadataz?Plugin metadata describing the plugin's interface and behavior.r   descriptionNmethods
parametersc                 C   s*   | j d u r	dg| _ | jd u rg | _d S d S )NPOST)r)   r*   r   r   r   r   r   2   s
   


zPluginMetadata.__post_init__)r   r   r   r    r!   r"   r)   r$   r*   r   r   r   r   r   r   r'   *   s   
 r'   c                   @   s   e Zd ZdZddefddZddd	Zd
ededdfddZde	dede
e fddZdede
e fddZdeeef fddZdeeef fddZdS )PluginRegistryz
    Central registry for all discovered plugins.
    
    Scans the plugins/ directory, loads valid plugin classes, and maintains
    a mapping of plugin names to plugin instances.
    pluginsplugins_dirc                 C   s   t || _i | _i | _dS )z
        Initialize the plugin registry.
        
        Args:
            plugins_dir: Path to the directory containing plugin subdirectories
        N)r   r.   r-   _load_errors)r   r.   r   r   r   __init__A   s   

zPluginRegistry.__init__returnNc                 C   s2  | j  std| j   td| j   | j jddd dS | j  s.td| j   dS dd | j  D }|sDtd	| j   dS td
| j  d |D ] }|d }| sid| j|j< td|j d qO| || qOtdd  tdt	| j
 d | jrtdt	| j d td d dS )aU  
        Scan plugins directory and load all valid plugins.
        
        Expected structure:
            plugins/
                wikipedia_search/
                    plugin.py  # Contains *Plugin class
                form_filler/
                    plugin.py
        
        Logs success/failure for each plugin discovered.
        u%   ⚠️  Plugins directory not found: z   Creating directory: T)parentsexist_okNu0   ❌ Plugins path exists but is not a directory: c                 S   s$   g | ]}|  r|jd s|qS )_)is_dirr   
startswith).0pr   r   r   
<listcomp>d   s   $ z4PluginRegistry.discover_and_load.<locals>.<listcomp>u'   ℹ️  No plugin directories found in u   
🔍 Scanning for plugins in z...z	plugin.pyzMissing plugin.py file   ⊘  z: Missing plugin.py file
z<============================================================u   ✅ Successfully loaded z
 plugin(s)u   ❌ Failed to load )r.   existsprintmkdirr5   iterdirr/   r   _load_plugin_from_filelenr-   )r   plugin_foldersplugin_folderplugin_filer   r   r   discover_and_loadL   s2   

z PluginRegistry.discover_and_loadrC   rD   c                    s  zd|j  d tj |}|du s|jdu rtd| tj|}|tj < |j	|  fddt
|t
jD }|sRd| j|j < td|j  d	 W dS |D ]}\}}| ||}|ru|| j|j < td
|j  d| d|  qTz3| }	|	jj }
|
| jv rtd|j  d| d|
 d W qT|	| j|
< td|j  d| d|
 d W qT ty } zd| | j|j < td
|j  d| d|  W Y d}~qTd}~ww W dS  ty } zd| | j|j < td
|j  d|  W Y d}~dS d}~ww )z
        Load a single plugin from a plugin.py file.
        
        Args:
            plugin_folder: The plugin's directory
            plugin_file: Path to the plugin.py file
        zplugins.z.pluginNzCould not load spec for c                    s,   g | ]\}}| d r|j kr||fqS )Plugin)endswithr   )r7   r   objmodule_namer   r   r9      s    
z9PluginRegistry._load_plugin_from_file.<locals>.<listcomp>zNo *Plugin class foundr:   z%: No class ending with 'Plugin' foundu   ❌ .z: u   ⚠️  z: Duplicate plugin name 'z' (skipping)u   ✅ u    → ''zFailed to instantiate: z: Failed to instantiate: zImport failed: z: Import failed: )r   	importlibutilspec_from_file_locationloaderImportErrormodule_from_specsysmodulesexec_moduleinspect
getmembersisclassr/   r=   _validate_plugin_classmetadatar-   	Exception)r   rC   rD   specmoduleplugin_classes
class_nameplugin_classvalidation_errorplugin_instanceplugin_nameer   rI   r   r@   ~   sP   



"($z%PluginRegistry._load_plugin_from_filer`   r_   c              
   C   s   t |dsdS t |dsdS t|d}t|sdS z.|j}t |dr&|js)W dS t |dr1|js4W d	S td
d |jD sFd|j dW S W dS  ty_ } zd| W  Y d}~S d}~ww )a"  
        Validate that a plugin class follows the required structure.
        
        Args:
            plugin_class: The class to validate
            class_name: Name of the class (for error messages)
        
        Returns:
            Error message if invalid, None if valid
        rZ   zMissing 'metadata' attributeexecutezMissing 'execute' methodz'execute' method must be asyncr   zmetadata.name is requiredr(   z metadata.description is requiredc                 s   s     | ]}|  p|d v V  qdS )z-_N)isalnum)r7   cr   r   r   	<genexpr>   s    z8PluginRegistry._validate_plugin_class.<locals>.<genexpr>zmetadata.name 'z?' is not URL-safe (use alphanumeric, hyphens, underscores only)zInvalid metadata: N)	hasattrgetattrrV   iscoroutinefunctionrZ   r   r(   allr[   )r   r`   r_   execute_methodrZ   rd   r   r   r   rY      s*   



z%PluginRegistry._validate_plugin_classr   c                 C   s   | j |S )z
        Get a plugin instance by name.
        
        Args:
            name: The plugin name (from metadata.name)
        
        Returns:
            Plugin instance or None if not found
        )r-   get)r   r   r   r   r   
get_plugin   s   
zPluginRegistry.get_pluginc                 C   
   | j  S )z
        Get all loaded plugins.
        
        Returns:
            Dictionary mapping plugin names to plugin instances
        )r-   copyr   r   r   r   list_plugins      
zPluginRegistry.list_pluginsc                 C   rp   )z
        Get all plugin load errors.
        
        Returns:
            Dictionary mapping plugin folder names to error messages
        )r/   rq   r   r   r   r   get_load_errors   rs   zPluginRegistry.get_load_errorsr-   )r1   N)r   r   r   r    r!   r0   rE   r   r@   r	   r   rY   r   ro   r   rr   rt   r   r   r   r   r,   9   s    
2=,	r,   _plugin_registryr1   c                   C   s   t du rt a t S )zo
    Get the global plugin registry instance.
    
    Returns:
        The global PluginRegistry instance
    N)rv   r,   r   r   r   r   get_plugin_registry
  s   rw   r-   r.   c                 C   s   t | at  tS )z
    Initialize the plugin system by discovering and loading all plugins.
    
    Args:
        plugins_dir: Path to the plugins directory
    
    Returns:
        The initialized PluginRegistry instance
    )r,   rv   rE   )r.   r   r   r   initialize_plugins  s   rx   ru   )r    importlib.utilrM   rV   rS   pathlibr   typingr   r   r   dataclassesr   r   r'   r,   rv   r"   rw   r!   rx   r   r   r   r   <module>   s     O