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.
Files changed (307) hide show
  1. open_swarm-0.1.1748636295.dist-info/METADATA +257 -0
  2. open_swarm-0.1.1748636295.dist-info/RECORD +89 -0
  3. {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +2 -1
  4. open_swarm-0.1.1748636295.dist-info/entry_points.txt +3 -0
  5. open_swarm-0.1.1748636295.dist-info/top_level.txt +1 -0
  6. swarm/__init__.py +2 -0
  7. swarm/agent/agent.py +49 -0
  8. swarm/auth.py +48 -113
  9. swarm/consumers.py +0 -19
  10. swarm/core.py +411 -0
  11. swarm/extensions/blueprint/__init__.py +16 -30
  12. swarm/extensions/blueprint/agent_utils.py +45 -0
  13. swarm/extensions/blueprint/blueprint_base.py +562 -0
  14. swarm/extensions/blueprint/blueprint_discovery.py +112 -0
  15. swarm/extensions/blueprint/django_utils.py +79 -181
  16. swarm/extensions/blueprint/interactive_mode.py +72 -67
  17. swarm/extensions/blueprint/output_utils.py +82 -0
  18. swarm/{core → extensions/blueprint}/spinner.py +21 -30
  19. swarm/extensions/cli/cli_args.py +0 -6
  20. swarm/extensions/cli/commands/blueprint_management.py +9 -47
  21. swarm/extensions/cli/commands/config_management.py +6 -5
  22. swarm/extensions/cli/commands/edit_config.py +7 -16
  23. swarm/extensions/cli/commands/list_blueprints.py +1 -1
  24. swarm/extensions/cli/commands/validate_env.py +4 -11
  25. swarm/extensions/cli/commands/validate_envvars.py +6 -6
  26. swarm/extensions/cli/interactive_shell.py +2 -16
  27. swarm/extensions/config/config_loader.py +345 -107
  28. swarm/{core → extensions/config}/config_manager.py +38 -50
  29. swarm/{core → extensions/config}/server_config.py +0 -32
  30. swarm/extensions/launchers/build_launchers.py +14 -0
  31. swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
  32. swarm/extensions/launchers/swarm_api.py +64 -8
  33. swarm/extensions/launchers/swarm_cli.py +300 -8
  34. swarm/extensions/mcp/__init__.py +1 -0
  35. swarm/extensions/mcp/cache_utils.py +32 -0
  36. swarm/extensions/mcp/mcp_client.py +233 -0
  37. swarm/extensions/mcp/mcp_tool_provider.py +135 -0
  38. swarm/extensions/mcp/mcp_utils.py +260 -0
  39. swarm/llm/chat_completion.py +166 -0
  40. swarm/serializers.py +5 -96
  41. swarm/settings.py +133 -85
  42. swarm/types.py +91 -0
  43. swarm/urls.py +74 -57
  44. swarm/utils/context_utils.py +4 -10
  45. swarm/utils/general_utils.py +0 -21
  46. swarm/utils/redact.py +36 -23
  47. swarm/views/api_views.py +39 -48
  48. swarm/views/chat_views.py +76 -236
  49. swarm/views/core_views.py +87 -80
  50. swarm/views/model_views.py +121 -64
  51. swarm/views/utils.py +439 -65
  52. swarm/views/web_views.py +2 -2
  53. open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
  54. open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
  55. open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
  56. swarm/blueprints/README.md +0 -68
  57. swarm/blueprints/blueprint_audit_status.json +0 -27
  58. swarm/blueprints/chatbot/README.md +0 -40
  59. swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
  60. swarm/blueprints/chatbot/metadata.json +0 -23
  61. swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
  62. swarm/blueprints/chucks_angels/README.md +0 -11
  63. swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
  64. swarm/blueprints/chucks_angels/test_basic.py +0 -3
  65. swarm/blueprints/codey/CODEY.md +0 -15
  66. swarm/blueprints/codey/README.md +0 -115
  67. swarm/blueprints/codey/blueprint_codey.py +0 -1072
  68. swarm/blueprints/codey/codey_cli.py +0 -373
  69. swarm/blueprints/codey/instructions.md +0 -17
  70. swarm/blueprints/codey/metadata.json +0 -23
  71. swarm/blueprints/common/operation_box_utils.py +0 -83
  72. swarm/blueprints/digitalbutlers/README.md +0 -11
  73. swarm/blueprints/digitalbutlers/__init__.py +0 -1
  74. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
  75. swarm/blueprints/digitalbutlers/test_basic.py +0 -3
  76. swarm/blueprints/divine_code/README.md +0 -3
  77. swarm/blueprints/divine_code/__init__.py +0 -10
  78. swarm/blueprints/divine_code/apps.py +0 -11
  79. swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
  80. swarm/blueprints/django_chat/apps.py +0 -6
  81. swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
  82. swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
  83. swarm/blueprints/django_chat/urls.py +0 -8
  84. swarm/blueprints/django_chat/views.py +0 -32
  85. swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
  86. swarm/blueprints/flock/README.md +0 -11
  87. swarm/blueprints/flock/__init__.py +0 -8
  88. swarm/blueprints/flock/blueprint_flock.py +0 -7
  89. swarm/blueprints/flock/test_basic.py +0 -3
  90. swarm/blueprints/geese/README.md +0 -10
  91. swarm/blueprints/geese/__init__.py +0 -8
  92. swarm/blueprints/geese/blueprint_geese.py +0 -384
  93. swarm/blueprints/geese/geese_cli.py +0 -102
  94. swarm/blueprints/jeeves/README.md +0 -41
  95. swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
  96. swarm/blueprints/jeeves/jeeves_cli.py +0 -55
  97. swarm/blueprints/jeeves/metadata.json +0 -24
  98. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
  99. swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
  100. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
  101. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
  102. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
  103. swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
  104. swarm/blueprints/poets/blueprint_poets.py +0 -546
  105. swarm/blueprints/poets/poets_cli.py +0 -23
  106. swarm/blueprints/rue_code/README.md +0 -8
  107. swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
  108. swarm/blueprints/rue_code/rue_code_cli.py +0 -43
  109. swarm/blueprints/stewie/apps.py +0 -12
  110. swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
  111. swarm/blueprints/stewie/models.py +0 -19
  112. swarm/blueprints/stewie/serializers.py +0 -10
  113. swarm/blueprints/stewie/settings.py +0 -17
  114. swarm/blueprints/stewie/urls.py +0 -11
  115. swarm/blueprints/stewie/views.py +0 -26
  116. swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
  117. swarm/blueprints/whinge_surf/README.md +0 -22
  118. swarm/blueprints/whinge_surf/__init__.py +0 -1
  119. swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
  120. swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
  121. swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
  122. swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
  123. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
  124. swarm/blueprints/zeus/__init__.py +0 -2
  125. swarm/blueprints/zeus/apps.py +0 -4
  126. swarm/blueprints/zeus/blueprint_zeus.py +0 -270
  127. swarm/blueprints/zeus/zeus_cli.py +0 -13
  128. swarm/cli/async_input.py +0 -65
  129. swarm/cli/async_input_demo.py +0 -32
  130. swarm/core/agent_utils.py +0 -21
  131. swarm/core/blueprint_base.py +0 -769
  132. swarm/core/blueprint_discovery.py +0 -125
  133. swarm/core/blueprint_runner.py +0 -59
  134. swarm/core/blueprint_ux.py +0 -109
  135. swarm/core/build_launchers.py +0 -15
  136. swarm/core/cli/__init__.py +0 -1
  137. swarm/core/cli/commands/__init__.py +0 -1
  138. swarm/core/cli/commands/blueprint_management.py +0 -7
  139. swarm/core/cli/interactive_shell.py +0 -14
  140. swarm/core/cli/main.py +0 -50
  141. swarm/core/cli/utils/__init__.py +0 -1
  142. swarm/core/cli/utils/discover_commands.py +0 -18
  143. swarm/core/config_loader.py +0 -122
  144. swarm/core/output_utils.py +0 -193
  145. swarm/core/session_logger.py +0 -42
  146. swarm/core/slash_commands.py +0 -89
  147. swarm/core/swarm_api.py +0 -68
  148. swarm/core/swarm_cli.py +0 -216
  149. swarm/core/utils/__init__.py +0 -0
  150. swarm/extensions/blueprint/cli_handler.py +0 -197
  151. swarm/extensions/blueprint/runnable_blueprint.py +0 -42
  152. swarm/extensions/cli/utils/__init__.py +0 -1
  153. swarm/extensions/cli/utils/async_input.py +0 -46
  154. swarm/extensions/cli/utils/prompt_user.py +0 -3
  155. swarm/management/__init__.py +0 -0
  156. swarm/management/commands/__init__.py +0 -0
  157. swarm/management/commands/runserver.py +0 -58
  158. swarm/middleware.py +0 -65
  159. swarm/permissions.py +0 -38
  160. swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
  161. swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
  162. swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
  163. swarm/static/contrib/markedjs/marked.min.js +0 -6
  164. swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
  165. swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
  166. swarm/static/contrib/tabler-icons/archive.svg +0 -21
  167. swarm/static/contrib/tabler-icons/artboard.svg +0 -27
  168. swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
  169. swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
  170. swarm/static/contrib/tabler-icons/carambola.svg +0 -19
  171. swarm/static/contrib/tabler-icons/copy.svg +0 -20
  172. swarm/static/contrib/tabler-icons/download.svg +0 -21
  173. swarm/static/contrib/tabler-icons/edit.svg +0 -21
  174. swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
  175. swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
  176. swarm/static/contrib/tabler-icons/headset.svg +0 -22
  177. swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
  178. swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
  179. swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
  180. swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
  181. swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
  182. swarm/static/contrib/tabler-icons/message-star.svg +0 -22
  183. swarm/static/contrib/tabler-icons/message-x.svg +0 -23
  184. swarm/static/contrib/tabler-icons/message.svg +0 -21
  185. swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
  186. swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
  187. swarm/static/contrib/tabler-icons/robot.svg +0 -26
  188. swarm/static/contrib/tabler-icons/search.svg +0 -19
  189. swarm/static/contrib/tabler-icons/settings.svg +0 -20
  190. swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
  191. swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
  192. swarm/static/css/dropdown.css +0 -22
  193. swarm/static/htmx/htmx.min.js +0 -0
  194. swarm/static/js/dropdown.js +0 -23
  195. swarm/static/rest_mode/css/base.css +0 -470
  196. swarm/static/rest_mode/css/chat-history.css +0 -286
  197. swarm/static/rest_mode/css/chat.css +0 -251
  198. swarm/static/rest_mode/css/chatbot.css +0 -74
  199. swarm/static/rest_mode/css/chatgpt.css +0 -62
  200. swarm/static/rest_mode/css/colors/corporate.css +0 -74
  201. swarm/static/rest_mode/css/colors/pastel.css +0 -81
  202. swarm/static/rest_mode/css/colors/tropical.css +0 -82
  203. swarm/static/rest_mode/css/general.css +0 -142
  204. swarm/static/rest_mode/css/layout.css +0 -167
  205. swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
  206. swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
  207. swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
  208. swarm/static/rest_mode/css/messages.css +0 -84
  209. swarm/static/rest_mode/css/messenger.css +0 -135
  210. swarm/static/rest_mode/css/settings.css +0 -91
  211. swarm/static/rest_mode/css/simple.css +0 -44
  212. swarm/static/rest_mode/css/slack.css +0 -58
  213. swarm/static/rest_mode/css/style.css +0 -156
  214. swarm/static/rest_mode/css/theme.css +0 -30
  215. swarm/static/rest_mode/css/toast.css +0 -40
  216. swarm/static/rest_mode/js/auth.js +0 -9
  217. swarm/static/rest_mode/js/blueprint.js +0 -41
  218. swarm/static/rest_mode/js/blueprintUtils.js +0 -12
  219. swarm/static/rest_mode/js/chatLogic.js +0 -79
  220. swarm/static/rest_mode/js/debug.js +0 -63
  221. swarm/static/rest_mode/js/events.js +0 -98
  222. swarm/static/rest_mode/js/main.js +0 -19
  223. swarm/static/rest_mode/js/messages.js +0 -264
  224. swarm/static/rest_mode/js/messengerLogic.js +0 -355
  225. swarm/static/rest_mode/js/modules/apiService.js +0 -84
  226. swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
  227. swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
  228. swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
  229. swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
  230. swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
  231. swarm/static/rest_mode/js/modules/state.js +0 -7
  232. swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
  233. swarm/static/rest_mode/js/modules/validation.js +0 -23
  234. swarm/static/rest_mode/js/rendering.js +0 -119
  235. swarm/static/rest_mode/js/settings.js +0 -130
  236. swarm/static/rest_mode/js/sidebar.js +0 -94
  237. swarm/static/rest_mode/js/simpleLogic.js +0 -37
  238. swarm/static/rest_mode/js/slackLogic.js +0 -66
  239. swarm/static/rest_mode/js/splash.js +0 -76
  240. swarm/static/rest_mode/js/theme.js +0 -111
  241. swarm/static/rest_mode/js/toast.js +0 -36
  242. swarm/static/rest_mode/js/ui.js +0 -265
  243. swarm/static/rest_mode/js/validation.js +0 -57
  244. swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
  245. swarm/static/rest_mode/svg/arrow_down.svg +0 -5
  246. swarm/static/rest_mode/svg/arrow_left.svg +0 -5
  247. swarm/static/rest_mode/svg/arrow_right.svg +0 -5
  248. swarm/static/rest_mode/svg/arrow_up.svg +0 -5
  249. swarm/static/rest_mode/svg/attach.svg +0 -8
  250. swarm/static/rest_mode/svg/avatar.svg +0 -7
  251. swarm/static/rest_mode/svg/canvas.svg +0 -6
  252. swarm/static/rest_mode/svg/chat_history.svg +0 -4
  253. swarm/static/rest_mode/svg/close.svg +0 -5
  254. swarm/static/rest_mode/svg/copy.svg +0 -4
  255. swarm/static/rest_mode/svg/dark_mode.svg +0 -3
  256. swarm/static/rest_mode/svg/edit.svg +0 -5
  257. swarm/static/rest_mode/svg/layout.svg +0 -9
  258. swarm/static/rest_mode/svg/logo.svg +0 -29
  259. swarm/static/rest_mode/svg/logout.svg +0 -5
  260. swarm/static/rest_mode/svg/mobile.svg +0 -5
  261. swarm/static/rest_mode/svg/new_chat.svg +0 -4
  262. swarm/static/rest_mode/svg/not_visible.svg +0 -5
  263. swarm/static/rest_mode/svg/plus.svg +0 -7
  264. swarm/static/rest_mode/svg/run_code.svg +0 -6
  265. swarm/static/rest_mode/svg/save.svg +0 -4
  266. swarm/static/rest_mode/svg/search.svg +0 -6
  267. swarm/static/rest_mode/svg/settings.svg +0 -4
  268. swarm/static/rest_mode/svg/speaker.svg +0 -5
  269. swarm/static/rest_mode/svg/stop.svg +0 -6
  270. swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
  271. swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
  272. swarm/static/rest_mode/svg/toggle_off.svg +0 -6
  273. swarm/static/rest_mode/svg/toggle_on.svg +0 -6
  274. swarm/static/rest_mode/svg/trash.svg +0 -10
  275. swarm/static/rest_mode/svg/undo.svg +0 -3
  276. swarm/static/rest_mode/svg/visible.svg +0 -8
  277. swarm/static/rest_mode/svg/voice.svg +0 -10
  278. swarm/templates/account/login.html +0 -22
  279. swarm/templates/account/signup.html +0 -32
  280. swarm/templates/base.html +0 -30
  281. swarm/templates/chat.html +0 -43
  282. swarm/templates/index.html +0 -35
  283. swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
  284. swarm/templates/rest_mode/components/header.html +0 -45
  285. swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
  286. swarm/templates/rest_mode/components/settings_dialog.html +0 -97
  287. swarm/templates/rest_mode/components/splash_screen.html +0 -7
  288. swarm/templates/rest_mode/components/top_bar.html +0 -28
  289. swarm/templates/rest_mode/message_ui.html +0 -50
  290. swarm/templates/rest_mode/slackbot.html +0 -30
  291. swarm/templates/simple_blueprint_page.html +0 -24
  292. swarm/templates/websocket_partials/final_system_message.html +0 -3
  293. swarm/templates/websocket_partials/system_message.html +0 -4
  294. swarm/templates/websocket_partials/user_message.html +0 -5
  295. swarm/utils/ansi_box.py +0 -34
  296. swarm/utils/disable_tracing.py +0 -38
  297. swarm/utils/log_utils.py +0 -63
  298. swarm/utils/openai_patch.py +0 -33
  299. swarm/ux/ansi_box.py +0 -43
  300. swarm/ux/spinner.py +0 -53
  301. {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
  302. /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
  303. /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
  304. /swarm/{core → extensions/config}/setup_wizard.py +0 -0
  305. /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
  306. /swarm/{core → extensions/config}/utils/logger.py +0 -0
  307. /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, path, include # Added path, include
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
- # but still useful to avoid redundant work during normal server startup.
37
- # Let's assume if DJANGO_SETTINGS_MODULE is set, setup has likely happened or will happen.
38
- # if not django_apps.ready and not getattr(settings, 'TESTING', False):
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) # Attempt merge
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
- # Assume the custom function sets _urls_registered if it succeeds
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 as e:
55
- # Catch cases where Django itself or essential parts are not installed/available
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
- module_name = blueprint.__class__.__module__
70
-
71
- if module_name == "__main__":
72
- # --- Handling direct script execution ---
73
- logger.debug(f"Blueprint class module is '__main__'. Determining path from file location.")
74
- try:
75
- # Get the path to the file where the blueprint class is defined
76
- blueprint_file_path = inspect.getfile(blueprint.__class__)
77
- blueprint_dir = os.path.dirname(blueprint_file_path)
78
- local_settings_path = os.path.join(blueprint_dir, "settings.py")
79
- # Create a unique, safe module name based on the blueprint class
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.debug(f"Could not find module spec or origin for '{module_name}'. Cannot determine local settings path.")
104
- setattr(blueprint, 'local_settings', None)
105
- return
106
- except Exception as e: # Catch potential errors during find_spec
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
- # Explicitly check for the original error if needed
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
- # Ensure settings.INSTALLED_APPS is available and is a list
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
- settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
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) # Directly modify the list
172
- apps_added_names.append(app)
173
- logger.debug(f"Added app '{app}' from blueprint {blueprint.__class__.__name__} to INSTALLED_APPS in settings.")
174
-
175
- if apps_added_names:
176
- logger.info(f"Merged INSTALLED_APPS from blueprint {blueprint.__class__.__name__}: {apps_added_names}. App registry reload/server restart might be needed.")
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
- # This might happen if django.conf.settings itself wasn't importable earlier
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 marked as registered.")
119
+ logger.debug(f"URLs for {blueprint.__class__.__name__} already registered.")
211
120
  return
212
121
 
213
- # Safely get metadata attributes
214
- metadata = getattr(blueprint, 'metadata', {})
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. Cannot register URLs.")
135
+ logger.error("settings.ROOT_URLCONF is not set.")
230
136
  return
231
137
 
232
- # --- Get the root urlpatterns list dynamically ---
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}'. 'urlpatterns' attribute is missing or not a list.")
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'. Skipping.")
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
- # Use blueprint's cli_name or class name as app_name for namespacing
265
- app_name = metadata.get("cli_name", blueprint.__class__.__name__.lower().replace('blueprint', ''))
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 (prefix + app_name) already exists
165
+ # Check if an identical pattern already exists
270
166
  already_exists = False
271
167
  for existing_pattern in root_urlpatterns:
272
- # Check if it's a resolver (include()) and compare prefix and app_name
273
- if (isinstance(existing_pattern, URLResolver) and
274
- str(existing_pattern.pattern) == str(new_pattern.pattern) and # Compare URL prefix pattern
275
- getattr(existing_pattern, 'app_name', None) == app_name): # Compare app_name
276
- logger.warning(f"URL pattern for prefix '{url_prefix}' and app '{app_name}' seems already registered in '{root_urlconf_name}'. Skipping.")
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}') into '{root_urlconf_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 (Important!) ---
182
+ # --- Force update of URL resolver ---
285
183
  clear_url_caches()
286
- # Reloading the root URLconf module is generally needed for changes to take effect
184
+ # Reload the root URLconf module itself
287
185
  try:
288
- importlib.reload(root_urlconf_module)
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 '{root_urlconf_name}': {e}")
292
- logger.warning("URL changes might not be active until server restart.")
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
- logger.debug(f"Cleared URL caches and reset urlconf. Django should rebuild resolver on next request.")
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: