From 0f7765bd104e0d17d30a494a931c4468323e8903 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Tue, 27 Aug 2024 17:25:56 -0400 Subject: [PATCH] feat: Support templates with `_template.json` metadata (#1631) --- CHANGELOG.md | 2 + shiny/_main.py | 14 +- shiny/_main_create.py | 601 +++++++++---- .../templates/app/01-basic-app/_template.json | 19 + .../01-basic-app}/app-core.py | 0 .../01-basic-app}/app-express.py | 0 .../app/02-basic-sidebar/_template.json | 19 + .../02-basic-sidebar}/app-core.py | 0 .../02-basic-sidebar}/app-express.py | 0 .../02-basic-sidebar}/penguins.csv | 0 .../02-basic-sidebar}/requirements.txt | 0 .../02-basic-sidebar}/shared.py | 0 .../templates/app/03-dashboard/_template.json | 19 + .../03-dashboard}/app-core.py | 0 .../03-dashboard}/app-express.py | 0 .../03-dashboard}/penguins.csv | 0 .../03-dashboard}/requirements.txt | 0 .../03-dashboard}/shared.py | 0 .../dashboard => app/03-dashboard}/styles.css | 0 .../04-dashboard-tips}/README.md | 0 .../app/04-dashboard-tips/_template.json | 19 + .../04-dashboard-tips}/app-core.py | 0 .../04-dashboard-tips}/app-express.py | 0 .../04-dashboard-tips}/requirements.txt | 0 .../04-dashboard-tips}/shared.py | 0 .../04-dashboard-tips}/styles.css | 0 .../04-dashboard-tips}/tips.csv | 0 .../app/05-basic-navigation/_template.json | 19 + .../05-basic-navigation}/app-core.py | 0 .../05-basic-navigation}/app-express.py | 0 .../05-basic-navigation}/penguins.csv | 0 .../05-basic-navigation}/requirements.txt | 0 .../05-basic-navigation}/shared.py | 0 .../aws-bedrock-anthropic/_template.json | 5 + .../enterprise/aws-bedrock-anthropic/app.py | 0 .../aws-bedrock-anthropic/app_utils.py | 0 .../aws-bedrock-anthropic/requirements.txt | 0 .../enterprise/azure-openai/_template.json | 5 + .../chat/enterprise/azure-openai/app.py | 0 .../chat/enterprise/azure-openai/app_utils.py | 0 .../enterprise/azure-openai/requirements.txt | 0 .../hello-providers/anthropic/_template.json | 5 + .../chat/hello-providers/anthropic/app.py | 44 + .../hello-providers/anthropic/app_utils.py | 0 .../anthropic/requirements.txt | 0 .../hello-providers/gemini/_template.json | 5 + .../chat/hello-providers/gemini/app.py | 0 .../chat/hello-providers/gemini/app_utils.py | 0 .../hello-providers/gemini/requirements.txt | 0 .../hello-providers/langchain/_template.json | 5 + .../chat/hello-providers/langchain/app.py | 0 .../hello-providers/langchain/app_utils.py | 0 .../langchain/requirements.txt | 0 .../hello-providers/ollama/_template.json | 5 + .../chat/hello-providers/ollama/app.py | 0 .../hello-providers/ollama/requirements.txt | 0 .../hello-providers/openai/_template.json | 5 + .../chat/hello-providers/openai/app.py | 0 .../chat/hello-providers/openai/app_utils.py | 0 .../hello-providers/openai/requirements.txt | 0 .../js-input/custom_component/distjs/index.js | 814 ------------------ .../{package-templates => package}/.gitignore | 0 .../js-input/.gitignore | 0 .../js-input/README.md | 0 .../templates/package/js-input/_template.json | 12 + .../js-input/custom_component/__init__.py | 0 .../custom_component/custom_component.py | 0 .../js-input/example-app/app.py | 0 .../js-input/package-lock.json | 0 .../js-input/package.json | 0 .../js-input/pyproject.toml | 0 .../js-input/srcts/index.ts | 0 .../js-input/tsconfig.json | 0 .../js-output/.gitignore | 0 .../js-output/README.md | 0 .../package/js-output/_template.json | 12 + .../js-output/custom_component/__init__.py | 0 .../custom_component/custom_component.py | 0 .../js-output/example-app/app.py | 0 .../js-output/package-lock.json | 0 .../js-output/package.json | 0 .../js-output/pyproject.toml | 0 .../js-output/srcts/index.ts | 0 .../js-output/tsconfig.json | 0 .../js-react/.gitignore | 0 .../js-react/README.md | 0 .../templates/package/js-react/_template.json | 12 + .../js-react/custom_component/__init__.py | 0 .../custom_component/custom_component.py | 0 .../js-react/example-app/app.py | 0 .../js-react/package-lock.json | 0 .../js-react/package.json | 0 .../js-react/pyproject.toml | 0 .../js-react/srcts/index.tsx | 0 .../js-react/tsconfig.json | 0 .../playwright/examples/test_shiny_create.py | 34 +- 96 files changed, 689 insertions(+), 986 deletions(-) create mode 100644 shiny/templates/app/01-basic-app/_template.json rename shiny/templates/{app-templates/basic-app => app/01-basic-app}/app-core.py (100%) rename shiny/templates/{app-templates/basic-app => app/01-basic-app}/app-express.py (100%) create mode 100644 shiny/templates/app/02-basic-sidebar/_template.json rename shiny/templates/{app-templates/basic-sidebar => app/02-basic-sidebar}/app-core.py (100%) rename shiny/templates/{app-templates/basic-sidebar => app/02-basic-sidebar}/app-express.py (100%) rename shiny/templates/{app-templates/basic-navigation => app/02-basic-sidebar}/penguins.csv (100%) rename shiny/templates/{app-templates/basic-navigation => app/02-basic-sidebar}/requirements.txt (100%) rename shiny/templates/{app-templates/basic-navigation => app/02-basic-sidebar}/shared.py (100%) create mode 100644 shiny/templates/app/03-dashboard/_template.json rename shiny/templates/{app-templates/dashboard => app/03-dashboard}/app-core.py (100%) rename shiny/templates/{app-templates/dashboard => app/03-dashboard}/app-express.py (100%) rename shiny/templates/{app-templates/basic-sidebar => app/03-dashboard}/penguins.csv (100%) rename shiny/templates/{app-templates/dashboard => app/03-dashboard}/requirements.txt (100%) rename shiny/templates/{app-templates/basic-sidebar => app/03-dashboard}/shared.py (100%) rename shiny/templates/{app-templates/dashboard => app/03-dashboard}/styles.css (100%) rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/README.md (100%) create mode 100644 shiny/templates/app/04-dashboard-tips/_template.json rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/app-core.py (100%) rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/app-express.py (100%) rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/requirements.txt (100%) rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/shared.py (100%) rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/styles.css (100%) rename shiny/templates/{app-templates/dashboard-tips => app/04-dashboard-tips}/tips.csv (100%) create mode 100644 shiny/templates/app/05-basic-navigation/_template.json rename shiny/templates/{app-templates/basic-navigation => app/05-basic-navigation}/app-core.py (100%) rename shiny/templates/{app-templates/basic-navigation => app/05-basic-navigation}/app-express.py (100%) rename shiny/templates/{app-templates/dashboard => app/05-basic-navigation}/penguins.csv (100%) rename shiny/templates/{app-templates/basic-sidebar => app/05-basic-navigation}/requirements.txt (100%) rename shiny/templates/{app-templates/dashboard => app/05-basic-navigation}/shared.py (100%) create mode 100644 shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json rename {examples => shiny/templates}/chat/enterprise/aws-bedrock-anthropic/app.py (100%) rename {examples => shiny/templates}/chat/enterprise/aws-bedrock-anthropic/app_utils.py (100%) rename {examples => shiny/templates}/chat/enterprise/aws-bedrock-anthropic/requirements.txt (100%) create mode 100644 shiny/templates/chat/enterprise/azure-openai/_template.json rename {examples => shiny/templates}/chat/enterprise/azure-openai/app.py (100%) rename {examples => shiny/templates}/chat/enterprise/azure-openai/app_utils.py (100%) rename {examples => shiny/templates}/chat/enterprise/azure-openai/requirements.txt (100%) create mode 100644 shiny/templates/chat/hello-providers/anthropic/_template.json create mode 100644 shiny/templates/chat/hello-providers/anthropic/app.py rename {examples => shiny/templates}/chat/hello-providers/anthropic/app_utils.py (100%) rename {examples => shiny/templates}/chat/hello-providers/anthropic/requirements.txt (100%) create mode 100644 shiny/templates/chat/hello-providers/gemini/_template.json rename {examples => shiny/templates}/chat/hello-providers/gemini/app.py (100%) rename {examples => shiny/templates}/chat/hello-providers/gemini/app_utils.py (100%) rename {examples => shiny/templates}/chat/hello-providers/gemini/requirements.txt (100%) create mode 100644 shiny/templates/chat/hello-providers/langchain/_template.json rename {examples => shiny/templates}/chat/hello-providers/langchain/app.py (100%) rename {examples => shiny/templates}/chat/hello-providers/langchain/app_utils.py (100%) rename {examples => shiny/templates}/chat/hello-providers/langchain/requirements.txt (100%) create mode 100644 shiny/templates/chat/hello-providers/ollama/_template.json rename {examples => shiny/templates}/chat/hello-providers/ollama/app.py (100%) rename {examples => shiny/templates}/chat/hello-providers/ollama/requirements.txt (100%) create mode 100644 shiny/templates/chat/hello-providers/openai/_template.json rename {examples => shiny/templates}/chat/hello-providers/openai/app.py (100%) rename {examples => shiny/templates}/chat/hello-providers/openai/app_utils.py (100%) rename {examples => shiny/templates}/chat/hello-providers/openai/requirements.txt (100%) delete mode 100644 shiny/templates/package-templates/js-input/custom_component/distjs/index.js rename shiny/templates/{package-templates => package}/.gitignore (100%) rename shiny/templates/{package-templates => package}/js-input/.gitignore (100%) rename shiny/templates/{package-templates => package}/js-input/README.md (100%) create mode 100644 shiny/templates/package/js-input/_template.json rename shiny/templates/{package-templates => package}/js-input/custom_component/__init__.py (100%) rename shiny/templates/{package-templates => package}/js-input/custom_component/custom_component.py (100%) rename shiny/templates/{package-templates => package}/js-input/example-app/app.py (100%) rename shiny/templates/{package-templates => package}/js-input/package-lock.json (100%) rename shiny/templates/{package-templates => package}/js-input/package.json (100%) rename shiny/templates/{package-templates => package}/js-input/pyproject.toml (100%) rename shiny/templates/{package-templates => package}/js-input/srcts/index.ts (100%) rename shiny/templates/{package-templates => package}/js-input/tsconfig.json (100%) rename shiny/templates/{package-templates => package}/js-output/.gitignore (100%) rename shiny/templates/{package-templates => package}/js-output/README.md (100%) create mode 100644 shiny/templates/package/js-output/_template.json rename shiny/templates/{package-templates => package}/js-output/custom_component/__init__.py (100%) rename shiny/templates/{package-templates => package}/js-output/custom_component/custom_component.py (100%) rename shiny/templates/{package-templates => package}/js-output/example-app/app.py (100%) rename shiny/templates/{package-templates => package}/js-output/package-lock.json (100%) rename shiny/templates/{package-templates => package}/js-output/package.json (100%) rename shiny/templates/{package-templates => package}/js-output/pyproject.toml (100%) rename shiny/templates/{package-templates => package}/js-output/srcts/index.ts (100%) rename shiny/templates/{package-templates => package}/js-output/tsconfig.json (100%) rename shiny/templates/{package-templates => package}/js-react/.gitignore (100%) rename shiny/templates/{package-templates => package}/js-react/README.md (100%) create mode 100644 shiny/templates/package/js-react/_template.json rename shiny/templates/{package-templates => package}/js-react/custom_component/__init__.py (100%) rename shiny/templates/{package-templates => package}/js-react/custom_component/custom_component.py (100%) rename shiny/templates/{package-templates => package}/js-react/example-app/app.py (100%) rename shiny/templates/{package-templates => package}/js-react/package-lock.json (100%) rename shiny/templates/{package-templates => package}/js-react/package.json (100%) rename shiny/templates/{package-templates => package}/js-react/pyproject.toml (100%) rename shiny/templates/{package-templates => package}/js-react/srcts/index.tsx (100%) rename shiny/templates/{package-templates => package}/js-react/tsconfig.json (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3437a3213..5d99e7506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `shiny create` now supports a succinct format for specifying the GitHub repository via the `--github` flag, e.g. `--github posit-dev/py-shiny-templates`. You can now also use `--github` and `--template` together, in which case `--github` should point to a repository containing a directory matching the name provided in `--template`. (#1623) +* `shiny create` now identifies templates in external repositories using a `_template.json` metadata file. This file should contain at an `"id"` and optionally a `"title"` and `"description"`. When `shiny create` is called with the `--github` flag but without a `--template` flag, it will offer a menu listing all available templates in the repository. (#1631) + ### Other changes ### Bug fixes diff --git a/shiny/_main.py b/shiny/_main.py index 4d2b98cd9..e004f4312 100644 --- a/shiny/_main.py +++ b/shiny/_main.py @@ -582,17 +582,21 @@ def create( dir: Optional[Path | str] = None, package_name: Optional[str] = None, ) -> None: - from ._main_create import use_template_github, use_template_internal - - print(f"dir is {dir}") + from ._main_create import use_github_template, use_internal_template if dir is not None: dir = Path(dir) if github is not None: - use_template_github(github, template=template, mode=mode, dest_dir=dir) + use_github_template( + github, + template_name=template, + mode=mode, + dest_dir=dir, + package_name=package_name, + ) else: - use_template_internal(template, mode, dir, package_name) + use_internal_template(template, mode, dir, package_name) @main.command( diff --git a/shiny/_main_create.py b/shiny/_main_create.py index bf19157ba..0230fac4b 100644 --- a/shiny/_main_create.py +++ b/shiny/_main_create.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import os import re import shutil @@ -7,9 +8,9 @@ import tempfile import textwrap import zipfile -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from typing import Generator, Optional, cast +from typing import Generator, Literal, Optional, cast from urllib.error import URLError from urllib.parse import urlparse from urllib.request import urlopen @@ -34,43 +35,207 @@ cli_url, cli_verbatim, cli_wait, + cli_warning, directory_prompt, ) -# These templates are copied over from the `shiny/templates/app_templates` -# directory. The process for adding new ones is to add your app folder to -# that directory, and then add another entry to this dictionary. -app_template_choices = { - "Basic app": "basic-app", - "Sidebar layout": "basic-sidebar", - "Basic dashboard": "dashboard", - "Intermediate dashboard": "dashboard-tips", - "Navigating multiple pages/panels": "basic-navigation", - "Custom JavaScript component ...": "js-component", - "Choose from the Shiny Templates website": "external-gallery", -} - -# These are templates which produce a Python package and have content filled in at -# various places based on the user input. You can add new ones by following the -# examples in `shiny/templates/package-templates` and then adding entries to this -# dictionary. -package_template_choices = { - "Input component": "js-input", - "Output component": "js-output", - "React component": "js-react", -} - styles_for_questions = questionary.Style([("secondary", "italic")]) # Prebuild some common choices cancel_choice: Choice = Choice(title=[("class:secondary", "[Cancel]")], value="cancel") back_choice: Choice = Choice(title=[("class:secondary", "← Back")], value="back") -def choice_from_dict(choice_dict: dict[str, str]) -> list[Choice]: - return [Choice(title=key, value=value) for key, value in choice_dict.items()] +def choice_from_templates(templates: list[ShinyTemplate]) -> list[Choice]: + return [Choice(title=t.title, value=t.id) for t in templates] + + +@dataclass +class ShinyTemplate: + """ + Shiny Template + + This class receives metadata for a Shiny template from a `_template.json` file. + (Alternatively, a template can be created from just `name` and `path` for legacy + reasons.) + + Attributes + ---------- + id + The identifier of the Shiny template. This `id` should be unique within a + repository of templates. + path + The path to the `_template.json` file or the root directory of the template. + type + The type of the template (e.g. "app", "package"), default: 'app'. + title + A brief title for the template, if provided. + description + A longer description of the template, if provided. + next_steps + A list of next steps or instructions related to this template, shown after the + default instructions are displayed. In the `_template.json` file, this field + can be a single string or an array of strings. + follow_up + A list of follow-up actions or information related to this template. In the + `_template.json` file, this field can be a single string, an array of strings, + or an object with a `text` field and an optional `type` field. The `type` field + can be "action", "info", "warning", "danger", or "text". + """ + + id: str + path: Path + type: str = "app" + title: str | None = None + description: str | None = None + next_steps: list[str] = field(default_factory=list) + follow_up: list[ShinyTemplateFollowUp] = field(default_factory=list) + _express_available: bool | None = None + + @property + def express_available(self) -> bool: + """ + Does the template include an Express variant, denoted by the presence of an + `app-express.py` file? + """ + + if self._express_available is None: + self._express_available = (self.path / "app-express.py").exists() + return self._express_available + + +class ShinyTemplateFollowUp: + def __init__( + self, + text: str, + type: str = "text", + ): + self.text = text + self.type: Literal["action", "info", "warning", "danger", "text"] = "text" + if type in ("action", "info", "warning", "danger"): + self.type = type + + +def find_templates(path: Path | str = ".") -> list[ShinyTemplate]: + path = Path(path) + templates: list[ShinyTemplate] = [] + duplicated_ids: set[str] = set() + + template_files = sorted(path.glob("**/_template.json")) + for tf in template_files: + with tf.open() as f: + try: + template = json.load(f) + except json.JSONDecodeError as err: + raise ValueError(f"Error parsing {tf}: {err}") + + # "next_steps" and "follow_up" can be either a string or an array of strings + # or an array of dictionaries (follow_up only) + follow_up_raw: str | list[dict[str, str]] = template.get("follow_up", []) + if isinstance(follow_up_raw, str): + follow_up_raw = [{"text": follow_up_raw}] + + follow_up = [ShinyTemplateFollowUp(**f) for f in follow_up_raw] + + next_steps: str | list[str] = template.get("next_steps", []) + if isinstance(next_steps, str): + next_steps = [next_steps] + + if "id" not in template: + raise ValueError(f"Template in {tf} is missing a 'id' field.") + + id = template["id"] + if id in [t.id for t in templates]: + duplicated_ids.add(id) + + templates.append( + ShinyTemplate( + id=id, + path=tf.parent.absolute(), + title=template.get("title"), + type=template.get("type", "app"), + description=template.get("description"), + follow_up=follow_up, + next_steps=next_steps, + ) + ) + + if duplicated_ids: + click.echo( + cli_danger( + "Warning: The following templates contain duplicate IDs. " + + "Only the first occurrence will be used." + ) + ) + for id in duplicated_ids: + paths = [t.path.relative_to(path) for t in templates if t.id == id] + click.echo( + cli_warning( + cli_code(f'"id": "{id}"') + + " used by: " + + ", ".join([cli_field(str(p)) for p in paths]) + ) + ) + + return templates + + +def template_by_name(templates: list[ShinyTemplate], name: str) -> ShinyTemplate | None: + for template in templates: + if template.id == name: + return template + return None + + +class ShinyInternalTemplates: + """ + Shiny's Internal Templates + + Internal templates that are built into the shiny package are always available via + `shiny create`. These templates are stored in the `shiny/templates` directory and + are divided into `app-templates` and `package-templates`. + + To add a new template, create the template subfolder in either of the two template + folders and add a `_template.json` file. See `ShinyTemplate` for expected fields. + + * `use_template_internal()` is the initial menu seen, which presents `templates/app` + templates with additional choices. + * `templates/package` templates are also referred to as `js-components` in the code + base, these templates appear as a submenu and are handled by + `use_internal_package_template()`. + * `templates/chat` templates are generative AI templates and are handled by + `use_internal_chat_ai_template()`, + """ + + def __init__(self): + self.templates: dict[str, list[ShinyTemplate]] = {} + + def _templates(self, dir: str = "templates") -> list[ShinyTemplate]: + if dir in self.templates: + return self.templates[dir] + self.templates[dir] = find_templates(Path(__file__).parent / dir) + return self.templates[dir] + @property + def apps(self) -> list[ShinyTemplate]: + return self._templates("templates/app") -def use_template_internal( + @property + def packages(self) -> list[ShinyTemplate]: + return self._templates("templates/package") + + @property + def chat_hello_providers(self) -> list[ShinyTemplate]: + return self._templates("templates/chat/hello-providers") + + @property + def chat_enterprise(self) -> list[ShinyTemplate]: + return self._templates("templates/chat/enterprise") + + +shiny_internal_templates = ShinyInternalTemplates() + + +def use_internal_template( question_state: Optional[str] = None, mode: Optional[str] = None, dest_dir: Optional[Path] = None, @@ -85,35 +250,44 @@ def use_template_internal( were at level 5 of a question chain and wanted to return to level 4. This is not that useful currently because we only have two levels of questions. - :param question_state: The question state you would like to return to. Currently, the options are: "cancel": Cancel the operation and exit. "js-component": Start the questions for creating a custom JavaScript component. """ + app_templates = shiny_internal_templates.apps + pkg_templates = shiny_internal_templates.packages + chat_templates = [ + *shiny_internal_templates.chat_hello_providers, + *shiny_internal_templates.chat_enterprise, + ] + + menu_choices = [ + Choice(title="Custom JavaScript component...", value="_js-component"), + Choice(title="Generative AI templates...", value="_chat-ai"), + Choice( + title="Choose from the Shiny Templates website", value="_external-gallery" + ), + ] + if question_state is None: - template = questionary.select( - "Which template would you like to use?:", - choices=[*choice_from_dict(app_template_choices), cancel_choice], - style=styles_for_questions, - ).ask() - else: - template = question_state - - valid_template_choices = {**app_template_choices, **package_template_choices} - if template not in valid_template_choices.values(): - raise click.BadOptionUsage( - "--template", - f"Invalid value for '--template' / '-t': {template} is not one of " - + f"""'{"', '".join(valid_template_choices.values())}'.""", - ) + question_state = question_choose_template(app_templates, *menu_choices) - # Define the control flow for the top level menu - if template is None or template == "cancel": - sys.exit(1) - elif template == "external-gallery": - url = cli_url("https://shiny.posit.co/py/templates") - click.echo(f"Opening {url} in your browser.") + template = template_by_name( + [*app_templates, *pkg_templates, *chat_templates], question_state + ) + + if template is not None: + if template.type == "app": + return app_template_questions(template, mode, dest_dir=dest_dir) + if template.type == "package": + return package_template_questions( + template, dest_dir=dest_dir, package_name=package_name + ) + + if question_state == "_external-gallery": + url = "https://shiny.posit.co/py/templates" + click.echo(f"Opening {cli_url(url)} in your browser.") click.echo( f"Choose a template and copy the {cli_code('shiny create')} command to use it." ) @@ -121,13 +295,124 @@ def use_template_internal( webbrowser.open(url) sys.exit(0) - elif template == "js-component": - js_component_questions(dest_dir=dest_dir, package_name=package_name) - return - elif template in package_template_choices.values(): - js_component_questions(template, dest_dir=dest_dir, package_name=package_name) + elif question_state == "_js-component": + use_internal_package_template(dest_dir=dest_dir, package_name=package_name) + elif question_state == "_chat-ai": + use_internal_chat_ai_template(dest_dir=dest_dir, package_name=package_name) else: - app_template_questions(template, mode, dest_dir=dest_dir) + valid_choices = [t.id for t in app_templates + pkg_templates] + if question_state not in valid_choices: + raise click.BadOptionUsage( + "--template", + f"Invalid value for '--template' / '-t': {question_state} is not one of " + + f"""'{"', '".join(valid_choices)}'.""", + ) + + +def use_internal_package_template( + dest_dir: Optional[Path] = None, + package_name: Optional[str] = None, +): + input = questionary.select( + "What kind of component do you want to build?", + choices=[ + *choice_from_templates(shiny_internal_templates.packages), + back_choice, + cancel_choice, + ], + style=styles_for_questions, + ).ask() + + if input == "back": + use_internal_template() + return + + if input is None or input == "cancel": + sys.exit(1) + + template = template_by_name(shiny_internal_templates.packages, input) + + if template is None: + # This should be valid because we're selecting from the list of templates + # but just in case and to make type checkers happy + raise ValueError(f"Package template for {input} not found.") + + package_template_questions(template, dest_dir=dest_dir, package_name=package_name) + + +def use_internal_chat_ai_template( + input: str | None = None, + dest_dir: Optional[Path] = None, + package_name: Optional[str] = None, +): + if input is None: + input = questionary.select( + "Which kind of generative AI template would you like to use?", + choices=[ + Choice(title="By provider...", value="_chat-ai_hello-providers"), + Choice(title="Enterprise providers...", value="_chat-ai_enterprise"), + back_choice, + cancel_choice, + ], + style=styles_for_questions, + ).ask() + + if input is None or input == "cancel": + sys.exit(1) + + if input == "back": + use_internal_template(dest_dir=dest_dir, package_name=package_name) + return + + use_internal_chat_ai_template( + input, dest_dir=dest_dir, package_name=package_name + ) + return + + template_choices = ( + shiny_internal_templates.chat_enterprise + if input == "_chat-ai_enterprise" + else shiny_internal_templates.chat_hello_providers + ) + + choice = question_choose_template(template_choices, back_choice) + + if choice == "back": + use_internal_chat_ai_template(dest_dir=dest_dir, package_name=package_name) + return + + template = template_by_name( + [ + *shiny_internal_templates.chat_hello_providers, + *shiny_internal_templates.chat_enterprise, + ], + choice, + ) + + if template is None: + raise ValueError(f"Chat AI template for {choice} not found.") + + app_template_questions(template, dest_dir=dest_dir, mode=None) + + +def question_choose_template( + templates: list[ShinyTemplate], + *extras: Choice, +) -> str: + """ + Ask the user to pick one of the templates. Includes and handles the cancel choice. + """ + + choice = questionary.select( + "Which template would you like to use?", + choices=[*choice_from_templates(templates), *extras, cancel_choice], + style=styles_for_questions, + ).ask() + + if choice is None or choice == "cancel": + sys.exit(1) + + return choice def download_and_extract_zip(url: str, temp_dir: Path) -> Path: @@ -159,11 +444,12 @@ def download_and_extract_zip(url: str, temp_dir: Path) -> Path: return temp_dir -def use_template_github( +def use_github_template( github: str, - template: str | None = None, + template_name: str | None = None, mode: str | None = None, dest_dir: Path | None = None, + package_name: str | None = None, ): # Github requires that we download the whole repository, so we need to # download and unzip the repo, then navigate to the subdirectory. @@ -208,12 +494,53 @@ def use_template_github( f"Template directory '{cli_input(spec.path)}' does not exist in {cli_field(spec_cli)}." ) - return app_template_questions( - template=template, - mode=mode, - template_dir=Path(template_dir), - dest_dir=dest_dir, - ) + templates = find_templates(template_dir) + + if not templates: + # Legacy: repo doesn't have _template.json files, so we have to rely on + # paths, i.e. template_dir / template_name + if template_name is None: + # warn that we're assuming the repo spec points to the template directly + click.echo( + cli_info( + f"Using {cli_field(spec_cli)} as the template. " + + f"Use {cli_code('--template')} to specify a template otherwise." + ) + ) + template_name = template_dir.name + else: + template_dir = template_dir / template_name + + template = ShinyTemplate( + id=template_name, + title=f"Template from {spec_cli}", + path=template_dir, + ) + elif template_name: + # Repo has templates and the user already picked one + template = template_by_name(templates, template_name) + if not template: + raise click.ClickException( + f"Template '{cli_input(template_name)}' not found in {cli_field(spec_cli)}." + ) + else: + # Has templates, but the user needs to pick one + template_name = question_choose_template(templates) + template = template_by_name(templates, template_name) + + if not template: + raise click.ClickException( + f"Template '{cli_input(template_name)}' not found in {cli_field(spec_cli)}." + ) + + if template.type == "package": + return package_template_questions( + template, + dest_dir=dest_dir, + package_name=package_name, + ) + else: + return app_template_questions(template, dest_dir=dest_dir, mode=mode) def github_zip_url(spec: GithubRepoLocation) -> Generator[str]: @@ -318,41 +645,22 @@ def parse_github_url(x: str) -> GithubRepoLocation: def app_template_questions( - template: Optional[str] = None, + template: ShinyTemplate, mode: Optional[str] = None, - template_dir: Optional[Path] = None, dest_dir: Optional[Path] = None, ): - if template_dir is None: - if template is None: - raise ValueError("You must provide either template or template_dir") - template_dir = Path(__file__).parent / "templates/app-templates" / template - elif template is not None: - template_dir = template_dir / template - - # FIXME: We don't have any special syntax of files to signal a "template", which - # means that we could end up here with `template_dir` being a repo of templates. If - # `template` is missing, we end up copying everything in `template_dir` as if it's - # all part of a single big template. When we introduce a way to signal or coordinate - # templates in a repo, we will add a check here to avoid copying more than one - # template. - click.echo( - cli_wait( - f"Creating Shiny app from template {cli_bold(cli_field(template_dir.name))}..." - ) - ) + template_dir = template.path + template_cli_name = cli_bold(cli_field(template.title or template.id)) - # Not all apps will be implemented in both express and core so we can - # avoid the questions if it's a core only app. - template_files = [file.name for file in template_dir.iterdir() if file.is_file()] - express_available = "app-express.py" in template_files - - if mode == "express" and not express_available: - raise Exception("Express mode not available for that template.") + if mode == "express" and not template.express_available: + raise click.BadParameter( + f"Express mode not available for the {template_cli_name} template." + ) + click.echo(cli_wait(f"Creating {template_cli_name} Shiny app...")) dest_dir = directory_prompt(dest_dir, template_dir.name) - if mode is None and express_available: + if mode is None and template.express_available: mode = questionary.select( "Would you like to use Shiny Express?", [ @@ -366,15 +674,10 @@ def app_template_questions( if mode is None or mode == "cancel": sys.exit(1) if mode == "back": - use_template_internal() + use_internal_template() return - app_dir = copy_template_files( - dest_dir, - template_dir=template_dir, - express_available=express_available, - mode=mode, - ) + app_dir = copy_template_files(template, dest_dir, mode=mode) click.echo(cli_success(f"Created Shiny app at {cli_field(str(app_dir))}")) click.echo() @@ -392,36 +695,14 @@ def app_template_questions( ) click.echo(f"- Open and edit the app file: {cli_field(str(app_dir / 'app.py'))}") + click_echo_next_steps_and_follow_up(template) + -def js_component_questions( - component_type: Optional[str] = None, +def package_template_questions( + template: ShinyTemplate, dest_dir: Optional[Path] = None, package_name: Optional[str] = None, ): - """ - Hand question branch for the custom js templates. This should handle the entire rest - of the question flow and is responsible for placing files etc. Currently it repeats - a lot of logic from the default flow but as the custom templates get more - complicated the logic will diverge - """ - if component_type is None: - component_type = questionary.select( - "What kind of component do you want to build?:", - choices=[ - *choice_from_dict(package_template_choices), - back_choice, - cancel_choice, - ], - style=styles_for_questions, - ).ask() - - if component_type == "back": - use_template_internal() - return - - if component_type is None or component_type == "cancel": - sys.exit(1) - # Ask what the user wants the name of their component to be if package_name is None: package_name = questionary.text( @@ -433,20 +714,12 @@ def js_component_questions( if package_name is None: sys.exit(1) - template_dir = ( - Path(__file__).parent / "templates/package-templates" / component_type - ) - - dest_dir = directory_prompt(dest_dir, package_name) - app_dir = copy_template_files( - dest_dir, - template_dir=template_dir, - express_available=False, + template, + dest_dir=directory_prompt(dest_dir, package_name), mode=None, ) - # Print messsage saying we're building the component click.echo(cli_wait(f"Setting up {cli_field(package_name)} component package...")) update_component_name_in_template(app_dir, package_name) @@ -468,21 +741,22 @@ def js_component_questions( f"- Open and run the example app in the {cli_field('example-app')} directory" ) + click_echo_next_steps_and_follow_up(template) + def copy_template_files( - app_dir: Path, - template_dir: Path, - express_available: bool, + template: ShinyTemplate, + dest_dir: Path, mode: Optional[str] = None, ): - files_to_check = [file.name for file in template_dir.iterdir()] + files_to_check = [file.name for file in template.path.iterdir()] if "__pycache__" in files_to_check: files_to_check.remove("__pycache__") files_to_check.append("app.py") - duplicate_files = [file for file in files_to_check if (app_dir / file).exists()] + duplicate_files = [file for file in files_to_check if (dest_dir / file).exists()] if any(duplicate_files): err_files = ", ".join([cli_input('"' + file + '"') for file in duplicate_files]) @@ -494,26 +768,53 @@ def copy_template_files( ) sys.exit(1) - if not app_dir.exists(): - app_dir.mkdir() + if not dest_dir.exists(): + dest_dir.mkdir() - for item in template_dir.iterdir(): + for item in template.path.iterdir(): if item.is_file(): - shutil.copy(item, app_dir / item.name) + if item.name == "_template.json": + continue + shutil.copy(item, dest_dir / item.name) else: if item.name != "__pycache__": - shutil.copytree(item, app_dir / item.name) + shutil.copytree(item, dest_dir / item.name) - def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = app_dir): + def rename_unlink(file_to_rename: str, file_to_delete: str, dir: Path = dest_dir): (dir / file_to_rename).rename(dir / "app.py") (dir / file_to_delete).unlink() - if express_available: + if template.express_available: if mode == "express": rename_unlink("app-express.py", "app-core.py") if mode == "core": rename_unlink("app-core.py", "app-express.py") - if (app_dir / "app-core.py").exists(): - (app_dir / "app-core.py").rename(app_dir / "app.py") + if (dest_dir / "app-core.py").exists(): + (dest_dir / "app-core.py").rename(dest_dir / "app.py") + + return dest_dir + + +def click_echo_next_steps_and_follow_up(template: ShinyTemplate): + for next_step in template.next_steps: + click.echo(f"- {next_step}") + + if len(template.follow_up) > 0: + click.echo() + for follow_up in template.follow_up: + click.echo(cli_follow_up(follow_up)) + + +def cli_follow_up(follow_up: ShinyTemplateFollowUp): + if follow_up.type == "text": + return follow_up.text + if follow_up.type == "action": + return cli_action(follow_up.text) + if follow_up.type == "info": + return cli_info(follow_up.text) + if follow_up.type == "warning": + return cli_danger(follow_up.text) + if follow_up.type == "danger": + return cli_danger(follow_up.text) - return app_dir + return follow_up.text diff --git a/shiny/templates/app/01-basic-app/_template.json b/shiny/templates/app/01-basic-app/_template.json new file mode 100644 index 000000000..0ea6216f1 --- /dev/null +++ b/shiny/templates/app/01-basic-app/_template.json @@ -0,0 +1,19 @@ +{ + "type": "app", + "id": "basic-app", + "title": "Basic app", + "description": "A basic Shiny app template.", + "next_steps": [ + "Run the app with `shiny run app.py` from the app directory." + ], + "follow_up": [ + { + "type": "info", + "text": "Just getting started with Shiny?" + }, + { + "type": "action", + "text": "Learn more at https://shiny.posit.co/py/docs/overview.html" + } + ] +} diff --git a/shiny/templates/app-templates/basic-app/app-core.py b/shiny/templates/app/01-basic-app/app-core.py similarity index 100% rename from shiny/templates/app-templates/basic-app/app-core.py rename to shiny/templates/app/01-basic-app/app-core.py diff --git a/shiny/templates/app-templates/basic-app/app-express.py b/shiny/templates/app/01-basic-app/app-express.py similarity index 100% rename from shiny/templates/app-templates/basic-app/app-express.py rename to shiny/templates/app/01-basic-app/app-express.py diff --git a/shiny/templates/app/02-basic-sidebar/_template.json b/shiny/templates/app/02-basic-sidebar/_template.json new file mode 100644 index 000000000..1fdbf7efb --- /dev/null +++ b/shiny/templates/app/02-basic-sidebar/_template.json @@ -0,0 +1,19 @@ +{ + "type": "app", + "id": "basic-sidebar", + "title": "Sidebar layout", + "description": "An app with inputs in a sidebar and a plot in the main area.", + "next_steps": [ + "Run the app with `shiny run app.py`." + ], + "follow_up": [ + { + "type": "info", + "text": "Just getting started with Shiny?" + }, + { + "type": "action", + "text": "Learn more at https://shiny.posit.co/py/docs/overview.html" + } + ] +} diff --git a/shiny/templates/app-templates/basic-sidebar/app-core.py b/shiny/templates/app/02-basic-sidebar/app-core.py similarity index 100% rename from shiny/templates/app-templates/basic-sidebar/app-core.py rename to shiny/templates/app/02-basic-sidebar/app-core.py diff --git a/shiny/templates/app-templates/basic-sidebar/app-express.py b/shiny/templates/app/02-basic-sidebar/app-express.py similarity index 100% rename from shiny/templates/app-templates/basic-sidebar/app-express.py rename to shiny/templates/app/02-basic-sidebar/app-express.py diff --git a/shiny/templates/app-templates/basic-navigation/penguins.csv b/shiny/templates/app/02-basic-sidebar/penguins.csv similarity index 100% rename from shiny/templates/app-templates/basic-navigation/penguins.csv rename to shiny/templates/app/02-basic-sidebar/penguins.csv diff --git a/shiny/templates/app-templates/basic-navigation/requirements.txt b/shiny/templates/app/02-basic-sidebar/requirements.txt similarity index 100% rename from shiny/templates/app-templates/basic-navigation/requirements.txt rename to shiny/templates/app/02-basic-sidebar/requirements.txt diff --git a/shiny/templates/app-templates/basic-navigation/shared.py b/shiny/templates/app/02-basic-sidebar/shared.py similarity index 100% rename from shiny/templates/app-templates/basic-navigation/shared.py rename to shiny/templates/app/02-basic-sidebar/shared.py diff --git a/shiny/templates/app/03-dashboard/_template.json b/shiny/templates/app/03-dashboard/_template.json new file mode 100644 index 000000000..09ad15e60 --- /dev/null +++ b/shiny/templates/app/03-dashboard/_template.json @@ -0,0 +1,19 @@ +{ + "type": "app", + "id": "dashboard", + "title": "Basic dashboard", + "description": "A basic, single page dashboard with value boxes, two plots in cards and a sidebar.", + "next_steps": [ + "Run the app with `shiny run app.py`." + ], + "follow_up": [ + { + "type": "info", + "text": "Just getting started with Shiny?" + }, + { + "type": "action", + "text": "Learn more at https://shiny.posit.co/py/docs/user-interfaces.html" + } + ] +} diff --git a/shiny/templates/app-templates/dashboard/app-core.py b/shiny/templates/app/03-dashboard/app-core.py similarity index 100% rename from shiny/templates/app-templates/dashboard/app-core.py rename to shiny/templates/app/03-dashboard/app-core.py diff --git a/shiny/templates/app-templates/dashboard/app-express.py b/shiny/templates/app/03-dashboard/app-express.py similarity index 100% rename from shiny/templates/app-templates/dashboard/app-express.py rename to shiny/templates/app/03-dashboard/app-express.py diff --git a/shiny/templates/app-templates/basic-sidebar/penguins.csv b/shiny/templates/app/03-dashboard/penguins.csv similarity index 100% rename from shiny/templates/app-templates/basic-sidebar/penguins.csv rename to shiny/templates/app/03-dashboard/penguins.csv diff --git a/shiny/templates/app-templates/dashboard/requirements.txt b/shiny/templates/app/03-dashboard/requirements.txt similarity index 100% rename from shiny/templates/app-templates/dashboard/requirements.txt rename to shiny/templates/app/03-dashboard/requirements.txt diff --git a/shiny/templates/app-templates/basic-sidebar/shared.py b/shiny/templates/app/03-dashboard/shared.py similarity index 100% rename from shiny/templates/app-templates/basic-sidebar/shared.py rename to shiny/templates/app/03-dashboard/shared.py diff --git a/shiny/templates/app-templates/dashboard/styles.css b/shiny/templates/app/03-dashboard/styles.css similarity index 100% rename from shiny/templates/app-templates/dashboard/styles.css rename to shiny/templates/app/03-dashboard/styles.css diff --git a/shiny/templates/app-templates/dashboard-tips/README.md b/shiny/templates/app/04-dashboard-tips/README.md similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/README.md rename to shiny/templates/app/04-dashboard-tips/README.md diff --git a/shiny/templates/app/04-dashboard-tips/_template.json b/shiny/templates/app/04-dashboard-tips/_template.json new file mode 100644 index 000000000..0c514ac0d --- /dev/null +++ b/shiny/templates/app/04-dashboard-tips/_template.json @@ -0,0 +1,19 @@ +{ + "type": "app", + "id": "dashboard-tips", + "title": "Intermediate dashboard", + "description": "An intermediate dashboard with value boxes, several plots in cards and a sidebar.", + "next_steps": [ + "Run the app with `shiny run app.py`." + ], + "follow_up": [ + { + "type": "info", + "text": "Just getting started with Shiny?" + }, + { + "type": "action", + "text": "Learn more at https://shiny.posit.co/py/docs/user-interfaces.html" + } + ] +} diff --git a/shiny/templates/app-templates/dashboard-tips/app-core.py b/shiny/templates/app/04-dashboard-tips/app-core.py similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/app-core.py rename to shiny/templates/app/04-dashboard-tips/app-core.py diff --git a/shiny/templates/app-templates/dashboard-tips/app-express.py b/shiny/templates/app/04-dashboard-tips/app-express.py similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/app-express.py rename to shiny/templates/app/04-dashboard-tips/app-express.py diff --git a/shiny/templates/app-templates/dashboard-tips/requirements.txt b/shiny/templates/app/04-dashboard-tips/requirements.txt similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/requirements.txt rename to shiny/templates/app/04-dashboard-tips/requirements.txt diff --git a/shiny/templates/app-templates/dashboard-tips/shared.py b/shiny/templates/app/04-dashboard-tips/shared.py similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/shared.py rename to shiny/templates/app/04-dashboard-tips/shared.py diff --git a/shiny/templates/app-templates/dashboard-tips/styles.css b/shiny/templates/app/04-dashboard-tips/styles.css similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/styles.css rename to shiny/templates/app/04-dashboard-tips/styles.css diff --git a/shiny/templates/app-templates/dashboard-tips/tips.csv b/shiny/templates/app/04-dashboard-tips/tips.csv similarity index 100% rename from shiny/templates/app-templates/dashboard-tips/tips.csv rename to shiny/templates/app/04-dashboard-tips/tips.csv diff --git a/shiny/templates/app/05-basic-navigation/_template.json b/shiny/templates/app/05-basic-navigation/_template.json new file mode 100644 index 000000000..9d530fcca --- /dev/null +++ b/shiny/templates/app/05-basic-navigation/_template.json @@ -0,0 +1,19 @@ +{ + "type": "app", + "id": "basic-navigation", + "title": "Navigating multiple pages/panels", + "description": "An app with a top navigation bar and two pages.", + "next_steps": [ + "Run the app with `shiny run app.py`." + ], + "follow_up": [ + { + "type": "info", + "text": "Just getting started with Shiny?" + }, + { + "type": "action", + "text": "Learn more at https://shiny.posit.co/py/docs/user-interfaces.html" + } + ] +} diff --git a/shiny/templates/app-templates/basic-navigation/app-core.py b/shiny/templates/app/05-basic-navigation/app-core.py similarity index 100% rename from shiny/templates/app-templates/basic-navigation/app-core.py rename to shiny/templates/app/05-basic-navigation/app-core.py diff --git a/shiny/templates/app-templates/basic-navigation/app-express.py b/shiny/templates/app/05-basic-navigation/app-express.py similarity index 100% rename from shiny/templates/app-templates/basic-navigation/app-express.py rename to shiny/templates/app/05-basic-navigation/app-express.py diff --git a/shiny/templates/app-templates/dashboard/penguins.csv b/shiny/templates/app/05-basic-navigation/penguins.csv similarity index 100% rename from shiny/templates/app-templates/dashboard/penguins.csv rename to shiny/templates/app/05-basic-navigation/penguins.csv diff --git a/shiny/templates/app-templates/basic-sidebar/requirements.txt b/shiny/templates/app/05-basic-navigation/requirements.txt similarity index 100% rename from shiny/templates/app-templates/basic-sidebar/requirements.txt rename to shiny/templates/app/05-basic-navigation/requirements.txt diff --git a/shiny/templates/app-templates/dashboard/shared.py b/shiny/templates/app/05-basic-navigation/shared.py similarity index 100% rename from shiny/templates/app-templates/dashboard/shared.py rename to shiny/templates/app/05-basic-navigation/shared.py diff --git a/shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json new file mode 100644 index 000000000..39712d0f7 --- /dev/null +++ b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-anthropic-aws", + "title": "Chat AI using Anthropic via AWS Bedrock" +} diff --git a/examples/chat/enterprise/aws-bedrock-anthropic/app.py b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/app.py similarity index 100% rename from examples/chat/enterprise/aws-bedrock-anthropic/app.py rename to shiny/templates/chat/enterprise/aws-bedrock-anthropic/app.py diff --git a/examples/chat/enterprise/aws-bedrock-anthropic/app_utils.py b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/app_utils.py similarity index 100% rename from examples/chat/enterprise/aws-bedrock-anthropic/app_utils.py rename to shiny/templates/chat/enterprise/aws-bedrock-anthropic/app_utils.py diff --git a/examples/chat/enterprise/aws-bedrock-anthropic/requirements.txt b/shiny/templates/chat/enterprise/aws-bedrock-anthropic/requirements.txt similarity index 100% rename from examples/chat/enterprise/aws-bedrock-anthropic/requirements.txt rename to shiny/templates/chat/enterprise/aws-bedrock-anthropic/requirements.txt diff --git a/shiny/templates/chat/enterprise/azure-openai/_template.json b/shiny/templates/chat/enterprise/azure-openai/_template.json new file mode 100644 index 000000000..14702f9c7 --- /dev/null +++ b/shiny/templates/chat/enterprise/azure-openai/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-azure-openai", + "title": "Chat AI using OpenAI via Azure" +} diff --git a/examples/chat/enterprise/azure-openai/app.py b/shiny/templates/chat/enterprise/azure-openai/app.py similarity index 100% rename from examples/chat/enterprise/azure-openai/app.py rename to shiny/templates/chat/enterprise/azure-openai/app.py diff --git a/examples/chat/enterprise/azure-openai/app_utils.py b/shiny/templates/chat/enterprise/azure-openai/app_utils.py similarity index 100% rename from examples/chat/enterprise/azure-openai/app_utils.py rename to shiny/templates/chat/enterprise/azure-openai/app_utils.py diff --git a/examples/chat/enterprise/azure-openai/requirements.txt b/shiny/templates/chat/enterprise/azure-openai/requirements.txt similarity index 100% rename from examples/chat/enterprise/azure-openai/requirements.txt rename to shiny/templates/chat/enterprise/azure-openai/requirements.txt diff --git a/shiny/templates/chat/hello-providers/anthropic/_template.json b/shiny/templates/chat/hello-providers/anthropic/_template.json new file mode 100644 index 000000000..79e2bf257 --- /dev/null +++ b/shiny/templates/chat/hello-providers/anthropic/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-anthropic", + "title": "Chat AI using Anthropic" +} diff --git a/shiny/templates/chat/hello-providers/anthropic/app.py b/shiny/templates/chat/hello-providers/anthropic/app.py new file mode 100644 index 000000000..9c7653ea3 --- /dev/null +++ b/shiny/templates/chat/hello-providers/anthropic/app.py @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------------------ +# A basic Shiny Chat example powered by Anthropic's Claude model. +# To run it, you'll need an Anthropic API key. +# To get one, follow the instructions at https://docs.anthropic.com/en/api/getting-started +# ------------------------------------------------------------------------------------ +import os + +from anthropic import AsyncAnthropic +from app_utils import load_dotenv + +from shiny.express import ui + +# Either explicitly set the ANTHROPIC_API_KEY environment variable before launching the +# app, or set them in a file named `.env`. The `python-dotenv` package will load `.env` +# as environment variables which can later be read by `os.getenv()`. +load_dotenv() +llm = AsyncAnthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) + +# Set some Shiny page options +ui.page_opts( + title="Hello Anthropic Claude Chat", + fillable=True, + fillable_mobile=True, +) + +# Create and display empty chat +chat = ui.Chat(id="chat") +chat.ui() + + +# Define a callback to run when the user submits a message +@chat.on_user_submit +async def _(): + # Get messages currently in the chat + messages = chat.messages(format="anthropic") + # Create a response message stream + response = await llm.messages.create( + model="claude-3-opus-20240229", + messages=messages, + stream=True, + max_tokens=1000, + ) + # Append the response stream into the chat + await chat.append_message_stream(response) diff --git a/examples/chat/hello-providers/anthropic/app_utils.py b/shiny/templates/chat/hello-providers/anthropic/app_utils.py similarity index 100% rename from examples/chat/hello-providers/anthropic/app_utils.py rename to shiny/templates/chat/hello-providers/anthropic/app_utils.py diff --git a/examples/chat/hello-providers/anthropic/requirements.txt b/shiny/templates/chat/hello-providers/anthropic/requirements.txt similarity index 100% rename from examples/chat/hello-providers/anthropic/requirements.txt rename to shiny/templates/chat/hello-providers/anthropic/requirements.txt diff --git a/shiny/templates/chat/hello-providers/gemini/_template.json b/shiny/templates/chat/hello-providers/gemini/_template.json new file mode 100644 index 000000000..baf30e7cd --- /dev/null +++ b/shiny/templates/chat/hello-providers/gemini/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-gemini", + "title": "Chat AI using Google Gemini" +} diff --git a/examples/chat/hello-providers/gemini/app.py b/shiny/templates/chat/hello-providers/gemini/app.py similarity index 100% rename from examples/chat/hello-providers/gemini/app.py rename to shiny/templates/chat/hello-providers/gemini/app.py diff --git a/examples/chat/hello-providers/gemini/app_utils.py b/shiny/templates/chat/hello-providers/gemini/app_utils.py similarity index 100% rename from examples/chat/hello-providers/gemini/app_utils.py rename to shiny/templates/chat/hello-providers/gemini/app_utils.py diff --git a/examples/chat/hello-providers/gemini/requirements.txt b/shiny/templates/chat/hello-providers/gemini/requirements.txt similarity index 100% rename from examples/chat/hello-providers/gemini/requirements.txt rename to shiny/templates/chat/hello-providers/gemini/requirements.txt diff --git a/shiny/templates/chat/hello-providers/langchain/_template.json b/shiny/templates/chat/hello-providers/langchain/_template.json new file mode 100644 index 000000000..3ac04a285 --- /dev/null +++ b/shiny/templates/chat/hello-providers/langchain/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-langchain", + "title": "Chat AI using LangChain" +} diff --git a/examples/chat/hello-providers/langchain/app.py b/shiny/templates/chat/hello-providers/langchain/app.py similarity index 100% rename from examples/chat/hello-providers/langchain/app.py rename to shiny/templates/chat/hello-providers/langchain/app.py diff --git a/examples/chat/hello-providers/langchain/app_utils.py b/shiny/templates/chat/hello-providers/langchain/app_utils.py similarity index 100% rename from examples/chat/hello-providers/langchain/app_utils.py rename to shiny/templates/chat/hello-providers/langchain/app_utils.py diff --git a/examples/chat/hello-providers/langchain/requirements.txt b/shiny/templates/chat/hello-providers/langchain/requirements.txt similarity index 100% rename from examples/chat/hello-providers/langchain/requirements.txt rename to shiny/templates/chat/hello-providers/langchain/requirements.txt diff --git a/shiny/templates/chat/hello-providers/ollama/_template.json b/shiny/templates/chat/hello-providers/ollama/_template.json new file mode 100644 index 000000000..9a1c53ccc --- /dev/null +++ b/shiny/templates/chat/hello-providers/ollama/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-ollama", + "title": "Chat AI using Ollama" +} diff --git a/examples/chat/hello-providers/ollama/app.py b/shiny/templates/chat/hello-providers/ollama/app.py similarity index 100% rename from examples/chat/hello-providers/ollama/app.py rename to shiny/templates/chat/hello-providers/ollama/app.py diff --git a/examples/chat/hello-providers/ollama/requirements.txt b/shiny/templates/chat/hello-providers/ollama/requirements.txt similarity index 100% rename from examples/chat/hello-providers/ollama/requirements.txt rename to shiny/templates/chat/hello-providers/ollama/requirements.txt diff --git a/shiny/templates/chat/hello-providers/openai/_template.json b/shiny/templates/chat/hello-providers/openai/_template.json new file mode 100644 index 000000000..89bfb15d3 --- /dev/null +++ b/shiny/templates/chat/hello-providers/openai/_template.json @@ -0,0 +1,5 @@ +{ + "type": "app", + "id": "chat-ai-openai", + "title": "Chat AI using OpenAI" +} diff --git a/examples/chat/hello-providers/openai/app.py b/shiny/templates/chat/hello-providers/openai/app.py similarity index 100% rename from examples/chat/hello-providers/openai/app.py rename to shiny/templates/chat/hello-providers/openai/app.py diff --git a/examples/chat/hello-providers/openai/app_utils.py b/shiny/templates/chat/hello-providers/openai/app_utils.py similarity index 100% rename from examples/chat/hello-providers/openai/app_utils.py rename to shiny/templates/chat/hello-providers/openai/app_utils.py diff --git a/examples/chat/hello-providers/openai/requirements.txt b/shiny/templates/chat/hello-providers/openai/requirements.txt similarity index 100% rename from examples/chat/hello-providers/openai/requirements.txt rename to shiny/templates/chat/hello-providers/openai/requirements.txt diff --git a/shiny/templates/package-templates/js-input/custom_component/distjs/index.js b/shiny/templates/package-templates/js-input/custom_component/distjs/index.js deleted file mode 100644 index 620c5999a..000000000 --- a/shiny/templates/package-templates/js-input/custom_component/distjs/index.js +++ /dev/null @@ -1,814 +0,0 @@ -"use strict"; -(() => { - var __defProp = Object.defineProperty; - var __getOwnPropDesc = Object.getOwnPropertyDescriptor; - var __decorateClass = (decorators, target, key, kind) => { - var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; - for (var i4 = decorators.length - 1, decorator; i4 >= 0; i4--) - if (decorator = decorators[i4]) - result = (kind ? decorator(target, key, result) : decorator(result)) || result; - if (kind && result) - __defProp(target, key, result); - return result; - }; - - // node_modules/@lit/reactive-element/css-tag.js - var t = globalThis; - var e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype; - var s = Symbol(); - var o = /* @__PURE__ */ new WeakMap(); - var n = class { - constructor(t4, e5, o5) { - if (this._$cssResult$ = true, o5 !== s) - throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); - this.cssText = t4, this.t = e5; - } - get styleSheet() { - let t4 = this.o; - const s4 = this.t; - if (e && void 0 === t4) { - const e5 = void 0 !== s4 && 1 === s4.length; - e5 && (t4 = o.get(s4)), void 0 === t4 && ((this.o = t4 = new CSSStyleSheet()).replaceSync(this.cssText), e5 && o.set(s4, t4)); - } - return t4; - } - toString() { - return this.cssText; - } - }; - var r = (t4) => new n("string" == typeof t4 ? t4 : t4 + "", void 0, s); - var i = (t4, ...e5) => { - const o5 = 1 === t4.length ? t4[0] : e5.reduce((e6, s4, o6) => e6 + ((t5) => { - if (true === t5._$cssResult$) - return t5.cssText; - if ("number" == typeof t5) - return t5; - throw Error("Value passed to 'css' function must be a 'css' function result: " + t5 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(s4) + t4[o6 + 1], t4[0]); - return new n(o5, t4, s); - }; - var S = (s4, o5) => { - if (e) - s4.adoptedStyleSheets = o5.map((t4) => t4 instanceof CSSStyleSheet ? t4 : t4.styleSheet); - else - for (const e5 of o5) { - const o6 = document.createElement("style"), n5 = t.litNonce; - void 0 !== n5 && o6.setAttribute("nonce", n5), o6.textContent = e5.cssText, s4.appendChild(o6); - } - }; - var c = e ? (t4) => t4 : (t4) => t4 instanceof CSSStyleSheet ? ((t5) => { - let e5 = ""; - for (const s4 of t5.cssRules) - e5 += s4.cssText; - return r(e5); - })(t4) : t4; - - // node_modules/@lit/reactive-element/reactive-element.js - var { is: i2, defineProperty: e2, getOwnPropertyDescriptor: r2, getOwnPropertyNames: h, getOwnPropertySymbols: o2, getPrototypeOf: n2 } = Object; - var a = globalThis; - var c2 = a.trustedTypes; - var l = c2 ? c2.emptyScript : ""; - var p = a.reactiveElementPolyfillSupport; - var d = (t4, s4) => t4; - var u = { toAttribute(t4, s4) { - switch (s4) { - case Boolean: - t4 = t4 ? l : null; - break; - case Object: - case Array: - t4 = null == t4 ? t4 : JSON.stringify(t4); - } - return t4; - }, fromAttribute(t4, s4) { - let i4 = t4; - switch (s4) { - case Boolean: - i4 = null !== t4; - break; - case Number: - i4 = null === t4 ? null : Number(t4); - break; - case Object: - case Array: - try { - i4 = JSON.parse(t4); - } catch (t5) { - i4 = null; - } - } - return i4; - } }; - var f = (t4, s4) => !i2(t4, s4); - var y = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f }; - Symbol.metadata ??= Symbol("metadata"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); - var b = class extends HTMLElement { - static addInitializer(t4) { - this._$Ei(), (this.l ??= []).push(t4); - } - static get observedAttributes() { - return this.finalize(), this._$Eh && [...this._$Eh.keys()]; - } - static createProperty(t4, s4 = y) { - if (s4.state && (s4.attribute = false), this._$Ei(), this.elementProperties.set(t4, s4), !s4.noAccessor) { - const i4 = Symbol(), r6 = this.getPropertyDescriptor(t4, i4, s4); - void 0 !== r6 && e2(this.prototype, t4, r6); - } - } - static getPropertyDescriptor(t4, s4, i4) { - const { get: e5, set: h3 } = r2(this.prototype, t4) ?? { get() { - return this[s4]; - }, set(t5) { - this[s4] = t5; - } }; - return { get() { - return e5?.call(this); - }, set(s5) { - const r6 = e5?.call(this); - h3.call(this, s5), this.requestUpdate(t4, r6, i4); - }, configurable: true, enumerable: true }; - } - static getPropertyOptions(t4) { - return this.elementProperties.get(t4) ?? y; - } - static _$Ei() { - if (this.hasOwnProperty(d("elementProperties"))) - return; - const t4 = n2(this); - t4.finalize(), void 0 !== t4.l && (this.l = [...t4.l]), this.elementProperties = new Map(t4.elementProperties); - } - static finalize() { - if (this.hasOwnProperty(d("finalized"))) - return; - if (this.finalized = true, this._$Ei(), this.hasOwnProperty(d("properties"))) { - const t5 = this.properties, s4 = [...h(t5), ...o2(t5)]; - for (const i4 of s4) - this.createProperty(i4, t5[i4]); - } - const t4 = this[Symbol.metadata]; - if (null !== t4) { - const s4 = litPropertyMetadata.get(t4); - if (void 0 !== s4) - for (const [t5, i4] of s4) - this.elementProperties.set(t5, i4); - } - this._$Eh = /* @__PURE__ */ new Map(); - for (const [t5, s4] of this.elementProperties) { - const i4 = this._$Eu(t5, s4); - void 0 !== i4 && this._$Eh.set(i4, t5); - } - this.elementStyles = this.finalizeStyles(this.styles); - } - static finalizeStyles(s4) { - const i4 = []; - if (Array.isArray(s4)) { - const e5 = new Set(s4.flat(1 / 0).reverse()); - for (const s5 of e5) - i4.unshift(c(s5)); - } else - void 0 !== s4 && i4.push(c(s4)); - return i4; - } - static _$Eu(t4, s4) { - const i4 = s4.attribute; - return false === i4 ? void 0 : "string" == typeof i4 ? i4 : "string" == typeof t4 ? t4.toLowerCase() : void 0; - } - constructor() { - super(), this._$Ep = void 0, this.isUpdatePending = false, this.hasUpdated = false, this._$Em = null, this._$Ev(); - } - _$Ev() { - this._$Eg = new Promise((t4) => this.enableUpdating = t4), this._$AL = /* @__PURE__ */ new Map(), this._$ES(), this.requestUpdate(), this.constructor.l?.forEach((t4) => t4(this)); - } - addController(t4) { - (this._$E_ ??= /* @__PURE__ */ new Set()).add(t4), void 0 !== this.renderRoot && this.isConnected && t4.hostConnected?.(); - } - removeController(t4) { - this._$E_?.delete(t4); - } - _$ES() { - const t4 = /* @__PURE__ */ new Map(), s4 = this.constructor.elementProperties; - for (const i4 of s4.keys()) - this.hasOwnProperty(i4) && (t4.set(i4, this[i4]), delete this[i4]); - t4.size > 0 && (this._$Ep = t4); - } - createRenderRoot() { - const t4 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return S(t4, this.constructor.elementStyles), t4; - } - connectedCallback() { - this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(true), this._$E_?.forEach((t4) => t4.hostConnected?.()); - } - enableUpdating(t4) { - } - disconnectedCallback() { - this._$E_?.forEach((t4) => t4.hostDisconnected?.()); - } - attributeChangedCallback(t4, s4, i4) { - this._$AK(t4, i4); - } - _$EO(t4, s4) { - const i4 = this.constructor.elementProperties.get(t4), e5 = this.constructor._$Eu(t4, i4); - if (void 0 !== e5 && true === i4.reflect) { - const r6 = (void 0 !== i4.converter?.toAttribute ? i4.converter : u).toAttribute(s4, i4.type); - this._$Em = t4, null == r6 ? this.removeAttribute(e5) : this.setAttribute(e5, r6), this._$Em = null; - } - } - _$AK(t4, s4) { - const i4 = this.constructor, e5 = i4._$Eh.get(t4); - if (void 0 !== e5 && this._$Em !== e5) { - const t5 = i4.getPropertyOptions(e5), r6 = "function" == typeof t5.converter ? { fromAttribute: t5.converter } : void 0 !== t5.converter?.fromAttribute ? t5.converter : u; - this._$Em = e5, this[e5] = r6.fromAttribute(s4, t5.type), this._$Em = null; - } - } - requestUpdate(t4, s4, i4, e5 = false, r6) { - if (void 0 !== t4) { - if (i4 ??= this.constructor.getPropertyOptions(t4), !(i4.hasChanged ?? f)(e5 ? r6 : this[t4], s4)) - return; - this.C(t4, s4, i4); - } - false === this.isUpdatePending && (this._$Eg = this._$EP()); - } - C(t4, s4, i4) { - this._$AL.has(t4) || this._$AL.set(t4, s4), true === i4.reflect && this._$Em !== t4 && (this._$Ej ??= /* @__PURE__ */ new Set()).add(t4); - } - async _$EP() { - this.isUpdatePending = true; - try { - await this._$Eg; - } catch (t5) { - Promise.reject(t5); - } - const t4 = this.scheduleUpdate(); - return null != t4 && await t4, !this.isUpdatePending; - } - scheduleUpdate() { - return this.performUpdate(); - } - performUpdate() { - if (!this.isUpdatePending) - return; - if (!this.hasUpdated) { - if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [t6, s5] of this._$Ep) - this[t6] = s5; - this._$Ep = void 0; - } - const t5 = this.constructor.elementProperties; - if (t5.size > 0) - for (const [s5, i4] of t5) - true !== i4.wrapped || this._$AL.has(s5) || void 0 === this[s5] || this.C(s5, this[s5], i4); - } - let t4 = false; - const s4 = this._$AL; - try { - t4 = this.shouldUpdate(s4), t4 ? (this.willUpdate(s4), this._$E_?.forEach((t5) => t5.hostUpdate?.()), this.update(s4)) : this._$ET(); - } catch (s5) { - throw t4 = false, this._$ET(), s5; - } - t4 && this._$AE(s4); - } - willUpdate(t4) { - } - _$AE(t4) { - this._$E_?.forEach((t5) => t5.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = true, this.firstUpdated(t4)), this.updated(t4); - } - _$ET() { - this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = false; - } - get updateComplete() { - return this.getUpdateComplete(); - } - getUpdateComplete() { - return this._$Eg; - } - shouldUpdate(t4) { - return true; - } - update(t4) { - this._$Ej &&= this._$Ej.forEach((t5) => this._$EO(t5, this[t5])), this._$ET(); - } - updated(t4) { - } - firstUpdated(t4) { - } - }; - b.elementStyles = [], b.shadowRootOptions = { mode: "open" }, b[d("elementProperties")] = /* @__PURE__ */ new Map(), b[d("finalized")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: b }), (a.reactiveElementVersions ??= []).push("2.0.2"); - - // node_modules/lit-html/lit-html.js - var t2 = globalThis; - var i3 = t2.trustedTypes; - var s2 = i3 ? i3.createPolicy("lit-html", { createHTML: (t4) => t4 }) : void 0; - var e3 = "$lit$"; - var h2 = `lit$${(Math.random() + "").slice(9)}$`; - var o3 = "?" + h2; - var n3 = `<${o3}>`; - var r3 = document; - var l2 = () => r3.createComment(""); - var c3 = (t4) => null === t4 || "object" != typeof t4 && "function" != typeof t4; - var a2 = Array.isArray; - var u2 = (t4) => a2(t4) || "function" == typeof t4?.[Symbol.iterator]; - var d2 = "[ \n\f\r]"; - var f2 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; - var v = /-->/g; - var _ = />/g; - var m = RegExp(`>|${d2}(?:([^\\s"'>=/]+)(${d2}*=${d2}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`, "g"); - var p2 = /'/g; - var g = /"/g; - var $2 = /^(?:script|style|textarea|title)$/i; - var y2 = (t4) => (i4, ...s4) => ({ _$litType$: t4, strings: i4, values: s4 }); - var x = y2(1); - var b2 = y2(2); - var w = Symbol.for("lit-noChange"); - var T = Symbol.for("lit-nothing"); - var A = /* @__PURE__ */ new WeakMap(); - var E = r3.createTreeWalker(r3, 129); - function C(t4, i4) { - if (!Array.isArray(t4) || !t4.hasOwnProperty("raw")) - throw Error("invalid template strings array"); - return void 0 !== s2 ? s2.createHTML(i4) : i4; - } - var P = (t4, i4) => { - const s4 = t4.length - 1, o5 = []; - let r6, l3 = 2 === i4 ? "" : "", c4 = f2; - for (let i5 = 0; i5 < s4; i5++) { - const s5 = t4[i5]; - let a3, u3, d3 = -1, y3 = 0; - for (; y3 < s5.length && (c4.lastIndex = y3, u3 = c4.exec(s5), null !== u3); ) - y3 = c4.lastIndex, c4 === f2 ? "!--" === u3[1] ? c4 = v : void 0 !== u3[1] ? c4 = _ : void 0 !== u3[2] ? ($2.test(u3[2]) && (r6 = RegExp("" === u3[0] ? (c4 = r6 ?? f2, d3 = -1) : void 0 === u3[1] ? d3 = -2 : (d3 = c4.lastIndex - u3[2].length, a3 = u3[1], c4 = void 0 === u3[3] ? m : '"' === u3[3] ? g : p2) : c4 === g || c4 === p2 ? c4 = m : c4 === v || c4 === _ ? c4 = f2 : (c4 = m, r6 = void 0); - const x2 = c4 === m && t4[i5 + 1].startsWith("/>") ? " " : ""; - l3 += c4 === f2 ? s5 + n3 : d3 >= 0 ? (o5.push(a3), s5.slice(0, d3) + e3 + s5.slice(d3) + h2 + x2) : s5 + h2 + (-2 === d3 ? i5 : x2); - } - return [C(t4, l3 + (t4[s4] || "") + (2 === i4 ? "" : "")), o5]; - }; - var V = class _V { - constructor({ strings: t4, _$litType$: s4 }, n5) { - let r6; - this.parts = []; - let c4 = 0, a3 = 0; - const u3 = t4.length - 1, d3 = this.parts, [f3, v2] = P(t4, s4); - if (this.el = _V.createElement(f3, n5), E.currentNode = this.el.content, 2 === s4) { - const t5 = this.el.content.firstChild; - t5.replaceWith(...t5.childNodes); - } - for (; null !== (r6 = E.nextNode()) && d3.length < u3; ) { - if (1 === r6.nodeType) { - if (r6.hasAttributes()) - for (const t5 of r6.getAttributeNames()) - if (t5.endsWith(e3)) { - const i4 = v2[a3++], s5 = r6.getAttribute(t5).split(h2), e5 = /([.?@])?(.*)/.exec(i4); - d3.push({ type: 1, index: c4, name: e5[2], strings: s5, ctor: "." === e5[1] ? k : "?" === e5[1] ? H : "@" === e5[1] ? I : R }), r6.removeAttribute(t5); - } else - t5.startsWith(h2) && (d3.push({ type: 6, index: c4 }), r6.removeAttribute(t5)); - if ($2.test(r6.tagName)) { - const t5 = r6.textContent.split(h2), s5 = t5.length - 1; - if (s5 > 0) { - r6.textContent = i3 ? i3.emptyScript : ""; - for (let i4 = 0; i4 < s5; i4++) - r6.append(t5[i4], l2()), E.nextNode(), d3.push({ type: 2, index: ++c4 }); - r6.append(t5[s5], l2()); - } - } - } else if (8 === r6.nodeType) - if (r6.data === o3) - d3.push({ type: 2, index: c4 }); - else { - let t5 = -1; - for (; -1 !== (t5 = r6.data.indexOf(h2, t5 + 1)); ) - d3.push({ type: 7, index: c4 }), t5 += h2.length - 1; - } - c4++; - } - } - static createElement(t4, i4) { - const s4 = r3.createElement("template"); - return s4.innerHTML = t4, s4; - } - }; - function N(t4, i4, s4 = t4, e5) { - if (i4 === w) - return i4; - let h3 = void 0 !== e5 ? s4._$Co?.[e5] : s4._$Cl; - const o5 = c3(i4) ? void 0 : i4._$litDirective$; - return h3?.constructor !== o5 && (h3?._$AO?.(false), void 0 === o5 ? h3 = void 0 : (h3 = new o5(t4), h3._$AT(t4, s4, e5)), void 0 !== e5 ? (s4._$Co ??= [])[e5] = h3 : s4._$Cl = h3), void 0 !== h3 && (i4 = N(t4, h3._$AS(t4, i4.values), h3, e5)), i4; - } - var S2 = class { - constructor(t4, i4) { - this._$AV = [], this._$AN = void 0, this._$AD = t4, this._$AM = i4; - } - get parentNode() { - return this._$AM.parentNode; - } - get _$AU() { - return this._$AM._$AU; - } - u(t4) { - const { el: { content: i4 }, parts: s4 } = this._$AD, e5 = (t4?.creationScope ?? r3).importNode(i4, true); - E.currentNode = e5; - let h3 = E.nextNode(), o5 = 0, n5 = 0, l3 = s4[0]; - for (; void 0 !== l3; ) { - if (o5 === l3.index) { - let i5; - 2 === l3.type ? i5 = new M(h3, h3.nextSibling, this, t4) : 1 === l3.type ? i5 = new l3.ctor(h3, l3.name, l3.strings, this, t4) : 6 === l3.type && (i5 = new L(h3, this, t4)), this._$AV.push(i5), l3 = s4[++n5]; - } - o5 !== l3?.index && (h3 = E.nextNode(), o5++); - } - return E.currentNode = r3, e5; - } - p(t4) { - let i4 = 0; - for (const s4 of this._$AV) - void 0 !== s4 && (void 0 !== s4.strings ? (s4._$AI(t4, s4, i4), i4 += s4.strings.length - 2) : s4._$AI(t4[i4])), i4++; - } - }; - var M = class _M { - get _$AU() { - return this._$AM?._$AU ?? this._$Cv; - } - constructor(t4, i4, s4, e5) { - this.type = 2, this._$AH = T, this._$AN = void 0, this._$AA = t4, this._$AB = i4, this._$AM = s4, this.options = e5, this._$Cv = e5?.isConnected ?? true; - } - get parentNode() { - let t4 = this._$AA.parentNode; - const i4 = this._$AM; - return void 0 !== i4 && 11 === t4?.nodeType && (t4 = i4.parentNode), t4; - } - get startNode() { - return this._$AA; - } - get endNode() { - return this._$AB; - } - _$AI(t4, i4 = this) { - t4 = N(this, t4, i4), c3(t4) ? t4 === T || null == t4 || "" === t4 ? (this._$AH !== T && this._$AR(), this._$AH = T) : t4 !== this._$AH && t4 !== w && this._(t4) : void 0 !== t4._$litType$ ? this.g(t4) : void 0 !== t4.nodeType ? this.$(t4) : u2(t4) ? this.T(t4) : this._(t4); - } - k(t4) { - return this._$AA.parentNode.insertBefore(t4, this._$AB); - } - $(t4) { - this._$AH !== t4 && (this._$AR(), this._$AH = this.k(t4)); - } - _(t4) { - this._$AH !== T && c3(this._$AH) ? this._$AA.nextSibling.data = t4 : this.$(r3.createTextNode(t4)), this._$AH = t4; - } - g(t4) { - const { values: i4, _$litType$: s4 } = t4, e5 = "number" == typeof s4 ? this._$AC(t4) : (void 0 === s4.el && (s4.el = V.createElement(C(s4.h, s4.h[0]), this.options)), s4); - if (this._$AH?._$AD === e5) - this._$AH.p(i4); - else { - const t5 = new S2(e5, this), s5 = t5.u(this.options); - t5.p(i4), this.$(s5), this._$AH = t5; - } - } - _$AC(t4) { - let i4 = A.get(t4.strings); - return void 0 === i4 && A.set(t4.strings, i4 = new V(t4)), i4; - } - T(t4) { - a2(this._$AH) || (this._$AH = [], this._$AR()); - const i4 = this._$AH; - let s4, e5 = 0; - for (const h3 of t4) - e5 === i4.length ? i4.push(s4 = new _M(this.k(l2()), this.k(l2()), this, this.options)) : s4 = i4[e5], s4._$AI(h3), e5++; - e5 < i4.length && (this._$AR(s4 && s4._$AB.nextSibling, e5), i4.length = e5); - } - _$AR(t4 = this._$AA.nextSibling, i4) { - for (this._$AP?.(false, true, i4); t4 && t4 !== this._$AB; ) { - const i5 = t4.nextSibling; - t4.remove(), t4 = i5; - } - } - setConnected(t4) { - void 0 === this._$AM && (this._$Cv = t4, this._$AP?.(t4)); - } - }; - var R = class { - get tagName() { - return this.element.tagName; - } - get _$AU() { - return this._$AM._$AU; - } - constructor(t4, i4, s4, e5, h3) { - this.type = 1, this._$AH = T, this._$AN = void 0, this.element = t4, this.name = i4, this._$AM = e5, this.options = h3, s4.length > 2 || "" !== s4[0] || "" !== s4[1] ? (this._$AH = Array(s4.length - 1).fill(new String()), this.strings = s4) : this._$AH = T; - } - _$AI(t4, i4 = this, s4, e5) { - const h3 = this.strings; - let o5 = false; - if (void 0 === h3) - t4 = N(this, t4, i4, 0), o5 = !c3(t4) || t4 !== this._$AH && t4 !== w, o5 && (this._$AH = t4); - else { - const e6 = t4; - let n5, r6; - for (t4 = h3[0], n5 = 0; n5 < h3.length - 1; n5++) - r6 = N(this, e6[s4 + n5], i4, n5), r6 === w && (r6 = this._$AH[n5]), o5 ||= !c3(r6) || r6 !== this._$AH[n5], r6 === T ? t4 = T : t4 !== T && (t4 += (r6 ?? "") + h3[n5 + 1]), this._$AH[n5] = r6; - } - o5 && !e5 && this.O(t4); - } - O(t4) { - t4 === T ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t4 ?? ""); - } - }; - var k = class extends R { - constructor() { - super(...arguments), this.type = 3; - } - O(t4) { - this.element[this.name] = t4 === T ? void 0 : t4; - } - }; - var H = class extends R { - constructor() { - super(...arguments), this.type = 4; - } - O(t4) { - this.element.toggleAttribute(this.name, !!t4 && t4 !== T); - } - }; - var I = class extends R { - constructor(t4, i4, s4, e5, h3) { - super(t4, i4, s4, e5, h3), this.type = 5; - } - _$AI(t4, i4 = this) { - if ((t4 = N(this, t4, i4, 0) ?? T) === w) - return; - const s4 = this._$AH, e5 = t4 === T && s4 !== T || t4.capture !== s4.capture || t4.once !== s4.once || t4.passive !== s4.passive, h3 = t4 !== T && (s4 === T || e5); - e5 && this.element.removeEventListener(this.name, this, s4), h3 && this.element.addEventListener(this.name, this, t4), this._$AH = t4; - } - handleEvent(t4) { - "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t4) : this._$AH.handleEvent(t4); - } - }; - var L = class { - constructor(t4, i4, s4) { - this.element = t4, this.type = 6, this._$AN = void 0, this._$AM = i4, this.options = s4; - } - get _$AU() { - return this._$AM._$AU; - } - _$AI(t4) { - N(this, t4); - } - }; - var Z = t2.litHtmlPolyfillSupport; - Z?.(V, M), (t2.litHtmlVersions ??= []).push("3.1.0"); - var j = (t4, i4, s4) => { - const e5 = s4?.renderBefore ?? i4; - let h3 = e5._$litPart$; - if (void 0 === h3) { - const t5 = s4?.renderBefore ?? null; - e5._$litPart$ = h3 = new M(i4.insertBefore(l2(), t5), t5, void 0, s4 ?? {}); - } - return h3._$AI(t4), h3; - }; - - // node_modules/lit-element/lit-element.js - var s3 = class extends b { - constructor() { - super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; - } - createRenderRoot() { - const t4 = super.createRenderRoot(); - return this.renderOptions.renderBefore ??= t4.firstChild, t4; - } - update(t4) { - const i4 = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t4), this._$Do = j(i4, this.renderRoot, this.renderOptions); - } - connectedCallback() { - super.connectedCallback(), this._$Do?.setConnected(true); - } - disconnectedCallback() { - super.disconnectedCallback(), this._$Do?.setConnected(false); - } - render() { - return w; - } - }; - s3._$litElement$ = true, s3["finalized", "finalized"] = true, globalThis.litElementHydrateSupport?.({ LitElement: s3 }); - var r4 = globalThis.litElementPolyfillSupport; - r4?.({ LitElement: s3 }); - (globalThis.litElementVersions ??= []).push("4.0.2"); - - // node_modules/@lit/reactive-element/decorators/custom-element.js - var t3 = (t4) => (e5, o5) => { - void 0 !== o5 ? o5.addInitializer(() => { - customElements.define(t4, e5); - }) : customElements.define(t4, e5); - }; - - // node_modules/@lit/reactive-element/decorators/property.js - var o4 = { attribute: true, type: String, converter: u, reflect: false, hasChanged: f }; - var r5 = (t4 = o4, e5, r6) => { - const { kind: n5, metadata: i4 } = r6; - let s4 = globalThis.litPropertyMetadata.get(i4); - if (void 0 === s4 && globalThis.litPropertyMetadata.set(i4, s4 = /* @__PURE__ */ new Map()), s4.set(r6.name, t4), "accessor" === n5) { - const { name: o5 } = r6; - return { set(r7) { - const n6 = e5.get.call(this); - e5.set.call(this, r7), this.requestUpdate(o5, n6, t4); - }, init(e6) { - return void 0 !== e6 && this.C(o5, void 0, t4), e6; - } }; - } - if ("setter" === n5) { - const { name: o5 } = r6; - return function(r7) { - const n6 = this[o5]; - e5.call(this, r7), this.requestUpdate(o5, n6, t4); - }; - } - throw Error("Unsupported decorator location: " + n5); - }; - function n4(t4) { - return (e5, o5) => "object" == typeof o5 ? r5(t4, e5, o5) : ((t5, e6, o6) => { - const r6 = e6.hasOwnProperty(o6); - return e6.constructor.createProperty(o6, r6 ? { ...t5, wrapped: true } : t5), r6 ? Object.getOwnPropertyDescriptor(e6, o6) : void 0; - })(t4, e5, o5); - } - - // node_modules/@posit-dev/shiny-bindings-core/dist/OptionalShiny.js - var Shiny = window.Shiny; - - // node_modules/@posit-dev/shiny-bindings-core/dist/makeInputBinding.js - function makeInputBinding(tagName, { type = null } = {}) { - if (!Shiny) { - return; - } - class NewCustomBinding extends Shiny["InputBinding"] { - constructor() { - super(); - } - find(scope) { - return $(scope).find(tagName); - } - getValue(el) { - return el.value; - } - getType(_2) { - return type; - } - subscribe(el, callback) { - el.notifyBindingOfChange = (ad) => callback(ad ?? false); - } - unsubscribe(el) { - el.notifyBindingOfChange = (_2) => { - }; - } - } - Shiny.inputBindings.register(new NewCustomBinding(), `${tagName}-Binding`); - } - - // srcts/index.ts - var customInputTag = "custom-component"; - var CustomComponentEl = class extends s3 { - constructor() { - super(...arguments); - this.value = 0; - /* - * The callback function that is called when the value of the input changes. - * This alerts Shiny that the value has changed and it should check for the - * latest value. This is set by the input binding. - */ - this.notifyBindingOfChange = () => { - }; - } - /** - * Function to run when the increment button is clicked. - */ - onIncrement() { - this.value++; - this.notifyBindingOfChange(true); - } - render() { - return x` - - Value: ${this.value} - - `; - } - }; - CustomComponentEl.styles = i` - :host { - display: block; - border: solid 1px gray; - padding: 16px; - max-width: 800px; - width: fit-content; - } - `; - __decorateClass([ - n4({ type: Number }) - ], CustomComponentEl.prototype, "value", 2); - CustomComponentEl = __decorateClass([ - t3(customInputTag) - ], CustomComponentEl); - makeInputBinding(customInputTag); -})(); -/*! Bundled license information: - -@lit/reactive-element/css-tag.js: - (** - * @license - * Copyright 2019 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/reactive-element.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -lit-html/lit-html.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -lit-element/lit-element.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -lit-html/is-server.js: - (** - * @license - * Copyright 2022 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/custom-element.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/property.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/state.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/event-options.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/base.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/query.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/query-all.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/query-async.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/query-assigned-elements.js: - (** - * @license - * Copyright 2021 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) - -@lit/reactive-element/decorators/query-assigned-nodes.js: - (** - * @license - * Copyright 2017 Google LLC - * SPDX-License-Identifier: BSD-3-Clause - *) -*/ diff --git a/shiny/templates/package-templates/.gitignore b/shiny/templates/package/.gitignore similarity index 100% rename from shiny/templates/package-templates/.gitignore rename to shiny/templates/package/.gitignore diff --git a/shiny/templates/package-templates/js-input/.gitignore b/shiny/templates/package/js-input/.gitignore similarity index 100% rename from shiny/templates/package-templates/js-input/.gitignore rename to shiny/templates/package/js-input/.gitignore diff --git a/shiny/templates/package-templates/js-input/README.md b/shiny/templates/package/js-input/README.md similarity index 100% rename from shiny/templates/package-templates/js-input/README.md rename to shiny/templates/package/js-input/README.md diff --git a/shiny/templates/package/js-input/_template.json b/shiny/templates/package/js-input/_template.json new file mode 100644 index 000000000..f5bc5f1f2 --- /dev/null +++ b/shiny/templates/package/js-input/_template.json @@ -0,0 +1,12 @@ +{ + "type": "package", + "id": "js-input", + "title": "Input component", + "description": "A Python package template providing a custom Shiny input component.", + "follow_up": [ + { + "type": "action", + "text": "Learn more about custom components for Shiny at https://shiny.posit.co/py/docs/custom-components-pkg.html" + } + ] +} diff --git a/shiny/templates/package-templates/js-input/custom_component/__init__.py b/shiny/templates/package/js-input/custom_component/__init__.py similarity index 100% rename from shiny/templates/package-templates/js-input/custom_component/__init__.py rename to shiny/templates/package/js-input/custom_component/__init__.py diff --git a/shiny/templates/package-templates/js-input/custom_component/custom_component.py b/shiny/templates/package/js-input/custom_component/custom_component.py similarity index 100% rename from shiny/templates/package-templates/js-input/custom_component/custom_component.py rename to shiny/templates/package/js-input/custom_component/custom_component.py diff --git a/shiny/templates/package-templates/js-input/example-app/app.py b/shiny/templates/package/js-input/example-app/app.py similarity index 100% rename from shiny/templates/package-templates/js-input/example-app/app.py rename to shiny/templates/package/js-input/example-app/app.py diff --git a/shiny/templates/package-templates/js-input/package-lock.json b/shiny/templates/package/js-input/package-lock.json similarity index 100% rename from shiny/templates/package-templates/js-input/package-lock.json rename to shiny/templates/package/js-input/package-lock.json diff --git a/shiny/templates/package-templates/js-input/package.json b/shiny/templates/package/js-input/package.json similarity index 100% rename from shiny/templates/package-templates/js-input/package.json rename to shiny/templates/package/js-input/package.json diff --git a/shiny/templates/package-templates/js-input/pyproject.toml b/shiny/templates/package/js-input/pyproject.toml similarity index 100% rename from shiny/templates/package-templates/js-input/pyproject.toml rename to shiny/templates/package/js-input/pyproject.toml diff --git a/shiny/templates/package-templates/js-input/srcts/index.ts b/shiny/templates/package/js-input/srcts/index.ts similarity index 100% rename from shiny/templates/package-templates/js-input/srcts/index.ts rename to shiny/templates/package/js-input/srcts/index.ts diff --git a/shiny/templates/package-templates/js-input/tsconfig.json b/shiny/templates/package/js-input/tsconfig.json similarity index 100% rename from shiny/templates/package-templates/js-input/tsconfig.json rename to shiny/templates/package/js-input/tsconfig.json diff --git a/shiny/templates/package-templates/js-output/.gitignore b/shiny/templates/package/js-output/.gitignore similarity index 100% rename from shiny/templates/package-templates/js-output/.gitignore rename to shiny/templates/package/js-output/.gitignore diff --git a/shiny/templates/package-templates/js-output/README.md b/shiny/templates/package/js-output/README.md similarity index 100% rename from shiny/templates/package-templates/js-output/README.md rename to shiny/templates/package/js-output/README.md diff --git a/shiny/templates/package/js-output/_template.json b/shiny/templates/package/js-output/_template.json new file mode 100644 index 000000000..440eafa5e --- /dev/null +++ b/shiny/templates/package/js-output/_template.json @@ -0,0 +1,12 @@ +{ + "type": "package", + "id": "js-output", + "title": "Output component", + "description": "A Python package template providing a custom Shiny output component.", + "follow_up": [ + { + "type": "action", + "text": "Learn more about custom components for Shiny at https://shiny.posit.co/py/docs/custom-components-pkg.html" + } + ] +} diff --git a/shiny/templates/package-templates/js-output/custom_component/__init__.py b/shiny/templates/package/js-output/custom_component/__init__.py similarity index 100% rename from shiny/templates/package-templates/js-output/custom_component/__init__.py rename to shiny/templates/package/js-output/custom_component/__init__.py diff --git a/shiny/templates/package-templates/js-output/custom_component/custom_component.py b/shiny/templates/package/js-output/custom_component/custom_component.py similarity index 100% rename from shiny/templates/package-templates/js-output/custom_component/custom_component.py rename to shiny/templates/package/js-output/custom_component/custom_component.py diff --git a/shiny/templates/package-templates/js-output/example-app/app.py b/shiny/templates/package/js-output/example-app/app.py similarity index 100% rename from shiny/templates/package-templates/js-output/example-app/app.py rename to shiny/templates/package/js-output/example-app/app.py diff --git a/shiny/templates/package-templates/js-output/package-lock.json b/shiny/templates/package/js-output/package-lock.json similarity index 100% rename from shiny/templates/package-templates/js-output/package-lock.json rename to shiny/templates/package/js-output/package-lock.json diff --git a/shiny/templates/package-templates/js-output/package.json b/shiny/templates/package/js-output/package.json similarity index 100% rename from shiny/templates/package-templates/js-output/package.json rename to shiny/templates/package/js-output/package.json diff --git a/shiny/templates/package-templates/js-output/pyproject.toml b/shiny/templates/package/js-output/pyproject.toml similarity index 100% rename from shiny/templates/package-templates/js-output/pyproject.toml rename to shiny/templates/package/js-output/pyproject.toml diff --git a/shiny/templates/package-templates/js-output/srcts/index.ts b/shiny/templates/package/js-output/srcts/index.ts similarity index 100% rename from shiny/templates/package-templates/js-output/srcts/index.ts rename to shiny/templates/package/js-output/srcts/index.ts diff --git a/shiny/templates/package-templates/js-output/tsconfig.json b/shiny/templates/package/js-output/tsconfig.json similarity index 100% rename from shiny/templates/package-templates/js-output/tsconfig.json rename to shiny/templates/package/js-output/tsconfig.json diff --git a/shiny/templates/package-templates/js-react/.gitignore b/shiny/templates/package/js-react/.gitignore similarity index 100% rename from shiny/templates/package-templates/js-react/.gitignore rename to shiny/templates/package/js-react/.gitignore diff --git a/shiny/templates/package-templates/js-react/README.md b/shiny/templates/package/js-react/README.md similarity index 100% rename from shiny/templates/package-templates/js-react/README.md rename to shiny/templates/package/js-react/README.md diff --git a/shiny/templates/package/js-react/_template.json b/shiny/templates/package/js-react/_template.json new file mode 100644 index 000000000..1b26e6ecc --- /dev/null +++ b/shiny/templates/package/js-react/_template.json @@ -0,0 +1,12 @@ +{ + "type": "package", + "id": "js-react", + "title": "React component", + "description": "A Python package template providing a custom React-based Shiny component.", + "follow_up": [ + { + "type": "action", + "text": "Learn more about custom components for Shiny at https://shiny.posit.co/py/docs/custom-components-pkg.html" + } + ] +} diff --git a/shiny/templates/package-templates/js-react/custom_component/__init__.py b/shiny/templates/package/js-react/custom_component/__init__.py similarity index 100% rename from shiny/templates/package-templates/js-react/custom_component/__init__.py rename to shiny/templates/package/js-react/custom_component/__init__.py diff --git a/shiny/templates/package-templates/js-react/custom_component/custom_component.py b/shiny/templates/package/js-react/custom_component/custom_component.py similarity index 100% rename from shiny/templates/package-templates/js-react/custom_component/custom_component.py rename to shiny/templates/package/js-react/custom_component/custom_component.py diff --git a/shiny/templates/package-templates/js-react/example-app/app.py b/shiny/templates/package/js-react/example-app/app.py similarity index 100% rename from shiny/templates/package-templates/js-react/example-app/app.py rename to shiny/templates/package/js-react/example-app/app.py diff --git a/shiny/templates/package-templates/js-react/package-lock.json b/shiny/templates/package/js-react/package-lock.json similarity index 100% rename from shiny/templates/package-templates/js-react/package-lock.json rename to shiny/templates/package/js-react/package-lock.json diff --git a/shiny/templates/package-templates/js-react/package.json b/shiny/templates/package/js-react/package.json similarity index 100% rename from shiny/templates/package-templates/js-react/package.json rename to shiny/templates/package/js-react/package.json diff --git a/shiny/templates/package-templates/js-react/pyproject.toml b/shiny/templates/package/js-react/pyproject.toml similarity index 100% rename from shiny/templates/package-templates/js-react/pyproject.toml rename to shiny/templates/package/js-react/pyproject.toml diff --git a/shiny/templates/package-templates/js-react/srcts/index.tsx b/shiny/templates/package/js-react/srcts/index.tsx similarity index 100% rename from shiny/templates/package-templates/js-react/srcts/index.tsx rename to shiny/templates/package/js-react/srcts/index.tsx diff --git a/shiny/templates/package-templates/js-react/tsconfig.json b/shiny/templates/package/js-react/tsconfig.json similarity index 100% rename from shiny/templates/package-templates/js-react/tsconfig.json rename to shiny/templates/package/js-react/tsconfig.json diff --git a/tests/playwright/examples/test_shiny_create.py b/tests/playwright/examples/test_shiny_create.py index 30f7867f4..662ae9214 100644 --- a/tests/playwright/examples/test_shiny_create.py +++ b/tests/playwright/examples/test_shiny_create.py @@ -8,8 +8,8 @@ from shiny._main_create import ( GithubRepoLocation, - app_template_choices, parse_github_arg, + shiny_internal_templates, ) @@ -48,15 +48,15 @@ def subprocess_create( @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) -@pytest.mark.parametrize("ex_app_path", get_apps("shiny/templates/app-templates")) +@pytest.mark.parametrize("ex_app_path", get_apps("shiny/templates/app")) def test_template_examples(page: Page, ex_app_path: str) -> None: validate_example(page, ex_app_path) -app_templates = list(app_template_choices.values()) -app_templates.remove("external-gallery") # Not actually a template -app_templates.remove("js-component") # Several templates that can't be easily tested +app_templates = [t.id for t in shiny_internal_templates.apps] +pkg_templates = [t.id for t in shiny_internal_templates.packages] assert len(app_templates) > 0 +assert len(pkg_templates) > 0 @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) @@ -76,7 +76,7 @@ def test_create_express(app_template: str, page: Page): @pytest.mark.flaky(reruns=reruns, reruns_delay=reruns_delay) -@pytest.mark.parametrize("app_template", ["js-input", "js-output", "js-react"]) +@pytest.mark.parametrize("app_template", pkg_templates) def test_create_js(app_template: str): with tempfile.TemporaryDirectory("example_apps") as tmpdir: subprocess_create(app_template, dest_dir=tmpdir, package_name="my_component") @@ -88,40 +88,40 @@ def test_parse_github_arg(): repo_owner="posit-dev", repo_name="py-shiny", ref="main", - path="shiny/templates/app-templates/basic-app", + path="shiny/templates/app/basic-app", ) # * {repo_owner}/{repo_name}@{ref}:{path} actual_ref_path = parse_github_arg( - "posit-dev/py-shiny@main:shiny/templates/app-templates/basic-app" + "posit-dev/py-shiny@main:shiny/templates/app/basic-app" ) assert actual_ref_path == expected # * {repo_owner}/{repo_name}:{path}@{ref} actual_path_ref = parse_github_arg( - "posit-dev/py-shiny:shiny/templates/app-templates/basic-app@main" + "posit-dev/py-shiny:shiny/templates/app/basic-app@main" ) assert actual_path_ref == expected # * {repo_owner}/{repo_name}/{path}@{ref} actual_path_slash_ref = parse_github_arg( - "posit-dev/py-shiny/shiny/templates/app-templates/basic-app@main" + "posit-dev/py-shiny/shiny/templates/app/basic-app@main" ) assert actual_path_slash_ref == expected # * {repo_owner}/{repo_name}/{path}?ref={ref} actual_path_slash_query = parse_github_arg( - "posit-dev/py-shiny/shiny/templates/app-templates/basic-app?ref=main" + "posit-dev/py-shiny/shiny/templates/app/basic-app?ref=main" ) assert actual_path_slash_query == expected actual_path_full = parse_github_arg( - "https://github.com/posit-dev/py-shiny/tree/main/shiny/templates/app-templates/basic-app" + "https://github.com/posit-dev/py-shiny/tree/main/shiny/templates/app/basic-app" ) assert actual_path_full == expected actual_path_part = parse_github_arg( - "github.com/posit-dev/py-shiny/tree/main/shiny/templates/app-templates/basic-app" + "github.com/posit-dev/py-shiny/tree/main/shiny/templates/app/basic-app" ) assert actual_path_part == expected @@ -130,25 +130,25 @@ def test_parse_github_arg(): # * {repo_owner}/{repo_name}:{path} actual_path_colon = parse_github_arg( - "posit-dev/py-shiny:shiny/templates/app-templates/basic-app" + "posit-dev/py-shiny:shiny/templates/app/basic-app" ) assert actual_path_colon == expected # * {repo_owner}/{repo_name}/{path} actual_path_slash = parse_github_arg( - "posit-dev/py-shiny/shiny/templates/app-templates/basic-app" + "posit-dev/py-shiny/shiny/templates/app/basic-app" ) assert actual_path_slash == expected # complicated ref actual_ref_tag = parse_github_arg( - "posit-dev/py-shiny@v0.1.0:shiny/templates/app-templates/basic-app" + "posit-dev/py-shiny@v0.1.0:shiny/templates/app/basic-app" ) expected.ref = "v0.1.0" assert actual_ref_tag == expected actual_ref_branch = parse_github_arg( - "posit-dev/py-shiny@feat/new-template:shiny/templates/app-templates/basic-app" + "posit-dev/py-shiny@feat/new-template:shiny/templates/app/basic-app" ) expected.ref = "feat/new-template" assert actual_ref_branch == expected