diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3124c8f7..1abe0e95 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 tool that returns parameter schemas for any rsconnect command, allowing LLMs to more easily construct valid CLI commands. +- support for deploying Holoviz Panel applications + ### Fixed - Snowflake SPCS (Snowpark Container Services) authentication now properly handles API keys diff --git a/docs/deploying.md b/docs/deploying.md index 51c197e6..ce91b1d2 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -115,9 +115,10 @@ You can deploy a variety of APIs and applications using sub-commands of the * `streamlit`: Streamlit apps * `bokeh`: Bokeh server apps * `gradio`: Gradio apps +* `panel`: HoloViz Panel apps All options below apply equally to the `api`, `fastapi`, `dash`, `streamlit`, -`gradio`, and `bokeh` sub-commands. +`gradio`, `bokeh`, and `panel` sub-commands. #### Including Extra Files @@ -297,7 +298,7 @@ rsconnect deploy notebook --title "My Notebook" my-notebook.ipynb ``` When using `rsconnect deploy api`, `rsconnect deploy fastapi`, `rsconnect deploy dash`, -`rsconnect deploy streamlit`, `rsconnect deploy bokeh`, or `rsconnect deploy gradio`, +`rsconnect deploy streamlit`, `rsconnect deploy bokeh`, `rsconnect deploy gradio`, or `rsconnect deploy panel`, the title is derived from the directory containing the API or application. When using `rsconnect deploy manifest`, the title is derived from the primary diff --git a/docs/index.md b/docs/index.md index 8816c988..a0024d5b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ This package provides a (command-line interface) CLI for interacting with and deploying to Posit Connect. Many types of content supported by Posit Connect may be deployed by this package, including WSGI-style APIs, Dash, Streamlit, -Gradio, and Bokeh applications. +Gradio, Bokeh, and Panel applications. Content types not directly supported by the CLI may also be deployed if they include a prepared `manifest.json` file. See ["Deploying R or Other diff --git a/docs/server-administration.md b/docs/server-administration.md index 857ac83b..a62272b6 100644 --- a/docs/server-administration.md +++ b/docs/server-administration.md @@ -158,7 +158,7 @@ rsconnect content search --help # -c, --cacert FILENAME The path to trusted TLS CA certificates. # --published Search only published content. # --unpublished Search only unpublished content. -# --content-type [unknown|shiny|rmd-static|rmd-shiny|static|api|tensorflow-saved-model|jupyter-static|python-api|python-dash|python-streamlit|python-bokeh|python-fastapi|python-gradio|quarto-shiny|quarto-static] +# --content-type [unknown|shiny|rmd-static|rmd-shiny|static|api|tensorflow-saved-model|jupyter-static|python-api|python-dash|python-streamlit|python-bokeh|python-panel|python-fastapi|python-gradio|quarto-shiny|quarto-static] # Filter content results by content type. # --r-version VERSIONSEARCHFILTER # Filter content results by R version. diff --git a/rsconnect/main.py b/rsconnect/main.py index 1ecf4914..9c6b9c09 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1871,6 +1871,7 @@ def deploy_app( generate_deploy_python(app_mode=AppModes.BOKEH_APP, alias="bokeh", min_version="1.8.4") generate_deploy_python(app_mode=AppModes.PYTHON_SHINY, alias="shiny", min_version="2022.07.0") generate_deploy_python(app_mode=AppModes.PYTHON_GRADIO, alias="gradio", min_version="2024.12.0") +generate_deploy_python(app_mode=AppModes.PYTHON_PANEL, alias="panel", min_version="2025.10.0") @deploy.command( @@ -2408,6 +2409,7 @@ def manifest_writer( generate_write_manifest_python(AppModes.PYTHON_SHINY, alias="shiny") generate_write_manifest_python(AppModes.STREAMLIT_APP, alias="streamlit") generate_write_manifest_python(AppModes.PYTHON_GRADIO, alias="gradio") +generate_write_manifest_python(AppModes.PYTHON_PANEL, alias="panel") # noinspection SpellCheckingInspection @@ -2428,7 +2430,7 @@ def _write_framework_manifest( env_management_r: Optional[bool], ): """ - A common function for writing manifests for APIs as well as Dash, Streamlit, and Bokeh apps. + A common function for writing manifests for APIs as well as Dash, Streamlit, Bokeh, and Panel apps. :param overwrite: overwrite the manifest.json, if it exists. :param entrypoint: the entry point for the thing being deployed. diff --git a/rsconnect/models.py b/rsconnect/models.py index bfaf5a2e..d8574bd2 100644 --- a/rsconnect/models.py +++ b/rsconnect/models.py @@ -98,6 +98,7 @@ class AppModes: PYTHON_SHINY = AppMode(15, "python-shiny", "Python Shiny Application") JUPYTER_VOILA = AppMode(16, "jupyter-voila", "Jupyter Voila Application") PYTHON_GRADIO = AppMode(17, "python-gradio", "Gradio Application") + PYTHON_PANEL = AppMode(18, "python-panel", "Panel Application") _modes = [ UNKNOWN, @@ -118,6 +119,7 @@ class AppModes: PYTHON_SHINY, JUPYTER_VOILA, PYTHON_GRADIO, + PYTHON_PANEL, ] Modes = Literal[ @@ -139,6 +141,7 @@ class AppModes: "python-shiny", "jupyter-voila", "python-gradio", + "python-panel", ] _cloud_to_connect_modes = { diff --git a/tests/test_bundle.py b/tests/test_bundle.py index e300954e..7cab72ed 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -2868,6 +2868,76 @@ def test_make_api_bundle_gradio(): assert gradio_dir_ans["files"].keys() == bundle_json["files"].keys() +panel_dir = os.path.join(cur_dir, "./testdata/panel") +panel_file = os.path.join(cur_dir, "./testdata/panel/app.py") + + +def test_make_api_manifest_panel(): + panel_dir_ans = { + "version": 1, + "locale": "en_US.UTF-8", + "metadata": {"appmode": "python-panel"}, + "python": { + "version": "3.8.12", + "package_manager": {"name": "pip", "version": "23.0.1", "package_file": "requirements.txt"}, + }, + "files": { + "requirements.txt": {"checksum": "f90113cfbf5f67bfa6c5c6a5a8bc7eaa"}, + "app.py": {"checksum": "e3b0c44298fc1c149afbf4c8996fb924"}, + }, + } + environment = Environment.create_python_environment( + panel_dir, + ) + manifest, _ = make_api_manifest( + panel_dir, + None, + AppModes.PYTHON_PANEL, + environment, + None, + None, + ) + + assert panel_dir_ans["metadata"] == manifest["metadata"] + assert panel_dir_ans["files"].keys() == manifest["files"].keys() + + +def test_make_api_bundle_panel(): + panel_dir_ans = { + "version": 1, + "locale": "en_US.UTF-8", + "metadata": {"appmode": "python-panel"}, + "python": { + "version": "3.8.12", + "package_manager": {"name": "pip", "version": "23.0.1", "package_file": "requirements.txt"}, + }, + "files": { + "requirements.txt": {"checksum": "f90113cfbf5f67bfa6c5c6a5a8bc7eaa"}, + "app.py": {"checksum": "e3b0c44298fc1c149afbf4c8996fb924"}, + }, + } + environment = Environment.create_python_environment( + panel_dir, + ) + with make_api_bundle( + panel_dir, + None, + AppModes.PYTHON_PANEL, + environment, + None, + None, + ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + names = sorted(tar.getnames()) + assert names == [ + "app.py", + "manifest.json", + "requirements.txt", + ] + bundle_json = json.loads(tar.extractfile("manifest.json").read().decode("utf-8")) + assert panel_dir_ans["metadata"] == bundle_json["metadata"] + assert panel_dir_ans["files"].keys() == bundle_json["files"].keys() + + empty_manifest_file = os.path.join(cur_dir, "./testdata/Manifest_data/empty_manifest.json") missing_file_manifest = os.path.join(cur_dir, "./testdata/Manifest_data/missing_file_manifest.json") diff --git a/tests/testdata/panel/app.py b/tests/testdata/panel/app.py new file mode 100644 index 00000000..09c384a5 --- /dev/null +++ b/tests/testdata/panel/app.py @@ -0,0 +1,24 @@ +import panel as pn + +pn.extension() + + +def greet(name): + return f"Hello, {name}!" + + +text_input = pn.widgets.TextInput(name="Enter your name", placeholder="Type here...") +button = pn.widgets.Button(name="Greet", button_type="primary") + +output = pn.pane.Markdown("Click the button to see a greeting!") + + +def update_output(event): + output.object = greet(text_input.value) + + +button.on_click(update_output) + +app = pn.Column("# Panel Greeting App", text_input, button, output) + +app.servable() diff --git a/tests/testdata/panel/requirements.txt b/tests/testdata/panel/requirements.txt new file mode 100644 index 00000000..f9ec12dd --- /dev/null +++ b/tests/testdata/panel/requirements.txt @@ -0,0 +1 @@ +panel