From 1befa4f2e7781eabede39d8dd9828479030ebf47 Mon Sep 17 00:00:00 2001 From: David Kegley Date: Fri, 8 Sep 2023 10:38:00 -0400 Subject: [PATCH 1/6] Pin pypa GHA to a supported release version --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c24bedfa..6c3064ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: - os: macos-latest python-version: '3.9' - os: windows-latest - python-version: '3.9' + python-version: '3.9' runs-on: ${{ matrix.os }} name: test (py${{ matrix.python-version }} ${{ matrix.os }}) steps: @@ -108,7 +108,7 @@ jobs: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') run: make sync-to-s3 - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} @@ -190,10 +190,10 @@ jobs: run: | pytest tests/test_main_system_caches.py pytest -m 'vetiver' - + test-jupyter: runs-on: ubuntu-latest - env: + env: CONNECT_LICENSE: ${{ secrets.RSC_LICENSE }} ADMIN_API_KEY: ${{ secrets.ADMIN_API_KEY }} steps: @@ -209,7 +209,7 @@ jobs: - name: Start Connect + rsconnect-jupyter run: | just integration-testing/up - + - name: Run Cypress Tests run: | export ADMIN_API_KEY="${{ secrets.ADMIN_API_KEY }}" @@ -219,7 +219,7 @@ jobs: - name: Save videos uses: actions/upload-artifact@v3 if: success() || failure() - with: + with: name: cypress-videos path: integration-testing/cypress/videos if-no-files-found: ignore From f476183e9ebc341fff7d65cc3b4c2ab9c736fb13 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 11 Sep 2023 10:39:49 -0500 Subject: [PATCH 2/6] send content_category to cloud through API --- rsconnect/actions.py | 14 +- rsconnect/api.py | 40 +++++- rsconnect/bundle.py | 100 +++++++------- tests/test_api.py | 47 +++++-- tests/test_bundle.py | 319 +++++++++++++++++++++---------------------- 5 files changed, 283 insertions(+), 237 deletions(-) diff --git a/rsconnect/actions.py b/rsconnect/actions.py index f67b4c95..c64ed80d 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -546,7 +546,7 @@ def write_quarto_manifest_json( """ warn("This method has been moved and will be deprecated.", DeprecationWarning, stacklevel=2) - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( file_or_directory, inspect, app_mode, @@ -562,7 +562,7 @@ def write_quarto_manifest_json( if not isdir(file_or_directory): base_dir = dirname(file_or_directory) manifest_path = join(base_dir, "manifest.json") - write_manifest_json(manifest_path, manifest) + write_manifest_json(manifest_path, quarto_manifest_info.manifest.data) def write_manifest_json(manifest_path, manifest): @@ -1236,7 +1236,7 @@ def create_notebook_deployment_bundle( image=image, env_management_py=env_management_py, env_management_r=env_management_r, - ) + ).bundle except subprocess.CalledProcessError as exc: # Jupyter rendering failures are often due to # user code failing, vs. an internal failure of rsconnect-python. @@ -1251,7 +1251,7 @@ def create_notebook_deployment_bundle( image=image, env_management_py=env_management_py, env_management_r=env_management_r, - ) + ).bundle def create_api_deployment_bundle( @@ -1296,7 +1296,7 @@ def create_api_deployment_bundle( return make_api_bundle( directory, entry_point, app_mode, environment, extra_files, excludes, image, env_management_py, env_management_r - ) + ).bundle def create_quarto_deployment_bundle( @@ -1343,7 +1343,7 @@ def create_quarto_deployment_bundle( image, env_management_py, env_management_r, - ) + ).bundle def deploy_bundle( @@ -1621,7 +1621,7 @@ def write_api_manifest_json( ) manifest_path = join(directory, "manifest.json") - write_manifest_json(manifest_path, manifest) + write_manifest_json(manifest_path, manifest.data) return exists(join(directory, environment.filename)) diff --git a/rsconnect/api.py b/rsconnect/api.py index c74aae58..e44bf80a 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1,6 +1,8 @@ """ Posit Connect API client and utility functions """ +from __future__ import annotations + import binascii import os from os.path import abspath @@ -30,6 +32,10 @@ from .bundle import _default_title, fake_module_file_from_directory from .timeouts import get_task_timeout, get_task_timeout_help_message +if typing.TYPE_CHECKING: + from .bundle import ManifestBundle + from .bundle import Manifest + class AbstractRemoteServer: def __init__(self, url: str, remote_name: str): @@ -626,7 +632,7 @@ def validate_rstudio_server( raise RSConnectException("Failed to verify with {} ({}).".format(server.remote_name, exc)) @cls_logged("Making bundle ...") - def make_bundle(self, func: Callable, *args, **kwargs): + def make_bundle(self, func: Callable[..., ManifestBundle], *args, **kwargs): path = ( self.get("path", **kwargs) or self.get("file", **kwargs) @@ -656,7 +662,8 @@ def make_bundle(self, func: Callable, *args, **kwargs): ) raise RSConnectException(msg) - d["bundle"] = bundle + d["bundle"] = bundle.bundle + d["manifest"] = bundle.manifest return self @@ -680,6 +687,7 @@ def deploy_bundle( title: str = None, title_is_default: bool = False, bundle: IO = None, + manifest: Manifest = None, env_vars=None, app_mode=None, visibility=None, @@ -689,6 +697,7 @@ def deploy_bundle( title = title or self.get("title") title_is_default = title_is_default or self.get("title_is_default") bundle = bundle or self.get("bundle") + manifest = manifest or self.get("manifest") env_vars = env_vars or self.get("env_vars") app_mode = app_mode or self.get("app_mode") visibility = visibility or self.get("visibility") @@ -725,7 +734,7 @@ def deploy_bundle( cloud_service = CloudService(self.client, self.remote_server, os.getenv("LUCID_APPLICATION_ID")) app_store_version = self.get("app_store_version") prepare_deploy_result = cloud_service.prepare_deploy( - app_id, deployment_name, bundle_size, bundle_hash, app_mode, app_store_version + app_id, deployment_name, bundle_size, bundle_hash, app_mode, manifest, app_store_version ) self.upload_rstudio_bundle(prepare_deploy_result, bundle_size, contents) cloud_service.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.application_id) @@ -1120,16 +1129,29 @@ def create_application(self, account_id, application_name): self._server.handle_bad_response(response) return response - def create_output(self, name: str, application_type: str, project_id=None, space_id=None, render_by=None): + def create_output( + self, + name: str, + application_type: str, + project_id: typing.Optional[int], + space_id: typing.Optional[int], + render_by: typing.Optional[str], + content_category: typing.Optional[str], + ): data = {"name": name, "space": space_id, "project": project_id, "application_type": application_type} if render_by: data["render_by"] = render_by + if content_category: + data["content_category"] = content_category response = self.post("/v1/outputs/", body=data) self._server.handle_bad_response(response) return response - def create_revision(self, content_id): - response = self.post("/v1/outputs/{}/revisions".format(content_id), body={}) + def create_revision(self, content_id: int, content_category: typing.Optional[str]): + body = {} + if content_category: + body["content_category"] = content_category + response = self.post("/v1/outputs/{}/revisions".format(content_id), body=body) self._server.handle_bad_response(response) return response @@ -1334,6 +1356,7 @@ def prepare_deploy( bundle_size: int, bundle_hash: str, app_mode: AppMode, + manifest: Manifest, app_store_version: typing.Optional[int], ) -> PrepareDeployOutputResult: @@ -1345,6 +1368,8 @@ def prepare_deploy( project_id = self._get_current_project_id() + content_category = manifest.data["metadata"].get("content_category") + if app_id is None: # this is a deployment of a new output if project_id is not None: @@ -1361,6 +1386,7 @@ def prepare_deploy( project_id=project_id, space_id=space_id, render_by=render_by, + content_category=content_category, ) app_id_int = output["source_id"] else: @@ -1379,7 +1405,7 @@ def prepare_deploy( output = self._rstudio_client.get_content(content_id) if application_type == "static": - revision = self._rstudio_client.create_revision(content_id) + revision = self._rstudio_client.create_revision(content_id, content_category) app_id_int = revision["application_id"] # associate the output with the current Posit Cloud project (if any) diff --git a/rsconnect/bundle.py b/rsconnect/bundle.py index f916ba82..84e4a093 100644 --- a/rsconnect/bundle.py +++ b/rsconnect/bundle.py @@ -1,7 +1,7 @@ """ Manifest generation and bundling utilities """ - +import dataclasses import hashlib import io import json @@ -311,6 +311,13 @@ def discard_from_buffer(self, key): del self.buffer[key] return self + +@dataclasses.dataclass +class ManifestBundle: + manifest: Manifest + bundle: typing.IO[bytes] + + def make_hasher(): try: return hashlib.md5() @@ -471,7 +478,7 @@ def make_notebook_source_bundle( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.IO[bytes]: +) -> ManifestBundle: """Create a bundle containing the specified notebook and python environment. Returns a file-like object containing the bundle tarball. @@ -522,7 +529,7 @@ def make_notebook_source_bundle( bundle_add_file(bundle, rel_path, base_dir) bundle_file.seek(0) - return bundle_file + return ManifestBundle(manifest, bundle_file) def make_quarto_source_bundle( @@ -535,14 +542,14 @@ def make_quarto_source_bundle( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.IO[bytes]: +) -> ManifestBundle: """ Create a bundle containing the specified Quarto content and (optional) python environment. Returns a file-like object containing the bundle tarball. """ - manifest, relevant_files = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( file_or_directory, inspect, app_mode, @@ -560,17 +567,17 @@ def make_quarto_source_bundle( base_dir = dirname(file_or_directory) with tarfile.open(mode="w:gz", fileobj=bundle_file) as bundle: - bundle_add_buffer(bundle, "manifest.json", json.dumps(manifest, indent=2)) + bundle_add_buffer(bundle, "manifest.json", json.dumps(quarto_manifest_info.manifest.data, indent=2)) if environment: bundle_add_buffer(bundle, environment.filename, environment.contents) - for rel_path in relevant_files: + for rel_path in quarto_manifest_info.relevant_files: bundle_add_file(bundle, rel_path, base_dir) # rewind file pointer bundle_file.seek(0) - return bundle_file + return ManifestBundle(manifest=quarto_manifest_info.manifest, bundle=bundle_file) def make_html_manifest( @@ -578,27 +585,15 @@ def make_html_manifest( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.Dict[str, typing.Any]: +) -> Manifest: # noinspection SpellCheckingInspection - manifest = { - "version": 1, - "metadata": { - "appmode": "static", - "primary_html": filename, - }, - } # type: typing.Dict[str, typing.Any] - - if image or env_management_py is not None or env_management_r is not None: - manifest["environment"] = {} - if image: - manifest["environment"]["image"] = image - if env_management_py is not None or env_management_r is not None: - manifest["environment"]["environment_management"] = {} - if env_management_py is not None: - manifest["environment"]["environment_management"]["python"] = env_management_py - if env_management_r is not None: - manifest["environment"]["environment_management"]["r"] = env_management_r - return manifest + return Manifest( + app_mode=AppModes.STATIC, + primary_html=filename, + image=image, + env_management_r=env_management_r, + env_management_py=env_management_py, + ) def make_notebook_html_bundle( @@ -610,7 +605,7 @@ def make_notebook_html_bundle( env_management_py: bool = None, env_management_r: bool = None, check_output: typing.Callable = subprocess.check_output, -) -> typing.IO[bytes]: +) -> ManifestBundle: # noinspection SpellCheckingInspection cmd = [ python, @@ -645,11 +640,11 @@ def make_notebook_html_bundle( # manifest manifest = make_html_manifest(filename, image, env_management_py, env_management_r) - bundle_add_buffer(bundle, "manifest.json", json.dumps(manifest, indent=2)) + bundle_add_buffer(bundle, "manifest.json", json.dumps(manifest.data, indent=2)) # rewind file pointer bundle_file.seek(0) - return bundle_file + return ManifestBundle(manifest=manifest, bundle=bundle_file) def keep_manifest_specified_file(relative_path, ignore_path_set=directories_to_ignore): @@ -698,7 +693,7 @@ def default_title_from_manifest(file): return title -def read_manifest_file(manifest_path): +def read_manifest_file(manifest_path: str) -> typing.Tuple[dict, str]: """ Read a manifest's content from its file. The content is provided as both a raw string and a parsed dictionary. @@ -713,7 +708,7 @@ def read_manifest_file(manifest_path): return manifest, raw_manifest -def make_manifest_bundle(manifest_path): +def make_manifest_bundle(manifest_path: str) -> ManifestBundle: """Create a bundle, given a manifest. :return: a file-like object containing the bundle tarball. @@ -738,7 +733,10 @@ def make_manifest_bundle(manifest_path): # rewind file pointer bundle_file.seek(0) - return bundle_file + manifest_obj = Manifest() + manifest_obj.data = manifest + + return ManifestBundle(manifest=manifest_obj, bundle=bundle_file) def create_glob_set(directory, excludes): @@ -797,7 +795,7 @@ def make_api_manifest( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.Tuple[typing.Dict[str, typing.Any], typing.List[str]]: +) -> typing.Tuple[Manifest, typing.List[str]]: """ Makes a manifest for an API. @@ -843,7 +841,7 @@ def make_api_manifest( for rel_path in relevant_files: manifest.add_file_relative(rel_path, directory) - return manifest.data, relevant_files + return manifest, relevant_files def create_html_manifest( @@ -925,7 +923,7 @@ def make_html_bundle( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.IO[bytes]: +) -> ManifestBundle: """ Create an html bundle, given a path and/or entrypoint. @@ -959,7 +957,7 @@ def make_html_bundle( bundle.add_to_buffer("manifest.json", json.dumps(manifest_flattened_copy_data, indent=2)) bundle.deploy_dir = manifest.deploy_dir - return bundle.to_file() + return ManifestBundle(manifest, bundle.to_file()) def create_file_list( @@ -1094,7 +1092,7 @@ def make_voila_bundle( env_management_py: bool = None, env_management_r: bool = None, multi_notebook: bool = False, -) -> typing.IO[bytes]: +) -> ManifestBundle: """ Create an voila bundle, given a path and/or entrypoint. @@ -1133,7 +1131,7 @@ def make_voila_bundle( bundle.add_to_buffer("manifest.json", json.dumps(manifest_flattened_copy_data, indent=2)) bundle.deploy_dir = manifest.deploy_dir - return bundle.to_file() + return ManifestBundle(manifest=manifest, bundle=bundle.to_file()) def make_api_bundle( @@ -1146,7 +1144,7 @@ def make_api_bundle( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.IO[bytes]: +) -> ManifestBundle: """ Create an API bundle, given a directory path and a manifest. @@ -1177,7 +1175,7 @@ def make_api_bundle( bundle_file = tempfile.TemporaryFile(prefix="rsc_bundle") with tarfile.open(mode="w:gz", fileobj=bundle_file) as bundle: - bundle_add_buffer(bundle, "manifest.json", json.dumps(manifest, indent=2)) + bundle_add_buffer(bundle, "manifest.json", json.dumps(manifest.data, indent=2)) bundle_add_buffer(bundle, environment.filename, environment.contents) for rel_path in relevant_files: @@ -1186,7 +1184,7 @@ def make_api_bundle( # rewind file pointer bundle_file.seek(0) - return bundle_file + return ManifestBundle(manifest=manifest, bundle=bundle_file) def _create_quarto_file_list( @@ -1218,6 +1216,12 @@ def _create_quarto_file_list( return file_list +@dataclasses.dataclass +class QuartoManifestInfo: + manifest: Manifest + relevant_files: list[str] + + def make_quarto_manifest( file_or_directory: str, quarto_inspection: typing.Dict[str, typing.Any], @@ -1228,7 +1232,7 @@ def make_quarto_manifest( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.Tuple[typing.Dict[str, typing.Any], typing.List[str]]: +) -> QuartoManifestInfo: """ Makes a manifest for a Quarto project. @@ -1293,7 +1297,7 @@ def make_quarto_manifest( for rel_path in relevant_files: manifest.add_file_relative(rel_path, base_dir) - return manifest.data, relevant_files + return QuartoManifestInfo(manifest=manifest, relevant_files=relevant_files) def _validate_title(title): @@ -1919,7 +1923,7 @@ def write_api_manifest_json( ) manifest_path = join(directory, "manifest.json") - write_manifest_json(manifest_path, manifest) + write_manifest_json(manifest_path, manifest.data) return exists(join(directory, environment.filename)) @@ -1993,12 +1997,12 @@ def write_quarto_manifest_json( """ extra_files = validate_extra_files(directory, extra_files) - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( directory, inspect, 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) + write_manifest_json(manifest_path, quarto_manifest_info.manifest.data) def write_manifest_json(manifest_path, manifest): diff --git a/tests/test_api.py b/tests/test_api.py index b37b475b..dc780256 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,6 +8,7 @@ import pytest +from rsconnect import bundle from rsconnect.exception import RSConnectException, DeploymentFailedException from rsconnect.models import AppModes from .utils import ( @@ -224,7 +225,6 @@ def test_deploy_existing_application_with_failure(self): class ShinyappsServiceTestCase(TestCase): - def setUp(self) -> None: super().setUp() self.cloud_client = Mock(spec=PositClient) @@ -236,11 +236,11 @@ def test_do_deploy(self): app_id = 2 task_id = 3 - self.cloud_client.deploy_application.return_value = {'id': task_id} + self.cloud_client.deploy_application.return_value = {"id": task_id} self.service.do_deploy(bundle_id, app_id) - self.cloud_client.set_bundle_status.assert_called_with(bundle_id, 'ready') + self.cloud_client.set_bundle_status.assert_called_with(bundle_id, "ready") self.cloud_client.deploy_application.assert_called_with(bundle_id, app_id) self.cloud_client.wait_until_task_is_successful.assert_called_with(task_id) @@ -250,9 +250,9 @@ def test_do_deploy_failure(self): task_id = 3 build_task_id = 4 - self.cloud_client.deploy_application.return_value = {'id': task_id} - self.cloud_client.wait_until_task_is_successful.side_effect = DeploymentFailedException('uh oh') - self.cloud_client.get_shinyapps_build_task.return_value = {'tasks': [{'id': build_task_id}]} + self.cloud_client.deploy_application.return_value = {"id": task_id} + self.cloud_client.wait_until_task_is_successful.side_effect = DeploymentFailedException("uh oh") + self.cloud_client.get_shinyapps_build_task.return_value = {"tasks": [{"id": build_task_id}]} task_logs_response = Mock() task_logs_response.response_body = "here's why it failed" self.cloud_client.get_task_logs.return_value = task_logs_response @@ -260,7 +260,7 @@ def test_do_deploy_failure(self): with pytest.raises(DeploymentFailedException): self.service.do_deploy(bundle_id, app_id) - self.cloud_client.set_bundle_status.assert_called_with(bundle_id, 'ready') + self.cloud_client.set_bundle_status.assert_called_with(bundle_id, "ready") self.cloud_client.deploy_application.assert_called_with(bundle_id, app_id) self.cloud_client.wait_until_task_is_successful.assert_called_with(task_id) self.cloud_client.get_shinyapps_build_task.assert_called_with(task_id) @@ -306,13 +306,19 @@ def test_prepare_new_deploy_python_shiny(self): bundle_size=bundle_size, bundle_hash=bundle_hash, app_mode=app_mode, + manifest=bundle.Manifest(), app_store_version=1, ) self.cloud_client.get_application.assert_called_with(self.project_application_id) self.cloud_client.get_content.assert_called_with(2) self.cloud_client.create_output.assert_called_with( - name=app_name, application_type="connect", project_id=2, space_id=1000, render_by=None + name=app_name, + application_type="connect", + project_id=2, + space_id=1000, + render_by=None, + content_category=None, ) self.cloud_client.create_bundle.assert_called_with(10, "application/x-tar", bundle_size, bundle_hash) @@ -337,6 +343,8 @@ def test_prepare_new_deploy_static_quarto(self): bundle_hash = "the_hash" app_mode = AppModes.STATIC_QUARTO + manifest = bundle.Manifest(quarto_inspection={"config": {"project": {"type": "website"}}}) + cloud_client.get_application.return_value = { "content_id": 2, } @@ -360,13 +368,19 @@ def test_prepare_new_deploy_static_quarto(self): bundle_size=bundle_size, bundle_hash=bundle_hash, app_mode=app_mode, + manifest=manifest, app_store_version=1, ) cloud_client.get_application.assert_called_with(project_application_id) cloud_client.get_content.assert_called_with(2) cloud_client.create_output.assert_called_with( - name=app_name, application_type="static", project_id=2, space_id=1000, render_by='server' + name=app_name, + application_type="static", + project_id=2, + space_id=1000, + render_by="server", + content_category="site", ) def test_prepare_redeploy(self): @@ -390,6 +404,7 @@ def test_prepare_redeploy(self): bundle_size=bundle_size, bundle_hash=bundle_hash, app_mode=app_mode, + manifest=bundle.Manifest(), app_store_version=1, ) self.cloud_client.get_content.assert_called_with(1) @@ -427,10 +442,11 @@ def test_prepare_redeploy_static(self): bundle_size=bundle_size, bundle_hash=bundle_hash, app_mode=app_mode, + manifest=bundle.Manifest(), app_store_version=1, ) self.cloud_client.get_content.assert_called_with(1) - self.cloud_client.create_revision.assert_called_with(1) + self.cloud_client.create_revision.assert_called_with(1, None) self.cloud_client.create_bundle.assert_called_with(11, "application/x-tar", bundle_size, bundle_hash) self.cloud_client.update_output.assert_called_with(1, {"project": 200}) @@ -465,6 +481,7 @@ def test_prepare_redeploy_preversioned_app_store(self): bundle_size=bundle_size, bundle_hash=bundle_hash, app_mode=app_mode, + manifest=bundle.Manifest(), app_store_version=None, ) # first call is to get the current project id, second call is to get the application @@ -485,11 +502,11 @@ def test_do_deploy(self): app_id = 2 task_id = 3 - self.cloud_client.deploy_application.return_value = {'id': task_id} + self.cloud_client.deploy_application.return_value = {"id": task_id} self.cloud_service.do_deploy(bundle_id, app_id) - self.cloud_client.set_bundle_status.assert_called_with(bundle_id, 'ready') + self.cloud_client.set_bundle_status.assert_called_with(bundle_id, "ready") self.cloud_client.deploy_application.assert_called_with(bundle_id, app_id) self.cloud_client.wait_until_task_is_successful.assert_called_with(task_id) @@ -498,8 +515,8 @@ def test_do_deploy_failure(self): app_id = 2 task_id = 3 - self.cloud_client.deploy_application.return_value = {'id': task_id} - self.cloud_client.wait_until_task_is_successful.side_effect = DeploymentFailedException('uh oh') + self.cloud_client.deploy_application.return_value = {"id": task_id} + self.cloud_client.wait_until_task_is_successful.side_effect = DeploymentFailedException("uh oh") task_logs_response = Mock() task_logs_response.response_body = "here's why it failed" self.cloud_client.get_task_logs.return_value = task_logs_response @@ -507,7 +524,7 @@ def test_do_deploy_failure(self): with pytest.raises(DeploymentFailedException): self.cloud_service.do_deploy(bundle_id, app_id) - self.cloud_client.set_bundle_status.assert_called_with(bundle_id, 'ready') + self.cloud_client.set_bundle_status.assert_called_with(bundle_id, "ready") self.cloud_client.deploy_application.assert_called_with(bundle_id, app_id) self.cloud_client.wait_until_task_is_successful.assert_called_with(task_id) self.cloud_client.get_task_logs.assert_called_with(task_id) diff --git a/tests/test_bundle.py b/tests/test_bundle.py index 6f1233a1..eff663d8 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -73,9 +73,15 @@ def test_make_notebook_source_bundle1(self): # the kernel environment and not the notebook server environment. environment = detect_environment(directory) with make_notebook_source_bundle( - nb_path, environment, None, hide_all_input=False, hide_tagged_input=False, - image=None, env_management_py=None, env_management_r=None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + nb_path, + environment, + None, + hide_all_input=False, + hide_tagged_input=False, + image=None, + env_management_py=None, + env_management_r=None, + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) self.assertEqual( names, @@ -147,7 +153,7 @@ def test_make_notebook_source_bundle2(self): image="rstudio/connect:bionic", env_management_py=False, env_management_r=False, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) self.assertEqual( names, @@ -203,7 +209,7 @@ def test_make_notebook_source_bundle2(self): "environment_management": { "python": False, "r": False, - } + }, }, "files": { "dummy.ipynb": { @@ -231,35 +237,30 @@ def test_make_quarto_source_bundle_from_project(self): fp = open(join(temp_proj, "_quarto.yml"), "w") fp.write("project:\n") fp.write(' title: "myquarto"\n') - fp.write('editor: visual\n') + fp.write("editor: visual\n") environment = detect_environment(temp_proj) # mock the result of running of `quarto inspect ` inspect = { - 'quarto': {'version': '1.3.433'}, - 'dir': temp_proj, - 'engines': ['jupyter'], - 'config': { - 'project': {'title': 'myquarto'}, - 'editor': 'visual', - 'language': {} + "quarto": {"version": "1.3.433"}, + "dir": temp_proj, + "engines": ["jupyter"], + "config": {"project": {"title": "myquarto"}, "editor": "visual", "language": {}}, + "files": { + "input": [temp_proj + "/myquarto.qmd"], + "resources": [], + "config": [temp_proj + "/_quarto.yml"], + "configResources": [], }, - 'files': { - 'input': [temp_proj + '/myquarto.qmd'], - 'resources': [], - 'config': [temp_proj + '/_quarto.yml'], - 'configResources': [] - } } - with make_quarto_source_bundle(temp_proj, - inspect, - AppModes.STATIC_QUARTO, - environment, - [], - [], - None) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + with ( + make_quarto_source_bundle( + temp_proj, inspect, AppModes.STATIC_QUARTO, environment, [], [], None + ).bundle as bundle, + tarfile.open(mode="r:gz", fileobj=bundle) as tar, + ): names = sorted(tar.getnames()) self.assertEqual( names, @@ -282,9 +283,7 @@ def test_make_quarto_source_bundle_from_project(self): { "version": 1, "locale": mock.ANY, - "metadata": { - "appmode": "quarto-static" - }, + "metadata": {"appmode": "quarto-static"}, "python": { "version": self.python_version(), "package_manager": { @@ -293,7 +292,7 @@ def test_make_quarto_source_bundle_from_project(self): "version": mock.ANY, }, }, - "quarto": {'engines': ['jupyter'], 'version': mock.ANY}, + "quarto": {"engines": ["jupyter"], "version": mock.ANY}, "files": { "_quarto.yml": {"checksum": mock.ANY}, "myquarto.qmd": {"checksum": mock.ANY}, @@ -319,7 +318,7 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): fp = open(join(temp_proj, "_quarto.yml"), "w") fp.write("project:\n") fp.write(' title: "myquarto"\n') - fp.write('editor: visual\n') + fp.write("editor: visual\n") fp = open(join(temp_proj, "requirements.txt"), "w") fp.write("dash\n") @@ -330,29 +329,24 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): # mock the result of running of `quarto inspect ` inspect = { - 'quarto': {'version': '1.3.433'}, - 'dir': temp_proj, - 'engines': ['jupyter'], - 'config': { - 'project': {'title': 'myquarto'}, - 'editor': 'visual', - 'language': {} + "quarto": {"version": "1.3.433"}, + "dir": temp_proj, + "engines": ["jupyter"], + "config": {"project": {"title": "myquarto"}, "editor": "visual", "language": {}}, + "files": { + "input": [temp_proj + "/myquarto.qmd"], + "resources": [], + "config": [temp_proj + "/_quarto.yml"], + "configResources": [], }, - 'files': { - 'input': [temp_proj + '/myquarto.qmd'], - 'resources': [], - 'config': [temp_proj + '/_quarto.yml'], - 'configResources': [] - } } - with make_quarto_source_bundle(temp_proj, - inspect, - AppModes.STATIC_QUARTO, - environment, - [], - [], - None) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + with ( + make_quarto_source_bundle( + temp_proj, inspect, AppModes.STATIC_QUARTO, environment, [], [], None + ).bundle as bundle, + tarfile.open(mode="r:gz", fileobj=bundle) as tar, + ): names = sorted(tar.getnames()) self.assertEqual( names, @@ -375,9 +369,7 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): { "version": 1, "locale": mock.ANY, - "metadata": { - "appmode": "quarto-static" - }, + "metadata": {"appmode": "quarto-static"}, "python": { "version": self.python_version(), "package_manager": { @@ -386,7 +378,7 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): "version": mock.ANY, }, }, - "quarto": {'engines': ['jupyter'], 'version': mock.ANY}, + "quarto": {"engines": ["jupyter"], "version": mock.ANY}, "files": { "_quarto.yml": {"checksum": mock.ANY}, "myquarto.qmd": {"checksum": mock.ANY}, @@ -409,17 +401,14 @@ def test_make_quarto_source_bundle_from_file(self): # mock the result of running of `quarto inspect ` inspect = { - 'quarto': {'version': '1.3.433'}, - 'engines': ['markdown'], + "quarto": {"version": "1.3.433"}, + "engines": ["markdown"], } - with make_quarto_source_bundle(temp_proj, - inspect, - AppModes.STATIC_QUARTO, - None, - [], - [], - None) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + with ( + make_quarto_source_bundle(temp_proj, inspect, AppModes.STATIC_QUARTO, None, [], [], None).bundle as bundle, + tarfile.open(mode="r:gz", fileobj=bundle) as tar, + ): names = sorted(tar.getnames()) self.assertEqual( names, @@ -436,10 +425,8 @@ def test_make_quarto_source_bundle_from_file(self): manifest, { "version": 1, - "metadata": { - "appmode": "quarto-static" - }, - "quarto": {'engines': ['markdown'], 'version': mock.ANY}, + "metadata": {"appmode": "quarto-static"}, + "quarto": {"engines": ["markdown"], "version": mock.ANY}, "files": { "myquarto.qmd": {"checksum": mock.ANY}, }, @@ -491,13 +478,14 @@ def do_test_html_bundle(self, directory): self.maxDiff = 5000 nb_path = join(directory, "dummy.ipynb") - bundle = make_notebook_html_bundle( + manifest_bundle = make_notebook_html_bundle( nb_path, sys.executable, hide_all_input=False, hide_tagged_input=False, image=None, ) + bundle = manifest_bundle.bundle tar = tarfile.open(mode="r:gz", fileobj=bundle) @@ -522,6 +510,7 @@ def do_test_html_bundle(self, directory): "appmode": "static", "primary_html": "dummy.html", }, + "files": {}, }, ) finally: @@ -548,7 +537,7 @@ def test_manifest_bundle(self): # noinspection SpellCheckingInspection manifest_path = join(dirname(__file__), "testdata", "R", "shinyapp", "manifest.json") - with make_manifest_bundle(manifest_path) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + with make_manifest_bundle(manifest_path).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: tar_names = sorted(tar.getnames()) manifest = json.loads(tar.extractfile("manifest.json").read().decode("utf-8")) manifest_names = sorted(filter(keep_manifest_specified_file, manifest["files"].keys())) @@ -569,8 +558,13 @@ def test_make_manifest(self): ) # include image parameter - manifest = Manifest(app_mode=AppModes.PYTHON_API, environment=None, entrypoint=None, quarto_inspection=None, - image="rstudio/connect:bionic") + manifest = Manifest( + app_mode=AppModes.PYTHON_API, + environment=None, + entrypoint=None, + quarto_inspection=None, + image="rstudio/connect:bionic", + ) self.assertEqual( manifest.data, { @@ -584,53 +578,59 @@ def test_make_manifest(self): ) # include env_management_py parameter - manifest = Manifest(app_mode=AppModes.PYTHON_API, environment=None, entrypoint=None, quarto_inspection=None, - env_management_py=False) + manifest = Manifest( + app_mode=AppModes.PYTHON_API, + environment=None, + entrypoint=None, + quarto_inspection=None, + env_management_py=False, + ) self.assertEqual( manifest.data, { "version": 1, "metadata": {"appmode": "python-api"}, - "environment": { - "environment_management": { - "python": False - } - }, + "environment": {"environment_management": {"python": False}}, "files": {}, }, ) # include env_management_r parameter - manifest = Manifest(app_mode=AppModes.PYTHON_API, environment=None, entrypoint=None, quarto_inspection=None, - env_management_r=False) + manifest = Manifest( + app_mode=AppModes.PYTHON_API, + environment=None, + entrypoint=None, + quarto_inspection=None, + env_management_r=False, + ) self.assertEqual( manifest.data, { "version": 1, "metadata": {"appmode": "python-api"}, - "environment": { - "environment_management": { - "r": False - } - }, + "environment": {"environment_management": {"r": False}}, "files": {}, }, ) # include all runtime environment parameters - manifest = Manifest(app_mode=AppModes.PYTHON_API, environment=None, entrypoint=None, quarto_inspection=None, - image="rstudio/connect:bionic", env_management_py=False, env_management_r=False) + manifest = Manifest( + app_mode=AppModes.PYTHON_API, + environment=None, + entrypoint=None, + quarto_inspection=None, + image="rstudio/connect:bionic", + env_management_py=False, + env_management_r=False, + ) self.assertEqual( manifest.data, { "version": 1, "metadata": {"appmode": "python-api"}, - "environment": { + "environment": { "image": "rstudio/connect:bionic", - "environment_management": { - "r": False, - "python": False - } + "environment_management": {"r": False, "python": False}, }, "files": {}, }, @@ -717,7 +717,7 @@ def test_make_quarto_manifest_project_no_opt_params(self): # excludes=None, # type: typing.Optional[typing.List[str]] # No optional parameters - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( temp_proj, { "quarto": {"version": "0.9.16"}, @@ -731,7 +731,7 @@ def test_make_quarto_manifest_project_no_opt_params(self): None, ) self.assertEqual( - manifest, + quarto_manifest_info.manifest.data, { "version": 1, "metadata": {"appmode": "quarto-shiny"}, @@ -750,7 +750,7 @@ def test_make_quarto_manifest_doc_no_opt_params(self): # excludes=None, # type: typing.Optional[typing.List[str]] # No optional parameters - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( temp_doc, { "quarto": {"version": "0.9.16"}, @@ -764,14 +764,12 @@ def test_make_quarto_manifest_doc_no_opt_params(self): None, ) self.assertEqual( - manifest, + quarto_manifest_info.manifest.data, { "version": 1, "metadata": {"appmode": "quarto-static"}, "quarto": {"version": "0.9.16", "engines": ["jupyter"]}, - "files": { - basename(temp_doc): {'checksum': mock.ANY} - }, + "files": {basename(temp_doc): {"checksum": mock.ANY}}, }, ) @@ -779,7 +777,7 @@ def test_make_quarto_manifest_project_with_image(self): temp_proj = tempfile.mkdtemp() # include image parameter - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( temp_proj, { "quarto": {"version": "0.9.16"}, @@ -793,7 +791,7 @@ def test_make_quarto_manifest_project_with_image(self): "rstudio/connect:bionic", ) self.assertEqual( - manifest, + quarto_manifest_info.manifest.data, { "version": 1, "metadata": {"appmode": "quarto-shiny"}, @@ -813,7 +811,7 @@ def test_make_quarto_manifest_project_with_env(self): fp.close() # include environment parameter - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( temp_proj, { "quarto": {"version": "0.9.16"}, @@ -838,7 +836,7 @@ def test_make_quarto_manifest_project_with_env(self): ) self.assertEqual( - manifest, + quarto_manifest_info.manifest.data, { "version": 1, "locale": "en_US.UTF-8", @@ -866,7 +864,7 @@ def test_make_quarto_manifest_project_with_extra_files(self): fp.write("This is file c\n") fp.close() - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( temp_proj, { "quarto": {"version": "0.9.16"}, @@ -890,7 +888,7 @@ def test_make_quarto_manifest_project_with_extra_files(self): c_hash = "53b36f1d5b6f7fb2cfaf0c15af7ffb2d" self.assertEqual( - manifest, + quarto_manifest_info.manifest.data, { "version": 1, "metadata": {"appmode": "quarto-shiny"}, @@ -924,7 +922,7 @@ def test_make_quarto_manifest_project_with_excludes(self): fp.close() # exclude the requirements.txt file, but not the other files - manifest, _ = make_quarto_manifest( + quarto_manifest_info = make_quarto_manifest( temp_proj, { "quarto": {"version": "0.9.16"}, @@ -939,7 +937,7 @@ def test_make_quarto_manifest_project_with_excludes(self): ) self.assertEqual( - manifest, + quarto_manifest_info.manifest.data, { "version": 1, "metadata": {"appmode": "quarto-shiny"}, @@ -960,22 +958,22 @@ def test_make_html_manifest(self): manifest = make_html_manifest("abc.html") # print(manifest) self.assertEqual( - manifest, + manifest.data, { "version": 1, "metadata": { "appmode": "static", "primary_html": "abc.html", }, + "files": {}, }, ) # include image parameter - manifest = make_html_manifest("abc.html", - image="rstudio/connect:bionic") + manifest = make_html_manifest("abc.html", image="rstudio/connect:bionic") # print(manifest) self.assertEqual( - manifest, + manifest.data, { "version": 1, "metadata": { @@ -985,15 +983,15 @@ def test_make_html_manifest(self): "environment": { "image": "rstudio/connect:bionic", }, + "files": {}, }, ) # include env_management_py parameter - manifest = make_html_manifest("abc.html", - env_management_py=False) + manifest = make_html_manifest("abc.html", env_management_py=False) # print(manifest) self.assertEqual( - manifest, + manifest.data, { "version": 1, "metadata": { @@ -1005,15 +1003,15 @@ def test_make_html_manifest(self): "python": False, } }, + "files": {}, }, ) # include env_management_r parameter - manifest = make_html_manifest("abc.html", - env_management_r=False) + manifest = make_html_manifest("abc.html", env_management_r=False) # print(manifest) self.assertEqual( - manifest, + manifest.data, { "version": 1, "metadata": { @@ -1025,17 +1023,17 @@ def test_make_html_manifest(self): "r": False, } }, + "files": {}, }, ) # include all runtime environment parameters - manifest = make_html_manifest("abc.html", - image="rstudio/connect:bionic", - env_management_py=False, - env_management_r=False) + manifest = make_html_manifest( + "abc.html", image="rstudio/connect:bionic", env_management_py=False, env_management_r=False + ) # print(manifest) self.assertEqual( - manifest, + manifest.data, { "version": 1, "metadata": { @@ -1047,8 +1045,9 @@ def test_make_html_manifest(self): "environment_management": { "python": False, "r": False, - } + }, }, + "files": {}, }, ) @@ -1689,7 +1688,7 @@ def test_make_voila_bundle( environment=environment, image=None, multi_notebook=False, - ) + ).bundle else: with make_voila_bundle( path, @@ -1700,7 +1699,7 @@ def test_make_voila_bundle( environment=environment, image=None, multi_notebook=False, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "bqplot.ipynb", @@ -1802,7 +1801,7 @@ def test_make_voila_bundle_multi_notebook( environment=environment, image=None, multi_notebook=True, - ) + ).bundle else: with make_voila_bundle( path, @@ -1813,7 +1812,7 @@ def test_make_voila_bundle_multi_notebook( environment=environment, image=None, multi_notebook=True, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "bqplot/bqplot.ipynb", @@ -1884,7 +1883,7 @@ def test_make_voila_bundle_2( environment=environment, image=None, multi_notebook=False, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "bqplot.ipynb", @@ -1941,7 +1940,7 @@ def test_make_voila_bundle_extra(): environment=environment, image=None, multi_notebook=False, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "bqplot.ipynb", @@ -2021,7 +2020,7 @@ def test_create_html_manifest(): "environment_management": { "python": False, "r": False, - } + }, }, } manifest = create_html_manifest( @@ -2221,7 +2220,7 @@ def test_make_html_bundle(): None, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2243,7 +2242,7 @@ def test_make_html_bundle(): None, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2258,7 +2257,7 @@ def test_make_html_bundle(): single_file_index_file, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2284,7 +2283,7 @@ def test_make_html_bundle(): None, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2305,7 +2304,7 @@ def test_make_html_bundle(): None, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2319,7 +2318,7 @@ def test_make_html_bundle(): multi_file_index_file, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2338,7 +2337,7 @@ def test_make_html_bundle(): None, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "b.html", @@ -2359,7 +2358,7 @@ def test_make_html_bundle(): multi_file_nonindex_fileb, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "a.html", @@ -2383,7 +2382,7 @@ def test_make_html_bundle(): None, [multi_file_nonindex_filea], None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "a.html", @@ -2408,7 +2407,7 @@ def test_make_html_bundle(): None, [multi_file_index_file2], None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "index.html", @@ -2454,8 +2453,8 @@ def test_make_api_manifest_fastapi(): None, ) - assert fastapi_dir_ans["metadata"] == manifest["metadata"] - assert fastapi_dir_ans["files"].keys() == manifest["files"].keys() + assert fastapi_dir_ans["metadata"] == manifest.data["metadata"] + assert fastapi_dir_ans["files"].keys() == manifest.data["files"].keys() def test_make_api_bundle_fastapi(): @@ -2484,7 +2483,7 @@ def test_make_api_bundle_fastapi(): environment, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", @@ -2530,8 +2529,8 @@ def test_make_api_manifest_flask(): None, ) - assert flask_dir_ans["metadata"] == manifest["metadata"] - assert flask_dir_ans["files"].keys() == manifest["files"].keys() + assert flask_dir_ans["metadata"] == manifest.data["metadata"] + assert flask_dir_ans["files"].keys() == manifest.data["files"].keys() def test_make_api_bundle_flask(): @@ -2560,7 +2559,7 @@ def test_make_api_bundle_flask(): environment, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", @@ -2605,8 +2604,8 @@ def test_make_api_manifest_streamlit(): None, None, ) - assert streamlit_dir_ans["metadata"] == manifest["metadata"] - assert streamlit_dir_ans["files"].keys() == manifest["files"].keys() + assert streamlit_dir_ans["metadata"] == manifest.data["metadata"] + assert streamlit_dir_ans["files"].keys() == manifest.data["files"].keys() def test_make_api_bundle_streamlit(): @@ -2635,7 +2634,7 @@ def test_make_api_bundle_streamlit(): environment, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", @@ -2682,8 +2681,8 @@ def test_make_api_manifest_dash(): None, ) - assert dash_dir_ans["metadata"] == manifest["metadata"] - assert dash_dir_ans["files"].keys() == manifest["files"].keys() + assert dash_dir_ans["metadata"] == manifest.data["metadata"] + assert dash_dir_ans["files"].keys() == manifest.data["files"].keys() def test_make_api_bundle_dash(): @@ -2712,7 +2711,7 @@ def test_make_api_bundle_dash(): environment, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", @@ -2758,8 +2757,8 @@ def test_make_api_manifest_bokeh(): None, ) - assert bokeh_dir_ans["metadata"] == manifest["metadata"] - assert bokeh_dir_ans["files"].keys() == manifest["files"].keys() + assert bokeh_dir_ans["metadata"] == manifest.data["metadata"] + assert bokeh_dir_ans["files"].keys() == manifest.data["files"].keys() def test_make_api_bundle_bokeh(): @@ -2789,7 +2788,7 @@ def test_make_api_bundle_bokeh(): environment, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", @@ -2835,8 +2834,8 @@ def test_make_api_manifest_shiny(): None, ) - assert shiny_dir_ans["metadata"] == manifest["metadata"] - assert shiny_dir_ans["files"].keys() == manifest["files"].keys() + assert shiny_dir_ans["metadata"] == manifest.data["metadata"] + assert shiny_dir_ans["files"].keys() == manifest.data["files"].keys() def test_make_api_bundle_shiny(): @@ -2865,7 +2864,7 @@ def test_make_api_bundle_shiny(): environment, None, None, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", @@ -2901,7 +2900,7 @@ def test_make_manifest_bundle(): } with make_manifest_bundle( pyshiny_manifest_file, - ) as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) assert names == [ "README.md", From 54e0d506a5751ddc08608e12259d0c97e8e7f1fb Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 11 Sep 2023 10:48:34 -0500 Subject: [PATCH 3/6] python 3.8 compatibility --- rsconnect/bundle.py | 2 +- tests/test_bundle.py | 25 +++++++++---------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/rsconnect/bundle.py b/rsconnect/bundle.py index 84e4a093..369c2993 100644 --- a/rsconnect/bundle.py +++ b/rsconnect/bundle.py @@ -1219,7 +1219,7 @@ def _create_quarto_file_list( @dataclasses.dataclass class QuartoManifestInfo: manifest: Manifest - relevant_files: list[str] + relevant_files: typing.List[str] def make_quarto_manifest( diff --git a/tests/test_bundle.py b/tests/test_bundle.py index eff663d8..fe8fb543 100644 --- a/tests/test_bundle.py +++ b/tests/test_bundle.py @@ -255,12 +255,9 @@ def test_make_quarto_source_bundle_from_project(self): }, } - with ( - make_quarto_source_bundle( - temp_proj, inspect, AppModes.STATIC_QUARTO, environment, [], [], None - ).bundle as bundle, - tarfile.open(mode="r:gz", fileobj=bundle) as tar, - ): + with make_quarto_source_bundle( + temp_proj, inspect, AppModes.STATIC_QUARTO, environment, [], [], None + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) self.assertEqual( names, @@ -341,12 +338,9 @@ def test_make_quarto_source_bundle_from_project_with_requirements(self): }, } - with ( - make_quarto_source_bundle( - temp_proj, inspect, AppModes.STATIC_QUARTO, environment, [], [], None - ).bundle as bundle, - tarfile.open(mode="r:gz", fileobj=bundle) as tar, - ): + with make_quarto_source_bundle( + temp_proj, inspect, AppModes.STATIC_QUARTO, environment, [], [], None + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) self.assertEqual( names, @@ -405,10 +399,9 @@ def test_make_quarto_source_bundle_from_file(self): "engines": ["markdown"], } - with ( - make_quarto_source_bundle(temp_proj, inspect, AppModes.STATIC_QUARTO, None, [], [], None).bundle as bundle, - tarfile.open(mode="r:gz", fileobj=bundle) as tar, - ): + with make_quarto_source_bundle( + temp_proj, inspect, AppModes.STATIC_QUARTO, None, [], [], None + ).bundle as bundle, tarfile.open(mode="r:gz", fileobj=bundle) as tar: names = sorted(tar.getnames()) self.assertEqual( names, From 6287d0183c0d6b16767a4e50d1a6396da305ca15 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Wed, 13 Sep 2023 15:59:31 -0500 Subject: [PATCH 4/6] proper return type on create_quarto_deployment_bundle --- rsconnect/actions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rsconnect/actions.py b/rsconnect/actions.py index c64ed80d..7e21a8d4 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -47,6 +47,9 @@ try: import typing + if typing.TYPE_CHECKING: + from rsconnect import bundle + except ImportError: typing = None @@ -1309,7 +1312,7 @@ def create_quarto_deployment_bundle( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> typing.IO[bytes]: +) -> bundle.ManifestBundle: """ Create an in-memory bundle, ready to deploy. @@ -1343,7 +1346,7 @@ def create_quarto_deployment_bundle( image, env_management_py, env_management_r, - ).bundle + ) def deploy_bundle( From e0e3e0e0801c23d1642c8016d3a512a220b6a77e Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Wed, 13 Sep 2023 16:01:34 -0500 Subject: [PATCH 5/6] try string return type instead of symbol --- rsconnect/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsconnect/actions.py b/rsconnect/actions.py index 7e21a8d4..3c5fb337 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -1312,7 +1312,7 @@ def create_quarto_deployment_bundle( image: str = None, env_management_py: bool = None, env_management_r: bool = None, -) -> bundle.ManifestBundle: +) -> 'bundle.ManifestBundle': """ Create an in-memory bundle, ready to deploy. From 38c93f26e840ef624af84beaeb590e7638a518ef Mon Sep 17 00:00:00 2001 From: Kevin Gartland <41300090+kgartland-rstudio@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:43:44 -0400 Subject: [PATCH 6/6] Use license_files in setup.cfg Seeing warnings in test runs stating that license_file will be deprecation next month: ``` integration-testing-client-1 | The license_file parameter is deprecated, use license_files instead. integration-testing-client-1 | integration-testing-client-1 | By 2023-Oct-30, you need to update your project and remove deprecated calls integration-testing-client-1 | or your builds will no longer be supported. integration-testing-client-1 | integration-testing-client-1 | See https://setuptools.pypa.io/en/latest/userguide/declarative_config.html for details. integration-testing-client-1 | ``` updating to use `license_files` instead. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4896c47f..eae9e885 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ author = Michael Marchetti author_email = mike@posit.co description = Python integration with Posit Connect license = GPL-2.0 -license_file = LICENSE.md +license_files = LICENSE.md long_description = file:README.md long_description_content_type = text/markdown name = rsconnect_python