From 6fced34493a54cb222d580a092bf593eaa67f2a0 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 14:15:01 -0500 Subject: [PATCH 01/24] add support for deployment on RStudio Cloud --- rsconnect/actions.py | 10 +- rsconnect/api.py | 291 ++++++++++++++---- rsconnect/main.py | 74 +++-- tests/test_main.py | 200 ++++++++++++ .../shinyapps-responses/create-output.json | 71 +++++ .../get-output-application.json | 39 +++ 6 files changed, 586 insertions(+), 99 deletions(-) create mode 100644 tests/testdata/shinyapps-responses/create-output.json create mode 100644 tests/testdata/shinyapps-responses/get-output-application.json diff --git a/rsconnect/actions.py b/rsconnect/actions.py index e49037f1..53b83b80 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -218,13 +218,13 @@ def test_server(connect_server): raise RSConnectException("\n".join(failures)) -def test_shinyapps_server(server: api.ShinyappsServer): - with api.ShinyappsClient(server) as client: +def test_lucid_server(server: api.LucidServer): + with api.LucidClient(server) as client: try: result = client.get_current_user() server.handle_bad_response(result) except RSConnectException as exc: - raise RSConnectException("Failed to verify with shinyapps.io ({}).".format(exc)) + raise RSConnectException("Failed to verify with {} ({}).".format(server.remote_name, exc)) def test_api_key(connect_server): @@ -1588,8 +1588,8 @@ def _gather_basic_deployment_info_for_framework( if isinstance(remote_server, api.RSConnectServer): app = api.get_app_info(remote_server, app_id) existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True) - elif isinstance(remote_server, api.ShinyappsServer): - app = api.get_shinyapp_info(remote_server, app_id) + elif isinstance(remote_server, api.LucidServer): + app = api.get_lucid_app_info(remote_server, app_id) existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"]) else: raise RSConnectException("Unable to infer Connect client.") diff --git a/rsconnect/api.py b/rsconnect/api.py index 87840477..f0a72959 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -62,19 +62,44 @@ def handle_bad_response(self, response): ) -class ShinyappsServer(AbstractRemoteServer): +class LucidServer(AbstractRemoteServer): """ - A simple class to encapsulate the information needed to interact with an - instance of the shinyapps.io server. + A class used to represent the server of the shinyapps.io and RStudio Cloud APIs. """ - def __init__(self, url: str, account_name: str, token: str, secret: str): - super().__init__(url or "https://api.shinyapps.io", "shinyapps.io") + def __init__(self, remote_name: str, url: str, account_name: str, token: str, secret: str): + super().__init__(url, remote_name) self.account_name = account_name self.token = token self.secret = secret +class ShinyappsServer(LucidServer): + """ + A class to encapsulate the information needed to interact with an + instance of the shinyapps.io server. + """ + + def __init__(self, url: str, account_name: str, token: str, secret: str): + remote_name = "shinyapps.io" + if url == "shinyapps.io" or url is None: + url = "https://api.shinyapps.io" + super().__init__(remote_name=remote_name, url=url, account_name=account_name, token=token, secret=secret) + + +class CloudServer(LucidServer): + """ + A class to encapsulate the information needed to interact with an + instance of the RStudio Cloud server. + """ + + def __init__(self, url: str, account_name: str, token: str, secret: str): + remote_name = "RStudio Cloud" + if url == "rstudio.cloud" or url is None: + url = "https://api.rstudio.cloud" + super().__init__(remote_name=remote_name, url=url, account_name=account_name, token=token, secret=secret) + + class RSConnectServer(AbstractRemoteServer): """ A simple class to encapsulate the information needed to interact with an @@ -90,7 +115,7 @@ def __init__(self, url, api_key, insecure=False, ca_data=None): self.cookie_jar = CookieJar() -TargetableServer = typing.Union[ShinyappsServer, RSConnectServer] +TargetableServer = typing.Union[ShinyappsServer, RSConnectServer, CloudServer] class S3Server(AbstractRemoteServer): @@ -415,15 +440,18 @@ def setup_remote_server( if api_key: self.remote_server = RSConnectServer(url, api_key, insecure, ca_data) elif token and secret: - self.remote_server = ShinyappsServer(url, account_name, token, secret) + if url and "rstudio.cloud" in url: + self.remote_server = CloudServer(url, account_name, token, secret) + else: + self.remote_server = ShinyappsServer(url, account_name, token, secret) else: raise RSConnectException("Unable to infer Connect server type and setup server.") def setup_client(self, cookies=None, timeout=30, **kwargs): if isinstance(self.remote_server, RSConnectServer): self.client = RSConnectClient(self.remote_server, cookies, timeout) - elif isinstance(self.remote_server, ShinyappsServer): - self.client = ShinyappsClient(self.remote_server, timeout) + elif isinstance(self.remote_server, LucidServer): + self.client = LucidClient(self.remote_server, timeout) else: raise RSConnectException("Unable to infer Connect client.") @@ -452,8 +480,8 @@ def validate_server( ): if (url and api_key) or isinstance(self.remote_server, RSConnectServer): self.validate_connect_server(name, url, api_key, insecure, cacert, api_key_is_required) - elif (url and token and secret) or isinstance(self.remote_server, ShinyappsServer): - self.validate_shinyapps_server(url, account_name, token, secret) + elif (url and token and secret) or isinstance(self.remote_server, LucidServer): + self.validate_lucid_server(url, account_name, token, secret) else: raise RSConnectException("Unable to validate server from information provided.") @@ -524,21 +552,24 @@ def validate_connect_server( return self - def validate_shinyapps_server( + def validate_lucid_server( self, url: str = None, account_name: str = None, token: str = None, secret: str = None, **kwargs ): url = url or self.remote_server.url account_name = account_name or self.remote_server.account_name token = token or self.remote_server.token secret = secret or self.remote_server.secret - server = ShinyappsServer(url, account_name, token, secret) + if "rstudio.cloud" in url: + server = CloudServer(url, account_name, token, secret) + else: + server = ShinyappsServer(url, account_name, token, secret) - with ShinyappsClient(server) as client: + with LucidClient(server) as client: try: result = client.get_current_user() server.handle_bad_response(result) except RSConnectException as exc: - raise RSConnectException("Failed to verify with shinyapps.io ({}).".format(exc)) + raise RSConnectException("Failed to verify with {} ({}).".format(server.remote_name, exc)) @cls_logged("Making bundle ...") def make_bundle(self, func: Callable, *args, **kwargs): @@ -590,7 +621,7 @@ def check_server_capabilities(self, capability_functions): :param details_source: the source for obtaining server details, gather_server_details(), by default. """ - if isinstance(self.remote_server, ShinyappsServer): + if isinstance(self.remote_server, LucidServer): return self details = self.server_details @@ -605,10 +636,23 @@ def check_server_capabilities(self, capability_functions): raise RSConnectException(message) return self + def upload_lucid_bundle(self, prepare_deploy_result, bundle_size: int, contents): + upload_url = prepare_deploy_result.presigned_url + parsed_upload_url = urlparse(upload_url) + with S3Client("{}://{}".format(parsed_upload_url.scheme, parsed_upload_url.netloc), timeout=120) as s3_client: + upload_result = s3_client.upload( + "{}?{}".format(parsed_upload_url.path, parsed_upload_url.query), + prepare_deploy_result.presigned_checksum, + bundle_size, + contents, + ) + S3Server(upload_url).handle_bad_response(upload_result) + @cls_logged("Deploying bundle ...") def deploy_bundle( self, app_id: int = None, + project_application_id: int = None, deployment_name: str = None, title: str = None, title_is_default: bool = False, @@ -616,6 +660,7 @@ def deploy_bundle( env_vars=None, ): app_id = app_id or self.get("app_id") + project_application_id = project_application_id or self.get("project_application_id") deployment_name = deployment_name or self.get("deployment_name") title = title or self.get("title") title_is_default = title_is_default or self.get("title_is_default") @@ -639,27 +684,27 @@ def deploy_bundle( bundle_size = len(contents) bundle_hash = hashlib.md5(contents).hexdigest() - prepare_deploy_result = self.client.prepare_deploy( - app_id, - deployment_name, - bundle_size, - bundle_hash, - ) - - upload_url = prepare_deploy_result.presigned_url - parsed_upload_url = urlparse(upload_url) - with S3Client( - "{}://{}".format(parsed_upload_url.scheme, parsed_upload_url.netloc), timeout=120 - ) as s3_client: - upload_result = s3_client.upload( - "{}?{}".format(parsed_upload_url.path, parsed_upload_url.query), - prepare_deploy_result.presigned_checksum, + if isinstance(self.remote_server, ShinyappsServer): + shinyapps_service = ShinyappsService(self.client, self.remote_server) + prepare_deploy_result = shinyapps_service.prepare_deploy( + app_id, + deployment_name, bundle_size, - contents, + bundle_hash, ) - S3Server(upload_url).handle_bad_response(upload_result) - - self.client.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.app_id) + self.upload_lucid_bundle(prepare_deploy_result, bundle_size, contents) + shinyapps_service.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.app_id) + else: + cloud_service = CloudService(self.client, self.remote_server) + prepare_deploy_result = cloud_service.prepare_deploy( + app_id, + project_application_id, + deployment_name, + bundle_size, + bundle_hash, + ) + self.upload_lucid_bundle(prepare_deploy_result, bundle_size, contents) + cloud_service.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.app_id) print("Application successfully deployed to {}".format(prepare_deploy_result.app_url)) webbrowser.open_new(prepare_deploy_result.app_url) @@ -769,8 +814,8 @@ def validate_app_mode(self, *args, **kwargs): if isinstance(self.remote_server, RSConnectServer): app = get_app_info(self.remote_server, app_id) existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True) - elif isinstance(self.remote_server, ShinyappsServer): - app = get_shinyapp_info(self.remote_server, app_id) + elif isinstance(self.remote_server, LucidServer): + app = get_lucid_app_info(self.remote_server, app_id) existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"]) else: raise RSConnectException("Unable to infer Connect client.") @@ -916,15 +961,32 @@ def __init__(self, app_id: int, app_url: str, bundle_id: int, presigned_url: str self.presigned_checksum = presigned_checksum -class ShinyappsClient(HTTPServer): +class PrepareDeployOutputResult(PrepareDeployResult): + def __init__( + self, app_id: int, app_url: str, bundle_id: int, presigned_url: str, presigned_checksum: str, output_id: int + ): + super().__init__( + app_id=app_id, + app_url=app_url, + bundle_id=bundle_id, + presigned_url=presigned_url, + presigned_checksum=presigned_checksum, + ) + self.output_id = output_id + + +class LucidClient(HTTPServer): + """ + An HTTP client to call the RStudio Cloud and shinyapps.io APIs. + """ _TERMINAL_STATUSES = {"success", "failed", "error"} - def __init__(self, shinyapps_server: ShinyappsServer, timeout: int = 30): - self._token = shinyapps_server.token - self._key = base64.b64decode(shinyapps_server.secret) - self._server = shinyapps_server - super().__init__(shinyapps_server.url, timeout=timeout) + def __init__(self, lucid_server: LucidServer, timeout: int = 30): + self._token = lucid_server.token + self._key = base64.b64decode(lucid_server.secret) + self._server = lucid_server + super().__init__(lucid_server.url, timeout=timeout) def _get_canonical_request(self, method, path, timestamp, content_hash): return "\n".join([method, path, timestamp, content_hash]) @@ -969,6 +1031,15 @@ def create_application(self, account_id, application_name): } return self.post("/v1/applications/", body=application_data) + def create_output(self, name, project_id=None, space_id=None): + data = { + "name": name, + "space": space_id, + "project": project_id, + } + print(f"creating output with {data}") + return self.post("/v1/outputs/", body=data) + def get_accounts(self): return self.get("/v1/accounts/") @@ -1019,10 +1090,38 @@ def wait_until_task_is_successful(self, task_id, timeout=180): print(" {} - {}".format(status, description)) time.sleep(2) + print("****task json data") + print(task.json_data) print("Task done: {}".format(description)) + def get_applications_like_name(self, name): + applications = [] + + results = self._get_applications_like_name_page(name, 0) + self._server.handle_bad_response(results) + offset = 0 + + while len(applications) < int(results.json_data["total"]): + results = self._get_applications_like_name_page(name, offset) + self._server.handle_bad_response(results) + applications = results.json_data["applications"] + applications.extend(applications) + offset += int(results.json_data["count"]) + + return [app["name"] for app in applications] + + +class ShinyappsService: + """ + Encapsulates operations involving multiple API calls to shinyapps.io. + """ + + def __init__(self, lucid_client: LucidClient, server: ShinyappsServer): + self._lucid_client = lucid_client + self._server = server + def prepare_deploy(self, app_id: typing.Optional[str], app_name: str, bundle_size: int, bundle_hash: str): - accounts = self.get_accounts() + accounts = self._lucid_client.get_accounts() self._server.handle_bad_response(accounts) account = next( filter(lambda acct: acct["name"] == self._server.account_name, accounts.json_data["accounts"]), None @@ -1034,14 +1133,14 @@ def prepare_deploy(self, app_id: typing.Optional[str], app_name: str, bundle_siz ) if app_id is None: - application = self.create_application(account["id"], app_name) + application = self._lucid_client.create_application(account["id"], app_name) else: - application = self.get_application(app_id) + application = self._lucid_client.get_application(app_id) self._server.handle_bad_response(application) app_id_int = application.json_data["id"] app_url = application.json_data["url"] - bundle = self.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) + bundle = self._lucid_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) self._server.handle_bad_response(bundle) return PrepareDeployResult( @@ -1053,28 +1152,85 @@ def prepare_deploy(self, app_id: typing.Optional[str], app_name: str, bundle_siz ) def do_deploy(self, bundle_id, app_id): - bundle_status_response = self.set_bundle_status(bundle_id, "ready") + bundle_status_response = self._lucid_client.set_bundle_status(bundle_id, "ready") self._server.handle_bad_response(bundle_status_response) - deploy_task = self.deploy_application(bundle_id, app_id) + deploy_task = self._lucid_client.deploy_application(bundle_id, app_id) self._server.handle_bad_response(deploy_task) - self.wait_until_task_is_successful(deploy_task.json_data["id"]) + self._lucid_client.wait_until_task_is_successful(deploy_task.json_data["id"]) - def get_applications_like_name(self, name): - applications = [] - results = self._get_applications_like_name_page(name, 0) - self._server.handle_bad_response(results) - offset = 0 +class CloudService: + """ + Encapsulates operations involving multiple API calls to RStudio Cloud. + """ - while len(applications) < int(results.json_data["total"]): - results = self._get_applications_like_name_page(name, offset) - self._server.handle_bad_response(results) - applications = results.json_data["applications"] - applications.extend(applications) - offset += int(results.json_data["count"]) + def __init__(self, lucid_client: LucidClient, server: CloudServer): + self._lucid_client = lucid_client + self._server = server - return [app["name"] for app in applications] + def prepare_deploy( + self, + app_id: typing.Optional[str], + project_application_id: typing.Optional[str], + app_name: str, + bundle_size: int, + bundle_hash: str, + ): + accounts = self._lucid_client.get_accounts() + self._server.handle_bad_response(accounts) + account = next( + filter(lambda acct: acct["name"] == self._server.account_name, accounts.json_data["accounts"]), None + ) + # TODO: also check this during `add` command + if account is None: + raise RSConnectException( + "No account found by name : %s for given user credential" % self._server.account_name + ) + + print("********") + if app_id is None: + print("app_id is none") + if project_application_id is not None: + print(f"project_application_id is {project_application_id}") + project_application = self._lucid_client.get_application(project_application_id) + self._server.handle_bad_response(project_application) + project_id = project_application.json_data["content_id"] + print(f"project_id is {project_id}") + else: + project_id = None + + output = self._lucid_client.create_output(name=app_name, project_id=project_id) + self._server.handle_bad_response(output) + print("****here is the output") + print(output.json_data) + app_id = output.json_data["source_id"] + + application = self._lucid_client.get_application(app_id) + self._server.handle_bad_response(application) + app_id_int = application.json_data["id"] + app_url = application.json_data["url"] + output_id = application.json_data["content_id"] + + bundle = self._lucid_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) + self._server.handle_bad_response(bundle) + + return PrepareDeployOutputResult( + app_id=app_id_int, + app_url=app_url, + bundle_id=int(bundle.json_data["id"]), + presigned_url=bundle.json_data["presigned_url"], + presigned_checksum=bundle.json_data["presigned_checksum"], + output_id=output_id, + ) + + def do_deploy(self, bundle_id, app_id): + bundle_status_response = self._lucid_client.set_bundle_status(bundle_id, "ready") + self._server.handle_bad_response(bundle_status_response) + + deploy_task = self._lucid_client.deploy_application(bundle_id, app_id) + self._server.handle_bad_response(deploy_task) + self._lucid_client.wait_until_task_is_successful(deploy_task.json_data["id"]) def verify_server(connect_server): @@ -1143,8 +1299,8 @@ def get_app_info(connect_server, app_id): return result -def get_shinyapp_info(server, app_id): - with ShinyappsClient(server) as client: +def get_lucid_app_info(server, app_id): + with LucidClient(server) as client: result = client.get_application(app_id) server.handle_bad_response(result) return result @@ -1352,9 +1508,12 @@ def find_unique_name(remote_server: TargetableServer, name: str): filters={"search": name}, mapping_function=lambda client, app: app["name"], ) - else: - client = ShinyappsClient(remote_server) + elif isinstance(remote_server, ShinyappsServer): + client = LucidClient(remote_server) existing_names = client.get_applications_like_name(name) + else: + # non-unique names are permitted in cloud + return name if name in existing_names: suffix = 1 diff --git a/rsconnect/main.py b/rsconnect/main.py index 97b431d9..da4eeb44 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -21,7 +21,7 @@ test_server, validate_quarto_engines, which_quarto, - test_shinyapps_server, + test_lucid_server, ) from .actions_content import ( download_bundle, @@ -133,24 +133,24 @@ def wrapper(*args, **kwargs): return wrapper -def shinyapps_args(func): +def lucid_args(func): @click.option( "--account", "-A", - envvar="SHINYAPPS_ACCOUNT", - help="The shinyapps.io account name.", + envvar=["SHINYAPPS_ACCOUNT", "RSCLOUD_ACCOUNT"], + help="The shinyapps.io/RStudio Cloud account name.", ) @click.option( "--token", "-T", - envvar="SHINYAPPS_TOKEN", - help="The shinyapps.io token.", + envvar=["SHINYAPPS_TOKEN", "RSCLOUD_TOKEN"], + help="The shinyapps.io/RStudio Cloud token.", ) @click.option( "--secret", "-S", - envvar="SHINYAPPS_SECRET", - help="The shinyapps.io token secret.", + envvar=["SHINYAPPS_SECRET", "RSCLOUD_SECRET"], + help="The shinyapps.io/RStudio Cloud token secret.", ) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -220,6 +220,20 @@ def wrapper(*args, **kwargs): return wrapper +def cloud_content_args(func): + @click.option( + "--project-application-id", + "-P", + envvar="LUCID_APPLICATION_ID", + help="The application of the project to to be associated with when creating an output.", + ) + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + @click.group(no_args_is_help=True) @click.option("--future", "-u", is_flag=True, hidden=True, help="Enables future functionality.") def cli(future): @@ -273,9 +287,9 @@ def _test_server_and_api(server, api_key, insecure, ca_cert): return real_server, me -def _test_shinyappsio_creds(server: api.ShinyappsServer): - with cli_feedback("Checking shinyapps.io credential"): - test_shinyapps_server(server) +def _test_lucid_creds(server: api.LucidServer): + with cli_feedback("Checking {} credential".format(server.remote_name)): + test_lucid_server(server) # noinspection SpellCheckingInspection @@ -314,7 +328,7 @@ def _test_shinyappsio_creds(server: api.ShinyappsServer): type=click.File(), help="The path to trusted TLS CA certificates.", ) -@shinyapps_args +@lucid_args @click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.") def add(name, server, api_key, insecure, cacert, account, token, secret, verbose): @@ -333,20 +347,24 @@ def add(name, server, api_key, insecure, cacert, account, token, secret, verbose old_server = server_store.get_by_name(name) if account: - shinyapps_server = api.ShinyappsServer(server, account, token, secret) - _test_shinyappsio_creds(shinyapps_server) + if server and "rstudio.cloud" in server: + real_server = api.CloudServer(server, account, token, secret) + else: + real_server = api.ShinyappsServer(server, account, token, secret) + + _test_lucid_creds(real_server) server_store.set( name, - shinyapps_server.url, - account_name=shinyapps_server.account_name, - token=shinyapps_server.token, - secret=shinyapps_server.secret, + real_server.url, + account_name=real_server.account_name, + token=real_server.token, + secret=real_server.secret, ) if old_server: - click.echo('Updated shinyapps.io credential "%s".' % name) + click.echo('Updated {} credential "{}".'.format(real_server.remote_name, name)) else: - click.echo('Added shinyapps.io credential "%s".' % name) + click.echo('Added {} credential "{}".'.format(real_server.remote_name, name)) else: # Server must be pingable and the API key must work to be added. real_server, _ = _test_server_and_api(server, api_key, insecure, cacert) @@ -745,7 +763,8 @@ def deploy_notebook( ) @server_args @content_args -@shinyapps_args +@cloud_content_args +@lucid_args @click.argument("file", type=click.Path(exists=True, dir_okay=True, file_okay=True)) @cli_exception_handler def deploy_manifest( @@ -759,6 +778,7 @@ def deploy_manifest( secret: str, new: bool, app_id: str, + project_application_id: typing.Optional[str], title: str, verbose: bool, file: str, @@ -984,9 +1004,7 @@ def deploy_html( ) -def generate_deploy_python(app_mode, alias, min_version, supported_by_shinyapps=False): - shinyapps = shinyapps_args if supported_by_shinyapps else _passthrough - +def generate_deploy_python(app_mode, alias, min_version): # noinspection SpellCheckingInspection @deploy.command( name=alias, @@ -1000,7 +1018,8 @@ def generate_deploy_python(app_mode, alias, min_version, supported_by_shinyapps= ) @server_args @content_args - @shinyapps + @lucid_args + @cloud_content_args @click.option( "--entrypoint", "-e", @@ -1075,6 +1094,7 @@ def deploy_app( account: str = None, token: str = None, secret: str = None, + project_application_id: typing.Optional[str] = None, ): kwargs = locals() kwargs["entrypoint"] = entrypoint = validate_entry_point(entrypoint, directory) @@ -1116,9 +1136,7 @@ def deploy_app( deploy_dash_app = generate_deploy_python(app_mode=AppModes.DASH_APP, alias="dash", min_version="1.8.2") deploy_streamlit_app = generate_deploy_python(app_mode=AppModes.STREAMLIT_APP, alias="streamlit", min_version="1.8.4") deploy_bokeh_app = generate_deploy_python(app_mode=AppModes.BOKEH_APP, alias="bokeh", min_version="1.8.4") -deploy_shiny = generate_deploy_python( - app_mode=AppModes.PYTHON_SHINY, alias="shiny", min_version="2022.07.0", supported_by_shinyapps=True -) +deploy_shiny = generate_deploy_python(app_mode=AppModes.PYTHON_SHINY, alias="shiny", min_version="2022.07.0") @deploy.command( diff --git a/tests/test_main.py b/tests/test_main.py index bed0820a..21d4d2ff 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -267,6 +267,171 @@ 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_deploy_manifest_cloud(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.rstudio.cloud/v1/users/me", + body=open("tests/testdata/shinyapps-responses/get-user.json", "r").read(), + status=200, + ) + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/applications" + "?filter=name:like:shinyapp&offset=0&count=100&use_advanced_filters=true", + body=open("tests/testdata/shinyapps-responses/get-applications.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/accounts/", + body=open("tests/testdata/shinyapps-responses/get-accounts.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + def post_output_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + try: + self.assertDictEqual(parsed_request, {"name": "myapp", "space_id": None, "project_id": None}) + except AssertionError as e: + return _error_to_response(e) + return [ + 201, + {"Content-Type": "application/json"}, + open("tests/testdata/shinyapps-responses/create-output.json", "r").read(), + ] + + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/applications/8442", + body=open("tests/testdata/shinyapps-responses/get-output-application.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + httpretty.register_uri( + httpretty.POST, + "https://api.rstudio.cloud/v1/outputs/", + body=post_output_callback, + ) + + def post_bundle_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + del parsed_request["checksum"] + del parsed_request["content_length"] + try: + self.assertDictEqual( + 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/shinyapps-responses/create-bundle.json", "r").read(), + ] + + httpretty.register_uri( + httpretty.POST, + "https://api.rstudio.cloud/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: + self.assertDictEqual(parsed_request, {"status": "ready"}) + except AssertionError as e: + return _error_to_response(e) + return [303, {"Location": "https://api.rstudio.cloud/v1/bundles/12640"}, ""] + + httpretty.register_uri( + httpretty.POST, + "https://api.rstudio.cloud/v1/bundles/12640/status", + body=post_bundle_status_callback, + ) + + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/bundles/12640", + body=open("tests/testdata/shinyapps-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: + self.assertDictEqual(parsed_request, {"bundle": 12640, "rebuild": False}) + except AssertionError as e: + return _error_to_response(e) + return [ + 303, + {"Location": "https://api.rstudio.cloud/v1/tasks/333"}, + open("tests/testdata/shinyapps-responses/post-deploy.json", "r").read(), + ] + + httpretty.register_uri( + httpretty.POST, + "https://api.rstudio.cloud/v1/applications/8442/deploy", + body=post_deploy_callback, + ) + + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/tasks/333", + body=open("tests/testdata/shinyapps-responses/get-task.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + + runner = CliRunner() + args = [ + "deploy", + "manifest", + get_manifest_path("shinyapp"), + "--server", + "rstudio.cloud", + "--account", + "some-account", + "--token", + "someToken", + "--secret", + "c29tZVNlY3JldAo=", + "--title", + "myApp", + ] + try: + result = runner.invoke(cli, args) + self.assertEqual(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 + def test_deploy_api(self): target = optional_target(get_api_path("flask")) runner = CliRunner() @@ -315,6 +480,41 @@ def test_add_shinyapps(self): if original_server_value: os.environ["CONNECT_SERVER"] = original_server_value + @httpretty.activate(verbose=True, allow_net_connect=False) + def test_add_shinyapps(self): + original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) + original_server_value = os.environ.pop("CONNECT_SERVER", None) + try: + httpretty.register_uri( + httpretty.GET, "https://api.rstudio.cloud/v1/users/me", body='{"id": 1000}', status=200 + ) + + runner = CliRunner() + result = runner.invoke( + cli, + [ + "add", + "--account", + "some-account", + "--name", + "my-cloud", + "--token", + "someToken", + "--secret", + "c29tZVNlY3JldAo=", + "--server", + "rstudio.cloud", + ], + ) + self.assertEqual(result.exit_code, 0, result.output) + self.assertIn("RStudio Cloud credential", 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 + def test_add_shinyapps_missing_options(self): original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) original_server_value = os.environ.pop("CONNECT_SERVER", None) diff --git a/tests/testdata/shinyapps-responses/create-output.json b/tests/testdata/shinyapps-responses/create-output.json new file mode 100644 index 00000000..982e0d73 --- /dev/null +++ b/tests/testdata/shinyapps-responses/create-output.json @@ -0,0 +1,71 @@ +{ + "id": 1, + "uuid": "5fd4c4c7cf584ab8b8541792f38f4495", + "name": "myapp", + "content_type": "output", + "description": null, + "author": { + "id": 47261, + "first_name": "Example", + "last_name": "User", + "display_name": "Example User", + "organization": null, + "homepage": null, + "location": null, + "picture_url": "https://www.gravatar.com/avatar/e1bc03fb1393b9b1c3b3dc309cc0b4cf", + "email": "example.user@rstudio.com", + "superuser": false, + "email_verified": true, + "local_auth": true, + "referral": null, + "google_auth_id": "100000000000000000000", + "github_auth_id": null, + "github_auth_token": null, + "last_login_attempt": "2022-05-31T22:20:28", + "login_attempts": 0, + "lockout_until": null, + "sso_account_id": null, + "grant": null, + "created_time": "2021-09-28T19:32:47", + "updated_time": "2022-05-31T22:20:28" + }, + "author_id": 47261, + "account_id": 1, + "space_id": 32, + "output_mode": "shiny", + "visibility": "public", + "status": "pending", + "state": "active", + "`url': `http": "//staging.rstudio.cloud/content/123`", + "source": { + "id": 8442, + "name": "0a1006cc11bd4e6f8137d01440aead0a-output-source", + "uuid": "f0360a2216414e76bdacd81d52745c59", + "type": "connect", + "status": "pending", + "account_id": 82069, + "deployment_id": null, + "next_deployment_id": null, + "prev_deployment_id": null, + "clone_parent_id": 23, + "exportable": false, + "created_time": "2017-06-16T14:41:06.802874", + "updated_time": "2017-06-16T14:41:06.802874" + }, + "source_id": 8442, + "permissions": [ + "ARCHIVE_OUTPUT", + "CREATE_CLONE_OUTPUT_SESSION", + "CREATE_SOURCE_OUTPUT_SESSION", + "DELETE_OUTPUT", + "MANAGE_OUTPUT_ACCESS", + "MODIFY_OUTPUT", + "MOVE_OUTPUT", + "RESTORE_ARCHIVED_OUTPUT", + "RESTORE_TRASHED_OUTPUT", + "TRASH_OUTPUT", + "VIEW_OUTPUT" + ], + "created_time": "2017-06-16T14:41:06.802874", + "updated_time": "2017-06-16T14:41:06.802873" +} diff --git a/tests/testdata/shinyapps-responses/get-output-application.json b/tests/testdata/shinyapps-responses/get-output-application.json new file mode 100644 index 00000000..d0559809 --- /dev/null +++ b/tests/testdata/shinyapps-responses/get-output-application.json @@ -0,0 +1,39 @@ +{ + "id": 8442, + "name": "0a1006cc11bd4e6f8137d01440aead0a-output-source", + "uuid": "f0360a2216414e76bdacd81d52745c59", + "url": "https://f0360a2216414e76bdacd81d52745c59.app.rstudio.cloud", + "type": "connect", + "mode": null, + "scheduler": "kubernetes", + "status": "pending", + "account_id": 47261, + "content_id": 1, + "logplex_channel": null, + "logplex_token": null, + "storage_initialized": true, + "deployment_id": null, + "deployment": null, + "environment": {}, + "resources": { + "memory_limit": 1024, + "cpu_limit": 1, + "effective_memory_limit": 1024, + "effective_cpu_limit": 1 + }, + "configuration": { + "timeout_minutes": 15, + "timeout_kill_minutes": 60, + "effective_timeout_minutes": 15, + "effective_timeout_kill_minutes": 60 + }, + "runtime_options": null, + "next_deployment_id": null, + "prev_deployment_id": null, + "clone_parent_id": null, + "copy_parent_id": null, + "storage": [], + "exportable": false, + "created_time": "2018-07-20T22:46:41", + "updated_time": "2018-07-20T23:06:06" +} From 8b5be54b60dc09977b84a24ff182ad408a29a5c1 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:00:19 -0500 Subject: [PATCH 02/24] remove extraneous logging --- rsconnect/api.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index f0a72959..acfc6c64 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1037,7 +1037,6 @@ def create_output(self, name, project_id=None, space_id=None): "space": space_id, "project": project_id, } - print(f"creating output with {data}") return self.post("/v1/outputs/", body=data) def get_accounts(self): @@ -1188,22 +1187,16 @@ def prepare_deploy( "No account found by name : %s for given user credential" % self._server.account_name ) - print("********") if app_id is None: - print("app_id is none") if project_application_id is not None: - print(f"project_application_id is {project_application_id}") project_application = self._lucid_client.get_application(project_application_id) self._server.handle_bad_response(project_application) project_id = project_application.json_data["content_id"] - print(f"project_id is {project_id}") else: project_id = None output = self._lucid_client.create_output(name=app_name, project_id=project_id) self._server.handle_bad_response(output) - print("****here is the output") - print(output.json_data) app_id = output.json_data["source_id"] application = self._lucid_client.get_application(app_id) From e84a5333f8867b8a06f46e83d960bbd8450dc6b1 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:12:38 -0500 Subject: [PATCH 03/24] remove extraneous logging --- rsconnect/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index acfc6c64..75d207c9 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1089,8 +1089,6 @@ def wait_until_task_is_successful(self, task_id, timeout=180): print(" {} - {}".format(status, description)) time.sleep(2) - print("****task json data") - print(task.json_data) print("Task done: {}".format(description)) def get_applications_like_name(self, name): From e4b6573f0582aef158fce41139fc9e728ef1da81 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:14:10 -0500 Subject: [PATCH 04/24] rename test --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 21d4d2ff..238fc588 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -481,7 +481,7 @@ def test_add_shinyapps(self): os.environ["CONNECT_SERVER"] = original_server_value @httpretty.activate(verbose=True, allow_net_connect=False) - def test_add_shinyapps(self): + def test_add_cloud(self): original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) original_server_value = os.environ.pop("CONNECT_SERVER", None) try: From 178fcbf20830133deeddc208d210c507516dedd5 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:19:48 -0500 Subject: [PATCH 05/24] fix type checking --- rsconnect/api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index 75d207c9..4a557659 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -559,6 +559,7 @@ def validate_lucid_server( account_name = account_name or self.remote_server.account_name token = token or self.remote_server.token secret = secret or self.remote_server.secret + server: LucidServer if "rstudio.cloud" in url: server = CloudServer(url, account_name, token, secret) else: @@ -652,7 +653,7 @@ def upload_lucid_bundle(self, prepare_deploy_result, bundle_size: int, contents) def deploy_bundle( self, app_id: int = None, - project_application_id: int = None, + project_application_id: str = None, deployment_name: str = None, title: str = None, title_is_default: bool = False, @@ -1117,7 +1118,7 @@ def __init__(self, lucid_client: LucidClient, server: ShinyappsServer): self._lucid_client = lucid_client self._server = server - def prepare_deploy(self, app_id: typing.Optional[str], app_name: str, bundle_size: int, bundle_hash: str): + def prepare_deploy(self, app_id: typing.Optional[int], app_name: str, bundle_size: int, bundle_hash: str): accounts = self._lucid_client.get_accounts() self._server.handle_bad_response(accounts) account = next( @@ -1168,7 +1169,7 @@ def __init__(self, lucid_client: LucidClient, server: CloudServer): def prepare_deploy( self, - app_id: typing.Optional[str], + app_id: typing.Optional[int], project_application_id: typing.Optional[str], app_name: str, bundle_size: int, From 900ec6bec4c822b5de3799236651fa468602d414 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:31:35 -0500 Subject: [PATCH 06/24] fix typing --- rsconnect/api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index 4a557659..5e96ebc8 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -559,11 +559,11 @@ def validate_lucid_server( account_name = account_name or self.remote_server.account_name token = token or self.remote_server.token secret = secret or self.remote_server.secret - server: LucidServer - if "rstudio.cloud" in url: - server = CloudServer(url, account_name, token, secret) - else: - server = ShinyappsServer(url, account_name, token, secret) + server = ( + CloudServer(url, account_name, token, secret) + if "rstudio.cloud" in url + else ShinyappsServer(url, account_name, token, secret) + ) with LucidClient(server) as client: try: From bf4030d052c8dcd13484f2633d8a7efb5424f214 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:37:37 -0500 Subject: [PATCH 07/24] fix test_deploy_manifest_cloud failure --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 238fc588..8f6c0437 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -297,7 +297,7 @@ def test_deploy_manifest_cloud(self): def post_output_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"name": "myapp", "space_id": None, "project_id": None}) + self.assertDictEqual(parsed_request, {"name": "myapp", "space": None, "project": None}) except AssertionError as e: return _error_to_response(e) return [ From b2f0c5871ab61426a8d95b0120b48bb12620fdde Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 15:49:45 -0500 Subject: [PATCH 08/24] pin mock_connect flask version to 2.1.3 for compatibility --- mock_connect/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock_connect/Dockerfile b/mock_connect/Dockerfile index 6abd3c20..99c7310a 100644 --- a/mock_connect/Dockerfile +++ b/mock_connect/Dockerfile @@ -2,4 +2,4 @@ FROM python:3.7-alpine MAINTAINER RStudio Connect # Add the Python packags we need. -RUN pip install flask +RUN pip install flask==2.1.3 From 6833acc0b393ca7e42ec1cbe23b5d6852bd851ae Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 16:16:16 -0500 Subject: [PATCH 09/24] rename text fixtures folder --- tests/test_main.py | 36 +++++++++---------- .../create-application.json | 0 .../create-bundle.json | 0 .../create-output.json | 0 .../get-accounts.json | 0 .../get-applications.json | 0 .../get-bundle.json | 0 .../get-output-application.json | 0 .../get-task.json | 0 .../get-user.json | 0 .../post-deploy.json | 0 11 files changed, 18 insertions(+), 18 deletions(-) rename tests/testdata/{shinyapps-responses => lucid-responses}/create-application.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/create-bundle.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/create-output.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/get-accounts.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/get-applications.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/get-bundle.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/get-output-application.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/get-task.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/get-user.json (100%) rename tests/testdata/{shinyapps-responses => lucid-responses}/post-deploy.json (100%) diff --git a/tests/test_main.py b/tests/test_main.py index 8f6c0437..9fd732a3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -119,21 +119,21 @@ def test_deploy_manifest_shinyapps(self): httpretty.register_uri( httpretty.GET, "https://api.shinyapps.io/v1/users/me", - body=open("tests/testdata/shinyapps-responses/get-user.json", "r").read(), + body=open("tests/testdata/lucid-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/shinyapps-responses/get-applications.json", "r").read(), + body=open("tests/testdata/lucid-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/shinyapps-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -147,7 +147,7 @@ def post_application_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/shinyapps-responses/create-application.json", "r").read(), + open("tests/testdata/lucid-responses/create-application.json", "r").read(), ] httpretty.register_uri( @@ -174,7 +174,7 @@ def post_bundle_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/shinyapps-responses/create-bundle.json", "r").read(), + open("tests/testdata/lucid-responses/create-bundle.json", "r").read(), ] httpretty.register_uri( @@ -213,7 +213,7 @@ def post_bundle_status_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.shinyapps.io/v1/bundles/12640", - body=open("tests/testdata/shinyapps-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -227,7 +227,7 @@ def post_deploy_callback(request, uri, response_headers): return [ 303, {"Location": "https://api.shinyapps.io/v1/tasks/333"}, - open("tests/testdata/shinyapps-responses/post-deploy.json", "r").read(), + open("tests/testdata/lucid-responses/post-deploy.json", "r").read(), ] httpretty.register_uri( @@ -239,7 +239,7 @@ def post_deploy_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.shinyapps.io/v1/tasks/333", - body=open("tests/testdata/shinyapps-responses/get-task.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-task.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -275,21 +275,21 @@ def test_deploy_manifest_cloud(self): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/users/me", - body=open("tests/testdata/shinyapps-responses/get-user.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-user.json", "r").read(), status=200, ) httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/applications" "?filter=name:like:shinyapp&offset=0&count=100&use_advanced_filters=true", - body=open("tests/testdata/shinyapps-responses/get-applications.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-applications.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/accounts/", - body=open("tests/testdata/shinyapps-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -297,19 +297,19 @@ def test_deploy_manifest_cloud(self): def post_output_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"name": "myapp", "space": None, "project": None}) + self.assertDictEqual(parsed_request, {"name": "myapp", "space": None, "project": 555}) except AssertionError as e: return _error_to_response(e) return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/shinyapps-responses/create-output.json", "r").read(), + open("tests/testdata/lucid-responses/create-output.json", "r").read(), ] httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/applications/8442", - body=open("tests/testdata/shinyapps-responses/get-output-application.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-output-application.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -337,7 +337,7 @@ def post_bundle_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/shinyapps-responses/create-bundle.json", "r").read(), + open("tests/testdata/lucid-responses/create-bundle.json", "r").read(), ] httpretty.register_uri( @@ -376,7 +376,7 @@ def post_bundle_status_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/bundles/12640", - body=open("tests/testdata/shinyapps-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -390,7 +390,7 @@ def post_deploy_callback(request, uri, response_headers): return [ 303, {"Location": "https://api.rstudio.cloud/v1/tasks/333"}, - open("tests/testdata/shinyapps-responses/post-deploy.json", "r").read(), + open("tests/testdata/lucid-responses/post-deploy.json", "r").read(), ] httpretty.register_uri( @@ -402,7 +402,7 @@ def post_deploy_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/tasks/333", - body=open("tests/testdata/shinyapps-responses/get-task.json", "r").read(), + body=open("tests/testdata/lucid-responses/get-task.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) diff --git a/tests/testdata/shinyapps-responses/create-application.json b/tests/testdata/lucid-responses/create-application.json similarity index 100% rename from tests/testdata/shinyapps-responses/create-application.json rename to tests/testdata/lucid-responses/create-application.json diff --git a/tests/testdata/shinyapps-responses/create-bundle.json b/tests/testdata/lucid-responses/create-bundle.json similarity index 100% rename from tests/testdata/shinyapps-responses/create-bundle.json rename to tests/testdata/lucid-responses/create-bundle.json diff --git a/tests/testdata/shinyapps-responses/create-output.json b/tests/testdata/lucid-responses/create-output.json similarity index 100% rename from tests/testdata/shinyapps-responses/create-output.json rename to tests/testdata/lucid-responses/create-output.json diff --git a/tests/testdata/shinyapps-responses/get-accounts.json b/tests/testdata/lucid-responses/get-accounts.json similarity index 100% rename from tests/testdata/shinyapps-responses/get-accounts.json rename to tests/testdata/lucid-responses/get-accounts.json diff --git a/tests/testdata/shinyapps-responses/get-applications.json b/tests/testdata/lucid-responses/get-applications.json similarity index 100% rename from tests/testdata/shinyapps-responses/get-applications.json rename to tests/testdata/lucid-responses/get-applications.json diff --git a/tests/testdata/shinyapps-responses/get-bundle.json b/tests/testdata/lucid-responses/get-bundle.json similarity index 100% rename from tests/testdata/shinyapps-responses/get-bundle.json rename to tests/testdata/lucid-responses/get-bundle.json diff --git a/tests/testdata/shinyapps-responses/get-output-application.json b/tests/testdata/lucid-responses/get-output-application.json similarity index 100% rename from tests/testdata/shinyapps-responses/get-output-application.json rename to tests/testdata/lucid-responses/get-output-application.json diff --git a/tests/testdata/shinyapps-responses/get-task.json b/tests/testdata/lucid-responses/get-task.json similarity index 100% rename from tests/testdata/shinyapps-responses/get-task.json rename to tests/testdata/lucid-responses/get-task.json diff --git a/tests/testdata/shinyapps-responses/get-user.json b/tests/testdata/lucid-responses/get-user.json similarity index 100% rename from tests/testdata/shinyapps-responses/get-user.json rename to tests/testdata/lucid-responses/get-user.json diff --git a/tests/testdata/shinyapps-responses/post-deploy.json b/tests/testdata/lucid-responses/post-deploy.json similarity index 100% rename from tests/testdata/shinyapps-responses/post-deploy.json rename to tests/testdata/lucid-responses/post-deploy.json From 1b3d5d190752658aaaf8f36dffe0754d6e89f730 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 17:19:16 -0500 Subject: [PATCH 10/24] add test case for associating output with project --- tests/test_main.py | 112 ++++++++------ .../get-project-application.json | 143 ++++++++++++++++++ 2 files changed, 207 insertions(+), 48 deletions(-) create mode 100644 tests/testdata/lucid-responses/get-project-application.json diff --git a/tests/test_main.py b/tests/test_main.py index 9fd732a3..607305db 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,9 +3,8 @@ import shutil from os.path import join -from unittest import TestCase - import httpretty +import pytest from click.testing import CliRunner from .utils import ( @@ -36,21 +35,21 @@ def _load_json(data): return json.loads(data) -class TestMain(TestCase): - def setUp(self): +class TestMain: + def setup_method(self): shutil.rmtree("test-home", ignore_errors=True) os.environ["HOME"] = "test-home" def require_connect(self): connect_server = os.environ.get("CONNECT_SERVER", None) if connect_server is None: - self.skipTest("Set CONNECT_SERVER to test this function.") + pytest.skip("Set CONNECT_SERVER to test this function.") return connect_server def require_api_key(self): connect_api_key = os.environ.get("CONNECT_API_KEY", None) if connect_api_key is None: - self.skipTest("Set CONNECT_API_KEY to test this function.") + pytest.skip("Set CONNECT_API_KEY to test this function.") return connect_api_key @staticmethod @@ -75,15 +74,15 @@ def create_deploy_args(self, deploy_command, target): def test_version(self): runner = CliRunner() result = runner.invoke(cli, ["version"]) - self.assertEqual(result.exit_code, 0, result.output) - self.assertIn(VERSION, result.output) + assert result.exit_code == 0, result.output + assert VERSION in result.output def test_ping(self): connect_server = self.require_connect() runner = CliRunner() result = runner.invoke(cli, ["details", "-s", connect_server]) - self.assertEqual(result.exit_code, 0, result.output) - self.assertIn("OK", result.output) + assert result.exit_code == 0, result.output + assert "OK" in result.output def test_ping_api_key(self): connect_server = require_connect(self) @@ -92,15 +91,15 @@ def test_ping_api_key(self): args = ["details"] apply_common_args(args, server=connect_server, key=api_key) result = runner.invoke(cli, args) - self.assertEqual(result.exit_code, 0, result.output) - self.assertIn("OK", result.output) + assert result.exit_code == 0, result.output + assert "OK" in result.output def test_deploy(self): target = optional_target(get_dir(join("pip1", "dummy.ipynb"))) runner = CliRunner() args = self.create_deploy_args("notebook", target) result = runner.invoke(cli, args) - self.assertEqual(result.exit_code, 0, result.output) + assert result.exit_code == 0, result.output # noinspection SpellCheckingInspection def test_deploy_manifest(self): @@ -108,7 +107,7 @@ def test_deploy_manifest(self): runner = CliRunner() args = self.create_deploy_args("manifest", target) result = runner.invoke(cli, args) - self.assertEqual(result.exit_code, 0, result.output) + assert result.exit_code == 0, result.output # noinspection SpellCheckingInspection @httpretty.activate(verbose=True, allow_net_connect=False) @@ -141,7 +140,7 @@ def test_deploy_manifest_shinyapps(self): def post_application_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"account": 82069, "name": "myapp", "template": "shiny"}) + assert parsed_request == {"account": 82069, "name": "myapp", "template": "shiny"} except AssertionError as e: return _error_to_response(e) return [ @@ -162,13 +161,10 @@ def post_bundle_callback(request, uri, response_headers): del parsed_request["checksum"] del parsed_request["content_length"] try: - self.assertDictEqual( - parsed_request, - { - "application": 8442, - "content_type": "application/x-tar", - }, - ) + assert parsed_request == { + "application": 8442, + "content_type": "application/x-tar", + } except AssertionError as e: return _error_to_response(e) return [ @@ -199,7 +195,7 @@ def post_bundle_callback(request, uri, response_headers): def post_bundle_status_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"status": "ready"}) + assert parsed_request == {"status": "ready"} except AssertionError as e: return _error_to_response(e) return [303, {"Location": "https://api.shinyapps.io/v1/bundles/12640"}, ""] @@ -221,7 +217,7 @@ def post_bundle_status_callback(request, uri, response_headers): def post_deploy_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"bundle": 12640, "rebuild": False}) + assert parsed_request == {"bundle": 12640, "rebuild": False} except AssertionError as e: return _error_to_response(e) return [ @@ -260,7 +256,7 @@ def post_deploy_callback(request, uri, response_headers): ] try: result = runner.invoke(cli, args) - self.assertEqual(result.exit_code, 0, result.output) + assert result.exit_code == 0, result.output finally: if original_api_key_value: os.environ["CONNECT_API_KEY"] = original_api_key_value @@ -268,9 +264,21 @@ def post_deploy_callback(request, uri, response_headers): os.environ["CONNECT_SERVER"] = original_server_value @httpretty.activate(verbose=True, allow_net_connect=False) - def test_deploy_manifest_cloud(self): + @pytest.mark.parametrize( + "project_application_id,project_id", + [(None, None), ("444", 555)], + ids=["without associated project", "with associated project"], + ) + def test_deploy_manifest_cloud(self, project_application_id, project_id): + """ + {} hellooo + """.format( + project_id + ) original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) original_server_value = os.environ.pop("CONNECT_SERVER", None) + if project_application_id: + os.environ["LUCID_APPLICATION_ID"] = project_application_id httpretty.register_uri( httpretty.GET, @@ -294,10 +302,19 @@ def test_deploy_manifest_cloud(self): status=200, ) + if project_application_id: + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/applications/444", + body=open("tests/testdata/lucid-responses/get-project-application.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) + def post_output_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"name": "myapp", "space": None, "project": 555}) + assert parsed_request == {"name": "myapp", "space": None, "project": project_id} except AssertionError as e: return _error_to_response(e) return [ @@ -325,13 +342,10 @@ def post_bundle_callback(request, uri, response_headers): del parsed_request["checksum"] del parsed_request["content_length"] try: - self.assertDictEqual( - parsed_request, - { - "application": 8442, - "content_type": "application/x-tar", - }, - ) + assert parsed_request == { + "application": 8442, + "content_type": "application/x-tar", + } except AssertionError as e: return _error_to_response(e) return [ @@ -362,7 +376,7 @@ def post_bundle_callback(request, uri, response_headers): def post_bundle_status_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"status": "ready"}) + assert parsed_request == {"status": "ready"} except AssertionError as e: return _error_to_response(e) return [303, {"Location": "https://api.rstudio.cloud/v1/bundles/12640"}, ""] @@ -384,7 +398,7 @@ def post_bundle_status_callback(request, uri, response_headers): def post_deploy_callback(request, uri, response_headers): parsed_request = _load_json(request.body) try: - self.assertDictEqual(parsed_request, {"bundle": 12640, "rebuild": False}) + assert parsed_request == {"bundle": 12640, "rebuild": False} except AssertionError as e: return _error_to_response(e) return [ @@ -425,27 +439,29 @@ def post_deploy_callback(request, uri, response_headers): ] try: result = runner.invoke(cli, args) - self.assertEqual(result.exit_code, 0, result.output) + 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 + if project_application_id: + del os.environ["LUCID_APPLICATION_ID"] def test_deploy_api(self): target = optional_target(get_api_path("flask")) runner = CliRunner() args = self.create_deploy_args("api", target) result = runner.invoke(cli, args) - self.assertEqual(result.exit_code, 0, result.output) + assert result.exit_code == 0, result.output def test_add_connect(self): connect_server = self.require_connect() api_key = self.require_api_key() runner = CliRunner() result = runner.invoke(cli, ["add", "--name", "my-connect", "--server", connect_server, "--api-key", api_key]) - self.assertEqual(result.exit_code, 0, result.output) - self.assertIn("OK", result.output) + assert result.exit_code == 0, result.output + assert "OK" in result.output @httpretty.activate(verbose=True, allow_net_connect=False) def test_add_shinyapps(self): @@ -471,8 +487,8 @@ def test_add_shinyapps(self): "c29tZVNlY3JldAo=", ], ) - self.assertEqual(result.exit_code, 0, result.output) - self.assertIn("shinyapps.io credential", result.output) + assert result.exit_code == 0, result.output + assert "shinyapps.io credential" in result.output finally: if original_api_key_value: @@ -506,8 +522,8 @@ def test_add_cloud(self): "rstudio.cloud", ], ) - self.assertEqual(result.exit_code, 0, result.output) - self.assertIn("RStudio Cloud credential", result.output) + assert result.exit_code == 0, result.output + assert "RStudio Cloud credential" in result.output finally: if original_api_key_value: @@ -530,10 +546,10 @@ def test_add_shinyapps_missing_options(self): "someToken", ], ) - self.assertEqual(result.exit_code, 1, result.output) - self.assertEqual( - str(result.exception), - "-A/--account, -T/--token, and -S/--secret must all be provided for shinyapps.io.", + assert result.exit_code == 1, result.output + assert ( + str(result.exception) + == "-A/--account, -T/--token, and -S/--secret must all be provided for shinyapps.io." ) finally: if original_api_key_value: diff --git a/tests/testdata/lucid-responses/get-project-application.json b/tests/testdata/lucid-responses/get-project-application.json new file mode 100644 index 00000000..38dc4cde --- /dev/null +++ b/tests/testdata/lucid-responses/get-project-application.json @@ -0,0 +1,143 @@ +{ + "id": 1230843, + "name": "21b10f198835410d8ffb340b0ae815b6-project-source", + "uuid": "5f2a944784f24c4ebf0ee9a4d1afa210", + "url": "https://5f2a944784f24c4ebf0ee9a4d1afa210.app.staging.rstudio.cloud/", + "type": "ide", + "mode": null, + "scheduler": "kubernetes", + "status": "sleeping", + "account_id": 82069, + "content_id": 555, + "storage_initialized": true, + "deployment_id": 936660, + "deployment": { + "id": 936660, + "application_memory_limit": 1024, + "application_cpu_limit": 1.0, + "application_timeout_minutes": 15, + "application_timeout_kill_minutes": 60, + "application_os_version": "focal", + "image": { + "id": 20368, + "app_id": null, + "active": true, + "status": "ready", + "bundle_id": 301, + "bundle": { + "id": 301, + "app_id": null, + "user_id": 1, + "status": "ready", + "name": "default-ide", + "url": "s3://lucid-uploads-staging/bundles/bare-ide.tar.gz", + "checksum": "9f50ae5883f5d9924a9247362cf25e03", + "parent_id": null, + "created_time": "2017-04-19T20:54:10", + "updated_time": "2017-07-20T20:09:54" + }, + "manifest": null, + "repository": null, + "registry": "default", + "tag": "provided", + "created_time": "2021-02-11T14:50:36", + "updated_time": "2021-02-11T14:50:36" + }, + "properties": { + "application.visibility": "private", + "application.build-pool": null, + "application.ide.image.tag": null, + "application.jupyter.image.tag": null, + "application.initialize.image.tag": null, + "application.sidecar.image.tag": null, + "application.ide.autosave.on.idle": true, + "application.instances.template": "large", + "application.instances.start": 1, + "application.instances.load.factor": 0.5, + "application.instances.idle-threshold": 15, + "application.instances.fault-threshold": 1, + "application.instances.agent-pool": null, + "application.shiny.timeout.init": 60, + "application.shiny.timeout.idle": 5, + "application.shiny.timeout.conn": 900, + "application.shiny.timeout.read": 3600, + "application.shiny.timeout.reconnect": null, + "application.shiny.scheduler.max.requests": 20, + "application.shiny.scheduler.max.processes": 3, + "application.shiny.scheduler.min.processes": 0, + "application.shiny.scheduler.load.factor": 0.05, + "application.shiny.sockjs.protocols.disabled": null, + "application.connect.debug.log": "", + "application.connect.version": "current", + "application.package.cache": true, + "application.ide.version": "current", + "application.frontend.iFrameResizer.log": false, + "application.frontend.iFrameResizer.sizeHeight": true, + "application.frontend.iFrameResizer.sizeWidth": false, + "application.frontend.iFrameResizer.heightCalculationMethod": "bodyOffset", + "application.frontend.iFrameResizer.widthCalculationMethod": "bodyOffset", + "application.storage.size": "20G", + "application.connect.timeout.init": 60, + "application.connect.timeout.idle": 5, + "application.connect.timeout.conn": 900, + "application.connect.timeout.read": 3600, + "application.connect.timeout.reconnect": null, + "application.connect.scheduler.max.requests": 20, + "application.connect.scheduler.max.processes": 3, + "application.connect.scheduler.min.processes": 0, + "application.connect.scheduler.load.factor": 0.05, + "application.unmigratable": "" + }, + "environment": {}, + "user": { + "id": 47261, + "first_name": "Matthew", + "last_name": "Lynch", + "display_name": "Matthew Lynch", + "organization": null, + "homepage": null, + "location": null, + "picture_url": "https://www.gravatar.com/avatar/e1bc03fb1393b9b1c3b3dc309cc0b4cf", + "email": "matthew.lynch@rstudio.com", + "email_verified": true, + "local_auth": true, + "referral": null, + "google_auth_id": "109815155294302939290", + "github_auth_id": null, + "github_auth_token": null, + "last_login_attempt": "2022-09-01T22:10:50", + "login_attempts": 0, + "lockout_until": null, + "sso_account_id": null, + "grant": null, + "created_time": "2021-09-28T19:32:47", + "updated_time": "2022-09-01T22:10:50" + }, + "created_time": "2022-08-24T20:29:59", + "updated_time": "2022-08-24T20:29:59" + }, + "environment": {}, + "resources": { + "memory_limit": 1024, + "cpu_limit": 1, + "effective_memory_limit": 1024, + "effective_cpu_limit": 1 + }, + "configuration": { + "timeout_minutes": 15, + "timeout_kill_minutes": 60, + "effective_timeout_minutes": 15, + "effective_timeout_kill_minutes": 60 + }, + "runtime_options": { + "os_version": "focal" + }, + "next_deployment_id": null, + "prev_deployment_id": 928918, + "clone_parent_id": null, + "copy_parent_id": null, + "storage": [], + "exportable": true, + "created_time": "2022-08-19T19:01:54", + "updated_time": "2022-09-01T18:33:06" +} From c218918b55542ffe99d4eac6d5e7aaded67ad1a5 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 17:22:04 -0500 Subject: [PATCH 11/24] update error messaging for cloud --- rsconnect/validation.py | 6 ++++-- tests/test_main.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rsconnect/validation.py b/rsconnect/validation.py index 561f82cd..5e3d0af9 100644 --- a/rsconnect/validation.py +++ b/rsconnect/validation.py @@ -33,11 +33,13 @@ def validate_connection_options(url, api_key, insecure, cacert, account_name, to if present_connect_options and present_shinyapps_options: raise RSConnectException( - "Connect options ({}) may not be passed alongside shinyapps.io options ({}).".format( + "Connect options ({}) may not be passed alongside shinyapps.io or RStudio Cloud options ({}).".format( ", ".join(present_connect_options), ", ".join(present_shinyapps_options) ) ) if present_shinyapps_options: if len(present_shinyapps_options) != 3: - raise RSConnectException("-A/--account, -T/--token, and -S/--secret must all be provided for shinyapps.io.") + raise RSConnectException( + "-A/--account, -T/--token, and -S/--secret must all be provided for shinyapps.io or RStudio Cloud." + ) diff --git a/tests/test_main.py b/tests/test_main.py index 607305db..3050fc65 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -549,7 +549,7 @@ def test_add_shinyapps_missing_options(self): assert result.exit_code == 1, result.output assert ( str(result.exception) - == "-A/--account, -T/--token, and -S/--secret must all be provided for shinyapps.io." + == "-A/--account, -T/--token, and -S/--secret must all be provided for shinyapps.io or RStudio Cloud." ) finally: if original_api_key_value: From 019fbc56be298616918f8455833af9807ad8ab09 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 17:32:55 -0500 Subject: [PATCH 12/24] pytest fixes --- tests/test_api.py | 8 ++++---- tests/test_main.py | 8 ++++---- tests/test_main_content.py | 20 ++++++++++---------- tests/utils.py | 11 ++++++----- 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index cbd0d5ba..925db932 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,8 +8,8 @@ class TestAPI(TestCase): def test_executor_init(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() ce = RSConnectExecutor(None, connect_server, api_key, True, None) self.assertEqual(ce.remote_server.url, connect_server) @@ -47,8 +47,8 @@ def test_to_server_check_list(self): self.assertEqual(a_list, ["scheme://no-scheme"]) def test_make_deployment_name(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() ce = RSConnectExecutor(None, connect_server, api_key, True, None) self.assertEqual(ce.make_deployment_name("title", False), "title") self.assertEqual(ce.make_deployment_name("Title", False), "title") diff --git a/tests/test_main.py b/tests/test_main.py index 3050fc65..648c9601 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -63,8 +63,8 @@ def optional_ca_data(default=None): # noinspection SpellCheckingInspection def create_deploy_args(self, deploy_command, target): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() cadata_file = optional_ca_data(None) args = ["deploy", deploy_command] apply_common_args(args, server=connect_server, key=api_key, cacert=cadata_file) @@ -85,8 +85,8 @@ def test_ping(self): assert "OK" in result.output def test_ping_api_key(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() args = ["details"] apply_common_args(args, server=connect_server, key=api_key) diff --git a/tests/test_main_content.py b/tests/test_main_content.py index 7dea3ee8..7306b7e9 100644 --- a/tests/test_main_content.py +++ b/tests/test_main_content.py @@ -40,8 +40,8 @@ def test_version(self): self.assertIn(VERSION, result.output) def test_content_search(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() args = ["content", "search"] apply_common_args(args, server=connect_server, key=api_key) @@ -52,8 +52,8 @@ def test_content_search(self): self.assertEqual(len(response), 3, result.output) def test_content_describe(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() args = ["content", "describe", "-g", _content_guids[0], "-g", _content_guids[1]] apply_common_args(args, server=connect_server, key=api_key) @@ -66,8 +66,8 @@ def test_content_describe(self): self.assertEqual(response[1]["guid"], _content_guids[1]) def test_content_download_bundle(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() args = ["content", "download-bundle", "-g", _content_guids[1], "-o", _bundle_download_dest] apply_common_args(args, server=connect_server, key=api_key) @@ -77,8 +77,8 @@ def test_content_download_bundle(self): self.assertIsNotNone(tgz.extractfile("manifest.json").read()) def test_build(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() # add a content item @@ -117,8 +117,8 @@ def test_build(self): self.assertEqual(listing[0]["rsconnect_build_status"], BuildStatus.COMPLETE) def test_build_rm(self): - connect_server = require_connect(self) - api_key = require_api_key(self) + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() # remove a content item diff --git a/tests/utils.py b/tests/utils.py index affbe1ce..04eba896 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,8 @@ import sys import os from os.path import join, dirname, exists -from unittest import TestCase + +import pytest def apply_common_args(args: list, server=None, key=None, cacert=None, insecure=False): @@ -24,17 +25,17 @@ def optional_ca_data(default=None): return os.environ.get("CONNECT_CADATA_FILE", default) -def require_connect(tc: TestCase): +def require_connect(): connect_server = os.environ.get("CONNECT_SERVER", None) if connect_server is None: - tc.skipTest("Set CONNECT_SERVER to test this function.") + pytest.skip("Set CONNECT_SERVER to test this function.") return connect_server -def require_api_key(tc: TestCase): +def require_api_key(): connect_api_key = os.environ.get("CONNECT_API_KEY", None) if connect_api_key is None: - tc.skipTest("Set CONNECT_API_KEY to test this function.") + pytest.skip("Set CONNECT_API_KEY to test this function.") return connect_api_key From 75b396237d2e103e32a883f125437ca518c530ba Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 17:35:57 -0500 Subject: [PATCH 13/24] remove duplicate methods --- tests/test_main.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 648c9601..f3ffb6f5 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -40,18 +40,6 @@ def setup_method(self): shutil.rmtree("test-home", ignore_errors=True) os.environ["HOME"] = "test-home" - def require_connect(self): - connect_server = os.environ.get("CONNECT_SERVER", None) - if connect_server is None: - pytest.skip("Set CONNECT_SERVER to test this function.") - return connect_server - - def require_api_key(self): - connect_api_key = os.environ.get("CONNECT_API_KEY", None) - if connect_api_key is None: - pytest.skip("Set CONNECT_API_KEY to test this function.") - return connect_api_key - @staticmethod def optional_target(default): return os.environ.get("CONNECT_DEPLOY_TARGET", default) @@ -78,7 +66,7 @@ def test_version(self): assert VERSION in result.output def test_ping(self): - connect_server = self.require_connect() + connect_server = require_connect() runner = CliRunner() result = runner.invoke(cli, ["details", "-s", connect_server]) assert result.exit_code == 0, result.output From a85fb8270cc595e3e2793c5b0e59a59690bd8766 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 1 Sep 2022 17:39:03 -0500 Subject: [PATCH 14/24] pytest fixes --- tests/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index f3ffb6f5..d41cfd06 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -444,8 +444,8 @@ def test_deploy_api(self): assert result.exit_code == 0, result.output def test_add_connect(self): - connect_server = self.require_connect() - api_key = self.require_api_key() + connect_server = require_connect() + api_key = require_api_key() runner = CliRunner() result = runner.invoke(cli, ["add", "--name", "my-connect", "--server", connect_server, "--api-key", api_key]) assert result.exit_code == 0, result.output From 9e3314192cd641e1d8853651789adf0da5da4c19 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 8 Sep 2022 09:42:07 -0500 Subject: [PATCH 15/24] rename lucid naming to rsconnect --- rsconnect/actions.py | 8 +- rsconnect/api.py | 86 +++++++++---------- rsconnect/main.py | 16 ++-- tests/test_main.py | 36 ++++---- .../create-application.json | 0 .../create-bundle.json | 0 .../create-output.json | 0 .../get-accounts.json | 0 .../get-applications.json | 0 .../get-bundle.json | 0 .../get-output-application.json | 0 .../get-project-application.json | 0 .../get-task.json | 0 .../get-user.json | 0 .../post-deploy.json | 0 15 files changed, 73 insertions(+), 73 deletions(-) rename tests/testdata/{lucid-responses => rstudio-responses}/create-application.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/create-bundle.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/create-output.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-accounts.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-applications.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-bundle.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-output-application.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-project-application.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-task.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/get-user.json (100%) rename tests/testdata/{lucid-responses => rstudio-responses}/post-deploy.json (100%) diff --git a/rsconnect/actions.py b/rsconnect/actions.py index 53b83b80..d52963d7 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -218,8 +218,8 @@ def test_server(connect_server): raise RSConnectException("\n".join(failures)) -def test_lucid_server(server: api.LucidServer): - with api.LucidClient(server) as client: +def test_rstudio_server(server: api.RStudioServer): + with api.RStudioClient(server) as client: try: result = client.get_current_user() server.handle_bad_response(result) @@ -1588,8 +1588,8 @@ def _gather_basic_deployment_info_for_framework( if isinstance(remote_server, api.RSConnectServer): app = api.get_app_info(remote_server, app_id) existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True) - elif isinstance(remote_server, api.LucidServer): - app = api.get_lucid_app_info(remote_server, app_id) + elif isinstance(remote_server, api.RStudioServer): + app = api.get_rstudio_app_info(remote_server, app_id) existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"]) else: raise RSConnectException("Unable to infer Connect client.") diff --git a/rsconnect/api.py b/rsconnect/api.py index 5e96ebc8..c0ed514d 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -62,7 +62,7 @@ def handle_bad_response(self, response): ) -class LucidServer(AbstractRemoteServer): +class RStudioServer(AbstractRemoteServer): """ A class used to represent the server of the shinyapps.io and RStudio Cloud APIs. """ @@ -74,7 +74,7 @@ def __init__(self, remote_name: str, url: str, account_name: str, token: str, se self.secret = secret -class ShinyappsServer(LucidServer): +class ShinyappsServer(RStudioServer): """ A class to encapsulate the information needed to interact with an instance of the shinyapps.io server. @@ -87,7 +87,7 @@ def __init__(self, url: str, account_name: str, token: str, secret: str): super().__init__(remote_name=remote_name, url=url, account_name=account_name, token=token, secret=secret) -class CloudServer(LucidServer): +class CloudServer(RStudioServer): """ A class to encapsulate the information needed to interact with an instance of the RStudio Cloud server. @@ -450,8 +450,8 @@ def setup_remote_server( def setup_client(self, cookies=None, timeout=30, **kwargs): if isinstance(self.remote_server, RSConnectServer): self.client = RSConnectClient(self.remote_server, cookies, timeout) - elif isinstance(self.remote_server, LucidServer): - self.client = LucidClient(self.remote_server, timeout) + elif isinstance(self.remote_server, RStudioServer): + self.client = RStudioClient(self.remote_server, timeout) else: raise RSConnectException("Unable to infer Connect client.") @@ -480,8 +480,8 @@ def validate_server( ): if (url and api_key) or isinstance(self.remote_server, RSConnectServer): self.validate_connect_server(name, url, api_key, insecure, cacert, api_key_is_required) - elif (url and token and secret) or isinstance(self.remote_server, LucidServer): - self.validate_lucid_server(url, account_name, token, secret) + elif (url and token and secret) or isinstance(self.remote_server, RStudioServer): + self.validate_rstudio_server(url, account_name, token, secret) else: raise RSConnectException("Unable to validate server from information provided.") @@ -552,7 +552,7 @@ def validate_connect_server( return self - def validate_lucid_server( + def validate_rstudio_server( self, url: str = None, account_name: str = None, token: str = None, secret: str = None, **kwargs ): url = url or self.remote_server.url @@ -565,7 +565,7 @@ def validate_lucid_server( else ShinyappsServer(url, account_name, token, secret) ) - with LucidClient(server) as client: + with RStudioClient(server) as client: try: result = client.get_current_user() server.handle_bad_response(result) @@ -622,7 +622,7 @@ def check_server_capabilities(self, capability_functions): :param details_source: the source for obtaining server details, gather_server_details(), by default. """ - if isinstance(self.remote_server, LucidServer): + if isinstance(self.remote_server, RStudioServer): return self details = self.server_details @@ -637,7 +637,7 @@ def check_server_capabilities(self, capability_functions): raise RSConnectException(message) return self - def upload_lucid_bundle(self, prepare_deploy_result, bundle_size: int, contents): + def upload_rstudio_bundle(self, prepare_deploy_result, bundle_size: int, contents): upload_url = prepare_deploy_result.presigned_url parsed_upload_url = urlparse(upload_url) with S3Client("{}://{}".format(parsed_upload_url.scheme, parsed_upload_url.netloc), timeout=120) as s3_client: @@ -693,7 +693,7 @@ def deploy_bundle( bundle_size, bundle_hash, ) - self.upload_lucid_bundle(prepare_deploy_result, bundle_size, contents) + self.upload_rstudio_bundle(prepare_deploy_result, bundle_size, contents) shinyapps_service.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.app_id) else: cloud_service = CloudService(self.client, self.remote_server) @@ -704,7 +704,7 @@ def deploy_bundle( bundle_size, bundle_hash, ) - self.upload_lucid_bundle(prepare_deploy_result, bundle_size, contents) + self.upload_rstudio_bundle(prepare_deploy_result, bundle_size, contents) cloud_service.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.app_id) print("Application successfully deployed to {}".format(prepare_deploy_result.app_url)) @@ -815,8 +815,8 @@ def validate_app_mode(self, *args, **kwargs): if isinstance(self.remote_server, RSConnectServer): app = get_app_info(self.remote_server, app_id) existing_app_mode = AppModes.get_by_ordinal(app.get("app_mode", 0), True) - elif isinstance(self.remote_server, LucidServer): - app = get_lucid_app_info(self.remote_server, app_id) + elif isinstance(self.remote_server, RStudioServer): + app = get_rstudio_app_info(self.remote_server, app_id) existing_app_mode = AppModes.get_by_cloud_name(app.json_data["mode"]) else: raise RSConnectException("Unable to infer Connect client.") @@ -976,18 +976,18 @@ def __init__( self.output_id = output_id -class LucidClient(HTTPServer): +class RStudioClient(HTTPServer): """ An HTTP client to call the RStudio Cloud and shinyapps.io APIs. """ _TERMINAL_STATUSES = {"success", "failed", "error"} - def __init__(self, lucid_server: LucidServer, timeout: int = 30): - self._token = lucid_server.token - self._key = base64.b64decode(lucid_server.secret) - self._server = lucid_server - super().__init__(lucid_server.url, timeout=timeout) + def __init__(self, rstudio_server: RStudioServer, timeout: int = 30): + self._token = rstudio_server.token + self._key = base64.b64decode(rstudio_server.secret) + self._server = rstudio_server + super().__init__(rstudio_server.url, timeout=timeout) def _get_canonical_request(self, method, path, timestamp, content_hash): return "\n".join([method, path, timestamp, content_hash]) @@ -1114,12 +1114,12 @@ class ShinyappsService: Encapsulates operations involving multiple API calls to shinyapps.io. """ - def __init__(self, lucid_client: LucidClient, server: ShinyappsServer): - self._lucid_client = lucid_client + def __init__(self, rstudio_client: RStudioClient, server: ShinyappsServer): + self._rstudio_client = rstudio_client self._server = server def prepare_deploy(self, app_id: typing.Optional[int], app_name: str, bundle_size: int, bundle_hash: str): - accounts = self._lucid_client.get_accounts() + accounts = self._rstudio_client.get_accounts() self._server.handle_bad_response(accounts) account = next( filter(lambda acct: acct["name"] == self._server.account_name, accounts.json_data["accounts"]), None @@ -1131,14 +1131,14 @@ def prepare_deploy(self, app_id: typing.Optional[int], app_name: str, bundle_siz ) if app_id is None: - application = self._lucid_client.create_application(account["id"], app_name) + application = self._rstudio_client.create_application(account["id"], app_name) else: - application = self._lucid_client.get_application(app_id) + application = self._rstudio_client.get_application(app_id) self._server.handle_bad_response(application) app_id_int = application.json_data["id"] app_url = application.json_data["url"] - bundle = self._lucid_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) + bundle = self._rstudio_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) self._server.handle_bad_response(bundle) return PrepareDeployResult( @@ -1150,12 +1150,12 @@ def prepare_deploy(self, app_id: typing.Optional[int], app_name: str, bundle_siz ) def do_deploy(self, bundle_id, app_id): - bundle_status_response = self._lucid_client.set_bundle_status(bundle_id, "ready") + bundle_status_response = self._rstudio_client.set_bundle_status(bundle_id, "ready") self._server.handle_bad_response(bundle_status_response) - deploy_task = self._lucid_client.deploy_application(bundle_id, app_id) + deploy_task = self._rstudio_client.deploy_application(bundle_id, app_id) self._server.handle_bad_response(deploy_task) - self._lucid_client.wait_until_task_is_successful(deploy_task.json_data["id"]) + self._rstudio_client.wait_until_task_is_successful(deploy_task.json_data["id"]) class CloudService: @@ -1163,8 +1163,8 @@ class CloudService: Encapsulates operations involving multiple API calls to RStudio Cloud. """ - def __init__(self, lucid_client: LucidClient, server: CloudServer): - self._lucid_client = lucid_client + def __init__(self, rstudio_client: RStudioClient, server: CloudServer): + self._rstudio_client = rstudio_client self._server = server def prepare_deploy( @@ -1175,7 +1175,7 @@ def prepare_deploy( bundle_size: int, bundle_hash: str, ): - accounts = self._lucid_client.get_accounts() + accounts = self._rstudio_client.get_accounts() self._server.handle_bad_response(accounts) account = next( filter(lambda acct: acct["name"] == self._server.account_name, accounts.json_data["accounts"]), None @@ -1188,23 +1188,23 @@ def prepare_deploy( if app_id is None: if project_application_id is not None: - project_application = self._lucid_client.get_application(project_application_id) + project_application = self._rstudio_client.get_application(project_application_id) self._server.handle_bad_response(project_application) project_id = project_application.json_data["content_id"] else: project_id = None - output = self._lucid_client.create_output(name=app_name, project_id=project_id) + output = self._rstudio_client.create_output(name=app_name, project_id=project_id) self._server.handle_bad_response(output) app_id = output.json_data["source_id"] - application = self._lucid_client.get_application(app_id) + application = self._rstudio_client.get_application(app_id) self._server.handle_bad_response(application) app_id_int = application.json_data["id"] app_url = application.json_data["url"] output_id = application.json_data["content_id"] - bundle = self._lucid_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) + bundle = self._rstudio_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) self._server.handle_bad_response(bundle) return PrepareDeployOutputResult( @@ -1217,12 +1217,12 @@ def prepare_deploy( ) def do_deploy(self, bundle_id, app_id): - bundle_status_response = self._lucid_client.set_bundle_status(bundle_id, "ready") + bundle_status_response = self._rstudio_client.set_bundle_status(bundle_id, "ready") self._server.handle_bad_response(bundle_status_response) - deploy_task = self._lucid_client.deploy_application(bundle_id, app_id) + deploy_task = self._rstudio_client.deploy_application(bundle_id, app_id) self._server.handle_bad_response(deploy_task) - self._lucid_client.wait_until_task_is_successful(deploy_task.json_data["id"]) + self._rstudio_client.wait_until_task_is_successful(deploy_task.json_data["id"]) def verify_server(connect_server): @@ -1291,8 +1291,8 @@ def get_app_info(connect_server, app_id): return result -def get_lucid_app_info(server, app_id): - with LucidClient(server) as client: +def get_rstudio_app_info(server, app_id): + with RStudioClient(server) as client: result = client.get_application(app_id) server.handle_bad_response(result) return result @@ -1501,7 +1501,7 @@ def find_unique_name(remote_server: TargetableServer, name: str): mapping_function=lambda client, app: app["name"], ) elif isinstance(remote_server, ShinyappsServer): - client = LucidClient(remote_server) + client = RStudioClient(remote_server) existing_names = client.get_applications_like_name(name) else: # non-unique names are permitted in cloud diff --git a/rsconnect/main.py b/rsconnect/main.py index da4eeb44..5c11f4b8 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -21,7 +21,7 @@ test_server, validate_quarto_engines, which_quarto, - test_lucid_server, + test_rstudio_server, ) from .actions_content import ( download_bundle, @@ -133,7 +133,7 @@ def wrapper(*args, **kwargs): return wrapper -def lucid_args(func): +def rstudio_args(func): @click.option( "--account", "-A", @@ -287,9 +287,9 @@ def _test_server_and_api(server, api_key, insecure, ca_cert): return real_server, me -def _test_lucid_creds(server: api.LucidServer): +def _test_rstudio_creds(server: api.RStudioServer): with cli_feedback("Checking {} credential".format(server.remote_name)): - test_lucid_server(server) + test_rstudio_server(server) # noinspection SpellCheckingInspection @@ -328,7 +328,7 @@ def _test_lucid_creds(server: api.LucidServer): type=click.File(), help="The path to trusted TLS CA certificates.", ) -@lucid_args +@rstudio_args @click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.") def add(name, server, api_key, insecure, cacert, account, token, secret, verbose): @@ -352,7 +352,7 @@ def add(name, server, api_key, insecure, cacert, account, token, secret, verbose else: real_server = api.ShinyappsServer(server, account, token, secret) - _test_lucid_creds(real_server) + _test_rstudio_creds(real_server) server_store.set( name, @@ -764,7 +764,7 @@ def deploy_notebook( @server_args @content_args @cloud_content_args -@lucid_args +@rstudio_args @click.argument("file", type=click.Path(exists=True, dir_okay=True, file_okay=True)) @cli_exception_handler def deploy_manifest( @@ -1018,7 +1018,7 @@ def generate_deploy_python(app_mode, alias, min_version): ) @server_args @content_args - @lucid_args + @rstudio_args @cloud_content_args @click.option( "--entrypoint", diff --git a/tests/test_main.py b/tests/test_main.py index d41cfd06..8c30e149 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -106,21 +106,21 @@ def test_deploy_manifest_shinyapps(self): httpretty.register_uri( httpretty.GET, "https://api.shinyapps.io/v1/users/me", - body=open("tests/testdata/lucid-responses/get-user.json", "r").read(), + 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/lucid-responses/get-applications.json", "r").read(), + 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/lucid-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -134,7 +134,7 @@ def post_application_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/lucid-responses/create-application.json", "r").read(), + open("tests/testdata/rstudio-responses/create-application.json", "r").read(), ] httpretty.register_uri( @@ -158,7 +158,7 @@ def post_bundle_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/lucid-responses/create-bundle.json", "r").read(), + open("tests/testdata/rstudio-responses/create-bundle.json", "r").read(), ] httpretty.register_uri( @@ -197,7 +197,7 @@ def post_bundle_status_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.shinyapps.io/v1/bundles/12640", - body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -211,7 +211,7 @@ def post_deploy_callback(request, uri, response_headers): return [ 303, {"Location": "https://api.shinyapps.io/v1/tasks/333"}, - open("tests/testdata/lucid-responses/post-deploy.json", "r").read(), + open("tests/testdata/rstudio-responses/post-deploy.json", "r").read(), ] httpretty.register_uri( @@ -223,7 +223,7 @@ def post_deploy_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.shinyapps.io/v1/tasks/333", - body=open("tests/testdata/lucid-responses/get-task.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-task.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -271,21 +271,21 @@ def test_deploy_manifest_cloud(self, project_application_id, project_id): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/users/me", - body=open("tests/testdata/lucid-responses/get-user.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-user.json", "r").read(), status=200, ) httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/applications" "?filter=name:like:shinyapp&offset=0&count=100&use_advanced_filters=true", - body=open("tests/testdata/lucid-responses/get-applications.json", "r").read(), + 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.rstudio.cloud/v1/accounts/", - body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -294,7 +294,7 @@ def test_deploy_manifest_cloud(self, project_application_id, project_id): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/applications/444", - body=open("tests/testdata/lucid-responses/get-project-application.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-project-application.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -308,13 +308,13 @@ def post_output_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/lucid-responses/create-output.json", "r").read(), + open("tests/testdata/rstudio-responses/create-output.json", "r").read(), ] httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/applications/8442", - body=open("tests/testdata/lucid-responses/get-output-application.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-output-application.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -339,7 +339,7 @@ def post_bundle_callback(request, uri, response_headers): return [ 201, {"Content-Type": "application/json"}, - open("tests/testdata/lucid-responses/create-bundle.json", "r").read(), + open("tests/testdata/rstudio-responses/create-bundle.json", "r").read(), ] httpretty.register_uri( @@ -378,7 +378,7 @@ def post_bundle_status_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/bundles/12640", - body=open("tests/testdata/lucid-responses/get-accounts.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-accounts.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) @@ -392,7 +392,7 @@ def post_deploy_callback(request, uri, response_headers): return [ 303, {"Location": "https://api.rstudio.cloud/v1/tasks/333"}, - open("tests/testdata/lucid-responses/post-deploy.json", "r").read(), + open("tests/testdata/rstudio-responses/post-deploy.json", "r").read(), ] httpretty.register_uri( @@ -404,7 +404,7 @@ def post_deploy_callback(request, uri, response_headers): httpretty.register_uri( httpretty.GET, "https://api.rstudio.cloud/v1/tasks/333", - body=open("tests/testdata/lucid-responses/get-task.json", "r").read(), + body=open("tests/testdata/rstudio-responses/get-task.json", "r").read(), adding_headers={"Content-Type": "application/json"}, status=200, ) diff --git a/tests/testdata/lucid-responses/create-application.json b/tests/testdata/rstudio-responses/create-application.json similarity index 100% rename from tests/testdata/lucid-responses/create-application.json rename to tests/testdata/rstudio-responses/create-application.json diff --git a/tests/testdata/lucid-responses/create-bundle.json b/tests/testdata/rstudio-responses/create-bundle.json similarity index 100% rename from tests/testdata/lucid-responses/create-bundle.json rename to tests/testdata/rstudio-responses/create-bundle.json diff --git a/tests/testdata/lucid-responses/create-output.json b/tests/testdata/rstudio-responses/create-output.json similarity index 100% rename from tests/testdata/lucid-responses/create-output.json rename to tests/testdata/rstudio-responses/create-output.json diff --git a/tests/testdata/lucid-responses/get-accounts.json b/tests/testdata/rstudio-responses/get-accounts.json similarity index 100% rename from tests/testdata/lucid-responses/get-accounts.json rename to tests/testdata/rstudio-responses/get-accounts.json diff --git a/tests/testdata/lucid-responses/get-applications.json b/tests/testdata/rstudio-responses/get-applications.json similarity index 100% rename from tests/testdata/lucid-responses/get-applications.json rename to tests/testdata/rstudio-responses/get-applications.json diff --git a/tests/testdata/lucid-responses/get-bundle.json b/tests/testdata/rstudio-responses/get-bundle.json similarity index 100% rename from tests/testdata/lucid-responses/get-bundle.json rename to tests/testdata/rstudio-responses/get-bundle.json diff --git a/tests/testdata/lucid-responses/get-output-application.json b/tests/testdata/rstudio-responses/get-output-application.json similarity index 100% rename from tests/testdata/lucid-responses/get-output-application.json rename to tests/testdata/rstudio-responses/get-output-application.json diff --git a/tests/testdata/lucid-responses/get-project-application.json b/tests/testdata/rstudio-responses/get-project-application.json similarity index 100% rename from tests/testdata/lucid-responses/get-project-application.json rename to tests/testdata/rstudio-responses/get-project-application.json diff --git a/tests/testdata/lucid-responses/get-task.json b/tests/testdata/rstudio-responses/get-task.json similarity index 100% rename from tests/testdata/lucid-responses/get-task.json rename to tests/testdata/rstudio-responses/get-task.json diff --git a/tests/testdata/lucid-responses/get-user.json b/tests/testdata/rstudio-responses/get-user.json similarity index 100% rename from tests/testdata/lucid-responses/get-user.json rename to tests/testdata/rstudio-responses/get-user.json diff --git a/tests/testdata/lucid-responses/post-deploy.json b/tests/testdata/rstudio-responses/post-deploy.json similarity index 100% rename from tests/testdata/lucid-responses/post-deploy.json rename to tests/testdata/rstudio-responses/post-deploy.json From acdd58d2777b0b2cff8e32272def1ed3d7e54b82 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 8 Sep 2022 09:51:02 -0500 Subject: [PATCH 16/24] remove project application id from CLI, but keep environment variable --- rsconnect/api.py | 6 ++---- rsconnect/main.py | 18 ------------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index c0ed514d..ab0875c6 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1,6 +1,7 @@ """ RStudio Connect API client and utility functions """ +import os from os.path import abspath import time from typing import IO, Callable @@ -653,7 +654,6 @@ def upload_rstudio_bundle(self, prepare_deploy_result, bundle_size: int, content def deploy_bundle( self, app_id: int = None, - project_application_id: str = None, deployment_name: str = None, title: str = None, title_is_default: bool = False, @@ -661,7 +661,6 @@ def deploy_bundle( env_vars=None, ): app_id = app_id or self.get("app_id") - project_application_id = project_application_id or self.get("project_application_id") deployment_name = deployment_name or self.get("deployment_name") title = title or self.get("title") title_is_default = title_is_default or self.get("title_is_default") @@ -699,7 +698,6 @@ def deploy_bundle( cloud_service = CloudService(self.client, self.remote_server) prepare_deploy_result = cloud_service.prepare_deploy( app_id, - project_application_id, deployment_name, bundle_size, bundle_hash, @@ -1170,7 +1168,6 @@ def __init__(self, rstudio_client: RStudioClient, server: CloudServer): def prepare_deploy( self, app_id: typing.Optional[int], - project_application_id: typing.Optional[str], app_name: str, bundle_size: int, bundle_hash: str, @@ -1187,6 +1184,7 @@ def prepare_deploy( ) if app_id is None: + project_application_id = os.getenv("LUCID_APPLICATION_ID") if project_application_id is not None: project_application = self._rstudio_client.get_application(project_application_id) self._server.handle_bad_response(project_application) diff --git a/rsconnect/main.py b/rsconnect/main.py index 5c11f4b8..d88ca090 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -220,20 +220,6 @@ def wrapper(*args, **kwargs): return wrapper -def cloud_content_args(func): - @click.option( - "--project-application-id", - "-P", - envvar="LUCID_APPLICATION_ID", - help="The application of the project to to be associated with when creating an output.", - ) - @functools.wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - - return wrapper - - @click.group(no_args_is_help=True) @click.option("--future", "-u", is_flag=True, hidden=True, help="Enables future functionality.") def cli(future): @@ -763,7 +749,6 @@ def deploy_notebook( ) @server_args @content_args -@cloud_content_args @rstudio_args @click.argument("file", type=click.Path(exists=True, dir_okay=True, file_okay=True)) @cli_exception_handler @@ -778,7 +763,6 @@ def deploy_manifest( secret: str, new: bool, app_id: str, - project_application_id: typing.Optional[str], title: str, verbose: bool, file: str, @@ -1019,7 +1003,6 @@ def generate_deploy_python(app_mode, alias, min_version): @server_args @content_args @rstudio_args - @cloud_content_args @click.option( "--entrypoint", "-e", @@ -1094,7 +1077,6 @@ def deploy_app( account: str = None, token: str = None, secret: str = None, - project_application_id: typing.Optional[str] = None, ): kwargs = locals() kwargs["entrypoint"] = entrypoint = validate_entry_point(entrypoint, directory) From 4b131b8dc2f5291030b72ffb0eb2ca199c9b8fa2 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Fri, 16 Sep 2022 14:43:03 -0500 Subject: [PATCH 17/24] don't fail on list if servers don't have insecure or ca_cert --- rsconnect/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index d88ca090..acf4c3b9 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -390,9 +390,9 @@ def list_servers(verbose): click.echo('Nickname: "%s"' % server["name"]) click.echo(" URL: %s" % server["url"]) click.echo(" API key is saved") - if server["insecure"]: + if server.get("insecure"): click.echo(" Insecure mode (TLS host/certificate validation disabled)") - if server["ca_cert"]: + if server.get("ca_cert"): click.echo(" Client TLS certificate data provided") click.echo() From 2116e4c000e0d82d93869f1e35d591bdcc9b8acd Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 26 Sep 2022 15:06:40 -0500 Subject: [PATCH 18/24] better error messaging for invalid secret --- rsconnect/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index ab0875c6..05ea2729 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1,6 +1,7 @@ """ RStudio Connect API client and utility functions """ +import binascii import os from os.path import abspath import time @@ -983,7 +984,10 @@ class RStudioClient(HTTPServer): def __init__(self, rstudio_server: RStudioServer, timeout: int = 30): self._token = rstudio_server.token - self._key = base64.b64decode(rstudio_server.secret) + try: + self._key = base64.b64decode(rstudio_server.secret) + except binascii.Error as e: + raise RSConnectException("Invalid secret.") from e self._server = rstudio_server super().__init__(rstudio_server.url, timeout=timeout) From 1cd0583afd435a055f2c2ad5a7027d76809698f7 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 26 Sep 2022 15:07:10 -0500 Subject: [PATCH 19/24] update help messages to be reflective of RStudio Cloud --- rsconnect/main.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index acf4c3b9..114df81d 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -224,18 +224,21 @@ def wrapper(*args, **kwargs): @click.option("--future", "-u", is_flag=True, hidden=True, help="Enables future functionality.") def cli(future): """ - This command line tool may be used to deploy Jupyter notebooks to RStudio - Connect. Support for deploying other content types is also provided. + This command line tool may be used to deploy various types of content to RStudio + Connect, RStudio Cloud, and shinyapps.io. The tool supports the notion of a simple nickname that represents the - information needed to interact with an RStudio Connect server instance. Use - the add, list and remove commands to manage these nicknames. + information needed to interact with a deployment target. Usethe add, list and + remove commands to manage these nicknames. The information about an instance of RStudio Connect includes its URL, the API key needed to authenticate against that instance, a flag that notes whether TLS certificate/host verification should be disabled and a path to a trusted CA certificate file to use for TLS. The last two items are only relevant if the URL specifies the "https" protocol. + + For RStudio Cloud and shinyapps.io, the information needed to connect includes + the account, auth token, auth secret, and server ('rstudio.cloud' or 'shinyapps.io'). """ global future_enabled future_enabled = future @@ -280,9 +283,9 @@ def _test_rstudio_creds(server: api.RStudioServer): # noinspection SpellCheckingInspection @cli.command( - short_help="Define a nickname for an RStudio Connect or shinyapps.io server and credential.", + short_help="Define a nickname for an RStudio Connect, RStudio Cloud, or shinyapps.io server and credential.", help=( - "Associate a simple nickname with the information needed to interact with an RStudio Connect server. " + "Associate a simple nickname with the information needed to interact with a deployment target. " "Specifying an existing nickname will cause its stored information to be replaced by what is given " "on the command line." ), @@ -292,7 +295,7 @@ def _test_rstudio_creds(server: api.RStudioServer): "--server", "-s", envvar="CONNECT_SERVER", - help="The URL for the RStudio Connect server to deploy to.", + help="The URL for the RStudio Connect server to deploy to, OR rstudio.cloud OR shinyapps.io.", ) @click.option( "--api-key", @@ -538,7 +541,7 @@ def info(file): click.echo("No saved deployment information was found for %s." % file) -@cli.group(no_args_is_help=True, help="Deploy content to RStudio Connect.") +@cli.group(no_args_is_help=True, help="Deploy content to RStudio Connect, RStudio Cloud, or shinyapps.io.") def deploy(): pass @@ -992,12 +995,12 @@ def generate_deploy_python(app_mode, alias, min_version): # noinspection SpellCheckingInspection @deploy.command( name=alias, - short_help="Deploy a {desc} to RStudio Connect [v{version}+].".format( + short_help="Deploy a {desc} to RStudio Connect, RStudio Cloud, orshinyapps.io [v{version}+].".format( desc=app_mode.desc(), version=min_version ), help=( - 'Deploy a {desc} module to RStudio Connect. The "directory" argument must refer to an ' - "existing directory that contains the application code." + "Deploy a {desc} module to RStudio Connect, RStudio Cloud, or shinyapps.io (if supported by the platform). " + 'The "directory" argument must refer to an existing directory that contains the application code.' ).format(desc=app_mode.desc()), ) @server_args From 86aad3ba136b8da40a36df60de12934514b9ef38 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 29 Sep 2022 15:10:18 -0500 Subject: [PATCH 20/24] tweak deploy help messages --- rsconnect/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 114df81d..3710c1d7 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -743,7 +743,7 @@ def deploy_notebook( # noinspection SpellCheckingInspection,DuplicatedCode @deploy.command( name="manifest", - short_help="Deploy content to RStudio Connect by manifest.", + short_help="Deploy content to RStudio Connect, RStudio Cloud, or shinyapps.io by manifest.", help=( "Deploy content to RStudio Connect using an existing manifest.json " 'file. The specified file must either be named "manifest.json" or ' @@ -995,7 +995,7 @@ def generate_deploy_python(app_mode, alias, min_version): # noinspection SpellCheckingInspection @deploy.command( name=alias, - short_help="Deploy a {desc} to RStudio Connect, RStudio Cloud, orshinyapps.io [v{version}+].".format( + short_help="Deploy a {desc} to RStudio Connect [v{version}+], RStudio Cloud, or shinyapps.io.".format( desc=app_mode.desc(), version=min_version ), help=( From 19509b59e913ba52544adb55e5f730795d2fa5ad Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 29 Sep 2022 15:39:36 -0500 Subject: [PATCH 21/24] handle deploy timeouts on rscloud/shinyapps.io with error --- rsconnect/api.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index 05ea2729..951ea143 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1079,19 +1079,23 @@ def wait_until_task_is_successful(self, task_id, timeout=180): while time.time() - start_time < timeout: task = self.get_task(task_id) self._server.handle_bad_response(task) + finished = task.json_data["finished"] status = task.json_data["status"] description = task.json_data["description"] error = task.json_data["error"] - if status == "success": + if finished: break - if status in {"failed", "error"}: - raise RSConnectException("Application deployment failed with error: {}".format(error)) - print(" {} - {}".format(status, description)) time.sleep(2) + if not finished: + raise RSConnectException("Application deployment timed out.") + + if status != 'success': + raise RSConnectException("Application deployment failed with error: {}".format(error)) + print("Task done: {}".format(description)) def get_applications_like_name(self, name): From 942e672357fbff0b399f477934c9860eab066f55 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Thu, 29 Sep 2022 16:15:29 -0500 Subject: [PATCH 22/24] deploy output to same space as project --- rsconnect/api.py | 9 ++- tests/test_main.py | 15 +++-- .../rstudio-responses/get-content.json | 63 +++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 tests/testdata/rstudio-responses/get-content.json diff --git a/rsconnect/api.py b/rsconnect/api.py index 951ea143..23be22bd 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1026,6 +1026,9 @@ def get_extra_headers(self, url, method, body): def get_application(self, application_id): return self.get("/v1/applications/{}".format(application_id)) + def get_content(self, content_id): + return self.get("/v1/content/{}".format(content_id)) + def create_application(self, account_id, application_name): application_data = { "account": account_id, @@ -1197,10 +1200,14 @@ def prepare_deploy( project_application = self._rstudio_client.get_application(project_application_id) self._server.handle_bad_response(project_application) project_id = project_application.json_data["content_id"] + project = self._rstudio_client.get_content(project_id) + self._server.handle_bad_response(project) + space_id = project.json_data["space_id"] else: project_id = None + space_id = None - output = self._rstudio_client.create_output(name=app_name, project_id=project_id) + output = self._rstudio_client.create_output(name=app_name, project_id=project_id, space_id=space_id) self._server.handle_bad_response(output) app_id = output.json_data["source_id"] diff --git a/tests/test_main.py b/tests/test_main.py index 8c30e149..b2934008 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -258,11 +258,6 @@ def post_deploy_callback(request, uri, response_headers): ids=["without associated project", "with associated project"], ) def test_deploy_manifest_cloud(self, project_application_id, project_id): - """ - {} hellooo - """.format( - project_id - ) original_api_key_value = os.environ.pop("CONNECT_API_KEY", None) original_server_value = os.environ.pop("CONNECT_SERVER", None) if project_application_id: @@ -298,11 +293,19 @@ def test_deploy_manifest_cloud(self, project_application_id, project_id): adding_headers={"Content-Type": "application/json"}, status=200, ) + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/content/555", + body=open("tests/testdata/rstudio-responses/get-content.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) def post_output_callback(request, uri, response_headers): + space_id = 917733 if project_application_id else None parsed_request = _load_json(request.body) try: - assert parsed_request == {"name": "myapp", "space": None, "project": project_id} + assert parsed_request == {"name": "myapp", "space": space_id, "project": project_id} except AssertionError as e: return _error_to_response(e) return [ diff --git a/tests/testdata/rstudio-responses/get-content.json b/tests/testdata/rstudio-responses/get-content.json new file mode 100644 index 00000000..0d5ee6a9 --- /dev/null +++ b/tests/testdata/rstudio-responses/get-content.json @@ -0,0 +1,63 @@ +{ + "id": 555, + "uuid": "594a15361cbf4183b67f6589725474a6", + "name": "pyshiny0", + "content_type": "project", + "visibility": "private", + "description": null, + "status": "running", + "state": "active", + "account_id": 82069, + "author_id": 47261, + "author": { + "id": 47261, + "first_name": "Matthew", + "last_name": "Lynch", + "display_name": "Matthew Lynch", + "organization": null, + "homepage": null, + "location": null, + "picture_url": "https://www.gravatar.com/avatar/e1bc03fb1393b9b1c3b3dc309cc0b4cf", + "grant": null, + "created_time": "2021-09-28T19:32:47", + "updated_time": "2022-09-28T18:31:01" + }, + "space_id": 917733, + "source_id": 1230843, + "url": "https://staging.rstudio.cloud/content/1205504", + "source": { + "id": 1230843, + "name": "21b10f198835410d8ffb340b0ae815b6-project-source", + "uuid": "5f2a944784f24c4ebf0ee9a4d1afa210", + "type": "ide", + "mode": null, + "scheduler": "kubernetes", + "status": "terminated", + "account_id": 82069, + "storage_initialized": true, + "deployment_id": 936660, + "next_deployment_id": null, + "prev_deployment_id": 928918, + "clone_parent_id": null, + "copy_parent_id": null, + "exportable": true, + "created_time": "2022-08-19T19:01:54", + "updated_time": "2022-09-01T18:33:06" + }, + "project_type": "ide", + "parent_id": null, + "parent": null, + "permissions": [ + "MOVE_OUTPUT", + "TRASH_OUTPUT", + "ARCHIVE_OUTPUT", + "VIEW_OUTPUT", + "RESTORE_TRASHED_OUTPUT", + "RESTORE_ARCHIVED_OUTPUT", + "MODIFY_OUTPUT", + "DELETE_OUTPUT", + "MANAGE_OUTPUT_ACCESS" + ], + "created_time": "2022-09-27T16:16:24", + "updated_time": "2022-09-29T15:57:35" +} From eb72990ebf5e09696961f891e47a87063853d0c6 Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Mon, 3 Oct 2022 13:31:51 -0500 Subject: [PATCH 23/24] open content url for cloud outputs --- rsconnect/api.py | 14 ++++++++++---- .../testdata/rstudio-responses/create-output.json | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index 23be22bd..121ba2c4 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1210,12 +1210,18 @@ def prepare_deploy( output = self._rstudio_client.create_output(name=app_name, project_id=project_id, space_id=space_id) self._server.handle_bad_response(output) app_id = output.json_data["source_id"] + application = self._rstudio_client.get_application(app_id) + self._server.handle_bad_response(application) + else: + application = self._rstudio_client.get_application(app_id) + self._server.handle_bad_response(application) + output = self._rstudio_client.get_content(application.json_data["content_id"]) + self._server.handle_bad_response(output) + - application = self._rstudio_client.get_application(app_id) - self._server.handle_bad_response(application) app_id_int = application.json_data["id"] - app_url = application.json_data["url"] - output_id = application.json_data["content_id"] + app_url = output.json_data["url"] + output_id = output.json_data["id"] bundle = self._rstudio_client.create_bundle(app_id_int, "application/x-tar", bundle_size, bundle_hash) self._server.handle_bad_response(bundle) diff --git a/tests/testdata/rstudio-responses/create-output.json b/tests/testdata/rstudio-responses/create-output.json index 982e0d73..b3951229 100644 --- a/tests/testdata/rstudio-responses/create-output.json +++ b/tests/testdata/rstudio-responses/create-output.json @@ -36,7 +36,7 @@ "visibility": "public", "status": "pending", "state": "active", - "`url': `http": "//staging.rstudio.cloud/content/123`", + "url": "http://staging.rstudio.cloud/content/123", "source": { "id": 8442, "name": "0a1006cc11bd4e6f8137d01440aead0a-output-source", From f3a93214121535cbc1efcb1345bdb3c3317f707d Mon Sep 17 00:00:00 2001 From: Matthew Lynch Date: Fri, 7 Oct 2022 09:01:55 -0500 Subject: [PATCH 24/24] fix test --- rsconnect/api.py | 3 +-- tests/test_main.py | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/rsconnect/api.py b/rsconnect/api.py index 69209345..1143193b 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1122,7 +1122,7 @@ def wait_until_task_is_successful(self, task_id, timeout=180): if not finished: raise RSConnectException("Application deployment timed out.") - if status != 'success': + if status != "success": raise RSConnectException("Application deployment failed with error: {}".format(error)) print("Task done: {}".format(description)) @@ -1244,7 +1244,6 @@ def prepare_deploy( output = self._rstudio_client.get_content(application.json_data["content_id"]) self._server.handle_bad_response(output) - app_id_int = application.json_data["id"] app_url = output.json_data["url"] output_id = output.json_data["id"] diff --git a/tests/test_main.py b/tests/test_main.py index b2934008..938c1f9e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -300,6 +300,13 @@ def test_deploy_manifest_cloud(self, project_application_id, project_id): adding_headers={"Content-Type": "application/json"}, status=200, ) + httpretty.register_uri( + httpretty.GET, + "https://api.rstudio.cloud/v1/content/1", + body=open("tests/testdata/rstudio-responses/create-output.json", "r").read(), + adding_headers={"Content-Type": "application/json"}, + status=200, + ) def post_output_callback(request, uri, response_headers): space_id = 917733 if project_application_id else None