| | """
|
| | Package management utilities for dynamic package installation in Modal sandboxes.
|
| | This module provides functions to analyze code for imports and manage package installation.
|
| | """
|
| | import ast
|
| | import re
|
| | from typing import Set, List
|
| |
|
| | try:
|
| | from mcp_hub.logging_config import logger
|
| | except ImportError:
|
| |
|
| | import logging
|
| | logger = logging.getLogger(__name__)
|
| |
|
| |
|
| |
|
| | CORE_PREINSTALLED_PACKAGES = {
|
| | "numpy", "pandas", "matplotlib", "requests", "json", "os", "sys",
|
| | "time", "datetime", "math", "random", "collections", "itertools",
|
| | "functools", "re", "urllib", "csv", "sqlite3", "pathlib", "typing",
|
| | "asyncio", "threading", "multiprocessing", "subprocess", "shutil",
|
| | "tempfile", "io", "gzip", "zipfile", "tarfile", "base64", "hashlib",
|
| | "secrets", "uuid", "pickle", "copy", "operator", "bisect", "heapq",
|
| | "contextlib", "weakref", "gc", "inspect", "types", "enum", "dataclasses",
|
| | "decimal", "fractions", "statistics", "string", "textwrap", "locale",
|
| | "calendar", "timeit", "argparse", "getopt", "logging", "warnings",
|
| | "platform", "signal", "errno", "ctypes", "struct", "array", "queue",
|
| | "socketserver", "http", "urllib2", "html", "xml", "email", "mailbox"
|
| | }
|
| |
|
| |
|
| | COMMON_PACKAGES = {
|
| | "scikit-learn": "sklearn",
|
| | "beautifulsoup4": "bs4",
|
| | "pillow": "PIL",
|
| | "opencv-python-headless": "cv2",
|
| | "python-dateutil": "dateutil",
|
| | "plotly": "plotly",
|
| | "seaborn": "seaborn",
|
| | "polars": "polars",
|
| | "lightgbm": "lightgbm",
|
| | "xgboost": "xgboost",
|
| | "flask": "flask",
|
| | "fastapi": "fastapi",
|
| | "httpx": "httpx",
|
| | "networkx": "networkx",
|
| | "wordcloud": "wordcloud",
|
| | "textblob": "textblob",
|
| | "spacy": "spacy",
|
| | "nltk": "nltk"
|
| | }
|
| |
|
| |
|
| | IMPORT_TO_PACKAGE = {v: k for k, v in COMMON_PACKAGES.items()}
|
| | IMPORT_TO_PACKAGE.update({k: k for k in COMMON_PACKAGES.keys()})
|
| |
|
| |
|
| | def extract_imports_from_code(code_str: str) -> Set[str]:
|
| | """
|
| | Extract all import statements from Python code using AST parsing.
|
| |
|
| | Args:
|
| | code_str: The Python code to analyze
|
| |
|
| | Returns:
|
| | Set of imported module names (top-level only)
|
| | """
|
| | imports = set()
|
| |
|
| | try:
|
| | tree = ast.parse(code_str)
|
| | for node in ast.walk(tree):
|
| | if isinstance(node, ast.Import):
|
| | for alias in node.names:
|
| |
|
| | module_name = alias.name.split('.')[0]
|
| | imports.add(module_name)
|
| | elif isinstance(node, ast.ImportFrom):
|
| | if node.module:
|
| |
|
| | module_name = node.module.split('.')[0]
|
| | imports.add(module_name)
|
| | except Exception as e:
|
| | logger.warning(f"Failed to parse code with AST, falling back to regex: {e}")
|
| |
|
| | imports.update(extract_imports_with_regex(code_str))
|
| |
|
| | return imports
|
| |
|
| |
|
| | def extract_imports_with_regex(code_str: str) -> Set[str]:
|
| | """
|
| | Fallback method to extract imports using regex patterns.
|
| |
|
| | Args:
|
| | code_str: The Python code to analyze
|
| |
|
| | Returns:
|
| | Set of imported module names
|
| | """
|
| | imports = set()
|
| |
|
| |
|
| | import_pattern = r'^import\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'
|
| |
|
| |
|
| | from_pattern = r'^from\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s+import'
|
| |
|
| | for line in code_str.split('\n'):
|
| | line = line.strip()
|
| | if not line or line.startswith('#'):
|
| | continue
|
| |
|
| |
|
| | import_match = re.match(import_pattern, line)
|
| | if import_match:
|
| | module_name = import_match.group(1).split('.')[0]
|
| | imports.add(module_name)
|
| | continue
|
| |
|
| |
|
| | from_match = re.match(from_pattern, line)
|
| | if from_match:
|
| | module_name = from_match.group(1).split('.')[0]
|
| | imports.add(module_name)
|
| |
|
| | return imports
|
| |
|
| |
|
| | def get_packages_to_install(detected_imports: Set[str]) -> List[str]:
|
| | """
|
| | Determine which packages need to be installed based on detected imports.
|
| |
|
| | Args:
|
| | detected_imports: Set of module names found in the code
|
| |
|
| | Returns:
|
| | List of package names that need to be pip installed
|
| | """
|
| | packages_to_install = []
|
| |
|
| | for import_name in detected_imports:
|
| |
|
| | if import_name in CORE_PREINSTALLED_PACKAGES:
|
| | continue
|
| |
|
| |
|
| | if import_name in IMPORT_TO_PACKAGE:
|
| | package_name = IMPORT_TO_PACKAGE[import_name]
|
| | packages_to_install.append(package_name)
|
| |
|
| | elif import_name not in CORE_PREINSTALLED_PACKAGES:
|
| | packages_to_install.append(import_name)
|
| |
|
| | return packages_to_install
|
| |
|
| |
|
| | def get_warmup_import_commands() -> List[str]:
|
| | """
|
| | Get list of import commands to run during sandbox warmup.
|
| |
|
| | Returns:
|
| | List of Python import statements for core packages
|
| | """
|
| | core_imports = [
|
| | "import numpy",
|
| | "import pandas",
|
| | "import matplotlib.pyplot",
|
| | "import requests",
|
| | "print('Core packages warmed up successfully')"
|
| | ]
|
| |
|
| | return core_imports
|
| |
|
| |
|
| | def create_package_install_command(packages: List[str]) -> str:
|
| | """
|
| | Create a pip install command for the given packages.
|
| |
|
| | Args:
|
| | packages: List of package names to install
|
| |
|
| | Returns:
|
| | Pip install command string
|
| | """
|
| | if not packages:
|
| | return ""
|
| |
|
| |
|
| | unique_packages = sorted(set(packages))
|
| | return f"pip install {' '.join(unique_packages)}" |