diff --git a/CHANGELOG.md b/CHANGELOG.md index 2984fa03..5a6f3f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Python virtualenvs are now detected in Windows environments, and are automatically excluded from the uploaded bundle. +- Error deploying to shinyapps.io when `--app-id` is provided [#464](https://github.com/rstudio/rsconnect-python/issues/464). ### Added diff --git a/rsconnect/api.py b/rsconnect/api.py index beeaaedf..c004958d 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -249,7 +249,7 @@ def deploy(self, app_id, app_name, app_title, title_is_default, tarball, env_var try: self._server.handle_bad_response(app) except RSConnectException as e: - raise RSConnectException(f"{e} Try setting the --new flag to overwrite the previous deployment.") + raise RSConnectException(f"{e} Try setting the --new flag to overwrite the previous deployment.") from e app_guid = app["guid"] if env_vars: @@ -847,7 +847,7 @@ def validate_app_mode(self, *args, **kwargs): except RSConnectException as e: raise RSConnectException( f"{e} Try setting the --new flag to overwrite the previous deployment." - ) + ) from e elif isinstance(self.remote_server, PositServer): try: app = get_rstudio_app_info(self.remote_server, app_id) @@ -855,7 +855,7 @@ def validate_app_mode(self, *args, **kwargs): except RSConnectException as e: raise RSConnectException( f"{e} Try setting the --new flag to overwrite the previous deployment." - ) + ) from e else: raise RSConnectException("Unable to infer Connect client.") if existing_app_mode and existing_app_mode not in (None, AppModes.UNKNOWN, app_mode): @@ -1485,8 +1485,11 @@ def get_app_info(connect_server, app_id): def get_rstudio_app_info(server, app_id): with PositClient(server) as client: - response = client.get_content(app_id) - return response["source"] + if isinstance(server, ShinyappsServer): + return client.get_application(app_id) + else: + response = client.get_content(app_id) + return response["source"] def get_app_config(connect_server, app_id): diff --git a/tests/test_main.py b/tests/test_main.py index 9d159bd4..8bad8096 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -140,7 +140,7 @@ def post_application_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/rstudio-responses/create-application.json", "r").read(), + open("tests/testdata/rstudio-responses/application.json", "r").read(), ] httpretty.register_uri( @@ -278,6 +278,172 @@ def post_deploy_callback(request, uri, response_headers): if original_server_value: os.environ["CONNECT_SERVER"] = original_server_value + @httpretty.activate(verbose=True, allow_net_connect=False) + def test_redeploy_manifest_shinyapps(self): + original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) + original_server_value = os.environ.pop("CONNECT_SERVER", None) + + httpretty.register_uri( + httpretty.GET, + "https://api.shinyapps.io/v1/users/me", + body=open("tests/testdata/rstudio-responses/get-user.json", "r").read(), + status=200, + ) + httpretty.register_uri( + httpretty.GET, + "https://api.shinyapps.io/v1/applications" + "?filter=name:like:shinyapp&offset=0&count=100&use_advanced_filters=true", + body=open("tests/testdata/rstudio-responses/get-applications.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + httpretty.register_uri( + httpretty.GET, + "https://api.shinyapps.io/v1/accounts/", + body=open("tests/testdata/rstudio-responses/get-accounts.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + httpretty.register_uri( + httpretty.GET, + "https://api.shinyapps.io/v1/applications/8442", + body=open("tests/testdata/rstudio-responses/application.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + def post_application_property_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + try: + assert parsed_request == {"value": "private"} + except AssertionError as e: + return _error_to_response(e) + return [ + 201, + {}, + b"", + ] + + httpretty.register_uri( + httpretty.PUT, + "https://api.shinyapps.io/v1/applications/8442/properties/application.visibility", + body=post_application_property_callback, + status=200, + ) + + def post_bundle_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + del parsed_request["checksum"] + del parsed_request["content_length"] + try: + assert parsed_request == { + "application": 8442, + "content_type": "application/x-tar", + } + except AssertionError as e: + return _error_to_response(e) + return [ + 201, + {"Content-Type": "application/json"}, + open("tests/testdata/rstudio-responses/create-bundle.json", "r").read(), + ] + + httpretty.register_uri( + httpretty.POST, + "https://api.shinyapps.io/v1/bundles", + body=post_bundle_callback, + ) + + httpretty.register_uri( + httpretty.PUT, + "https://lucid-uploads-staging.s3.amazonaws.com/bundles/application-8442/" + "6c9ed0d91ee9426687d9ac231d47dc83.tar.gz" + "?AWSAccessKeyId=theAccessKeyId" + "&Signature=dGhlU2lnbmF0dXJlCg%3D%3D" + "&content-md5=D1blMI4qTiI3tgeUOYXwkg%3D%3D" + "&content-type=application%2Fx-tar" + "&x-amz-security-token=dGhlVG9rZW4K" + "&Expires=1656715153", + body="", + ) + + def post_bundle_status_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + try: + assert parsed_request == {"status": "ready"} + except AssertionError as e: + return _error_to_response(e) + return [303, {"Location": "https://api.shinyapps.io/v1/bundles/12640"}, ""] + + httpretty.register_uri( + httpretty.POST, + "https://api.shinyapps.io/v1/bundles/12640/status", + body=post_bundle_status_callback, + ) + + httpretty.register_uri( + httpretty.GET, + "https://api.shinyapps.io/v1/bundles/12640", + body=open("tests/testdata/rstudio-responses/get-accounts.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + def post_deploy_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + try: + assert parsed_request == {"bundle": 12640, "rebuild": False} + except AssertionError as e: + return _error_to_response(e) + return [ + 303, + {"Location": "https://api.shinyapps.io/v1/tasks/333"}, + open("tests/testdata/rstudio-responses/post-deploy.json", "r").read(), + ] + + httpretty.register_uri( + httpretty.POST, + "https://api.shinyapps.io/v1/applications/8442/deploy", + body=post_deploy_callback, + ) + + httpretty.register_uri( + httpretty.GET, + "https://api.shinyapps.io/v1/tasks/333", + body=open("tests/testdata/rstudio-responses/get-task.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + runner = CliRunner() + args = [ + "deploy", + "manifest", + get_manifest_path("shinyapp"), + "--account", + "some-account", + "--token", + "someToken", + "--secret", + "c29tZVNlY3JldAo=", + "--title", + "myApp", + "--visibility", + "private", + "--app-id", + "8442", + ] + try: + result = runner.invoke(cli, args) + assert result.exit_code == 0, result.output + finally: + if original_api_key_value: + os.environ["CONNECT_API_KEY"] = original_api_key_value + if original_server_value: + os.environ["CONNECT_SERVER"] = original_server_value + + @httpretty.activate(verbose=True, allow_net_connect=False) @pytest.mark.parametrize( "project_application_id,project_id", diff --git a/tests/testdata/rstudio-responses/create-application.json b/tests/testdata/rstudio-responses/application.json similarity index 100% rename from tests/testdata/rstudio-responses/create-application.json rename to tests/testdata/rstudio-responses/application.json