Skip to content

Commit

Permalink
add gradio app mode support (posit-dev#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
edavidaja authored Oct 25, 2024
1 parent f2aae49 commit 395861b
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- You can now deploy Gradio applications. This requires Posit Connect release 2024.11.0
or later. Use `rsconnect deploy gradio` to deploy, or `rsconnect write-manifest gradio`
to create a manifest file.

### Changed

- The `rsconnect content build run --poll-wait` argument specifies an integral
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ This package provides a CLI (command-line interface) for interacting
with and deploying to Posit Connect. This is also used by the
[`rsconnect-jupyter`](https://github.com/rstudio/rsconnect-jupyter) package to deploy
Jupyter notebooks via the Jupyter web console. Many types of content supported by Posit
Connect may be deployed by this package, including WSGI-style APIs, Dash, Streamlit, and
Bokeh applications.
Connect may be deployed by this package, including WSGI-style APIs, Dash, Streamlit,
Gradio, and Bokeh 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 Expand Up @@ -288,9 +288,10 @@ You can deploy a variety of APIs and applications using sub-commands of the
* `dash`: Python Dash apps
* `streamlit`: Streamlit apps
* `bokeh`: Bokeh server apps
* `gradio`: Gradio apps

All options below apply equally to the `api`, `fastapi`, `dash`, `streamlit`,
and `bokeh` sub-commands.
`gradio`, and `bokeh` sub-commands.

#### Including Extra Files

Expand Down Expand Up @@ -470,8 +471,8 @@ rsconnect deploy notebook --title "My Notebook" my-notebook.ipynb
```

When using `rsconnect deploy api`, `rsconnect deploy fastapi`, `rsconnect deploy dash`,
`rsconnect deploy streamlit`, or `rsconnect deploy bokeh`, the title is derived from the directory
containing the API or application.
`rsconnect deploy streamlit`, `rsconnect deploy bokeh`, or `rsconnect deploy gradio`,
the title is derived from the directory containing the API or application.

When using `rsconnect deploy manifest`, the title is derived from the primary
filename referenced in the manifest.
Expand Down Expand Up @@ -726,7 +727,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|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-fastapi|python-gradio|quarto-shiny|quarto-static]
# Filter content results by content type.
# --r-version VERSIONSEARCHFILTER
# Filter content results by R version.
Expand Down
3 changes: 2 additions & 1 deletion rsconnect/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,7 @@ def deploy_app(
generate_deploy_python(app_mode=AppModes.STREAMLIT_APP, alias="streamlit", min_version="1.8.4")
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.11.0")

@deploy.command(
name="other-content",
Expand Down Expand Up @@ -2272,6 +2272,7 @@ def manifest_writer(
generate_write_manifest_python(AppModes.PYTHON_FASTAPI, alias="fastapi")
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")


# noinspection SpellCheckingInspection
Expand Down
3 changes: 3 additions & 0 deletions rsconnect/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class AppModes:
STATIC_QUARTO = AppMode(14, "quarto-static", "Quarto Document", ".qmd")
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")

_modes = [
UNKNOWN,
Expand All @@ -116,6 +117,7 @@ class AppModes:
STATIC_QUARTO,
PYTHON_SHINY,
JUPYTER_VOILA,
PYTHON_GRADIO
]

Modes = Literal[
Expand All @@ -136,6 +138,7 @@ class AppModes:
"quarto-static",
"python-shiny",
"jupyter-voila",
"python-gradio"
]

_cloud_to_connect_modes = {
Expand Down
86 changes: 75 additions & 11 deletions tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1059,9 +1059,7 @@ def test_make_tensorflow_manifest_empty(self):
manifest,
{
"version": 1,
"metadata": {
"appmode": "tensorflow-saved-model"
},
"metadata": {"appmode": "tensorflow-saved-model"},
"files": {},
},
)
Expand All @@ -1082,9 +1080,7 @@ def test_make_tensorflow_manifest(self):
manifest,
{
"version": 1,
"metadata": {
"appmode": "tensorflow-saved-model"
},
"metadata": {"appmode": "tensorflow-saved-model"},
"files": {
"1/saved_model.pb": {"checksum": mock.ANY},
},
Expand All @@ -1105,17 +1101,15 @@ def test_make_tensorflow_bundle(self):
[
"1/saved_model.pb",
"manifest.json",
],
)
],
)
manifest_data = tar.extractfile("manifest.json").read().decode("utf-8")
manifest = json.loads(manifest_data)
self.assertEqual(
manifest,
{
"version": 1,
"metadata": {
"appmode": "tensorflow-saved-model"
},
"metadata": {"appmode": "tensorflow-saved-model"},
"files": {
"1/saved_model.pb": {"checksum": mock.ANY},
},
Expand Down Expand Up @@ -2909,6 +2903,76 @@ def test_make_manifest_bundle():
assert manifest["files"].keys() == bundle_json["files"].keys()


gradio_dir = os.path.join(cur_dir, "./testdata/gradio")
gradio_file = os.path.join(cur_dir, "./testdata/gradio/app.py")


def test_make_api_manifest_gradio():
gradio_dir_ans = {
"version": 1,
"locale": "en_US.UTF-8",
"metadata": {"appmode": "python-gradio"},
"python": {
"version": "3.8.12",
"package_manager": {"name": "pip", "version": "23.0.1", "package_file": "requirements.txt"},
},
"files": {
"requirements.txt": {"checksum": "381ccadfb8d4848add470e33033b198f"},
"app.py": {"checksum": "22feec76e9c02ac6b5a34a083e2983b6"},
},
}
environment = create_python_environment(
gradio_dir,
)
manifest, _ = make_api_manifest(
gradio_dir,
None,
AppModes.PYTHON_GRADIO,
environment,
None,
None,
)

assert gradio_dir_ans["metadata"] == manifest["metadata"]
assert gradio_dir_ans["files"].keys() == manifest["files"].keys()


def test_make_api_bundle_gradio():
gradio_dir_ans = {
"version": 1,
"locale": "en_US.UTF-8",
"metadata": {"appmode": "python-gradio"},
"python": {
"version": "3.8.12",
"package_manager": {"name": "pip", "version": "23.0.1", "package_file": "requirements.txt"},
},
"files": {
"requirements.txt": {"checksum": "381ccadfb8d4848add470e33033b198f"},
"app.py": {"checksum": "22feec76e9c02ac6b5a34a083e2983b6"},
},
}
environment = create_python_environment(
gradio_dir,
)
with make_api_bundle(
gradio_dir,
None,
AppModes.PYTHON_GRADIO,
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 gradio_dir_ans["metadata"] == bundle_json["metadata"]
assert gradio_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
12 changes: 12 additions & 0 deletions tests/testdata/gradio/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import gradio as gr

def greet(name, intensity):
return "Hello, " + name + "!" * int(intensity)

demo = gr.Interface(
fn=greet,
inputs=["text", "slider"],
outputs=["text"],
)

demo.launch(auth = ("username", "password"))
1 change: 1 addition & 0 deletions tests/testdata/gradio/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gradio

0 comments on commit 395861b

Please sign in to comment.