From 395861b66fc596cacf07fea0b81745282e39b875 Mon Sep 17 00:00:00 2001 From: "E. David Aja" Date: Fri, 25 Oct 2024 14:33:12 -0400 Subject: [PATCH] add gradio app mode support (#619) --- CHANGELOG.md | 6 ++ README.md | 13 ++-- rsconnect/main.py | 3 +- rsconnect/models.py | 3 + tests/test_bundle.py | 86 ++++++++++++++++++++++---- tests/testdata/gradio/app.py | 12 ++++ tests/testdata/gradio/requirements.txt | 1 + 7 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 tests/testdata/gradio/app.py create mode 100644 tests/testdata/gradio/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index a6509c90..b87b9f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 8b45d132..98847982 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. @@ -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. diff --git a/rsconnect/main.py b/rsconnect/main.py index 49571e4f..aa0057c5 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -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", @@ -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 diff --git a/rsconnect/models.py b/rsconnect/models.py index f289ec57..8c4ef452 100644 --- a/rsconnect/models.py +++ b/rsconnect/models.py @@ -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, @@ -116,6 +117,7 @@ class AppModes: STATIC_QUARTO, PYTHON_SHINY, JUPYTER_VOILA, + PYTHON_GRADIO ] Modes = Literal[ @@ -136,6 +138,7 @@ class AppModes: "quarto-static", "python-shiny", "jupyter-voila", + "python-gradio" ] _cloud_to_connect_modes = { diff --git a/tests/test_bundle.py b/tests/test_bundle.py index 1dc1b787..7a2c31aa 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -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": {}, }, ) @@ -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}, }, @@ -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}, }, @@ -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") diff --git a/tests/testdata/gradio/app.py b/tests/testdata/gradio/app.py new file mode 100644 index 00000000..d4eb3f69 --- /dev/null +++ b/tests/testdata/gradio/app.py @@ -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")) diff --git a/tests/testdata/gradio/requirements.txt b/tests/testdata/gradio/requirements.txt new file mode 100644 index 00000000..25acedda --- /dev/null +++ b/tests/testdata/gradio/requirements.txt @@ -0,0 +1 @@ +gradio