Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 HoloViz 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
Expand Down
2 changes: 1 addition & 1 deletion docs/server-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion rsconnect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions rsconnect/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -118,6 +119,7 @@ class AppModes:
PYTHON_SHINY,
JUPYTER_VOILA,
PYTHON_GRADIO,
PYTHON_PANEL,
]

Modes = Literal[
Expand All @@ -139,6 +141,7 @@ class AppModes:
"python-shiny",
"jupyter-voila",
"python-gradio",
"python-panel",
]

_cloud_to_connect_modes = {
Expand Down
70 changes: 70 additions & 0 deletions tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
24 changes: 24 additions & 0 deletions tests/testdata/panel/app.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions tests/testdata/panel/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
panel
Loading