From 1305b9b571394db21d826e50c655fb27640d3ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Fri, 8 Apr 2022 17:49:57 +0300 Subject: [PATCH 01/10] Add features to google-play tracks interface --- src/codemagic/google_play/__init__.py | 3 - src/codemagic/google_play/api_client.py | 198 +++++++++++------- src/codemagic/google_play/api_error.py | 61 +++++- src/codemagic/google_play/resource_printer.py | 33 --- .../google_play/resources/__init__.py | 2 +- src/codemagic/google_play/resources/enums.py | 7 - src/codemagic/google_play/resources/track.py | 10 +- src/codemagic/tools/google_play.py | 184 ++++++++++------ tests/conftest.py | 7 +- tests/google_play/test_api_client.py | 8 + tests/tools/test_google_play.py | 6 +- 11 files changed, 316 insertions(+), 203 deletions(-) delete mode 100644 src/codemagic/google_play/resource_printer.py diff --git a/src/codemagic/google_play/__init__.py b/src/codemagic/google_play/__init__.py index b21e2b4d..4861204d 100644 --- a/src/codemagic/google_play/__init__.py +++ b/src/codemagic/google_play/__init__.py @@ -1,4 +1 @@ from .api_error import GooglePlayDeveloperAPIClientError -from .api_error import VersionCodeFromTrackError -from .resource_printer import ResourcePrinter -from .resources import TrackName diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py index 473cff53..ba9be6ee 100644 --- a/src/codemagic/google_play/api_client.py +++ b/src/codemagic/google_play/api_client.py @@ -1,19 +1,29 @@ +import contextlib import json +from functools import lru_cache +from typing import AnyStr +from typing import List from typing import Optional +from typing import Union import httplib2 from googleapiclient import discovery from googleapiclient import errors from oauth2client.service_account import ServiceAccountCredentials +from codemagic.utilities import log + from .api_error import AuthorizationError -from .api_error import CredentialsError from .api_error import EditError -from .api_error import VersionCodeFromTrackError -from .resource_printer import ResourcePrinter +from .api_error import GetResourceError +from .api_error import GooglePlayDeveloperAPIClientError +from .api_error import ListResourcesError from .resources import Edit from .resources import Track -from .resources import TrackName + +CredentialsJson = Union[AnyStr, dict] +EditId = Union[str, Edit] +MaybeEditId = Optional[EditId] class GooglePlayDeveloperAPIClient: @@ -21,78 +31,124 @@ class GooglePlayDeveloperAPIClient: SCOPE_URI = f'https://www.googleapis.com/auth/{SCOPE}' API_VERSION = 'v3' - _service_instance: Optional[discovery.Resource] = None - - def __init__(self, - credentials: str, - package_name: str, - resource_printer: ResourcePrinter): + def __init__(self, service_account_json_keyfile: CredentialsJson): """ - :param credentials: Your Gloud Service account credentials with JSON key type - :param package_name: package name of the app in Google Play Console (Ex: com.google.example) - :param resource_printer: printer initialized in google-play tool + :param service_account_json_keyfile: Your Gcloud Service account credentials with JSON key type """ - self._credentials = credentials - self.package_name = package_name - self.resource_printer = resource_printer + self._service_account_json_keyfile = service_account_json_keyfile + self._logger = log.get_logger(self.__class__) - def get_edit_resource(self): - return self.service.edits() # type: ignore + @classmethod + def _edit_id(cls, edit: EditId) -> str: + if isinstance(edit, Edit): + return edit.id + return edit + + def _get_json_keyfile_dict(self) -> dict: + if isinstance(self._service_account_json_keyfile, dict): + return self._service_account_json_keyfile + try: + return json.loads(self._service_account_json_keyfile) + except ValueError as ve: + message = 'Unable to parse service account credentials, must be a valid json' + raise GooglePlayDeveloperAPIClientError(message) from ve + + @lru_cache(1) + def _get_android_publisher_service(self): + json_keyfile = self._get_json_keyfile_dict() + http = httplib2.Http() + try: + service_account_credentials = ServiceAccountCredentials.from_json_keyfile_dict( + json_keyfile, + scopes=self.SCOPE_URI, + ) + http = service_account_credentials.authorize(http) + return discovery.build( + self.SCOPE, + self.API_VERSION, + http=http, + cache_discovery=False, + ) + except (errors.Error, errors.HttpError) as e: + raise AuthorizationError(e) from e + + @property + def android_publishing_service(self): + return self._get_android_publisher_service() @property - def service(self) -> discovery.Resource: - if self._service_instance is None: - try: - json_credentials = json.loads(str(self._credentials)) - except json.decoder.JSONDecodeError: - raise CredentialsError() - - http = httplib2.Http() - try: - service_account_credentials = ServiceAccountCredentials.from_json_keyfile_dict( - json_credentials, scopes=self.SCOPE_URI) - http = service_account_credentials.authorize(http) - self._service_instance = discovery.build(self.SCOPE, self.API_VERSION, http=http, cache_discovery=False) - except errors.HttpError as e: - raise AuthorizationError(e._get_reason() or 'Http Error') - except errors.Error as e: - raise AuthorizationError(str(e)) - return self._service_instance - - def create_edit(self) -> Edit: - self.resource_printer.log_request(f'Create an edit for the package "{self.package_name}"') + def edits_service(self): + return self.android_publishing_service.edits() + + def create_edit(self, package_name: str) -> Edit: + self._logger.debug(f'Create an edit for the package {package_name!r}') try: - edit_response = self.get_edit_resource().insert(body={}, packageName=self.package_name).execute() - resource = Edit(**edit_response) - self.resource_printer.print_resource(resource) - return resource - except errors.HttpError as e: - raise EditError('create', self.package_name, e._get_reason() or 'Http Error') - except errors.Error as e: - raise EditError('create', self.package_name, str(e)) - - def delete_edit(self, edit_id: str) -> None: - self.resource_printer.log_request(f'Delete the edit "{edit_id}" for the package "{self.package_name}"') + edit_request = self.edits_service.insert(body={}, packageName=package_name) + edit_response = edit_request.execute() + self._logger.debug(f'Created edit {edit_response} for package {package_name!r}') + except (errors.Error, errors.HttpError) as e: + raise EditError('create', package_name, e) from e + else: + return Edit(**edit_response) + + def delete_edit(self, edit: EditId, package_name: str) -> None: + edit_id = self._edit_id(edit) + self._logger.debug(f'Delete the edit {edit_id!r} for the package {package_name!r}') try: - self.get_edit_resource().delete(packageName=self.package_name, editId=edit_id).execute() - self._service_instance = None - except errors.HttpError as e: - raise EditError('delete', self.package_name, e._get_reason() or 'Http Error') - except errors.Error as e: - raise EditError('delete', self.package_name, str(e)) - - def get_track_information(self, edit_id: str, track_name: TrackName) -> Track: - self.resource_printer.log_request( - f'Get information about the track "{track_name.value}" ' - f'for the package "{self.package_name}"', - ) + delete_request = self.edits_service.delete( + packageName=package_name, + editId=edit_id, + ) + delete_request.execute() + self._logger.debug(f'Deleted edit {edit_id} for package {package_name!r}') + except (errors.Error, errors.HttpError) as e: + raise EditError('delete', package_name, e) from e + + @contextlib.contextmanager + def use_app_edit(self, package_name: str) -> Edit: + edit = self.create_edit(package_name) + try: + yield edit + finally: + self.delete_edit(edit, package_name) + + def get_track(self, package_name: str, track_name: str, edit: MaybeEditId = None) -> Track: + if edit is not None: + return self._get_track(package_name, track_name, self._edit_id(edit)) + with self.use_app_edit(package_name) as edit: + return self._get_track(package_name, track_name, edit.id) + + def _get_track(self, package_name: str, track_name: str, edit_id: str) -> Track: + self._logger.debug(f'Get track {track_name!r} for package {package_name!r} using edit {edit_id}') + try: + track_request = self.edits_service.tracks().get( + packageName=package_name, + editId=edit_id, + track=track_name, + ) + track_response = track_request.execute() + self._logger.debug(f'Got track {track_name!r} for package {package_name!r}: {track_response}') + except (errors.Error, errors.HttpError) as e: + raise GetResourceError('track', package_name, e) from e + else: + return Track(**track_response) + + def list_tracks(self, package_name: str, edit_id: MaybeEditId = None) -> List[Track]: + if edit_id is not None: + return self._list_tracks(package_name, self._edit_id(edit_id)) + with self.use_app_edit(package_name) as edit: + return self._list_tracks(package_name, edit.id) + + def _list_tracks(self, package_name: str, edit_id: str) -> List[Track]: + self._logger.debug(f'List tracks for package {package_name!r} using edit {edit_id}') try: - track_response = self.get_edit_resource().tracks().get( - packageName=self.package_name, editId=edit_id, track=track_name.value).execute() - resource = Track(**track_response) - self.resource_printer.print_resource(resource) - return resource - except errors.HttpError as e: - raise VersionCodeFromTrackError(track_name.value, e._get_reason() or 'Http Error') - except errors.Error as e: - raise VersionCodeFromTrackError(track_name.value, str(e)) + tracks_request = self.edits_service.tracks().list( + packageName=package_name, + editId=edit_id, + ) + tracks_response = tracks_request.execute() + self._logger.debug(f'Got tracks for package {package_name!r}: {tracks_response}') + except (errors.Error, errors.HttpError) as e: + raise ListResourcesError('tracks', package_name, e) from e + else: + return [Track(**track) for track in tracks_response['tracks']] diff --git a/src/codemagic/google_play/api_error.py b/src/codemagic/google_play/api_error.py index 844d6679..630a0fb1 100644 --- a/src/codemagic/google_play/api_error.py +++ b/src/codemagic/google_play/api_error.py @@ -1,23 +1,62 @@ -class GooglePlayDeveloperAPIClientError(Exception): - pass +from abc import ABC +from abc import abstractmethod +from typing import Union + +from googleapiclient import errors -class CredentialsError(GooglePlayDeveloperAPIClientError): - def __init__(self): - super().__init__('Unable to parse service account credentials, must be a valid json') +class GooglePlayDeveloperAPIClientError(Exception): + @classmethod + def _get_error_reason(cls, error: Union[errors.Error, errors.HttpError]) -> str: + if isinstance(error, errors.Error): + return str(error) + else: + reason = error._get_reason() + return reason or 'Http Error' class AuthorizationError(GooglePlayDeveloperAPIClientError): - def __init__(self, reason: str): + def __init__(self, error: Union[errors.Error, errors.HttpError]): + reason = self._get_error_reason(error) super().__init__(f'Unable to authorize with provided credentials. {reason}') class EditError(GooglePlayDeveloperAPIClientError): - def __init__(self, action: str, package_name: str, reason: str): + def __init__(self, action: str, package_name: str, error: Union[errors.Error, errors.HttpError]): + reason = self._get_error_reason(error) super().__init__(f'Unable to {action} an edit for package "{package_name}". {reason}') -class VersionCodeFromTrackError(GooglePlayDeveloperAPIClientError): - def __init__(self, track: str, reason: str): - super().__init__( - f'Failed to get version code from Google Play from {track} track. {reason}') +class _RequestError(GooglePlayDeveloperAPIClientError, ABC): + def __init__( + self, + resource_description: str, + package_name: str, + request_error: Union[errors.Error, errors.HttpError], + ): + self.package_name = package_name + self.request_error = request_error + super().__init__(self._get_message(resource_description)) + + def _get_reason(self) -> str: + return self._get_error_reason(self.request_error) + + @abstractmethod + def _get_message(self, resource_description: str) -> str: + raise NotImplementedError() + + +class GetResourceError(_RequestError): + def _get_message(self, resource_description: str) -> str: + return ( + f'Failed to get {resource_description} from Google Play for package "{self.package_name}". ' + f'{self._get_reason()}' + ) + + +class ListResourcesError(_RequestError): + def _get_message(self, resource_description: str) -> str: + return ( + f'Failed to list {resource_description} from Google Play for package "{self.package_name}". ' + f'{self._get_reason()}' + ) diff --git a/src/codemagic/google_play/resource_printer.py b/src/codemagic/google_play/resource_printer.py deleted file mode 100644 index b077aa31..00000000 --- a/src/codemagic/google_play/resource_printer.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Callable -from typing import TypeVar - -from codemagic.cli import Colors - -from .resources.resource import Resource - -ResourceToPrint = TypeVar('ResourceToPrint', bound=Resource) - - -class ResourcePrinter: - - def __init__(self, - should_print: bool = False, - print_json: bool = False, - print_function: Callable[[str], None] = None): - self.print = print_function - self.should_print = should_print - self.print_json = print_json - - def log_request(self, header: str) -> None: - if not self.print: - return - self.print(Colors.BLUE(header)) - - def print_resource(self, resource: ResourceToPrint) -> None: - if not self.print: - return - if self.should_print: - if self.print_json: - self.print(resource.json()) - else: - self.print(str(resource)) diff --git a/src/codemagic/google_play/resources/__init__.py b/src/codemagic/google_play/resources/__init__.py index 32b10132..b1c85f51 100644 --- a/src/codemagic/google_play/resources/__init__.py +++ b/src/codemagic/google_play/resources/__init__.py @@ -1,3 +1,3 @@ from .edit import Edit -from .enums import TrackName +from .resource import Resource from .track import Track diff --git a/src/codemagic/google_play/resources/enums.py b/src/codemagic/google_play/resources/enums.py index c3961863..3f954bc4 100644 --- a/src/codemagic/google_play/resources/enums.py +++ b/src/codemagic/google_play/resources/enums.py @@ -6,13 +6,6 @@ def __str__(self): return str(self.value) -class TrackName(ResourceEnum): - INTERNAL = 'internal' - ALPHA = 'alpha' - BETA = 'beta' - PRODUCTION = 'production' - - class ReleaseStatus(ResourceEnum): STATUS_UNSPECIFIED = 'statusUnspecified' DRAFT = 'draft' diff --git a/src/codemagic/google_play/resources/track.py b/src/codemagic/google_play/resources/track.py index 11359331..d23b9891 100644 --- a/src/codemagic/google_play/resources/track.py +++ b/src/codemagic/google_play/resources/track.py @@ -5,8 +5,6 @@ from typing import List from typing import Optional -from codemagic.google_play import VersionCodeFromTrackError - from .enums import ReleaseStatus from .resource import Resource @@ -77,11 +75,11 @@ def __post_init__(self): if isinstance(self.releases, list): self.releases = [Release(**release) for release in self.releases] - @property - def max_version_code(self) -> int: + def get_max_version_code(self) -> int: + error_prefix = f'Failed to get version code from Google Play from {self.track} track' if not self.releases: - raise VersionCodeFromTrackError(self.track, 'No release information') + raise ValueError(f'{error_prefix}. No release information') version_codes = [release.versionCodes for release in self.releases if release.versionCodes] if not version_codes: - raise VersionCodeFromTrackError(self.track, 'No releases with uploaded App bundles or APKs') + raise ValueError(f'{error_prefix}. No releases with uploaded App bundles or APKs') return max(map(int, chain(*version_codes))) diff --git a/src/codemagic/tools/google_play.py b/src/codemagic/tools/google_play.py index 61ebc394..feef2ee2 100644 --- a/src/codemagic/tools/google_play.py +++ b/src/codemagic/tools/google_play.py @@ -4,15 +4,15 @@ import argparse import json +from typing import Dict from typing import List from typing import Optional from typing import Sequence from codemagic import cli from codemagic.google_play import GooglePlayDeveloperAPIClientError -from codemagic.google_play import ResourcePrinter from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient -from codemagic.google_play.resources import TrackName +from codemagic.google_play.resources import Track class Types: @@ -43,20 +43,6 @@ class GooglePlayArgument(cli.Argument): ), argparse_kwargs={'required': False}, ) - PACKAGE_NAME = cli.ArgumentProperties( - key='package_name', - flags=('--package-name',), - type=Types.PackageName, - description='Package name of the app in Google Play Console (Ex: com.google.example)', - argparse_kwargs={'required': True}, - ) - LOG_REQUESTS = cli.ArgumentProperties( - key='log_requests', - flags=('--log-api-calls',), - type=bool, - description='Turn on logging for Google Play Developer API requests', - argparse_kwargs={'required': False, 'action': 'store_true'}, - ) JSON_OUTPUT = cli.ArgumentProperties( key='json_output', flags=('--json',), @@ -66,20 +52,33 @@ class GooglePlayArgument(cli.Argument): ) +class TracksArgument(cli.Argument): + PACKAGE_NAME = cli.ArgumentProperties( + key='package_name', + flags=('-p', '--package-name'), + type=Types.PackageName, + description='Package name of the app in Google Play Console (Ex: com.google.example)', + argparse_kwargs={'required': True}, + ) + TRACK_NAME = cli.ArgumentProperties( + key='track_name', + flags=('-t', '--track'), + description='Release track name. For example `alpha` or `production`', + argparse_kwargs={'required': True}, + ) + + class BuildNumberArgument(cli.Argument): TRACKS = cli.ArgumentProperties( key='tracks', - flags=('--tracks',), - type=TrackName, + flags=('-t', '--tracks'), description=( 'Get the build number from the specified track(s). ' - 'If not specified, the highest build number across all tracks ' - f'({", ".join(list(map(str, TrackName)))}) is returned' + 'If not specified, the highest build number across all tracks is returned' ), argparse_kwargs={ 'required': False, 'nargs': '+', - 'choices': list(TrackName), }, ) @@ -88,77 +87,140 @@ class GooglePlayError(cli.CliAppException): pass -@cli.common_arguments(*GooglePlayArgument) +class GooglePlayActionGroups(cli.ActionGroup): + TRACKS = cli.ActionGroupProperties( + name='tracks', + description='Manage your Google Play release tracks', + ) + + +@cli.common_arguments(GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS) class GooglePlay(cli.CliApp): """ Utility to get the latest build numbers from Google Play using Google Play Developer API """ - def __init__(self, - credentials: str, - package_name: Types.PackageName, - log_requests: bool = False, - json_output: bool = False, - **kwargs): + def __init__( + self, + credentials: str, + **kwargs, + ): super().__init__(**kwargs) self.credentials = credentials - self.package_name = package_name - print_command = self.logger.info if kwargs.get('enable_logging') else None - printer = ResourcePrinter(bool(log_requests), bool(json_output), print_command) - self.api_client = GooglePlayDeveloperAPIClient(credentials, package_name, printer) + self.api_client = GooglePlayDeveloperAPIClient(credentials) @classmethod def from_cli_args(cls, cli_args: argparse.Namespace) -> GooglePlay: credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.from_args(cli_args) - package_name_argument = GooglePlayArgument.PACKAGE_NAME.from_args(cli_args) - if credentials_argument is None: raise GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.raise_argument_error() - if package_name_argument is None: - raise GooglePlayArgument.PACKAGE_NAME.raise_argument_error() return GooglePlay( credentials=credentials_argument.value, - package_name=package_name_argument, - log_requests=cli_args.log_requests, - json_output=cli_args.json_output, **cls._parent_class_kwargs(cli_args), ) - @cli.action('get-latest-build-number', BuildNumberArgument.TRACKS) - def get_latest_build_number(self, tracks: Sequence[TrackName] = None) -> Optional[int]: + @cli.action( + 'get-latest-build-number', + TracksArgument.PACKAGE_NAME, + BuildNumberArgument.TRACKS, + ) + def get_latest_build_number( + self, + package_name: str, + tracks: Optional[Sequence[str]] = None, + ) -> Optional[int]: """ Get latest build number from Google Play Developer API matching given constraints """ - try: - edit = self.api_client.create_edit() - except GooglePlayDeveloperAPIClientError as api_error: - raise GooglePlayError(str(api_error)) + package_tracks = self.list_tracks(package_name, should_print=False) + track_version_codes: Dict[str, int] = {} - track_version_codes: List[int] = [] - track_names: Sequence[TrackName] = tracks or list(TrackName) - for track_name in track_names: + for track in package_tracks: + track_name = track.track + if tracks and track_name not in tracks: + continue try: - track = self.api_client.get_track_information(edit.id, track_name) - version_code: int = track.max_version_code - except GooglePlayDeveloperAPIClientError as api_error: - self.logger.warning(api_error) + version_code = track.get_max_version_code() + except ValueError as ve: + self.logger.warning(ve) else: - self.logger.info(f'Latest version code for {track_name.value} track: {str(version_code)}') - track_version_codes.append(version_code) - try: - self.api_client.delete_edit(edit.id) - except GooglePlayDeveloperAPIClientError as api_error: - self.logger.warning(api_error) + self.logger.info(f'Latest version code for {track_name} track: {version_code}') + track_version_codes[track_name] = version_code - try: - latest_build_number = max(track_version_codes) - except ValueError: + if not track_version_codes: raise GooglePlayError('Version code info is missing from all tracks') + + missing_track_names = set(tracks or []).difference(track_version_codes.keys()) + for missing_track_name in missing_track_names: + self.logger.warning(( + f'Failed to get version code from Google Play for track "{missing_track_name}". ' + 'Track was not found.' + )) + + latest_build_number = max(track_version_codes.values()) self.echo(str(latest_build_number)) return latest_build_number + @cli.action( + 'get', + TracksArgument.PACKAGE_NAME, + TracksArgument.TRACK_NAME, + GooglePlayArgument.JSON_OUTPUT, + action_group=GooglePlayActionGroups.TRACKS, + ) + def get_track( + self, + package_name: str, + track_name: str, + json_output: bool = False, + should_print: bool = True, + ) -> Track: + """ + Get information about specified track from Google Play Developer API + """ + + try: + track = self.api_client.get_track(package_name, track_name) + except GooglePlayDeveloperAPIClientError as api_error: + raise GooglePlayError(str(api_error)) + + if should_print: + self.logger.info(track.json() if json_output else str(track)) + + return track + + @cli.action( + 'list', + TracksArgument.PACKAGE_NAME, + GooglePlayArgument.JSON_OUTPUT, + action_group=GooglePlayActionGroups.TRACKS, + ) + def list_tracks( + self, + package_name: str, + json_output: bool = False, + should_print: bool = True, + ) -> List[Track]: + """ + Get information about specified track from Google Play Developer API + """ + + try: + tracks = self.api_client.list_tracks(package_name) + except GooglePlayDeveloperAPIClientError as api_error: + raise GooglePlayError(str(api_error)) + + if should_print: + if json_output: + self.echo(json.dumps([t.dict() for t in tracks], indent=4)) + else: + for track in tracks: + self.echo(str(track)) + + return tracks + if __name__ == '__main__': GooglePlay.invoke_cli() diff --git a/tests/conftest.py b/tests/conftest.py index 5feb507a..8bc98231 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,6 @@ from codemagic.apple.app_store_connect import IssuerId # noqa: E402 from codemagic.apple.app_store_connect import KeyIdentifier # noqa: E402 from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient # noqa: E402 -from codemagic.google_play.api_client import ResourcePrinter # noqa: E402 from codemagic.utilities import log # noqa: E402 log.initialize_logging( @@ -83,11 +82,7 @@ def _google_play_api_client() -> GooglePlayDeveloperAPIClient: 'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH', 'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_CONTENT', ) - return GooglePlayDeveloperAPIClient( - credentials, - os.environ['TEST_GCLOUD_PACKAGE_NAME'], - ResourcePrinter(False, False), - ) + return GooglePlayDeveloperAPIClient(credentials) def _logger(): diff --git a/tests/google_play/test_api_client.py b/tests/google_play/test_api_client.py index 7d706015..c804f9f0 100644 --- a/tests/google_play/test_api_client.py +++ b/tests/google_play/test_api_client.py @@ -1,4 +1,5 @@ import os +import pathlib import unittest import pytest @@ -29,3 +30,10 @@ def test_get_edit_and_track_information(self): self.api_client.delete_edit(edit.id) assert not self._service_exists() + + +def test_google_play_api_client(): + credentials = pathlib.Path('~/google_play_service_account_credentials.json').expanduser().read_text() + client = GooglePlayDeveloperAPIClient(credentials, package_name='io.codemagic.artemii.capybara') + print() + print(client) diff --git a/tests/tools/test_google_play.py b/tests/tools/test_google_play.py index 62ae85c9..d3a8cda8 100644 --- a/tests/tools/test_google_play.py +++ b/tests/tools/test_google_play.py @@ -9,6 +9,7 @@ from codemagic.tools.google_play import GooglePlay from codemagic.tools.google_play import GooglePlayArgument +from codemagic.tools.google_play import TracksArgument from codemagic.tools.google_play import Types credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS @@ -24,9 +25,6 @@ def register_args(cli_argument_group): def namespace_kwargs(): ns_kwargs = { credentials_argument.key: Types.CredentialsArgument('{"type":"service_account"}'), - GooglePlayArgument.PACKAGE_NAME.key: Types.PackageName('package.name'), - GooglePlayArgument.LOG_REQUESTS.key: False, - GooglePlayArgument.JSON_OUTPUT.key: False, } for arg in GooglePlay.CLASS_ARGUMENTS: if not hasattr(arg.type, 'environment_variable_key'): @@ -48,7 +46,7 @@ def _test_missing_argument(argument, _namespace_kwargs): @pytest.mark.parametrize('argument', [ credentials_argument, - GooglePlayArgument.PACKAGE_NAME, + TracksArgument.PACKAGE_NAME, ]) def test_missing_arg(cli_argument_group, namespace_kwargs, argument): namespace_kwargs[argument.key] = None From eedd4938a1a9648cd446ec7bf790e646ea5fbf40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 14:10:56 +0300 Subject: [PATCH 02/10] Fix type hints --- src/codemagic/google_play/api_client.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py index ba9be6ee..410c2d74 100644 --- a/src/codemagic/google_play/api_client.py +++ b/src/codemagic/google_play/api_client.py @@ -2,6 +2,7 @@ import json from functools import lru_cache from typing import AnyStr +from typing import Generator from typing import List from typing import Optional from typing import Union @@ -22,8 +23,6 @@ from .resources import Track CredentialsJson = Union[AnyStr, dict] -EditId = Union[str, Edit] -MaybeEditId = Optional[EditId] class GooglePlayDeveloperAPIClient: @@ -39,7 +38,7 @@ def __init__(self, service_account_json_keyfile: CredentialsJson): self._logger = log.get_logger(self.__class__) @classmethod - def _edit_id(cls, edit: EditId) -> str: + def _edit_id(cls, edit: Union[str, Edit]) -> str: if isinstance(edit, Edit): return edit.id return edit @@ -91,7 +90,7 @@ def create_edit(self, package_name: str) -> Edit: else: return Edit(**edit_response) - def delete_edit(self, edit: EditId, package_name: str) -> None: + def delete_edit(self, edit: Union[str, Edit], package_name: str) -> None: edit_id = self._edit_id(edit) self._logger.debug(f'Delete the edit {edit_id!r} for the package {package_name!r}') try: @@ -105,14 +104,19 @@ def delete_edit(self, edit: EditId, package_name: str) -> None: raise EditError('delete', package_name, e) from e @contextlib.contextmanager - def use_app_edit(self, package_name: str) -> Edit: + def use_app_edit(self, package_name: str) -> Generator[Edit, None, None]: edit = self.create_edit(package_name) try: yield edit finally: self.delete_edit(edit, package_name) - def get_track(self, package_name: str, track_name: str, edit: MaybeEditId = None) -> Track: + def get_track( + self, + package_name: str, + track_name: str, + edit: Optional[Union[str, Edit]] = None, + ) -> Track: if edit is not None: return self._get_track(package_name, track_name, self._edit_id(edit)) with self.use_app_edit(package_name) as edit: @@ -133,7 +137,11 @@ def _get_track(self, package_name: str, track_name: str, edit_id: str) -> Track: else: return Track(**track_response) - def list_tracks(self, package_name: str, edit_id: MaybeEditId = None) -> List[Track]: + def list_tracks( + self, + package_name: str, + edit_id: Optional[Union[str, Edit]] = None, + ) -> List[Track]: if edit_id is not None: return self._list_tracks(package_name, self._edit_id(edit_id)) with self.use_app_edit(package_name) as edit: From 4b9ab2ad7a6ebddf8bbed94904647559713ebfce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 14:41:58 +0300 Subject: [PATCH 03/10] Update mypy --- Pipfile.lock | 610 ++++++++++++++++++++++----------------------------- 1 file changed, 264 insertions(+), 346 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 8a46b511..4f91eb8e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,11 +18,11 @@ "default": { "cachetools": { "hashes": [ - "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693", - "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1" + "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6", + "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4" ], - "markers": "python_version ~= '3.5'", - "version": "==4.2.4" + "markers": "python_version ~= '3.7'", + "version": "==5.0.0" }, "certifi": { "hashes": [ @@ -88,11 +88,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", - "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], "markers": "python_version >= '3'", - "version": "==2.0.10" + "version": "==2.0.12" }, "cryptography": { "hashes": [ @@ -129,11 +129,11 @@ }, "google-auth": { "hashes": [ - "sha256:a348a50b027679cb7dae98043ac8dbcc1d7951f06d8387496071a1e05a2465c0", - "sha256:d83570a664c10b97a1dc6f8df87e5fdfff012f48f62be131e449c20dfc32630e" + "sha256:5e079eb4d21df1853d55cf2b6766b77ef36f7f7bdaf7d4a70434aa97c7578d60", + "sha256:d65bb0e3701eaaa64fd2aa85e1325580524b0bddc6dc5db3ab89c481b6a20141" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.3.3" + "version": "==2.6.3" }, "google-auth-httplib2": { "hashes": [ @@ -288,14 +288,6 @@ "markers": "python_version >= '3.6' and python_version < '4'", "version": "==4.8" }, - "setuptools": { - "hashes": [ - "sha256:2404879cda71495fc4d5cbc445ed52fdaddf352b36e40be8dcc63147cb4edabe", - "sha256:68eb94073fc486091447fcb0501efd6560a0e5a1839ba249e5ff3c4c93f05f90" - ], - "markers": "python_version >= '3.7'", - "version": "==60.5.0" - }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -314,21 +306,29 @@ }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "version": "==1.26.9" } }, "develop": { + "appnope": { + "hashes": [ + "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", + "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.3" + }, "astroid": { "hashes": [ - "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877", - "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6" + "sha256:8d0a30fe6481ce919f56690076eafbb2fb649142a89dc874f1ec0e7a011492d0", + "sha256:cc8cc0d2d916c42d0a7c476c57550a4557a083081976bf42a73414322a6411d9" ], "markers": "python_full_version >= '3.6.2'", - "version": "==2.9.3" + "version": "==2.11.2" }, "attrs": { "hashes": [ @@ -355,11 +355,11 @@ }, "bleach": { "hashes": [ - "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", - "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" + "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1", + "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565" ], - "markers": "python_version >= '3.6'", - "version": "==4.1.0" + "markers": "python_version >= '3.7'", + "version": "==5.0.0" }, "certifi": { "hashes": [ @@ -368,166 +368,87 @@ ], "version": "==2021.10.8" }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, "charset-normalizer": { "hashes": [ - "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", - "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" + "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" ], "markers": "python_version >= '3'", - "version": "==2.0.10" + "version": "==2.0.12" }, - "colorama": { + "commonmark": { "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60", + "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.4.4" + "version": "==0.9.1" }, "coverage": { "extras": [ "toml" ], "hashes": [ - "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0", - "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd", - "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884", - "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48", - "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76", - "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0", - "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64", - "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685", - "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47", - "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d", - "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840", - "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f", - "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971", - "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c", - "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a", - "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de", - "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17", - "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4", - "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521", - "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57", - "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b", - "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282", - "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644", - "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475", - "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d", - "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da", - "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953", - "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2", - "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e", - "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c", - "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc", - "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64", - "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74", - "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617", - "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3", - "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d", - "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa", - "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739", - "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8", - "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8", - "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781", - "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58", - "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9", - "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c", - "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd", - "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e", - "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49" - ], - "markers": "python_version >= '3.6'", - "version": "==6.2" - }, - "cryptography": { - "hashes": [ - "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e", - "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b", - "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7", - "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085", - "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc", - "sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d", - "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a", - "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498", - "sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89", - "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9", - "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c", - "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7", - "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb", - "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14", - "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af", - "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e", - "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5", - "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06", - "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7" + "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", + "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", + "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", + "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", + "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", + "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", + "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", + "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", + "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", + "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", + "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", + "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", + "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", + "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", + "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", + "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", + "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", + "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", + "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", + "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", + "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", + "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", + "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", + "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", + "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", + "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", + "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", + "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", + "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", + "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", + "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", + "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", + "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", + "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", + "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", + "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", + "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", + "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", + "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", + "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", + "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" ], - "index": "pypi", - "version": "==3.4.8" + "markers": "python_version >= '3.7'", + "version": "==6.3.2" }, "decorator": { "hashes": [ "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" ], - "markers": "python_version >= '3.5'", + "markers": "python_version >= '3.7'", "version": "==5.1.1" }, + "dill": { + "hashes": [ + "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f", + "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675" + ], + "markers": "python_version >= '2.7' and python_version != '3.0'", + "version": "==0.3.4" + }, "distlib": { "hashes": [ "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b", @@ -545,11 +466,11 @@ }, "filelock": { "hashes": [ - "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80", - "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146" + "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85", + "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0" ], "markers": "python_version >= '3.7'", - "version": "==3.4.2" + "version": "==3.6.0" }, "flake8": { "hashes": [ @@ -613,11 +534,11 @@ }, "ipython": { "hashes": [ - "sha256:55df3e0bd0f94e715abd968bedd89d4e8a7bce4bf498fb123fed4f5398fea874", - "sha256:b5548ec5329a4bcf054a5deed5099b0f9622eb9ea51aaa7104d215fece201d8c" + "sha256:468abefc45c15419e3c8e8c0a6a5c115b2127bafa34d7c641b1d443658793909", + "sha256:86df2cf291c6c70b5be6a7b608650420e89180c8ec74f376a34e2dc15c3400e7" ], - "index": "pypi", - "version": "==7.31.1" + "markers": "python_version >= '3.7'", + "version": "==7.32.0" }, "isort": { "hashes": [ @@ -635,14 +556,6 @@ "markers": "python_version >= '3.6'", "version": "==0.18.1" }, - "jeepney": { - "hashes": [ - "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac", - "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f" - ], - "markers": "sys_platform == 'linux'", - "version": "==0.7.1" - }, "keyring": { "hashes": [ "sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9", @@ -718,29 +631,32 @@ }, "mypy": { "hashes": [ - "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce", - "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d", - "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069", - "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c", - "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d", - "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714", - "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a", - "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d", - "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05", - "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266", - "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697", - "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc", - "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799", - "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd", - "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00", - "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7", - "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a", - "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0", - "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0", - "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166" + "sha256:0e2dd88410937423fba18e57147dd07cd8381291b93d5b1984626f173a26543e", + "sha256:10daab80bc40f84e3f087d896cdb53dc811a9f04eae4b3f95779c26edee89d16", + "sha256:17e44649fec92e9f82102b48a3bf7b4a5510ad0cd22fa21a104826b5db4903e2", + "sha256:1a0459c333f00e6a11cbf6b468b870c2b99a906cb72d6eadf3d1d95d38c9352c", + "sha256:246e1aa127d5b78488a4a0594bd95f6d6fb9d63cf08a66dafbff8595d8891f67", + "sha256:2b184db8c618c43c3a31b32ff00cd28195d39e9c24e7c3b401f3db7f6e5767f5", + "sha256:2bc249409a7168d37c658e062e1ab5173300984a2dada2589638568ddc1db02b", + "sha256:3841b5433ff936bff2f4dc8d54cf2cdbfea5d8e88cedfac45c161368e5770ba6", + "sha256:4c3e497588afccfa4334a9986b56f703e75793133c4be3a02d06a3df16b67a58", + "sha256:5bf44840fb43ac4074636fd47ee476d73f0039f4f54e86d7265077dc199be24d", + "sha256:64235137edc16bee6f095aba73be5334677d6f6bdb7fa03cfab90164fa294a17", + "sha256:6776e5fa22381cc761df53e7496a805801c1a751b27b99a9ff2f0ca848c7eca0", + "sha256:6ce34a118d1a898f47def970a2042b8af6bdcc01546454726c7dd2171aa6dfca", + "sha256:6f6ad963172152e112b87cc7ec103ba0f2db2f1cd8997237827c052a3903eaa6", + "sha256:6f7106cbf9cc2f403693bf50ed7c9fa5bb3dfa9007b240db3c910929abe2a322", + "sha256:7742d2c4e46bb5017b51c810283a6a389296cda03df805a4f7869a6f41246534", + "sha256:9521c1265ccaaa1791d2c13582f06facf815f426cd8b07c3a485f486a8ffc1f3", + "sha256:a1b383fe99678d7402754fe90448d4037f9512ce70c21f8aee3b8bf48ffc51db", + "sha256:b840cfe89c4ab6386c40300689cd8645fc8d2d5f20101c7f8bd23d15fca14904", + "sha256:d8d3ba77e56b84cd47a8ee45b62c84b6d80d32383928fe2548c9a124ea0a725c", + "sha256:dcd955f36e0180258a96f880348fbca54ce092b40fbb4b37372ae3b25a0b0a46", + "sha256:e865fec858d75b78b4d63266c9aff770ecb6a39dfb6d6b56c47f7f8aba6baba8", + "sha256:edf7237137a1a9330046dbb14796963d734dd740a98d5e144a3eb1d267f5f9ee" ], "index": "pypi", - "version": "==0.931" + "version": "==0.942" }, "mypy-extensions": { "hashes": [ @@ -790,19 +706,19 @@ }, "pip": { "hashes": [ - "sha256:deaf32dcd9ab821e359cd8330786bcd077604b5c5730c0b096eda46f95c24a2d", - "sha256:fd11ba3d0fdb4c07fbc5ecbba0b1b719809420f25038f8ee3cd913d3faa3033a" + "sha256:b3a9de2c6ef801e9247d1527a4b16f92f2cc141cd1489f3fffaf6a9e96729764", + "sha256:c6aca0f2f081363f689f041d90dab2a07a9a07fb840284db2218117a52da800b" ], - "markers": "python_version >= '3.6'", - "version": "==21.3.1" + "markers": "python_version >= '3.7'", + "version": "==22.0.4" }, "pipenv": { "hashes": [ - "sha256:3b80b4512934b9d8e8ce12c988394642ff96bb697680e5b092e59af503178327", - "sha256:f84d7119239b22ab2ac2b8fbc7d619d83cf41135206d72a17c4f151cda529fd0" + "sha256:3ac62c1121477b0758b0eef9e47643d5a2ba1b57e47a0789620ce5ef9bbc007e", + "sha256:53562bf69d9e5238f99a1e2101c356746b1c0aefa5dceb9b8a84a5a3e201de0d" ], "index": "pypi", - "version": "==2022.1.8" + "version": "==2022.4.8" }, "pkginfo": { "hashes": [ @@ -813,11 +729,11 @@ }, "platformdirs": { "hashes": [ - "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", - "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" + "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d", + "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227" ], "markers": "python_version >= '3.7'", - "version": "==2.4.1" + "version": "==2.5.1" }, "pluggy": { "hashes": [ @@ -829,11 +745,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6", - "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506" + "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752", + "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.24" + "version": "==3.0.29" }, "ptyprocess": { "hashes": [ @@ -858,14 +774,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.8.0" }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, "pyflakes": { "hashes": [ "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", @@ -884,11 +792,11 @@ }, "pylint": { "hashes": [ - "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9", - "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74" + "sha256:c149694cfdeaee1aa2465e6eaab84c87a881a7d55e6e93e09466be7164764d1e", + "sha256:dab221658368c7a05242e673c275c488670144123f4bd262b2777249c1c0de9b" ], "index": "pypi", - "version": "==2.12.2" + "version": "==2.13.5" }, "pyparsing": { "hashes": [ @@ -900,11 +808,11 @@ }, "pytest": { "hashes": [ - "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", - "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63", + "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea" ], "index": "pypi", - "version": "==6.2.5" + "version": "==7.1.1" }, "pytest-cov": { "hashes": [ @@ -916,11 +824,11 @@ }, "readme-renderer": { "hashes": [ - "sha256:a50a0f2123a4c1145ac6f420e1a348aafefcc9211c846e3d51df05fe3d865b7d", - "sha256:b512beafa6798260c7d5af3e1b1f097e58bfcd9a575da7c4ddd5e037490a5b85" + "sha256:262510fe6aae81ed4e94d8b169077f325614c0b1a45916a80442c6576264a9c2", + "sha256:dfb4d17f21706d145f7473e0b61ca245ba58e810cf9b2209a48239677f82e5b0" ], "markers": "python_version >= '3.6'", - "version": "==32.0" + "version": "==34.0" }, "requests": { "hashes": [ @@ -945,21 +853,21 @@ "markers": "python_version >= '3.7'", "version": "==2.0.0" }, - "secretstorage": { + "rich": { "hashes": [ - "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f", - "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195" + "sha256:c50f3d253bc6a9bb9c79d61a26d510d74abdf1b16881260fab5edfc3edfb082f", + "sha256:ea74bc9dad9589d8eea3e3fd0b136d8bf6e428888955f215824c2894f0da8b47" ], - "markers": "sys_platform == 'linux'", - "version": "==3.3.1" + "markers": "python_version < '4' and python_full_version >= '3.6.3'", + "version": "==12.2.0" }, "setuptools": { "hashes": [ - "sha256:2404879cda71495fc4d5cbc445ed52fdaddf352b36e40be8dcc63147cb4edabe", - "sha256:68eb94073fc486091447fcb0501efd6560a0e5a1839ba249e5ff3c4c93f05f90" + "sha256:09980778aa734c3037a47997f28d6db5ab18bdf2af0e49f719bfc53967fd2e82", + "sha256:608a7885b664342ae9fafc43840b29d219c5a578876f6f7e00c4e2612160587f" ], - "markers": "python_version >= '3.7'", - "version": "==60.5.0" + "index": "pypi", + "version": "==59.8.0" }, "six": { "hashes": [ @@ -979,19 +887,11 @@ }, "tomli": { "hashes": [ - "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224", - "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1" + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "tqdm": { - "hashes": [ - "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c", - "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.3" + "version": "==2.0.1" }, "traitlets": { "hashes": [ @@ -1003,75 +903,80 @@ }, "twine": { "hashes": [ - "sha256:28460a3db6b4532bde6a5db6755cf2dce6c5020bada8a641bb2c5c7a9b1f35b8", - "sha256:8c120845fc05270f9ee3e9d7ebbed29ea840e41f48cd059e04733f7e1d401345" + "sha256:6f7496cf14a3a8903474552d5271c79c71916519edb42554f23f42a8563498a9", + "sha256:817aa0c0bdc02a5ebe32051e168e23c71a0608334e624c793011f120dbbc05b7" ], "index": "pypi", - "version": "==3.7.1" + "version": "==4.0.0" }, "typed-ast": { "hashes": [ - "sha256:24058827d8f5d633f97223f5148a7d22628099a3d2efe06654ce872f46f07cdb", - "sha256:256115a5bc7ea9e665c6314ed6671ee2c08ca380f9d5f130bd4d2c1f5848d695", - "sha256:38cf5c642fa808300bae1281460d4f9b7617cf864d4e383054a5ef336e344d32", - "sha256:484137cab8ecf47e137260daa20bafbba5f4e3ec7fda1c1e69ab299b75fa81c5", - "sha256:4f30a2bcd8e68adbb791ce1567fdb897357506f7ea6716f6bbdd3053ac4d9471", - "sha256:591bc04e507595887160ed7aa8d6785867fb86c5793911be79ccede61ae96f4d", - "sha256:5b6ab14c56bc9c7e3c30228a0a0b54b915b1579613f6e463ba6f4eb1382e7fd4", - "sha256:5d8314c92414ce7481eee7ad42b353943679cf6f30237b5ecbf7d835519e1212", - "sha256:71dcda943a471d826ea930dd449ac7e76db7be778fcd722deb63642bab32ea3f", - "sha256:7c42707ab981b6cf4b73490c16e9d17fcd5227039720ca14abe415d39a173a30", - "sha256:9caaf2b440efb39ecbc45e2fabde809cbe56272719131a6318fd9bf08b58e2cb", - "sha256:a2b8d7007f6280e36fa42652df47087ac7b0a7d7f09f9468f07792ba646aac2d", - "sha256:a6d495c1ef572519a7bac9534dbf6d94c40e5b6a608ef41136133377bba4aa08", - "sha256:a80d84f535642420dd17e16ae25bb46c7f4c16ee231105e7f3eb43976a89670a", - "sha256:b53ae5de5500529c76225d18eeb060efbcec90ad5e030713fe8dab0fb4531631", - "sha256:b6d17f37f6edd879141e64a5db17b67488cfeffeedad8c5cec0392305e9bc775", - "sha256:c9bcad65d66d594bffab8575f39420fe0ee96f66e23c4d927ebb4e24354ec1af", - "sha256:ca9e8300d8ba0b66d140820cf463438c8e7b4cdc6fd710c059bfcfb1531d03fb", - "sha256:de4ecae89c7d8b56169473e08f6bfd2df7f95015591f43126e4ea7865928677e" + "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e", + "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344", + "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266", + "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a", + "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd", + "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d", + "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837", + "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098", + "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e", + "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27", + "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b", + "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596", + "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76", + "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30", + "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4", + "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78", + "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca", + "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985", + "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb", + "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88", + "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7", + "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5", + "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e", + "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7" ], "markers": "python_version < '3.8'", - "version": "==1.5.1" + "version": "==1.5.2" }, "types-requests": { "hashes": [ - "sha256:5e0dc681e9bfadb5c1d17f63d29f6d13d29ef2897d30cb9c79285b34a1655fd7", - "sha256:8f8a3cf66c525247750b0a0e6ffef4a210d7c8637065a1e7a3cb5769b1eb82a4" + "sha256:2437a5f4d16c0c8bd7539a8126d492b7aeb41e6cda670d76b286c7f83a658d42", + "sha256:c8010c18b291a7efb60b1452dbe12530bc25693dd657e70c62803fcdc4bffe9b" ], "index": "pypi", - "version": "==2.27.6" + "version": "==2.27.16" }, "types-urllib3": { "hashes": [ - "sha256:3adcf2cb5981809091dbff456e6999fe55f201652d8c360f99997de5ac2f556e", - "sha256:cfd1fbbe4ba9a605ed148294008aac8a7b8b7472651d1cc357d507ae5962e3d2" + "sha256:24d64e441168851eb05f1d022de18ae31558f5649c8f1117e384c2e85e31315b", + "sha256:bd0abc01e9fb963e4fddd561a56d21cc371b988d1245662195c90379077139cd" ], - "version": "==1.26.7" + "version": "==1.26.11" }, "typing-extensions": { "hashes": [ - "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", - "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", + "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" ], - "markers": "python_version < '3.10'", - "version": "==4.0.1" + "markers": "python_version >= '3.6'", + "version": "==4.1.1" }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "version": "==1.26.9" }, "virtualenv": { "hashes": [ - "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09", - "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd" + "sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a", + "sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==20.13.0" + "version": "==20.14.1" }, "virtualenv-clone": { "hashes": [ @@ -1097,68 +1002,81 @@ }, "wrapt": { "hashes": [ - "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179", - "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096", - "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374", - "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df", - "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185", - "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785", - "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7", - "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909", - "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918", - "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33", - "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068", - "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829", - "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af", - "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79", - "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce", - "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc", - "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36", - "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade", - "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca", - "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32", - "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125", - "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e", - "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709", - "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f", - "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b", - "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb", - "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb", - "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489", - "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640", - "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb", - "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851", - "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d", - "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44", - "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13", - "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2", - "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb", - "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b", - "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9", - "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755", - "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c", - "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a", - "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf", - "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3", - "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229", - "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e", - "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de", - "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554", - "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10", - "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80", - "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", - "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" + "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b", + "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0", + "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330", + "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3", + "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68", + "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa", + "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe", + "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd", + "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b", + "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80", + "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38", + "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f", + "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350", + "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd", + "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb", + "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3", + "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0", + "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff", + "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c", + "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758", + "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036", + "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb", + "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763", + "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9", + "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7", + "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1", + "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7", + "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0", + "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5", + "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce", + "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8", + "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279", + "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0", + "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06", + "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561", + "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a", + "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311", + "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131", + "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4", + "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291", + "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4", + "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8", + "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8", + "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d", + "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c", + "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd", + "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d", + "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6", + "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775", + "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e", + "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627", + "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e", + "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8", + "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1", + "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48", + "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc", + "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3", + "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6", + "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425", + "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d", + "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23", + "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c", + "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33", + "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.13.3" + "version": "==1.14.0" }, "zipp": { "hashes": [ - "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", - "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" + "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", + "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" ], "markers": "python_version >= '3.7'", - "version": "==3.7.0" + "version": "==3.8.0" } } } From bb862efb40f9d8b3bbafe0926fdd6241020b0e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 14:42:28 +0300 Subject: [PATCH 04/10] Rename variables and methods --- src/codemagic/cli/cli_process_stream.py | 4 ++-- src/codemagic/google_play/api_client.py | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/codemagic/cli/cli_process_stream.py b/src/codemagic/cli/cli_process_stream.py index fb2ac088..977d6e23 100644 --- a/src/codemagic/cli/cli_process_stream.py +++ b/src/codemagic/cli/cli_process_stream.py @@ -91,7 +91,7 @@ def unblock(self): def read(self, buffer_size=1024) -> bytes: # https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile try: - chunk, _result = winapi.ReadFile(self._pipe_handle, buffer_size, 0) + chunk, _result = winapi.ReadFile(self._pipe_handle, buffer_size, 0) # type: ignore except BrokenPipeError: chunk = b'' return self._bytes(chunk or b'') @@ -100,7 +100,7 @@ def read_all(self) -> bytes: # https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe # Check how much is there still remaining in the pipe to be read and use that as the final buffer size try: - _data, buffer_size, *_rest = winapi.PeekNamedPipe(self._pipe_handle, 1) + _data, buffer_size, *_rest = winapi.PeekNamedPipe(self._pipe_handle, 1) # type: ignore except BrokenPipeError: # The pipe has already been ended return b'' diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py index 410c2d74..80a72ece 100644 --- a/src/codemagic/google_play/api_client.py +++ b/src/codemagic/google_play/api_client.py @@ -2,7 +2,6 @@ import json from functools import lru_cache from typing import AnyStr -from typing import Generator from typing import List from typing import Optional from typing import Union @@ -38,7 +37,7 @@ def __init__(self, service_account_json_keyfile: CredentialsJson): self._logger = log.get_logger(self.__class__) @classmethod - def _edit_id(cls, edit: Union[str, Edit]) -> str: + def _get_edit_id(cls, edit: Union[str, Edit]) -> str: if isinstance(edit, Edit): return edit.id return edit @@ -91,7 +90,7 @@ def create_edit(self, package_name: str) -> Edit: return Edit(**edit_response) def delete_edit(self, edit: Union[str, Edit], package_name: str) -> None: - edit_id = self._edit_id(edit) + edit_id = self._get_edit_id(edit) self._logger.debug(f'Delete the edit {edit_id!r} for the package {package_name!r}') try: delete_request = self.edits_service.delete( @@ -104,7 +103,7 @@ def delete_edit(self, edit: Union[str, Edit], package_name: str) -> None: raise EditError('delete', package_name, e) from e @contextlib.contextmanager - def use_app_edit(self, package_name: str) -> Generator[Edit, None, None]: + def use_app_edit(self, package_name: str): edit = self.create_edit(package_name) try: yield edit @@ -118,9 +117,10 @@ def get_track( edit: Optional[Union[str, Edit]] = None, ) -> Track: if edit is not None: - return self._get_track(package_name, track_name, self._edit_id(edit)) - with self.use_app_edit(package_name) as edit: - return self._get_track(package_name, track_name, edit.id) + edit_id = self._get_edit_id(edit) + return self._get_track(package_name, track_name, edit_id) + with self.use_app_edit(package_name) as _edit: + return self._get_track(package_name, track_name, _edit.id) def _get_track(self, package_name: str, track_name: str, edit_id: str) -> Track: self._logger.debug(f'Get track {track_name!r} for package {package_name!r} using edit {edit_id}') @@ -140,12 +140,13 @@ def _get_track(self, package_name: str, track_name: str, edit_id: str) -> Track: def list_tracks( self, package_name: str, - edit_id: Optional[Union[str, Edit]] = None, + edit: Optional[Union[str, Edit]] = None, ) -> List[Track]: - if edit_id is not None: - return self._list_tracks(package_name, self._edit_id(edit_id)) - with self.use_app_edit(package_name) as edit: - return self._list_tracks(package_name, edit.id) + if edit is not None: + edit_id = self._get_edit_id(edit) + return self._list_tracks(package_name, edit_id) + with self.use_app_edit(package_name) as _edit: + return self._list_tracks(package_name, _edit.id) def _list_tracks(self, package_name: str, edit_id: str) -> List[Track]: self._logger.debug(f'List tracks for package {package_name!r} using edit {edit_id}') From b6ca39a1f405f5dd82583baf9965d6e9416e8d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 15:14:55 +0300 Subject: [PATCH 05/10] Fix tests --- tests/conftest.py | 15 ++-- .../resources/test_track_resource.py | 11 ++- tests/google_play/test_api_client.py | 49 ++++++------- tests/tools/test_google_play.py | 69 ++++++++----------- 4 files changed, 58 insertions(+), 86 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8bc98231..7f0be914 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +import json import logging import os import pathlib @@ -70,8 +71,8 @@ def _appstore_api_client() -> AppStoreConnectApiClient: ) -@lru_cache() -def _google_play_api_client() -> GooglePlayDeveloperAPIClient: +@lru_cache(1) +def _google_play_api_credentials() -> dict: if 'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH' in os.environ: credentials_path = pathlib.Path(os.environ['TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH']) credentials = credentials_path.expanduser().read_text() @@ -82,7 +83,7 @@ def _google_play_api_client() -> GooglePlayDeveloperAPIClient: 'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_PATH', 'TEST_GCLOUD_SERVICE_ACCOUNT_CREDENTIALS_CONTENT', ) - return GooglePlayDeveloperAPIClient(credentials) + return json.loads(credentials) def _logger(): @@ -108,7 +109,8 @@ def app_store_api_client() -> AppStoreConnectApiClient: @pytest.fixture def google_play_api_client() -> GooglePlayDeveloperAPIClient: - return _google_play_api_client() + credentials = _google_play_api_credentials() + return GooglePlayDeveloperAPIClient(credentials) @pytest.fixture() @@ -121,11 +123,6 @@ def class_appstore_api_client(request): request.cls.api_client = _appstore_api_client() -@pytest.fixture(scope='class') -def class_google_play_api_client(request): - request.cls.api_client = _google_play_api_client() - - @pytest.fixture def logger() -> logging.Logger: return _logger() diff --git a/tests/google_play/resources/test_track_resource.py b/tests/google_play/resources/test_track_resource.py index 1e26c6fe..83fa766b 100644 --- a/tests/google_play/resources/test_track_resource.py +++ b/tests/google_play/resources/test_track_resource.py @@ -2,7 +2,6 @@ import pytest -from codemagic.google_play import VersionCodeFromTrackError from codemagic.google_play.resources import Track @@ -13,14 +12,14 @@ def test_track_initialization(api_track): def test_max_version_code(api_track): track = Track(**api_track) - assert track.max_version_code == 29 + assert track.get_max_version_code() == 29 def test_max_version_code_error_no_releases(api_track): api_track.pop('releases') track = Track(**api_track) - with pytest.raises(VersionCodeFromTrackError) as e: - track.max_version_code + with pytest.raises(ValueError) as e: + track.get_max_version_code() assert 'No release information' in str(e.value) @@ -28,6 +27,6 @@ def test_max_version_code_error_no_version_codes(api_track): for release in api_track['releases']: release.pop('versionCodes') track = Track(**api_track) - with pytest.raises(VersionCodeFromTrackError) as e: - track.max_version_code + with pytest.raises(ValueError) as e: + track.get_max_version_code() assert 'No releases with uploaded App bundles or APKs' in str(e.value) diff --git a/tests/google_play/test_api_client.py b/tests/google_play/test_api_client.py index c804f9f0..a1f74a8b 100644 --- a/tests/google_play/test_api_client.py +++ b/tests/google_play/test_api_client.py @@ -1,39 +1,30 @@ import os -import pathlib -import unittest import pytest from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient -from codemagic.google_play.resources import TrackName @pytest.mark.skipif(not os.environ.get('RUN_LIVE_API_TESTS'), reason='Live Google Play Developer API access') -@pytest.mark.usefixtures('class_google_play_api_client') -class ApiTests(unittest.TestCase): - api_client: GooglePlayDeveloperAPIClient +def test_list_tracks(google_play_api_client: GooglePlayDeveloperAPIClient): + tracks = google_play_api_client.list_tracks(package_name='io.codemagic.artemii.capybara') + found_track_names = {t.track for t in tracks} + assert found_track_names == {'production', 'beta', 'alpha', 'internal'} - def _service_exists(self): - return self.api_client._service_instance is not None - def test_get_edit_and_track_information(self): - assert not self._service_exists() - edit = self.api_client.create_edit() - assert self._service_exists() - assert edit.id is not None - assert edit.expiryTimeSeconds is not None - - track = self.api_client.get_track_information(edit.id, TrackName.INTERNAL) - assert track.releases - for release in track.releases: - assert release.versionCodes - - self.api_client.delete_edit(edit.id) - assert not self._service_exists() - - -def test_google_play_api_client(): - credentials = pathlib.Path('~/google_play_service_account_credentials.json').expanduser().read_text() - client = GooglePlayDeveloperAPIClient(credentials, package_name='io.codemagic.artemii.capybara') - print() - print(client) +@pytest.mark.skipif(not os.environ.get('RUN_LIVE_API_TESTS'), reason='Live Google Play Developer API access') +def test_get_track(google_play_api_client: GooglePlayDeveloperAPIClient): + track = google_play_api_client.get_track( + package_name='io.codemagic.artemii.capybara', + track_name='alpha', + ) + assert track.dict() == { + 'releases': [ + { + 'name': '65 (0.0.42)', + 'status': 'completed', + 'versionCodes': ['65'], + }, + ], + 'track': 'alpha', + } diff --git a/tests/tools/test_google_play.py b/tests/tools/test_google_play.py index d3a8cda8..5ea4619f 100644 --- a/tests/tools/test_google_play.py +++ b/tests/tools/test_google_play.py @@ -9,7 +9,6 @@ from codemagic.tools.google_play import GooglePlay from codemagic.tools.google_play import GooglePlayArgument -from codemagic.tools.google_play import TracksArgument from codemagic.tools.google_play import Types credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS @@ -33,29 +32,18 @@ def namespace_kwargs(): return ns_kwargs -def _test_missing_argument(argument, _namespace_kwargs): - cli_args = argparse.Namespace(**dict(_namespace_kwargs.items())) +def test_missing_credentials_arg(namespace_kwargs): + namespace_kwargs[credentials_argument.key] = None + cli_args = argparse.Namespace(**dict(namespace_kwargs.items())) + with pytest.raises(argparse.ArgumentError) as exception_info: GooglePlay.from_cli_args(cli_args) - message = str(exception_info.value) - assert argument.key.upper() in message - if hasattr(argument.type, 'environment_variable_key'): - assert argument.type.environment_variable_key in message - assert ','.join(argument.flags) in message - - -@pytest.mark.parametrize('argument', [ - credentials_argument, - TracksArgument.PACKAGE_NAME, -]) -def test_missing_arg(cli_argument_group, namespace_kwargs, argument): - namespace_kwargs[argument.key] = None - _test_missing_argument(argument, namespace_kwargs) - -def test_missing_creedentials_arg(namespace_kwargs): - namespace_kwargs[credentials_argument.key] = None - _test_missing_argument(credentials_argument, namespace_kwargs) + message = str(exception_info.value) + assert credentials_argument.key.upper() in message + if hasattr(credentials_argument.type, 'environment_variable_key'): + assert credentials_argument.type.environment_variable_key in message + assert ','.join(credentials_argument.flags) in message def test_invalid_credentials_from_env(namespace_kwargs): @@ -78,47 +66,44 @@ def test_credentials_invalid_path(namespace_kwargs): @mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient') def test_read_private_key(mock_google_play_api_client, namespace_kwargs): - credentials = '{"type":"service_account"}' - namespace_kwargs[credentials_argument.key] = Types.CredentialsArgument(credentials) - _do_credentials_assertions(credentials, mock_google_play_api_client, namespace_kwargs) + namespace_kwargs[credentials_argument.key] = Types.CredentialsArgument('{"type":"service_account"}') + _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs)) + mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}') @pytest.mark.parametrize('configure_variable', [ lambda filename, ns_kwargs: os.environ.update( - {credentials_argument.type.environment_variable_key: f'@file:{filename}'}), + {credentials_argument.type.environment_variable_key: f'@file:{filename}'}, + ), lambda filename, ns_kwargs: ns_kwargs.update( - {credentials_argument.key: Types.CredentialsArgument(f'@file:{filename}')}), + {credentials_argument.key: Types.CredentialsArgument(f'@file:{filename}')}, + ), ]) @mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient') def test_private_key_path_arg(mock_google_play_api_client, configure_variable, namespace_kwargs): - credentials = '{"type":"service_account"}' with NamedTemporaryFile(mode='w') as tf: - tf.write(credentials) + tf.write('{"type":"service_account"}') tf.flush() namespace_kwargs[credentials_argument.key] = None configure_variable(tf.name, namespace_kwargs) - _do_credentials_assertions(credentials, mock_google_play_api_client, namespace_kwargs) + + _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs)) + mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}') @pytest.mark.parametrize('configure_variable', [ lambda ns_kwargs: os.environ.update( - {credentials_argument.type.environment_variable_key: '@env:CREDENTIALS'}), + {credentials_argument.type.environment_variable_key: '@env:CREDENTIALS'}, + ), lambda ns_kwargs: ns_kwargs.update( - {credentials_argument.key: Types.CredentialsArgument('@env:CREDENTIALS')}), + {credentials_argument.key: Types.CredentialsArgument('@env:CREDENTIALS')}, + ), ]) @mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient') def test_private_key_env_arg(mock_google_play_api_client, configure_variable, namespace_kwargs): - credentials = '{"type":"service_account"}' - os.environ['CREDENTIALS'] = credentials + os.environ['CREDENTIALS'] = '{"type":"service_account"}' namespace_kwargs[credentials_argument.key] = None configure_variable(namespace_kwargs) - _do_credentials_assertions(credentials, mock_google_play_api_client, namespace_kwargs) - -def _do_credentials_assertions(credentials_value, moc_google_play_api_client, cli_namespace): - cli_args = argparse.Namespace(**dict(cli_namespace.items())) - _ = GooglePlay.from_cli_args(cli_args) - credentials_arg, _, _ = moc_google_play_api_client.call_args[0] - print(type(credentials_arg)) - assert isinstance(credentials_arg, str) - assert credentials_arg == credentials_value + _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs)) + mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}') From 97d7f92e715fbda69ecdeffd3a6a64da42c10464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 16:47:52 +0300 Subject: [PATCH 06/10] Split google-play tool implementation into separate action classes --- src/codemagic/google_play/resources/track.py | 6 +- src/codemagic/tools/google_play.py | 226 ------------------ src/codemagic/tools/google_play/__init__.py | 1 + src/codemagic/tools/google_play/__main__.py | 3 + .../google_play/action_groups/__init__.py | 1 + .../google_play_action_groups.py | 8 + .../action_groups/tracks_action_group.py | 73 ++++++ .../tools/google_play/actions/__init__.py | 1 + .../actions/get_latest_build_number_action.py | 101 ++++++++ .../tools/google_play/argument_types.py | 20 ++ src/codemagic/tools/google_play/arguments.py | 55 +++++ src/codemagic/tools/google_play/errors.py | 5 + .../tools/google_play/google_play.py | 38 +++ .../google_play/google_play_base_action.py | 55 +++++ .../resources/test_track_resource.py | 4 +- tests/tools/test_google_play.py | 22 +- 16 files changed, 377 insertions(+), 242 deletions(-) delete mode 100644 src/codemagic/tools/google_play.py create mode 100644 src/codemagic/tools/google_play/__init__.py create mode 100644 src/codemagic/tools/google_play/__main__.py create mode 100644 src/codemagic/tools/google_play/action_groups/__init__.py create mode 100644 src/codemagic/tools/google_play/action_groups/google_play_action_groups.py create mode 100644 src/codemagic/tools/google_play/action_groups/tracks_action_group.py create mode 100644 src/codemagic/tools/google_play/actions/__init__.py create mode 100644 src/codemagic/tools/google_play/actions/get_latest_build_number_action.py create mode 100644 src/codemagic/tools/google_play/argument_types.py create mode 100644 src/codemagic/tools/google_play/arguments.py create mode 100644 src/codemagic/tools/google_play/errors.py create mode 100644 src/codemagic/tools/google_play/google_play.py create mode 100644 src/codemagic/tools/google_play/google_play_base_action.py diff --git a/src/codemagic/google_play/resources/track.py b/src/codemagic/google_play/resources/track.py index d23b9891..da91334b 100644 --- a/src/codemagic/google_play/resources/track.py +++ b/src/codemagic/google_play/resources/track.py @@ -76,10 +76,10 @@ def __post_init__(self): self.releases = [Release(**release) for release in self.releases] def get_max_version_code(self) -> int: - error_prefix = f'Failed to get version code from Google Play from {self.track} track' + error_prefix = f'Failed to get version code from "{self.track}" track' if not self.releases: - raise ValueError(f'{error_prefix}. No release information') + raise ValueError(f'{error_prefix}: track has no releases') version_codes = [release.versionCodes for release in self.releases if release.versionCodes] if not version_codes: - raise ValueError(f'{error_prefix}. No releases with uploaded App bundles or APKs') + raise ValueError(f'{error_prefix}: releases with version code do not exist') return max(map(int, chain(*version_codes))) diff --git a/src/codemagic/tools/google_play.py b/src/codemagic/tools/google_play.py deleted file mode 100644 index feef2ee2..00000000 --- a/src/codemagic/tools/google_play.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import argparse -import json -from typing import Dict -from typing import List -from typing import Optional -from typing import Sequence - -from codemagic import cli -from codemagic.google_play import GooglePlayDeveloperAPIClientError -from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient -from codemagic.google_play.resources import Track - - -class Types: - class PackageName(str): - pass - - class CredentialsArgument(cli.EnvironmentArgumentValue[str]): - environment_variable_key = 'GCLOUD_SERVICE_ACCOUNT_CREDENTIALS' - - @classmethod - def _is_valid(cls, value: str) -> bool: - try: - json_content = json.loads(value) - except json.decoder.JSONDecodeError: - return False - else: - return json_content.get('type') == 'service_account' - - -class GooglePlayArgument(cli.Argument): - GCLOUD_SERVICE_ACCOUNT_CREDENTIALS = cli.ArgumentProperties( - key='credentials', - flags=('--credentials',), - type=Types.CredentialsArgument, - description=( - 'Gcloud service account credentials with `JSON` key type ' - 'to access Google Play Developer API' - ), - argparse_kwargs={'required': False}, - ) - JSON_OUTPUT = cli.ArgumentProperties( - key='json_output', - flags=('--json',), - type=bool, - description='Whether to show the request response in JSON format', - argparse_kwargs={'required': False, 'action': 'store_true'}, - ) - - -class TracksArgument(cli.Argument): - PACKAGE_NAME = cli.ArgumentProperties( - key='package_name', - flags=('-p', '--package-name'), - type=Types.PackageName, - description='Package name of the app in Google Play Console (Ex: com.google.example)', - argparse_kwargs={'required': True}, - ) - TRACK_NAME = cli.ArgumentProperties( - key='track_name', - flags=('-t', '--track'), - description='Release track name. For example `alpha` or `production`', - argparse_kwargs={'required': True}, - ) - - -class BuildNumberArgument(cli.Argument): - TRACKS = cli.ArgumentProperties( - key='tracks', - flags=('-t', '--tracks'), - description=( - 'Get the build number from the specified track(s). ' - 'If not specified, the highest build number across all tracks is returned' - ), - argparse_kwargs={ - 'required': False, - 'nargs': '+', - }, - ) - - -class GooglePlayError(cli.CliAppException): - pass - - -class GooglePlayActionGroups(cli.ActionGroup): - TRACKS = cli.ActionGroupProperties( - name='tracks', - description='Manage your Google Play release tracks', - ) - - -@cli.common_arguments(GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS) -class GooglePlay(cli.CliApp): - """ - Utility to get the latest build numbers from Google Play using Google Play Developer API - """ - - def __init__( - self, - credentials: str, - **kwargs, - ): - super().__init__(**kwargs) - self.credentials = credentials - self.api_client = GooglePlayDeveloperAPIClient(credentials) - - @classmethod - def from_cli_args(cls, cli_args: argparse.Namespace) -> GooglePlay: - credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.from_args(cli_args) - if credentials_argument is None: - raise GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.raise_argument_error() - - return GooglePlay( - credentials=credentials_argument.value, - **cls._parent_class_kwargs(cli_args), - ) - - @cli.action( - 'get-latest-build-number', - TracksArgument.PACKAGE_NAME, - BuildNumberArgument.TRACKS, - ) - def get_latest_build_number( - self, - package_name: str, - tracks: Optional[Sequence[str]] = None, - ) -> Optional[int]: - """ - Get latest build number from Google Play Developer API matching given constraints - """ - - package_tracks = self.list_tracks(package_name, should_print=False) - track_version_codes: Dict[str, int] = {} - - for track in package_tracks: - track_name = track.track - if tracks and track_name not in tracks: - continue - try: - version_code = track.get_max_version_code() - except ValueError as ve: - self.logger.warning(ve) - else: - self.logger.info(f'Latest version code for {track_name} track: {version_code}') - track_version_codes[track_name] = version_code - - if not track_version_codes: - raise GooglePlayError('Version code info is missing from all tracks') - - missing_track_names = set(tracks or []).difference(track_version_codes.keys()) - for missing_track_name in missing_track_names: - self.logger.warning(( - f'Failed to get version code from Google Play for track "{missing_track_name}". ' - 'Track was not found.' - )) - - latest_build_number = max(track_version_codes.values()) - self.echo(str(latest_build_number)) - return latest_build_number - - @cli.action( - 'get', - TracksArgument.PACKAGE_NAME, - TracksArgument.TRACK_NAME, - GooglePlayArgument.JSON_OUTPUT, - action_group=GooglePlayActionGroups.TRACKS, - ) - def get_track( - self, - package_name: str, - track_name: str, - json_output: bool = False, - should_print: bool = True, - ) -> Track: - """ - Get information about specified track from Google Play Developer API - """ - - try: - track = self.api_client.get_track(package_name, track_name) - except GooglePlayDeveloperAPIClientError as api_error: - raise GooglePlayError(str(api_error)) - - if should_print: - self.logger.info(track.json() if json_output else str(track)) - - return track - - @cli.action( - 'list', - TracksArgument.PACKAGE_NAME, - GooglePlayArgument.JSON_OUTPUT, - action_group=GooglePlayActionGroups.TRACKS, - ) - def list_tracks( - self, - package_name: str, - json_output: bool = False, - should_print: bool = True, - ) -> List[Track]: - """ - Get information about specified track from Google Play Developer API - """ - - try: - tracks = self.api_client.list_tracks(package_name) - except GooglePlayDeveloperAPIClientError as api_error: - raise GooglePlayError(str(api_error)) - - if should_print: - if json_output: - self.echo(json.dumps([t.dict() for t in tracks], indent=4)) - else: - for track in tracks: - self.echo(str(track)) - - return tracks - - -if __name__ == '__main__': - GooglePlay.invoke_cli() diff --git a/src/codemagic/tools/google_play/__init__.py b/src/codemagic/tools/google_play/__init__.py new file mode 100644 index 00000000..5f507dfb --- /dev/null +++ b/src/codemagic/tools/google_play/__init__.py @@ -0,0 +1 @@ +from .google_play import GooglePlay diff --git a/src/codemagic/tools/google_play/__main__.py b/src/codemagic/tools/google_play/__main__.py new file mode 100644 index 00000000..d8cee147 --- /dev/null +++ b/src/codemagic/tools/google_play/__main__.py @@ -0,0 +1,3 @@ +if __name__ == '__main__': + from .google_play import GooglePlay + GooglePlay.invoke_cli() diff --git a/src/codemagic/tools/google_play/action_groups/__init__.py b/src/codemagic/tools/google_play/action_groups/__init__.py new file mode 100644 index 00000000..1d283ddd --- /dev/null +++ b/src/codemagic/tools/google_play/action_groups/__init__.py @@ -0,0 +1 @@ +from .tracks_action_group import TracksActionGroup diff --git a/src/codemagic/tools/google_play/action_groups/google_play_action_groups.py b/src/codemagic/tools/google_play/action_groups/google_play_action_groups.py new file mode 100644 index 00000000..18dc048a --- /dev/null +++ b/src/codemagic/tools/google_play/action_groups/google_play_action_groups.py @@ -0,0 +1,8 @@ +from codemagic import cli + + +class GooglePlayActionGroups(cli.ActionGroup): + TRACKS = cli.ActionGroupProperties( + name='tracks', + description='Manage your Google Play release tracks', + ) diff --git a/src/codemagic/tools/google_play/action_groups/tracks_action_group.py b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py new file mode 100644 index 00000000..72a24b77 --- /dev/null +++ b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py @@ -0,0 +1,73 @@ +import json +from abc import ABCMeta +from typing import List + +from codemagic import cli +from codemagic.google_play import GooglePlayDeveloperAPIClientError +from codemagic.google_play.resources import Track + +from ..arguments import GooglePlayArgument +from ..arguments import TracksArgument +from ..errors import GooglePlayError +from ..google_play_base_action import GooglePlayBaseAction +from .google_play_action_groups import GooglePlayActionGroups + + +class TracksActionGroup(GooglePlayBaseAction, metaclass=ABCMeta): + @cli.action( + 'get', + TracksArgument.PACKAGE_NAME, + TracksArgument.TRACK_NAME, + GooglePlayArgument.JSON_OUTPUT, + action_group=GooglePlayActionGroups.TRACKS, + ) + def get_track( + self, + package_name: str, + track_name: str, + json_output: bool = False, + should_print: bool = True, + ) -> Track: + """ + Get information about specified track from Google Play Developer API + """ + + try: + track = self.api_client.get_track(package_name, track_name) + except GooglePlayDeveloperAPIClientError as api_error: + raise GooglePlayError(str(api_error)) + + if should_print: + self.logger.info(track.json() if json_output else str(track)) + + return track + + @cli.action( + 'list', + TracksArgument.PACKAGE_NAME, + GooglePlayArgument.JSON_OUTPUT, + action_group=GooglePlayActionGroups.TRACKS, + ) + def list_tracks( + self, + package_name: str, + json_output: bool = False, + should_print: bool = True, + ) -> List[Track]: + """ + Get information about specified track from Google Play Developer API + """ + + try: + tracks = self.api_client.list_tracks(package_name) + except GooglePlayDeveloperAPIClientError as api_error: + raise GooglePlayError(str(api_error)) + + if should_print: + if json_output: + self.echo(json.dumps([t.dict() for t in tracks], indent=4)) + else: + for track in tracks: + self.echo(str(track)) + + return tracks diff --git a/src/codemagic/tools/google_play/actions/__init__.py b/src/codemagic/tools/google_play/actions/__init__.py new file mode 100644 index 00000000..9597525d --- /dev/null +++ b/src/codemagic/tools/google_play/actions/__init__.py @@ -0,0 +1 @@ +from .get_latest_build_number_action import GetLatestBuildNumberAction diff --git a/src/codemagic/tools/google_play/actions/get_latest_build_number_action.py b/src/codemagic/tools/google_play/actions/get_latest_build_number_action.py new file mode 100644 index 00000000..f74e00dc --- /dev/null +++ b/src/codemagic/tools/google_play/actions/get_latest_build_number_action.py @@ -0,0 +1,101 @@ +from abc import ABCMeta +from typing import Dict +from typing import Optional +from typing import Sequence + +from codemagic import cli +from codemagic.cli import Colors +from codemagic.google_play.resources import Track + +from ..arguments import LatestBuildNumberArgument +from ..arguments import TracksArgument +from ..errors import GooglePlayError +from ..google_play_base_action import GooglePlayBaseAction + + +class GetLatestBuildNumberAction(GooglePlayBaseAction, metaclass=ABCMeta): + @cli.action( + 'get-latest-build-number', + TracksArgument.PACKAGE_NAME, + LatestBuildNumberArgument.TRACKS, + ) + def get_latest_build_number( + self, + package_name: str, + tracks: Optional[Sequence[str]] = None, + ) -> Optional[int]: + """ + Get latest build number from Google Play Developer API matching given constraints + """ + + requested_track_names = tuple(tracks or []) + self._log_get_package_action_started(package_name, requested_track_names) + package_tracks = self.list_tracks(package_name, should_print=False) + track_version_codes = self._get_track_version_codes(requested_track_names, package_tracks) + self._show_missing_tracks_warnings(requested_track_names, tuple(track_version_codes.keys())) + latest_build_number = self._get_latest_build_number(package_name, requested_track_names, track_version_codes) + + self.echo(str(latest_build_number)) + return latest_build_number + + def _log_get_package_action_started(self, package_name: str, requested_tracks: Sequence[str]): + if not requested_tracks: + message = f'Get package "{package_name}" latest build number across all tracks' + elif len(requested_tracks) == 1: + message = f'Get package "{package_name}" latest build number from track "{requested_tracks[0]}"' + else: + formatted_specified_tracks = ', '.join(f'"{track}"' for track in requested_tracks) + message = f'Get package "{package_name}" latest build number from tracks {formatted_specified_tracks}' + self.logger.info(Colors.BLUE(message)) + + def _get_track_version_codes( + self, + requested_tracks: Sequence[str], + package_tracks: Sequence[Track], + ) -> Dict[str, Optional[int]]: + track_version_codes: Dict[str, Optional[int]] = {} + + for track in package_tracks: + track_name = track.track + if requested_tracks and track_name not in requested_tracks: + continue + + try: + version_code = track.get_max_version_code() + except ValueError as ve: + self.logger.warning(ve) + track_version_codes[track_name] = None + else: + self.logger.info(f'Found latest version code from "{track_name}" track: {version_code}') + track_version_codes[track_name] = version_code + + return track_version_codes + + def _show_missing_tracks_warnings(self, requested_tracks: Sequence[str], found_tracks: Sequence[str]): + missing_track_names = set(requested_tracks).difference(found_tracks) + for missing_track_name in missing_track_names: + message = f'Failed to get version code from track "{missing_track_name}": track was not found' + self.logger.warning(Colors.YELLOW(message)) + + def _get_latest_build_number( + self, + package_name: str, + requested_tracks: Sequence[str], + track_version_codes: Dict[str, Optional[int]], + ) -> int: + version_codes = [version_code for version_code in track_version_codes.values() if version_code is not None] + if not version_codes: + error = self._get_missing_version_error(package_name, requested_tracks) + raise GooglePlayError(error) + return max(version_codes) + + @classmethod + def _get_missing_version_error(cls, package_name: str, requested_tracks: Sequence[str]) -> str: + if not requested_tracks: + return f'Version code info is missing from all tracks for package "{package_name}' + elif len(requested_tracks) == 1: + _track = requested_tracks[0] + return f'Version code info is missing from track "{_track}" for package "{package_name}' + else: + _tracks = ', '.join(f'"{track}"' for track in requested_tracks) + return f'Version code info is missing from tracks {_tracks} for package "{package_name}' diff --git a/src/codemagic/tools/google_play/argument_types.py b/src/codemagic/tools/google_play/argument_types.py new file mode 100644 index 00000000..515c4a40 --- /dev/null +++ b/src/codemagic/tools/google_play/argument_types.py @@ -0,0 +1,20 @@ +import json + +from codemagic import cli + + +class PackageName(str): + pass + + +class CredentialsArgument(cli.EnvironmentArgumentValue[str]): + environment_variable_key = 'GCLOUD_SERVICE_ACCOUNT_CREDENTIALS' + + @classmethod + def _is_valid(cls, value: str) -> bool: + try: + json_content = json.loads(value) + except json.decoder.JSONDecodeError: + return False + else: + return json_content.get('type') == 'service_account' diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py new file mode 100644 index 00000000..42e9558d --- /dev/null +++ b/src/codemagic/tools/google_play/arguments.py @@ -0,0 +1,55 @@ +from codemagic import cli + +from .argument_types import CredentialsArgument +from .argument_types import PackageName + + +class GooglePlayArgument(cli.Argument): + GCLOUD_SERVICE_ACCOUNT_CREDENTIALS = cli.ArgumentProperties( + key='credentials', + flags=('--credentials',), + type=CredentialsArgument, + description=( + 'Gcloud service account credentials with `JSON` key type ' + 'to access Google Play Developer API' + ), + argparse_kwargs={'required': False}, + ) + JSON_OUTPUT = cli.ArgumentProperties( + key='json_output', + flags=('--json',), + type=bool, + description='Whether to show the request response in JSON format', + argparse_kwargs={'required': False, 'action': 'store_true'}, + ) + + +class TracksArgument(cli.Argument): + PACKAGE_NAME = cli.ArgumentProperties( + key='package_name', + flags=('-p', '--package-name'), + type=PackageName, + description='Package name of the app in Google Play Console (Ex: com.google.example)', + argparse_kwargs={'required': True}, + ) + TRACK_NAME = cli.ArgumentProperties( + key='track_name', + flags=('-t', '--track'), + description='Release track name. For example `alpha` or `production`', + argparse_kwargs={'required': True}, + ) + + +class LatestBuildNumberArgument(cli.Argument): + TRACKS = cli.ArgumentProperties( + key='tracks', + flags=('-t', '--tracks'), + description=( + 'Get the build number from the specified track(s). ' + 'If not specified, the highest build number across all tracks is returned' + ), + argparse_kwargs={ + 'required': False, + 'nargs': '+', + }, + ) diff --git a/src/codemagic/tools/google_play/errors.py b/src/codemagic/tools/google_play/errors.py new file mode 100644 index 00000000..c2df5d4d --- /dev/null +++ b/src/codemagic/tools/google_play/errors.py @@ -0,0 +1,5 @@ +from codemagic import cli + + +class GooglePlayError(cli.CliAppException): + pass diff --git a/src/codemagic/tools/google_play/google_play.py b/src/codemagic/tools/google_play/google_play.py new file mode 100644 index 00000000..184cb8f4 --- /dev/null +++ b/src/codemagic/tools/google_play/google_play.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse + +from codemagic import cli +from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient + +from .action_groups import TracksActionGroup +from .actions import GetLatestBuildNumberAction +from .arguments import GooglePlayArgument + + +@cli.common_arguments(GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS) +class GooglePlay( + cli.CliApp, + GetLatestBuildNumberAction, + TracksActionGroup, +): + """ + Utility to get the latest build numbers from Google Play using Google Play Developer API + """ + + def __init__(self, credentials: str, **kwargs): + super().__init__(**kwargs) + self.api_client = GooglePlayDeveloperAPIClient(credentials) + + @classmethod + def from_cli_args(cls, cli_args: argparse.Namespace) -> GooglePlay: + credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.from_args(cli_args) + if credentials_argument is None: + raise GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS.raise_argument_error() + + return GooglePlay( + credentials=credentials_argument.value, + **cls._parent_class_kwargs(cli_args), + ) diff --git a/src/codemagic/tools/google_play/google_play_base_action.py b/src/codemagic/tools/google_play/google_play_base_action.py new file mode 100644 index 00000000..87713ad7 --- /dev/null +++ b/src/codemagic/tools/google_play/google_play_base_action.py @@ -0,0 +1,55 @@ +import logging +from abc import ABCMeta +from abc import abstractmethod +from typing import List +from typing import Optional +from typing import Sequence + +from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient +from codemagic.google_play.resources import Track + + +class GooglePlayBaseAction(metaclass=ABCMeta): + api_client: GooglePlayDeveloperAPIClient + logger: logging.Logger + + # Define signatures for self-reference to other action groups + + @classmethod + def echo(cls, message: str, *args, **kwargs) -> None: + ... + + # Action signatures in alphabetical order + + @abstractmethod + def get_latest_build_number( + self, + package_name: str, + tracks: Optional[Sequence[str]] = None, + ) -> Optional[int]: + from .actions import GetLatestBuildNumberAction + _ = GetLatestBuildNumberAction.get_latest_build_number # Implementation + raise NotImplementedError() + + @abstractmethod + def get_track( + self, + package_name: str, + track_name: str, + json_output: bool = False, + should_print: bool = True, + ) -> Track: + from .action_groups import TracksActionGroup + _ = TracksActionGroup.get_track # Implementation + raise NotImplementedError + + @abstractmethod + def list_tracks( + self, + package_name: str, + json_output: bool = False, + should_print: bool = True, + ) -> List[Track]: + from .action_groups import TracksActionGroup + _ = TracksActionGroup.list_tracks # Implementation + raise NotImplementedError() diff --git a/tests/google_play/resources/test_track_resource.py b/tests/google_play/resources/test_track_resource.py index 83fa766b..106dcf59 100644 --- a/tests/google_play/resources/test_track_resource.py +++ b/tests/google_play/resources/test_track_resource.py @@ -20,7 +20,7 @@ def test_max_version_code_error_no_releases(api_track): track = Track(**api_track) with pytest.raises(ValueError) as e: track.get_max_version_code() - assert 'No release information' in str(e.value) + assert str(e.value) == 'Failed to get version code from "internal" track: track has no releases' def test_max_version_code_error_no_version_codes(api_track): @@ -29,4 +29,4 @@ def test_max_version_code_error_no_version_codes(api_track): track = Track(**api_track) with pytest.raises(ValueError) as e: track.get_max_version_code() - assert 'No releases with uploaded App bundles or APKs' in str(e.value) + assert str(e.value) == 'Failed to get version code from "internal" track: releases with version code do not exist' diff --git a/tests/tools/test_google_play.py b/tests/tools/test_google_play.py index 5ea4619f..3b080773 100644 --- a/tests/tools/test_google_play.py +++ b/tests/tools/test_google_play.py @@ -8,8 +8,8 @@ import pytest from codemagic.tools.google_play import GooglePlay -from codemagic.tools.google_play import GooglePlayArgument -from codemagic.tools.google_play import Types +from codemagic.tools.google_play.argument_types import CredentialsArgument +from codemagic.tools.google_play.arguments import GooglePlayArgument credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS @@ -23,7 +23,7 @@ def register_args(cli_argument_group): @pytest.fixture() def namespace_kwargs(): ns_kwargs = { - credentials_argument.key: Types.CredentialsArgument('{"type":"service_account"}'), + credentials_argument.key: CredentialsArgument('{"type":"service_account"}'), } for arg in GooglePlay.CLASS_ARGUMENTS: if not hasattr(arg.type, 'environment_variable_key'): @@ -47,7 +47,7 @@ def test_missing_credentials_arg(namespace_kwargs): def test_invalid_credentials_from_env(namespace_kwargs): - os.environ[Types.CredentialsArgument.environment_variable_key] = 'invalid credentials' + os.environ[CredentialsArgument.environment_variable_key] = 'invalid credentials' namespace_kwargs[credentials_argument.key] = None cli_args = argparse.Namespace(**dict(namespace_kwargs.items())) with pytest.raises(argparse.ArgumentError) as exception_info: @@ -56,7 +56,7 @@ def test_invalid_credentials_from_env(namespace_kwargs): def test_credentials_invalid_path(namespace_kwargs): - os.environ[Types.CredentialsArgument.environment_variable_key] = '@file:this-is-not-a-file' + os.environ[CredentialsArgument.environment_variable_key] = '@file:this-is-not-a-file' namespace_kwargs[credentials_argument.key] = None cli_args = argparse.Namespace(**dict(namespace_kwargs.items())) with pytest.raises(argparse.ArgumentError) as exception_info: @@ -64,9 +64,9 @@ def test_credentials_invalid_path(namespace_kwargs): assert str(exception_info.value) == 'argument --credentials: File "this-is-not-a-file" does not exist' -@mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient') +@mock.patch('codemagic.tools.google_play.google_play.GooglePlayDeveloperAPIClient') def test_read_private_key(mock_google_play_api_client, namespace_kwargs): - namespace_kwargs[credentials_argument.key] = Types.CredentialsArgument('{"type":"service_account"}') + namespace_kwargs[credentials_argument.key] = CredentialsArgument('{"type":"service_account"}') _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs)) mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}') @@ -76,10 +76,10 @@ def test_read_private_key(mock_google_play_api_client, namespace_kwargs): {credentials_argument.type.environment_variable_key: f'@file:{filename}'}, ), lambda filename, ns_kwargs: ns_kwargs.update( - {credentials_argument.key: Types.CredentialsArgument(f'@file:{filename}')}, + {credentials_argument.key: CredentialsArgument(f'@file:{filename}')}, ), ]) -@mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient') +@mock.patch('codemagic.tools.google_play.google_play.GooglePlayDeveloperAPIClient') def test_private_key_path_arg(mock_google_play_api_client, configure_variable, namespace_kwargs): with NamedTemporaryFile(mode='w') as tf: tf.write('{"type":"service_account"}') @@ -96,10 +96,10 @@ def test_private_key_path_arg(mock_google_play_api_client, configure_variable, n {credentials_argument.type.environment_variable_key: '@env:CREDENTIALS'}, ), lambda ns_kwargs: ns_kwargs.update( - {credentials_argument.key: Types.CredentialsArgument('@env:CREDENTIALS')}, + {credentials_argument.key: CredentialsArgument('@env:CREDENTIALS')}, ), ]) -@mock.patch('codemagic.tools.google_play.GooglePlayDeveloperAPIClient') +@mock.patch('codemagic.tools.google_play.google_play.GooglePlayDeveloperAPIClient') def test_private_key_env_arg(mock_google_play_api_client, configure_variable, namespace_kwargs): os.environ['CREDENTIALS'] = '{"type":"service_account"}' namespace_kwargs[credentials_argument.key] = None From eb1ec797d04219d49d8c4a7d6357dcbd36aad5bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 16:51:01 +0300 Subject: [PATCH 07/10] Add direct source file invocation hook --- src/codemagic/tools/google_play/google_play.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/codemagic/tools/google_play/google_play.py b/src/codemagic/tools/google_play/google_play.py index 184cb8f4..3bd1ddff 100644 --- a/src/codemagic/tools/google_play/google_play.py +++ b/src/codemagic/tools/google_play/google_play.py @@ -36,3 +36,7 @@ def from_cli_args(cls, cli_args: argparse.Namespace) -> GooglePlay: credentials=credentials_argument.value, **cls._parent_class_kwargs(cli_args), ) + + +if __name__ == '__main__': + GooglePlay.invoke_cli() From 50b88072a817306633f991bf76f62e2bc058b01e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Apr 2022 16:58:30 +0300 Subject: [PATCH 08/10] Reforamt CLI args --- src/codemagic/tools/google_play/arguments.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index 42e9558d..efbeb910 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -17,7 +17,7 @@ class GooglePlayArgument(cli.Argument): ) JSON_OUTPUT = cli.ArgumentProperties( key='json_output', - flags=('--json',), + flags=('--json', '-j'), type=bool, description='Whether to show the request response in JSON format', argparse_kwargs={'required': False, 'action': 'store_true'}, @@ -27,14 +27,14 @@ class GooglePlayArgument(cli.Argument): class TracksArgument(cli.Argument): PACKAGE_NAME = cli.ArgumentProperties( key='package_name', - flags=('-p', '--package-name'), + flags=('--package-name', '-p'), type=PackageName, - description='Package name of the app in Google Play Console (Ex: com.google.example)', + description='Package name of the app in Google Play Console. For example `com.example.app`', argparse_kwargs={'required': True}, ) TRACK_NAME = cli.ArgumentProperties( key='track_name', - flags=('-t', '--track'), + flags=('--track', '-t'), description='Release track name. For example `alpha` or `production`', argparse_kwargs={'required': True}, ) @@ -43,7 +43,7 @@ class TracksArgument(cli.Argument): class LatestBuildNumberArgument(cli.Argument): TRACKS = cli.ArgumentProperties( key='tracks', - flags=('-t', '--tracks'), + flags=('--tracks', '-t'), description=( 'Get the build number from the specified track(s). ' 'If not specified, the highest build number across all tracks is returned' From 68f554b1593823ebe563b07ed17f9ba9fd61aed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Tue, 12 Apr 2022 12:45:02 +0300 Subject: [PATCH 09/10] Add tests for google-play actions --- .../tools/google_play/google_play.py | 3 +- tests/tools/mocks/google_play_tracks.json | 46 +++++++++++ tests/tools/test_google_play.py | 79 +++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/tools/mocks/google_play_tracks.json diff --git a/src/codemagic/tools/google_play/google_play.py b/src/codemagic/tools/google_play/google_play.py index 3bd1ddff..bcba8dfb 100644 --- a/src/codemagic/tools/google_play/google_play.py +++ b/src/codemagic/tools/google_play/google_play.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +from typing import Union from codemagic import cli from codemagic.google_play.api_client import GooglePlayDeveloperAPIClient @@ -22,7 +23,7 @@ class GooglePlay( Utility to get the latest build numbers from Google Play using Google Play Developer API """ - def __init__(self, credentials: str, **kwargs): + def __init__(self, credentials: Union[str, dict], **kwargs): super().__init__(**kwargs) self.api_client = GooglePlayDeveloperAPIClient(credentials) diff --git a/tests/tools/mocks/google_play_tracks.json b/tests/tools/mocks/google_play_tracks.json new file mode 100644 index 00000000..21e8541b --- /dev/null +++ b/tests/tools/mocks/google_play_tracks.json @@ -0,0 +1,46 @@ +[ + { + "track": "production", + "releases": [ + { + "status": "draft" + } + ] + }, + { + "track": "beta", + "releases": [ + { + "status": "completed", + "name": "66 (0.0.42)", + "versionCodes": [ + "66" + ] + } + ] + }, + { + "track": "alpha", + "releases": [ + { + "status": "completed", + "name": "65 (0.0.42)", + "versionCodes": [ + "65" + ] + } + ] + }, + { + "track": "internal", + "releases": [ + { + "status": "completed", + "name": "67 (0.0.42)", + "versionCodes": [ + "67" + ] + } + ] + } +] diff --git a/tests/tools/test_google_play.py b/tests/tools/test_google_play.py index 3b080773..d384b3ab 100644 --- a/tests/tools/test_google_play.py +++ b/tests/tools/test_google_play.py @@ -1,19 +1,31 @@ from __future__ import annotations import argparse +import json import os +import pathlib from tempfile import NamedTemporaryFile +from typing import List from unittest import mock import pytest +from codemagic.google_play.resources import Track from codemagic.tools.google_play import GooglePlay from codemagic.tools.google_play.argument_types import CredentialsArgument from codemagic.tools.google_play.arguments import GooglePlayArgument +from codemagic.tools.google_play.errors import GooglePlayError credentials_argument = GooglePlayArgument.GCLOUD_SERVICE_ACCOUNT_CREDENTIALS +@pytest.fixture +def mock_tracks() -> List[Track]: + mock_response_path = pathlib.Path(__file__).parent / 'mocks' / 'google_play_tracks.json' + tracks_response = json.loads(mock_response_path.read_text()) + return [Track(**track) for track in tracks_response] + + @pytest.fixture(autouse=True) def register_args(cli_argument_group): for arg in GooglePlay.CLASS_ARGUMENTS: @@ -107,3 +119,70 @@ def test_private_key_env_arg(mock_google_play_api_client, configure_variable, na _ = GooglePlay.from_cli_args(argparse.Namespace(**namespace_kwargs)) mock_google_play_api_client.assert_called_once_with('{"type":"service_account"}') + + +def test_get_track(mock_tracks): + google_play = GooglePlay({'type': 'service_account'}) + mock_track = mock_tracks[0] + with mock.patch.object(google_play.api_client, 'get_track', return_value=mock_track) as mock_get_track: + track = google_play.get_track('com.example.app', mock_track.track) + mock_get_track.assert_called_once_with('com.example.app', mock_track.track) + assert track == mock_track + + +def test_list_tracks(mock_tracks): + google_play = GooglePlay({'type': 'service_account'}) + with mock.patch.object(google_play.api_client, 'list_tracks', return_value=mock_tracks) as mock_list_tracks: + tracks = google_play.list_tracks('com.example.app') + mock_list_tracks.assert_called_once_with('com.example.app') + assert tracks == mock_tracks + + +@pytest.mark.parametrize('tracks, expected_version_code', [ + ('alpha', 65), + ('beta', 66), + ('internal', 67), + ((), 67), + (('alpha', 'production'), 65), + (('beta', 'production'), 66), + (('internal', 'production'), 67), + (('alpha', 'beta', 'internal'), 67), + (('alpha', 'beta', 'internal', 'production'), 67), +]) +def test_get_latest_build_number(tracks, expected_version_code, mock_tracks): + tracks = (tracks,) if isinstance(tracks, str) else tracks + google_play = GooglePlay({'type': 'service_account'}) + with mock.patch.object(google_play.api_client, 'list_tracks', return_value=mock_tracks) as mock_list_tracks: + build_number = google_play.get_latest_build_number('com.example.app', tracks) + mock_list_tracks.assert_called_once_with('com.example.app') + assert build_number == expected_version_code + + +def test_get_latest_build_number_no_tracks(): + google_play = GooglePlay({'type': 'service_account'}) + with mock.patch.object(google_play.api_client, 'list_tracks', return_value=[]) as mock_list_tracks: + with pytest.raises(GooglePlayError): + google_play.get_latest_build_number('com.example.app') + mock_list_tracks.assert_called_once_with('com.example.app') + + +@pytest.mark.parametrize('track_releases', [None, []]) +def test_get_latest_build_number_no_releases(track_releases, mock_tracks): + mock_track = mock_tracks[0] + mock_track.releases = track_releases + google_play = GooglePlay({'type': 'service_account'}) + with mock.patch.object(google_play.api_client, 'list_tracks', return_value=[mock_track]) as mock_list_tracks: + with pytest.raises(GooglePlayError): + google_play.get_latest_build_number('com.example.app') + mock_list_tracks.assert_called_once_with('com.example.app') + + +@pytest.mark.parametrize('track_releases', [None, []]) +def test_get_latest_build_number_no_version_codes(track_releases, mock_tracks): + # Production track does not have releases with version code + production_track = next(t for t in mock_tracks if t.track == 'production') + google_play = GooglePlay({'type': 'service_account'}) + with mock.patch.object(google_play.api_client, 'list_tracks', return_value=[production_track]) as mock_list_tracks: + with pytest.raises(GooglePlayError): + google_play.get_latest_build_number('com.example.app') + mock_list_tracks.assert_called_once_with('com.example.app') From ae5c3634bc9ed6e78e0fe343f81dad239757ba79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Tue, 12 Apr 2022 15:06:38 +0300 Subject: [PATCH 10/10] Update changelog --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++ src/codemagic/__version__.py | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1295679a..57d366ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +Version 0.24.0 +------------- + +Changes in this release improve usability of tool `google-play`. Updates are from [PR #215](https://github.com/codemagic-ci-cd/cli-tools/pull/215) and [PR #216](https://github.com/codemagic-ci-cd/cli-tools/pull/216). + +**Breaking** + +**None of the breaking changes have an effect on command line usage**, only the Python API of is affected. + +- Method signature changes: + - Signature of `GooglePlay` (tool `google-play`) initialization was changed: + - positional argument `package_name` was removed, + - keyword argument `log_requests` was removed, + - keyword argument `json_output` was removed, + - positional argument `credentials` accepts now Google Play service account credentials both as JSON `str` and parsed `dict`. + - Signature of `GooglePlayDeveloperAPIClient` initialization was changed and simplified: + - positional argument `resource_printer` was removed, + - positional argument `package_name` was removed. + - `GooglePlay` method `get_latest_build_number` requires `package_name` argument, + - `GooglePlayDeveloperAPIClient` methods `create_edit` and `delete_edit` require `package_name` argument, + - property `max_version_code` of `Track` was converted into a method `get_max_version_code()`. +- CLI argument definitions for `google-play` were updated and moved from `codemagic.tools.google_play` to `codemagic.tools.google_play.arguments`. +- Removed definitions: + - enumeration `codemagic.google_play.resources.TrackName` was removed. + - class `codemagic.google_play.ResourcePrinter` was removed. + - exception `codemagic.google_play.VersionCodeFromTrackError` was removed. + - removals in `GooglePlayDeveloperAPIClient`: + - method `get_track_information` was removed, + - property `service` was removed. + +**Features** + +- Update tool `google-play`: + - Allow using custom release tracks with action `google-play get-latest-build-number`. + - Add new action `google-play tracks get` to get information about specific release track for given package name. + - Add new action `google-play tracks list` to get information about all available release tracks for given package name. + +**Development** + +- Module `codemagic.tools.google_play` was refactored by splitting single source file into a subpackage. + - Define actions group `TracksActionGroup` for working with tracks. + - Move `get_latest_build_number` action / method implementation into separate subclass `GetLatestBuildNumberAction`. +- Rework the internals of `GooglePlayDeveloperAPIClient`: + - add context manager to handle `edit` lifecycle so that callers don't have to take care of deletion afterwards, + - add new methods `get_track` and `list_tracks`, + - service resource instance was removed from class instance as it wasn't thread safe. +- Update `mypy` version. +- Add more tests for `GooglePlay` tool. + +**Docs** + +- TBD + Version 0.23.1 ------------- diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index de703209..0a517d15 100644 --- a/src/codemagic/__version__.py +++ b/src/codemagic/__version__.py @@ -1,5 +1,5 @@ __title__ = 'codemagic-cli-tools' __description__ = 'CLI tools used in Codemagic builds' -__version__ = '0.23.1' +__version__ = '0.24.0' __url__ = 'https://github.com/codemagic-ci-cd/cli-tools' __licence__ = 'GNU General Public License v3.0'