Source code for scitex_app.appmaker._scaffold

"""App initializer — generate complete boilerplate for a SciTeX app plugin."""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Optional

from ._license import generate_license_text
from ._scaffold_agent_context import _agents_md
from ._scaffold_html import (
    _agents_json,
    _app_css,
    _apps_py,
    _index_html,
    _index_partial_html,
    _manifest_json,
    _readme_md,
    _skill_py,
    _tests_py,
    _urls_py,
    _views_py,
)
from ._scaffold_react import build_react_files

logger = logging.getLogger(__name__)


[docs] def init_app( target_dir: str | Path, name: str, *, label: str = "", icon: str = "fas fa-puzzle-piece", description: str = "", manifest: Optional[dict] = None, license_id: str = "AGPL-3.0", overwrite: bool = False, frontend_type: str = "html", ) -> list[str]: """Generate complete app boilerplate in target_dir. Parameters ---------- target_dir : path Project directory (e.g. data/users/alice/proj/my_app/). name : str Python module name (must end with _app, e.g. 'my_awesome_app'). label : str Human-readable label (default: derived from name). icon : str Font Awesome icon class (default: 'fas fa-puzzle-piece'). description : str Short description for the app. manifest : dict, optional Extra manifest fields to merge. license_id : str SPDX license identifier (default: 'AGPL-3.0'). overwrite : bool If True, overwrite existing files (default: False). frontend_type : str Frontend type: 'html' (default) or 'react' for React+Vite+Zustand. Returns ------- list[str] Relative paths of created files. """ target = Path(target_dir) if not target.exists(): target.mkdir(parents=True, exist_ok=True) if not label: label = name.replace("_", " ").title().removesuffix(" App") class_name = label.replace(" ", "") + "App" files = _build_all_files( name, label, class_name, icon, description, manifest, license_id, frontend_type=frontend_type, ) created = [] for relpath, content in files.items(): filepath = target / relpath if filepath.exists() and not overwrite: logger.debug("Skipping existing file: %s", relpath) continue filepath.parent.mkdir(parents=True, exist_ok=True) filepath.write_text(content, encoding="utf-8") created.append(relpath) logger.debug("Created: %s", relpath) logger.info("Scaffolded %d/%d files in %s", len(created), len(files), target) return created
def _build_all_files( name, label, class_name, icon, description, manifest, license_id, *, frontend_type: str = "html", ): """Build dict of relpath -> content for all scaffold files. LAYOUT (nested-package fix, port of scitex-hub PR #293): Wrapper-root files (build config + project metadata read directly by tooling and by the operator): <wrapper>/pyproject.toml <wrapper>/README.md <wrapper>/LICENSE <wrapper>/AGENTS.md <wrapper>/.gitignore Python-package files (nested under ``<name>/`` so pip + hatchling can build the wheel — the package directory MUST match the normalized project name, otherwise ``hatchling.build`` errors with "Unable to determine which files to ship inside the wheel"): <wrapper>/<name>/__init__.py <wrapper>/<name>/apps.py <wrapper>/<name>/views.py <wrapper>/<name>/urls.py <wrapper>/<name>/tests.py <wrapper>/<name>/skill.py <wrapper>/<name>/_cli.py <wrapper>/<name>/manifest.json <wrapper>/<name>/templates/<name>/... <wrapper>/<name>/static/<name>/... <wrapper>/<name>/.agents/agents.json <wrapper>/<name>/docs/PLATFORM.md The hub's ``pip_install_user_app`` runs ``pip install --no-deps --target=<install_dir> <gitea-archive-url>`` against this shape; hatchling auto-detects ``<name>/`` as the package directory (via the ``[tool.hatch.build.targets.wheel] packages = ["<name>"]`` declaration in the emitted pyproject) and lands the wheel cleanly on ``sys.path``. ``<project.scripts> <slug> = "<name>._cli:main"`` also requires ``_cli.py`` to live INSIDE the package directory. """ use_react = frontend_type == "react" files = {} # ----------------------------------------------------------------- # Wrapper-root files (build system + project metadata) # ----------------------------------------------------------------- files["pyproject.toml"] = _pyproject_toml(name, label, description, license_id) files["README.md"] = _readme_md( name, label, description, license_id, frontend_type=frontend_type ) license_text = generate_license_text(license_id) if license_text is None: license_text = generate_license_text("AGPL-3.0") files["LICENSE"] = license_text files["AGENTS.md"] = _agents_md(name, label, icon, description, frontend_type) files[".gitignore"] = "\n".join( [ "# Runtime data (created by platform, not app source)", "scitex/", "", "# Python", "__pycache__/", "*.pyc", "*.pyo", "*.egg-info/", "", "# Environment", ".env", ".env.*", ".venv/", "venv/", "node_modules/", "", "# IDE", ".vscode/", ".idea/", "", "# OS", ".DS_Store", "Thumbs.db", "", ] ) # ----------------------------------------------------------------- # Python-package files (nested under <name>/) # ----------------------------------------------------------------- files[f"{name}/__init__.py"] = f'"""SciTeX Cloud App: {label}."""\n' files[f"{name}/apps.py"] = _apps_py(name, label, class_name) files[f"{name}/views.py"] = _views_py(name, label, description) files[f"{name}/urls.py"] = _urls_py(name) files[f"{name}/tests.py"] = _tests_py(name, label) files[f"{name}/skill.py"] = _skill_py(name, label, description) files[f"{name}/_cli.py"] = _cli_py(name, label) files[f"{name}/manifest.json"] = _manifest_json( name, label, icon, description, manifest, license_id, frontend_type ) files[f"{name}/templates/{name}/index.html"] = _index_html( name, label, include_js_bundle=use_react ) files[f"{name}/templates/{name}/index_partial.html"] = _index_partial_html( name, label, icon, react_mount=use_react ) files[f"{name}/static/{name}/css/{name}.css"] = _app_css(name, label) files[f"{name}/.agents/agents.json"] = _agents_json(name, label) from ._scaffold_docs import _platform_docs_md files[f"{name}/docs/PLATFORM.md"] = _platform_docs_md(name) # React frontend files (mount under the package directory too) if use_react: for relpath, content in build_react_files(name, label, icon).items(): files[f"{name}/{relpath}"] = content return files def _cli_py(name, label): """Generate _cli.py — standalone GUI launcher.""" slug = name.replace("_", "-") return f'''"""CLI for {label} — standalone GUI launcher.""" import click @click.group(invoke_without_command=True) @click.pass_context def main(ctx): """{label} — SciTeX Cloud App.""" if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) @main.command() @click.option("--port", "-p", default=8050, help="Server port") @click.option("--host", "-h", default="127.0.0.1", help="Host to bind") @click.option("--no-browser", is_flag=True, help="Don't open browser") @click.option( "--force", is_flag=True, help="Stop a previous instance of this app's own GUI, then serve.", ) def gui(port, host, no_browser, force): """Launch standalone GUI with workspace shell. Binds exactly --port, or fails loud with the process holding it -- never silently drifts to the next free port. Refuses a second instance of this app's own GUI unless --force is given; --force never kills a process it does not own. """ import functools from scitex_app.embed import run_standalone, serve_gui raise SystemExit( serve_gui( "{slug}", ".", port, host, force, functools.partial( run_standalone, app_module="{name}", port=port, host=host, open_browser=not no_browser, ), ) ) ''' def _pyproject_toml(name, label, description, license_id): """Generate pyproject.toml for dual-mode app (standalone + scitex-hub extension). Includes ``[tool.hatch.build.targets.wheel] packages = ["<name>"]`` so the wheel build works against the nested-package layout that ``_build_all_files`` emits — without this, hatchling errors "Unable to determine which files to ship inside the wheel using the following heuristics" and ``pip_install_user_app`` fails at install time. """ slug = name.replace("_", "-") desc = description or f"{label} — a SciTeX Hub app." return f"""[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "{slug}" version = "0.1.0" description = "{desc}" requires-python = ">=3.10" license = "{license_id}" [project.scripts] {slug} = "{name}._cli:main" [project.optional-dependencies] scitex = ["scitex-app>=0.1.0", "scitex-ui>=0.1.0"] dev = ["pytest>=7.0.0"] [project.entry-points."scitex_modules"] {name} = "{name}:get_module_config" [tool.hatch.build.targets.wheel] packages = ["{name}"] """ # EOF