Skip to content

Commit d5881af

Browse files
mconflitti-pbcclaudeedavidaja
authored
Add support for deploying HoloViz Panel applications (#709)
* Add support for deploying HoloViz Panel applications This change adds support for deploying HoloViz Panel applications to Posit Connect. Panel is a popular Python framework for creating interactive dashboards and data apps. Changes include: - Added PYTHON_PANEL app mode (mode 18) to models.py - Added deploy and write-manifest commands for Panel apps in main.py - Added comprehensive tests for Panel manifest and bundle creation - Added test data files for Panel app testing This requires Posit Connect release 2025.10.0 or later. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * formatting * chores for adding panel app mode * Update rsconnect/models.py Co-authored-by: E. David Aja <[email protected]> * Update docs/index.md --------- Co-authored-by: Claude <[email protected]> Co-authored-by: E. David Aja <[email protected]>
1 parent f2e79e6 commit d5881af

File tree

9 files changed

+108
-5
lines changed

9 files changed

+108
-5
lines changed

docs/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
tool that returns parameter schemas for any rsconnect command, allowing LLMs
1515
to more easily construct valid CLI commands.
1616

17+
- support for deploying Holoviz Panel applications
18+
1719
### Fixed
1820

1921
- Snowflake SPCS (Snowpark Container Services) authentication now properly handles API keys

docs/deploying.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,10 @@ You can deploy a variety of APIs and applications using sub-commands of the
115115
* `streamlit`: Streamlit apps
116116
* `bokeh`: Bokeh server apps
117117
* `gradio`: Gradio apps
118+
* `panel`: HoloViz Panel apps
118119

119120
All options below apply equally to the `api`, `fastapi`, `dash`, `streamlit`,
120-
`gradio`, and `bokeh` sub-commands.
121+
`gradio`, `bokeh`, and `panel` sub-commands.
121122

122123
#### Including Extra Files
123124

@@ -297,7 +298,7 @@ rsconnect deploy notebook --title "My Notebook" my-notebook.ipynb
297298
```
298299

299300
When using `rsconnect deploy api`, `rsconnect deploy fastapi`, `rsconnect deploy dash`,
300-
`rsconnect deploy streamlit`, `rsconnect deploy bokeh`, or `rsconnect deploy gradio`,
301+
`rsconnect deploy streamlit`, `rsconnect deploy bokeh`, `rsconnect deploy gradio`, or `rsconnect deploy panel`,
301302
the title is derived from the directory containing the API or application.
302303

303304
When using `rsconnect deploy manifest`, the title is derived from the primary

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This package provides a (command-line interface) CLI for interacting
44
with and deploying to Posit Connect. Many types of content supported by Posit
55
Connect may be deployed by this package, including WSGI-style APIs, Dash, Streamlit,
6-
Gradio, and Bokeh applications.
6+
Gradio, Bokeh, and Panel applications.
77

88
Content types not directly supported by the CLI may also be deployed if they include a
99
prepared `manifest.json` file. See ["Deploying R or Other

docs/server-administration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ rsconnect content search --help
158158
# -c, --cacert FILENAME The path to trusted TLS CA certificates.
159159
# --published Search only published content.
160160
# --unpublished Search only unpublished content.
161-
# --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]
161+
# --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]
162162
# Filter content results by content type.
163163
# --r-version VERSIONSEARCHFILTER
164164
# Filter content results by R version.

rsconnect/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,7 @@ def deploy_app(
18711871
generate_deploy_python(app_mode=AppModes.BOKEH_APP, alias="bokeh", min_version="1.8.4")
18721872
generate_deploy_python(app_mode=AppModes.PYTHON_SHINY, alias="shiny", min_version="2022.07.0")
18731873
generate_deploy_python(app_mode=AppModes.PYTHON_GRADIO, alias="gradio", min_version="2024.12.0")
1874+
generate_deploy_python(app_mode=AppModes.PYTHON_PANEL, alias="panel", min_version="2025.10.0")
18741875

18751876

18761877
@deploy.command(
@@ -2408,6 +2409,7 @@ def manifest_writer(
24082409
generate_write_manifest_python(AppModes.PYTHON_SHINY, alias="shiny")
24092410
generate_write_manifest_python(AppModes.STREAMLIT_APP, alias="streamlit")
24102411
generate_write_manifest_python(AppModes.PYTHON_GRADIO, alias="gradio")
2412+
generate_write_manifest_python(AppModes.PYTHON_PANEL, alias="panel")
24112413

24122414

24132415
# noinspection SpellCheckingInspection
@@ -2428,7 +2430,7 @@ def _write_framework_manifest(
24282430
env_management_r: Optional[bool],
24292431
):
24302432
"""
2431-
A common function for writing manifests for APIs as well as Dash, Streamlit, and Bokeh apps.
2433+
A common function for writing manifests for APIs as well as Dash, Streamlit, Bokeh, and Panel apps.
24322434
24332435
:param overwrite: overwrite the manifest.json, if it exists.
24342436
:param entrypoint: the entry point for the thing being deployed.

rsconnect/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class AppModes:
9898
PYTHON_SHINY = AppMode(15, "python-shiny", "Python Shiny Application")
9999
JUPYTER_VOILA = AppMode(16, "jupyter-voila", "Jupyter Voila Application")
100100
PYTHON_GRADIO = AppMode(17, "python-gradio", "Gradio Application")
101+
PYTHON_PANEL = AppMode(18, "python-panel", "Panel Application")
101102

102103
_modes = [
103104
UNKNOWN,
@@ -118,6 +119,7 @@ class AppModes:
118119
PYTHON_SHINY,
119120
JUPYTER_VOILA,
120121
PYTHON_GRADIO,
122+
PYTHON_PANEL,
121123
]
122124

123125
Modes = Literal[
@@ -139,6 +141,7 @@ class AppModes:
139141
"python-shiny",
140142
"jupyter-voila",
141143
"python-gradio",
144+
"python-panel",
142145
]
143146

144147
_cloud_to_connect_modes = {

tests/test_bundle.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2868,6 +2868,76 @@ def test_make_api_bundle_gradio():
28682868
assert gradio_dir_ans["files"].keys() == bundle_json["files"].keys()
28692869

28702870

2871+
panel_dir = os.path.join(cur_dir, "./testdata/panel")
2872+
panel_file = os.path.join(cur_dir, "./testdata/panel/app.py")
2873+
2874+
2875+
def test_make_api_manifest_panel():
2876+
panel_dir_ans = {
2877+
"version": 1,
2878+
"locale": "en_US.UTF-8",
2879+
"metadata": {"appmode": "python-panel"},
2880+
"python": {
2881+
"version": "3.8.12",
2882+
"package_manager": {"name": "pip", "version": "23.0.1", "package_file": "requirements.txt"},
2883+
},
2884+
"files": {
2885+
"requirements.txt": {"checksum": "f90113cfbf5f67bfa6c5c6a5a8bc7eaa"},
2886+
"app.py": {"checksum": "e3b0c44298fc1c149afbf4c8996fb924"},
2887+
},
2888+
}
2889+
environment = Environment.create_python_environment(
2890+
panel_dir,
2891+
)
2892+
manifest, _ = make_api_manifest(
2893+
panel_dir,
2894+
None,
2895+
AppModes.PYTHON_PANEL,
2896+
environment,
2897+
None,
2898+
None,
2899+
)
2900+
2901+
assert panel_dir_ans["metadata"] == manifest["metadata"]
2902+
assert panel_dir_ans["files"].keys() == manifest["files"].keys()
2903+
2904+
2905+
def test_make_api_bundle_panel():
2906+
panel_dir_ans = {
2907+
"version": 1,
2908+
"locale": "en_US.UTF-8",
2909+
"metadata": {"appmode": "python-panel"},
2910+
"python": {
2911+
"version": "3.8.12",
2912+
"package_manager": {"name": "pip", "version": "23.0.1", "package_file": "requirements.txt"},
2913+
},
2914+
"files": {
2915+
"requirements.txt": {"checksum": "f90113cfbf5f67bfa6c5c6a5a8bc7eaa"},
2916+
"app.py": {"checksum": "e3b0c44298fc1c149afbf4c8996fb924"},
2917+
},
2918+
}
2919+
environment = Environment.create_python_environment(
2920+
panel_dir,
2921+
)
2922+
with make_api_bundle(
2923+
panel_dir,
2924+
None,
2925+
AppModes.PYTHON_PANEL,
2926+
environment,
2927+
None,
2928+
None,
2929+
) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar:
2930+
names = sorted(tar.getnames())
2931+
assert names == [
2932+
"app.py",
2933+
"manifest.json",
2934+
"requirements.txt",
2935+
]
2936+
bundle_json = json.loads(tar.extractfile("manifest.json").read().decode("utf-8"))
2937+
assert panel_dir_ans["metadata"] == bundle_json["metadata"]
2938+
assert panel_dir_ans["files"].keys() == bundle_json["files"].keys()
2939+
2940+
28712941
empty_manifest_file = os.path.join(cur_dir, "./testdata/Manifest_data/empty_manifest.json")
28722942
missing_file_manifest = os.path.join(cur_dir, "./testdata/Manifest_data/missing_file_manifest.json")
28732943

tests/testdata/panel/app.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import panel as pn
2+
3+
pn.extension()
4+
5+
6+
def greet(name):
7+
return f"Hello, {name}!"
8+
9+
10+
text_input = pn.widgets.TextInput(name="Enter your name", placeholder="Type here...")
11+
button = pn.widgets.Button(name="Greet", button_type="primary")
12+
13+
output = pn.pane.Markdown("Click the button to see a greeting!")
14+
15+
16+
def update_output(event):
17+
output.object = greet(text_input.value)
18+
19+
20+
button.on_click(update_output)
21+
22+
app = pn.Column("# Panel Greeting App", text_input, button, output)
23+
24+
app.servable()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
panel

0 commit comments

Comments
 (0)