| """
|
| `dense-evolution` console script (see pyproject.toml [project.scripts]).
|
|
|
| Three real subcommands:
|
| serve starts the local Composer kernel (local_site.app.server)
|
| that the published Composer page (docs/composer.md)
|
| talks to.
|
| offline-composer downloads the real published Composer page (the same
|
| HTML Github Pages serves, not a hand-rolled copy) plus
|
| the same-origin assets it references, into a local
|
| folder -- so it opens via file:// with no internet at
|
| all, while still talking to the local kernel above.
|
| mcp starts the dense_evolution_mcp MCP server (mcp_server/),
|
| a thin adapter that exposes the same Composer kernel
|
| endpoints as MCP tools for an agent to call directly.
|
| Requires `serve` running separately -- this process
|
| talks to that kernel over HTTP, it doesn't embed it.
|
|
|
| fastapi/uvicorn/pydantic (composer) and mcp/httpx (mcp) are optional
|
| extras, not core dependencies -- imported here, inside their own command
|
| branch, not at module level, so `import dense_evolution` itself never
|
| requires them.
|
| """
|
|
|
| import sys
|
|
|
| USAGE = """usage: dense-evolution <command>
|
|
|
| commands:
|
| serve Start the local Composer kernel (http://127.0.0.1:8800)
|
| that the published Composer page (docs/composer.md)
|
| connects to.
|
| offline-composer [DEST] Download the real published Composer page and its
|
| assets into DEST (default: ./composer-offline) so
|
| it works via file:// with no internet.
|
| mcp Start the dense_evolution_mcp MCP server (stdio
|
| transport) so an agent can call the Composer
|
| kernel's endpoints directly. Requires `serve`
|
| running separately (default http://127.0.0.1:8800,
|
| override with DENSE_EVOLUTION_KERNEL_URL).
|
|
|
| Requires the composer extra: pip install dense-evolution[composer]
|
| The mcp command requires the mcp extra: pip install dense-evolution[mcp]
|
| """
|
|
|
| COMPOSER_PAGE_URL = "https://tatopenn-cell.github.io/Dense-Evolution/composer/"
|
|
|
|
|
| def _require_composer_extra():
|
| try:
|
| import fastapi, uvicorn, pydantic
|
| except ImportError as exc:
|
| print(
|
| "dense-evolution needs the composer extra:\n"
|
| " pip install dense-evolution[composer]\n"
|
| f"(missing: {exc.name})",
|
| file=sys.stderr,
|
| )
|
| sys.exit(1)
|
|
|
|
|
| def _cmd_serve():
|
| _require_composer_extra()
|
| from local_site.app.server import main as serve_main
|
| serve_main()
|
|
|
|
|
| def _require_mcp_extra():
|
| try:
|
| import mcp, httpx
|
| except ImportError as exc:
|
| print(
|
| "dense-evolution needs the mcp extra:\n"
|
| " pip install dense-evolution[mcp]\n"
|
| f"(missing: {exc.name})",
|
| file=sys.stderr,
|
| )
|
| sys.exit(1)
|
|
|
|
|
| def _cmd_mcp():
|
| _require_mcp_extra()
|
| from mcp_server.server import main as mcp_main
|
| mcp_main()
|
|
|
|
|
| def _cmd_offline_composer(dest: str):
|
| """Mirrors COMPOSER_PAGE_URL into `dest`: the page itself plus every
|
| same-origin <link href>/<script src> it references (Material's shared
|
| theme CSS/JS bundle, the Composer-specific app.js/style.css), each
|
| saved at the same relative path it already uses on the live site --
|
| mkdocs/Material already generate those as page-relative, precisely so
|
| a subtree like this stays self-consistent once copied elsewhere. Not a
|
| full recursive mirror (fonts referenced only from inside a CSS url()
|
| are not followed -- Material falls back to system fonts without them,
|
| a cosmetic gap, not a functional one): scoped to what the page's own
|
| <head>/<body> actually link to, which is everything the Composer UI
|
| itself needs to run."""
|
| import os
|
| import urllib.request
|
| from html.parser import HTMLParser
|
| from urllib.parse import urljoin, urlparse
|
|
|
| class _AssetFinder(HTMLParser):
|
| def __init__(self):
|
| super().__init__()
|
| self.assets = []
|
|
|
| def handle_starttag(self, tag, attrs):
|
| attrs = dict(attrs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if tag == "link" and attrs.get("href") and attrs.get("rel") in ("stylesheet", "icon"):
|
| self.assets.append(attrs["href"])
|
| elif tag == "script" and attrs.get("src"):
|
| self.assets.append(attrs["src"])
|
| elif tag == "img" and attrs.get("src"):
|
| self.assets.append(attrs["src"])
|
|
|
| print(f"Scarico {COMPOSER_PAGE_URL} ...")
|
| with urllib.request.urlopen(COMPOSER_PAGE_URL, timeout=30) as resp:
|
| html_bytes = resp.read()
|
| html_text = html_bytes.decode("utf-8", errors="replace")
|
|
|
| parser = _AssetFinder()
|
| parser.feed(html_text)
|
|
|
| page_url = urlparse(COMPOSER_PAGE_URL)
|
| base_origin = f"{page_url.scheme}://{page_url.netloc}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| site_root_path = page_url.path.rsplit("composer/", 1)[0]
|
|
|
| def _relative_to_site_root(url: str) -> str:
|
| path = urlparse(url).path
|
| if path.startswith(site_root_path):
|
| path = path[len(site_root_path):]
|
| return path.lstrip("/")
|
|
|
| os.makedirs(dest, exist_ok=True)
|
| page_relative_path = _relative_to_site_root(COMPOSER_PAGE_URL) + "index.html"
|
| page_local_path = os.path.join(dest, page_relative_path)
|
| os.makedirs(os.path.dirname(page_local_path), exist_ok=True)
|
| with open(page_local_path, "w", encoding="utf-8") as f:
|
| f.write(html_text)
|
|
|
| seen = set()
|
| for href in parser.assets:
|
| absolute_url = urljoin(COMPOSER_PAGE_URL, href)
|
| if not absolute_url.startswith(base_origin) or absolute_url in seen:
|
| continue
|
| seen.add(absolute_url)
|
|
|
| relative_path = _relative_to_site_root(absolute_url)
|
| local_path = os.path.join(dest, relative_path)
|
| os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
| try:
|
| with urllib.request.urlopen(absolute_url, timeout=30) as resp:
|
| data = resp.read()
|
| with open(local_path, "wb") as f:
|
| f.write(data)
|
| print(f" scaricato: {relative_path}")
|
| except Exception as exc:
|
| print(f" saltato (non essenziale): {relative_path} ({exc})")
|
|
|
| print(f"\nCopia offline pronta: {page_local_path}")
|
| return page_local_path
|
|
|
|
|
| def main(argv=None):
|
| args = argv if argv is not None else sys.argv[1:]
|
| if args == ["serve"]:
|
| _cmd_serve()
|
| return
|
| if args and args[0] == "offline-composer":
|
| dest = args[1] if len(args) > 1 else "composer-offline"
|
| _cmd_offline_composer(dest)
|
| return
|
| if args == ["mcp"]:
|
| _cmd_mcp()
|
| return
|
|
|
| print(USAGE, file=sys.stderr if args else sys.stdout)
|
| sys.exit(1 if args else 0)
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|