From e5e8f00f2ff23dac94a8ef413430e364ed1986f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Mon, 17 May 2021 11:56:55 +0300 Subject: [PATCH] Feature: Add actions to `app-store-connect` to manage App Store Version submissions (#85) * Add option to group CLI actions into categories * Add client actions to manage App Store Version submissions * Bump version and update changelog * Update error response model to respect error meta and associated errors * Fix do not include meta in error serialization if it is not defined * update docs with new generation * remove not needed file Co-authored-by: Stanislav Bondarenko --- CHANGELOG.md | 11 +- doc.py | 127 +++++++++++++----- docs/app-store-connect/README.md | 6 + .../app-store-version-submissions.md | 80 +++++++++++ .../app-store-version-submissions/create.md | 80 +++++++++++ .../app-store-version-submissions/delete.md | 87 ++++++++++++ .../apple/app_store_connect/api_client.py | 5 + .../app_store_connect/versioning/__init__.py | 1 + .../app_store_version_submissions.py | 41 ++++++ src/codemagic/apple/resources/__init__.py | 1 + .../resources/app_store_version_submission.py | 16 +++ src/codemagic/apple/resources/enums.py | 3 +- .../apple/resources/error_response.py | 39 +++++- .../tools/_app_store_connect/action_group.py | 8 ++ .../tools/_app_store_connect/arguments.py | 10 ++ src/codemagic/tools/app_store_connect.py | 33 +++++ .../versioning/test_app_store_versions.py | 2 +- 17 files changed, 510 insertions(+), 40 deletions(-) create mode 100644 docs/app-store-connect/app-store-version-submissions.md create mode 100644 docs/app-store-connect/app-store-version-submissions/create.md create mode 100644 docs/app-store-connect/app-store-version-submissions/delete.md create mode 100644 src/codemagic/apple/app_store_connect/versioning/app_store_version_submissions.py create mode 100644 src/codemagic/apple/resources/app_store_version_submission.py create mode 100644 src/codemagic/tools/_app_store_connect/action_group.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a631697b..2b86f920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,18 @@ Version 0.6.0 **New features** +- Add action group support for tools. - Add action `get-profile` to `app-store-connect` to show provisioning profile based on resource identifier. +- Add action `app-store-connect app-store-version-submissions create` to submit App Store Version to review. +- Add action `app-store-connect app-store-version-submissions delete` to remove App Store Version from review. **Development / Docs** -- Add documentation for new action `app-store-connect get-profile`. -- Add `SERVICES` as valid value to `--platform` option in `app-store-connect` actions. -- Update `--profile` option default values for action `xcode-project use-profiles`. +- Update `--profile` option default value in action `xcode-project use-profiles` docs. +- Generate documentation for action groups and list groups under tool documentation pages. +- Add documentation for action `app-store-connect get-profile`. +- Add documentation for action `app-store-connect app-store-version-submissions create`. +- Add documentation for action `app-store-connect app-store-version-submissions delete`. Version 0.5.9 ------------- diff --git a/doc.py b/doc.py index bf82fa67..4458c2a0 100755 --- a/doc.py +++ b/doc.py @@ -11,6 +11,7 @@ from typing import Iterable from typing import List from typing import NamedTuple +from typing import Optional from mdutils.mdutils import MdUtils from mdutils.tools.Table import Table @@ -40,6 +41,12 @@ class Action(NamedTuple): optional_args: List[SerializedArgument] +class ActionGroup(NamedTuple): + name: str + description: str + actions: List[Action] + + class ArgumentKwargs(NamedTuple): nargs: bool required: bool @@ -129,34 +136,68 @@ def __init__(self, tool, main_dir: str): self.tool_required_args = class_args_serializer.required_args self.tool_options = self._serialize_default_options(self.tool) self.tool_serialized_actions = self._serialize_actions(self.tool) + self.tool_serialized_action_groups = self._serialize_action_groups(self.tool) def generate(self): - def _write_tool_command_arguments_and_options(): - writer.write_arguments(f'command `{self.tool_command}`', self.tool_optional_args, self.tool_required_args) - writer.write_options(self.tool_options) + self._write_tool_page() - # docs//README.md + for action in self.tool_serialized_actions: + self._write_action_page(action) + + for group in self.tool_serialized_action_groups: + self._write_action_group_page(group) + + def _write_tool_command_arguments_and_options(self, writer): + writer.write_arguments(f'command `{self.tool_command}`', self.tool_optional_args, self.tool_required_args) + writer.write_options(self.tool_options) + + def _write_tool_page(self): os.makedirs(self.tool_prefix, exist_ok=True) md = MdUtils(file_name=f'{self.tool_prefix}/README', title=self.tool_command) writer = Writer(md) writer.write_description(self.tool.__doc__) - writer.write_tool_command_usage(self) - _write_tool_command_arguments_and_options() + writer.write_command_usage(self) + self._write_tool_command_arguments_and_options(writer) writer.write_actions_table(self.tool_serialized_actions) + writer.write_action_groups_table(self.tool_serialized_action_groups) md.create_md_file() - for action in self.tool_serialized_actions: - # docs//.md - md = MdUtils(file_name=f'{self.tool_prefix}/{action.action_name}', title=action.action_name) - writer = Writer(md) - writer.write_description(action.description) - writer.write_action_command_usage(self, action) - writer.write_arguments(f'action `{action.action_name}`', action.optional_args, action.required_args) - _write_tool_command_arguments_and_options() - md.create_md_file() + def _write_action_group_page(self, action_group: ActionGroup): + group_path = f'{self.tool_prefix}/{action_group.name}' + md = MdUtils(file_name=group_path, title=action_group.name) + writer = Writer(md) + writer.write_description(action_group.description) + writer.write_command_usage(self, action_group=action_group) + self._write_tool_command_arguments_and_options(writer) + writer.write_actions_table(action_group.actions, action_group=action_group) + md.create_md_file() + os.makedirs(group_path, exist_ok=True) + for action in action_group.actions: + self._write_action_page(action, action_group=action_group) + + def _write_action_page(self, action: Action, action_group: Optional[ActionGroup] = None): + group_str = f'{action_group.name}/' if action_group else '' + md = MdUtils(file_name=f'{self.tool_prefix}/{group_str}{action.action_name}', title=action.action_name) + writer = Writer(md) + writer.write_description(action.description) + writer.write_command_usage(self, action_group=action_group, action=action) + writer.write_arguments(f'action `{action.action_name}`', action.optional_args, action.required_args) + self._write_tool_command_arguments_and_options(writer) + md.create_md_file() @classmethod - def _serialize_actions(cls, tool: cli.CliApp) -> List[Action]: + def _serialize_action_groups(cls, tool: cli.CliApp) -> List[ActionGroup]: + def _serialize_action_group(group) -> ActionGroup: + return ActionGroup( + name=group.name, + description=group.description, + actions=cls._serialize_actions(tool, action_group=group), + ) + + return list(map(_serialize_action_group, tool.list_class_action_groups())) + + @classmethod + def _serialize_actions(cls, tool: cli.CliApp, action_group=None) -> List[Action]: def _serialize_action(action: Callable) -> Action: action_args_serializer = ArgumentsSerializer(action.arguments).serialize() return Action( @@ -167,7 +208,7 @@ def _serialize_action(action: Callable) -> Action: optional_args=action_args_serializer.optional_args, ) - return list(map(_serialize_action, tool.iter_class_cli_actions())) + return list(map(_serialize_action, tool.iter_class_cli_actions(action_group=action_group))) @classmethod def _serialize_default_options(cls, tool: cli.CliApp) -> List[SerializedArgument]: @@ -195,19 +236,21 @@ class CommandUsageGenerator: def __init__(self, doc_generator: ToolDocumentationGenerator): self.doc_generator = doc_generator - def get_tool_command_usage(self) -> List[str]: - return [ - f'{self.doc_generator.tool_command} {self._get_opt_common_flags()}', - *self._get_tool_arguments_and_flags(), - 'ACTION', - ] + def get_command_usage(self, + action_group: Optional[ActionGroup] = None, + action: Optional[Action] = None) -> List[str]: + action_group_str = f' {action_group.name}' if action_group else '' + action_str = f' {action.action_name}' if action else '' - def get_action_command_usage(self, action: Action) -> List[str]: - return [ - f'{self.doc_generator.tool_command} {action.action_name} {self._get_opt_common_flags()}', - *self._get_tool_arguments_and_flags(), + action_args = [ *map(self._get_formatted_flag, action.optional_args), *map(self._get_formatted_flag, action.required_args), + ] if action else ['ACTION'] + + return [ + f'{self.doc_generator.tool_command}{action_group_str}{action_str} {self._get_opt_common_flags()}', + *self._get_tool_arguments_and_flags(), + *action_args, ] def _get_opt_common_flags(self) -> str: @@ -239,11 +282,12 @@ def write_description(self, content: str): content = str_plain(content) self.file.new_paragraph(f'**{content}**') - def write_tool_command_usage(self, generator: ToolDocumentationGenerator): - self._write_command_usage(self.file, CommandUsageGenerator(generator).get_tool_command_usage()) - - def write_action_command_usage(self, generator: ToolDocumentationGenerator, action: Action): - self._write_command_usage(self.file, CommandUsageGenerator(generator).get_action_command_usage(action)) + def write_command_usage(self, + generator: ToolDocumentationGenerator, + action_group: Optional[ActionGroup] = None, + action: Optional[Action] = None): + lines = CommandUsageGenerator(generator).get_command_usage(action_group=action_group, action=action) + self._write_command_usage(self.file, lines) def write_table(self, content: List[List[str]], header: List[str]): flat_content: List[str] = sum(content, []) @@ -264,13 +308,28 @@ def _get_tool_doc(tool: cli.CliApp) -> List[str]: self.write_table(list(map(_get_tool_doc, tools)), ['Tool name', 'Description']) - def write_actions_table(self, actions: List[Action]): + def write_actions_table(self, actions: List[Action], action_group: Optional[ActionGroup] = None): + if not actions: + return + def _get_action_doc(action: Action) -> List[str]: - return [f'[`{action.action_name}`]({action.action_name}.md)', str_plain(action.description)] + action_group_str = f'{action_group.name}/' if action_group else '' + action_link = f'{action_group_str}{action.action_name}.md' + return [f'[`{action.action_name}`]({action_link})', str_plain(action.description)] self.file.new_header(level=3, title='Actions', add_table_of_contents='n') self.write_table(list(map(_get_action_doc, actions)), ['Action', 'Description']) + def write_action_groups_table(self, groups: List[ActionGroup]): + if not groups: + return + + def _get_group_doc(group: ActionGroup) -> List[str]: + return [f'[`{group.name}`]({group.name}.md)', str_plain(group.description)] + + self.file.new_header(level=3, title='Action groups', add_table_of_contents='n') + self.write_table(list(map(_get_group_doc, groups)), ['Action group', 'Description']) + def write_arguments(self, obj: str, optional: List[SerializedArgument], required: List[SerializedArgument]): self._write_arguments(self.file, f'Required arguments for {obj}', required) self._write_arguments(self.file, f'Optional arguments for {obj}', optional) diff --git a/docs/app-store-connect/README.md b/docs/app-store-connect/README.md index 0e34a9f1..12ebdac9 100644 --- a/docs/app-store-connect/README.md +++ b/docs/app-store-connect/README.md @@ -94,3 +94,9 @@ Enable verbose logging for commands |[`list-certificates`](list-certificates.md)|List Signing Certificates from Apple Developer Portal matching given constraints| |[`list-devices`](list-devices.md)|List Devices from Apple Developer portal matching given constraints| |[`list-profiles`](list-profiles.md)|List Profiles from Apple Developer portal matching given constraints| + +### Action groups + +|Action group|Description| +| :--- | :--- | +|[`app-store-version-submissions`](app-store-version-submissions.md)|Manage your application's App Store version review process| diff --git a/docs/app-store-connect/app-store-version-submissions.md b/docs/app-store-connect/app-store-version-submissions.md new file mode 100644 index 00000000..4eff11b0 --- /dev/null +++ b/docs/app-store-connect/app-store-version-submissions.md @@ -0,0 +1,80 @@ + +app-store-version-submissions +============================= + + +**Manage your application's App Store version review process** +### Usage +```bash +app-store-connect app-store-version-submissions [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--log-api-calls] + [--json] + [--issuer-id ISSUER_ID] + [--key-id KEY_IDENTIFIER] + [--private-key PRIVATE_KEY] + [--certificates-dir CERTIFICATES_DIRECTORY] + [--profiles-dir PROFILES_DIRECTORY] + ACTION +``` +### Optional arguments for command `app-store-connect` + +##### `--log-api-calls` + + +Turn on logging for App Store Connect API HTTP requests +##### `--json` + + +Whether to show the resource in JSON format +##### `--issuer-id=ISSUER_ID` + + +App Store Connect API Key Issuer ID. Identifies the issuer who created the authentication token. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering` ISSUER_ID `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--key-id=KEY_IDENTIFIER` + + +App Store Connect API Key ID. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering` KEY_IDENTIFIER `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--private-key=PRIVATE_KEY` + + +App Store Connect API private key. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering` PRIVATE_KEY `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--certificates-dir=CERTIFICATES_DIRECTORY` + + +Directory where the code signing certificates will be saved. Default: `$HOME/Library/MobileDevice/Certificates` +##### `--profiles-dir=PROFILES_DIRECTORY` + + +Directory where the provisioning profiles will be saved. Default: `$HOME/Library/MobileDevice/Provisioning Profiles` +### 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 +### Actions + +|Action|Description| +| :--- | :--- | +|[`create`](app-store-version-submissions/create.md)|Submit an App Store Version to App Review| +|[`delete`](app-store-version-submissions/delete.md)|Remove a version submission from App Store review| diff --git a/docs/app-store-connect/app-store-version-submissions/create.md b/docs/app-store-connect/app-store-version-submissions/create.md new file mode 100644 index 00000000..8fe538c9 --- /dev/null +++ b/docs/app-store-connect/app-store-version-submissions/create.md @@ -0,0 +1,80 @@ + +create +====== + + +**Submit an App Store Version to App Review** +### Usage +```bash +app-store-connect app-store-version-submissions create [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--log-api-calls] + [--json] + [--issuer-id ISSUER_ID] + [--key-id KEY_IDENTIFIER] + [--private-key PRIVATE_KEY] + [--certificates-dir CERTIFICATES_DIRECTORY] + [--profiles-dir PROFILES_DIRECTORY] + APP_STORE_VERSION_ID +``` +### Required arguments for action `create` + +##### `APP_STORE_VERSION_ID` + + +UUID value of the App Store Version +### Optional arguments for command `app-store-connect` + +##### `--log-api-calls` + + +Turn on logging for App Store Connect API HTTP requests +##### `--json` + + +Whether to show the resource in JSON format +##### `--issuer-id=ISSUER_ID` + + +App Store Connect API Key Issuer ID. Identifies the issuer who created the authentication token. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering` ISSUER_ID `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--key-id=KEY_IDENTIFIER` + + +App Store Connect API Key ID. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering` KEY_IDENTIFIER `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--private-key=PRIVATE_KEY` + + +App Store Connect API private key. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering` PRIVATE_KEY `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--certificates-dir=CERTIFICATES_DIRECTORY` + + +Directory where the code signing certificates will be saved. Default: `$HOME/Library/MobileDevice/Certificates` +##### `--profiles-dir=PROFILES_DIRECTORY` + + +Directory where the provisioning profiles will be saved. Default: `$HOME/Library/MobileDevice/Provisioning Profiles` +### 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 \ No newline at end of file diff --git a/docs/app-store-connect/app-store-version-submissions/delete.md b/docs/app-store-connect/app-store-version-submissions/delete.md new file mode 100644 index 00000000..c5eb7a3f --- /dev/null +++ b/docs/app-store-connect/app-store-version-submissions/delete.md @@ -0,0 +1,87 @@ + +delete +====== + + +**Remove a version submission from App Store review** +### Usage +```bash +app-store-connect app-store-version-submissions delete [-h] [--log-stream STREAM] [--no-color] [--version] [-s] [-v] + [--log-api-calls] + [--json] + [--issuer-id ISSUER_ID] + [--key-id KEY_IDENTIFIER] + [--private-key PRIVATE_KEY] + [--certificates-dir CERTIFICATES_DIRECTORY] + [--profiles-dir PROFILES_DIRECTORY] + [--ignore-not-found] + APP_STORE_VERSION_SUBMISSION_ID +``` +### Required arguments for action `delete` + +##### `APP_STORE_VERSION_SUBMISSION_ID` + + +UUID value of the App Store Version Submission +### Optional arguments for action `delete` + +##### `--ignore-not-found` + + +Do not raise exceptions if the specified resource does not exist. +### Optional arguments for command `app-store-connect` + +##### `--log-api-calls` + + +Turn on logging for App Store Connect API HTTP requests +##### `--json` + + +Whether to show the resource in JSON format +##### `--issuer-id=ISSUER_ID` + + +App Store Connect API Key Issuer ID. Identifies the issuer who created the authentication token. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_ISSUER_ID`. Alternatively to entering` ISSUER_ID `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--key-id=KEY_IDENTIFIER` + + +App Store Connect API Key ID. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_KEY_IDENTIFIER`. Alternatively to entering` KEY_IDENTIFIER `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--private-key=PRIVATE_KEY` + + +App Store Connect API private key. Learn more at https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api. If not given, the value will be checked from environment variable `APP_STORE_CONNECT_PRIVATE_KEY`. Alternatively to entering` PRIVATE_KEY `in plaintext, it may also be specified using a `@env:` prefix followed by a environment variable name, or `@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 file at ``. +##### `--certificates-dir=CERTIFICATES_DIRECTORY` + + +Directory where the code signing certificates will be saved. Default: `$HOME/Library/MobileDevice/Certificates` +##### `--profiles-dir=PROFILES_DIRECTORY` + + +Directory where the provisioning profiles will be saved. Default: `$HOME/Library/MobileDevice/Provisioning Profiles` +### 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 \ No newline at end of file diff --git a/src/codemagic/apple/app_store_connect/api_client.py b/src/codemagic/apple/app_store_connect/api_client.py index 2cb9902d..8c4f6fc5 100644 --- a/src/codemagic/apple/app_store_connect/api_client.py +++ b/src/codemagic/apple/app_store_connect/api_client.py @@ -21,6 +21,7 @@ from .provisioning import Profiles from .provisioning import SigningCertificates from .versioning import AppStoreVersions +from .versioning import AppStoreVersionSubmissions from .versioning import PreReleaseVersions @@ -122,6 +123,10 @@ def paginate_with_included(self, url, params=None, page_size: Optional[int] = 10 def app_store_versions(self) -> AppStoreVersions: return AppStoreVersions(self) + @property + def app_store_version_submissions(self) -> AppStoreVersionSubmissions: + return AppStoreVersionSubmissions(self) + @property def builds(self) -> Builds: return Builds(self) diff --git a/src/codemagic/apple/app_store_connect/versioning/__init__.py b/src/codemagic/apple/app_store_connect/versioning/__init__.py index c2b9cf77..445d9b84 100644 --- a/src/codemagic/apple/app_store_connect/versioning/__init__.py +++ b/src/codemagic/apple/app_store_connect/versioning/__init__.py @@ -1,2 +1,3 @@ +from .app_store_version_submissions import AppStoreVersionSubmissions from .app_store_versions import AppStoreVersions from .pre_release_versions import PreReleaseVersions diff --git a/src/codemagic/apple/app_store_connect/versioning/app_store_version_submissions.py b/src/codemagic/apple/app_store_connect/versioning/app_store_version_submissions.py new file mode 100644 index 00000000..54c34b68 --- /dev/null +++ b/src/codemagic/apple/app_store_connect/versioning/app_store_version_submissions.py @@ -0,0 +1,41 @@ +from typing import Type +from typing import Union + +from codemagic.apple.app_store_connect.resource_manager import ResourceManager +from codemagic.apple.resources import AppStoreVersion +from codemagic.apple.resources import AppStoreVersionSubmission +from codemagic.apple.resources import LinkedResourceData +from codemagic.apple.resources import ResourceId +from codemagic.apple.resources import ResourceType + + +class AppStoreVersionSubmissions(ResourceManager[AppStoreVersionSubmission]): + """ + App Store Version Submissions + https://developer.apple.com/documentation/appstoreconnectapi/app_store_version_submissions + """ + + @property + def resource_type(self) -> Type[AppStoreVersionSubmission]: + return AppStoreVersionSubmission + + def create(self, app_store_version: Union[ResourceId, AppStoreVersion]): + """ + https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_submission + """ + relationships = { + 'appStoreVersion': { + 'data': self._get_attribute_data(app_store_version, ResourceType.APP_STORE_VERSIONS), + }, + } + payload = self._get_create_payload( + ResourceType.APP_STORE_VERSION_SUBMISSIONS, relationships=relationships) + response = self.client.session.post(f'{self.client.API_URL}/appStoreVersionSubmissions', json=payload).json() + return AppStoreVersionSubmission(response['data'], created=True) + + def delete(self, app_store_version_submission: Union[LinkedResourceData, ResourceId]): + """ + https://developer.apple.com/documentation/appstoreconnectapi/delete_an_app_store_version_submission + """ + submission_id = self._get_resource_id(app_store_version_submission) + self.client.session.delete(f'{self.client.API_URL}/appStoreVersionSubmissions/{submission_id}') diff --git a/src/codemagic/apple/resources/__init__.py b/src/codemagic/apple/resources/__init__.py index 4ac2710f..cdd3a135 100644 --- a/src/codemagic/apple/resources/__init__.py +++ b/src/codemagic/apple/resources/__init__.py @@ -1,4 +1,5 @@ from .app_store_version import AppStoreVersion +from .app_store_version_submission import AppStoreVersionSubmission from .build import Build from .bundle_id import BundleId from .bundle_id_capability import BundleIdCapability diff --git a/src/codemagic/apple/resources/app_store_version_submission.py b/src/codemagic/apple/resources/app_store_version_submission.py new file mode 100644 index 00000000..11b0b541 --- /dev/null +++ b/src/codemagic/apple/resources/app_store_version_submission.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from .resource import Relationship +from .resource import Resource + + +class AppStoreVersionSubmission(Resource): + """ + https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionsubmission + """ + + @dataclass + class Relationships(Resource.Relationships): + appStoreVersion: Relationship diff --git a/src/codemagic/apple/resources/enums.py b/src/codemagic/apple/resources/enums.py index fc64ac27..cb6ed883 100644 --- a/src/codemagic/apple/resources/enums.py +++ b/src/codemagic/apple/resources/enums.py @@ -190,7 +190,8 @@ class ReleaseType(_ResourceEnum): class ResourceType(_ResourceEnum): - APP_STORER_VERSIONS = 'appStoreVersions' + APP_STORE_VERSIONS = 'appStoreVersions' + APP_STORE_VERSION_SUBMISSIONS = 'appStoreVersionSubmissions' BUILDS = 'builds' BUNDLE_ID = 'bundleIds' BUNDLE_ID_CAPABILITIES = 'bundleIdCapabilities' diff --git a/src/codemagic/apple/resources/error_response.py b/src/codemagic/apple/resources/error_response.py index 9478ea9c..981f720d 100644 --- a/src/codemagic/apple/resources/error_response.py +++ b/src/codemagic/apple/resources/error_response.py @@ -1,7 +1,9 @@ from __future__ import annotations +import textwrap from dataclasses import dataclass from typing import Dict +from typing import List from typing import Optional from requests import Response @@ -9,14 +11,49 @@ from .resource import DictSerializable +@dataclass +class ErrorMeta(DictSerializable): + associatedErrors: Optional[Dict[str, List[Error]]] = None + + def __post_init__(self): + if self.associatedErrors is None: + return + for scope in self.associatedErrors.keys(): + self.associatedErrors[scope] = [ + Error(**error) if isinstance(error, dict) else error + for error in self.associatedErrors[scope] + ] + + def __str__(self): + lines = [] + for scope, errors in (self.associatedErrors or {}).items(): + lines.extend(f'Associated error: {e}' for e in errors) + return '\n'.join(lines) + + @dataclass class Error(DictSerializable): + _OMIT_IF_NONE_KEYS = ('meta',) + code: str status: str title: str detail: str id: Optional[str] = None source: Optional[Dict[str, str]] = None + meta: Optional[ErrorMeta] = None + + def __post_init__(self): + if isinstance(self.meta, dict): + self.meta = ErrorMeta(**self.meta) + + def __str__(self): + s = f'{self.title} - {self.detail}' + if self.meta: + meta = textwrap.indent(str(self.meta), '\t') + if meta: + s += f'\n{meta}' + return s class ErrorResponse(DictSerializable): @@ -36,4 +73,4 @@ def from_raw_response(cls, response: Response) -> ErrorResponse: return error_response def __str__(self): - return '\n'.join(f'{error.title} - {error.detail}' for error in self.errors) + return '\n'.join(map(str, self.errors)) diff --git a/src/codemagic/tools/_app_store_connect/action_group.py b/src/codemagic/tools/_app_store_connect/action_group.py new file mode 100644 index 00000000..e7d7e2ec --- /dev/null +++ b/src/codemagic/tools/_app_store_connect/action_group.py @@ -0,0 +1,8 @@ +from codemagic import cli + + +class AppStoreConnectActionGroup(cli.ActionGroup): + APP_STORE_VERSION_SUBMISSIONS = cli.ActionGroupProperties( + name='app-store-version-submissions', + description="Manage your application's App Store version review process", + ) diff --git a/src/codemagic/tools/_app_store_connect/arguments.py b/src/codemagic/tools/_app_store_connect/arguments.py index 25d638e1..20d85c62 100644 --- a/src/codemagic/tools/_app_store_connect/arguments.py +++ b/src/codemagic/tools/_app_store_connect/arguments.py @@ -111,6 +111,16 @@ class AppStoreVersionArgument(cli.Argument): ), argparse_kwargs={'required': False}, ) + APP_STORE_VERSION_ID = cli.ArgumentProperties( + key='app_store_version_id', + type=ResourceId, + description='UUID value of the App Store Version', + ) + APP_STORE_VERSION_SUBMISSION_ID = cli.ArgumentProperties( + key='app_store_version_submission_id', + type=ResourceId, + description='UUID value of the App Store Version Submission', + ) class BuildArgument(cli.Argument): diff --git a/src/codemagic/tools/app_store_connect.py b/src/codemagic/tools/app_store_connect.py index d0e6b7c9..6ce4ca30 100755 --- a/src/codemagic/tools/app_store_connect.py +++ b/src/codemagic/tools/app_store_connect.py @@ -19,6 +19,7 @@ from codemagic.apple.app_store_connect import AppStoreConnectApiClient from codemagic.apple.app_store_connect import IssuerId from codemagic.apple.app_store_connect import KeyIdentifier +from codemagic.apple.resources import AppStoreVersionSubmission from codemagic.apple.resources import Build from codemagic.apple.resources import BuildProcessingState from codemagic.apple.resources import BundleId @@ -39,6 +40,7 @@ from codemagic.models import PrivateKey from codemagic.models import ProvisioningProfile +from ._app_store_connect.action_group import AppStoreConnectActionGroup from ._app_store_connect.arguments import AppStoreConnectArgument from ._app_store_connect.arguments import AppStoreVersionArgument from ._app_store_connect.arguments import BuildArgument @@ -462,6 +464,37 @@ def list_certificates(self, return certificates + @cli.action('create', + AppStoreVersionArgument.APP_STORE_VERSION_ID, + action_group=AppStoreConnectActionGroup.APP_STORE_VERSION_SUBMISSIONS) + def create_app_store_version_submission(self, + app_store_version_id: ResourceId, + should_print: bool = True) -> AppStoreVersionSubmission: + """ + Submit an App Store Version to App Review + """ + return self._create_resource( + self.api_client.app_store_version_submissions, + should_print, + app_store_version=app_store_version_id, + ) + + @cli.action('delete', + AppStoreVersionArgument.APP_STORE_VERSION_SUBMISSION_ID, + CommonArgument.IGNORE_NOT_FOUND, + action_group=AppStoreConnectActionGroup.APP_STORE_VERSION_SUBMISSIONS) + def delete_app_store_version_submission(self, + app_store_version_submission_id: ResourceId, + ignore_not_found: bool = False) -> None: + """ + Remove a version submission from App Store review + """ + self._delete_resource( + self.api_client.app_store_version_submissions, + app_store_version_submission_id, + ignore_not_found=ignore_not_found, + ) + @cli.action('create-profile', BundleIdArgument.BUNDLE_ID_RESOURCE_ID, CertificateArgument.CERTIFICATE_RESOURCE_IDS, diff --git a/tests/apple/app_store_connect/versioning/test_app_store_versions.py b/tests/apple/app_store_connect/versioning/test_app_store_versions.py index 4d6d2b22..e03449b4 100644 --- a/tests/apple/app_store_connect/versioning/test_app_store_versions.py +++ b/tests/apple/app_store_connect/versioning/test_app_store_versions.py @@ -21,4 +21,4 @@ def test_list(self): assert len(app_store_versions) > 0 for app_store_version in app_store_versions: assert isinstance(app_store_version, AppStoreVersion) - assert app_store_version.type is ResourceType.APP_STORER_VERSIONS + assert app_store_version.type is ResourceType.APP_STORE_VERSIONS