universal-mcp 0.1.24rc14__py3-none-any.whl → 0.1.24rc17__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 (60) hide show
  1. universal_mcp/agentr/registry.py +4 -4
  2. universal_mcp/applications/application.py +0 -2
  3. universal_mcp/applications/utils.py +52 -0
  4. universal_mcp/servers/server.py +4 -3
  5. universal_mcp/tools/manager.py +0 -3
  6. universal_mcp/types.py +1 -21
  7. universal_mcp/utils/prompts.py +0 -2
  8. universal_mcp/utils/testing.py +1 -1
  9. {universal_mcp-0.1.24rc14.dist-info → universal_mcp-0.1.24rc17.dist-info}/METADATA +2 -1
  10. universal_mcp-0.1.24rc17.dist-info/RECORD +54 -0
  11. universal_mcp/__init__.py +0 -0
  12. universal_mcp/agents/__init__.py +0 -10
  13. universal_mcp/agents/autoagent/__init__.py +0 -30
  14. universal_mcp/agents/autoagent/__main__.py +0 -25
  15. universal_mcp/agents/autoagent/context.py +0 -26
  16. universal_mcp/agents/autoagent/graph.py +0 -151
  17. universal_mcp/agents/autoagent/prompts.py +0 -9
  18. universal_mcp/agents/autoagent/state.py +0 -27
  19. universal_mcp/agents/autoagent/studio.py +0 -25
  20. universal_mcp/agents/autoagent/utils.py +0 -13
  21. universal_mcp/agents/base.py +0 -129
  22. universal_mcp/agents/bigtool/__init__.py +0 -54
  23. universal_mcp/agents/bigtool/__main__.py +0 -24
  24. universal_mcp/agents/bigtool/context.py +0 -24
  25. universal_mcp/agents/bigtool/graph.py +0 -166
  26. universal_mcp/agents/bigtool/prompts.py +0 -31
  27. universal_mcp/agents/bigtool/state.py +0 -27
  28. universal_mcp/agents/bigtool2/__init__.py +0 -53
  29. universal_mcp/agents/bigtool2/__main__.py +0 -24
  30. universal_mcp/agents/bigtool2/agent.py +0 -11
  31. universal_mcp/agents/bigtool2/context.py +0 -33
  32. universal_mcp/agents/bigtool2/graph.py +0 -169
  33. universal_mcp/agents/bigtool2/prompts.py +0 -12
  34. universal_mcp/agents/bigtool2/state.py +0 -27
  35. universal_mcp/agents/builder.py +0 -80
  36. universal_mcp/agents/cli.py +0 -27
  37. universal_mcp/agents/codeact/__init__.py +0 -243
  38. universal_mcp/agents/codeact/sandbox.py +0 -27
  39. universal_mcp/agents/codeact/test.py +0 -15
  40. universal_mcp/agents/codeact/utils.py +0 -61
  41. universal_mcp/agents/hil.py +0 -104
  42. universal_mcp/agents/llm.py +0 -45
  43. universal_mcp/agents/planner/__init__.py +0 -37
  44. universal_mcp/agents/planner/__main__.py +0 -24
  45. universal_mcp/agents/planner/graph.py +0 -82
  46. universal_mcp/agents/planner/prompts.py +0 -1
  47. universal_mcp/agents/planner/state.py +0 -12
  48. universal_mcp/agents/react.py +0 -84
  49. universal_mcp/agents/shared/agent_node.py +0 -34
  50. universal_mcp/agents/shared/tool_node.py +0 -235
  51. universal_mcp/agents/simple.py +0 -40
  52. universal_mcp/agents/tools.py +0 -35
  53. universal_mcp/agents/utils.py +0 -111
  54. universal_mcp/analytics.py +0 -111
  55. universal_mcp/applications/__init__.py +0 -70
  56. universal_mcp/utils/common.py +0 -278
  57. universal_mcp-0.1.24rc14.dist-info/RECORD +0 -99
  58. {universal_mcp-0.1.24rc14.dist-info → universal_mcp-0.1.24rc17.dist-info}/WHEEL +0 -0
  59. {universal_mcp-0.1.24rc14.dist-info → universal_mcp-0.1.24rc17.dist-info}/entry_points.txt +0 -0
  60. {universal_mcp-0.1.24rc14.dist-info → universal_mcp-0.1.24rc17.dist-info}/licenses/LICENSE +0 -0
@@ -1,40 +0,0 @@
1
- import asyncio
2
- from typing import Annotated
3
-
4
- from langgraph.checkpoint.base import BaseCheckpointSaver
5
- from langgraph.graph import END, START, StateGraph
6
- from langgraph.graph.message import add_messages
7
- from typing_extensions import TypedDict
8
-
9
- from universal_mcp.agents.base import BaseAgent
10
- from universal_mcp.agents.llm import load_chat_model
11
-
12
-
13
- class State(TypedDict):
14
- messages: Annotated[list, add_messages]
15
-
16
-
17
- class SimpleAgent(BaseAgent):
18
- def __init__(self, name: str, instructions: str, model: str, memory: BaseCheckpointSaver = None, **kwargs):
19
- super().__init__(name, instructions, model, memory, **kwargs)
20
- self.llm = load_chat_model(model)
21
-
22
- async def _build_graph(self):
23
- graph_builder = StateGraph(State)
24
-
25
- async def chatbot(state: State):
26
- messages = [
27
- {"role": "system", "content": self.instructions},
28
- *state["messages"],
29
- ]
30
- return {"messages": [await self.llm.ainvoke(messages)]}
31
-
32
- graph_builder.add_node("chatbot", chatbot)
33
- graph_builder.add_edge(START, "chatbot")
34
- graph_builder.add_edge("chatbot", END)
35
- return graph_builder.compile(checkpointer=self.memory)
36
-
37
-
38
- if __name__ == "__main__":
39
- agent = SimpleAgent("Simple Agent", "You are a helpful assistant", "azure/gpt-4o")
40
- asyncio.run(agent.run_interactive())
@@ -1,35 +0,0 @@
1
- import json
2
-
3
- from langchain_mcp_adapters.client import MultiServerMCPClient
4
-
5
- from universal_mcp.agentr.integration import AgentrIntegration
6
- from universal_mcp.applications import app_from_slug
7
- from universal_mcp.tools.adapters import ToolFormat
8
- from universal_mcp.tools.manager import ToolManager
9
- from universal_mcp.types import ToolConfig
10
-
11
-
12
- async def load_agentr_tools(agentr_servers: dict):
13
- tool_manager = ToolManager()
14
- for app_name, tool_names in agentr_servers.items():
15
- app = app_from_slug(app_name)
16
- integration = AgentrIntegration(name=app_name)
17
- app_instance = app(integration=integration)
18
- tool_manager.register_tools_from_app(app_instance, tool_names=tool_names["tools"])
19
- tools = tool_manager.list_tools(format=ToolFormat.LANGCHAIN)
20
- return tools
21
-
22
-
23
- async def load_mcp_tools(mcp_servers: dict):
24
- client = MultiServerMCPClient(mcp_servers)
25
- tools = await client.get_tools()
26
- return tools
27
-
28
-
29
- async def load_tools(path: str) -> ToolConfig:
30
- with open(path) as f:
31
- data = json.load(f)
32
- config = ToolConfig.model_validate(data)
33
- agentr_tools = await load_agentr_tools(config.model_dump(exclude_none=True)["agentrServers"])
34
- mcp_tools = await load_mcp_tools(config.model_dump(exclude_none=True)["mcpServers"])
35
- return agentr_tools + mcp_tools
@@ -1,111 +0,0 @@
1
- import json
2
- from contextlib import contextmanager
3
-
4
- from rich.console import Console
5
- from rich.live import Live
6
- from rich.markdown import Markdown
7
- from rich.panel import Panel
8
- from rich.prompt import Prompt
9
- from rich.table import Table
10
-
11
-
12
- class RichCLI:
13
- def __init__(self):
14
- self.console = Console()
15
-
16
- def display_welcome(self, agent_name: str):
17
- """Display welcome message"""
18
- welcome_text = f"""
19
- # Welcome to {agent_name}!
20
-
21
- Available commands:
22
- - Type your questions naturally
23
- - `/help` - Show help
24
- - `/tools` - List available tools
25
- - `/exit` - Exit the application
26
- """
27
- self.console.print(Panel(Markdown(welcome_text), title="🤖 AI Agent CLI", border_style="blue"))
28
-
29
- def display_agent_response(self, response: str, agent_name: str):
30
- """Display agent response with formatting"""
31
- self.console.print(Panel(Markdown(response), title=f"🤖 {agent_name}", border_style="green", padding=(1, 2)))
32
-
33
- @contextmanager
34
- def display_agent_response_streaming(self, agent_name: str):
35
- """Context manager for streaming agent response updates."""
36
-
37
- with Live(refresh_per_second=10, console=self.console) as live:
38
-
39
- class StreamUpdater:
40
- content = []
41
-
42
- def update(self, chunk: str):
43
- self.content.append(chunk)
44
- panel = Panel(
45
- Markdown("".join(self.content)),
46
- title=f"🤖 {agent_name}",
47
- border_style="green",
48
- padding=(1, 2),
49
- )
50
- live.update(panel)
51
-
52
- yield StreamUpdater()
53
-
54
- def display_thinking(self, thought: str):
55
- """Display agent's thinking process"""
56
- if thought:
57
- self.console.print(Panel(thought, title="💭 Thinking", border_style="yellow", padding=(1, 2)))
58
-
59
- def display_tools(self, tools: list):
60
- """Display available tools in a table"""
61
- table = Table(title="🛠️ Available Tools")
62
- table.add_column("Tool Name", style="cyan")
63
- table.add_column("Description", style="white")
64
-
65
- for tool in tools:
66
- func_info = tool["function"]
67
- table.add_row(func_info["name"], func_info["description"])
68
-
69
- self.console.print(table)
70
-
71
- def display_tool_call(self, tool_call: dict):
72
- """Display tool call"""
73
- tool_call_str = json.dumps(tool_call, indent=2)
74
- self.console.print(Panel(tool_call_str, title="🛠️ Tool Call", border_style="green", padding=(1, 2)))
75
-
76
- def display_tool_result(self, tool_result: dict):
77
- """Display tool result"""
78
- tool_result_str = json.dumps(tool_result, indent=2)
79
- self.console.print(Panel(tool_result_str, title="🛠️ Tool Result", border_style="green", padding=(1, 2)))
80
-
81
- def display_error(self, error: str):
82
- """Display error message"""
83
- self.console.print(Panel(error, title="❌ Error", border_style="red"))
84
-
85
- def get_user_input(self) -> str:
86
- """Get user input with rich prompt"""
87
- return Prompt.ask("[bold blue]You[/bold blue]", console=self.console)
88
-
89
- def display_info(self, message: str):
90
- """Display info message"""
91
- self.console.print(f"[bold cyan]ℹ️ {message}[/bold cyan]")
92
-
93
- def clear_screen(self):
94
- """Clear the screen"""
95
- self.console.clear()
96
-
97
- def handle_interrupt(self, interrupt) -> str | bool:
98
- interrupt_type = interrupt.value["type"]
99
- if interrupt_type == "text":
100
- value = Prompt.ask(interrupt.value["question"])
101
- return value
102
- elif interrupt_type == "bool":
103
- value = Prompt.ask(interrupt.value["question"], choices=["y", "n"], default="y")
104
- return value
105
- elif interrupt_type == "choice":
106
- value = Prompt.ask(
107
- interrupt.value["question"], choices=interrupt.value["choices"], default=interrupt.value["choices"][0]
108
- )
109
- return value
110
- else:
111
- raise ValueError(f"Invalid interrupt type: {interrupt.value['type']}")
@@ -1,111 +0,0 @@
1
- import os
2
- import uuid
3
- from functools import lru_cache
4
- from importlib.metadata import version
5
-
6
- import posthog
7
- from loguru import logger
8
-
9
-
10
- class Analytics:
11
- """A singleton class for tracking analytics events using PostHog.
12
-
13
- This class handles the initialization of the PostHog client and provides
14
- methods to track key events such as application loading and tool execution.
15
- Telemetry can be disabled by setting the TELEMETRY_DISABLED environment
16
- variable to "true".
17
- """
18
-
19
- _instance = None
20
-
21
- def __new__(cls):
22
- if cls._instance is None:
23
- cls._instance = super().__new__(cls)
24
- cls._instance._initialize()
25
- return cls._instance
26
-
27
- def _initialize(self):
28
- """Initializes the PostHog client and sets up analytics properties.
29
-
30
- This internal method configures the PostHog API key and host.
31
- It also determines if analytics should be enabled based on the
32
- TELEMETRY_DISABLED environment variable and generates a unique
33
- user ID.
34
- """
35
- posthog.host = "https://us.i.posthog.com"
36
- posthog.api_key = "phc_6HXMDi8CjfIW0l04l34L7IDkpCDeOVz9cOz1KLAHXh8"
37
- self.enabled = os.getenv("TELEMETRY_DISABLED", "false").lower() != "true"
38
- self.user_id = str(uuid.uuid4())[:8]
39
-
40
- @staticmethod
41
- @lru_cache(maxsize=1)
42
- def get_version():
43
- """Retrieves the installed version of the universal_mcp package.
44
-
45
- Uses importlib.metadata to get the package version.
46
- Caches the result for efficiency.
47
-
48
- Returns:
49
- str: The package version string, or "unknown" if not found.
50
- """
51
- try:
52
- return version("universal_mcp")
53
- except ImportError: # Should be PackageNotFoundError, but matching existing code
54
- return "unknown"
55
-
56
- def track_app_loaded(self, app_name: str):
57
- """Tracks an event when an application is successfully loaded.
58
-
59
- This event helps understand which applications are being utilized.
60
-
61
- Args:
62
- app_name (str): The name of the application that was loaded.
63
- """
64
- if not self.enabled:
65
- return
66
- try:
67
- properties = {
68
- "version": self.get_version(),
69
- "app_name": app_name,
70
- "user_id": self.user_id,
71
- }
72
- posthog.capture("app_loaded", properties=properties)
73
- except Exception as e:
74
- logger.error(f"Failed to track app_loaded event: {e}")
75
-
76
- def track_tool_called(
77
- self,
78
- tool_name: str,
79
- app_name: str,
80
- status: str,
81
- error: str | None = None,
82
- ):
83
- """Tracks an event when a tool is called within an application.
84
-
85
- This event provides insights into tool usage patterns, success rates,
86
- and potential errors.
87
-
88
- Args:
89
- tool_name (str): The name of the tool that was called.
90
- app_name (str): The name of the application the tool belongs to.
91
- status (str): The status of the tool call (e.g., "success", "error").
92
- error (str, optional): The error message if the tool call failed.
93
- Defaults to None.
94
- """
95
- if not self.enabled:
96
- return
97
- try:
98
- properties = {
99
- "tool_name": tool_name,
100
- "app_name": app_name,
101
- "status": status,
102
- "error": error,
103
- "version": self.get_version(),
104
- "user_id": self.user_id,
105
- }
106
- posthog.capture("tool_called", properties=properties)
107
- except Exception as e:
108
- logger.error(f"Failed to track tool_called event: {e}")
109
-
110
-
111
- analytics = Analytics()
@@ -1,70 +0,0 @@
1
- from loguru import logger
2
-
3
- from universal_mcp.applications.application import (
4
- APIApplication,
5
- BaseApplication,
6
- GraphQLApplication,
7
- )
8
- from universal_mcp.config import AppConfig
9
- from universal_mcp.utils.common import (
10
- load_app_from_local_file,
11
- load_app_from_local_folder,
12
- load_app_from_package,
13
- load_app_from_remote_file,
14
- load_app_from_remote_zip,
15
- )
16
-
17
- app_cache: dict[str, type[BaseApplication]] = {}
18
-
19
-
20
- def app_from_slug(slug: str) -> type[BaseApplication]:
21
- """
22
- Dynamically resolve and return the application class based on slug.
23
- """
24
- return app_from_config(AppConfig(name=slug, source_type="package"))
25
-
26
-
27
- def app_from_config(config: AppConfig) -> type[BaseApplication]:
28
- """
29
- Dynamically resolve and return the application class based on AppConfig.
30
- """
31
- if config.name in app_cache:
32
- return app_cache[config.name]
33
-
34
- app_class = None
35
- try:
36
- match config.source_type:
37
- case "package":
38
- app_class = load_app_from_package(config)
39
- case "local_folder":
40
- app_class = load_app_from_local_folder(config)
41
- case "remote_zip":
42
- app_class = load_app_from_remote_zip(config)
43
- case "remote_file":
44
- app_class = load_app_from_remote_file(config)
45
- case "local_file":
46
- app_class = load_app_from_local_file(config)
47
- case _:
48
- raise ValueError(f"Unsupported source_type: {config.source_type}")
49
-
50
- except Exception as e:
51
- logger.error(
52
- f"Failed to load application '{config.name}' from source '{config.source_type}': {e}",
53
- exc_info=True,
54
- )
55
- raise
56
-
57
- if not app_class:
58
- raise ImportError(f"Could not load application class for '{config.name}'")
59
-
60
- logger.debug(f"Loaded class '{app_class.__name__}' for app '{config.name}'")
61
- app_cache[config.name] = app_class
62
- return app_class
63
-
64
-
65
- __all__ = [
66
- "app_from_config",
67
- "BaseApplication",
68
- "APIApplication",
69
- "GraphQLApplication",
70
- ]
@@ -1,278 +0,0 @@
1
- import hashlib
2
- import importlib
3
- import importlib.util
4
- import inspect
5
- import io
6
- import os
7
- import subprocess
8
- import sys
9
- import zipfile
10
- from pathlib import Path
11
-
12
- import httpx
13
- from loguru import logger
14
-
15
- from universal_mcp.applications.application import BaseApplication
16
- from universal_mcp.config import AppConfig
17
-
18
- # --- Global Constants and Setup ---
19
-
20
- UNIVERSAL_MCP_HOME = Path.home() / ".universal-mcp" / "packages"
21
- REMOTE_CACHE_DIR = UNIVERSAL_MCP_HOME / "remote_cache"
22
-
23
- if not UNIVERSAL_MCP_HOME.exists():
24
- UNIVERSAL_MCP_HOME.mkdir(parents=True, exist_ok=True)
25
- if not REMOTE_CACHE_DIR.exists():
26
- REMOTE_CACHE_DIR.mkdir(exist_ok=True)
27
-
28
- # set python path to include the universal-mcp home directory
29
- if str(UNIVERSAL_MCP_HOME) not in sys.path:
30
- sys.path.append(str(UNIVERSAL_MCP_HOME))
31
-
32
-
33
- # --- Default Name Generators ---
34
-
35
-
36
- def get_default_repository_path(slug: str) -> str:
37
- """
38
- Convert a repository slug to a repository URL.
39
- """
40
- slug = slug.strip().lower()
41
- return f"universal-mcp-{slug}"
42
-
43
-
44
- def get_default_package_name(slug: str) -> str:
45
- """
46
- Convert a repository slug to a package name.
47
- """
48
- slug = slug.strip().lower()
49
- package_name = f"universal_mcp_{slug.replace('-', '_')}"
50
- return package_name
51
-
52
-
53
- def get_default_module_path(slug: str) -> str:
54
- """
55
- Convert a repository slug to a module path.
56
- """
57
- package_name = get_default_package_name(slug)
58
- module_path = f"{package_name}.app"
59
- return module_path
60
-
61
-
62
- def get_default_class_name(slug: str) -> str:
63
- """
64
- Convert a repository slug to a class name.
65
- """
66
- slug = slug.strip().lower()
67
- class_name = "".join(part.capitalize() for part in slug.split("-")) + "App"
68
- return class_name
69
-
70
-
71
- # --- Installation and Loading Helpers ---
72
-
73
-
74
- def install_or_upgrade_package(package_name: str, repository_path: str):
75
- """
76
- Helper to install a package via pip from the universal-mcp GitHub repository.
77
- """
78
- uv_path = os.getenv("UV_PATH")
79
- uv_executable = str(Path(uv_path) / "uv") if uv_path else "uv"
80
- logger.info(f"Using uv executable: {uv_executable}")
81
- cmd = [
82
- uv_executable,
83
- "pip",
84
- "install",
85
- "--upgrade",
86
- repository_path,
87
- "--target",
88
- str(UNIVERSAL_MCP_HOME),
89
- ]
90
- logger.debug(f"Installing package '{package_name}' with command: {' '.join(cmd)}")
91
- try:
92
- result = subprocess.run(cmd, capture_output=True, text=True, check=True)
93
- if result.stdout:
94
- logger.info(f"Command stdout: {result.stdout}")
95
- if result.stderr:
96
- logger.warning(f"Command stderr: {result.stderr}")
97
- except subprocess.CalledProcessError as e:
98
- logger.error(f"Installation failed for '{package_name}': {e}")
99
- logger.error(f"Command stdout:\n{e.stdout}")
100
- logger.error(f"Command stderr:\n{e.stderr}")
101
- raise ModuleNotFoundError(f"Installation failed for package '{package_name}'") from e
102
- else:
103
- logger.debug(f"Package {package_name} installed successfully")
104
-
105
-
106
- def install_dependencies_from_path(project_root: Path, target_install_dir: Path):
107
- """
108
- Installs dependencies from pyproject.toml or requirements.txt found in project_root.
109
- """
110
- uv_path = os.getenv("UV_PATH")
111
- uv_executable = str(Path(uv_path) / "uv") if uv_path else "uv"
112
- cmd = []
113
-
114
- if (project_root / "pyproject.toml").exists():
115
- logger.info(f"Found pyproject.toml in {project_root}, installing dependencies.")
116
- cmd = [uv_executable, "pip", "install", ".", "--target", str(target_install_dir)]
117
- elif (project_root / "requirements.txt").exists():
118
- logger.info(f"Found requirements.txt in {project_root}, installing dependencies.")
119
- cmd = [
120
- uv_executable,
121
- "pip",
122
- "install",
123
- "-r",
124
- "requirements.txt",
125
- "--target",
126
- str(target_install_dir),
127
- ]
128
- else:
129
- logger.debug(f"No dependency file found in {project_root}. Skipping dependency installation.")
130
- return
131
-
132
- try:
133
- result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=project_root)
134
- if result.stdout:
135
- logger.info(f"Dependency installation stdout:\n{result.stdout}")
136
- if result.stderr:
137
- logger.warning(f"Dependency installation stderr:\n{result.stderr}")
138
- except subprocess.CalledProcessError as e:
139
- logger.error(f"Dependency installation failed for project at '{project_root}': {e}")
140
- logger.error(f"Command stdout:\n{e.stdout}")
141
- logger.error(f"Command stderr:\n{e.stderr}")
142
- raise RuntimeError(f"Failed to install dependencies for {project_root}") from e
143
-
144
-
145
- def _load_class_from_project_root(project_root: Path, config: AppConfig) -> type[BaseApplication]:
146
- """Internal helper to load an application class from a local project directory."""
147
- logger.debug(f"Attempting to load '{config.name}' from project root: {project_root}")
148
- src_path = project_root / "src"
149
- if not src_path.is_dir():
150
- raise FileNotFoundError(f"Required 'src' directory not found in project at {project_root}")
151
-
152
- install_dependencies_from_path(project_root, UNIVERSAL_MCP_HOME)
153
-
154
- if str(src_path) not in sys.path:
155
- sys.path.insert(0, str(src_path))
156
- logger.debug(f"Added to sys.path: {src_path}")
157
-
158
- module_path_str = get_default_module_path(config.name)
159
- class_name_str = get_default_class_name(config.name)
160
-
161
- try:
162
- module = importlib.import_module(module_path_str)
163
- importlib.reload(module) # Reload to pick up changes
164
- app_class = getattr(module, class_name_str)
165
- return app_class
166
- except (ModuleNotFoundError, AttributeError) as e:
167
- logger.error(f"Failed to load module/class '{module_path_str}.{class_name_str}': {e}")
168
- raise
169
-
170
-
171
- # --- Application Loaders ---
172
-
173
-
174
- def load_app_from_package(config: AppConfig) -> type[BaseApplication]:
175
- """Loads an application from a pip-installable package."""
176
- logger.debug(f"Loading '{config.name}' as a package.")
177
- slug = config.name
178
- repository_path = get_default_repository_path(slug)
179
- package_name = get_default_package_name(slug)
180
- install_or_upgrade_package(package_name, repository_path)
181
-
182
- module_path_str = get_default_module_path(slug)
183
- class_name_str = get_default_class_name(slug)
184
- module = importlib.import_module(module_path_str)
185
- return getattr(module, class_name_str)
186
-
187
-
188
- def load_app_from_local_folder(config: AppConfig) -> type[BaseApplication]:
189
- """Loads an application from a local folder path."""
190
- project_path = Path(config.source_path).resolve()
191
- return _load_class_from_project_root(project_path, config)
192
-
193
-
194
- def load_app_from_remote_zip(config: AppConfig) -> type[BaseApplication]:
195
- """Downloads, caches, and loads an application from a remote .zip file."""
196
- url_hash = hashlib.sha256(config.source_path.encode()).hexdigest()[:16]
197
- project_path = REMOTE_CACHE_DIR / f"{config.name}-{url_hash}"
198
-
199
- if not project_path.exists():
200
- logger.info(f"Downloading remote project for '{config.name}' from {config.source_path}")
201
- project_path.mkdir(parents=True, exist_ok=True)
202
- response = httpx.get(config.source_path, follow_redirects=True, timeout=120)
203
- response.raise_for_status()
204
- with zipfile.ZipFile(io.BytesIO(response.content)) as z:
205
- z.extractall(project_path)
206
- logger.info(f"Extracted remote project to {project_path}")
207
-
208
- return _load_class_from_project_root(project_path, config)
209
-
210
-
211
- def load_app_from_remote_file(config: AppConfig) -> type[BaseApplication]:
212
- """Downloads, caches, and loads an application from a remote Python file."""
213
- logger.debug(f"Loading '{config.name}' as a remote file from {config.source_path}")
214
- url_hash = hashlib.sha256(config.source_path.encode()).hexdigest()[:16]
215
- cached_file_path = REMOTE_CACHE_DIR / f"{config.name}-{url_hash}.py"
216
-
217
- if not cached_file_path.exists():
218
- logger.info(f"Downloading remote file for '{config.name}' from {config.source_path}")
219
- try:
220
- response = httpx.get(config.source_path, follow_redirects=True, timeout=60)
221
- response.raise_for_status()
222
- cached_file_path.write_text(response.text, encoding="utf-8")
223
- logger.info(f"Cached remote file to {cached_file_path}")
224
- except httpx.HTTPStatusError as e:
225
- logger.error(f"Failed to download remote file: {e.response.status_code} {e.response.reason_phrase}")
226
- raise
227
- except Exception as e:
228
- logger.error(f"An unexpected error occurred during download: {e}")
229
- raise
230
-
231
- if not cached_file_path.stat().st_size > 0:
232
- raise ImportError(f"Remote file at {cached_file_path} is empty.")
233
-
234
- module_name = f"remote_app_{config.name}_{url_hash}"
235
- spec = importlib.util.spec_from_file_location(module_name, cached_file_path)
236
- if spec is None or spec.loader is None:
237
- raise ImportError(f"Could not create module spec for {cached_file_path}")
238
-
239
- module = importlib.util.module_from_spec(spec)
240
- sys.modules[module_name] = module
241
- spec.loader.exec_module(module)
242
-
243
- for name, obj in inspect.getmembers(module, inspect.isclass):
244
- if obj.__module__ == module_name and issubclass(obj, BaseApplication) and obj is not BaseApplication:
245
- logger.debug(f"Found application class '{name}' defined in remote file for '{config.name}'.")
246
- return obj
247
-
248
- raise ImportError(f"No class inheriting from BaseApplication found in remote file {config.source_path}")
249
-
250
-
251
- def load_app_from_local_file(config: AppConfig) -> type[BaseApplication]:
252
- """Loads an application from a local Python file."""
253
- logger.debug(f"Loading '{config.name}' as a local file from {config.source_path}")
254
- local_file_path = Path(config.source_path).resolve()
255
-
256
- if not local_file_path.is_file():
257
- raise FileNotFoundError(f"Local file not found at: {local_file_path}")
258
-
259
- if not local_file_path.stat().st_size > 0:
260
- raise ImportError(f"Local file at {local_file_path} is empty.")
261
-
262
- path_hash = hashlib.sha256(str(local_file_path).encode()).hexdigest()[:16]
263
- module_name = f"local_app_{config.name}_{path_hash}"
264
-
265
- spec = importlib.util.spec_from_file_location(module_name, local_file_path)
266
- if spec is None or spec.loader is None:
267
- raise ImportError(f"Could not create module spec for {local_file_path}")
268
-
269
- module = importlib.util.module_from_spec(spec)
270
- sys.modules[module_name] = module
271
- spec.loader.exec_module(module)
272
-
273
- for name, obj in inspect.getmembers(module, inspect.isclass):
274
- if obj.__module__ == module_name and issubclass(obj, BaseApplication) and obj is not BaseApplication:
275
- logger.debug(f"Found application class '{name}' in local file for '{config.name}'.")
276
- return obj
277
-
278
- raise ImportError(f"No class inheriting from BaseApplication found in local file {config.source_path}")