Source code for sharepack.build

"""Assemble the single-file HTML artifact."""

import base64
import json
from collections.abc import Sequence
from dataclasses import dataclass
from importlib import resources
from pathlib import Path

from .adapters import detect
from .collect import CollectionResult, SkippedFile, collect
from .errors import ProjectError

PYODIDE_VERSION = "0.26.4"
SIZE_WARN_BYTES = 20_000_000


[docs] @dataclass(frozen=True) class BuildResult: """Everything a build produced, for reporting or programmatic use. Attributes: out: Path of the written HTML file. adapter_name: Name of the detected framework adapter. n_files: Number of files bundled. size_bytes: Size of the written HTML file in bytes. scrubbed: Messages describing secret scrubbing that took place. skipped: Files that were not bundled, with reasons. warnings: Compatibility and size warnings. db_files: Relative paths of bundled database files. db_bytes: Total size of bundled database files in bytes. """ out: Path adapter_name: str n_files: int size_bytes: int scrubbed: list[str] skipped: list[SkippedFile] warnings: list[str] db_files: list[str] db_bytes: int
def _size_warnings(collection: CollectionResult) -> list[str]: """Warn when the bundled payload is uncomfortably large.""" if collection.total_bytes <= SIZE_WARN_BYTES: return [] total_mb = collection.total_bytes / 1e6 msg = ( f"payload is {total_mb:.0f} MB before base64 encoding " "(the HTML file will be ~33% larger)" ) if collection.db_bytes > collection.total_bytes / 2: msg += ( f"; database files account for {collection.db_bytes / 1e6:.0f} MB " "— consider sharing a trimmed copy of the database" ) return [msg]
[docs] def build( project: Path, out: Path, *, include: Sequence[str] = (), exclude: Sequence[str] = (), settings_module: str | None = None, app_spec: str | None = None, pyodide_version: str = PYODIDE_VERSION, pip_pin: str | None = None, ) -> BuildResult: """Bundle a project into a single self-contained HTML file. Args: project: Project root directory (for Django: contains manage.py). out: Path of the HTML file to write. include: Glob patterns forcing extra files into the bundle. exclude: Glob patterns keeping files out of the bundle. settings_module: Explicit Django settings module, bypassing detection from manage.py. app_spec: Explicit ``module:variable`` locating a Flask or FastAPI app, bypassing entry-module scanning. pyodide_version: Pyodide release loaded from the CDN at view time. pip_pin: Override for the framework's pip requirement string. Returns: A report of what was built. Raises: ProjectError: If ``project`` does not exist or is not a directory. Note: A ``DetectionError`` propagates from framework detection when no supported framework is found in ``project``. """ project = Path(project) out = Path(out) if not project.is_dir(): raise ProjectError(f"project path is not a directory: {project}") collection = collect(project, include=include, exclude=exclude) adapter = detect( project, collection.files, settings_module=settings_module, app_spec=app_spec, ) payload = { rel: base64.b64encode(data).decode() for rel, data in collection.files.items() } template = (resources.files("sharepack") / "template.html").read_text( encoding="utf-8" ) html = template.replace("__TITLE__", project.resolve().name) html = html.replace("__PYODIDE_VERSION__", pyodide_version) html = html.replace("__FILES_JSON__", json.dumps(payload)) for key, value in adapter.template_context(pip_pin=pip_pin).items(): html = html.replace(key, value) out.write_text(html, encoding="utf-8") return BuildResult( out=out, adapter_name=adapter.name, n_files=len(collection.files), size_bytes=out.stat().st_size, scrubbed=collection.scrubbed, skipped=collection.skipped, warnings=adapter.warnings + _size_warnings(collection), db_files=adapter.db_files, db_bytes=collection.db_bytes, )