eptm_dashboard/.venv/lib/python3.12/site-packages/reflex/utils/templates.py

442 lines
14 KiB
Python

"""This module provides utilities for managing Reflex app templates."""
import dataclasses
import shutil
import tempfile
import zipfile
from pathlib import Path
from urllib.parse import urlparse
from reflex_base import constants
from reflex_base.config import get_config
from reflex.utils import console, net, path_ops, redir
from reflex.utils.rename import rename_imports_and_app_name
@dataclasses.dataclass(frozen=True)
class Template:
"""A template for a Reflex app."""
name: str
description: str
code_url: str
def create_config(app_name: str):
"""Create a new rxconfig file.
Args:
app_name: The name of the app.
"""
# Import here to avoid circular imports.
from reflex.compiler import templates
console.debug(f"Creating {constants.Config.FILE}")
constants.Config.FILE.write_text(templates.rxconfig_template(app_name=app_name))
def initialize_app_directory(
app_name: str,
template_name: str = constants.Templates.DEFAULT,
template_code_dir_name: str | None = None,
template_dir: Path | None = None,
):
"""Initialize the app directory on reflex init.
Args:
app_name: The name of the app.
template_name: The name of the template to use.
template_code_dir_name: The name of the code directory in the template.
template_dir: The directory of the template source files.
Raises:
SystemExit: If template_name, template_code_dir_name, template_dir combination is not supported.
"""
console.log("Initializing the app directory.")
# By default, use the blank template from local assets.
if template_name == constants.Templates.DEFAULT:
if template_code_dir_name is not None or template_dir is not None:
console.error(
f"Only {template_name=} should be provided, got {template_code_dir_name=}, {template_dir=}."
)
raise SystemExit(1)
template_code_dir_name = constants.Templates.Dirs.CODE
template_dir = Path(constants.Templates.Dirs.BASE, "apps", template_name)
else:
if template_code_dir_name is None or template_dir is None:
console.error(
f"For `{template_name}` template, `template_code_dir_name` and `template_dir` should both be provided."
)
raise SystemExit(1)
console.debug(f"Using {template_name=} {template_dir=} {template_code_dir_name=}.")
# Remove __pycache__ dirs in template directory and current directory.
for pycache_dir in [
*template_dir.glob("**/__pycache__"),
*Path.cwd().glob("**/__pycache__"),
]:
shutil.rmtree(pycache_dir, ignore_errors=True)
for file in template_dir.iterdir():
# Copy the file to current directory but keep the name the same.
path_ops.cp(str(file), file.name)
# Rename the template app to the app name.
path_ops.mv(template_code_dir_name, app_name)
path_ops.mv(
Path(app_name) / (template_name + constants.Ext.PY),
Path(app_name) / (app_name + constants.Ext.PY),
)
# Fix up the imports.
path_ops.find_replace(
app_name,
f"from {template_name}",
f"from {app_name}",
)
def initialize_default_app(app_name: str):
"""Initialize the default app.
Args:
app_name: The name of the app.
"""
create_config(app_name)
initialize_app_directory(app_name)
def create_config_init_app_from_remote_template(app_name: str, template_url: str):
"""Create new rxconfig and initialize app using a remote template.
Args:
app_name: The name of the app.
template_url: The path to the template source code as a zip file.
Raises:
SystemExit: If any download, file operations fail or unexpected zip file format.
"""
import httpx
# Create a temp directory for the zip download.
try:
temp_dir = tempfile.mkdtemp()
except OSError as ose:
console.error(f"Failed to create temp directory for download: {ose}")
raise SystemExit(1) from None
# Use httpx GET with redirects to download the zip file.
zip_file_path: Path = Path(temp_dir) / "template.zip"
try:
# Note: following redirects can be risky. We only allow this for reflex built templates at the moment.
response = net.get(template_url, follow_redirects=True)
console.debug(f"Server responded download request: {response}")
response.raise_for_status()
except httpx.HTTPError as he:
console.error(f"Failed to download the template: {he}")
raise SystemExit(1) from None
try:
zip_file_path.write_bytes(response.content)
console.debug(f"Downloaded the zip to {zip_file_path}")
except OSError as ose:
console.error(f"Unable to write the downloaded zip to disk {ose}")
raise SystemExit(1) from None
# Create a temp directory for the zip extraction.
try:
unzip_dir = Path(tempfile.mkdtemp())
except OSError as ose:
console.error(f"Failed to create temp directory for extracting zip: {ose}")
raise SystemExit(1) from None
try:
zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir)
# The zip file downloaded from github looks like:
# repo-name-branch/**/*, so we need to remove the top level directory.
except Exception as uze:
console.error(f"Failed to unzip the template: {uze}")
raise SystemExit(1) from None
if len(subdirs := list(unzip_dir.iterdir())) != 1:
console.error(f"Expected one directory in the zip, found {subdirs}")
raise SystemExit(1)
template_dir = unzip_dir / subdirs[0]
console.debug(f"Template folder is located at {template_dir}")
# Move the rxconfig file here first.
path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE)
new_config = get_config(reload=True)
# Get the template app's name from rxconfig in case it is different than
# the source code repo name on github.
template_name = new_config.app_name
# Rewrite in place instead of regenerating from a stock template, so the
# template's own config (db_url, redis_url, plugins, etc.) is preserved.
rename_imports_and_app_name(constants.Config.FILE, template_name, app_name)
initialize_app_directory(
app_name,
template_name=template_name,
template_code_dir_name=template_name,
template_dir=template_dir,
)
pyproject_file = Path(constants.PyprojectToml.FILE)
req_file = Path(constants.RequirementsTxt.FILE)
if pyproject_file.exists():
console.info(
"Run `uv sync` to install the required Python packages for this template."
)
elif req_file.exists() and len(req_file.read_text().splitlines()) > 1:
console.info(
"Run `uv pip install -r requirements.txt` to install the required Python packages for this template."
)
# Clean up the temp directories.
shutil.rmtree(temp_dir)
shutil.rmtree(unzip_dir)
def validate_and_create_app_using_remote_template(
app_name: str, template: str, templates: dict[str, Template]
):
"""Validate and create an app using a remote template.
Args:
app_name: The name of the app.
template: The name of the template.
templates: The available templates.
Raises:
SystemExit: If the template is not found.
"""
# If user selects a template, it needs to exist.
if template in templates:
from reflex_cli.v2.utils import hosting
authenticated_token = hosting.authenticated_token()
if not authenticated_token or not authenticated_token[0]:
console.print(
f"Please use `reflex login` to access the '{template}' template."
)
raise SystemExit(3)
template_url = templates[template].code_url
else:
template_parsed_url = urlparse(template)
# Check if the template is a github repo.
if template_parsed_url.hostname == "github.com":
path = template_parsed_url.path.strip("/").removesuffix(".git")
template_url = f"https://github.com/{path}/archive/main.zip"
else:
console.error(f"Template `{template}` not found or invalid.")
raise SystemExit(1)
if template_url is None:
return
create_config_init_app_from_remote_template(
app_name=app_name, template_url=template_url
)
def fetch_app_templates(version: str) -> dict[str, Template]:
"""Fetch a dict of templates from the templates repo using github API.
Args:
version: The version of the templates to fetch.
Returns:
The dict of templates.
"""
def get_release_by_tag(tag: str) -> dict | None:
url = f"{constants.Reflex.RELEASES_URL}/tags/v{tag}"
response = net.get(url)
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
release = get_release_by_tag(version)
if release is None:
console.warn(f"No templates known for version {version}")
return {}
asset_map = {
a["name"]: a["browser_download_url"] for a in release.get("assets", [])
}
templates_url = asset_map.get("templates.json")
if not templates_url:
console.warn(f"Templates metadata not found for version {version}")
return {}
templates_data = (
net.get(templates_url, follow_redirects=True).json().get("templates", [])
)
known_fields = {f.name for f in dataclasses.fields(Template)}
filtered_templates = {}
for template in templates_data:
code_url = (
""
if template["name"] == "blank"
else asset_map.get(f"{template['name']}.zip")
)
if template["hidden"] or code_url is None:
continue
filtered_templates[template["name"]] = Template(
**{k: v for k, v in template.items() if k in known_fields},
code_url=code_url,
)
return filtered_templates
def fetch_remote_templates(
template: str,
) -> tuple[str, dict[str, Template]]:
"""Fetch the available remote templates.
Args:
template: The name of the template.
Returns:
The selected template and the available templates.
"""
available_templates = {}
try:
# Get the available templates
available_templates = fetch_app_templates(constants.Reflex.VERSION)
except Exception as e:
console.warn("Failed to fetch templates. Falling back to default template.")
console.debug(f"Error while fetching templates: {e}")
template = constants.Templates.DEFAULT
return template, available_templates
def prompt_for_template_options(templates: list[Template]) -> str:
"""Prompt the user to specify a template.
Args:
templates: The templates to choose from.
Returns:
The template name the user selects.
Raises:
SystemExit: If the user does not select a template.
"""
# Show the user the URLs of each template to preview.
console.print("\nGet started with a template:")
# Prompt the user to select a template.
for index, template in enumerate(templates):
console.print(f"({index}) {template.description}")
template = console.ask(
"Which template would you like to use?",
choices=[str(i) for i in range(len(templates))],
show_choices=False,
default="0",
)
if not template:
console.error("No template selected.")
raise SystemExit(1)
try:
template_index = int(template)
except ValueError:
console.error("Invalid template selected.")
raise SystemExit(1) from None
if template_index < 0 or template_index >= len(templates):
console.error("Invalid template selected.")
raise SystemExit(1)
# Return the template.
return templates[template_index].name
def initialize_app(app_name: str, template: str | None = None) -> str | None:
"""Initialize the app either from a remote template or a blank app. If the config file exists, it is considered as reinit.
Args:
app_name: The name of the app.
template: The name of the template to use.
Returns:
The name of the template.
Raises:
SystemExit: If the template is not valid or unspecified.
"""
# Local imports to avoid circular imports.
from reflex.utils import telemetry
# Check if the app is already initialized.
if constants.Config.FILE.exists():
telemetry.send("reinit")
return None
templates: dict[str, Template] = {}
# Don't fetch app templates if the user directly asked for DEFAULT.
if template is not None and template != constants.Templates.DEFAULT:
template, templates = fetch_remote_templates(template)
if template is None:
template = prompt_for_template_options(get_init_cli_prompt_options())
if template == constants.Templates.CHOOSE_TEMPLATES:
redir.reflex_templates()
raise SystemExit(0)
if template == constants.Templates.AI:
redir.reflex_build_redirect()
raise SystemExit(0)
# If the blank template is selected, create a blank app.
if template == constants.Templates.DEFAULT:
# Default app creation behavior: a blank app.
initialize_default_app(app_name)
else:
validate_and_create_app_using_remote_template(
app_name=app_name, template=template, templates=templates
)
telemetry.send("init", template=template)
return template
def get_init_cli_prompt_options() -> list[Template]:
"""Get the CLI options for initializing a Reflex app.
Returns:
The CLI options.
"""
return [
Template(
name=constants.Templates.DEFAULT,
description="A blank Reflex app.",
code_url="",
),
Template(
name=constants.Templates.CHOOSE_TEMPLATES,
description="Premade templates built by the Reflex team.",
code_url="",
),
Template(
name=constants.Templates.AI,
description="[bold]Try our AI builder.",
code_url="",
),
]