From fe219c07228071029a202782fb2b6bc583e86063 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Mar 2024 18:03:41 -0500 Subject: [PATCH 01/13] Remove redundant click dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 66254db4..1839b4e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ requires-python = ">=3.8" dependencies = [ "six>=1.14.0", - "click>=7.0.0", "pip>=10.0.0", "semver>=2.0.0,<3.0.0", "pyjwt>=2.4.0", From 0dd196afb3ffece94aeaefda0f418d136252b402 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Mar 2024 18:14:59 -0500 Subject: [PATCH 02/13] Clean up types --- rsconnect/actions.py | 16 +- rsconnect/actions_content.py | 13 +- rsconnect/api.py | 4 +- rsconnect/bundle.py | 32 +-- rsconnect/main.py | 539 ++++++++++++++++++----------------- 5 files changed, 313 insertions(+), 291 deletions(-) diff --git a/rsconnect/actions.py b/rsconnect/actions.py index 3839212e..9f7d3825 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -1,6 +1,7 @@ """ Public API for managing settings and deploying content. """ +from __future__ import annotations import contextlib import json @@ -11,7 +12,8 @@ import subprocess import sys import traceback -from typing import IO +import typing +from typing import IO, Optional from warnings import warn from os.path import abspath, basename, dirname, exists, isdir, join, relpath, splitext from .exception import RSConnectException @@ -46,10 +48,6 @@ import click from six.moves.urllib_parse import urlparse -try: - import typing -except ImportError: - typing = None line_width = 45 _module_pattern = re.compile(r"^[A-Za-z0-9_]+:[A-Za-z0-9_]+$") @@ -153,7 +151,7 @@ def inspect_environment( return MakeEnvironment(**json.loads(environment_json)) # type: ignore -def _verify_server(connect_server): +def _verify_server(connect_server: api.RSConnectServer): """ Test whether the server identified by the given full URL can be reached and is running Connect. @@ -188,7 +186,7 @@ def _to_server_check_list(url): return [item % url for item in items] -def test_server(connect_server): +def test_server(connect_server: api.RSConnectServer) -> tuple[api.RSConnectServer, Unknown]: """ Test whether the given server can be reached and is running Connect. The server may be provided with or without a scheme. If a scheme is omitted, the server will @@ -228,7 +226,7 @@ def test_rstudio_server(server: api.PositServer): raise RSConnectException("Failed to verify with {} ({}).".format(server.remote_name, exc)) -def test_api_key(connect_server): +def test_api_key(connect_server: api.RSConnectServer) -> str: """ Test that an API Key may be used to authenticate with the given Posit Connect server. If the API key verifies, we return the username of the associated user. @@ -424,7 +422,7 @@ def validate_entry_point(entry_point, directory): return entry_point -def which_quarto(quarto=None): +def which_quarto(quarto: Optional[str] = None) -> str: """ Identify a valid Quarto executable. When a Quarto location is not provided as input, an attempt is made to discover Quarto from the PATH and other diff --git a/rsconnect/actions_content.py b/rsconnect/actions_content.py index 9533352b..a5c2f56d 100644 --- a/rsconnect/actions_content.py +++ b/rsconnect/actions_content.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta import semver -from .api import RSConnectClient, emit_task_log +from .api import RSConnectClient, RSConnectServer, emit_task_log from .log import logger from .models import BuildStatus, ContentGuidWithBundle from .metadata import ContentBuildStore @@ -18,7 +18,7 @@ _content_build_store = None # type: ContentBuildStore -def init_content_build_store(connect_server): +def init_content_build_store(connect_server: RSConnectServer): global _content_build_store if not _content_build_store: logger.info("Initializing ContentBuildStore for %s" % connect_server.url) @@ -64,7 +64,12 @@ def build_add_content(connect_server, content_guids_with_bundle): _content_build_store.set_content_item_build_status(content["guid"], BuildStatus.NEEDS_BUILD) -def build_remove_content(connect_server, guid, all=False, purge=False): +def build_remove_content( + connect_server: RSConnectServer, + guid: str, + all: bool = False, + purge: bool = False, +) -> list[str]: """ :return: A list of guids of the content items that were removed """ @@ -73,7 +78,7 @@ def build_remove_content(connect_server, guid, all=False, purge=False): raise RSConnectException( "There is a build running on this server, " + "please wait for it to finish before removing content." ) - guids = [guid] + guids: list[str] = [guid] if all: guids = [c["guid"] for c in _content_build_store.get_content_items()] for guid in guids: diff --git a/rsconnect/api.py b/rsconnect/api.py index a8795be0..142bfa4d 100644 --- a/rsconnect/api.py +++ b/rsconnect/api.py @@ -1454,7 +1454,7 @@ def do_deploy(self, bundle_id, app_id): raise e -def verify_server(connect_server): +def verify_server(connect_server: RSConnectServer): """ Verify that the given server information represents a Connect instance that is reachable, active and appears to be actually running Posit Connect. If the @@ -1473,7 +1473,7 @@ def verify_server(connect_server): raise RSConnectException("There is an SSL/TLS configuration problem: %s" % ssl_error) -def verify_api_key(connect_server): +def verify_api_key(connect_server: RSConnectServer) -> str: """ Verify that an API Key may be used to authenticate with the given Posit Connect server. If the API key verifies, we return the username of the associated user. diff --git a/rsconnect/bundle.py b/rsconnect/bundle.py index d875ddb2..c57fd258 100644 --- a/rsconnect/bundle.py +++ b/rsconnect/bundle.py @@ -20,7 +20,7 @@ from mimetypes import guess_type from pathlib import Path from copy import deepcopy -from typing import Optional, Any +from typing import Optional, Any, Sequence import click from os.path import basename, dirname, exists, isdir, join, relpath, splitext, isfile, abspath @@ -844,12 +844,12 @@ def make_api_manifest( entry_point: str, app_mode: AppMode, environment: Environment, - extra_files: list[str], - excludes: list[str], + extra_files: Sequence[str], + excludes: Sequence[str], image: Optional[str] = None, env_management_py: Optional[bool] = None, env_management_r: Optional[bool] = None, -) -> typing.Tuple[typing.Dict[str, typing.Any], typing.List[str]]: +) -> tuple[dict[str, Any], list[str]]: """ Makes a manifest for an API. @@ -1406,7 +1406,7 @@ def validate_extra_files(directory: str, extra_files: typing.Sequence[str], use_ return result -def validate_manifest_file(file_or_directory): +def validate_manifest_file(file_or_directory: str) -> str: """ Validates that the name given represents either an existing manifest.json file or a directory that contains one. If not, an exception is raised. @@ -1674,12 +1674,12 @@ def write_notebook_manifest_json( entry_point_file: str, environment: Environment, app_mode: AppMode, - extra_files: typing.List[str], - hide_all_input: bool, - hide_tagged_input: bool, - image: str = None, - env_management_py: bool = None, - env_management_r: bool = None, + extra_files: Sequence[str], + hide_all_input: Optional[bool], + hide_tagged_input: Optional[bool], + image: Optional[str] = None, + env_management_py: Optional[bool] = None, + env_management_r: Optional[bool] = None, ) -> bool: """ Creates and writes a manifest.json file for the given entry point file. If @@ -1932,11 +1932,11 @@ def write_api_manifest_json( entry_point: str, environment: Environment, app_mode: AppMode, - extra_files: typing.List[str], - excludes: typing.List[str], - image: str = None, - env_management_py: bool = None, - env_management_r: bool = None, + extra_files: Sequence[str], + excludes: Sequence[str], + image: Optional[str] = None, + env_management_py: Optional[bool] = None, + env_management_r: Optional[bool] = None, ) -> bool: """ Creates and writes a manifest.json file for the given entry point file. If diff --git a/rsconnect/main.py b/rsconnect/main.py index e0be28a2..e6e2e510 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -10,7 +10,7 @@ import click from os.path import abspath, dirname, exists, isdir, join from functools import wraps -from typing import Optional, cast +from typing import Callable, ItemsView, Literal, Optional, ParamSpec, TypeVar, cast from rsconnect.certificates import read_certificate_file @@ -85,14 +85,17 @@ from .shiny_express import escape_to_var_name, is_express_app from .utils_package import fix_starlette_requirements +T = TypeVar("T") +P = ParamSpec("P") + server_store = ServerStore() future_enabled = False -def cli_exception_handler(func): +def cli_exception_handler(func: Callable[P, T]) -> Callable[P, T]: @wraps(func) - def wrapper(*args, **kwargs): - def failed(err): + def wrapper(*args: P.args, **kwargs: P.kwargs): + def failed(err: str): click.secho(str(err), fg="bright_red", err=False) sys.exit(1) @@ -115,7 +118,7 @@ def failed(err): def output_params( ctx: click.Context, - vars, + vars: ItemsView[str, object], ): if click.__version__ >= "8.0.0" and sys.version_info >= (3, 7): logger.log(VERBOSE, "Detected the following inputs:") @@ -130,7 +133,7 @@ def output_params( logger.log(VERBOSE, " %-18s%s (from %s)", (k + ":"), val, sourceName) -def server_args(func): +def server_args(func: Callable[P, T]) -> Callable[P, T]: @click.option("--name", "-n", help="The nickname of the Posit Connect server to deploy to.") @click.option( "--server", @@ -163,13 +166,13 @@ def server_args(func): ) @click.option("--verbose", "-v", count=True, help="Enable verbose output. Use -vv for very verbose (debug) output.") @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs): return func(*args, **kwargs) return wrapper -def cloud_shinyapps_args(func): +def cloud_shinyapps_args(func: Callable[P, T]) -> Callable[P, T]: @click.option( "--account", "-A", @@ -192,13 +195,13 @@ def cloud_shinyapps_args(func): (Also settable via SHINYAPPS_SECRET or RSCLOUD_SECRET environment variables.)", ) @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs): return func(*args, **kwargs) return wrapper -def shinyapps_deploy_args(func): +def shinyapps_deploy_args(func: Callable[P, T]) -> Callable[P, T]: @click.option( "--visibility", "-V", @@ -206,22 +209,22 @@ def shinyapps_deploy_args(func): help="The visibility of the resource being deployed. (shinyapps.io only; must be public (default) or private)", ) @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs): return func(*args, **kwargs) return wrapper -def _passthrough(func): +def _passthrough(func: Callable[P, T]) -> Callable[P, T]: @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs): return func(*args, **kwargs) return wrapper -def validate_env_vars(ctx, param, all_values): - vars = {} +def validate_env_vars(ctx: click.Context, param: click.Parameter, all_values: tuple[str, ...]) -> dict[str, str]: + vars: dict[str, str] = {} for s in all_values: if not isinstance(s, str): @@ -240,7 +243,7 @@ def validate_env_vars(ctx, param, all_values): return vars -def content_args(func): +def content_args(func: Callable[P, T]) -> Callable[P, T]: @click.option( "--new", "-N", @@ -272,7 +275,7 @@ def content_args(func): help="Don't access the deployed content to verify that it started correctly.", ) @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs): return func(*args, **kwargs) return wrapper @@ -285,7 +288,7 @@ def wrapper(*args, **kwargs): # otherwise returns None. This is so that we can pass the # non-negative (env_management_r, env_management_py) args to our API functions, # which is more consistent when writing these values to the manifest. -def env_management_callback(ctx, param, value) -> typing.Optional[bool]: +def env_management_callback(ctx: click.Context, param: click.Parameter, value: Optional[bool]) -> bool | None: # eval the shorthand flag if it was provided disable_env_management = ctx.params.get("disable_env_management") if disable_env_management is not None: @@ -297,7 +300,7 @@ def env_management_callback(ctx, param, value) -> typing.Optional[bool]: return value -def runtime_environment_args(func): +def runtime_environment_args(func: Callable[P, T]) -> Callable[P, T]: @click.option( "--image", "-I", @@ -332,7 +335,7 @@ def runtime_environment_args(func): callback=env_management_callback, ) @functools.wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: P.args, **kwargs: P.kwargs): return func(*args, **kwargs) return wrapper @@ -340,7 +343,7 @@ def wrapper(*args, **kwargs): @click.group(no_args_is_help=True) @click.option("--future", "-u", is_flag=True, hidden=True, help="Enables future functionality.") -def cli(future): +def cli(future: bool): """ This command line tool may be used to deploy various types of content to Posit Connect, Posit Cloud, and shinyapps.io. @@ -368,7 +371,7 @@ def version(): click.echo(VERSION) -def _test_server_and_api(server, api_key, insecure, ca_cert): +def _test_server_and_api(server: str, api_key: str, insecure: bool, ca_cert: str | None): """ Test the specified server information to make sure it works. If so, a ConnectServer object is returned with the potentially expanded URL. @@ -437,12 +440,12 @@ def _test_rstudio_creds(server: api.PositServer): @click.option("--verbose", "-v", count=True, help="Enable verbose output. Use -vv for very verbose (debug) output.") @cli_exception_handler def bootstrap( - server, - insecure, - cacert, - jwt_keypath, - raw, - verbose, + server: str, + insecure: bool, + cacert: Optional[str], + jwt_keypath: Optional[str], + raw: bool, + verbose: int, ): set_verbosity(verbose) if not server.startswith("http"): @@ -523,7 +526,18 @@ def bootstrap( @click.option("--verbose", "-v", count=True, help="Enable verbose output. Use -vv for very verbose (debug) output.") @cloud_shinyapps_args @click.pass_context -def add(ctx, name, server, api_key, insecure, cacert, account, token, secret, verbose): +def add( + ctx: click.Context, + name: str, + server: Optional[str], + api_key: Optional[str], + insecure: bool, + cacert: Optional[str], + account: Optional[str], + token: Optional[str], + secret: Optional[str], + verbose: int, +): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -583,10 +597,10 @@ def add(ctx, name, server, api_key, insecure, cacert, account, token, secret, ve help="Show the stored information about each known server nickname.", ) @click.option("--verbose", "-v", count=True, help="Enable verbose output. Use -vv for very verbose (debug) output.") -def list_servers(verbose): +def list_servers(verbose: int): set_verbosity(verbose) with cli_feedback(""): - servers = server_store.get_all_servers() + servers: dict[str, RSConnectServer] = server_store.get_all_servers() click.echo("Server information from %s" % server_store.get_path()) @@ -622,11 +636,11 @@ def list_servers(verbose): @click.pass_context def details( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], verbose: int, ): set_verbosity(verbose) @@ -669,7 +683,12 @@ def details( @click.option("--server", "-s", help="The URL of the Posit Connect server to remove.") @click.option("--verbose", "-v", count=True, help="Enable verbose output. Use -vv for very verbose (debug) output.") @click.pass_context -def remove(ctx, name, server, verbose): +def remove( + ctx: click.Context, + name: Optional[str], + server: Optional[str], + verbose: int, +): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -697,7 +716,7 @@ def remove(ctx, name, server, verbose): click.echo(message) -def _get_names_to_check(file_or_directory): +def _get_names_to_check(file_or_directory: str) -> list[str]: """ A function to determine a set files to look for in getting information about a deployment. @@ -724,7 +743,7 @@ def _get_names_to_check(file_or_directory): no_args_is_help=True, ) @click.argument("file", type=click.Path(exists=True, dir_okay=True, file_okay=True)) -def info(file): +def info(file: str): with cli_feedback(""): for file_name in _get_names_to_check(file): app_store = AppStore(file_name) @@ -762,7 +781,7 @@ def deploy(): pass -def _warn_on_ignored_manifest(directory): +def _warn_on_ignored_manifest(directory: str): """ Checks for the existence of a file called manifest.json in the given directory. If it's there, a warning noting that it will be ignored will be printed. @@ -776,7 +795,7 @@ def _warn_on_ignored_manifest(directory): ) -def _warn_if_no_requirements_file(directory): +def _warn_if_no_requirements_file(directory: str): """ Checks for the existence of a file called requirements.txt in the given directory. If it's not there, a warning will be printed. @@ -791,7 +810,7 @@ def _warn_if_no_requirements_file(directory): ) -def _warn_if_environment_directory(directory): +def _warn_if_environment_directory(directory: str): """ Issue a warning if the deployment directory is itself a virtualenv (yikes!). @@ -805,7 +824,7 @@ def _warn_if_environment_directory(directory): ) -def _warn_on_ignored_requirements(directory, requirements_file_name): +def _warn_on_ignored_requirements(directory: str, requirements_file_name: str): """ Checks for the existence of a file called manifest.json in the given directory. If it's there, a warning noting that it will be ignored will be printed. @@ -873,27 +892,27 @@ def _warn_on_ignored_requirements(directory, requirements_file_name): @click.pass_context def deploy_notebook( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], static: bool, new: bool, - app_id: str, - title: str, - python, - force_generate, + app_id: Optional[str], + title: Optional[str], + python: Optional[str], + force_generate: bool, verbose: int, file: str, - extra_files, + extra_files: tuple[str, ...], hide_all_input: bool, hide_tagged_input: bool, - env_vars: typing.Dict[str, str], - image: str, - disable_env_management: bool, - env_management_py: bool, - env_management_r: bool, + env_vars: dict[str, str], + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], no_verify: bool = False, ): kwargs = locals() @@ -998,29 +1017,29 @@ def deploy_notebook( @click.pass_context def deploy_voila( ctx: click.Context, - path: str = None, - entrypoint: str = None, - python=None, - force_generate=False, - extra_files=None, - exclude=None, - image: str = "", - disable_env_management: bool = None, - env_management_py: bool = None, - env_management_r: bool = None, - title: str = None, - env_vars: typing.Dict[str, str] = None, - verbose: int = 0, - new: bool = False, - app_id: str = None, - name: str = None, - server: str = None, - api_key: str = None, - insecure: bool = False, - cacert: str = None, - connect_server: api.RSConnectServer = None, - multi_notebook: bool = False, - no_verify: bool = False, + path: str, + entrypoint: Optional[str], + python: Optional[str], + force_generate: bool, + extra_files: tuple[str, ...], + exclude: tuple[str, ...], + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], + title: Optional[str], + env_vars: dict[str, str], + verbose: int, + new: bool, + app_id: Optional[str], + name: Optional[str], + server: Optional[str], + api_key: Optional[str], + insecure: bool, + cacert: Optional[str], + multi_notebook: bool, + no_verify: bool, + connect_server: Optional[api.RSConnectServer] = None, ): kwargs = locals() set_verbosity(verbose) @@ -1069,22 +1088,22 @@ def deploy_voila( @click.pass_context def deploy_manifest( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, - account: str, - token: str, - secret: str, + cacert: Optional[str], + account: Optional[str], + token: Optional[str], + secret: Optional[str], new: bool, - app_id: str, - title: str, + app_id: Optional[str], + title: Optional[str], verbose: int, file: str, - env_vars: typing.Dict[str, str], - visibility: typing.Optional[str], - no_verify: bool = False, + env_vars: dict[str, str], + visibility: Optional[str], + no_verify: bool, ): kwargs = locals() set_verbosity(verbose) @@ -1167,27 +1186,27 @@ def deploy_manifest( @click.pass_context def deploy_quarto( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], new: bool, - app_id: str, - title: str, - exclude, - quarto, - python, + app_id: Optional[str], + title: Optional[str], + exclude: tuple[str, ...], + quarto: Optional[str], + python: Optional[str], force_generate: bool, verbose: int, - file_or_directory, - extra_files, - env_vars: typing.Dict[str, str], - image: str, + file_or_directory: Optional[str], + extra_files: tuple[str, ...], + env_vars: dict[str, str], + image: Optional[str], disable_env_management: bool, env_management_py: bool, env_management_r: bool, - no_verify: bool = False, + no_verify: bool, ): kwargs = locals() set_verbosity(verbose) @@ -1278,25 +1297,25 @@ def deploy_quarto( @click.pass_context def deploy_html( ctx: click.Context, - connect_server: api.RSConnectServer = None, - path: str = None, - entrypoint: str = None, - extra_files=None, - exclude=None, - title: str = None, - env_vars: typing.Dict[str, str] = None, - verbose: int = 0, - new: bool = False, - app_id: str = None, - name: str = None, - server: str = None, - api_key: str = None, - insecure: bool = False, - cacert: str = None, - account: str = None, - token: str = None, - secret: str = None, - no_verify: bool = False, + path: str, + entrypoint: Optional[str], + extra_files: tuple[str, ...], + exclude: tuple[str, ...], + title: Optional[str], + env_vars: dict[str, str], + verbose: int, + new: bool, + app_id: Optional[str], + name: Optional[str], + server: Optional[str], + api_key: Optional[str], + insecure: bool, + cacert: Optional[str], + account: Optional[str], + token: Optional[str], + secret: Optional[str], + no_verify: bool, + connect_server: Optional[api.RSConnectServer] = None, ): kwargs = locals() set_verbosity(verbose) @@ -1391,16 +1410,16 @@ def generate_deploy_python(app_mode: AppMode, alias: str, min_version: str, desc @click.pass_context def deploy_app( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], entrypoint: Optional[str], - exclude: Optional[list[str]], + exclude: tuple[str, ...], new: bool, - app_id: str, - title: str, + app_id: Optional[str], + title: Optional[str], python: Optional[str], force_generate: bool, verbose: int, @@ -1408,14 +1427,14 @@ def deploy_app( extra_files: tuple[str, ...], visibility: Optional[str], env_vars: dict[str, str], - image: str, - disable_env_management: bool, - env_management_py: bool, - env_management_r: bool, - account: Optional[str] = None, - token: Optional[str] = None, - secret: Optional[str] = None, - no_verify: bool = False, + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], + account: Optional[str], + token: Optional[str], + secret: Optional[str], + no_verify: bool, ): set_verbosity(verbose) entrypoint = validate_entry_point(entrypoint, directory) @@ -1563,19 +1582,19 @@ def write_manifest(): @runtime_environment_args @click.pass_context def write_manifest_notebook( - ctx, - overwrite, - python, - force_generate, - verbose, - file, - extra_files, - image, - disable_env_management, - env_management_py, - env_management_r, - hide_all_input=None, - hide_tagged_input=None, + ctx: click.Context, + overwrite: bool, + python: Optional[str], + force_generate: bool, + verbose: int, + file: str, + extra_files: tuple[str, ...], + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], + hide_all_input: Optional[bool] = None, + hide_tagged_input: Optional[bool] = None, ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -1666,18 +1685,18 @@ def write_manifest_notebook( def write_manifest_voila( ctx: click.Context, path: str, - entrypoint: str, - overwrite, - python, - force_generate, - verbose, - extra_files, - exclude, - image, - disable_env_management, - env_management_py, - env_management_r, - multi_notebook, + entrypoint: Optional[str], + overwrite: bool, + python: Optional[str], + force_generate: bool, + verbose: int, + extra_files: tuple[str, ...], + exclude: tuple[str, ...], + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], + multi_notebook: bool, ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -1771,19 +1790,19 @@ def write_manifest_voila( @runtime_environment_args @click.pass_context def write_manifest_quarto( - ctx, - overwrite, - exclude, - quarto, - python, - force_generate, - verbose, - file_or_directory, - extra_files, - image, - disable_env_management, - env_management_py, - env_management_r, + ctx: click.Context, + overwrite: bool, + exclude: tuple[str, ...], + quarto: Optional[str], + python: Optional[str], + force_generate: bool, + verbose: int, + file_or_directory: str, + extra_files: tuple[str, ...], + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -1833,7 +1852,7 @@ def write_manifest_quarto( ) -def generate_write_manifest_python(app_mode, alias, desc: Optional[str] = None): +def generate_write_manifest_python(app_mode: AppMode, alias, desc: Optional[str] = None): if desc is None: desc = app_mode.desc() @@ -1888,19 +1907,19 @@ def generate_write_manifest_python(app_mode, alias, desc: Optional[str] = None): @runtime_environment_args @click.pass_context def manifest_writer( - ctx, - overwrite, - entrypoint, - exclude, - python, - force_generate, - verbose, - directory, - extra_files, - image, - disable_env_management, - env_management_py, - env_management_r, + ctx: click.Context, + overwrite: bool, + entrypoint: Optional[str], + exclude: tuple[str, ...], + python: Optional[str], + force_generate: bool, + verbose: int, + directory: str, + extra_files: tuple[str, ...], + image: Optional[str], + disable_env_management: Optional[bool], + env_management_py: Optional[bool], + env_management_r: Optional[bool], ): _write_framework_manifest( ctx, @@ -1932,19 +1951,19 @@ def manifest_writer( # noinspection SpellCheckingInspection def _write_framework_manifest( - ctx, - overwrite, - entrypoint, - exclude, - python, - force_generate, - verbose, - directory, - extra_files, - app_mode, - image, - env_management_py, - env_management_r, + ctx: click.Context, + overwrite: bool, + entrypoint: Optional[str], + exclude: tuple[str, ...], + python: Optional[str], + force_generate: bool, + verbose: int, + directory: str, + extra_files: tuple[str, ...], + app_mode: AppMode, + image: Optional[str], + env_management_py: Optional[bool], + env_management_r: Optional[bool], ): """ A common function for writing manifests for APIs as well as Dash, Streamlit, and Bokeh apps. @@ -2002,7 +2021,7 @@ def _write_framework_manifest( write_environment_file(environment, directory) -def _validate_build_rm_args(guid, all, purge): +def _validate_build_rm_args(guid: str, all: bool, purge: bool): if guid and all: raise RSConnectException("You must specify only one of -g/--guid or --all, not both.") if not guid and not all: @@ -2060,19 +2079,19 @@ def content(): @click.pass_context def content_search( ctx: click.Context, - name, - server, - api_key, - insecure, - cacert, - published, - unpublished, - content_type, - r_version, - py_version, - title_contains, - order_by, - verbose, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], + insecure: bool, + cacert: Optional[str], + published: bool, + unpublished: bool, + content_type: tuple[str, ...], + r_version: Optional[str], + py_version: Optional[str], + title_contains: Optional[str], + order_by: Optional[Literal["created", "last_deployed"]], + verbose: int, ): set_verbosity(verbose) output_params(ctx, locals().items()) @@ -2103,11 +2122,11 @@ def content_search( @click.pass_context def content_describe( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], guid: str, verbose: int, ): @@ -2148,11 +2167,11 @@ def content_describe( @click.pass_context def content_bundle_download( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], guid: str, output: str, overwrite: bool, @@ -2192,12 +2211,12 @@ def build(): @click.pass_context def add_content_build( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, - guid: str, + cacert: Optional[str], + guid: tuple[str, ...], verbose: int, ): set_verbosity(verbose) @@ -2240,12 +2259,12 @@ def add_content_build( @click.pass_context def remove_content_build( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, - guid: str, + cacert: Optional[str], + guid: Optional[str], all: bool, purge: bool, verbose: int, @@ -2285,12 +2304,12 @@ def remove_content_build( @click.pass_context def list_content_build( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, - status: str, + cacert: Optional[str], + status: Optional[str], guid: str, verbose: int, ): @@ -2317,11 +2336,11 @@ def list_content_build( @click.pass_context def get_build_history( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], guid: str, verbose: int, ): @@ -2365,13 +2384,13 @@ def get_build_history( @click.pass_context def get_build_logs( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], guid: str, - task_id: str, + task_id: Optional[str], format: str, verbose: int, ): @@ -2425,11 +2444,11 @@ def get_build_logs( @click.pass_context def start_content_build( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], parallelism: int, aborted: bool, error: bool, @@ -2504,14 +2523,14 @@ def system_caches_list(name, server, api_key, insecure, cacert, verbose): @click.pass_context def system_caches_delete( ctx: click.Context, - name: str, - server: str, - api_key: str, + name: Optional[str], + server: Optional[str], + api_key: Optional[str], insecure: bool, - cacert: str, + cacert: Optional[str], verbose: int, - language: str, - version: str, + language: Optional[str], + version: Optional[str], image_name: str, dry_run: bool, ): From d4602e0602cc47a1300d5f567e722dad29a78c45 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Mar 2024 18:46:38 -0500 Subject: [PATCH 03/13] Use Shiny Express entrypoint only for Shiny for Python apps --- rsconnect/main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index e6e2e510..3932f2a8 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1445,8 +1445,9 @@ def deploy_app( python, ) - if is_express_app(entrypoint + ".py", directory): - entrypoint = "shiny.express.app:" + escape_to_var_name(entrypoint + ".py") + if app_mode == AppModes.PYTHON_SHINY: + if is_express_app(entrypoint + ".py", directory): + entrypoint = "shiny.express.app:" + escape_to_var_name(entrypoint + ".py") extra_args = dict( directory=directory, From e300bbce24c2e944c0101169772f45470126d4eb Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Mar 2024 18:47:10 -0500 Subject: [PATCH 04/13] Add support for Shiny Express for manifests --- rsconnect/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rsconnect/main.py b/rsconnect/main.py index 3932f2a8..08d1258f 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -1999,6 +1999,11 @@ def _write_framework_manifest( with cli_feedback("Inspecting Python environment"): _, environment = get_python_env_info(directory, python, force_generate) + if app_mode == AppModes.PYTHON_SHINY: + with cli_feedback("Inspecting Shiny for Python app"): + if is_express_app(entrypoint + ".py", directory): + entrypoint = "shiny.express.app:" + escape_to_var_name(entrypoint + ".py") + with cli_feedback("Creating manifest.json"): environment_file_exists = write_api_manifest_json( directory, From 14fd553537dc46512421abcdaed5a02906c2aba4 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Mar 2024 19:08:02 -0500 Subject: [PATCH 05/13] Remove unused import --- rsconnect/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 08d1258f..3ecc6535 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -5,7 +5,6 @@ import os import sys import traceback -import typing import textwrap import click from os.path import abspath, dirname, exists, isdir, join From da1b1499e233cb096fd4d1fe6ea0ed4d53536be9 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 10:38:37 -0500 Subject: [PATCH 06/13] Fix type --- rsconnect/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsconnect/actions.py b/rsconnect/actions.py index 9f7d3825..7ca31fe4 100644 --- a/rsconnect/actions.py +++ b/rsconnect/actions.py @@ -186,7 +186,7 @@ def _to_server_check_list(url): return [item % url for item in items] -def test_server(connect_server: api.RSConnectServer) -> tuple[api.RSConnectServer, Unknown]: +def test_server(connect_server: api.RSConnectServer) -> tuple[api.RSConnectServer, object]: """ Test whether the given server can be reached and is running Connect. The server may be provided with or without a scheme. If a scheme is omitted, the server will From 14a1439cd34a08f4885938e812c24946a5d077fd Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 11:08:48 -0500 Subject: [PATCH 07/13] Typing fixes --- rsconnect/bundle.py | 16 ++++++++-------- rsconnect/main.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rsconnect/bundle.py b/rsconnect/bundle.py index c57fd258..4d291bb2 100644 --- a/rsconnect/bundle.py +++ b/rsconnect/bundle.py @@ -1237,8 +1237,8 @@ def make_api_bundle( def _create_quarto_file_list( directory: str, - extra_files: typing.List[str], - excludes: typing.List[str], + extra_files: Sequence[str], + excludes: Sequence[str], ) -> typing.List[str]: """ Builds a full list of files under the given directory that should be included @@ -1269,8 +1269,8 @@ def make_quarto_manifest( quarto_inspection: typing.Dict[str, typing.Any], app_mode: AppMode, environment: Environment, - extra_files: typing.List[str], - excludes: typing.List[str], + extra_files: Sequence[str], + excludes: Sequence[str], image: str = None, env_management_py: bool = None, env_management_r: bool = None, @@ -1840,8 +1840,8 @@ def write_voila_manifest_json( entrypoint: str, environment: Environment, app_mode: AppMode = AppModes.JUPYTER_VOILA, - extra_files: typing.List[str] = None, - excludes: typing.List[str] = None, + extra_files: Sequence[str] = None, + excludes: Sequence[str] = None, force_generate: bool = True, image: str = None, env_management_py: bool = None, @@ -2015,8 +2015,8 @@ def write_quarto_manifest_json( inspect: typing.Any, app_mode: AppMode, environment: Environment, - extra_files: typing.List[str], - excludes: typing.List[str], + extra_files: Sequence[str], + excludes: Sequence[str], image: str = None, env_management_py: bool = None, env_management_r: bool = None, diff --git a/rsconnect/main.py b/rsconnect/main.py index 3ecc6535..9e098b8a 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -9,7 +9,7 @@ import click from os.path import abspath, dirname, exists, isdir, join from functools import wraps -from typing import Callable, ItemsView, Literal, Optional, ParamSpec, TypeVar, cast +from typing import Callable, ItemsView, Literal, Optional, ParamSpec, Sequence, TypeVar, cast from rsconnect.certificates import read_certificate_file @@ -599,7 +599,7 @@ def add( def list_servers(verbose: int): set_verbosity(verbose) with cli_feedback(""): - servers: dict[str, RSConnectServer] = server_store.get_all_servers() + servers = server_store.get_all_servers() click.echo("Server information from %s" % server_store.get_path()) @@ -904,7 +904,7 @@ def deploy_notebook( force_generate: bool, verbose: int, file: str, - extra_files: tuple[str, ...], + extra_files: Sequence[str], hide_all_input: bool, hide_tagged_input: bool, env_vars: dict[str, str], @@ -1199,7 +1199,7 @@ def deploy_quarto( force_generate: bool, verbose: int, file_or_directory: Optional[str], - extra_files: tuple[str, ...], + extra_files: Sequence[str], env_vars: dict[str, str], image: Optional[str], disable_env_management: bool, From f0ebbfbdc341cec344ff6bc8eee488578c48b5af Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 11:09:13 -0500 Subject: [PATCH 08/13] Fix var name for mypy --- rsconnect/main.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 9e098b8a..da7f8b92 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -554,6 +554,7 @@ def add( old_server = server_store.get_by_name(name) if token: + real_server: api.CloudServer | api.ShinyappsServer if server and ("rstudio.cloud" in server or "posit.cloud" in server): real_server = api.CloudServer(server, account, token, secret) else: @@ -574,20 +575,21 @@ def add( 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) + real_server_rsc: RSConnectServer + real_server_rsc, _ = _test_server_and_api(server, api_key, insecure, cacert) server_store.set( name, - real_server.url, - real_server.api_key, - real_server.insecure, - real_server.ca_data, + real_server_rsc.url, + real_server_rsc.api_key, + real_server_rsc.insecure, + real_server_rsc.ca_data, ) if old_server: - click.echo('Updated Connect server "%s" with URL %s' % (name, real_server.url)) + click.echo('Updated Connect server "%s" with URL %s' % (name, real_server_rsc.url)) else: - click.echo('Added Connect server "%s" with URL %s' % (name, real_server.url)) + click.echo('Added Connect server "%s" with URL %s' % (name, real_server_rsc.url)) @cli.command( From 2046305d787dc398e3c663f2e50b3e762e095781 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 11:22:22 -0500 Subject: [PATCH 09/13] Fix incorrect argument --- rsconnect/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index da7f8b92..f573b011 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -105,7 +105,7 @@ def failed(err: str): except EnvironmentException as exc: failed("Error: " + str(exc)) except Exception as exc: - if click.get_current_context("verbose"): + if click.get_current_context(True): traceback.print_exc() failed("Internal error: " + str(exc)) finally: From 96e243a1d6af58f66626d5ef9021523a6e4ca5f2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 11:22:36 -0500 Subject: [PATCH 10/13] Import Paramspec based on Python version --- rsconnect/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index f573b011..8e2381f0 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -9,7 +9,13 @@ import click from os.path import abspath, dirname, exists, isdir, join from functools import wraps -from typing import Callable, ItemsView, Literal, Optional, ParamSpec, Sequence, TypeVar, cast +from typing import Callable, ItemsView, Literal, Optional, Sequence, TypeVar, cast + +if sys.version_info >= (3, 10): + from typing import ParamSpec +else: + from typing_extensions import ParamSpec + from rsconnect.certificates import read_certificate_file From 96de532b60446d412eebcf0c34ff0cb3573b9c0d Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 11:25:28 -0500 Subject: [PATCH 11/13] Import from __future__ --- rsconnect/actions_content.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rsconnect/actions_content.py b/rsconnect/actions_content.py index a5c2f56d..edd8f880 100644 --- a/rsconnect/actions_content.py +++ b/rsconnect/actions_content.py @@ -2,6 +2,8 @@ Public API for administering content. """ +from __future__ import annotations + import json import time import traceback From 33a5c79a1239e745098100fcc3abfece7ecf7cc7 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 14:24:13 -0500 Subject: [PATCH 12/13] Typing cleanup --- rsconnect/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 8e2381f0..6ce7e173 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -560,7 +560,7 @@ def add( old_server = server_store.get_by_name(name) if token: - real_server: api.CloudServer | api.ShinyappsServer + real_server: api.PositServer # This annotation seems to be necessary for mypy if server and ("rstudio.cloud" in server or "posit.cloud" in server): real_server = api.CloudServer(server, account, token, secret) else: @@ -581,7 +581,6 @@ def add( 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_rsc: RSConnectServer real_server_rsc, _ = _test_server_and_api(server, api_key, insecure, cacert) server_store.set( From 6267897e0cd5882fedbe4199fc6686b71bb40589 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Mar 2024 15:38:52 -0500 Subject: [PATCH 13/13] Drop condition for printing traceback --- rsconnect/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rsconnect/main.py b/rsconnect/main.py index 6ce7e173..454f94f3 100644 --- a/rsconnect/main.py +++ b/rsconnect/main.py @@ -111,8 +111,7 @@ def failed(err: str): except EnvironmentException as exc: failed("Error: " + str(exc)) except Exception as exc: - if click.get_current_context(True): - traceback.print_exc() + traceback.print_exc() failed("Internal error: " + str(exc)) finally: logger.set_in_feedback(False)