""" Utility functions for backend plugins """ import logging import re from pathlib import Path from typing import Any from fastapi_app.config import get_settings logger = logging.getLogger(__name__) def get_plugin_config( config_key: str, env_var: str, default: Any = None, value_type: str = "string" ) -> Any: """ Get plugin configuration value with env var fallback. Priority: config.json > environment variable > default Creates config key from env var if it doesn't exist. Args: config_key: Dot-notation config key (e.g., "plugin.local-sync.enabled") env_var: Environment variable name default: Default value if neither source has value value_type: Type for validation ("string", "boolean", "number", "array") Returns: Configuration value Example: >>> enabled = get_plugin_config( ... "plugin.local-sync.enabled", ... "PLUGIN_LOCAL_SYNC_ENABLED", ... default=False, ... value_type="boolean" ... ) """ from fastapi_app.lib.utils.config_utils import get_config import os config = get_config() # Try to get from config value = config.get(config_key) if value is None: # Check environment variable env_value = os.environ.get(env_var) if env_value is not None: # Parse env value based on type if value_type == "boolean": value = env_value.lower() in ("true", "1", "yes") elif value_type == "number": value = int(env_value) if env_value.isdigit() else float(env_value) elif value_type == "array": import json value = json.loads(env_value) else: value = env_value # Create config key from env var config.set(config_key, value) else: # Use default value = default if value is not None: config.set(config_key, value) return value def _extract_sandbox_methods() -> list[dict[str, str]]: """ Extract public method signatures from PluginSandbox class. Reads backend-plugin-sandbox.js and parses JSDoc comments and method signatures to build a list of available public methods. Returns: List of dicts with 'name', 'params', and 'doc' keys Example return: [ { 'name': 'openDocument', 'params': ['stableId'], 'doc': 'Open a document by updating xml state and closing dialog' }, ... ] """ # Find the sandbox module file sandbox_file = get_settings().project_root_dir / 'app' / 'src' / 'modules' / 'backend-plugin-sandbox.js' if not sandbox_file.exists(): # Fallback to empty list if file not found return [] content = sandbox_file.read_text(encoding='utf-8') methods = [] # Pattern to match JSDoc comment followed by method definition # Matches: /** ... */ async methodName(param1, param2) { pattern = r'/\*\*\s*(.*?)\s*\*/\s*(?:async\s+)?(\w+)\s*\((.*?)\)\s*\{' for match in re.finditer(pattern, content, re.DOTALL): jsdoc = match.group(1) method_name = match.group(2) params_str = match.group(3) # Skip constructor and private methods if method_name == 'constructor' or method_name.startswith('_'): continue # Extract parameter names (ignore types and defaults) params = [] if params_str.strip(): for param in params_str.split(','): param = param.strip() # Extract just the name (before = or :) param_name = re.split(r'[=:]', param)[0].strip() if param_name: params.append(param_name) # Extract first line of JSDoc as description doc = '' for line in jsdoc.split('\n'): line = line.strip().lstrip('*').strip() # Skip @param, @returns, etc. if line and not line.startswith('@'): doc = line break methods.append({ 'name': method_name, 'params': params, 'doc': doc }) return methods def generate_sandbox_client_script() -> str: """ Generate JavaScript code that establishes connection with parent window and provides sandbox API access. Dynamically generates client methods based on PluginSandbox class definition. Returns: JavaScript code as string to be embedded in