open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636295__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.
- open_swarm-0.1.1748636295.dist-info/METADATA +257 -0
- open_swarm-0.1.1748636295.dist-info/RECORD +89 -0
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +2 -1
- open_swarm-0.1.1748636295.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1748636295.dist-info/top_level.txt +1 -0
- swarm/__init__.py +2 -0
- swarm/agent/agent.py +49 -0
- swarm/auth.py +48 -113
- swarm/consumers.py +0 -19
- swarm/core.py +411 -0
- swarm/extensions/blueprint/__init__.py +16 -30
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/django_utils.py +79 -181
- swarm/extensions/blueprint/interactive_mode.py +72 -67
- swarm/extensions/blueprint/output_utils.py +82 -0
- swarm/{core → extensions/blueprint}/spinner.py +21 -30
- swarm/extensions/cli/cli_args.py +0 -6
- swarm/extensions/cli/commands/blueprint_management.py +9 -47
- swarm/extensions/cli/commands/config_management.py +6 -5
- swarm/extensions/cli/commands/edit_config.py +7 -16
- swarm/extensions/cli/commands/list_blueprints.py +1 -1
- swarm/extensions/cli/commands/validate_env.py +4 -11
- swarm/extensions/cli/commands/validate_envvars.py +6 -6
- swarm/extensions/cli/interactive_shell.py +2 -16
- swarm/extensions/config/config_loader.py +345 -107
- swarm/{core → extensions/config}/config_manager.py +38 -50
- swarm/{core → extensions/config}/server_config.py +0 -32
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_api.py +64 -8
- swarm/extensions/launchers/swarm_cli.py +300 -8
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +32 -0
- swarm/extensions/mcp/mcp_client.py +233 -0
- swarm/extensions/mcp/mcp_tool_provider.py +135 -0
- swarm/extensions/mcp/mcp_utils.py +260 -0
- swarm/llm/chat_completion.py +166 -0
- swarm/serializers.py +5 -96
- swarm/settings.py +133 -85
- swarm/types.py +91 -0
- swarm/urls.py +74 -57
- swarm/utils/context_utils.py +4 -10
- swarm/utils/general_utils.py +0 -21
- swarm/utils/redact.py +36 -23
- swarm/views/api_views.py +39 -48
- swarm/views/chat_views.py +76 -236
- swarm/views/core_views.py +87 -80
- swarm/views/model_views.py +121 -64
- swarm/views/utils.py +439 -65
- swarm/views/web_views.py +2 -2
- open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
- open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
- open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
- swarm/blueprints/README.md +0 -68
- swarm/blueprints/blueprint_audit_status.json +0 -27
- swarm/blueprints/chatbot/README.md +0 -40
- swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
- swarm/blueprints/chatbot/metadata.json +0 -23
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
- swarm/blueprints/chucks_angels/README.md +0 -11
- swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
- swarm/blueprints/chucks_angels/test_basic.py +0 -3
- swarm/blueprints/codey/CODEY.md +0 -15
- swarm/blueprints/codey/README.md +0 -115
- swarm/blueprints/codey/blueprint_codey.py +0 -1072
- swarm/blueprints/codey/codey_cli.py +0 -373
- swarm/blueprints/codey/instructions.md +0 -17
- swarm/blueprints/codey/metadata.json +0 -23
- swarm/blueprints/common/operation_box_utils.py +0 -83
- swarm/blueprints/digitalbutlers/README.md +0 -11
- swarm/blueprints/digitalbutlers/__init__.py +0 -1
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
- swarm/blueprints/digitalbutlers/test_basic.py +0 -3
- swarm/blueprints/divine_code/README.md +0 -3
- swarm/blueprints/divine_code/__init__.py +0 -10
- swarm/blueprints/divine_code/apps.py +0 -11
- swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
- swarm/blueprints/django_chat/apps.py +0 -6
- swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
- swarm/blueprints/django_chat/urls.py +0 -8
- swarm/blueprints/django_chat/views.py +0 -32
- swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
- swarm/blueprints/flock/README.md +0 -11
- swarm/blueprints/flock/__init__.py +0 -8
- swarm/blueprints/flock/blueprint_flock.py +0 -7
- swarm/blueprints/flock/test_basic.py +0 -3
- swarm/blueprints/geese/README.md +0 -10
- swarm/blueprints/geese/__init__.py +0 -8
- swarm/blueprints/geese/blueprint_geese.py +0 -384
- swarm/blueprints/geese/geese_cli.py +0 -102
- swarm/blueprints/jeeves/README.md +0 -41
- swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
- swarm/blueprints/jeeves/jeeves_cli.py +0 -55
- swarm/blueprints/jeeves/metadata.json +0 -24
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
- swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
- swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
- swarm/blueprints/poets/blueprint_poets.py +0 -546
- swarm/blueprints/poets/poets_cli.py +0 -23
- swarm/blueprints/rue_code/README.md +0 -8
- swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
- swarm/blueprints/rue_code/rue_code_cli.py +0 -43
- swarm/blueprints/stewie/apps.py +0 -12
- swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
- swarm/blueprints/stewie/models.py +0 -19
- swarm/blueprints/stewie/serializers.py +0 -10
- swarm/blueprints/stewie/settings.py +0 -17
- swarm/blueprints/stewie/urls.py +0 -11
- swarm/blueprints/stewie/views.py +0 -26
- swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
- swarm/blueprints/whinge_surf/README.md +0 -22
- swarm/blueprints/whinge_surf/__init__.py +0 -1
- swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
- swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
- swarm/blueprints/zeus/__init__.py +0 -2
- swarm/blueprints/zeus/apps.py +0 -4
- swarm/blueprints/zeus/blueprint_zeus.py +0 -270
- swarm/blueprints/zeus/zeus_cli.py +0 -13
- swarm/cli/async_input.py +0 -65
- swarm/cli/async_input_demo.py +0 -32
- swarm/core/agent_utils.py +0 -21
- swarm/core/blueprint_base.py +0 -769
- swarm/core/blueprint_discovery.py +0 -125
- swarm/core/blueprint_runner.py +0 -59
- swarm/core/blueprint_ux.py +0 -109
- swarm/core/build_launchers.py +0 -15
- swarm/core/cli/__init__.py +0 -1
- swarm/core/cli/commands/__init__.py +0 -1
- swarm/core/cli/commands/blueprint_management.py +0 -7
- swarm/core/cli/interactive_shell.py +0 -14
- swarm/core/cli/main.py +0 -50
- swarm/core/cli/utils/__init__.py +0 -1
- swarm/core/cli/utils/discover_commands.py +0 -18
- swarm/core/config_loader.py +0 -122
- swarm/core/output_utils.py +0 -193
- swarm/core/session_logger.py +0 -42
- swarm/core/slash_commands.py +0 -89
- swarm/core/swarm_api.py +0 -68
- swarm/core/swarm_cli.py +0 -216
- swarm/core/utils/__init__.py +0 -0
- swarm/extensions/blueprint/cli_handler.py +0 -197
- swarm/extensions/blueprint/runnable_blueprint.py +0 -42
- swarm/extensions/cli/utils/__init__.py +0 -1
- swarm/extensions/cli/utils/async_input.py +0 -46
- swarm/extensions/cli/utils/prompt_user.py +0 -3
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +0 -58
- swarm/middleware.py +0 -65
- swarm/permissions.py +0 -38
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
- swarm/static/contrib/markedjs/marked.min.js +0 -6
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
- swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
- swarm/static/contrib/tabler-icons/archive.svg +0 -21
- swarm/static/contrib/tabler-icons/artboard.svg +0 -27
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
- swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
- swarm/static/contrib/tabler-icons/carambola.svg +0 -19
- swarm/static/contrib/tabler-icons/copy.svg +0 -20
- swarm/static/contrib/tabler-icons/download.svg +0 -21
- swarm/static/contrib/tabler-icons/edit.svg +0 -21
- swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
- swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
- swarm/static/contrib/tabler-icons/headset.svg +0 -22
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
- swarm/static/contrib/tabler-icons/message-star.svg +0 -22
- swarm/static/contrib/tabler-icons/message-x.svg +0 -23
- swarm/static/contrib/tabler-icons/message.svg +0 -21
- swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
- swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
- swarm/static/contrib/tabler-icons/robot.svg +0 -26
- swarm/static/contrib/tabler-icons/search.svg +0 -19
- swarm/static/contrib/tabler-icons/settings.svg +0 -20
- swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
- swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
- swarm/static/css/dropdown.css +0 -22
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +0 -23
- swarm/static/rest_mode/css/base.css +0 -470
- swarm/static/rest_mode/css/chat-history.css +0 -286
- swarm/static/rest_mode/css/chat.css +0 -251
- swarm/static/rest_mode/css/chatbot.css +0 -74
- swarm/static/rest_mode/css/chatgpt.css +0 -62
- swarm/static/rest_mode/css/colors/corporate.css +0 -74
- swarm/static/rest_mode/css/colors/pastel.css +0 -81
- swarm/static/rest_mode/css/colors/tropical.css +0 -82
- swarm/static/rest_mode/css/general.css +0 -142
- swarm/static/rest_mode/css/layout.css +0 -167
- swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
- swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
- swarm/static/rest_mode/css/messages.css +0 -84
- swarm/static/rest_mode/css/messenger.css +0 -135
- swarm/static/rest_mode/css/settings.css +0 -91
- swarm/static/rest_mode/css/simple.css +0 -44
- swarm/static/rest_mode/css/slack.css +0 -58
- swarm/static/rest_mode/css/style.css +0 -156
- swarm/static/rest_mode/css/theme.css +0 -30
- swarm/static/rest_mode/css/toast.css +0 -40
- swarm/static/rest_mode/js/auth.js +0 -9
- swarm/static/rest_mode/js/blueprint.js +0 -41
- swarm/static/rest_mode/js/blueprintUtils.js +0 -12
- swarm/static/rest_mode/js/chatLogic.js +0 -79
- swarm/static/rest_mode/js/debug.js +0 -63
- swarm/static/rest_mode/js/events.js +0 -98
- swarm/static/rest_mode/js/main.js +0 -19
- swarm/static/rest_mode/js/messages.js +0 -264
- swarm/static/rest_mode/js/messengerLogic.js +0 -355
- swarm/static/rest_mode/js/modules/apiService.js +0 -84
- swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
- swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
- swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
- swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
- swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
- swarm/static/rest_mode/js/modules/state.js +0 -7
- swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
- swarm/static/rest_mode/js/modules/validation.js +0 -23
- swarm/static/rest_mode/js/rendering.js +0 -119
- swarm/static/rest_mode/js/settings.js +0 -130
- swarm/static/rest_mode/js/sidebar.js +0 -94
- swarm/static/rest_mode/js/simpleLogic.js +0 -37
- swarm/static/rest_mode/js/slackLogic.js +0 -66
- swarm/static/rest_mode/js/splash.js +0 -76
- swarm/static/rest_mode/js/theme.js +0 -111
- swarm/static/rest_mode/js/toast.js +0 -36
- swarm/static/rest_mode/js/ui.js +0 -265
- swarm/static/rest_mode/js/validation.js +0 -57
- swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
- swarm/static/rest_mode/svg/arrow_down.svg +0 -5
- swarm/static/rest_mode/svg/arrow_left.svg +0 -5
- swarm/static/rest_mode/svg/arrow_right.svg +0 -5
- swarm/static/rest_mode/svg/arrow_up.svg +0 -5
- swarm/static/rest_mode/svg/attach.svg +0 -8
- swarm/static/rest_mode/svg/avatar.svg +0 -7
- swarm/static/rest_mode/svg/canvas.svg +0 -6
- swarm/static/rest_mode/svg/chat_history.svg +0 -4
- swarm/static/rest_mode/svg/close.svg +0 -5
- swarm/static/rest_mode/svg/copy.svg +0 -4
- swarm/static/rest_mode/svg/dark_mode.svg +0 -3
- swarm/static/rest_mode/svg/edit.svg +0 -5
- swarm/static/rest_mode/svg/layout.svg +0 -9
- swarm/static/rest_mode/svg/logo.svg +0 -29
- swarm/static/rest_mode/svg/logout.svg +0 -5
- swarm/static/rest_mode/svg/mobile.svg +0 -5
- swarm/static/rest_mode/svg/new_chat.svg +0 -4
- swarm/static/rest_mode/svg/not_visible.svg +0 -5
- swarm/static/rest_mode/svg/plus.svg +0 -7
- swarm/static/rest_mode/svg/run_code.svg +0 -6
- swarm/static/rest_mode/svg/save.svg +0 -4
- swarm/static/rest_mode/svg/search.svg +0 -6
- swarm/static/rest_mode/svg/settings.svg +0 -4
- swarm/static/rest_mode/svg/speaker.svg +0 -5
- swarm/static/rest_mode/svg/stop.svg +0 -6
- swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
- swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
- swarm/static/rest_mode/svg/toggle_off.svg +0 -6
- swarm/static/rest_mode/svg/toggle_on.svg +0 -6
- swarm/static/rest_mode/svg/trash.svg +0 -10
- swarm/static/rest_mode/svg/undo.svg +0 -3
- swarm/static/rest_mode/svg/visible.svg +0 -8
- swarm/static/rest_mode/svg/voice.svg +0 -10
- swarm/templates/account/login.html +0 -22
- swarm/templates/account/signup.html +0 -32
- swarm/templates/base.html +0 -30
- swarm/templates/chat.html +0 -43
- swarm/templates/index.html +0 -35
- swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
- swarm/templates/rest_mode/components/header.html +0 -45
- swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
- swarm/templates/rest_mode/components/settings_dialog.html +0 -97
- swarm/templates/rest_mode/components/splash_screen.html +0 -7
- swarm/templates/rest_mode/components/top_bar.html +0 -28
- swarm/templates/rest_mode/message_ui.html +0 -50
- swarm/templates/rest_mode/slackbot.html +0 -30
- swarm/templates/simple_blueprint_page.html +0 -24
- swarm/templates/websocket_partials/final_system_message.html +0 -3
- swarm/templates/websocket_partials/system_message.html +0 -4
- swarm/templates/websocket_partials/user_message.html +0 -5
- swarm/utils/ansi_box.py +0 -34
- swarm/utils/disable_tracing.py +0 -38
- swarm/utils/log_utils.py +0 -63
- swarm/utils/openai_patch.py +0 -33
- swarm/ux/ansi_box.py +0 -43
- swarm/ux/spinner.py +0 -53
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
- /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
- /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
- /swarm/{core → extensions/config}/setup_wizard.py +0 -0
- /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
- /swarm/{core → extensions/config}/utils/logger.py +0 -0
- /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
"""
|
2
|
+
Blueprint Discovery Module for Open Swarm MCP.
|
3
|
+
|
4
|
+
This module dynamically discovers and imports blueprints from specified directories.
|
5
|
+
It identifies classes derived from BlueprintBase as valid blueprints and extracts their metadata.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import importlib.util
|
9
|
+
import inspect
|
10
|
+
import logging
|
11
|
+
import os
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Dict, List, Any
|
14
|
+
from swarm.settings import DEBUG
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
18
|
+
|
19
|
+
try:
|
20
|
+
from .blueprint_base import BlueprintBase
|
21
|
+
except ImportError as e:
|
22
|
+
logger.critical(f"Failed to import BlueprintBase: {e}")
|
23
|
+
raise
|
24
|
+
|
25
|
+
def discover_blueprints(directories: List[str]) -> Dict[str, Dict[str, Any]]:
|
26
|
+
"""
|
27
|
+
Discover and load blueprints from specified directories.
|
28
|
+
Extract metadata including title, description, and other attributes.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
directories (List[str]): List of directories to search for blueprints.
|
32
|
+
|
33
|
+
Returns:
|
34
|
+
Dict[str, Dict[str, Any]]: Dictionary containing blueprint metadata.
|
35
|
+
"""
|
36
|
+
blueprints = {}
|
37
|
+
logger.info("Starting blueprint discovery.")
|
38
|
+
swarm_blueprints = os.getenv("SWARM_BLUEPRINTS", "").split(",")
|
39
|
+
if swarm_blueprints and swarm_blueprints[0]:
|
40
|
+
logger.debug(f"Filtering blueprints to: {swarm_blueprints}")
|
41
|
+
|
42
|
+
for directory in directories:
|
43
|
+
logger.debug(f"Searching for blueprints in: {directory}")
|
44
|
+
dir_path = Path(directory)
|
45
|
+
|
46
|
+
if not dir_path.exists() or not dir_path.is_dir():
|
47
|
+
logger.warning(f"Invalid directory: {directory}. Skipping...")
|
48
|
+
continue
|
49
|
+
|
50
|
+
for blueprint_file in dir_path.rglob("blueprint_*.py"):
|
51
|
+
module_name = blueprint_file.stem
|
52
|
+
blueprint_name = module_name.replace("blueprint_", "")
|
53
|
+
if swarm_blueprints and swarm_blueprints[0] and blueprint_name not in swarm_blueprints:
|
54
|
+
logger.debug(f"Skipping blueprint '{blueprint_name}' not in SWARM_BLUEPRINTS")
|
55
|
+
continue
|
56
|
+
module_path = str(blueprint_file.parent)
|
57
|
+
|
58
|
+
logger.debug(f"Found blueprint file: {blueprint_file}")
|
59
|
+
logger.debug(f"Module name: {module_name}, Blueprint name: {blueprint_name}, Module path: {module_path}")
|
60
|
+
|
61
|
+
try:
|
62
|
+
spec = importlib.util.spec_from_file_location(module_name, str(blueprint_file))
|
63
|
+
if spec is None or spec.loader is None:
|
64
|
+
logger.error(f"Cannot load module spec for blueprint file: {blueprint_file}. Skipping.")
|
65
|
+
continue
|
66
|
+
module = importlib.util.module_from_spec(spec)
|
67
|
+
spec.loader.exec_module(module)
|
68
|
+
logger.debug(f"Successfully imported module: {module_name}")
|
69
|
+
|
70
|
+
for name, obj in inspect.getmembers(module, inspect.isclass):
|
71
|
+
if not issubclass(obj, BlueprintBase) or obj is BlueprintBase:
|
72
|
+
continue
|
73
|
+
|
74
|
+
logger.debug(f"Discovered blueprint class: {name}")
|
75
|
+
|
76
|
+
try:
|
77
|
+
metadata = obj.metadata
|
78
|
+
if callable(metadata):
|
79
|
+
metadata = metadata()
|
80
|
+
elif isinstance(metadata, property):
|
81
|
+
if metadata.fget is not None:
|
82
|
+
metadata = metadata.fget(obj)
|
83
|
+
else:
|
84
|
+
logger.error(f"Blueprint '{blueprint_name}' property 'metadata' has no getter.")
|
85
|
+
raise ValueError(f"Blueprint '{blueprint_name}' metadata is inaccessible.")
|
86
|
+
|
87
|
+
if not isinstance(metadata, dict):
|
88
|
+
logger.error(f"Metadata for blueprint '{blueprint_name}' is not a dictionary.")
|
89
|
+
raise ValueError(f"Metadata for blueprint '{blueprint_name}' is invalid or inaccessible.")
|
90
|
+
|
91
|
+
if "title" not in metadata or "description" not in metadata:
|
92
|
+
logger.error(f"Required metadata fields (title, description) are missing for blueprint '{blueprint_name}'.")
|
93
|
+
raise ValueError(f"Metadata for blueprint '{blueprint_name}' is invalid or inaccessible.")
|
94
|
+
|
95
|
+
except Exception as e:
|
96
|
+
logger.error(f"Error retrieving metadata for blueprint '{blueprint_name}': {e}")
|
97
|
+
continue
|
98
|
+
|
99
|
+
blueprints[blueprint_name] = {
|
100
|
+
"blueprint_class": obj,
|
101
|
+
"title": metadata["title"],
|
102
|
+
"description": metadata["description"],
|
103
|
+
}
|
104
|
+
logger.debug(f"Added blueprint '{blueprint_name}' with metadata: {metadata}")
|
105
|
+
except ImportError as e:
|
106
|
+
logger.error(f"Failed to import module '{module_name}': {e}")
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"Unexpected error importing '{module_name}': {e}", exc_info=True)
|
109
|
+
|
110
|
+
logger.info("Blueprint discovery complete.")
|
111
|
+
logger.debug(f"Discovered blueprints: {list(blueprints.keys())}")
|
112
|
+
return blueprints
|
@@ -5,13 +5,10 @@ Django integration utilities for blueprint extensions.
|
|
5
5
|
import logging
|
6
6
|
import os
|
7
7
|
import importlib.util
|
8
|
-
import sys # Import sys
|
9
|
-
import inspect # Import inspect
|
10
8
|
from typing import Any, TYPE_CHECKING
|
11
9
|
from django.conf import settings # Import settings directly
|
12
10
|
# Import necessary URL handling functions
|
13
|
-
from django.urls import clear_url_caches, get_resolver, get_urlconf, set_urlconf, URLPattern, URLResolver
|
14
|
-
from django.utils.module_loading import import_module # More standard way to import
|
11
|
+
from django.urls import clear_url_caches, get_resolver, get_urlconf, set_urlconf, URLPattern, URLResolver
|
15
12
|
from collections import OrderedDict
|
16
13
|
from django.apps import apps as django_apps
|
17
14
|
|
@@ -23,7 +20,6 @@ logger = logging.getLogger(__name__)
|
|
23
20
|
|
24
21
|
def register_django_components(blueprint: 'BlueprintBase') -> None:
|
25
22
|
"""Register Django settings and URLs if applicable for the given blueprint."""
|
26
|
-
# Use getattr to safely check _urls_registered, default to False if not present
|
27
23
|
if blueprint.skip_django_registration or getattr(blueprint, "_urls_registered", False):
|
28
24
|
logger.debug(f"Skipping Django registration for {blueprint.__class__.__name__}: Skipped by flag or already registered.")
|
29
25
|
return
|
@@ -33,121 +29,52 @@ def register_django_components(blueprint: 'BlueprintBase') -> None:
|
|
33
29
|
|
34
30
|
try:
|
35
31
|
# App readiness check less critical now if called within test fixtures after setup
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# logger.debug("Django apps not ready; registration likely handled by AppConfig.ready().")
|
40
|
-
# return
|
32
|
+
if not django_apps.ready and not getattr(settings, 'TESTING', False):
|
33
|
+
logger.debug("Django apps not ready; registration likely handled by AppConfig.ready().")
|
34
|
+
return
|
41
35
|
|
42
36
|
_load_local_settings(blueprint)
|
43
|
-
_merge_installed_apps(blueprint) #
|
37
|
+
_merge_installed_apps(blueprint) # Still attempt, might need restart/reload
|
44
38
|
|
45
39
|
if hasattr(blueprint, 'register_blueprint_urls') and callable(blueprint.register_blueprint_urls):
|
46
40
|
logger.debug(f"Calling blueprint-specific register_blueprint_urls for {blueprint.__class__.__name__}")
|
47
41
|
blueprint.register_blueprint_urls()
|
48
|
-
|
49
|
-
# blueprint._urls_registered = True # Let the custom function handle this flag
|
42
|
+
blueprint._urls_registered = True
|
50
43
|
else:
|
51
44
|
logger.debug(f"Using generic URL registration for {blueprint.__class__.__name__}")
|
52
45
|
_register_blueprint_urls_generic(blueprint)
|
53
46
|
|
54
|
-
except ImportError
|
55
|
-
|
56
|
-
logger.warning(f"Django not fully available; skipping Django component registration for {blueprint.__class__.__name__}. Error: {e}")
|
47
|
+
except ImportError:
|
48
|
+
logger.warning("Django not available; skipping Django component registration.")
|
57
49
|
except Exception as e:
|
58
50
|
logger.error(f"Failed to register Django components for {blueprint.__class__.__name__}: {e}", exc_info=True)
|
59
51
|
|
60
|
-
|
61
52
|
def _load_local_settings(blueprint: 'BlueprintBase') -> None:
|
62
|
-
"""Load local settings.py from the blueprint's directory if it exists.
|
63
|
-
Handles being called when the blueprint module is '__main__'.
|
64
|
-
"""
|
65
|
-
local_settings_path = None
|
66
|
-
settings_module_name = None # A unique name for the loaded settings module
|
67
|
-
|
53
|
+
"""Load local settings.py from the blueprint's directory if it exists."""
|
68
54
|
try:
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
# Using a prefix to avoid potential clashes with real modules
|
81
|
-
settings_module_name = f"_swarm_local_settings_{blueprint.__class__.__name__}"
|
82
|
-
logger.debug(f"Derived potential local settings path for __main__: {local_settings_path}")
|
83
|
-
except TypeError as e:
|
84
|
-
logger.error(f"Could not determine file path for blueprint class {blueprint.__class__.__name__} when run as __main__: {e}")
|
85
|
-
setattr(blueprint, 'local_settings', None) # Ensure attribute exists
|
86
|
-
return
|
87
|
-
except Exception as e:
|
88
|
-
logger.error(f"Unexpected error getting blueprint file path for __main__: {e}", exc_info=True)
|
89
|
-
setattr(blueprint, 'local_settings', None)
|
90
|
-
return
|
91
|
-
else:
|
92
|
-
# --- Handling standard import execution ---
|
93
|
-
logger.debug(f"Blueprint class module is '{module_name}'. Using importlib.")
|
94
|
-
try:
|
95
|
-
module_spec = importlib.util.find_spec(module_name)
|
96
|
-
if module_spec and module_spec.origin:
|
97
|
-
blueprint_dir = os.path.dirname(module_spec.origin)
|
98
|
-
local_settings_path = os.path.join(blueprint_dir, "settings.py")
|
99
|
-
# Use a name relative to the original module to avoid clashes
|
100
|
-
settings_module_name = f"{module_name}.local_settings"
|
101
|
-
logger.debug(f"Derived potential local settings path via importlib: {local_settings_path}")
|
55
|
+
module_spec = importlib.util.find_spec(blueprint.__class__.__module__)
|
56
|
+
if module_spec and module_spec.origin:
|
57
|
+
blueprint_dir = os.path.dirname(module_spec.origin)
|
58
|
+
local_settings_path = os.path.join(blueprint_dir, "settings.py")
|
59
|
+
if os.path.isfile(local_settings_path):
|
60
|
+
spec = importlib.util.spec_from_file_location(f"{blueprint.__class__.__module__}.local_settings", local_settings_path)
|
61
|
+
if spec and spec.loader:
|
62
|
+
local_settings = importlib.util.module_from_spec(spec)
|
63
|
+
blueprint.local_settings = local_settings
|
64
|
+
spec.loader.exec_module(local_settings)
|
65
|
+
logger.debug(f"Loaded local settings from {local_settings_path} for {blueprint.__class__.__name__}")
|
102
66
|
else:
|
103
|
-
logger.
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
logger.error(f"Error finding spec for module '{module_name}': {e}", exc_info=True)
|
108
|
-
setattr(blueprint, 'local_settings', None)
|
109
|
-
return
|
110
|
-
|
111
|
-
# --- Common Loading Logic ---
|
112
|
-
if local_settings_path and os.path.isfile(local_settings_path):
|
113
|
-
# Check if already loaded (using the determined name)
|
114
|
-
if settings_module_name in sys.modules:
|
115
|
-
logger.debug(f"Local settings module '{settings_module_name}' already loaded. Assigning.")
|
116
|
-
blueprint.local_settings = sys.modules[settings_module_name]
|
117
|
-
# Optionally, re-apply settings if your local_settings has an apply function
|
118
|
-
# if hasattr(blueprint.local_settings, 'apply_settings'):
|
119
|
-
# blueprint.local_settings.apply_settings()
|
120
|
-
return
|
121
|
-
|
122
|
-
spec = importlib.util.spec_from_file_location(settings_module_name, local_settings_path)
|
123
|
-
if spec and spec.loader:
|
124
|
-
local_settings = importlib.util.module_from_spec(spec)
|
125
|
-
# Add to sys.modules BEFORE execution to handle potential internal imports
|
126
|
-
sys.modules[settings_module_name] = local_settings
|
127
|
-
blueprint.local_settings = local_settings # Assign early
|
128
|
-
logger.info(f"Loading and executing local settings from '{local_settings_path}' as '{settings_module_name}' for '{blueprint.__class__.__name__}'.")
|
129
|
-
spec.loader.exec_module(local_settings)
|
130
|
-
logger.debug(f"Finished executing local settings module '{settings_module_name}'.")
|
131
|
-
else:
|
132
|
-
logger.warning(f"Could not create module spec/loader for local settings at '{local_settings_path}'")
|
133
|
-
setattr(blueprint, 'local_settings', None)
|
134
|
-
else:
|
135
|
-
logger.debug(f"No local settings file found at '{local_settings_path}' for {blueprint.__class__.__name__}.")
|
136
|
-
setattr(blueprint, 'local_settings', None)
|
137
|
-
|
67
|
+
logger.warning(f"Could not create module spec for local settings at {local_settings_path}")
|
68
|
+
blueprint.local_settings = None
|
69
|
+
else: blueprint.local_settings = None
|
70
|
+
else: blueprint.local_settings = None
|
138
71
|
except Exception as e:
|
139
72
|
logger.error(f"Error loading local settings for {blueprint.__class__.__name__}: {e}", exc_info=True)
|
140
|
-
|
141
|
-
if isinstance(e, ValueError) and "__spec__ is None" in str(e):
|
142
|
-
logger.critical("Original Error Context: Failed during importlib processing, likely due to __main__ module details.")
|
143
|
-
setattr(blueprint, 'local_settings', None) # Ensure attribute exists even on error
|
73
|
+
blueprint.local_settings = None
|
144
74
|
|
145
75
|
|
146
76
|
def _merge_installed_apps(blueprint: 'BlueprintBase') -> None:
|
147
|
-
"""Merge INSTALLED_APPS from blueprint's local settings into main Django settings.
|
148
|
-
Note: This might require a server restart/reload to take full effect.
|
149
|
-
"""
|
150
|
-
# Check if local_settings was successfully loaded and has INSTALLED_APPS
|
77
|
+
"""Merge INSTALLED_APPS from blueprint's local settings into main Django settings."""
|
151
78
|
if hasattr(blueprint, "local_settings") and blueprint.local_settings and hasattr(blueprint.local_settings, "INSTALLED_APPS"):
|
152
79
|
try:
|
153
80
|
blueprint_apps = getattr(blueprint.local_settings, "INSTALLED_APPS", [])
|
@@ -155,147 +82,118 @@ def _merge_installed_apps(blueprint: 'BlueprintBase') -> None:
|
|
155
82
|
logger.warning(f"Blueprint {blueprint.__class__.__name__}'s local INSTALLED_APPS is not a list or tuple.")
|
156
83
|
return
|
157
84
|
|
158
|
-
|
159
|
-
if not hasattr(settings, 'INSTALLED_APPS'):
|
160
|
-
logger.error("Cannot merge apps: django.conf.settings.INSTALLED_APPS is not defined.")
|
161
|
-
return
|
85
|
+
apps_added = False
|
162
86
|
if isinstance(settings.INSTALLED_APPS, tuple):
|
163
|
-
|
164
|
-
elif not isinstance(settings.INSTALLED_APPS, list):
|
165
|
-
logger.error(f"Cannot merge apps: django.conf.settings.INSTALLED_APPS is not a list or tuple (type: {type(settings.INSTALLED_APPS)}).")
|
166
|
-
return
|
87
|
+
settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
|
167
88
|
|
168
|
-
apps_added_names = []
|
169
89
|
for app in blueprint_apps:
|
170
90
|
if app not in settings.INSTALLED_APPS:
|
171
|
-
settings.INSTALLED_APPS.append(app)
|
172
|
-
|
173
|
-
logger.debug(f"Added app '{app}' from blueprint {blueprint.__class__.__name__} to INSTALLED_APPS
|
174
|
-
|
175
|
-
if
|
176
|
-
logger.info(f"Merged INSTALLED_APPS from blueprint {blueprint.__class__.__name__}
|
91
|
+
settings.INSTALLED_APPS.append(app)
|
92
|
+
apps_added = True
|
93
|
+
logger.debug(f"Added app '{app}' from blueprint {blueprint.__class__.__name__} to INSTALLED_APPS.")
|
94
|
+
|
95
|
+
if apps_added:
|
96
|
+
logger.info(f"Merged INSTALLED_APPS from blueprint {blueprint.__class__.__name__}. App registry reload might be needed.")
|
97
|
+
# Attempt app registry reload - Use with caution!
|
98
|
+
try:
|
99
|
+
logger.debug("Attempting to reload Django app registry...")
|
100
|
+
django_apps.app_configs = OrderedDict()
|
101
|
+
django_apps.ready = False
|
102
|
+
django_apps.clear_cache()
|
103
|
+
django_apps.populate(settings.INSTALLED_APPS)
|
104
|
+
logger.info("Successfully reloaded Django app registry.")
|
105
|
+
except RuntimeError as e:
|
106
|
+
logger.error(f"Could not reload app registry (likely reentrant call): {e}")
|
107
|
+
except Exception as e:
|
108
|
+
logger.error(f"Error reloading Django app registry: {e}", exc_info=True)
|
177
109
|
|
178
|
-
# Attempt app registry reload - Use with extreme caution! Can lead to instability.
|
179
|
-
# It's generally safer to rely on server restart/reload mechanisms.
|
180
|
-
if getattr(settings, 'AUTO_RELOAD_APP_REGISTRY', False): # Add a setting to control this
|
181
|
-
try:
|
182
|
-
logger.warning("Attempting to dynamically reload Django app registry (Experimental)...")
|
183
|
-
django_apps.app_configs = OrderedDict()
|
184
|
-
django_apps.ready = False
|
185
|
-
django_apps.clear_cache()
|
186
|
-
django_apps.populate(settings.INSTALLED_APPS)
|
187
|
-
logger.info("Successfully reloaded Django app registry.")
|
188
|
-
except RuntimeError as e:
|
189
|
-
logger.error(f"Could not reload app registry (likely reentrant call): {e}")
|
190
|
-
except Exception as e:
|
191
|
-
logger.error(f"Error reloading Django app registry: {e}", exc_info=True)
|
192
|
-
else:
|
193
|
-
logger.debug("Automatic app registry reload is disabled (settings.AUTO_RELOAD_APP_REGISTRY=False).")
|
194
110
|
|
195
111
|
except ImportError:
|
196
|
-
|
197
|
-
logger.error("Could not import or access django.conf.settings to merge INSTALLED_APPS.")
|
112
|
+
logger.error("Could not import django.conf.settings to merge INSTALLED_APPS.")
|
198
113
|
except Exception as e:
|
199
114
|
logger.error(f"Error merging INSTALLED_APPS for {blueprint.__class__.__name__}: {e}", exc_info=True)
|
200
|
-
else:
|
201
|
-
logger.debug(f"No local settings or INSTALLED_APPS found for blueprint {blueprint.__class__.__name__} to merge.")
|
202
|
-
|
203
115
|
|
204
116
|
def _register_blueprint_urls_generic(blueprint: 'BlueprintBase') -> None:
|
205
|
-
"""Generic function to register blueprint URLs based on metadata.
|
206
|
-
Dynamically adds patterns to the root urlconf's urlpatterns list.
|
207
|
-
"""
|
208
|
-
# Check if already done for this blueprint instance
|
117
|
+
"""Generic function to register blueprint URLs based on metadata."""
|
209
118
|
if getattr(blueprint, "_urls_registered", False):
|
210
|
-
logger.debug(f"URLs for {blueprint.__class__.__name__} already
|
119
|
+
logger.debug(f"URLs for {blueprint.__class__.__name__} already registered.")
|
211
120
|
return
|
212
121
|
|
213
|
-
|
214
|
-
|
215
|
-
if not isinstance(metadata, dict):
|
216
|
-
logger.warning(f"Blueprint {blueprint.__class__.__name__} metadata is not a dictionary. Skipping URL registration.")
|
217
|
-
return
|
218
|
-
django_modules = metadata.get("django_modules", {})
|
219
|
-
module_path = django_modules.get("urls")
|
220
|
-
url_prefix = metadata.get("url_prefix", "")
|
122
|
+
module_path = blueprint.metadata.get("django_modules", {}).get("urls")
|
123
|
+
url_prefix = blueprint.metadata.get("url_prefix", "")
|
221
124
|
|
222
125
|
if not module_path:
|
223
126
|
logger.debug(f"No 'urls' module specified in metadata for {blueprint.__class__.__name__}; skipping generic URL registration.")
|
224
127
|
return
|
225
128
|
|
226
129
|
try:
|
130
|
+
from django.urls import include, path
|
131
|
+
from importlib import import_module
|
132
|
+
|
227
133
|
root_urlconf_name = settings.ROOT_URLCONF
|
228
134
|
if not root_urlconf_name:
|
229
|
-
logger.error("settings.ROOT_URLCONF is not set.
|
135
|
+
logger.error("settings.ROOT_URLCONF is not set.")
|
230
136
|
return
|
231
137
|
|
232
|
-
# --- Get the root urlpatterns list
|
138
|
+
# --- Get the root urlpatterns list directly ---
|
139
|
+
# This is potentially fragile if ROOT_URLCONF itself changes, but necessary for tests
|
233
140
|
try:
|
234
|
-
# Use Django's utility function for importing
|
235
141
|
root_urlconf_module = import_module(root_urlconf_name)
|
236
142
|
if not hasattr(root_urlconf_module, 'urlpatterns') or not isinstance(root_urlconf_module.urlpatterns, list):
|
237
|
-
logger.error(f"Cannot modify urlpatterns in '{root_urlconf_name}'. '
|
143
|
+
logger.error(f"Cannot modify urlpatterns in '{root_urlconf_name}'. It's missing or not a list.")
|
238
144
|
return
|
239
145
|
root_urlpatterns = root_urlconf_module.urlpatterns
|
240
146
|
except ImportError:
|
241
147
|
logger.error(f"Could not import main URLconf '{root_urlconf_name}' to modify urlpatterns.")
|
242
148
|
return
|
243
|
-
except Exception as e:
|
244
|
-
logger.error(f"Error accessing urlpatterns in '{root_urlconf_name}': {e}", exc_info=True)
|
245
|
-
return
|
246
149
|
|
247
150
|
# Import the blueprint's URL module
|
248
151
|
try:
|
249
152
|
urls_module = import_module(module_path)
|
250
153
|
if not hasattr(urls_module, "urlpatterns"):
|
251
|
-
logger.debug(f"Blueprint URL module '{module_path}' has no 'urlpatterns'.
|
252
|
-
# Mark as registered even if no patterns, to avoid re-attempting
|
154
|
+
logger.debug(f"Blueprint URL module '{module_path}' has no 'urlpatterns'.")
|
253
155
|
blueprint._urls_registered = True
|
254
156
|
return
|
255
157
|
except ImportError:
|
256
158
|
logger.error(f"Could not import blueprint URL module: '{module_path}'")
|
257
159
|
return
|
258
|
-
except Exception as e:
|
259
|
-
logger.error(f"Error importing or accessing urlpatterns in '{module_path}': {e}", exc_info=True)
|
260
|
-
return
|
261
160
|
|
262
|
-
# Prepare the new pattern
|
263
161
|
if url_prefix and not url_prefix.endswith('/'): url_prefix += '/'
|
264
|
-
|
265
|
-
|
266
|
-
# Include the module directly for `include` to find its `urlpatterns`
|
267
|
-
new_pattern = path(url_prefix, include((urls_module, app_name))) # Pass tuple (module, app_name)
|
162
|
+
app_name = blueprint.metadata.get("cli_name", blueprint.__class__.__name__.lower())
|
163
|
+
new_pattern = path(url_prefix, include((urls_module, app_name)))
|
268
164
|
|
269
|
-
# Check if an identical pattern
|
165
|
+
# Check if an identical pattern already exists
|
270
166
|
already_exists = False
|
271
167
|
for existing_pattern in root_urlpatterns:
|
272
|
-
#
|
273
|
-
if (isinstance(existing_pattern, URLResolver) and
|
274
|
-
str(existing_pattern.pattern) == str(new_pattern.pattern) and
|
275
|
-
getattr(existing_pattern, 'app_name', None) == app_name
|
276
|
-
|
168
|
+
# Compare based on pattern regex and included module/app_name if possible
|
169
|
+
if (isinstance(existing_pattern, (URLPattern, URLResolver)) and
|
170
|
+
str(existing_pattern.pattern) == str(new_pattern.pattern) and
|
171
|
+
getattr(existing_pattern, 'app_name', None) == app_name and
|
172
|
+
getattr(existing_pattern, 'namespace', None) == getattr(new_pattern, 'namespace', None)): # Check namespace too
|
173
|
+
# A bit more robust check, might need refinement
|
174
|
+
logger.warning(f"URL pattern for prefix '{url_prefix}' and app '{app_name}' seems already registered. Skipping.")
|
277
175
|
already_exists = True
|
278
176
|
break
|
279
177
|
|
280
178
|
if not already_exists:
|
281
179
|
root_urlpatterns.append(new_pattern)
|
282
|
-
logger.info(f"Dynamically registered URLs from '{module_path}' at prefix '{url_prefix}' (app_name: '{app_name}')
|
180
|
+
logger.info(f"Dynamically registered URLs from '{module_path}' at prefix '{url_prefix}' (app_name: '{app_name}')")
|
283
181
|
|
284
|
-
# --- Force update of URL resolver
|
182
|
+
# --- Force update of URL resolver ---
|
285
183
|
clear_url_caches()
|
286
|
-
#
|
184
|
+
# Reload the root URLconf module itself
|
287
185
|
try:
|
288
|
-
|
186
|
+
reload(root_urlconf_module)
|
289
187
|
logger.debug(f"Reloaded root URLconf module: {root_urlconf_name}")
|
290
188
|
except Exception as e:
|
291
|
-
logger.error(f"Failed to reload root URLconf module
|
292
|
-
|
293
|
-
|
294
|
-
# Explicitly reset the URLconf to force Django to re-read it
|
189
|
+
logger.error(f"Failed to reload root URLconf module: {e}")
|
190
|
+
# Try setting urlconf to None to force re-reading from settings
|
295
191
|
set_urlconf(None)
|
296
|
-
|
192
|
+
# Explicitly getting the resolver again might help
|
193
|
+
resolver = get_resolver(get_urlconf())
|
194
|
+
resolver._populate() # Re-populate cache
|
195
|
+
logger.info(f"Cleared URL caches and attempted to refresh resolver for {root_urlconf_name}.")
|
297
196
|
|
298
|
-
# Mark this blueprint instance as having its URLs registered (or attempted)
|
299
197
|
blueprint._urls_registered = True
|
300
198
|
|
301
199
|
except ImportError as e:
|