Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 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