Skip to content

Commit 80b63d2

Browse files
list versions with include param
1 parent 1fa9fc8 commit 80b63d2

4 files changed

Lines changed: 166 additions & 42 deletions

File tree

src/codemagic/apple/app_store_connect/apps/apps.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from dataclasses import dataclass
2+
from typing import Dict
23
from typing import List
34
from typing import Optional
45
from typing import Sequence
56
from typing import Type
67
from typing import Union
78

89
from codemagic.apple.app_store_connect.resource_manager import ResourceManager
10+
from codemagic.apple.app_store_connect.type_declarations import PaginateResult
911
from codemagic.apple.app_store_connect.versioning import AppStoreVersions
1012
from codemagic.apple.resources import App
1113
from codemagic.apple.resources import AppStoreState
@@ -111,6 +113,25 @@ def list_app_store_versions_data(
111113
url = f"{self.client.API_URL}/apps/{app_id}/appStoreVersions"
112114
return self.client.paginate(url, params=params, limit=limit, page_size=page_size)
113115

116+
def list_app_store_versions_data_with_include(
117+
self,
118+
app: Union[LinkedResourceData, ResourceId],
119+
include: str,
120+
resource_filter: AppStoreVersions.Filter = AppStoreVersions.Filter(),
121+
extra_params: Optional[Dict[str, str]] = None,
122+
limit: Optional[int] = None,
123+
page_size: Optional[int] = 100,
124+
) -> PaginateResult:
125+
"""
126+
https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_versions_for_an_app
127+
"""
128+
app_id = self._get_resource_id(app)
129+
params: Dict[str, str] = {"include": include, **resource_filter.as_query_params()}
130+
if extra_params:
131+
params.update(extra_params)
132+
url = f"{self.client.API_URL}/apps/{app_id}/appStoreVersions"
133+
return self.client.paginate_with_included(url, params=params, limit=limit, page_size=page_size)
134+
114135
def list_app_store_versions(
115136
self,
116137
app: Union[LinkedResourceData, ResourceId],

src/codemagic/apple/app_store_connect/builds/builds.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass
2+
from typing import Dict
23
from typing import List
34
from typing import Optional
45
from typing import Sequence
@@ -8,6 +9,7 @@
89
from typing import Union
910

1011
from codemagic.apple.app_store_connect.resource_manager import ResourceManager
12+
from codemagic.apple.app_store_connect.type_declarations import PaginateResult
1113
from codemagic.apple.resources import App
1214
from codemagic.apple.resources import AppStoreVersion
1315
from codemagic.apple.resources import BetaReviewState
@@ -83,6 +85,23 @@ def list(
8385
builds = self.client.paginate(f"{self.client.API_URL}/builds", params=params)
8486
return [Build(build) for build in builds]
8587

88+
def list_data_with_include(
89+
self,
90+
include: str,
91+
resource_filter: Filter = Filter(),
92+
extra_params: Optional[Dict[str, str]] = None,
93+
limit: Optional[int] = None,
94+
page_size: Optional[int] = 100,
95+
) -> PaginateResult:
96+
"""
97+
https://developer.apple.com/documentation/appstoreconnectapi/list_builds
98+
"""
99+
params: Dict[str, str] = {"include": include, **resource_filter.as_query_params()}
100+
if extra_params:
101+
params.update(extra_params)
102+
url = f"{self.client.API_URL}/builds"
103+
return self.client.paginate_with_included(url, params=params, limit=limit, page_size=page_size)
104+
86105
def read_app(self, build: Union[Build, ResourceId]) -> App:
87106
"""
88107
https://developer.apple.com/documentation/appstoreconnectapi/read_the_app_information_of_a_build

src/codemagic/tools/app_store_connect/actions/latest_build_number_actions.py

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -136,46 +136,108 @@ def __get_testflight_latest_build_info(
136136
self,
137137
pre_release_versions: List[_ResourceVersion],
138138
build_expired_status: Optional[bool],
139-
all_versions: bool = False,
140139
) -> Optional[_LatestBuildInfo]:
141-
candidates: List[_LatestBuildInfo] = []
142140
for pre_release_version in pre_release_versions:
143141
max_build = self.__get_pre_release_version_max_build(pre_release_version.id, build_expired_status)
144142
if not max_build:
145143
continue
146-
candidate = _LatestBuildInfo(
144+
return _LatestBuildInfo(
147145
build_id=max_build.id,
148146
build_number=max_build.version,
149147
pre_release_version=pre_release_version.version,
150148
)
151-
if not all_versions:
152-
return candidate
153-
candidates.append(candidate)
154-
if not candidates:
155-
return None
156-
return max(candidates, key=lambda info: versions.sorting_key(info.build_number))
149+
return None
157150

158151
def __get_app_store_latest_build_info(
159152
self,
160153
app_store_versions: List[_ResourceVersion],
161-
all_versions: bool = False,
162154
) -> Optional[_LatestBuildInfo]:
163-
candidates: List[_LatestBuildInfo] = []
164155
for app_store_version in app_store_versions:
165156
max_build_data = self.api_client.app_store_versions.read_build_data(
166157
ResourceId(app_store_version.id),
167158
fields=("version",),
168159
)
169160
if not max_build_data:
170161
continue
171-
candidate = _LatestBuildInfo(
162+
return _LatestBuildInfo(
172163
build_id=ResourceId(max_build_data["id"]),
173164
build_number=max_build_data["attributes"]["version"],
174165
app_store_version=app_store_version.version,
175166
)
176-
if not all_versions:
177-
return candidate
178-
candidates.append(candidate)
167+
return None
168+
169+
def __get_app_store_latest_build_info_all_versions(
170+
self,
171+
application_id: ResourceId,
172+
platform: Optional[Platform],
173+
) -> Optional[_LatestBuildInfo]:
174+
versions_filter = self.api_client.app_store_versions.Filter(platform=platform)
175+
result = self.api_client.apps.list_app_store_versions_data_with_include(
176+
application_id,
177+
include="build",
178+
resource_filter=versions_filter,
179+
extra_params={
180+
"fields[appStoreVersions]": "versionString,build",
181+
"fields[builds]": "version",
182+
},
183+
page_size=200,
184+
)
185+
builds_by_id = {b["id"]: b for b in result.included}
186+
candidates: List[_LatestBuildInfo] = []
187+
for asv in result.data:
188+
build_ref = asv.get("relationships", {}).get("build", {}).get("data")
189+
if not build_ref:
190+
continue
191+
build = builds_by_id.get(build_ref["id"])
192+
if not build:
193+
continue
194+
candidates.append(
195+
_LatestBuildInfo(
196+
build_id=ResourceId(build["id"]),
197+
build_number=build["attributes"]["version"],
198+
app_store_version=asv["attributes"]["versionString"],
199+
),
200+
)
201+
if not candidates:
202+
return None
203+
return max(candidates, key=lambda info: versions.sorting_key(info.build_number))
204+
205+
def __get_testflight_latest_build_info_all_versions(
206+
self,
207+
application_id: ResourceId,
208+
platform: Optional[Platform],
209+
build_expired_status: Optional[bool],
210+
) -> Optional[_LatestBuildInfo]:
211+
builds_filter = self.api_client.builds.Filter(
212+
app=application_id,
213+
expired=build_expired_status,
214+
pre_release_version_platform=platform,
215+
)
216+
result = self.api_client.builds.list_data_with_include(
217+
include="preReleaseVersion",
218+
resource_filter=builds_filter,
219+
extra_params={
220+
"fields[builds]": "version,preReleaseVersion",
221+
"fields[preReleaseVersions]": "version",
222+
},
223+
page_size=200,
224+
)
225+
prvs_by_id = {p["id"]: p for p in result.included}
226+
candidates: List[_LatestBuildInfo] = []
227+
for build in result.data:
228+
prv_ref = build.get("relationships", {}).get("preReleaseVersion", {}).get("data")
229+
if not prv_ref:
230+
continue
231+
prv = prvs_by_id.get(prv_ref["id"])
232+
if not prv:
233+
continue
234+
candidates.append(
235+
_LatestBuildInfo(
236+
build_id=ResourceId(build["id"]),
237+
build_number=build["attributes"]["version"],
238+
pre_release_version=prv["attributes"]["version"],
239+
),
240+
)
179241
if not candidates:
180242
return None
181243
return max(candidates, key=lambda info: versions.sorting_key(info.build_number))
@@ -189,16 +251,18 @@ def _get_testflight_latest_build_info(
189251
all_versions: bool = False,
190252
) -> Optional[_LatestBuildInfo]:
191253
try:
254+
if all_versions:
255+
return self.__get_testflight_latest_build_info_all_versions(
256+
application_id,
257+
platform,
258+
build_expired_status,
259+
)
192260
pre_release_version_numbers = self.__get_ordered_pre_release_version_numbers(
193261
application_id,
194262
pre_release_version,
195263
platform,
196264
)
197-
return self.__get_testflight_latest_build_info(
198-
pre_release_version_numbers,
199-
build_expired_status,
200-
all_versions=all_versions,
201-
)
265+
return self.__get_testflight_latest_build_info(pre_release_version_numbers, build_expired_status)
202266
except AppStoreConnectApiError as api_error:
203267
raise AppStoreConnectError(str(api_error))
204268

@@ -210,15 +274,14 @@ def _get_app_store_latest_build_info(
210274
all_versions: bool = False,
211275
) -> Optional[_LatestBuildInfo]:
212276
try:
277+
if all_versions:
278+
return self.__get_app_store_latest_build_info_all_versions(application_id, platform)
213279
app_store_version_numbers = self.__get_ordered_app_store_version_numbers(
214280
application_id,
215281
version_string,
216282
platform,
217283
)
218-
return self.__get_app_store_latest_build_info(
219-
app_store_version_numbers,
220-
all_versions=all_versions,
221-
)
284+
return self.__get_app_store_latest_build_info(app_store_version_numbers)
222285
except AppStoreConnectApiError as api_error:
223286
raise AppStoreConnectError(str(api_error))
224287

tests/tools/app_store_connect/actions/test_latest_build_number_actions.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from codemagic.apple.app_store_connect import IssuerId
77
from codemagic.apple.app_store_connect import KeyIdentifier
8+
from codemagic.apple.app_store_connect.type_declarations import PaginateResult
89
from codemagic.apple.resources import ResourceId
910
from codemagic.tools import AppStoreConnect
1011
from codemagic.tools.app_store_connect.actions.latest_build_number_actions import _LatestBuildInfo
@@ -199,14 +200,24 @@ def test_get_latest_app_store_build_number_all_versions_picks_global_max(app_sto
199200
application_id = ResourceId("application-id")
200201

201202
mock_api = mock.MagicMock()
202-
mock_api.apps.list_app_store_versions_data.return_value = [
203-
{"id": "asv1", "attributes": {"versionString": "2.0.0"}},
204-
{"id": "asv2", "attributes": {"versionString": "1.9.0"}},
205-
]
206-
mock_api.app_store_versions.read_build_data.side_effect = [
207-
{"id": "build-2.0.0", "attributes": {"version": "5"}},
208-
{"id": "build-1.9.0", "attributes": {"version": "12"}},
209-
]
203+
mock_api.apps.list_app_store_versions_data_with_include.return_value = PaginateResult(
204+
data=[
205+
{
206+
"id": "asv1",
207+
"attributes": {"versionString": "2.0.0"},
208+
"relationships": {"build": {"data": {"id": "build-2.0.0"}}},
209+
},
210+
{
211+
"id": "asv2",
212+
"attributes": {"versionString": "1.9.0"},
213+
"relationships": {"build": {"data": {"id": "build-1.9.0"}}},
214+
},
215+
],
216+
included=[
217+
{"id": "build-2.0.0", "attributes": {"version": "5"}},
218+
{"id": "build-1.9.0", "attributes": {"version": "12"}},
219+
],
220+
)
210221

211222
with mock.patch.object(app_store_connect, "_get_api_client", return_value=mock_api), mock.patch.object(
212223
app_store_connect,
@@ -215,21 +226,31 @@ def test_get_latest_app_store_build_number_all_versions_picks_global_max(app_sto
215226
result = app_store_connect.get_latest_app_store_build_number(application_id, all_versions=True)
216227

217228
assert result == "12"
218-
assert mock_api.app_store_versions.read_build_data.call_count == 2
229+
assert mock_api.apps.list_app_store_versions_data_with_include.call_count == 1
219230

220231

221232
def test_get_latest_testflight_build_number_all_versions_picks_global_max(app_store_connect: AppStoreConnect):
222233
application_id = ResourceId("application-id")
223234

224235
mock_api = mock.MagicMock()
225-
mock_api.pre_release_versions.list_data.return_value = [
226-
{"id": "prv1", "attributes": {"version": "2.0.0"}},
227-
{"id": "prv2", "attributes": {"version": "1.9.0"}},
228-
]
229-
mock_api.pre_release_versions.list_builds_data.side_effect = [
230-
[{"id": "b1", "attributes": {"version": "5"}}],
231-
[{"id": "b2", "attributes": {"version": "12"}}],
232-
]
236+
mock_api.builds.list_data_with_include.return_value = PaginateResult(
237+
data=[
238+
{
239+
"id": "b1",
240+
"attributes": {"version": "5"},
241+
"relationships": {"preReleaseVersion": {"data": {"id": "prv1"}}},
242+
},
243+
{
244+
"id": "b2",
245+
"attributes": {"version": "12"},
246+
"relationships": {"preReleaseVersion": {"data": {"id": "prv2"}}},
247+
},
248+
],
249+
included=[
250+
{"id": "prv1", "attributes": {"version": "2.0.0"}},
251+
{"id": "prv2", "attributes": {"version": "1.9.0"}},
252+
],
253+
)
233254

234255
with mock.patch.object(app_store_connect, "_get_api_client", return_value=mock_api), mock.patch.object(
235256
app_store_connect,
@@ -238,7 +259,7 @@ def test_get_latest_testflight_build_number_all_versions_picks_global_max(app_st
238259
result = app_store_connect.get_latest_testflight_build_number(application_id, all_versions=True)
239260

240261
assert result == "12"
241-
assert mock_api.pre_release_versions.list_builds_data.call_count == 2
262+
assert mock_api.builds.list_data_with_include.call_count == 1
242263

243264

244265
def test_get_latest_build_number_with_all_versions_and_version_raises(app_store_connect: AppStoreConnect):

0 commit comments

Comments
 (0)