diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c78cede..2640e070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ UNRELEASED **Bugfixes** - Fix App Store Connect API responses deserialization for cases when resource contains an empty relationship. [PR #401](https://github.com/codemagic-ci-cd/cli-tools/pull/401) +- **Development** +- Add missing attributes and relationships to `codemagic.apple.resources.App` and `codemagic.apple.resources.Build`. [PR #383](https://github.com/codemagic-ci-cd/cli-tools/pull/383) +- Define new enumerations `codemagic.apple.resources.enums.BuildAudienceType` and `codemagic.apple.resources.enums.SubscriptionStatusUrlVersion`. [PR #383](https://github.com/codemagic-ci-cd/cli-tools/pull/383) + Version 0.50.7 ------------- diff --git a/src/codemagic/apple/resources/__init__.py b/src/codemagic/apple/resources/__init__.py index ab0e4ac6..0eba492f 100644 --- a/src/codemagic/apple/resources/__init__.py +++ b/src/codemagic/apple/resources/__init__.py @@ -16,6 +16,7 @@ from .device import Device from .enums import AppStoreState from .enums import BetaReviewState +from .enums import BuildAudienceType from .enums import BuildProcessingState from .enums import BundleIdPlatform from .enums import CapabilityOptionKey @@ -36,6 +37,7 @@ from .enums import ResourceType from .enums import ReviewSubmissionItemState from .enums import ReviewSubmissionState +from .enums import SubscriptionStatusUrlVersion from .error_response import ErrorResponse from .pre_release_version import PreReleaseVersion from .profile import Profile diff --git a/src/codemagic/apple/resources/app.py b/src/codemagic/apple/resources/app.py index 91750d45..841f90eb 100644 --- a/src/codemagic/apple/resources/app.py +++ b/src/codemagic/apple/resources/app.py @@ -5,6 +5,7 @@ from .enums import ContentRightsDeclaration from .enums import Locale +from .enums import SubscriptionStatusUrlVersion from .resource import Relationship from .resource import Resource @@ -26,15 +27,49 @@ class Attributes(Resource.Attributes): contentRightsDeclaration: ContentRightsDeclaration isOrEverWasMadeForKids: bool + subscriptionStatusUrl: Optional[str] = None + subscriptionStatusUrlForSandbox: Optional[str] = None + subscriptionStatusUrlVersion: Optional[SubscriptionStatusUrlVersion] = None + subscriptionStatusUrlVersionForSandbox: Optional[SubscriptionStatusUrlVersion] = None + def __post_init__(self): if isinstance(self.contentRightsDeclaration, str): self.contentRightsDeclaration = ContentRightsDeclaration(self.contentRightsDeclaration) if isinstance(self.primaryLocale, str): self.primaryLocale = Locale(self.primaryLocale) + if isinstance(self.subscriptionStatusUrlVersion, str): + self.subscriptionStatusUrlVersion = SubscriptionStatusUrlVersion(self.subscriptionStatusUrlVersion) + if isinstance(self.subscriptionStatusUrlVersionForSandbox, str): + self.subscriptionStatusUrlVersionForSandbox = SubscriptionStatusUrlVersion( + self.subscriptionStatusUrlVersionForSandbox, + ) @dataclass class Relationships(Resource.Relationships): - _OMIT_IF_NONE_KEYS = ("betaTesters", "ciProduct", "perfPowerMetrics") + _OMIT_IF_NONE_KEYS = ( + "alternativeDistributionKey", + "analyticsReportRequests", + "appAvailability", + "appAvailabilityV2", + "appClips", + "appCustomProductPages", + "appEvents", + "appPricePoints", + "appPriceSchedule", + "appStoreVersionExperimentsV2", + "betaTesters", + "ciProduct", + "customerReviews", + "gameCenterDetail", + "inAppPurchasesV2", + "marketplaceSearchDetail", + "perfPowerMetrics", + "pricePoints", + "promotedPurchases", + "reviewSubmissions", + "subscriptionGracePeriod", + "subscriptionGroups", + ) appInfos: Relationship appStoreVersions: Relationship @@ -49,6 +84,25 @@ class Relationships(Resource.Relationships): preOrder: Relationship preReleaseVersions: Relationship + alternativeDistributionKey: Optional[Relationship] = None + analyticsReportRequests: Optional[Relationship] = None + appAvailability: Optional[Relationship] = None + appAvailabilityV2: Optional[Relationship] = None + appClips: Optional[Relationship] = None + appCustomProductPages: Optional[Relationship] = None + appEvents: Optional[Relationship] = None + appPricePoints: Optional[Relationship] = None + appPriceSchedule: Optional[Relationship] = None + appStoreVersionExperimentsV2: Optional[Relationship] = None betaTesters: Optional[Relationship] = None ciProduct: Optional[Relationship] = None + customerReviews: Optional[Relationship] = None + gameCenterDetail: Optional[Relationship] = None + inAppPurchasesV2: Optional[Relationship] = None + marketplaceSearchDetail: Optional[Relationship] = None perfPowerMetrics: Optional[Relationship] = None + pricePoints: Optional[Relationship] = None + promotedPurchases: Optional[Relationship] = None + reviewSubmissions: Optional[Relationship] = None + subscriptionGracePeriod: Optional[Relationship] = None + subscriptionGroups: Optional[Relationship] = None diff --git a/src/codemagic/apple/resources/build.py b/src/codemagic/apple/resources/build.py index 2b1f1a60..44603e8a 100644 --- a/src/codemagic/apple/resources/build.py +++ b/src/codemagic/apple/resources/build.py @@ -4,6 +4,7 @@ from datetime import datetime from typing import Optional +from .enums import BuildAudienceType from .enums import BuildProcessingState from .resource import DictSerializable from .resource import Relationship @@ -55,8 +56,15 @@ class Attributes(Resource.Attributes): usesNonExemptEncryption: bool uploadedDate: datetime expirationDate: datetime + buildAudienceType: BuildAudienceType + + computedMinMacOsVersion: Optional[str] = None + lsMinimumSystemVersion: Optional[str] = None + computedMinVisionOsVersion: Optional[str] = None def __post_init__(self): + if isinstance(self.buildAudienceType, str): + self.buildAudienceType = BuildAudienceType(self.buildAudienceType) if isinstance(self.processingState, str): self.processingState = BuildProcessingState(self.processingState) if isinstance(self.uploadedDate, str): diff --git a/src/codemagic/apple/resources/enums.py b/src/codemagic/apple/resources/enums.py index 66662da4..391326d9 100644 --- a/src/codemagic/apple/resources/enums.py +++ b/src/codemagic/apple/resources/enums.py @@ -52,6 +52,15 @@ class BuildProcessingState(ResourceEnum): VALID = "VALID" +class BuildAudienceType(ResourceEnum): + """ + https://developer.apple.com/documentation/appstoreconnectapi/buildaudiencetype + """ + + INTERNAL_ONLY = "INTERNAL_ONLY" + APP_STORE_ELIGIBLE = "APP_STORE_ELIGIBLE" + + class BundleIdPlatform(ResourceEnum): IOS = "IOS" MAC_OS = "MAC_OS" @@ -392,3 +401,14 @@ class Locale(ResourceEnum): VI = "vi" ZH_HANS = "zh-Hans" ZH_HANT = "zh-Hant" + + +class SubscriptionStatusUrlVersion(ResourceEnum): + """ + https://developer.apple.com/documentation/appstoreconnectapi/subscriptionstatusurlversion + """ + + V1 = "V1" + V2 = "V2" + v1 = "v1" + v2 = "v2" diff --git a/src/codemagic/apple/resources/resource.py b/src/codemagic/apple/resources/resource.py index daa0d72e..2aa88993 100644 --- a/src/codemagic/apple/resources/resource.py +++ b/src/codemagic/apple/resources/resource.py @@ -317,7 +317,8 @@ def _format_attribute_name(self, name: str) -> str: type_prefix = self.type.value.rstrip("s") name = re.sub(f"{type_prefix}s?", "", name) name = re.sub(r"([a-z])([A-Z])", r"\1 \2", name) - return name.lower().capitalize() + name = name.lower().capitalize() + return re.sub("(i|vision|mac) os ", r"\1OS", name) def _hide_attribute_value(self, attribute_name: str) -> bool: if not hasattr(self.attributes, "__dataclass_fields__"): diff --git a/tests/apple/resources/mocks/build.json b/tests/apple/resources/mocks/build.json index 3ee07173..b0b8c58a 100644 --- a/tests/apple/resources/mocks/build.json +++ b/tests/apple/resources/mocks/build.json @@ -7,12 +7,16 @@ "expirationDate":"2021-05-03T04:00:19-07:00", "expired":false, "minOsVersion":"10.2", + "lsMinimumSystemVersion":null, + "computedMinMacOsVersion":"11.0", + "computedMinVisionOsVersion":"1.0", "iconAssetToken":{ "templateUrl":"https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/80/d3/39/80d339cc-1ce8-0987-e3c0-25fc8de3275a/Icon-83.5@2x.png.png/{w}x{h}bb.{f}", "width":167, "height":167 }, "processingState":"VALID", + "buildAudienceType":"APP_STORE_ELIGIBLE", "usesNonExemptEncryption":false }, "relationships":{ diff --git a/tests/apple/resources/test_app_resource.py b/tests/apple/resources/test_app_resource.py index 702c5571..6fffad44 100644 --- a/tests/apple/resources/test_app_resource.py +++ b/tests/apple/resources/test_app_resource.py @@ -3,6 +3,11 @@ from codemagic.apple.resources import App +def test_app_initialization(api_app): + app = App(api_app) + assert app.dict() == api_app + + def test_app_with_empty_relationship(api_app): api_app["relationships"]["ciProduct"] = {} app = App(api_app) diff --git a/tests/tools/app_store_connect/action_groups/__init__.py b/tests/tools/app_store_connect/action_groups/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tools/app_store_connect/action_groups/test_apps_action_group.py b/tests/tools/app_store_connect/action_groups/test_apps_action_group.py new file mode 100644 index 00000000..b81082f1 --- /dev/null +++ b/tests/tools/app_store_connect/action_groups/test_apps_action_group.py @@ -0,0 +1,16 @@ +import os + +import pytest +from codemagic.apple.resources import App +from codemagic.apple.resources import ResourceId +from codemagic.tools import AppStoreConnect + + +@pytest.mark.skipif( + not os.environ.get("RUN_LIVE_API_TESTS"), + reason="Live App Store Connect API access", +) +def test_get_app(app_store_connect: AppStoreConnect): + app = app_store_connect.get_app(ResourceId("1481211155")) + assert isinstance(app, App) + assert app.id == "1481211155" diff --git a/tests/tools/app_store_connect/conftest.py b/tests/tools/app_store_connect/conftest.py index 8c167355..b7d4d353 100644 --- a/tests/tools/app_store_connect/conftest.py +++ b/tests/tools/app_store_connect/conftest.py @@ -1,5 +1,6 @@ from __future__ import annotations +import argparse import os import pathlib @@ -42,3 +43,27 @@ def namespace_kwargs(mock_auth_key): continue os.environ.pop(arg.type.environment_variable_key, None) return ns_kwargs + + +@pytest.fixture() +def app_store_connect(namespace_kwargs) -> AppStoreConnect: + args = AppStoreConnectArgument + if "TEST_APPLE_PRIVATE_KEY_PATH" in os.environ: + key_path = pathlib.Path(os.environ["TEST_APPLE_PRIVATE_KEY_PATH"]) + private_key = key_path.expanduser().read_text() + key_identifier = os.environ["TEST_APPLE_KEY_IDENTIFIER"] + issuer_id = os.environ["TEST_APPLE_ISSUER_ID"] + elif "TEST_APPLE_PRIVATE_KEY_CONTENT" in os.environ: + private_key = os.environ["TEST_APPLE_PRIVATE_KEY_CONTENT"] + key_identifier = os.environ["TEST_APPLE_KEY_IDENTIFIER"] + issuer_id = os.environ["TEST_APPLE_ISSUER_ID"] + else: + raise RuntimeError("Missing App Store Connect authentication information") + + ns = namespace_kwargs | { + args.ISSUER_ID.key: Types.IssuerIdArgument(issuer_id), + args.KEY_IDENTIFIER.key: Types.KeyIdentifierArgument(key_identifier), + args.PRIVATE_KEY.key: Types.PrivateKeyArgument(private_key), + } + cli_args = argparse.Namespace(**ns) + return AppStoreConnect.from_cli_args(cli_args)