diff --git a/rsconnect/bundle.py b/rsconnect/bundle.py index 274b7ee8..4f14fc2b 100644 --- a/rsconnect/bundle.py +++ b/rsconnect/bundle.py @@ -40,6 +40,7 @@ Sequence, Union, cast, + Any, ) # Even though TypedDict is available in Python 3.8, because it's used with NotRequired, @@ -123,6 +124,15 @@ class ManifestDataPythonPackageManager(TypedDict): package_file: str +class ManifestIntegrationRequests(TypedDict): + guid: NotRequired[str] + name: NotRequired[str] + description: NotRequired[str] + auth_type: NotRequired[str] + integration_type: NotRequired[str] + config: NotRequired[dict[str, Any]] + + class ManifestData(TypedDict): version: int files: dict[str, ManifestDataFile] @@ -132,6 +142,7 @@ class ManifestData(TypedDict): quarto: NotRequired[ManifestDataQuarto] python: NotRequired[ManifestDataPython] environment: NotRequired[ManifestDataEnvironment] + integration_requests: list[ManifestIntegrationRequests] class Manifest: @@ -397,6 +408,7 @@ def make_source_manifest( env_management_py=env_management_py, env_management_r=env_management_r, ) + manifest.data["integration_requests"] = [] return manifest.data @@ -1659,6 +1671,7 @@ def write_notebook_manifest_json( manifest_data = make_source_manifest( app_mode, environment, file_name, None, image, env_management_py, env_management_r ) + if hide_all_input or hide_tagged_input: if "jupyter" not in manifest_data: manifest_data["jupyter"] = {} @@ -1761,6 +1774,7 @@ def create_voila_manifest( env_management_py=env_management_py, env_management_r=env_management_r, ) + manifest.data["integration_requests"] = [] manifest.deploy_dir = deploy_dir if entrypoint and isfile(entrypoint): validate_file_is_notebook(entrypoint) @@ -1825,6 +1839,7 @@ def write_voila_manifest_json( manifest_flattened_copy_data = manifest.get_flattened_copy().data if multi_notebook and "metadata" in manifest_flattened_copy_data: manifest_flattened_copy_data["metadata"]["entrypoint"] = "" + manifest_path = join(deploy_dir, "manifest.json") write_manifest_json(manifest_path, manifest_flattened_copy_data) return exists(manifest_path) @@ -1915,6 +1930,7 @@ def write_api_manifest_json( manifest, _ = make_api_manifest( directory, entry_point, app_mode, environment, extra_files, excludes, image, env_management_py, env_management_r ) + manifest_path = join(directory, "manifest.json") write_manifest_json(manifest_path, manifest) @@ -2029,6 +2045,7 @@ def write_tensorflow_manifest_json( excludes, image, ) + manifest_path = join(directory, "manifest.json") write_manifest_json(manifest_path, manifest) diff --git a/tests/test_bundle.py b/tests/test_bundle.py index e300954e..49e6708e 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -35,6 +35,11 @@ to_bytes, validate_entry_point, validate_extra_files, + write_tensorflow_manifest_json, + write_api_manifest_json, + write_notebook_manifest_json, + write_quarto_manifest_json, + write_voila_manifest_json, ) from rsconnect.environment import Environment from rsconnect.exception import RSConnectException @@ -138,6 +143,7 @@ def test_make_notebook_source_bundle1(self): }, "requirements.txt": {"checksum": "5f2a5e862fe7afe3def4a57bb5cfb214"}, }, + "integration_requests": [], }, ) @@ -224,6 +230,7 @@ def test_make_notebook_source_bundle2(self): }, "data.csv": {"checksum": data_csv_hash}, }, + "integration_requests": [], }, ) @@ -263,6 +270,7 @@ def test_make_quarto_source_bundle_from_simple_project(self): "config": [temp_proj + "/_quarto.yml"], "configResources": [], }, + "integration_requests": [], } with make_quarto_source_bundle( @@ -305,6 +313,7 @@ def test_make_quarto_source_bundle_from_simple_project(self): "myquarto.qmd": {"checksum": mock.ANY}, "requirements.txt": {"checksum": mock.ANY}, }, + "integration_requests": [], }, ) @@ -371,6 +380,7 @@ def test_make_quarto_source_bundle_from_complex_project(self): "config": [temp_proj + "/_quarto.yml"], "configResources": [], }, + "integration_requests": [], } with make_quarto_source_bundle( @@ -421,6 +431,7 @@ def test_make_quarto_source_bundle_from_complex_project(self): "about.qmd": {"checksum": mock.ANY}, "requirements.txt": {"checksum": mock.ANY}, }, + "integration_requests": [], }, ) @@ -462,6 +473,7 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): "config": [temp_proj + "/_quarto.yml"], "configResources": [], }, + "integration_requests": [], } with make_quarto_source_bundle( @@ -504,6 +516,7 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): "myquarto.qmd": {"checksum": mock.ANY}, "requirements.txt": {"checksum": mock.ANY}, }, + "integration_requests": [], }, ) @@ -554,6 +567,7 @@ def test_make_quarto_source_bundle_from_file(self): "files": { "myquarto.qmd": {"checksum": mock.ANY}, }, + "integration_requests": [], }, ) @@ -670,7 +684,7 @@ def test_make_source_manifest(self): manifest = make_source_manifest(AppModes.PYTHON_API, None, None, None) self.assertEqual( manifest, - {"version": 1, "metadata": {"appmode": "python-api"}, "files": {}}, + {"version": 1, "metadata": {"appmode": "python-api"}, "files": {}, "integration_requests": []}, ) # include image parameter @@ -684,6 +698,7 @@ def test_make_source_manifest(self): "image": "rstudio/connect:bionic", }, "files": {}, + "integration_requests": [], }, ) @@ -696,6 +711,7 @@ def test_make_source_manifest(self): "metadata": {"appmode": "python-api"}, "environment": {"environment_management": {"python": False}}, "files": {}, + "integration_requests": [], }, ) @@ -708,6 +724,7 @@ def test_make_source_manifest(self): "metadata": {"appmode": "python-api"}, "environment": {"environment_management": {"r": False}}, "files": {}, + "integration_requests": [], }, ) @@ -731,6 +748,7 @@ def test_make_source_manifest(self): "environment_management": {"r": False, "python": False}, }, "files": {}, + "integration_requests": [], }, ) @@ -764,6 +782,7 @@ def test_make_source_manifest(self): "package_manager": {"name": "pip", "version": "22.0.4", "package_file": "requirements.txt"}, }, "files": {}, + "integration_requests": [], }, ) @@ -778,7 +797,12 @@ def test_make_source_manifest(self): # print(manifest) self.assertEqual( manifest, - {"version": 1, "metadata": {"appmode": "python-api", "entrypoint": "main.py"}, "files": {}}, + { + "version": 1, + "metadata": {"appmode": "python-api", "entrypoint": "main.py"}, + "files": {}, + "integration_requests": [], + }, ) # include quarto_inspection parameter @@ -803,6 +827,7 @@ def test_make_source_manifest(self): }, "quarto": {"version": "0.9.16", "engines": ["jupyter"]}, "files": {}, + "integration_requests": [], }, ) @@ -836,6 +861,7 @@ def test_make_quarto_manifest_project_no_opt_params(self): "metadata": {"appmode": "quarto-shiny"}, "quarto": {"version": "0.9.16", "engines": ["jupyter"]}, "files": {}, + "integration_requests": [], }, ) @@ -869,6 +895,7 @@ def test_make_quarto_manifest_doc_no_opt_params(self): "metadata": {"appmode": "quarto-static"}, "quarto": {"version": "0.9.16", "engines": ["jupyter"]}, "files": {basename(temp_doc): {"checksum": mock.ANY}}, + "integration_requests": [], }, ) @@ -897,6 +924,7 @@ def test_make_quarto_manifest_project_with_image(self): "quarto": {"version": "0.9.16", "engines": ["jupyter"]}, "environment": {"image": "rstudio/connect:bionic"}, "files": {}, + "integration_requests": [], }, ) @@ -947,6 +975,7 @@ def test_make_quarto_manifest_project_with_env(self): "package_manager": {"name": "pip", "version": "22.0.4", "package_file": "requirements.txt"}, }, "files": {"requirements.txt": {"checksum": mock.ANY}}, + "integration_requests": [], }, ) @@ -998,6 +1027,7 @@ def test_make_quarto_manifest_project_with_extra_files(self): "b": {"checksum": b_hash}, "c": {"checksum": c_hash}, }, + "integration_requests": [], }, ) @@ -1047,6 +1077,37 @@ def test_make_quarto_manifest_project_with_excludes(self): "e": {"checksum": mock.ANY}, "f": {"checksum": mock.ANY}, }, + "integration_requests": [], + }, + ) + + def test_write_quarto_manifest_json(self): + temp_proj = tempfile.mkdtemp() + # No optional parameters + write_quarto_manifest_json( + temp_proj, + { + "quarto": {"version": "0.9.16"}, + "engines": ["jupyter"], + "config": {"project": {"title": "quarto-proj-py"}, "editor": "visual", "language": {}}, + }, + AppModes.SHINY_QUARTO, + None, + [], + [], + None, + ) + manifest_path = join(temp_proj, "manifest.json") + with open(manifest_path) as f: + manifest_data = json.load(f) + self.assertEqual( + manifest_data, + { + "version": 1, + "metadata": {"appmode": "quarto-shiny"}, + "quarto": {"version": "0.9.16", "engines": ["jupyter"]}, + "files": {}, + "integration_requests": [], }, ) @@ -1059,6 +1120,7 @@ def test_make_tensorflow_manifest_empty(self): "version": 1, "metadata": {"appmode": "tensorflow-saved-model"}, "files": {}, + "integration_requests": [], }, ) @@ -1082,6 +1144,29 @@ def test_make_tensorflow_manifest(self): "files": { "1/saved_model.pb": {"checksum": mock.ANY}, }, + "integration_requests": [], + }, + ) + + def test_write_tensorflow_manifest_json(self): + temp_proj = tempfile.mkdtemp() + os.mkdir(join(temp_proj, "1")) + model_file = join(temp_proj, "1", "saved_model.pb") + with open(model_file, "w") as fp: + fp.write("fake model file\n") + write_tensorflow_manifest_json(temp_proj, [], []) + manifest_path = join(temp_proj, "manifest.json") + with open(manifest_path) as f: + manifest_data = json.load(f) + self.assertEqual( + manifest_data, + { + "version": 1, + "metadata": {"appmode": "tensorflow-saved-model"}, + "files": { + "1/saved_model.pb": {"checksum": mock.ANY}, + }, + "integration_requests": [], }, ) @@ -1111,9 +1196,114 @@ def test_make_tensorflow_bundle(self): "files": { "1/saved_model.pb": {"checksum": mock.ANY}, }, + "integration_requests": [], }, ) + def test_write_api_manifest_json(self): + """Test that write_api_manifest_json includes empty integrations field""" + with tempfile.TemporaryDirectory() as temp_dir: + environment = Environment.from_dict( + dict( + contents="flask\npandas\n", + error=None, + filename="requirements.txt", + locale="en_US.UTF-8", + package_manager="pip", + pip="22.0.4", + python="3.9.12", + source="file", + ) + ) + + api_path = join(temp_dir, "app.py") + with open(api_path, "w") as f: + f.write("from flask import Flask\napp = Flask(__name__)") + + write_api_manifest_json( + temp_dir, + "app:app", + environment, + AppModes.PYTHON_API, + [], + [], + ) + manifest_path = join(temp_dir, "manifest.json") + with open(manifest_path) as f: + manifest_data = json.load(f) + self.assertIn("integration_requests", manifest_data) + self.assertEqual(manifest_data["integration_requests"], []) + + def test_write_voila_manifest_json(self): + """Test that write_voila_manifest_json includes empty integrations field""" + with tempfile.TemporaryDirectory() as temp_dir: + environment = Environment.from_dict( + dict( + contents="numpy\npandas\n", + error=None, + filename="requirements.txt", + locale="en_US.UTF-8", + package_manager="pip", + pip="22.0.4", + python="3.9.12", + source="file", + ) + ) + + notebook_path = join(temp_dir, "notebook.ipynb") + with open(notebook_path, "w") as f: + f.write('{"cells": [], "metadata": {}}') + + write_voila_manifest_json( + notebook_path, + None, + environment, + [], + [], + True, + ) + + manifest_path = join(temp_dir, "manifest.json") + with open(manifest_path) as f: + manifest_data = json.load(f) + self.assertIn("integration_requests", manifest_data) + self.assertEqual(manifest_data["integration_requests"], []) + + def test_write_notebook_manifest_json(self): + """Test that write_notebook_manifest_json includes empty integrations field""" + with tempfile.TemporaryDirectory() as temp_dir: + environment = Environment.from_dict( + dict( + contents="numpy\npandas\n", + error=None, + filename="requirements.txt", + locale="en_US.UTF-8", + package_manager="pip", + pip="22.0.4", + python="3.9.12", + source="file", + ) + ) + + notebook_path = join(temp_dir, "notebook.ipynb") + with open(notebook_path, "w") as f: + f.write('{"cells": [], "metadata": {}}') + + write_notebook_manifest_json( + notebook_path, + environment, + AppModes.JUPYTER_NOTEBOOK, + [], + False, + False, + ) + manifest_path = join(temp_dir, "manifest.json") + with open(manifest_path) as f: + manifest_data = json.load(f) + + self.assertIn("integration_requests", manifest_data) + self.assertEqual(manifest_data["integration_requests"], []) + def test_make_html_manifest(self): # Verify the optional parameters # image=None, # type: str @@ -1298,6 +1488,7 @@ def test_create_voila_manifest_1(path, entrypoint): "requirements.txt": {"checksum": "9cce1aac313043abd5690f67f84338ed"}, "bqplot.ipynb": {"checksum": checksum_hash}, }, + "integration_requests": [], } manifest = Manifest() if (path, entrypoint) in ( @@ -1376,6 +1567,7 @@ def test_create_voila_manifest_2(path, entrypoint): "bqplot.ipynb": {"checksum": bqplot_hash}, "dashboard.ipynb": {"checksum": dashboard_hash}, }, + "integration_requests": [], } manifest = create_voila_manifest( path, @@ -1426,6 +1618,7 @@ def test_create_voila_manifest_extra(): "bqplot.ipynb": {"checksum": bqplot_checksum}, "dashboard.ipynb": {"checksum": dashboard_checksum}, }, + "integration_requests": [], } manifest = create_voila_manifest( dashboard_ipynb, @@ -1510,6 +1703,7 @@ def test_create_voila_manifest_multi_notebook(path, entrypoint): "bqplot/bqplot.ipynb": {"checksum": bqplot_hash}, "dashboard/dashboard.ipynb": {"checksum": dashboard_hash}, }, + "integration_requests": [], } manifest = Manifest() if (path, entrypoint) in ( @@ -1614,6 +1808,7 @@ def test_make_voila_bundle( "requirements.txt": {"checksum": "9395f3162b7779c57c86b187fa441d96"}, "bqplot.ipynb": {"checksum": checksum_hash}, }, + "integration_requests": [], } if (path, entrypoint) in ( (None, None), @@ -1726,6 +1921,7 @@ def test_make_voila_bundle_multi_notebook( "bqplot/bqplot.ipynb": {"checksum": bqplot_hash}, "dashboard/dashboard.ipynb": {"checksum": dashboard_hash}, }, + "integration_requests": [], } if (path, entrypoint) in ( (None, None), @@ -1817,6 +2013,7 @@ def test_make_voila_bundle_2( "bqplot.ipynb": {"checksum": bqplot_hash}, "dashboard.ipynb": {"checksum": dashboard_hash}, }, + "integration_requests": [], } with make_voila_bundle( path, @@ -1875,6 +2072,7 @@ def test_make_voila_bundle_extra(): "bqplot.ipynb": {"checksum": bqplot_hash}, "dashboard.ipynb": {"checksum": dashboard_hash}, }, + "integration_requests": [], } with make_voila_bundle( dashboard_ipynb, diff --git a/tests/testdata/Manifest/html_manifest.json b/tests/testdata/Manifest/html_manifest.json index 1cbb0d97..02330e34 100644 --- a/tests/testdata/Manifest/html_manifest.json +++ b/tests/testdata/Manifest/html_manifest.json @@ -16,4 +16,4 @@ "checksum": "0a576fd324b6985bac6aa934131d2f5c" } } -} \ No newline at end of file +}