diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e258346..6197a20e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - getdefaultlocale no longer work with newer versions of Python [#397](https://github.com/rstudio/rsconnect-python/issues/397) [#399](https://github.com/rstudio/rsconnect-python/issues/399). - extra files not being included in write-manifest [#416](https://github.com/rstudio/rsconnect-python/issues/416). +## Unreleased + +### Added +- Deploys for Posit Cloud and shinyapps.io now accept the `--visibility` flag. + ## [1.16.0] - 2023-03-27 ### Added diff --git a/rsconnect/api.py b/rsconnect/api.py index 7ab80527..b0f75040 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -676,6 +676,7 @@ def deploy_bundle( bundle: IO = None, env_vars=None, app_mode=None, + visibility=None, ): app_id = app_id or self.get("app_id") deployment_name = deployment_name or self.get("deployment_name") @@ -684,6 +685,7 @@ def deploy_bundle( bundle = bundle or self.get("bundle") env_vars = env_vars or self.get("env_vars") app_mode = app_mode or self.get("app_mode") + visibility = visibility or self.get("visibility") if isinstance(self.remote_server, RSConnectServer): result = self.client.deploy( @@ -709,6 +711,7 @@ def deploy_bundle( deployment_name, bundle_size, bundle_hash, + visibility, ) self.upload_rstudio_bundle(prepare_deploy_result, bundle_size, contents) shinyapps_service.do_deploy(prepare_deploy_result.bundle_id, prepare_deploy_result.app_id) @@ -1091,6 +1094,11 @@ def get_application(self, application_id): self._server.handle_bad_response(response) return response + def update_application_property(self, application_id: int, property: str, value: str): + response = self.put("/v1/applications/{}/properties/{}".format(application_id, property), body={"value": value}) + self._server.handle_bad_response(response) + return response + def get_content(self, content_id): response = self.get("/v1/content/{}".format(content_id)) self._server.handle_bad_response(response) @@ -1117,6 +1125,9 @@ def create_revision(self, content_id): self._server.handle_bad_response(response) return response + def update_output(self, output_id: int, output_data: dict): + return self.patch("/v1/outputs/{}".format(output_id), body=output_data) + def get_accounts(self): response = self.get("/v1/accounts/") self._server.handle_bad_response(response) @@ -1211,7 +1222,14 @@ def __init__(self, rstudio_client: PositClient, 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): + def prepare_deploy( + self, + app_id: typing.Optional[int], + app_name: str, + bundle_size: int, + bundle_hash: str, + visibility: typing.Optional[str], + ): accounts = self._rstudio_client.get_accounts() self._server.handle_bad_response(accounts) account = next(filter(lambda acct: acct["name"] == self._server.account_name, accounts["accounts"]), None) @@ -1223,9 +1241,23 @@ def prepare_deploy(self, app_id: typing.Optional[int], app_name: str, bundle_siz if app_id is None: application = self._rstudio_client.create_application(account["id"], app_name) + self._server.handle_bad_response(application) + if visibility is not None: + property_update = self._rstudio_client.update_application_property( + application["id"], "application.visibility", visibility + ) + self._server.handle_bad_response(property_update) + else: application = self._rstudio_client.get_application(app_id) - self._server.handle_bad_response(application) + self._server.handle_bad_response(application) + + if visibility is not None: + if visibility != application["deployment"]["properties"]["application.visibility"]: + self._rstudio_client.update_application_property( + application["id"], "application.visibility", visibility + ) + app_id_int = application["id"] app_url = application["url"] diff --git a/rsconnect/main.py b/rsconnect/main.py index f175f700..8425c4fd 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -169,6 +169,20 @@ def wrapper(*args, **kwargs): return wrapper +def shinyapps_deploy_args(func): + @click.option( + "--visibility", + "-V", + type=click.Choice(["public", "private"]), + help="The visibility of the resource being deployed. (shinyapps.io only; must be public (default) or private)", + ) + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + return wrapper + + def _passthrough(func): @functools.wraps(func) def wrapper(*args, **kwargs): @@ -961,6 +975,7 @@ def deploy_voila( @content_args @cloud_shinyapps_args @click.argument("file", type=click.Path(exists=True, dir_okay=True, file_okay=True)) +@shinyapps_deploy_args @cli_exception_handler def deploy_manifest( name: str, @@ -977,6 +992,7 @@ def deploy_manifest( verbose: bool, file: str, env_vars: typing.Dict[str, str], + visibility: typing.Optional[str], ): kwargs = locals() set_verbosity(verbose) @@ -1272,6 +1288,7 @@ def generate_deploy_python(app_mode, alias, min_version): nargs=-1, type=click.Path(exists=True, dir_okay=False, file_okay=True), ) + @shinyapps_deploy_args @cli_exception_handler def deploy_app( name: str, @@ -1290,6 +1307,7 @@ def deploy_app( verbose: bool, directory, extra_files, + visibility: typing.Optional[str], env_vars: typing.Dict[str, str], image: str, account: str = None, diff --git a/tests/test_main.py b/tests/test_main.py index 198bcd09..aff6581e 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -149,6 +149,25 @@ def post_application_callback(request, uri, response_headers): status=200, ) + def post_application_property_callback(request, uri, response_headers): + parsed_request = _load_json(request.body) + try: + assert parsed_request == {"value": "private"} + except AssertionError as e: + return _error_to_response(e) + return [ + 201, + {}, + b"", + ] + + httpretty.register_uri( + httpretty.PUT, + "https://api.shinyapps.io/v1/applications/8442/properties/application.visibility", + body=post_application_property_callback, + status=200, + ) + def post_bundle_callback(request, uri, response_headers): parsed_request = _load_json(request.body) del parsed_request["checksum"] @@ -246,6 +265,8 @@ def post_deploy_callback(request, uri, response_headers): "c29tZVNlY3JldAo=", "--title", "myApp", + "--visibility", + "private" ] try: result = runner.invoke(cli, args) @@ -428,6 +449,8 @@ def post_deploy_callback(request, uri, response_headers): "c29tZVNlY3JldAo=", "--title", "myApp", + "--visibility", + "public" ] try: result = runner.invoke(cli, args)