From 31fea20970b6b134651dee8f92a4549d2306c2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 11 Sep 2023 18:25:55 +0300 Subject: [PATCH 01/14] Add action to promote release to another Google Play track --- src/codemagic/__version__.py | 2 +- src/codemagic/google_play/api_client.py | 41 +++++++++++++ src/codemagic/google_play/api_error.py | 8 +++ .../google_play/resources/__init__.py | 1 + .../google_play/resources/resource.py | 4 +- src/codemagic/google_play/resources/track.py | 4 +- .../action_groups/tracks_action_group.py | 59 ++++++++++++++++++- src/codemagic/tools/google_play/arguments.py | 25 ++++++++ 8 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index 804c398a..26c59cb6 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.43.0.dev" +__version__ = "0.44.0.dev" __url__ = "https://github.com/codemagic-ci-cd/cli-tools" __licence__ = "GNU General Public License v3.0" diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py index 54834ef2..ac7a2de8 100644 --- a/src/codemagic/google_play/api_client.py +++ b/src/codemagic/google_play/api_client.py @@ -18,6 +18,7 @@ from .api_error import GetResourceError from .api_error import GooglePlayDeveloperAPIClientError from .api_error import ListResourcesError +from .api_error import UpdateResourceError from .resources import Edit from .resources import Track @@ -100,6 +101,8 @@ def delete_edit(self, edit: Union[str, Edit], package_name: str) -> None: delete_request.execute() self._logger.debug(f"Deleted edit {edit_id} for package {package_name!r}") except (errors.Error, errors.HttpError) as e: + if isinstance(e, errors.HttpError) and "edit has already been successfully committed" in e.error_details: + return raise EditError("delete", package_name, e) from e @contextlib.contextmanager @@ -161,3 +164,41 @@ def _list_tracks(self, package_name: str, edit_id: str) -> List[Track]: raise ListResourcesError("tracks", package_name, e) from e else: return [Track(**track) for track in tracks_response["tracks"]] + + def update_track( + self, + package_name: str, + track: Track, + ) -> Track: + with self.use_app_edit(package_name) as _edit: + return self._update_track(package_name, track, _edit.id) + + def _update_track( + self, + package_name: str, + track: Track, + edit_id: str, + ) -> Track: + track_update = track.dict() + self._logger.debug( + f"Update track {track.track!r} for package {package_name} using edit {edit_id} with {track_update!r}", + ) + track_request = self.edits_service.tracks().update( + packageName=package_name, + editId=edit_id, + track=track.track, + body=track_update, + ) + commit_request = self.edits_service.commit( + packageName=package_name, + editId=edit_id, + ) + try: + track_response = track_request.execute() + commit_response = commit_request.execute() + except (errors.Error, errors.HttpError) as e: + raise UpdateResourceError("track", package_name, e) from e + + self._logger.debug(f"Track {track.track!r} update response for package {package_name!r}: {track_response}") + self._logger.debug(f"Track {track.track!r} commit response for package {package_name!r}: {commit_response}") + return Track(**track_response) diff --git a/src/codemagic/google_play/api_error.py b/src/codemagic/google_play/api_error.py index e9fba402..714b3fcc 100644 --- a/src/codemagic/google_play/api_error.py +++ b/src/codemagic/google_play/api_error.py @@ -60,3 +60,11 @@ def _get_message(self, resource_description: str) -> str: f'Failed to list {resource_description} from Google Play for package "{self.package_name}". ' f"{self._get_reason()}" ) + + +class UpdateResourceError(_RequestError): + def _get_message(self, resource_description: str) -> str: + return ( + f'Failed to update {resource_description} in Google Play for package "{self.package_name}". ' + f"{self._get_reason()}" + ) diff --git a/src/codemagic/google_play/resources/__init__.py b/src/codemagic/google_play/resources/__init__.py index b1c85f51..f28957b4 100644 --- a/src/codemagic/google_play/resources/__init__.py +++ b/src/codemagic/google_play/resources/__init__.py @@ -1,3 +1,4 @@ from .edit import Edit +from .enums import ReleaseStatus from .resource import Resource from .track import Track diff --git a/src/codemagic/google_play/resources/resource.py b/src/codemagic/google_play/resources/resource.py index 48cbd27f..b6850880 100644 --- a/src/codemagic/google_play/resources/resource.py +++ b/src/codemagic/google_play/resources/resource.py @@ -96,9 +96,9 @@ def _format_attribute_value(cls, value: Any, tabs_count: int = 0) -> str: return str(value) def __str__(self) -> str: - return "".join( + return "\n".join( [ - f"\n{self._format_attribute_name(k)}: {self._format_attribute_value(v)}" + f"{self._format_attribute_name(k)}: {self._format_attribute_value(v)}" for k, v in self.__dict__.items() if v is not None ], diff --git a/src/codemagic/google_play/resources/track.py b/src/codemagic/google_play/resources/track.py index e3d759fb..f274e9d3 100644 --- a/src/codemagic/google_play/resources/track.py +++ b/src/codemagic/google_play/resources/track.py @@ -74,7 +74,9 @@ class Track(Resource): def __post_init__(self): if isinstance(self.releases, list): - self.releases = [Release(**release) for release in self.releases] + self.releases = [ + release if isinstance(release, Release) else Release(**release) for release in self.releases + ] def get_max_version_code(self) -> int: error_prefix = f'Failed to get version code from "{self.track}" track' 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 index add50d58..145a7172 100644 --- a/src/codemagic/tools/google_play/action_groups/tracks_action_group.py +++ b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py @@ -1,9 +1,12 @@ +import dataclasses import json from abc import ABCMeta from typing import List from codemagic import cli +from codemagic.cli import Colors from codemagic.google_play import GooglePlayDeveloperAPIClientError +from codemagic.google_play.resources import ReleaseStatus from codemagic.google_play.resources import Track from ..arguments import GooglePlayArgument @@ -68,6 +71,60 @@ def list_tracks( self.echo(json.dumps([t.dict() for t in tracks], indent=4)) else: for track in tracks: - self.echo(str(track)) + self.echo(f"{track}\n") return tracks + + @cli.action( + "promote", + TracksArgument.PACKAGE_NAME, + TracksArgument.SOURCE_TRACK_NAME, + TracksArgument.TARGET_TRACK_NAME, + TracksArgument.TRACK_PROMOTED_RELEASE_STATUS, + GooglePlayArgument.JSON_OUTPUT, + action_group=GooglePlayActionGroups.TRACKS, + ) + def promote_track( + self, + package_name: str, + source_track_name: str, + target_track_name: str, + promoted_release_status: ReleaseStatus = TracksArgument.TRACK_PROMOTED_RELEASE_STATUS.get_default(), + json_output: bool = False, + should_print: bool = True, + ) -> Track: + """ + Promote releases from source track to target track + """ + + source_track = self.get_track(package_name, source_track_name, should_print=False) + target_track = self.get_track(package_name, target_track_name, should_print=False) + + if not source_track.releases: + raise GooglePlayError("Source track does not have any releases") + + release_to_promote = dataclasses.replace( + source_track.releases[0], + status=promoted_release_status, + ) + + promoted_version_codes = ", ".join(release_to_promote.versionCodes or ["version code N/A"]) + self.logger.info( + f"Promote release {release_to_promote.name} ({promoted_version_codes}) " + f'from track "{source_track.track}" to track "{target_track.track}"', + ) + + try: + updated_track = self.api_client.update_track( + package_name, + dataclasses.replace(target_track, releases=[release_to_promote]), + ) + except GooglePlayDeveloperAPIClientError as api_error: + raise GooglePlayError(str(api_error)) + + self.logger.info(Colors.GREEN(f"Successfully Completed release promotion to track {target_track.track}")) + + if should_print: + self.echo(updated_track.json() if json_output else str(updated_track)) + + return updated_track diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index 2173d729..400506a1 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -1,4 +1,5 @@ from codemagic import cli +from codemagic.google_play.resources import ReleaseStatus from .argument_types import CredentialsArgument from .argument_types import PackageName @@ -36,6 +37,30 @@ class TracksArgument(cli.Argument): argparse_kwargs={"required": True}, ) + SOURCE_TRACK_NAME = cli.ArgumentProperties( + key="source_track_name", + flags=("--source-track",), + description="Name of the track from where releases are promoted from. For example `internal`", + argparse_kwargs={"required": True}, + ) + TARGET_TRACK_NAME = cli.ArgumentProperties( + key="target_track_name", + flags=("--target-track",), + description="Name of the track to where releases are promoted to. For example `alpha`", + argparse_kwargs={"required": True}, + ) + TRACK_PROMOTED_RELEASE_STATUS = cli.ArgumentProperties( + key="promoted_release_status", + flags=("--promoted-release-status",), + type=ReleaseStatus, + description="Release status in a promoted track", + argparse_kwargs={ + "required": False, + "default": ReleaseStatus.COMPLETED, + "choices": list(ReleaseStatus), + }, + ) + class LatestBuildNumberArgument(cli.Argument): TRACKS = cli.ArgumentProperties( From 166c524ba81d40fefed94058f398ccdd4eb40e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Tue, 12 Sep 2023 17:08:14 +0300 Subject: [PATCH 02/14] Add more options for release promotion --- .../cli/argument/common_argument_types.py | 20 ++++++++ .../google_play/resources/__init__.py | 1 + .../action_groups/tracks_action_group.py | 46 +++++++++++++----- src/codemagic/tools/google_play/arguments.py | 48 ++++++++++++++++--- 4 files changed, 95 insertions(+), 20 deletions(-) diff --git a/src/codemagic/cli/argument/common_argument_types.py b/src/codemagic/cli/argument/common_argument_types.py index f7580802..d114e3fe 100644 --- a/src/codemagic/cli/argument/common_argument_types.py +++ b/src/codemagic/cli/argument/common_argument_types.py @@ -2,6 +2,7 @@ import json import pathlib from datetime import datetime +from typing import Callable from typing import Dict @@ -74,3 +75,22 @@ def iso_8601_datetime(iso_8601_timestamp: str) -> datetime: except ValueError: continue raise argparse.ArgumentTypeError(f'"{iso_8601_timestamp}" is not a valid ISO 8601 timestamp') + + @staticmethod + def bounded_float(lower_limit: float, upper_limit: float, inclusive: bool) -> Callable[[str], float]: + def _bounded_float(number: str): + try: + f = float(number) + except ValueError: + raise argparse.ArgumentTypeError(f"Value {number} is not a valid floating point number") + + if inclusive and lower_limit > f or f > upper_limit: + error = f"Value {f} is out of allowed bounds, {lower_limit} <= value <= {upper_limit}" + raise argparse.ArgumentTypeError(error) + if not inclusive and lower_limit >= f or f >= upper_limit: + error = f"Value {f} is out of allowed bounds, {lower_limit} < value < {upper_limit}" + raise argparse.ArgumentTypeError(error) + + return f + + return _bounded_float diff --git a/src/codemagic/google_play/resources/__init__.py b/src/codemagic/google_play/resources/__init__.py index f28957b4..68fcf933 100644 --- a/src/codemagic/google_play/resources/__init__.py +++ b/src/codemagic/google_play/resources/__init__.py @@ -1,4 +1,5 @@ from .edit import Edit from .enums import ReleaseStatus from .resource import Resource +from .track import Release from .track import Track 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 index 145a7172..04c6a681 100644 --- a/src/codemagic/tools/google_play/action_groups/tracks_action_group.py +++ b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py @@ -2,14 +2,17 @@ import json from abc import ABCMeta from typing import List +from typing import Optional from codemagic import cli from codemagic.cli import Colors from codemagic.google_play import GooglePlayDeveloperAPIClientError +from codemagic.google_play.resources import Release from codemagic.google_play.resources import ReleaseStatus from codemagic.google_play.resources import Track from ..arguments import GooglePlayArgument +from ..arguments import PromoteArgument from ..arguments import TracksArgument from ..errors import GooglePlayError from ..google_play_base_action import GooglePlayBaseAction @@ -76,37 +79,54 @@ def list_tracks( return tracks @cli.action( - "promote", + "promote-release", TracksArgument.PACKAGE_NAME, - TracksArgument.SOURCE_TRACK_NAME, - TracksArgument.TARGET_TRACK_NAME, - TracksArgument.TRACK_PROMOTED_RELEASE_STATUS, + PromoteArgument.SOURCE_TRACK_NAME, + PromoteArgument.TARGET_TRACK_NAME, + PromoteArgument.PROMOTED_STATUS, + PromoteArgument.PROMOTED_USER_FRACTION, + PromoteArgument.PROMOTE_VERSION_CODE, + PromoteArgument.PROMOTE_STATUS, GooglePlayArgument.JSON_OUTPUT, action_group=GooglePlayActionGroups.TRACKS, ) - def promote_track( + def promote_release( self, package_name: str, source_track_name: str, target_track_name: str, - promoted_release_status: ReleaseStatus = TracksArgument.TRACK_PROMOTED_RELEASE_STATUS.get_default(), + promoted_status: ReleaseStatus = PromoteArgument.PROMOTED_STATUS.get_default(), + promoted_user_fraction: Optional[float] = None, + promote_version_code: Optional[str] = None, + promote_status: Optional[ReleaseStatus] = None, json_output: bool = False, should_print: bool = True, ) -> Track: """ - Promote releases from source track to target track + Promote releases from source track to target track. If filters for source + track release are not specified, then the latest release will be promoted """ - source_track = self.get_track(package_name, source_track_name, should_print=False) target_track = self.get_track(package_name, target_track_name, should_print=False) - if not source_track.releases: - raise GooglePlayError("Source track does not have any releases") + source_releases: List[Release] = source_track.releases or [] + if promote_version_code: + source_releases = [r for r in source_releases if r.versionCodes and promote_version_code in r.versionCodes] + if promote_status: + source_releases = [r for r in source_releases if r.status is promote_status] + + if not source_releases: + error = f'Source track "{source_track_name}" does not have any releases' + if promote_version_code or promote_status: + error = f"{error} matching specified filters" + raise GooglePlayError(error) release_to_promote = dataclasses.replace( - source_track.releases[0], - status=promoted_release_status, + source_releases[0], + status=promoted_status, ) + if promoted_user_fraction: + release_to_promote.userFraction = promoted_user_fraction promoted_version_codes = ", ".join(release_to_promote.versionCodes or ["version code N/A"]) self.logger.info( @@ -122,7 +142,7 @@ def promote_track( except GooglePlayDeveloperAPIClientError as api_error: raise GooglePlayError(str(api_error)) - self.logger.info(Colors.GREEN(f"Successfully Completed release promotion to track {target_track.track}")) + self.logger.info(Colors.GREEN(f"Successfully completed release promotion to track {target_track.track}")) if should_print: self.echo(updated_track.json() if json_output else str(updated_track)) diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index 400506a1..e0cb45ec 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -1,4 +1,5 @@ from codemagic import cli +from codemagic.cli import Colors from codemagic.google_play.resources import ReleaseStatus from .argument_types import CredentialsArgument @@ -27,31 +28,37 @@ class TracksArgument(cli.Argument): key="package_name", flags=("--package-name", "-p"), type=PackageName, - description="Package name of the app in Google Play Console. For example `com.example.app`", + description=( + f"Package name of the app in Google Play Console. For example `{Colors.WHITE('com.example.app')}`" + ), argparse_kwargs={"required": True}, ) TRACK_NAME = cli.ArgumentProperties( key="track_name", flags=("--track", "-t"), - description="Release track name. For example `alpha` or `production`", + description=f"Release track name. For example `{Colors.WHITE('alpha')}` or `{Colors.WHITE('production')}`", argparse_kwargs={"required": True}, ) + +class PromoteArgument(cli.Argument): SOURCE_TRACK_NAME = cli.ArgumentProperties( key="source_track_name", flags=("--source-track",), - description="Name of the track from where releases are promoted from. For example `internal`", + description=( + f"Name of the track from where releases are promoted from. For example `{Colors.WHITE('internal')}`" + ), argparse_kwargs={"required": True}, ) TARGET_TRACK_NAME = cli.ArgumentProperties( key="target_track_name", flags=("--target-track",), - description="Name of the track to where releases are promoted to. For example `alpha`", + description=f"Name of the track to which releases are promoted to. For example `{Colors.WHITE('alpha')}`", argparse_kwargs={"required": True}, ) - TRACK_PROMOTED_RELEASE_STATUS = cli.ArgumentProperties( - key="promoted_release_status", - flags=("--promoted-release-status",), + PROMOTED_STATUS = cli.ArgumentProperties( + key="promoted_status", + flags=("--release-status",), type=ReleaseStatus, description="Release status in a promoted track", argparse_kwargs={ @@ -60,6 +67,33 @@ class TracksArgument(cli.Argument): "choices": list(ReleaseStatus), }, ) + PROMOTED_USER_FRACTION = cli.ArgumentProperties( + key="promoted_user_fraction", + flags=("--user-fraction",), + type=cli.CommonArgumentTypes.bounded_float(0, 1, inclusive=False), + description=( + "Fraction of users who are eligible for a staged release in promoted track. " + f"Number from interval `{Colors.WHITE('0 < fraction < 1')}`. Can only be set when status is " + f"`{Colors.WHITE(str(ReleaseStatus.IN_PROGRESS))}` or `{Colors.WHITE(str(ReleaseStatus.HALTED))}`" + ), + argparse_kwargs={"required": False}, + ) + PROMOTE_VERSION_CODE = cli.ArgumentProperties( + key="promote_version_code", + flags=("--version-code-filter",), + description="Promote only release from source track that contains specified version code", + argparse_kwargs={"required": False}, + ) + PROMOTE_STATUS = cli.ArgumentProperties( + key="promote_status", + flags=("--release-status-filter",), + type=ReleaseStatus, + description="Promote only release from source track with specified status", + argparse_kwargs={ + "required": False, + "choices": list(ReleaseStatus), + }, + ) class LatestBuildNumberArgument(cli.Argument): From 18a405e15640a6052526344306b6e73679cf7bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Tue, 12 Sep 2023 17:13:19 +0300 Subject: [PATCH 03/14] Fix test --- tests/google_play/resources/test_resource_print.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/google_play/resources/test_resource_print.py b/tests/google_play/resources/test_resource_print.py index e148ea9c..6924b1ec 100644 --- a/tests/google_play/resources/test_resource_print.py +++ b/tests/google_play/resources/test_resource_print.py @@ -6,7 +6,6 @@ def test_track_string(api_track): track = Track(**api_track) expected_output = ( - "\n" "Track: internal\n" "Releases: [\n" " Status: draft\n" From 8eb96ee4a9fb18ce34a9077d0861f3e10c9ee42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 10:39:18 +0300 Subject: [PATCH 04/14] Update docs --- docs/google-play/tracks.md | 1 + docs/google-play/tracks/promote-release.md | 87 ++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 docs/google-play/tracks/promote-release.md diff --git a/docs/google-play/tracks.md b/docs/google-play/tracks.md index 518d41af..11c42df2 100644 --- a/docs/google-play/tracks.md +++ b/docs/google-play/tracks.md @@ -48,3 +48,4 @@ Enable verbose logging for commands | :--- | :--- | |[`get`](tracks/get.md)|Get information about specified track from Google Play Developer API| |[`list`](tracks/list.md)|Get information about specified track from Google Play Developer API| +|[`promote-release`](tracks/promote-release.md)|Promote releases from source track to target track. If filters for source track release are not specified, then the latest release will be promoted| diff --git a/docs/google-play/tracks/promote-release.md b/docs/google-play/tracks/promote-release.md new file mode 100644 index 00000000..6c1be2f8 --- /dev/null +++ b/docs/google-play/tracks/promote-release.md @@ -0,0 +1,87 @@ + +promote-release +=============== + + +**Promote releases from source track to target track. If filters for source track release are not specified, then the latest release will be promoted** +### Usage +```bash +google-play tracks promote-release [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--credentials GCLOUD_SERVICE_ACCOUNT_CREDENTIALS] + [--release-status PROMOTED_STATUS] + [--user-fraction PROMOTED_USER_FRACTION] + [--version-code-filter PROMOTE_VERSION_CODE] + [--release-status-filter PROMOTE_STATUS] + [--json] + --package-name PACKAGE_NAME + --source-track SOURCE_TRACK_NAME + --target-track TARGET_TRACK_NAME +``` +### Required arguments for action `promote-release` + +##### `--package-name, -p=PACKAGE_NAME` + + +Package name of the app in Google Play Console. For example `com.example.app` +##### `--source-track=SOURCE_TRACK_NAME` + + +Name of the track from where releases are promoted from. For example `internal` +##### `--target-track=TARGET_TRACK_NAME` + + +Name of the track to which releases are promoted to. For example `alpha` +### Optional arguments for action `promote-release` + +##### `--release-status=statusUnspecified | draft | inProgress | halted | completed` + + +Release status in a promoted track. Default: `completed` +##### `--user-fraction=PROMOTED_USER_FRACTION` + + +Fraction of users who are eligible for a staged release in promoted track. Number from interval `0 < fraction < 1`. Can only be set when status is `inProgress` or `halted` +##### `--version-code-filter=PROMOTE_VERSION_CODE` + + +Promote only release from source track that contains specified version code +##### `--release-status-filter=statusUnspecified | draft | inProgress | halted | completed` + + +Promote only release from source track with specified status +##### `--json, -j` + + +Whether to show the request response in JSON format +### Optional arguments for command `google-play` + +##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` + + +Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +### Common options + +##### `-h, --help` + + +show this help message and exit +##### `--log-stream=stderr | stdout` + + +Log output stream. Default `stderr` +##### `--no-color` + + +Do not use ANSI colors to format terminal output +##### `--version` + + +Show tool version and exit +##### `-s, --silent` + + +Disable log output for commands +##### `-v, --verbose` + + +Enable verbose logging for commands From c6c536049860029512f2ad8ea3d7fddc5479748a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 11:24:46 +0300 Subject: [PATCH 05/14] Fix bounded number type and add tests to it --- .../cli/argument/common_argument_types.py | 30 ++++--- src/codemagic/tools/google_play/arguments.py | 2 +- .../test_bounded_number.py | 78 +++++++++++++++++++ 3 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 tests/cli/argument/common_argument_types/test_bounded_number.py diff --git a/src/codemagic/cli/argument/common_argument_types.py b/src/codemagic/cli/argument/common_argument_types.py index d114e3fe..cb0c695d 100644 --- a/src/codemagic/cli/argument/common_argument_types.py +++ b/src/codemagic/cli/argument/common_argument_types.py @@ -4,6 +4,10 @@ from datetime import datetime from typing import Callable from typing import Dict +from typing import Type +from typing import TypeVar + +N = TypeVar("N", int, float) class CommonArgumentTypes: @@ -77,20 +81,26 @@ def iso_8601_datetime(iso_8601_timestamp: str) -> datetime: raise argparse.ArgumentTypeError(f'"{iso_8601_timestamp}" is not a valid ISO 8601 timestamp') @staticmethod - def bounded_float(lower_limit: float, upper_limit: float, inclusive: bool) -> Callable[[str], float]: - def _bounded_float(number: str): + def bounded_number( + number_type: Type[N], + lower_limit: N, + upper_limit: N, + inclusive: bool, + ) -> Callable[[str], N]: + def _resolve_number(number_as_string: str): try: - f = float(number) + n = number_type(number_as_string) except ValueError: - raise argparse.ArgumentTypeError(f"Value {number} is not a valid floating point number") + type_description = "floating point number" if number_type is float else "integer" + raise argparse.ArgumentTypeError(f"Value {number_as_string} is not a valid {type_description}") - if inclusive and lower_limit > f or f > upper_limit: - error = f"Value {f} is out of allowed bounds, {lower_limit} <= value <= {upper_limit}" + if inclusive and (lower_limit > n or n > upper_limit): + error = f"Value {n} is out of allowed bounds, {lower_limit} <= value <= {upper_limit}" raise argparse.ArgumentTypeError(error) - if not inclusive and lower_limit >= f or f >= upper_limit: - error = f"Value {f} is out of allowed bounds, {lower_limit} < value < {upper_limit}" + if not inclusive and (lower_limit >= n or n >= upper_limit): + error = f"Value {n} is out of allowed bounds, {lower_limit} < value < {upper_limit}" raise argparse.ArgumentTypeError(error) - return f + return n - return _bounded_float + return _resolve_number diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index e0cb45ec..acfbf908 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -70,7 +70,7 @@ class PromoteArgument(cli.Argument): PROMOTED_USER_FRACTION = cli.ArgumentProperties( key="promoted_user_fraction", flags=("--user-fraction",), - type=cli.CommonArgumentTypes.bounded_float(0, 1, inclusive=False), + type=cli.CommonArgumentTypes.bounded_number(float, 0, 1, inclusive=False), description=( "Fraction of users who are eligible for a staged release in promoted track. " f"Number from interval `{Colors.WHITE('0 < fraction < 1')}`. Can only be set when status is " diff --git a/tests/cli/argument/common_argument_types/test_bounded_number.py b/tests/cli/argument/common_argument_types/test_bounded_number.py new file mode 100644 index 00000000..c8aecb85 --- /dev/null +++ b/tests/cli/argument/common_argument_types/test_bounded_number.py @@ -0,0 +1,78 @@ +from argparse import ArgumentTypeError + +import pytest +from codemagic.cli.argument import CommonArgumentTypes + + +@pytest.mark.parametrize( + ("number_as_string", "requested_type", "expected_number"), + ( + ("1", int, 1), + ("1.0", float, 1.0), + ("2", int, 2), + ("2", float, 2.0), + ), +) +def test_successful_conversion(number_as_string, requested_type, expected_number): + constructor = CommonArgumentTypes.bounded_number(requested_type, -100, 100, inclusive=True) + resolved_number = constructor(number_as_string) + assert isinstance(resolved_number, requested_type) + assert resolved_number == expected_number + + +@pytest.mark.parametrize( + ("invalid_number_input", "requested_type", "expected_error_message"), + ( + ("1.0", int, "Value 1.0 is not a valid integer"), + ("a", int, "Value a is not a valid integer"), + ("?", int, "Value ? is not a valid integer"), + ("?", float, "Value ? is not a valid floating point number"), + ("x", float, "Value x is not a valid floating point number"), + ), +) +def test_invalid_input(invalid_number_input, requested_type, expected_error_message): + constructor = CommonArgumentTypes.bounded_number(requested_type, -100, 100, inclusive=True) + with pytest.raises(ArgumentTypeError) as error_info: + constructor(invalid_number_input) + assert str(error_info.value) == expected_error_message + + +@pytest.mark.parametrize( + ("number_as_string", "inclusive_comparison", "expected_number"), + ( + ("1", True, 1), + ("10", True, 10), + ("2", False, 2), + ("9", False, 9), + ), +) +def test_within_bounds(number_as_string, inclusive_comparison, expected_number): + constructor = CommonArgumentTypes.bounded_number(int, 1, 10, inclusive=inclusive_comparison) + resolved_value = constructor(number_as_string) + assert resolved_value == expected_number + + +@pytest.mark.parametrize( + ("out_of_bounds_number_string", "inclusive_comparison"), + ( + ("-1", True), + ("0", True), + ("11", True), + ("20", True), + ("-1", False), + ("1", False), + ("10", False), + ("11", False), + ("20", False), + ), +) +def test_out_of_bounds(out_of_bounds_number_string, inclusive_comparison): + constructor = CommonArgumentTypes.bounded_number(int, 1, 10, inclusive=inclusive_comparison) + with pytest.raises(ArgumentTypeError) as error_info: + constructor(out_of_bounds_number_string) + + comparison_op = "<=" if inclusive_comparison else "<" + expected_error_message = ( + f"Value {out_of_bounds_number_string} is out of allowed bounds, 1 {comparison_op} value {comparison_op} 10" + ) + assert str(error_info.value) == expected_error_message From 4107b63608114dc0abe78fee04dc022aab407007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 11:36:45 +0300 Subject: [PATCH 06/14] Add comment --- src/codemagic/google_play/api_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py index ac7a2de8..2ddec22b 100644 --- a/src/codemagic/google_play/api_client.py +++ b/src/codemagic/google_play/api_client.py @@ -102,6 +102,7 @@ def delete_edit(self, edit: Union[str, Edit], package_name: str) -> None: self._logger.debug(f"Deleted edit {edit_id} for package {package_name!r}") except (errors.Error, errors.HttpError) as e: if isinstance(e, errors.HttpError) and "edit has already been successfully committed" in e.error_details: + # This can be ignored as the commit has already been exhausted return raise EditError("delete", package_name, e) from e From 1f667b602594866e7fe1bb47743f1f1d9bd4dcf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 11:55:27 +0300 Subject: [PATCH 07/14] Remove intermediate private method --- src/codemagic/google_play/api_client.py | 48 +++++++++++-------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/codemagic/google_play/api_client.py b/src/codemagic/google_play/api_client.py index 2ddec22b..e3a81abb 100644 --- a/src/codemagic/google_play/api_client.py +++ b/src/codemagic/google_play/api_client.py @@ -171,34 +171,26 @@ def update_track( package_name: str, track: Track, ) -> Track: - with self.use_app_edit(package_name) as _edit: - return self._update_track(package_name, track, _edit.id) - - def _update_track( - self, - package_name: str, - track: Track, - edit_id: str, - ) -> Track: - track_update = track.dict() - self._logger.debug( - f"Update track {track.track!r} for package {package_name} using edit {edit_id} with {track_update!r}", - ) - track_request = self.edits_service.tracks().update( - packageName=package_name, - editId=edit_id, - track=track.track, - body=track_update, - ) - commit_request = self.edits_service.commit( - packageName=package_name, - editId=edit_id, - ) - try: - track_response = track_request.execute() - commit_response = commit_request.execute() - except (errors.Error, errors.HttpError) as e: - raise UpdateResourceError("track", package_name, e) from e + with self.use_app_edit(package_name) as edit: + track_update = track.dict() + self._logger.debug( + f"Update track {track.track!r} for package {package_name} using edit {edit.id} with {track_update!r}", + ) + track_request = self.edits_service.tracks().update( + packageName=package_name, + editId=edit.id, + track=track.track, + body=track_update, + ) + commit_request = self.edits_service.commit( + packageName=package_name, + editId=edit.id, + ) + try: + track_response = track_request.execute() + commit_response = commit_request.execute() + except (errors.Error, errors.HttpError) as e: + raise UpdateResourceError("track", package_name, e) from e self._logger.debug(f"Track {track.track!r} update response for package {package_name!r}: {track_response}") self._logger.debug(f"Track {track.track!r} commit response for package {package_name!r}: {commit_response}") From e1697ebdea47220b4c791a8f38284b6e1b5d2780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 12:04:37 +0300 Subject: [PATCH 08/14] Update changelog and bump version --- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ee7c5f..e99a04a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +Version 0.44.0 +------------- + +Additions and changes from [pull request #345](https://github.com/codemagic-ci-cd/cli-tools/pull/345). + +**Features** +- Add new action `google-play tracks promote-release` to promote a release from one Google Play release track to another. + +**Development** +- Define new common argument type `bounded_number` for CLI usage that can be used to load floats and integers from CLI inputs within specified ranges. +- Add new client method `update_track` to update release track in Google Play API client `codemagic.google_play.api_client.GooglePlayDeveloperAPIClient`. +- +**Documentation** +- Update documentation for action group `google-play tracks`. +- Add documentation for action `google-play tracks promote-release`. + Version 0.43.0 ------------- diff --git a/pyproject.toml b/pyproject.toml index 33ebabd4..e6db8fcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "codemagic-cli-tools" -version = "0.43.0" +version = "0.44.0" description = "CLI tools used in Codemagic builds" readme = "README.md" authors = [ From ebf4ddd44b8ee95ab3a8d8819c20ade0df94acd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 12:16:07 +0300 Subject: [PATCH 09/14] Update descriptions for promote-release arguments --- docs/google-play/tracks/promote-release.md | 4 ++-- src/codemagic/tools/google_play/arguments.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/google-play/tracks/promote-release.md b/docs/google-play/tracks/promote-release.md index 6c1be2f8..44292ebd 100644 --- a/docs/google-play/tracks/promote-release.md +++ b/docs/google-play/tracks/promote-release.md @@ -36,11 +36,11 @@ Name of the track to which releases are promoted to. For example `alpha` ##### `--release-status=statusUnspecified | draft | inProgress | halted | completed` -Release status in a promoted track. Default: `completed` +Promoted release status in the target track. Default: `completed` ##### `--user-fraction=PROMOTED_USER_FRACTION` -Fraction of users who are eligible for a staged release in promoted track. Number from interval `0 < fraction < 1`. Can only be set when status is `inProgress` or `halted` +Fraction of users who are eligible for a staged promoted release in the target track. Number from interval `0 < fraction < 1`. Can only be set when status is `inProgress` or `halted` ##### `--version-code-filter=PROMOTE_VERSION_CODE` diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index acfbf908..5ccf8e05 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -60,7 +60,7 @@ class PromoteArgument(cli.Argument): key="promoted_status", flags=("--release-status",), type=ReleaseStatus, - description="Release status in a promoted track", + description="Promoted release status in the target track", argparse_kwargs={ "required": False, "default": ReleaseStatus.COMPLETED, @@ -72,7 +72,7 @@ class PromoteArgument(cli.Argument): flags=("--user-fraction",), type=cli.CommonArgumentTypes.bounded_number(float, 0, 1, inclusive=False), description=( - "Fraction of users who are eligible for a staged release in promoted track. " + "Fraction of users who are eligible for a staged promoted release in the target track. " f"Number from interval `{Colors.WHITE('0 < fraction < 1')}`. Can only be set when status is " f"`{Colors.WHITE(str(ReleaseStatus.IN_PROGRESS))}` or `{Colors.WHITE(str(ReleaseStatus.HALTED))}`" ), From d0ee1e118697cf9c67e8236d2383bd7e80635240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 12:34:59 +0300 Subject: [PATCH 10/14] Fix promoted release use fraction --- .../tools/google_play/action_groups/tracks_action_group.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 04c6a681..1c22e55a 100644 --- a/src/codemagic/tools/google_play/action_groups/tracks_action_group.py +++ b/src/codemagic/tools/google_play/action_groups/tracks_action_group.py @@ -124,9 +124,8 @@ def promote_release( release_to_promote = dataclasses.replace( source_releases[0], status=promoted_status, + userFraction=promoted_user_fraction, ) - if promoted_user_fraction: - release_to_promote.userFraction = promoted_user_fraction promoted_version_codes = ", ".join(release_to_promote.versionCodes or ["version code N/A"]) self.logger.info( From dde977160dc6dd7447cdb1e0545218cb601b7eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 14:20:16 +0300 Subject: [PATCH 11/14] Apply suggestions from code review Co-authored-by: helinanever <36853001+helinanever@users.noreply.github.com> --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e99a04a7..f414e323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ Version 0.44.0 Additions and changes from [pull request #345](https://github.com/codemagic-ci-cd/cli-tools/pull/345). **Features** -- Add new action `google-play tracks promote-release` to promote a release from one Google Play release track to another. +- Add a new action `google-play tracks promote-release` to promote a release from one Google Play release track to another. **Development** -- Define new common argument type `bounded_number` for CLI usage that can be used to load floats and integers from CLI inputs within specified ranges. -- Add new client method `update_track` to update release track in Google Play API client `codemagic.google_play.api_client.GooglePlayDeveloperAPIClient`. +- Define a new common argument type `bounded_number` for CLI usage that can be used to load floats and integers from CLI inputs within specified ranges. +- Add a new client method `update_track` to update release track in Google Play API client `codemagic.google_play.api_client.GooglePlayDeveloperAPIClient`. - **Documentation** - Update documentation for action group `google-play tracks`. From 2f77a8f0f5e2b29a7c1b6f2a17cbe9d736085c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 14:22:51 +0300 Subject: [PATCH 12/14] Update descriptions --- docs/google-play/README.md | 2 +- docs/google-play/get-latest-build-number.md | 2 +- docs/google-play/tracks.md | 2 +- docs/google-play/tracks/get.md | 2 +- docs/google-play/tracks/list.md | 2 +- docs/google-play/tracks/promote-release.md | 10 +++++----- src/codemagic/tools/google_play/arguments.py | 10 +++++----- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/google-play/README.md b/docs/google-play/README.md index 774f8b57..7179e0f1 100644 --- a/docs/google-play/README.md +++ b/docs/google-play/README.md @@ -15,7 +15,7 @@ google-play [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] ##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` -Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +Gcloud service account credentials with the `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. ### Common options ##### `-h, --help` diff --git a/docs/google-play/get-latest-build-number.md b/docs/google-play/get-latest-build-number.md index 901e1dfd..e761de24 100644 --- a/docs/google-play/get-latest-build-number.md +++ b/docs/google-play/get-latest-build-number.md @@ -28,7 +28,7 @@ Get the build number from the specified track(s). If not specified, the highest ##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` -Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +Gcloud service account credentials with the `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. ### Common options ##### `-h, --help` diff --git a/docs/google-play/tracks.md b/docs/google-play/tracks.md index 11c42df2..3f2b7887 100644 --- a/docs/google-play/tracks.md +++ b/docs/google-play/tracks.md @@ -15,7 +15,7 @@ google-play tracks [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] ##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` -Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +Gcloud service account credentials with the `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. ### Common options ##### `-h, --help` diff --git a/docs/google-play/tracks/get.md b/docs/google-play/tracks/get.md index 7406d2d7..408b88ca 100644 --- a/docs/google-play/tracks/get.md +++ b/docs/google-play/tracks/get.md @@ -33,7 +33,7 @@ Whether to show the request response in JSON format ##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` -Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +Gcloud service account credentials with the `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. ### Common options ##### `-h, --help` diff --git a/docs/google-play/tracks/list.md b/docs/google-play/tracks/list.md index c3afdbeb..c1eb0358 100644 --- a/docs/google-play/tracks/list.md +++ b/docs/google-play/tracks/list.md @@ -28,7 +28,7 @@ Whether to show the request response in JSON format ##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` -Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +Gcloud service account credentials with the `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. ### Common options ##### `-h, --help` diff --git a/docs/google-play/tracks/promote-release.md b/docs/google-play/tracks/promote-release.md index 44292ebd..f55d6dab 100644 --- a/docs/google-play/tracks/promote-release.md +++ b/docs/google-play/tracks/promote-release.md @@ -30,13 +30,13 @@ Name of the track from where releases are promoted from. For example `internal` ##### `--target-track=TARGET_TRACK_NAME` -Name of the track to which releases are promoted to. For example `alpha` +Name of the track to which releases are promoted. For example `alpha` ### Optional arguments for action `promote-release` ##### `--release-status=statusUnspecified | draft | inProgress | halted | completed` -Promoted release status in the target track. Default: `completed` +Status of the promoted release in the target track. Default: `completed` ##### `--user-fraction=PROMOTED_USER_FRACTION` @@ -44,11 +44,11 @@ Fraction of users who are eligible for a staged promoted release in the target t ##### `--version-code-filter=PROMOTE_VERSION_CODE` -Promote only release from source track that contains specified version code +Promote only a source track release that contains the specified version code ##### `--release-status-filter=statusUnspecified | draft | inProgress | halted | completed` -Promote only release from source track with specified status +Promote only a source track release with the specified status ##### `--json, -j` @@ -58,7 +58,7 @@ Whether to show the request response in JSON format ##### `--credentials=GCLOUD_SERVICE_ACCOUNT_CREDENTIALS` -Gcloud service account credentials with `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. +Gcloud service account credentials with the `JSON` key type to access Google Play Developer API. If not given, the value will be checked from the environment variable `GCLOUD_SERVICE_ACCOUNT_CREDENTIALS`. Alternatively to entering CREDENTIALS in plaintext, it may also be specified using the `@env:` prefix followed by an environment variable name, or the `@file:` prefix followed by a path to the file containing the value. Example: `@env:` uses the value in the environment variable named ``, and `@file:` uses the value from the file at ``. ### Common options ##### `-h, --help` diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index 5ccf8e05..9fc3a5b4 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -11,7 +11,7 @@ class GooglePlayArgument(cli.Argument): key="credentials", flags=("--credentials",), type=CredentialsArgument, - description="Gcloud service account credentials with `JSON` key type to access Google Play Developer API", + description="Gcloud service account credentials with the `JSON` key type to access Google Play Developer API", argparse_kwargs={"required": False}, ) JSON_OUTPUT = cli.ArgumentProperties( @@ -53,14 +53,14 @@ class PromoteArgument(cli.Argument): TARGET_TRACK_NAME = cli.ArgumentProperties( key="target_track_name", flags=("--target-track",), - description=f"Name of the track to which releases are promoted to. For example `{Colors.WHITE('alpha')}`", + description=f"Name of the track to which releases are promoted. For example `{Colors.WHITE('alpha')}`", argparse_kwargs={"required": True}, ) PROMOTED_STATUS = cli.ArgumentProperties( key="promoted_status", flags=("--release-status",), type=ReleaseStatus, - description="Promoted release status in the target track", + description="Status of the promoted release in the target track", argparse_kwargs={ "required": False, "default": ReleaseStatus.COMPLETED, @@ -81,14 +81,14 @@ class PromoteArgument(cli.Argument): PROMOTE_VERSION_CODE = cli.ArgumentProperties( key="promote_version_code", flags=("--version-code-filter",), - description="Promote only release from source track that contains specified version code", + description="Promote only a source track release that contains the specified version code", argparse_kwargs={"required": False}, ) PROMOTE_STATUS = cli.ArgumentProperties( key="promote_status", flags=("--release-status-filter",), type=ReleaseStatus, - description="Promote only release from source track with specified status", + description="Promote only a source track release with the specified status", argparse_kwargs={ "required": False, "choices": list(ReleaseStatus), From 6df4303087d4fdbe474503703105b6b2ac30163a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Wed, 13 Sep 2023 16:30:15 +0300 Subject: [PATCH 13/14] Apply suggestions --- docs/google-play/tracks/promote-release.md | 2 +- src/codemagic/tools/google_play/arguments.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/google-play/tracks/promote-release.md b/docs/google-play/tracks/promote-release.md index f55d6dab..3b8f3417 100644 --- a/docs/google-play/tracks/promote-release.md +++ b/docs/google-play/tracks/promote-release.md @@ -26,7 +26,7 @@ Package name of the app in Google Play Console. For example `com.example.app` ##### `--source-track=SOURCE_TRACK_NAME` -Name of the track from where releases are promoted from. For example `internal` +Name of the track from where releases are promoted. For example `internal` ##### `--target-track=TARGET_TRACK_NAME` diff --git a/src/codemagic/tools/google_play/arguments.py b/src/codemagic/tools/google_play/arguments.py index 9fc3a5b4..e90daf10 100644 --- a/src/codemagic/tools/google_play/arguments.py +++ b/src/codemagic/tools/google_play/arguments.py @@ -45,9 +45,7 @@ class PromoteArgument(cli.Argument): SOURCE_TRACK_NAME = cli.ArgumentProperties( key="source_track_name", flags=("--source-track",), - description=( - f"Name of the track from where releases are promoted from. For example `{Colors.WHITE('internal')}`" - ), + description=(f"Name of the track from where releases are promoted. For example `{Colors.WHITE('internal')}`"), argparse_kwargs={"required": True}, ) TARGET_TRACK_NAME = cli.ArgumentProperties( From 2d62d6e218f5018ac502bcb326420771e86b260b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Thu, 14 Sep 2023 12:04:59 +0300 Subject: [PATCH 14/14] Split parametrized test into two separate test cases --- .../test_bounded_number.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/tests/cli/argument/common_argument_types/test_bounded_number.py b/tests/cli/argument/common_argument_types/test_bounded_number.py index c8aecb85..76b893b3 100644 --- a/tests/cli/argument/common_argument_types/test_bounded_number.py +++ b/tests/cli/argument/common_argument_types/test_bounded_number.py @@ -52,27 +52,21 @@ def test_within_bounds(number_as_string, inclusive_comparison, expected_number): assert resolved_value == expected_number -@pytest.mark.parametrize( - ("out_of_bounds_number_string", "inclusive_comparison"), - ( - ("-1", True), - ("0", True), - ("11", True), - ("20", True), - ("-1", False), - ("1", False), - ("10", False), - ("11", False), - ("20", False), - ), -) -def test_out_of_bounds(out_of_bounds_number_string, inclusive_comparison): - constructor = CommonArgumentTypes.bounded_number(int, 1, 10, inclusive=inclusive_comparison) +@pytest.mark.parametrize("out_of_bounds_number_string", ("-1", "0", "11", "20")) +def test_out_of_bounds_inclusive(out_of_bounds_number_string): + constructor = CommonArgumentTypes.bounded_number(int, 1, 10, inclusive=True) + with pytest.raises(ArgumentTypeError) as error_info: + constructor(out_of_bounds_number_string) + + expected_error_message = f"Value {out_of_bounds_number_string} is out of allowed bounds, 1 <= value <= 10" + assert str(error_info.value) == expected_error_message + + +@pytest.mark.parametrize("out_of_bounds_number_string", ("-1", "1", "10", "11", "20")) +def test_out_of_bounds_exclusive(out_of_bounds_number_string): + constructor = CommonArgumentTypes.bounded_number(int, 1, 10, inclusive=False) with pytest.raises(ArgumentTypeError) as error_info: constructor(out_of_bounds_number_string) - comparison_op = "<=" if inclusive_comparison else "<" - expected_error_message = ( - f"Value {out_of_bounds_number_string} is out of allowed bounds, 1 {comparison_op} value {comparison_op} 10" - ) + expected_error_message = f"Value {out_of_bounds_number_string} is out of allowed bounds, 1 < value < 10" assert str(error_info.value) == expected_error_message