From 43da9c1f59e5486cdcd673f79f9f517d5a04cacf Mon Sep 17 00:00:00 2001 From: Runner OMA Date: Fri, 24 Apr 2020 14:28:53 +0200 Subject: [PATCH 01/59] added functions about prerelease versions and build processing --- appstoreconnect/api.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 8326951..60d0eee 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -78,6 +78,7 @@ def beta_groups(self, app_id): def create_beta_group(self, group_name, app_id): post_data = {'data': {'attributes': {'name': group_name}, 'relationships': {'app': {'data': {'id': app_id, 'type': 'apps'}}}, 'type': 'betaGroups'}} + print(json.dumps(post_data)) return self._api_call("/v1/betaGroups", HttpMethod.POST, post_data) @@ -102,16 +103,27 @@ def create_beta_tester(self, beta_group_id, email, first_name, last_name): def beta_group_beta_testers(self, beta_group_id): return self._api_call("/v1/betaGroups/" + beta_group_id + "/betaTesters", HttpMethod.GET, None) + #prereleaseVersions + + def prerelease_versions_id_for_app_and_version(self, app_id, version): + return self._api_call("/v1/preReleaseVersions?filter[app]=" + app_id + "&filter[version]=" + version, HttpMethod.GET, None) + #builds + def build_prerelease_version(self, build_id): + return self._api_call("/v1/builds/" + build_id + "/preReleaseVersion", HttpMethod.GET, None) + def builds(self): return self._api_call("/v1/builds", HttpMethod.GET, None) def builds_for_app(self, app_id): - return self._api_call("/v1/builds?filter[app]=" + app_id, HttpMethod.GET, None) + return self._api_call("/v1/builds?include=preReleaseVersion&filter[app]=" + app_id, HttpMethod.GET, None) + + def builds_for_app_and_version_and_prerelease_version(self, app_id, version, prerelease_version): + return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&filter[preReleaseVersion]=" + prerelease_version, HttpMethod.GET, None) - def build_processing_state(self, app_id, version): - return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&fields[builds]=processingState", HttpMethod.GET, None) + def build_processing_state(self, app_id, build_id): + return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[id]=" + build_id + "&fields[builds]=processingState", HttpMethod.GET, None) def set_uses_non_encryption_exemption_setting(self, build_id, uses_non_encryption_exemption_setting): post_data = {'data': {'attributes': {'usesNonExemptEncryption': uses_non_encryption_exemption_setting}, 'id': build_id, 'type': 'builds'}} From 18c51a66534d6174ce7f22e60064b26050afcec7 Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Fri, 24 Apr 2020 17:47:30 +0200 Subject: [PATCH 02/59] . --- appstoreconnect/api.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 8326951..496993f 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -102,16 +102,27 @@ def create_beta_tester(self, beta_group_id, email, first_name, last_name): def beta_group_beta_testers(self, beta_group_id): return self._api_call("/v1/betaGroups/" + beta_group_id + "/betaTesters", HttpMethod.GET, None) + #prereleaseVersions + + def prerelease_versions_id_for_app_and_version(self, app_id, version): + return self._api_call("/v1/preReleaseVersions?filter[app]=" + app_id + "&filter[version]=" + version, HttpMethod.GET, None) + #builds + def build_prerelease_version(self, build_id): + return self._api_call("/v1/builds/" + build_id + "/preReleaseVersion", HttpMethod.GET, None) + def builds(self): return self._api_call("/v1/builds", HttpMethod.GET, None) def builds_for_app(self, app_id): - return self._api_call("/v1/builds?filter[app]=" + app_id, HttpMethod.GET, None) + return self._api_call("/v1/builds?include=preReleaseVersion&filter[app]=" + app_id, HttpMethod.GET, None) + + def builds_for_app_and_version_and_prerelease_version(self, app_id, version, prerelease_version): + return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&filter[preReleaseVersion]=" + prerelease_version, HttpMethod.GET, None) - def build_processing_state(self, app_id, version): - return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&fields[builds]=processingState", HttpMethod.GET, None) + def build_processing_state(self, app_id, build_id): + return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[id]=" + build_id + "&fields[builds]=processingState", HttpMethod.GET, None) def set_uses_non_encryption_exemption_setting(self, build_id, uses_non_encryption_exemption_setting): post_data = {'data': {'attributes': {'usesNonExemptEncryption': uses_non_encryption_exemption_setting}, 'id': build_id, 'type': 'builds'}} From 5921b931c16fcc8956637b34d3b318957a42691c Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Wed, 6 May 2020 19:28:01 +0200 Subject: [PATCH 03/59] added some functions --- appstoreconnect/api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 60d0eee..869d2bd 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -78,10 +78,14 @@ def beta_groups(self, app_id): def create_beta_group(self, group_name, app_id): post_data = {'data': {'attributes': {'name': group_name}, 'relationships': {'app': {'data': {'id': app_id, 'type': 'apps'}}}, 'type': 'betaGroups'}} - print(json.dumps(post_data)) return self._api_call("/v1/betaGroups", HttpMethod.POST, post_data) + def activate_public_link_for_beta_group(self, beta_group_id): + post_data = {'data': {'attributes': {'publicLinkEnabled': True}, 'id': beta_group_id, 'type': 'betaGroups'}} + + return self._api_call("/v1/betaGroups/" + beta_group_id, HttpMethod.PATCH, post_data) + def beta_group_info(self, beta_group_id): return self._api_call("/v1/betaGroups/" + beta_group_id, HttpMethod.GET, None) @@ -151,6 +155,9 @@ def submit_app_for_beta_review(self, build_id): post_data = {'data': { 'type': 'betaAppReviewSubmissions', 'relationships': {'build': {'data': {'id': build_id, 'type': 'builds'}}}}} return self._api_call("/v1/betaAppReviewSubmissions", HttpMethod.POST, post_data) + def beta_appreview_submission(self, appreview_id): + return self._api_call("/v1/betaAppReviewSubmissions/" + appreview_id, HttpMethod.GET, None) + @property def token(self): # generate a new token every 15 minutes From f0fbaf75d57aabfdd6f0f45b8b1119eebdaf6e6e Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Tue, 27 Oct 2020 10:29:55 +0100 Subject: [PATCH 04/59] added some entry points for new appstore versions api --- appstoreconnect/__init__.py | 3 +- appstoreconnect/api.py | 69 +++++++++++++++++++++++++++++++++++-- main.py | 23 +++++++++++-- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/appstoreconnect/__init__.py b/appstoreconnect/__init__.py index c8c10fe..6992764 100644 --- a/appstoreconnect/__init__.py +++ b/appstoreconnect/__init__.py @@ -1 +1,2 @@ -from .api import Api \ No newline at end of file +from .api import Api +from .api import AppStoreState diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 869d2bd..5349208 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -13,6 +13,30 @@ class HttpMethod(Enum): POST = 2 PATCH = 3 +class AppStoreState(Enum): + DEVELOPER_REMOVED_FROM_SALE = "DEVELOPER_REMOVED_FROM_SALE" + DEVELOPER_REJECTED = "DEVELOPER_REJECTED" + IN_REVIEW = "IN_REVIEW" + INVALID_BINARY = "INVALID_BINARY" + METADATA_REJECTED = "METADATA_REJECTED" + PENDING_APPLE_RELEASE = "PENDING_APPLE_RELEASE" + PENDING_CONTRACT = "PENDING_CONTRACT" + PENDING_DEVELOPER_RELEASE = "PENDING_DEVELOPER_RELEASE" + PREPARE_FOR_SUBMISSION = "PREPARE_FOR_SUBMISSION" + PREORDER_READY_FOR_SALE = "PREORDER_READY_FOR_SALE" + PROCESSING_FOR_APP_STORE = "PROCESSING_FOR_APP_STORE" + READY_FOR_SALE = "READY_FOR_SALE" + REJECTED = "REJECTED" + REMOVED_FROM_SALE = "REMOVED_FROM_SALE" + WAITING_FOR_EXPORT_COMPLIANCE = "WAITING_FOR_EXPORT_COMPLIANCE" + WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" + REPLACED_WITH_NEW_VERSION = "REPLACED_WITH_NEW_VERSION" + + @staticmethod + def editableStates(): + return list(map(lambda x: x.name, [AppStoreState.DEVELOPER_REJECTED, AppStoreState.INVALID_BINARY, AppStoreState.METADATA_REJECTED, AppStoreState.PREPARE_FOR_SUBMISSION, AppStoreState.REJECTED])) + + class Api: def __init__(self, key_id, key_file, issuer_id): @@ -47,11 +71,11 @@ def _api_call(self, route, method, post_data): contentType = r.headers['content-type'] + if r.status_code not in range(200,299): + print("Error [%d][%s]" % (r.status_code, r.content)) + if contentType == "application/json": return r.json() - else: - if r.status_code not in range(200,299): - print("Error [%d][%s]" % (r.status_code, r.content)) #apps @@ -61,6 +85,9 @@ def apps(self): def app_for_sku(self, sku): return self._api_call("/v1/apps?filter[sku]=" + sku, HttpMethod.GET, None) + def app_for_bundleId(self, bundle_id): + return self._api_call("/v1/apps?filter[bundleId]=" + bundle_id, HttpMethod.GET, None) + #users def users(self): @@ -126,6 +153,9 @@ def builds_for_app(self, app_id): def builds_for_app_and_version_and_prerelease_version(self, app_id, version, prerelease_version): return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&filter[preReleaseVersion]=" + prerelease_version, HttpMethod.GET, None) + def builds_for_app_and_version_and_prerelease_version_version(self, app_id, prerelease_version_version, version): + return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&filter[preReleaseVersion.version]=" + prerelease_version_version, HttpMethod.GET, None) + def build_processing_state(self, app_id, build_id): return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[id]=" + build_id + "&fields[builds]=processingState", HttpMethod.GET, None) @@ -158,6 +188,39 @@ def submit_app_for_beta_review(self, build_id): def beta_appreview_submission(self, appreview_id): return self._api_call("/v1/betaAppReviewSubmissions/" + appreview_id, HttpMethod.GET, None) + #territories + + def territories(self): + return self._api_call("/v1/territories", HttpMethod.GET, None) + + #build icons + def build_icons_for_build(self, build_id): + return self._api_call("/v1/builds/" + build_id+ "/icons", HttpMethod.GET, None) + + #appstore versions + def appstoreversions_for_app(self, app_id): + return self._api_call("/v1/apps/" + app_id+ "/appStoreVersions", HttpMethod.GET, None) + + def create_new_version_for_app(self, versionString, app_id): + post_data = {'data': { 'type': 'appStoreVersions', 'relationships': {'app': {'data': {'id': app_id, 'type': 'apps'}}}, 'attributes': { 'platform': 'IOS', 'versionString': versionString}}} + return self._api_call("/v1/appStoreVersions", HttpMethod.POST, post_data) + + def update_appstoreversion(self, appstoreversion_id, attributes, relationships): + post_data = {'data': { 'id': appstoreversion_id, 'type': 'appStoreVersions', 'attributes': attributes , 'relationships': relationships }} + return self._api_call("/v1/appStoreVersions/" + appstoreversion_id, HttpMethod.PATCH, post_data) + + def update_versionString_for_appstoreversion(self, appstoreversion_id, versionString): + attributes = { 'versionString': versionString } + self.update_appstoreversion(appstoreversion_id, attributes, {}) + + def associate_build_to_appstoreversion(self, build_id, appstoreversion_id): + post_data = {'data': { 'id': build_id, 'type': 'builds' }} + return self._api_call("/v1/appStoreVersions/" + appstoreversion_id + "/relationships/build", HttpMethod.PATCH, post_data) + + def idfadeclaration_for_appstoreversion(self, appstoreversion_id): + return self._api_call("/v1/appStoreVersions/" + appstoreversion_id+ "/idfaDeclaration", HttpMethod.GET, None) + + @property def token(self): # generate a new token every 15 minutes diff --git a/main.py b/main.py index da5c7da..5af5353 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import sys from appstoreconnect import Api +from appstoreconnect import AppStoreState if __name__ == "__main__": @@ -9,7 +10,23 @@ key_file = sys.argv[2] issuer_id = sys.argv[3] api = Api(key_id, key_file, issuer_id) - apps = api.apps() - for app in apps["data"]: - print(app["attributes"]["name"]) + app = api.app_for_bundleId("com.orange.fr.orangeetmoi") + app_id = app["data"][0]["id"] + appstoreversions = api.appstoreversions_for_app(app_id) + appstoreversion_id = appstoreversions["data"][0]["id"] + #appstorestate = appstoreversions["data"][0]["attributes"]["appStoreState"] + + #print(appstoreversions["data"][0]) + #builds = api.builds_for_app_and_version_and_prerelease_version_version(app_id, "1.0.0", "4") + #build_id = builds["data"][0]["id"] + #icons = api.build_icons_for_build(build_id) + + #if appstorestate in AppStoreState.editableStates(): + #r = api.update_idfaUse_for_appstoreversion(appstoreversion_id, True) + #print(r) + + r = api.idfadeclaration_for_appstoreversion(appstoreversion_id) + print(r) + r = api.update_idfaUse_for_appstoreversion(appstoreversion_id, False, True) + print(r) From b137506c64d0f729ea9796e3a50139632c849a6d Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 1 Dec 2020 17:33:14 +0100 Subject: [PATCH 05/59] new api version implemented --- appstoreconnect/api.py | 770 ++++++++++++++++++++++++++++------- appstoreconnect/resources.py | 229 +++++++++++ 2 files changed, 842 insertions(+), 157 deletions(-) create mode 100644 appstoreconnect/resources.py diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 5349208..65cdbf9 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -1,17 +1,27 @@ import requests import jwt +import gzip +import platform +import hashlib +from collections import defaultdict +from pathlib import Path from datetime import datetime, timedelta import time import json from enum import Enum +from resources import * +from __version__ import __version__ as version + ALGORITHM = 'ES256' BASE_API = "https://api.appstoreconnect.apple.com" + class HttpMethod(Enum): GET = 1 POST = 2 PATCH = 3 + DELETE = 4 class AppStoreState(Enum): DEVELOPER_REMOVED_FROM_SALE = "DEVELOPER_REMOVED_FROM_SALE" @@ -36,29 +46,207 @@ class AppStoreState(Enum): def editableStates(): return list(map(lambda x: x.name, [AppStoreState.DEVELOPER_REJECTED, AppStoreState.INVALID_BINARY, AppStoreState.METADATA_REJECTED, AppStoreState.PREPARE_FOR_SUBMISSION, AppStoreState.REJECTED])) +class APIError(Exception): + def __init__(self, error_string, status_code): + try: + self.status_code = int(status_code) + except ValueError: + pass + super().__init__(error_string) + class Api: - def __init__(self, key_id, key_file, issuer_id): + def __init__(self, key_id, key_file, issuer_id, submit_stats=True): self._token = None self.token_gen_date = None self.exp = None self.key_id = key_id self.key_file = key_file self.issuer_id = issuer_id + self.submit_stats = submit_stats + self._call_stats = defaultdict(int) + if self.submit_stats: + self._submit_stats("session_start") + + self._debug = False token = self.token # generate first token + def __del__(self): + if self.submit_stats: + self._submit_stats("session_end") + def _generate_token(self): - key = open(self.key_file, 'r').read() + try: + key = open(self.key_file, 'r').read() + except IOError as e: + key = self.key_file self.token_gen_date = datetime.now() exp = int(time.mktime((self.token_gen_date + timedelta(minutes=20)).timetuple())) return jwt.encode({'iss': self.issuer_id, 'exp': exp, 'aud': 'appstoreconnect-v1'}, key, headers={'kid': self.key_id, 'typ': 'JWT'}, algorithm=ALGORITHM).decode('ascii') - def _api_call(self, route, method, post_data): + def _get_resource(self, Resource, resource_id): + url = "%s%s/%s" % (BASE_API, Resource.endpoint, resource_id) + payload = self._api_call(url) + return Resource(payload.get('data', {}), self) + + def _get_related_resource(self, Resource, full_url): + payload = self._api_call(full_url) + return Resource(payload.get('data', {}), self) + + def _create_resource(self, Resource, args): + attributes = {} + for attribute in Resource.attributes: + if attribute in args and args[attribute] is not None: + attributes[attribute] = args[attribute] + + relationships_dict = {} + for relation in Resource.relationships.keys(): + if relation in args and args[relation] is not None: + relationships_dict[relation] = {} + if Resource.relationships[relation].get('multiple', False): + relationships_dict[relation]['data'] = [] + relationship_objects = args[relation] + if type(relationship_objects) is not list: + relationship_objects = [relationship_objects] + for relationship_object in relationship_objects: + relationships_dict[relation]['data'].append({ + 'id': relationship_object.id, + 'type': relationship_object.type + }) + else: + relationships_dict[relation]['data'] = { + 'id': args[relation].id, + 'type': args[relation].type + } + + post_data = { + 'data': { + 'attributes': attributes, + 'relationships': relationships_dict, + 'type': Resource.type + } + } + url = "%s%s" % (BASE_API, Resource.endpoint) + if self._debug: + print(post_data) + payload = self._api_call(url, HttpMethod.POST, post_data) + + return Resource(payload.get('data', {}), self) + + def _modify_resource(self, Resource, args): + attributes = {} + for attribute in Resource.attributes: + if attribute in args and args[attribute] is not None: + attributes[attribute] = args[attribute] + + relationships_dict = {} + for relation in Resource.relationships.keys(): + if relation in args and args[relation] is not None: + relationships_dict[relation] = {} + if Resource.relationships[relation].get('multiple', False): + relationships_dict[relation]['data'] = [] + relationship_objects = args[relation] + if type(relationship_objects) is not list: + relationship_objects = [relationship_objects] + for relationship_object in relationship_objects: + relationships_dict[relation]['data'].append({ + 'id': relationship_object.id, + 'type': relationship_object.type + }) + else: + relationships_dict[relation]['data'] = { + 'id': args[relation].id, + 'type': args[relation].type + } + post_data = { + 'data': { + 'attributes': attributes, + 'relationships': relationships_dict, + 'id': Resource.id, + 'type': Resource.type + } + } + url = "%s%s/%s" % (BASE_API, Resource.endpoint, Resource.id) + if self._debug: + print(post_data) + payload = self._api_call(url, HttpMethod.PATCH, post_data) + + return type(Resource)(payload.get('data', {}), self) + + def _delete_resource(self, resource: Resource): + url = "%s%s/%s" % (BASE_API, resource.endpoint, resource.id) + self._api_call(url, HttpMethod.DELETE) + + def _get_resources(self, Resource, filters=None, sort=None, full_url=None): + class IterResource: + def __init__(self, api, url): + self.api = api + self.url = url + self.index = 0 + self.total_length = None + self.payload = None + + def __iter__(self): + return self + + def __repr__(self): + return "Iterator over %s resource" % Resource.__name__ + + def __len__(self): + if not self.payload: + self.fetch_page() + return self.total_length + + def __next__(self): + if not self.payload: + self.fetch_page() + if self.index < len(self.payload.get('data', [])): + data = self.payload.get('data', [])[self.index] + self.index += 1 + return Resource(data, self.api) + else: + self.url = self.payload.get('links', {}).get('next', None) + self.index = 0 + if self.url: + self.fetch_page() + if self.index < len(self.payload.get('data', [])): + data = self.payload.get('data', [])[self.index] + self.index += 1 + return Resource(data, self.api) + raise StopIteration() + + def fetch_page(self): + self.payload = self.api._api_call(self.url) + self.total_length = self.payload.get('meta', {}).get('paging', {}).get('total', 0) + + url = full_url if full_url else "%s%s" % (BASE_API, Resource.endpoint) + url = self._build_query_parameters(url, filters, sort) + return IterResource(self, url) + + def _build_query_parameters(self, url, filters, sort = None): + separator = '?' + if type(filters) is dict: + for index, (filter_name, filter_value) in enumerate(filters.items()): + filter_name = "filter[%s]" % filter_name + url = "%s%s%s=%s" % (url, separator, filter_name, filter_value) + separator = '&' + if type(sort) is str: + url = "%s%ssort=%s" % (url, separator, sort) + return url + + def _api_call(self, url, method=HttpMethod.GET, post_data=None): headers = {"Authorization": "Bearer %s" % self.token} - url = "%s%s" % (BASE_API, route) - r = {} + if self._debug: + print(url) + + if self._submit_stats: + endpoint = url.replace(BASE_API, '') + if method in (HttpMethod.PATCH, HttpMethod.DELETE): # remove last bit of endpoint which is a resource id + endpoint = "/".join(endpoint.split('/')[:-1]) + request = "%s %s" % (method.name, endpoint) + self._call_stats[request] += 1 if method == HttpMethod.GET: r = requests.get(url, headers=headers) @@ -68,163 +256,431 @@ def _api_call(self, route, method, post_data): elif method == HttpMethod.PATCH: headers["Content-Type"] = "application/json" r = requests.patch(url=url, headers=headers, data=json.dumps(post_data)) + elif method == HttpMethod.DELETE: + r = requests.delete(url=url, headers=headers) + else: + raise APIError("Unknown HTTP method") + + content_type = r.headers['content-type'] + + if content_type in [ "application/json", "application/vnd.api+json" ]: + payload = r.json() + if 'errors' in payload: + raise APIError( + payload.get('errors', [])[0].get('detail', 'Unknown error'), + payload.get('errors', [])[0].get('status', None) + ) + return payload + elif content_type == 'application/a-gzip': + # TODO implement stream decompress + data_gz = b"" + for chunk in r.iter_content(1024 * 1024): + if chunk: + data_gz = data_gz + chunk + + data = gzip.decompress(data_gz) + return data.decode("utf-8") + else: + if not 200 <= r.status_code <= 299: + raise APIError("HTTP error [%d][%s]" % (r.status_code, r.content)) + return r + + def _submit_stats(self, event_type): + """ + this submits anonymous usage statistics to help us better understand how this library is used + you can opt-out by initializing the client with submit_stats=False + """ + payload = { + 'project': 'appstoreconnectapi', + 'version': version, + 'type': event_type, + 'parameters': { + 'python_version': platform.python_version(), + 'platform': platform.platform(), + 'issuer_id_hash': hashlib.sha1(self.issuer_id.encode()).hexdigest(), # send anonymized hash + } + } + if event_type == 'session_end': + payload['parameters']['endpoints'] = self._call_stats + requests.post('https://stats.ponytech.net/new-event', json.dumps(payload)) - contentType = r.headers['content-type'] - - if r.status_code not in range(200,299): - print("Error [%d][%s]" % (r.status_code, r.content)) - - if contentType == "application/json": - return r.json() - - #apps - - def apps(self): - return self._api_call("/v1/apps", HttpMethod.GET, None) - - def app_for_sku(self, sku): - return self._api_call("/v1/apps?filter[sku]=" + sku, HttpMethod.GET, None) - - def app_for_bundleId(self, bundle_id): - return self._api_call("/v1/apps?filter[bundleId]=" + bundle_id, HttpMethod.GET, None) - - #users - - def users(self): - return self._api_call("/v1/users", HttpMethod.GET, None) - - #userInvitations - - def user_invitations(self): - return self._api_call("/v1/userInvitations", HttpMethod.GET, None) - - #betaGroups - - def beta_groups(self, app_id): - return self._api_call("/v1/apps/" + app_id + "/betaGroups", HttpMethod.GET, None) - - def create_beta_group(self, group_name, app_id): - post_data = {'data': {'attributes': {'name': group_name}, 'relationships': {'app': {'data': {'id': app_id, 'type': 'apps'}}}, 'type': 'betaGroups'}} - - return self._api_call("/v1/betaGroups", HttpMethod.POST, post_data) - - def activate_public_link_for_beta_group(self, beta_group_id): - post_data = {'data': {'attributes': {'publicLinkEnabled': True}, 'id': beta_group_id, 'type': 'betaGroups'}} + @property + def token(self): + # generate a new token every 15 minutes + if (self._token is None) or (self.token_gen_date + timedelta(minutes=15) < datetime.now()): + self._token = self._generate_token() - return self._api_call("/v1/betaGroups/" + beta_group_id, HttpMethod.PATCH, post_data) + return self._token - def beta_group_info(self, beta_group_id): - return self._api_call("/v1/betaGroups/" + beta_group_id, HttpMethod.GET, None) + # Users and Roles + def list_users(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_users + :return: an iterator over User resources + """ + return self._get_resources(User, filters, sort) + + def list_invited_users(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_invited_users + :return: an iterator over UserInvitation resources + """ + return self._get_resources(UserInvitation, filters, sort) + + # TODO: implement POST requests using Resource + def invite_user(self, all_apps_visible, email, first_name, last_name, provisioning_allowed, roles, visible_apps=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/invite_a_user + :return: a UserInvitation resource + """ + post_data = {'data': {'attributes': {'allAppsVisible': all_apps_visible, 'email': email, 'firstName': first_name, 'lastName': last_name, 'provisioningAllowed': provisioning_allowed, 'roles': roles}, 'type': 'userInvitations'}} + if visible_apps is not None: + visible_apps_relationship = list(map(lambda a: {'id': a, 'type': 'apps'}, visible_apps)) + visible_apps_data = {'visibleApps': {'data': visible_apps_relationship}} + post_data['data']['relationships'] = visible_apps_data + payload = self._api_call(BASE_API + "/v1/userInvitations", HttpMethod.POST, post_data) + return UserInvitation(payload.get('data'), {}) + + def read_user_invitation_information(self, user_invitation_id: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_user_invitation_information + :return: a UserInvitation resource + """ + return self._get_resource(UserInvitation, user_invitation_id) + + # Beta Testers and Groups + def create_beta_tester(self, email: str, firstName: str = None, lastName: str = None, betaGroups: BetaGroup = None, builds: Build = None) -> BetaTester: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_tester + :return: an BetaTester resource + """ + return self._create_resource(BetaTester, locals()) + + def delete_beta_tester(self, betaTester: BetaTester) -> None: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_a_beta_tester + :return: None + """ + return self._delete_resource(betaTester) + + def list_beta_testers(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_testers + :return: an iterator over BetaTester resources + """ + return self._get_resources(BetaTester, filters, sort) + + def read_beta_tester_information(self, beta_tester_id: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_tester_information + :return: a BetaTester resource + """ + return self._get_resource(BetaTester, beta_tester_id) + + def create_beta_group(self, app: App, name: str, publicLinkEnabled: bool = None, publicLinkLimit: int = None, publicLinkLimitEnabled: bool = None) -> BetaGroup: + """ + :reference:https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_group + :return: a BetaGroup resource + """ + return self._create_resource(BetaGroup, locals()) + + def modify_beta_group(self, betaGroup: BetaGroup, name: str = None, publicLinkEnabled: bool = None, publicLinkLimit: int = None, publicLinkLimitEnabled: bool = None) -> BetaGroup: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_group + :return: a BetaGroup resource + """ + return self._modify_resource(betaGroup, locals()) + + def delete_beta_group(self, betaGroup: BetaGroup): + return self._delete_resource(betaGroup) + + def list_beta_groups(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_groups + :return: an iterator over BetaGroup resources + """ + return self._get_resources(BetaGroup, filters, sort) + + def read_beta_group_information(self, beta_group_ip): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_group_information + :return: an BetaGroup resource + """ + return self._get_resource(BetaGroup, beta_group_ip) def add_build_to_beta_group(self, beta_group_id, build_id): post_data = {'data': [{ 'id': build_id, 'type': 'builds'}]} - - return self._api_call("/v1/betaGroups/" + beta_group_id + "/relationships/builds", HttpMethod.POST, post_data) - - #betaTesters - - def beta_testers(self): - return self._api_call("/v1/betaTesters", HttpMethod.GET, None) - - def create_beta_tester(self, beta_group_id, email, first_name, last_name): - post_data = {'data': {'attributes': {'email': email, 'firstName': first_name, 'lastName': last_name}, 'relationships': {'betaGroups': {'data': [{ 'id': beta_group_id ,'type': 'betaGroups'}]}}, 'type': 'betaTesters'}} - - return self._api_call("/v1/betaTesters", HttpMethod.POST, post_data) - - def beta_group_beta_testers(self, beta_group_id): - return self._api_call("/v1/betaGroups/" + beta_group_id + "/betaTesters", HttpMethod.GET, None) - - #prereleaseVersions - - def prerelease_versions_id_for_app_and_version(self, app_id, version): - return self._api_call("/v1/preReleaseVersions?filter[app]=" + app_id + "&filter[version]=" + version, HttpMethod.GET, None) - - #builds - - def build_prerelease_version(self, build_id): - return self._api_call("/v1/builds/" + build_id + "/preReleaseVersion", HttpMethod.GET, None) - - def builds(self): - return self._api_call("/v1/builds", HttpMethod.GET, None) - - def builds_for_app(self, app_id): - return self._api_call("/v1/builds?include=preReleaseVersion&filter[app]=" + app_id, HttpMethod.GET, None) - - def builds_for_app_and_version_and_prerelease_version(self, app_id, version, prerelease_version): - return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&filter[preReleaseVersion]=" + prerelease_version, HttpMethod.GET, None) - - def builds_for_app_and_version_and_prerelease_version_version(self, app_id, prerelease_version_version, version): - return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&filter[preReleaseVersion.version]=" + prerelease_version_version, HttpMethod.GET, None) - - def build_processing_state(self, app_id, build_id): - return self._api_call("/v1/builds?filter[app]=" + app_id + "&filter[id]=" + build_id + "&fields[builds]=processingState", HttpMethod.GET, None) - + payload = self._api_call(BASE_API + "/v1/betaGroups/" + beta_group_id + "/relationships/builds", HttpMethod.POST, post_data) + return BetaGroup(payload.get('data'), {}) + + # App Resources + def read_app_information(self, app_ip): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_information + :param app_ip: + :return: an App resource + """ + return self._get_resource(App, app_ip) + + def list_apps(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_apps + :return: an iterator over App resources + """ + return self._get_resources(App, filters, sort) + + def list_prerelease_versions(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_prerelease_versions + :return: an iterator over PreReleaseVersion resources + """ + return self._get_resources(PreReleaseVersion, filters, sort) + + def list_beta_app_localizations(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_app_localizations + :return: an iterator over BetaAppLocalization resources + """ + return self._get_resources(BetaAppLocalization, filters) + + def read_beta_app_localization_information(self, beta_app_id: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_app_localization_information + :return: an BetaAppLocalization resource + """ + return self._get_resource(BetaAppLocalization, beta_app_id) + + def create_beta_app_localization(self, app: App, locale: str, description: str = None, feedbackEmail: str = None, marketingUrl: str = None, privacyPolicyUrl: str = None, tvOsPrivacyPolicy: str = None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_app_localization + :return: an BetaAppLocalization resource + """ + return self._create_resource(BetaAppLocalization, locals()) + + def list_app_encryption_declarations(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_app_encryption_declarations + :return: an iterator over AppEncryptionDeclaration resources + """ + return self._get_resources(AppEncryptionDeclaration, filters) + + def list_beta_license_agreements(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_license_agreements + :return: an iterator over BetaLicenseAgreement resources + """ + return self._get_resources(BetaLicenseAgreement, filters) + + # App Metadata Resources + + def list_app_store_versions(self, app_id: str, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_versions_for_an_app + :return: an iterator over AppStoreVersion resources + """ + + full_url = BASE_API + "/v1/apps/" + app_id + "/appStoreVersions" + return self._get_resources(AppStoreVersion, filters, sort, full_url) + + # Build Resources + def list_builds(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_builds + :return: an iterator over Build resources + """ + return self._get_resources(Build, filters, sort) + + # TODO: handle fields on get_resources() + def build_processing_state(self, app_id, version): + return self._api_call(BASE_API + "/v1/builds?filter[app]=" + app_id + "&filter[version]=" + version + "&fields[builds]=processingState") + + # TODO: implement POST requests using Resource def set_uses_non_encryption_exemption_setting(self, build_id, uses_non_encryption_exemption_setting): post_data = {'data': {'attributes': {'usesNonExemptEncryption': uses_non_encryption_exemption_setting}, 'id': build_id, 'type': 'builds'}} - return self._api_call("/v1/builds/" + build_id, HttpMethod.PATCH, post_data) - - #betaBuildLocalizations - - def beta_build_localizations_for_build(self, build_id): - return self._api_call("/v1/betaBuildLocalizations?filter[build]=" + build_id, HttpMethod.GET, None) - - def beta_build_localizations_for_build_and_locale(self, build_id, locale): - return self._api_call("/v1/betaBuildLocalizations?filter[build]=" + build_id + "&filter[locale]=" + locale, HttpMethod.GET, None) - - def create_beta_build_localization(self, build_id, locale, whatsNew): - post_data = {'data': { 'type': 'betaBuildLocalizations', 'relationships': {'build': {'data': {'id': build_id, 'type': 'builds'}}}, 'attributes': { 'locale': locale, 'whatsNew': whatsNew}}} - return self._api_call("/v1/betaBuildLocalizations", HttpMethod.POST, post_data) - - def modify_beta_build_localization(self, beta_build_localization_id, whatsNew): - post_data = {'data': { 'type': 'betaBuildLocalizations', 'id': beta_build_localization_id, 'attributes': {'whatsNew': whatsNew}}} - return self._api_call("/v1/betaBuildLocalizations/" + beta_build_localization_id, HttpMethod.PATCH, post_data) - - #betaAppReviewSubmissions - - def submit_app_for_beta_review(self, build_id): - post_data = {'data': { 'type': 'betaAppReviewSubmissions', 'relationships': {'build': {'data': {'id': build_id, 'type': 'builds'}}}}} - return self._api_call("/v1/betaAppReviewSubmissions", HttpMethod.POST, post_data) - - def beta_appreview_submission(self, appreview_id): - return self._api_call("/v1/betaAppReviewSubmissions/" + appreview_id, HttpMethod.GET, None) - - #territories - - def territories(self): - return self._api_call("/v1/territories", HttpMethod.GET, None) - - #build icons - def build_icons_for_build(self, build_id): - return self._api_call("/v1/builds/" + build_id+ "/icons", HttpMethod.GET, None) - - #appstore versions - def appstoreversions_for_app(self, app_id): - return self._api_call("/v1/apps/" + app_id+ "/appStoreVersions", HttpMethod.GET, None) - - def create_new_version_for_app(self, versionString, app_id): - post_data = {'data': { 'type': 'appStoreVersions', 'relationships': {'app': {'data': {'id': app_id, 'type': 'apps'}}}, 'attributes': { 'platform': 'IOS', 'versionString': versionString}}} - return self._api_call("/v1/appStoreVersions", HttpMethod.POST, post_data) - - def update_appstoreversion(self, appstoreversion_id, attributes, relationships): - post_data = {'data': { 'id': appstoreversion_id, 'type': 'appStoreVersions', 'attributes': attributes , 'relationships': relationships }} - return self._api_call("/v1/appStoreVersions/" + appstoreversion_id, HttpMethod.PATCH, post_data) - - def update_versionString_for_appstoreversion(self, appstoreversion_id, versionString): - attributes = { 'versionString': versionString } - self.update_appstoreversion(appstoreversion_id, attributes, {}) - - def associate_build_to_appstoreversion(self, build_id, appstoreversion_id): - post_data = {'data': { 'id': build_id, 'type': 'builds' }} - return self._api_call("/v1/appStoreVersions/" + appstoreversion_id + "/relationships/build", HttpMethod.PATCH, post_data) - - def idfadeclaration_for_appstoreversion(self, appstoreversion_id): - return self._api_call("/v1/appStoreVersions/" + appstoreversion_id+ "/idfaDeclaration", HttpMethod.GET, None) - - - @property - def token(self): - # generate a new token every 15 minutes - if not self._token or self.token_gen_date + timedelta(minutes=15) > datetime.now(): - self._token = self._generate_token() - - return self._token + payload = self._api_call(BASE_API + "/v1/builds/" + build_id, HttpMethod.PATCH, post_data) + return Build(payload.get('data'), {}) + + def list_build_beta_details(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_build_beta_details + :return: an iterator over BuildBetaDetail resources + """ + return self._get_resources(BuildBetaDetail, filters) + + def create_beta_build_localization(self, build: Build, locale: str, whatsNew: str = None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_build_localization + :return: a BetaBuildLocalization resource + """ + return self._create_resource(BetaBuildLocalization, locals()) + + def modify_beta_build_localization(self, beta_build_localization: BetaBuildLocalization, whatsNew: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_build_localization + :return: a BetaBuildLocalization resource + """ + return self._modify_resource(beta_build_localization, locals()) + + def list_beta_build_localizations(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_build_localizations + :return: an iterator over BetaBuildLocalization resources + """ + return self._get_resources(BetaBuildLocalization, filters) + + def list_beta_app_review_details(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_app_review_details + :return: an iterator over BetaAppReviewDetail resources + """ + return self._get_resources(BetaAppReviewDetail, filters) + + def submit_app_for_beta_review(self, build: Build) -> BetaAppReviewSubmission: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/submit_an_app_for_beta_review + :return: a BetaAppReviewSubmission resource + """ + + return self._create_resource(BetaAppReviewSubmission, locals()) + + def list_beta_app_review_submissions(self, filters=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_app_review_submissions + :return: an iterator over BetaAppReviewSubmission resources + """ + return self._get_resources(BetaAppReviewSubmission, filters) + + def read_beta_app_review_submission_information(self, beta_app_id: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_app_review_submission_information + :return: an BetaAppReviewSubmission resource + """ + return self._get_resource(BetaAppReviewSubmission, beta_app_id) + + # Provisioning + def list_bundle_ids(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_bundle_ids + :return: an iterator over BundleId resources + """ + return self._get_resources(BundleId, filters, sort) + + def list_certificates(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_and_download_certificates + :return: an iterator over Certificate resources + """ + return self._get_resources(Certificate, filters, sort) + + def list_devices(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_devices + :return: an iterator over Device resources + """ + return self._get_resources(Device, filters, sort) + + def register_new_device(self, name: str, platform: str, udid: str) -> Device: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/register_a_new_device + :return: a Device resource + """ + return self._create_resource(Device, locals()) + + def modify_registered_device(self, device: Device, name: str = None, status: str = None) -> Device: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device + :return: a Device resource + """ + return self._modify_resource(device, locals()) + + def list_profiles(self, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_and_download_profiles + :return: an iterator over Profile resources + """ + return self._get_resources(Profile, filters, sort) + + def update_app_store_version(self, app_store_version: AppStoreVersion, args): + return self._modify_resource(app_store_version, args) + + def create_new_version_for_app(self, app_store_version: AppStoreVersion, args): + return self._create_resource(app_store_version, args) + + def get_build_info(self, build_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_build_information + :return: an iterator over Build resources + """ + return self._get_resource(Build, build_id) + + # Reporting + def download_finance_reports(self, filters=None, split_response=False, save_to=None): + # setup required filters if not provided + for required_key, default_value in ( + ('regionCode', 'ZZ'), + ('reportType', 'FINANCIAL'), + # vendorNumber is required but we cannot provide a default value + # reportDate is required but we cannot provide a default value + ): + if required_key not in filters: + filters[required_key] = default_value + + url = "%s%s" % (BASE_API, FinanceReport.endpoint) + url = self._build_query_parameters(url, filters) + response = self._api_call(url) + + if split_response: + res1 = response.split('Total_Rows')[0] + res2 = '\n'.join(response.split('Total_Rows')[1].split('\n')[1:]) + + if save_to: + file1 = Path(save_to[0]) + file1.write_text(res1, 'utf-8') + file2 = Path(save_to[1]) + file2.write_text(res2, 'utf-8') + + return res1, res2 + + if save_to: + file = Path(save_to) + file.write_text(response, 'utf-8') + + return response + + def download_sales_and_trends_reports(self, filters=None, save_to=None): + # setup required filters if not provided + default_versions = { + 'SALES': '1_0', + 'SUBSCRIPTION': '1_2', + 'SUBSCRIPTION_EVENT': '1_2', + 'SUBSCRIBER': '1_2', + 'NEWSSTAND': '1_0', + 'PRE_ORDER': '1_0', + } + default_subtypes = { + 'SALES': 'SUMMARY', + 'SUBSCRIPTION': 'SUMMARY', + 'SUBSCRIPTION_EVENT': 'SUMMARY', + 'SUBSCRIBER': 'DETAILED', + 'NEWSSTAND': 'DETAILED', + 'PRE_ORDER': 'SUMMARY', + } + for required_key, default_value in ( + ('frequency', 'DAILY'), + ('reportType', 'SALES'), + ('reportSubType', default_subtypes.get(filters.get('reportType', 'SALES'), 'SUMMARY')), + ('version', default_versions.get(filters.get('reportType', 'SALES'), '1_0')), + # vendorNumber is required but we cannot provide a default value + ): + if required_key not in filters: + filters[required_key] = default_value + + url = "%s%s" % (BASE_API, SalesReport.endpoint) + url = self._build_query_parameters(url, filters) + response = self._api_call(url) + + if save_to: + file = Path(save_to) + file.write_text(response, 'utf-8') + + return response diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py new file mode 100644 index 0000000..8cd5b2c --- /dev/null +++ b/appstoreconnect/resources.py @@ -0,0 +1,229 @@ +from abc import ABC, abstractmethod +import sys + +class Resource(ABC): + + def __init__(self, data, api): + self._data = data + self._api = api + + def __getattr__(self, item): + if item == 'id': + return self._data.get('id') + if item in self._data.get('attributes', {}): + return self._data.get('attributes', {})[item] + if item in self._data.get('relationships', {}): + def callable(): + # Try to fetch relationship + nonlocal item + is_resources = item[-1] == 's' + try: + item_cls = getattr(sys.modules[__name__], item[0].upper() + (item[1:-1] if is_resources else item[1:])) + except AttributeError: + item_cls = Resource + url = self._data.get('relationships', {})[item]['links']['related'] + # List of resources + if is_resources: + return self._api._get_resources(item_cls, full_url=url) + else: + return self._api._get_related_resource(item_cls, full_url=url) + return callable + + raise AttributeError('%s have no attributes %s' % (self.type_name, item)) + + def __repr__(self): + return '%s id %s' % (self.type_name, self._data.get('id')) + + def __dir__(self): + return ['id'] + list(self._data.get('attributes', {}).keys()) + list(self._data.get('relationships', {}).keys()) + + @property + def type_name(self): + return type(self).__name__ + + @property + @abstractmethod + def endpoint(self): + pass + + +# Beta Testers and Groups + +class BetaTester(Resource): + endpoint = '/v1/betaTesters' + type = 'betaTesters' + attributes = ['email', 'firstName', 'inviteType', 'lastName'] + relationships = { + 'apps': {'multiple': True}, + 'betaGroups': {'multiple': True}, + 'builds': {'multiple': True}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betatester' + + +class BetaGroup(Resource): + endpoint = '/v1/betaGroups' + type = 'betaGroups' + attributes = ['isInternalGroup', 'name', 'publicLink', 'publicLinkEnabled', 'publicLinkId', 'publicLinkLimit', 'publicLinkLimitEnabled', 'createdDate'] + relationships = { + 'app': {'multiple': False}, + 'betaTesters': {'multiple': True}, + 'builds': {'multiple': True}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betagroup' + + + +# App Metadata Resources + +class AppStoreVersion(Resource): + endpoint = '/v1/appStoreVersions' + type = 'appStoreVersions' + attributes = ['platform', 'appStoreState', 'copyright', 'earliestReleaseDate', 'releaseType', 'usesIdfa', 'versionString', 'createdDate', 'downloadable'] + relationships = { + 'app': {'multiple': False}, + 'ageRatingDeclaration': {'multiple': False}, + 'appStoreReviewDetail': {'multiple': False}, + 'appStoreVersionLocalizations': {'multiple': True}, + 'appStoreVersionPhasedRelease': {'multiple': False}, + 'appStoreVersionSubmission': {'multiple': False}, + 'build': {'multiple': False}, + 'idfaDeclaration': {'multiple': False}, + 'routingAppCoverage': {'multiple': False}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversion' + +# App Resources + +class App(Resource): + endpoint = '/v1/apps' + type = 'apps' + attributes = ['bundleId', 'name', 'primaryLocale', 'sku'] + relationships = { + 'betaLicenseAgreement': {'multiple': False}, + 'preReleaseVersions': {'multiple': True}, + 'betaAppLocalizations': {'multiple': True}, + 'betaGroups': {'multiple': True}, + 'betaTesters': {'multiple': True}, + 'builds': {'multiple': True}, + 'betaAppReviewDetail': {'multiple': False}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/app' + + +class PreReleaseVersion(Resource): + endpoint = '/v1/preReleaseVersions' + type = 'preReleaseVersion' + attributes = ['platform', 'version'] + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/preReleaseVersion/attributes' + + +class BetaAppLocalization(Resource): + endpoint = '/v1/betaAppLocalizations' + type = 'betaAppLocalizations' + attributes = ['description', 'feedbackEmail', 'locale', 'marketingUrl', 'privacyPolicyUrl', 'tvOsPrivacyPolicy'] + relationships = { + 'app': {'multiple': False} + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppLocalization/attributes' + + +class AppEncryptionDeclaration(Resource): + endpoint = '/v1/appEncryptionDeclarations' + type = 'appEncryptionDeclarations' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appEncryptionDeclaration/attributes' + + +class BetaLicenseAgreement(Resource): + endpoint = '/v1/betaLicenseAgreements' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaLicenseAgreement/attributes' + + +# Build Resources + +class Build(Resource): + endpoint = '/v1/builds' + type = 'builds' + attributes = ['expired', 'iconAssetToken', 'minOsVersion', 'processingState', 'version', 'usesNonExemptEncryption', 'uploadedDate', 'expirationDate'] + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/build/attributes' + + +class BuildBetaDetail(Resource): + endpoint = '/v1/buildBetaDetails' + type = 'buildBetaDetails' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/buildBetaDetail/attributes' + + +class BetaBuildLocalization(Resource): + endpoint = '/v1/betaBuildLocalizations' + type = 'betaBuildLocalizations' + attributes = ['locale', 'whatsNew'] + relationships = { + 'build': {'multiple': False}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaBuildLocalization/attributes' + + +class BetaAppReviewDetail(Resource): + endpoint = '/v1/betaAppReviewDetails' + type = 'betaAppReviewDetails' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppReviewDetail/attributes' + + +class BetaAppReviewSubmission(Resource): + endpoint = '/v1/betaAppReviewSubmissions' + type = 'betaAppReviewSubmissions' + attributes = ['betaReviewState'] + relationships = { + 'build': {'multiple': False}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppReviewSubmission/attributes' + + +# Users and Roles + +class User(Resource): + endpoint = '/v1/users' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/user/attributes' + + +class UserInvitation(Resource): + endpoint = '/v1/userInvitations' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/userinvitation/attributes' + + +# Provisioning +class BundleId(Resource): + endpoint = '/v1/bundleIds' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/bundleid/attributes' + + +class Certificate(Resource): + endpoint = '/v1/certificates' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/certificate/attributes' + + +class Device(Resource): + endpoint = '/v1/devices' + type = 'devices' + attributes = ['name', 'platform', 'udid', 'status'] + relationships = { + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/device/attributes' + + +class Profile(Resource): + endpoint = '/v1/profiles' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/profile/attributes' + + +# Reporting + +class FinanceReport(Resource): + endpoint = '/v1/financeReports' + filters = 'https://developer.apple.com/documentation/appstoreconnectapi/download_finance_reports' + + +class SalesReport(Resource): + endpoint = '/v1/salesReports' + filters = 'https://developer.apple.com/documentation/appstoreconnectapi/download_sales_and_trends_reports' From b3469aae9b94ae39aabd25e78b5256bb20ce02d2 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Wed, 2 Dec 2020 18:04:28 +0100 Subject: [PATCH 06/59] add update category method --- appstoreconnect/api.py | 10 ++++++++++ appstoreconnect/resources.py | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 65cdbf9..ea79821 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -420,6 +420,7 @@ def read_app_information(self, app_ip): :param app_ip: :return: an App resource """ + print("HELLLLLOO") return self._get_resource(App, app_ip) def list_apps(self, filters=None, sort=None): @@ -613,6 +614,15 @@ def get_build_info(self, build_id): """ return self._get_resource(Build, build_id) + def UpdateAppCategories(self, appInfoId, primary, secondary): + if (len(secondary) != 0 and len(primary) != 0): + post_data = {'data':{'type':'appInfos','id':appInfoId,'relationships':{'primaryCategory':{'data':{'type':'appCategories','id':primary}},'secondaryCategory':{'data':{'type':'appCategories','id':secondary}}}}} + elif (len(primary) != 0 and len(secondary) == 0): + post_data = {'data':{'type':'appInfos','id':appInfoId,'relationships':{'primaryCategory':{'data':{'type':'appCategories','id':primary}}}}} + else: + return + return self._api_call(f"https://api.appstoreconnect.apple.com/v1/appInfos/{appInfoId}", HttpMethod.PATCH, post_data) + # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): # setup required filters if not provided diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 8cd5b2c..f09dc06 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -227,3 +227,12 @@ class FinanceReport(Resource): class SalesReport(Resource): endpoint = '/v1/salesReports' filters = 'https://developer.apple.com/documentation/appstoreconnectapi/download_sales_and_trends_reports' + +class AppCategory(Resource): + type = 'appCategories' + attributes = ['platforms'] + relationships = { + 'parent': {'multiple': False}, + 'subcategories': {'multiple': True} + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appcategory' From c4601a4073128c9b3ec1b86d8fd26c02a02a9938 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Thu, 3 Dec 2020 14:12:04 +0100 Subject: [PATCH 07/59] update prilary category miss secondary category --- appstoreconnect/api.py | 26 ++++++++++++++++++++++++-- appstoreconnect/resources.py | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index ea79821..102f408 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -420,7 +420,6 @@ def read_app_information(self, app_ip): :param app_ip: :return: an App resource """ - print("HELLLLLOO") return self._get_resource(App, app_ip) def list_apps(self, filters=None, sort=None): @@ -614,7 +613,25 @@ def get_build_info(self, build_id): """ return self._get_resource(Build, build_id) - def UpdateAppCategories(self, appInfoId, primary, secondary): + def read_app_category_info(self, app_category_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_category_information + :return: an iterator over AppCategory resources + """ + return self._get_resource(AppCategory, app_category_id) + + def get_app_info(self, app_id): + return self._api_call(BASE_API+f"/v1/apps/{app_id}/appInfos", HttpMethod.GET, None) + + def list_app_infos(self, app_id: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_infos_for_an_app + :return: an iterator over AppCategory resources + """ + full_url = BASE_API + "/v1/apps/" + app_id + "/appInfos" + return self._get_resources(AppInfo, None, None, full_url) + + '''def update_app_info(self, appInfoId, primary, secondary): if (len(secondary) != 0 and len(primary) != 0): post_data = {'data':{'type':'appInfos','id':appInfoId,'relationships':{'primaryCategory':{'data':{'type':'appCategories','id':primary}},'secondaryCategory':{'data':{'type':'appCategories','id':secondary}}}}} elif (len(primary) != 0 and len(secondary) == 0): @@ -622,6 +639,11 @@ def UpdateAppCategories(self, appInfoId, primary, secondary): else: return return self._api_call(f"https://api.appstoreconnect.apple.com/v1/appInfos/{appInfoId}", HttpMethod.PATCH, post_data) + ''' + def update_app_store_version(self, app_store_version: AppStoreVersion, args): + return self._modify_resource(app_store_version, args) + def update_app_info(self, app_information: AppInfo, args): + return self._modify_resource(app_information, args) # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index f09dc06..c556b38 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -229,6 +229,7 @@ class SalesReport(Resource): filters = 'https://developer.apple.com/documentation/appstoreconnectapi/download_sales_and_trends_reports' class AppCategory(Resource): + endpoint = '/v1/appCategories' type = 'appCategories' attributes = ['platforms'] relationships = { @@ -236,3 +237,20 @@ class AppCategory(Resource): 'subcategories': {'multiple': True} } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appcategory' + +class AppInfo(Resource): + endpoint = '/v1/appInfos' + type = 'appInfos' + attributes = ['appStoreAgeRating', 'appStoreState', 'brazilAgeRating', 'kidsAgeBand'] + relationships = { + 'app': {'multiple': False}, + 'appInfoLocalizations': {'multiple': True}, + 'primaryCategory': {'multiple': False}, + 'primarySubcategoryOne': {'multiple': False}, + 'primarySubcategoryTwo': {'multiple': False}, + 'secondaryCategory': {'multiple': False}, + 'secondarySubcategoryOne': {'multiple': False}, + 'secondarySubcategoryTwo': {'multiple': False} + + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appinfo' From bdb42907b9b65a4140e61f5994040c8eacf5523b Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Mon, 7 Dec 2020 16:38:30 +0100 Subject: [PATCH 08/59] add app version and info localizations update and this commit contains to app categories update --- appstoreconnect/api.py | 54 ++++++++++++++++++++++++------------ appstoreconnect/resources.py | 20 +++++++++++++ 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 102f408..19dac20 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -600,9 +600,6 @@ def list_profiles(self, filters=None, sort=None): """ return self._get_resources(Profile, filters, sort) - def update_app_store_version(self, app_store_version: AppStoreVersion, args): - return self._modify_resource(app_store_version, args) - def create_new_version_for_app(self, app_store_version: AppStoreVersion, args): return self._create_resource(app_store_version, args) @@ -620,9 +617,6 @@ def read_app_category_info(self, app_category_id): """ return self._get_resource(AppCategory, app_category_id) - def get_app_info(self, app_id): - return self._api_call(BASE_API+f"/v1/apps/{app_id}/appInfos", HttpMethod.GET, None) - def list_app_infos(self, app_id: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_infos_for_an_app @@ -631,19 +625,43 @@ def list_app_infos(self, app_id: str): full_url = BASE_API + "/v1/apps/" + app_id + "/appInfos" return self._get_resources(AppInfo, None, None, full_url) - '''def update_app_info(self, appInfoId, primary, secondary): - if (len(secondary) != 0 and len(primary) != 0): - post_data = {'data':{'type':'appInfos','id':appInfoId,'relationships':{'primaryCategory':{'data':{'type':'appCategories','id':primary}},'secondaryCategory':{'data':{'type':'appCategories','id':secondary}}}}} - elif (len(primary) != 0 and len(secondary) == 0): - post_data = {'data':{'type':'appInfos','id':appInfoId,'relationships':{'primaryCategory':{'data':{'type':'appCategories','id':primary}}}}} - else: - return - return self._api_call(f"https://api.appstoreconnect.apple.com/v1/appInfos/{appInfoId}", HttpMethod.PATCH, post_data) - ''' - def update_app_store_version(self, app_store_version: AppStoreVersion, args): + def modify_app_store_version(self, app_store_version: AppStoreVersion, args): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version + :return: an iterator over AppStoreVersion resources + """ return self._modify_resource(app_store_version, args) - def update_app_info(self, app_information: AppInfo, args): - return self._modify_resource(app_information, args) + + def modify_app_info(self, app_information: AppInfo, args): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info + :return: an iterator over AppInfo resources + """ + return self._modify_resource(app_information, args) + + # appStoreVersions localization + def get_app_store_version_localizations(self, appstoreversion_id): + full_url = BASE_API + f"/v1/appStoreVersions/{appstoreversion_id}/appStoreVersionLocalizations" + return self._get_resources(AppStoreVersionLocalization, None, None, full_url) + + def modify_app_store_version_localization(self, AppStoreVersionLocalizations, attributes): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version_localization + :return: an iterator over AppInfoLocalization resources + """ + return self._modify_resource(AppStoreVersionLocalizations, attributes) + + # appStoreInfo localization + def get_app_store_info_localization(self, app_information): + full_url = BASE_API + f"/v1/appInfos/{app_information.id}/appInfoLocalizations" + return self._get_resources(AppInfoLocalization, None, None, full_url) + + def modify_app_store_info_localization(self, AppInfoLocalization, attributes): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info_localization + :return: an iterator over AppInfoLocalization resources + """ + return self._modify_resource(AppInfoLocalization, attributes) # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index c556b38..efe2fbf 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -76,6 +76,17 @@ class BetaGroup(Resource): # App Metadata Resources +class AppStoreVersionLocalization(Resource): + endpoint = '/v1/appStoreVersionLocalizations' + type = 'appStoreVersionLocalizations' + attributes = ['description', 'keywords', 'locale', 'marketingUrl', 'promotionalText' 'supportUrl', 'whatsNew'] + relationships = { + 'appPreviewSets': {'multiple': True}, + 'appScreenshotSets': {'multiple': True}, + 'appStoreVersion': {'multiple': False} + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionlocalization' + class AppStoreVersion(Resource): endpoint = '/v1/appStoreVersions' type = 'appStoreVersions' @@ -93,6 +104,15 @@ class AppStoreVersion(Resource): } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversion' +class AppInfoLocalization(Resource): + endpoint = '/v1/appInfoLocalizations' + type = 'appInfoLocalizations' + attributes = ['locale', 'name', 'privacyPolicyText', 'privacyPolicyUrl', 'subtitle'] + relationships = { + 'appInfo': {'multiple': False}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appinfolocalization' + # App Resources class App(Resource): From b9d78aa35a7dbc89fea8344fcc1b151c5e3217d8 Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Tue, 8 Dec 2020 11:29:27 +0100 Subject: [PATCH 09/59] added missing object definition for AppStoreVersion relationships modified add_build_to_beta_group because it does not return a BetaGroup object modified modify and create app_store_version functions --- appstoreconnect/api.py | 24 ++++++++++++++++-------- appstoreconnect/resources.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 65cdbf9..68cabf9 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -410,8 +410,7 @@ def read_beta_group_information(self, beta_group_ip): def add_build_to_beta_group(self, beta_group_id, build_id): post_data = {'data': [{ 'id': build_id, 'type': 'builds'}]} - payload = self._api_call(BASE_API + "/v1/betaGroups/" + beta_group_id + "/relationships/builds", HttpMethod.POST, post_data) - return BetaGroup(payload.get('data'), {}) + self._api_call(BASE_API + "/v1/betaGroups/" + beta_group_id + "/relationships/builds", HttpMethod.POST, post_data) # App Resources def read_app_information(self, app_ip): @@ -600,12 +599,6 @@ def list_profiles(self, filters=None, sort=None): """ return self._get_resources(Profile, filters, sort) - def update_app_store_version(self, app_store_version: AppStoreVersion, args): - return self._modify_resource(app_store_version, args) - - def create_new_version_for_app(self, app_store_version: AppStoreVersion, args): - return self._create_resource(app_store_version, args) - def get_build_info(self, build_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_build_information @@ -613,6 +606,21 @@ def get_build_info(self, build_id): """ return self._get_resource(Build, build_id) + # App Metadata + def modify_app_store_version(self, app_store_version: AppStoreVersion, versionString: str, build: Build = None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version + :return: a Device resource + """ + return self._modify_resource(app_store_version, locals()) + + def create_new_app_store_version(self, platform: str, versionString: str, app: App, build: Build = None) -> AppStoreVersion: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version + :return: a AppStoreVersion resource + """ + return self._create_resource(AppStoreVersion, locals()) + # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): # setup required filters if not provided diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 8cd5b2c..92351ac 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -79,7 +79,7 @@ class BetaGroup(Resource): class AppStoreVersion(Resource): endpoint = '/v1/appStoreVersions' type = 'appStoreVersions' - attributes = ['platform', 'appStoreState', 'copyright', 'earliestReleaseDate', 'releaseType', 'usesIdfa', 'versionString', 'createdDate', 'downloadable'] + attributes = ['platform', 'appStoreState', 'copyright', 'earliestReleaseDate', 'releaseType', 'usesIdfa', 'versionString', 'downloadable', 'createdDate'] relationships = { 'app': {'multiple': False}, 'ageRatingDeclaration': {'multiple': False}, @@ -93,6 +93,36 @@ class AppStoreVersion(Resource): } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversion' +class AgeRatingDeclaration(Resource): + endpoint = '/v1/ageRatingDeclarations' + type = "ageRatingDeclarations" + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/ageratingdeclaration' + +class AppStoreReviewDetail(Resource): + endpoint = '/v1/appStoreReviewDetails' + type = "appStoreReviewDetails" + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstorereviewdetail' + +class AppStoreVersionLocalizations(Resource): + endpoint = '/v1/appStoreVersionLocalizations' + type = "appStoreVersionLocalizations" + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionlocalization' + +class AppStoreVersionSubmission(Resource): + endpoint = '/v1/appStoreVersionSubmissions' + type = "appStoreVersionSubmissions" + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionsubmission' + +class IdfaDeclaration(Resource): + endpoint = '/v1/idfaDeclarations' + type = "idfaDeclarations" + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/idfadeclaration' + +class RoutingAppCoverage(Resource): + endpoint = '/v1/routingAppCoverages' + type = "routingAppCoverages" + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/routingappcoverages' + # App Resources class App(Resource): From c52dda6688d7cc022f466946e4e298585cee6f87 Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Tue, 8 Dec 2020 17:48:07 +0100 Subject: [PATCH 10/59] deleted AppStoreState class --- appstoreconnect/api.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 68cabf9..c384537 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -23,29 +23,6 @@ class HttpMethod(Enum): PATCH = 3 DELETE = 4 -class AppStoreState(Enum): - DEVELOPER_REMOVED_FROM_SALE = "DEVELOPER_REMOVED_FROM_SALE" - DEVELOPER_REJECTED = "DEVELOPER_REJECTED" - IN_REVIEW = "IN_REVIEW" - INVALID_BINARY = "INVALID_BINARY" - METADATA_REJECTED = "METADATA_REJECTED" - PENDING_APPLE_RELEASE = "PENDING_APPLE_RELEASE" - PENDING_CONTRACT = "PENDING_CONTRACT" - PENDING_DEVELOPER_RELEASE = "PENDING_DEVELOPER_RELEASE" - PREPARE_FOR_SUBMISSION = "PREPARE_FOR_SUBMISSION" - PREORDER_READY_FOR_SALE = "PREORDER_READY_FOR_SALE" - PROCESSING_FOR_APP_STORE = "PROCESSING_FOR_APP_STORE" - READY_FOR_SALE = "READY_FOR_SALE" - REJECTED = "REJECTED" - REMOVED_FROM_SALE = "REMOVED_FROM_SALE" - WAITING_FOR_EXPORT_COMPLIANCE = "WAITING_FOR_EXPORT_COMPLIANCE" - WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" - REPLACED_WITH_NEW_VERSION = "REPLACED_WITH_NEW_VERSION" - - @staticmethod - def editableStates(): - return list(map(lambda x: x.name, [AppStoreState.DEVELOPER_REJECTED, AppStoreState.INVALID_BINARY, AppStoreState.METADATA_REJECTED, AppStoreState.PREPARE_FOR_SUBMISSION, AppStoreState.REJECTED])) - class APIError(Exception): def __init__(self, error_string, status_code): try: From 0377b277616d203fbf3fd4dac8107b7f2819763f Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Wed, 9 Dec 2020 13:06:35 +0100 Subject: [PATCH 11/59] fixed typo --- appstoreconnect/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index dc34ebf..4f1b66a 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -632,7 +632,7 @@ def modify_app_store_version(self, app_store_version: AppStoreVersion, versionSt def create_new_app_store_version(self, platform: str, versionString: str, app: App, build: Build = None) -> AppStoreVersion: """ - :reference: https://ssdeveloper.apple.com/documentation/appstoreconnectapi/create_an_app_store_version + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version :return: a AppStoreVersion resource """ return self._create_resource(AppStoreVersion, locals()) From aad14c1ffe765d1eb6717b9b6c46f24b827c2fa3 Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Wed, 9 Dec 2020 14:05:33 +0100 Subject: [PATCH 12/59] added again modify_app_store_version_localization ( removed accidentaly ) added copyroth parameters form create and modify app store version functions --- appstoreconnect/api.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 4f1b66a..bcf46e5 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -610,6 +610,13 @@ def get_app_store_version_localizations(self, appstoreversion_id): full_url = BASE_API + f"/v1/appStoreVersions/{appstoreversion_id}/appStoreVersionLocalizations" return self._get_resources(AppStoreVersionLocalization, None, None, full_url) + def modify_app_store_version_localization(self, AppStoreVersionLocalizations, attributes): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version_localization + :return: an iterator over AppInfoLocalization resources + """ + return self._modify_resource(AppStoreVersionLocalizations, attributes) + # appStoreInfo localization def get_app_store_info_localization(self, app_information): full_url = BASE_API + f"/v1/appInfos/{app_information.id}/appInfoLocalizations" @@ -623,14 +630,14 @@ def modify_app_store_info_localization(self, AppInfoLocalization, attributes): return self._modify_resource(AppInfoLocalization, attributes) # App Metadata - def modify_app_store_version(self, app_store_version: AppStoreVersion, versionString: str, build: Build = None): + def modify_app_store_version(self, app_store_version: AppStoreVersion, versionString: str, copyright: str, build: Build = None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version :return: a Device resource """ return self._modify_resource(app_store_version, locals()) - def create_new_app_store_version(self, platform: str, versionString: str, app: App, build: Build = None) -> AppStoreVersion: + def create_new_app_store_version(self, platform: str, versionString: str, copyright: str, app: App, build: Build = None) -> AppStoreVersion: """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version :return: a AppStoreVersion resource From d84c0d5ac21cad7269bd98c6de020245b31d17f7 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk <49659495+dmytrolutsyk@users.noreply.github.com> Date: Wed, 9 Dec 2020 14:42:11 +0100 Subject: [PATCH 13/59] Update api.py Change localisation of functions --- appstoreconnect/api.py | 46 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index bcf46e5..c8c5be4 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -397,6 +397,14 @@ def read_app_information(self, app_ip): :return: an App resource """ return self._get_resource(App, app_ip) + + def list_app_infos(self, app_id: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_infos_for_an_app + :return: an iterator over AppCategory resources + """ + full_url = BASE_API + "/v1/apps/" + app_id + "/appInfos" + return self._get_resources(AppInfo, None, None, full_url) def list_apps(self, filters=None, sort=None): """ @@ -583,28 +591,6 @@ def get_build_info(self, build_id): """ return self._get_resource(Build, build_id) - def read_app_category_info(self, app_category_id): - """ - :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_category_information - :return: an iterator over AppCategory resources - """ - return self._get_resource(AppCategory, app_category_id) - - def list_app_infos(self, app_id: str): - """ - :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_infos_for_an_app - :return: an iterator over AppCategory resources - """ - full_url = BASE_API + "/v1/apps/" + app_id + "/appInfos" - return self._get_resources(AppInfo, None, None, full_url) - - def modify_app_info(self, app_information: AppInfo, args): - """ - :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info - :return: an iterator over AppInfo resources - """ - return self._modify_resource(app_information, args) - # appStoreVersions localization def get_app_store_version_localizations(self, appstoreversion_id): full_url = BASE_API + f"/v1/appStoreVersions/{appstoreversion_id}/appStoreVersionLocalizations" @@ -643,6 +629,22 @@ def create_new_app_store_version(self, platform: str, versionString: str, copyri :return: a AppStoreVersion resource """ return self._create_resource(AppStoreVersion, locals()) + + def read_app_category_info(self, app_category_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_category_information + :return: an iterator over AppCategory resources + """ + return self._get_resource(AppCategory, app_category_id) + + # App info Resources + def modify_app_info(self, app_information: AppInfo, args): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info + :return: an iterator over AppInfo resources + """ + return self._modify_resource(app_information, args) + # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): # setup required filters if not provided From 67792faff6295fae0e7cd346029e99803c950473 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Wed, 9 Dec 2020 15:11:33 +0100 Subject: [PATCH 14/59] change parameters of modify_app_info --- appstoreconnect/api.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index c8c5be4..d041025 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -397,7 +397,7 @@ def read_app_information(self, app_ip): :return: an App resource """ return self._get_resource(App, app_ip) - + def list_app_infos(self, app_id: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_infos_for_an_app @@ -456,7 +456,6 @@ def list_beta_license_agreements(self, filters=None): return self._get_resources(BetaLicenseAgreement, filters) # App Metadata Resources - def list_app_store_versions(self, app_id: str, filters=None, sort=None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_versions_for_an_app @@ -629,21 +628,21 @@ def create_new_app_store_version(self, platform: str, versionString: str, copyri :return: a AppStoreVersion resource """ return self._create_resource(AppStoreVersion, locals()) - + def read_app_category_info(self, app_category_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_category_information :return: an iterator over AppCategory resources """ return self._get_resource(AppCategory, app_category_id) - + # App info Resources - def modify_app_info(self, app_information: AppInfo, args): + def modify_app_info(self, app_information: AppInfo, primaryCategory: str = None, secondaryCategory:str = None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info :return: an iterator over AppInfo resources """ - return self._modify_resource(app_information, args) + return self._modify_resource(app_information, locals()) # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): From 8293a0b0bda01d71c00c9fbea4a2c8e9dc6eedbc Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Thu, 10 Dec 2020 15:27:37 +0100 Subject: [PATCH 15/59] change list_app_store_info_localizations and add doc ref --- appstoreconnect/api.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d041025..09cb37e 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -603,7 +603,11 @@ def modify_app_store_version_localization(self, AppStoreVersionLocalizations, at return self._modify_resource(AppStoreVersionLocalizations, attributes) # appStoreInfo localization - def get_app_store_info_localization(self, app_information): + def list_app_store_info_localizations(self, app_information): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_info_localizations_for_an_app_info + :return: an iterator over AppInfoLocalization resources + """ full_url = BASE_API + f"/v1/appInfos/{app_information.id}/appInfoLocalizations" return self._get_resources(AppInfoLocalization, None, None, full_url) @@ -637,6 +641,7 @@ def read_app_category_info(self, app_category_id): return self._get_resource(AppCategory, app_category_id) # App info Resources + def modify_app_info(self, app_information: AppInfo, primaryCategory: str = None, secondaryCategory:str = None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info From 97c11b6ed30ded0aa63cf4e660b05f3153b6d4a5 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Thu, 10 Dec 2020 19:01:08 +0100 Subject: [PATCH 16/59] - change modify_app_store_info_localization and modify_app_store_version_localization parameters - add reference for list_app_store_version_localizations and list_app_store_info_localizations --- appstoreconnect/api.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 09cb37e..a9f7751 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -591,16 +591,20 @@ def get_build_info(self, build_id): return self._get_resource(Build, build_id) # appStoreVersions localization - def get_app_store_version_localizations(self, appstoreversion_id): - full_url = BASE_API + f"/v1/appStoreVersions/{appstoreversion_id}/appStoreVersionLocalizations" + def list_app_store_version_localizations(self, appstoreversion): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_version_localizations_for_an_app_store_version + :return: an iterator over AppStoreVersionLocalization resources + """ + full_url = BASE_API + f"/v1/appStoreVersions/{appstoreversion.id}/appStoreVersionLocalizations" return self._get_resources(AppStoreVersionLocalization, None, None, full_url) - def modify_app_store_version_localization(self, AppStoreVersionLocalizations, attributes): + def modify_app_store_version_localization(self, app_store_version_localization: AppStoreVersionLocalization, description: str, keywords: str, marketingUrl: str, promotionalText: str, supportUrl: str, whatsNew: str ): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version_localization :return: an iterator over AppInfoLocalization resources """ - return self._modify_resource(AppStoreVersionLocalizations, attributes) + return self._modify_resource(app_store_version_localization, locals()) # appStoreInfo localization def list_app_store_info_localizations(self, app_information): @@ -611,12 +615,12 @@ def list_app_store_info_localizations(self, app_information): full_url = BASE_API + f"/v1/appInfos/{app_information.id}/appInfoLocalizations" return self._get_resources(AppInfoLocalization, None, None, full_url) - def modify_app_store_info_localization(self, AppInfoLocalization, attributes): + def modify_app_store_info_localization(self, app_info_localization: AppInfoLocalization, name: str, privacyPolicyUrl: str, subtitle: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_info_localization :return: an iterator over AppInfoLocalization resources """ - return self._modify_resource(AppInfoLocalization, attributes) + return self._modify_resource(app_info_localization, locals()) # App Metadata def modify_app_store_version(self, app_store_version: AppStoreVersion, versionString: str, copyright: str, build: Build = None): From 07b49db5ff8fa1ce66189dcbe1d37d8ce9ff396e Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Fri, 11 Dec 2020 18:18:22 +0100 Subject: [PATCH 17/59] add AgeRatingDeclarations class and methods to use read and modify agerating --- appstoreconnect/api.py | 15 +++++++++++++++ appstoreconnect/resources.py | 9 +++++++++ 2 files changed, 24 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index a9f7751..c800bc8 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -465,6 +465,21 @@ def list_app_store_versions(self, app_id: str, filters=None, sort=None): full_url = BASE_API + "/v1/apps/" + app_id + "/appStoreVersions" return self._get_resources(AppStoreVersion, filters, sort, full_url) + def modify_Age_Rating_Declarations(self, Resource, args): + """ + :reference: https://api.appstoreconnect.apple.com/v1/ageRatingDeclarations/{id} + :return: an iterator over Age Rating Declaration resources + """ + return self._modify_resource(Resource, args) + + def read_age_rating_declarations_info(self, appstoreversion_id): + return self._get_resource_adi(AgeRatingDeclarations, appstoreversion_id) + + def _get_resource_adi(self, Resource, resource_id): + url = BASE_API + "/v1/appStoreVersions/" + resource_id + "/ageRatingDeclaration" + payload = self._api_call(url) + return Resource(payload.get('data', {}), self) + # Build Resources def list_builds(self, filters=None, sort=None): """ diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 5730be2..26eedb8 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -87,6 +87,15 @@ class AppStoreVersionLocalization(Resource): } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionlocalization' +class AgeRatingDeclarations(Resource): + endpoint = '/v1/ageRatingDeclarations' + attributes = ['alcoholTobaccoOrDrugUseOrReferences', 'gamblingAndContests', 'kidsAgeBand', 'medicalOrTreatmentInformation', + 'profanityOrCrudeHumor', 'sexualContentOrNudity', 'unrestrictedWebAccess', 'gamblingSimulated', 'horrorOrFearThemes', + 'matureOrSuggestiveThemes', 'sexualContentGraphicAndNudity', 'violenceCartoonOrFantasy', 'violenceRealistic', 'violenceRealisticProlongedGraphicOrSadistic'] + relationships = {} + type = 'ageRatingDeclarations' + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/ageratingdeclaration' + class AppStoreVersion(Resource): endpoint = '/v1/appStoreVersions' type = 'appStoreVersions' From b9eb88f63732b30177c17b3bb574d19ca31b069d Mon Sep 17 00:00:00 2001 From: dmytrolutsyk <49659495+dmytrolutsyk@users.noreply.github.com> Date: Sat, 12 Dec 2020 00:12:46 +0100 Subject: [PATCH 18/59] Update resources.py fix coma bug in AppStoreVersionLocalization class attributes list --- appstoreconnect/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 5730be2..e5fa787 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -79,7 +79,7 @@ class BetaGroup(Resource): class AppStoreVersionLocalization(Resource): endpoint = '/v1/appStoreVersionLocalizations' type = 'appStoreVersionLocalizations' - attributes = ['description', 'keywords', 'locale', 'marketingUrl', 'promotionalText' 'supportUrl', 'whatsNew'] + attributes = ['description', 'keywords', 'locale', 'marketingUrl', 'promotionalText', 'supportUrl', 'whatsNew'] relationships = { 'appPreviewSets': {'multiple': True}, 'appScreenshotSets': {'multiple': True}, From 983dc18ee052b701b42716880b5dc86f6ee04667 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Mon, 21 Dec 2020 11:03:13 +0100 Subject: [PATCH 19/59] factorize read_age_rating_declarations_info, add good reference, refactor the name of function to lowercase string --- appstoreconnect/api.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index c800bc8..526b095 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -465,20 +465,21 @@ def list_app_store_versions(self, app_id: str, filters=None, sort=None): full_url = BASE_API + "/v1/apps/" + app_id + "/appStoreVersions" return self._get_resources(AppStoreVersion, filters, sort, full_url) - def modify_Age_Rating_Declarations(self, Resource, args): + def modify_age_rating_declarations(self, Resource, args): """ - :reference: https://api.appstoreconnect.apple.com/v1/ageRatingDeclarations/{id} - :return: an iterator over Age Rating Declaration resources + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_age_rating_declaration + :return: an iterator over AgeRatingDeclarations resources """ return self._modify_resource(Resource, args) def read_age_rating_declarations_info(self, appstoreversion_id): - return self._get_resource_adi(AgeRatingDeclarations, appstoreversion_id) - - def _get_resource_adi(self, Resource, resource_id): - url = BASE_API + "/v1/appStoreVersions/" + resource_id + "/ageRatingDeclaration" - payload = self._api_call(url) - return Resource(payload.get('data', {}), self) + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_the_age_rating_declaration_information_of_an_app_store_version + :return: an iterator over AgeRatingDeclarations resources + """ + url = BASE_API + "/v1/appStoreVersions/" + appstoreversion_id + "/ageRatingDeclaration" + payload = self._api_call(url) + return AgeRatingDeclarations(payload.get('data', {}), self) # Build Resources def list_builds(self, filters=None, sort=None): From e4c9c4ff6c752e339637a858219611060626415b Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Mon, 21 Dec 2020 14:03:22 +0100 Subject: [PATCH 20/59] add territory class in ressouces.py and and add list_territories list_all_available_territories_for_an_app in api.py --- appstoreconnect/api.py | 15 +++++++++++++++ appstoreconnect/resources.py | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index a9f7751..ddfffed 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -644,6 +644,21 @@ def read_app_category_info(self, app_category_id): """ return self._get_resource(AppCategory, app_category_id) + def list_all_available_territories_for_an_app(self, app_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_available_territories_for_an_app + :return: an iterator over Territory resources + """ + full_url = BASE_API + "/v1/apps/" + app_id + "/availableTerritories" + return self._get_resources(Territory, None, None, full_url) + + def list_territories(self): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_territories + :return: an iterator over a list of Territory resources + """ + return self._get_resources(Territory, None, None, None) + # App info Resources def modify_app_info(self, app_information: AppInfo, primaryCategory: str = None, secondaryCategory:str = None): diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index e5fa787..d3938ad 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -109,6 +109,14 @@ class AgeRatingDeclaration(Resource): type = "ageRatingDeclarations" documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/ageratingdeclaration' +class Territory(Resource): + endpoint = '/v1/territories' + type = 'territories' + attributes = 'currency' + relationships = {} + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/territory' + + class AppStoreReviewDetail(Resource): endpoint = '/v1/appStoreReviewDetails' type = "appStoreReviewDetails" From 79725aa189356ea33af32bb69206bf314e758fd4 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 22 Dec 2020 12:02:50 +0100 Subject: [PATCH 21/59] add AppScreenshot AppScreenshotSet classes and list_all_app_screenshots_for_an_app_screenshot_set list_all_app_screenshots_sets_for_an_app_store_version_localization methodes --- appstoreconnect/api.py | 17 ++++++++++++++++- appstoreconnect/resources.py | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 526b095..d556fcd 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -461,7 +461,6 @@ def list_app_store_versions(self, app_id: str, filters=None, sort=None): :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_versions_for_an_app :return: an iterator over AppStoreVersion resources """ - full_url = BASE_API + "/v1/apps/" + app_id + "/appStoreVersions" return self._get_resources(AppStoreVersion, filters, sort, full_url) @@ -481,6 +480,22 @@ def read_age_rating_declarations_info(self, appstoreversion_id): payload = self._api_call(url) return AgeRatingDeclarations(payload.get('data', {}), self) + def list_all_app_screenshots_sets_for_an_app_store_version_localization(self, appstoreversionlocalization_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_screenshot_sets_for_an_app_store_version_localization + :return: an iterator over AppScreenshotSet resources + """ + url = BASE_API + "/v1/appStoreVersionLocalizations/" + appstoreversionlocalization_id + "/appScreenshotSets" + return self._get_resources(AppScreenshotSet, None, None, url) + + def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_screenshots_for_an_app_screenshot_set + :return: an iterator over AppScreenshot resources + """ + url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" + return self._get_resources(AppScreenshotSet, None, None, url) + # Build Resources def list_builds(self, filters=None, sort=None): """ diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index d4e75b5..fe7da76 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -76,6 +76,25 @@ class BetaGroup(Resource): # App Metadata Resources +class AppScreenshot(Resource): + endpoint = '/v1/appScreenshots' + type = 'appScreenshots' + attributes = ['assetDeliveryState', 'assetToken', 'assetType', 'fileName', 'fileSize', 'imageAsset', 'sourceFileChecksum', 'uploadOperations'] + relationships = { + 'appScreenshotSet': {'multiple': False} + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appscreenshot' + +class AppScreenshotSet(Resource): + endpoint = '/v1/appScreenshotSets' + type = 'appScreenshotSets' + attributes = ['screenshotDisplayType'] + relationships = { + 'appScreenshots': {'multiple': True}, + 'appStoreVersionLocalization': {'multiple': False} + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appscreenshotset' + class AppStoreVersionLocalization(Resource): endpoint = '/v1/appStoreVersionLocalizations' type = 'appStoreVersionLocalizations' From 0a2a6e67ee7af201181335239f70c2170a658a87 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 22 Dec 2020 23:50:10 +0100 Subject: [PATCH 22/59] change Ressource return --- appstoreconnect/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d556fcd..7d25aef 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -494,7 +494,7 @@ def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_i :return: an iterator over AppScreenshot resources """ url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" - return self._get_resources(AppScreenshotSet, None, None, url) + return self._get_resources(AppScreenshot, None, None, url) # Build Resources def list_builds(self, filters=None, sort=None): From 37ebf546ad1222108d7a94a2ec5b8250d4e400ef Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Wed, 23 Dec 2020 15:26:39 +0100 Subject: [PATCH 23/59] add modify app screenshot method --- appstoreconnect/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 7d25aef..be4f657 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -496,6 +496,13 @@ def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_i url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) + def modify_an_app_screenshot(self, Ressource, appscreenshot): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_screenshot + :return: an iterator over AppScreenshot resources + """ + return self._modify_resource(Resource, appscreenshot) + # Build Resources def list_builds(self, filters=None, sort=None): """ From 1fc401d3d01ef26394e5f3a8f3b975faf8f7c829 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Thu, 24 Dec 2020 13:51:34 +0100 Subject: [PATCH 24/59] add modify_an_app_screenshot and create_an_asset_reversion --- appstoreconnect/api.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index be4f657..6331038 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -496,12 +496,19 @@ def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_i url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) - def modify_an_app_screenshot(self, Ressource, appscreenshot): + def modify_an_app_screenshot(self, res, sourceFileChecksum): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_screenshot :return: an iterator over AppScreenshot resources """ - return self._modify_resource(Resource, appscreenshot) + return self._modify_resource(res, sourceFileChecksum) + + def create_an_asset_reversion(self, fileSize: str, fileName: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect + :return: an iterator over AppScreenshot resources + """ + return self._create_resource(AppScreenshot, locals()) # Build Resources def list_builds(self, filters=None, sort=None): From d8403c5c31aaf6b9864c0fdf15f8a2dd3b4dca39 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Thu, 24 Dec 2020 17:53:44 +0100 Subject: [PATCH 25/59] upload asset methode no finished --- appstoreconnect/api.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 6331038..6df597b 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -503,13 +503,25 @@ def modify_an_app_screenshot(self, res, sourceFileChecksum): """ return self._modify_resource(res, sourceFileChecksum) - def create_an_asset_reversion(self, fileSize: str, fileName: str): + def create_an_asset_reversion(self, appScreenshotSet: AppScreenshotSet, fileSize: int, fileName: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect :return: an iterator over AppScreenshot resources """ return self._create_resource(AppScreenshot, locals()) + def upload_the_asset(self, uploadOperation, binary): + '''envoyer juste le header et le body = image direct sans legth offset ect''' + method = uploadOperation['method'] + url = uploadOperation['url'] + length = uploadOperation['length'] + offset = uploadOperation['offset'] + requestHeaders = uploadOperation['requestHeaders'] + + return requests.put(url=url, data={"headers":requestHeaders, "length":length, "offset":offset}) + + + # Build Resources def list_builds(self, filters=None, sort=None): """ From 47ee04d348e150146b104db016ba7a9f4b6244c6 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Mon, 28 Dec 2020 14:46:10 +0100 Subject: [PATCH 26/59] add read_app_screenshot_information and modify create_an_asset_reservation and modify_an_app_screenshot --- appstoreconnect/api.py | 19 +++++++++++++------ appstoreconnect/resources.py | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 6df597b..714d64a 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -232,6 +232,7 @@ def _api_call(self, url, method=HttpMethod.GET, post_data=None): r = requests.post(url=url, headers=headers, data=json.dumps(post_data)) elif method == HttpMethod.PATCH: headers["Content-Type"] = "application/json" + print("HERE:", json.dumps(post_data)) r = requests.patch(url=url, headers=headers, data=json.dumps(post_data)) elif method == HttpMethod.DELETE: r = requests.delete(url=url, headers=headers) @@ -496,14 +497,15 @@ def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_i url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) - def modify_an_app_screenshot(self, res, sourceFileChecksum): + def modify_an_app_screenshot(self, screenshot, sourceFileChecksum: str, uploaded: bool): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_screenshot :return: an iterator over AppScreenshot resources """ - return self._modify_resource(res, sourceFileChecksum) + attributes = {'sourceFileChecksum':sourceFileChecksum, 'uploaded':uploaded} + return self._modify_resource(screenshot, attributes) - def create_an_asset_reversion(self, appScreenshotSet: AppScreenshotSet, fileSize: int, fileName: str): + def create_an_asset_reservation(self, appScreenshotSet: AppScreenshotSet, fileSize: int, fileName: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect :return: an iterator over AppScreenshot resources @@ -514,11 +516,16 @@ def upload_the_asset(self, uploadOperation, binary): '''envoyer juste le header et le body = image direct sans legth offset ect''' method = uploadOperation['method'] url = uploadOperation['url'] - length = uploadOperation['length'] - offset = uploadOperation['offset'] requestHeaders = uploadOperation['requestHeaders'] - return requests.put(url=url, data={"headers":requestHeaders, "length":length, "offset":offset}) + return requests.put(url=url, data = binary, headers = {'Content-Type':'image/png'}) + + def read_app_screenshot_information(self, appScreenshot_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_screenshot_information + :return: an iterator over AppScreenshot resource + """ + return self._get_resource(AppScreenshot, appScreenshot_id) diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index fe7da76..cf3c233 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -79,7 +79,7 @@ class BetaGroup(Resource): class AppScreenshot(Resource): endpoint = '/v1/appScreenshots' type = 'appScreenshots' - attributes = ['assetDeliveryState', 'assetToken', 'assetType', 'fileName', 'fileSize', 'imageAsset', 'sourceFileChecksum', 'uploadOperations'] + attributes = ['assetDeliveryState', 'assetToken', 'assetType', 'fileName', 'fileSize', 'imageAsset', 'sourceFileChecksum', 'uploadOperations', 'uploaded'] relationships = { 'appScreenshotSet': {'multiple': False} } From 58c5a3bc15cb83480568f2f59e7d4a1b81405bce Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 29 Dec 2020 17:57:02 +0100 Subject: [PATCH 27/59] add delete screenshot methid and update modify screenshot methid --- appstoreconnect/api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 714d64a..0aaa870 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -497,13 +497,13 @@ def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_i url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) - def modify_an_app_screenshot(self, screenshot, sourceFileChecksum: str, uploaded: bool): + def modify_an_app_screenshot(self, app_screenshot: AppScreenshot, sourceFileChecksum: str, uploaded: bool): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_screenshot :return: an iterator over AppScreenshot resources """ attributes = {'sourceFileChecksum':sourceFileChecksum, 'uploaded':uploaded} - return self._modify_resource(screenshot, attributes) + return self._modify_resource(app_screenshot, attributes) def create_an_asset_reservation(self, appScreenshotSet: AppScreenshotSet, fileSize: int, fileName: str): """ @@ -527,6 +527,12 @@ def read_app_screenshot_information(self, appScreenshot_id): """ return self._get_resource(AppScreenshot, appScreenshot_id) + def delete_an_app_screenshot(self, app_screenshot): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_an_app_screenshot + :return: an iterator over AppScreenshot resource + """ + return self._delete_resource(app_screenshot) # Build Resources From a12cba72166ca4cbf1983dc60ab52e1f2554c925 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Thu, 31 Dec 2020 11:52:06 +0100 Subject: [PATCH 28/59] add replace_all_app_screenshots_for_an_app_screenshot_set --- appstoreconnect/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 0aaa870..e738d04 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -534,6 +534,13 @@ def delete_an_app_screenshot(self, app_screenshot): """ return self._delete_resource(app_screenshot) + def replace_all_app_screenshots_for_an_app_screenshot_set(self, appScreenshot_Set, post_data): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/replace_all_app_screenshots_for_an_app_screenshot_set + :return: an iterator over AppScreenshotSet resource + """ + return self._api_call(BASE_API + "/v1/appScreenshotSets/" + appScreenshot_Set.id + "/relationships/appScreenshots", HttpMethod.PATCH, post_data) + # Build Resources def list_builds(self, filters=None, sort=None): From ef3c1b48774ad81df96ad9862ee1ba850c7d81e4 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Wed, 6 Jan 2021 10:00:41 +0100 Subject: [PATCH 29/59] reformate post data --- appstoreconnect/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index e738d04..b6610fb 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -534,11 +534,12 @@ def delete_an_app_screenshot(self, app_screenshot): """ return self._delete_resource(app_screenshot) - def replace_all_app_screenshots_for_an_app_screenshot_set(self, appScreenshot_Set, post_data): + def replace_all_app_screenshots_for_an_app_screenshot_set(self, appScreenshot_Set, data): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/replace_all_app_screenshots_for_an_app_screenshot_set :return: an iterator over AppScreenshotSet resource """ + post_data = {"data": data } return self._api_call(BASE_API + "/v1/appScreenshotSets/" + appScreenshot_Set.id + "/relationships/appScreenshots", HttpMethod.PATCH, post_data) From bd4b606315fe965b4d4382e426f15df5bbf73960 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Fri, 8 Jan 2021 11:23:17 +0100 Subject: [PATCH 30/59] delete useless data print --- appstoreconnect/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index b6610fb..9fd892e 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -232,7 +232,6 @@ def _api_call(self, url, method=HttpMethod.GET, post_data=None): r = requests.post(url=url, headers=headers, data=json.dumps(post_data)) elif method == HttpMethod.PATCH: headers["Content-Type"] = "application/json" - print("HERE:", json.dumps(post_data)) r = requests.patch(url=url, headers=headers, data=json.dumps(post_data)) elif method == HttpMethod.DELETE: r = requests.delete(url=url, headers=headers) From 1c8230aefbf6c624c442acf01bd82e3af1de2d7d Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 12 Jan 2021 12:00:38 +0100 Subject: [PATCH 31/59] add create, read, modify, delete method for AppStoreVersionPhasedRelease class added himself too --- appstoreconnect/api.py | 29 +++++++++++++++++++++++++++++ appstoreconnect/resources.py | 9 +++++++++ 2 files changed, 38 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index f92bc21..1bdb590 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -675,6 +675,35 @@ def list_territories(self): """ return self._get_resources(Territory, None, None, None) + #App Store Version Phased RELEASE + def create_an_app_store_version_phased_release(self, phased_release_state: str, appStoreVersion: AppStoreVersion): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :return: an iterator over AppStoreVersionPhasedRelease resources + """ + return self._create_resource(AppStoreVersionPhasedRelease, locals()) + + def read_the_app_store_version_phased_release_information_of_an_app_store_version(self, resource_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :return: an iterator over AppStoreVersionPhasedRelease resources + """ + url = f"https://api.appstoreconnect.apple.com/v1/appStoreVersions/{resource_id}/appStoreVersionPhasedRelease" + return self._get_related_resource(AppStoreVersionPhasedRelease, url) + + def delete_an_app_store_version_phased_release(self, appStoreVersionPhasedRelease: AppStoreVersionPhasedRelease ): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :return: an empty iterator over a list of AppStoreVersionPhasedRelease resources + """ + return self._delete_resource(appStoreVersionPhasedRelease) + + def modify_an_app_store_version_phased_release(self, appStoreVersionPhasedRelease: AppStoreVersionPhasedRelease, phasedReleaseState: str): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :return: an iterator over a list of AppStoreVersionPhasedRelease resources + """ + return self._modify_resource(appStoreVersionPhasedRelease, locals()) # App info Resources def modify_app_info(self, app_information: AppInfo, primaryCategory: str = None, secondaryCategory:str = None): diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 40f0725..eea0f40 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -160,6 +160,15 @@ class AppInfoLocalization(Resource): } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appinfolocalization' +class AppStoreVersionPhasedRelease(Resource): + endpoint = '/v1/appStoreVersionPhasedReleases' + type = 'appStoreVersionPhasedReleases' + attributes = ['currentDayNumber', 'phasedReleaseState', 'startDate', 'totalPauseDuration'] + relationships = { + 'appStoreVersion': {'multiple': False}, + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionphasedrelease' + # App Resources class App(Resource): From 4c2afa3696b9b2c04ee30e2b91b514ff30bd3d3a Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Mon, 18 Jan 2021 14:43:57 +0100 Subject: [PATCH 32/59] a quick fix to get all territories; the fix to do would be to support paging --- appstoreconnect/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index f92bc21..8001eae 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -665,7 +665,7 @@ def list_all_available_territories_for_an_app(self, app_id): :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_available_territories_for_an_app :return: an iterator over Territory resources """ - full_url = BASE_API + "/v1/apps/" + app_id + "/availableTerritories" + full_url = BASE_API + "/v1/apps/" + app_id + "/availableTerritories?limit=200" return self._get_resources(Territory, None, None, full_url) def list_territories(self): From e4207743c0ea87adced9259efd48780e957ccac3 Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Wed, 20 Jan 2021 17:05:49 +0100 Subject: [PATCH 33/59] added delete_app_store_version function --- appstoreconnect/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 8001eae..c567373 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -646,6 +646,13 @@ def modify_app_store_version(self, app_store_version: AppStoreVersion, versionSt """ return self._modify_resource(app_store_version, locals()) + def delete_app_store_version(self, app_store_version: AppStoreVersion): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_an_app_store_version + :return: None + """ + return self._delete_resource(app_store_version) + def create_new_app_store_version(self, platform: str, versionString: str, copyright: str, app: App, build: Build = None) -> AppStoreVersion: """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version From e4e826a1269b3960f486040041cdd3725e2b864b Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Mon, 25 Jan 2021 10:55:16 +0100 Subject: [PATCH 34/59] add reference to upload_the_asset() method --- appstoreconnect/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 9fd892e..768bd1a 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -512,7 +512,10 @@ def create_an_asset_reservation(self, appScreenshotSet: AppScreenshotSet, fileSi return self._create_resource(AppScreenshot, locals()) def upload_the_asset(self, uploadOperation, binary): - '''envoyer juste le header et le body = image direct sans legth offset ect''' + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect + :return: an json answer + """ method = uploadOperation['method'] url = uploadOperation['url'] requestHeaders = uploadOperation['requestHeaders'] From 606f87e2c363dc027a84ceaf1c2975056d8e6ed9 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 26 Jan 2021 12:23:29 +0100 Subject: [PATCH 35/59] provisional fix issue with Content-type --- appstoreconnect/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index a846859..7abb8e0 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -518,9 +518,10 @@ def upload_the_asset(self, uploadOperation, binary): """ method = uploadOperation['method'] url = uploadOperation['url'] - requestHeaders = uploadOperation['requestHeaders'] + requestHeaderType = uploadOperation['requestHeaders'][0]['name'] + requestHeaderImage = uploadOperation['requestHeaders'][0]['value'] - return requests.put(url=url, data = binary, headers = {'Content-Type':'image/png'}) + return requests.put(url=url, data = binary, headers = {requestHeaderType:requestHeaderImage}) def read_app_screenshot_information(self, appScreenshot_id): """ From 162fd5c7e6761640443c642a24350295cb654901 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 26 Jan 2021 14:17:31 +0100 Subject: [PATCH 36/59] remplace precedent issue fix by generic headers --- appstoreconnect/api.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 7abb8e0..ed146a8 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta import time import json +import yaml from enum import Enum from resources import * @@ -518,10 +519,17 @@ def upload_the_asset(self, uploadOperation, binary): """ method = uploadOperation['method'] url = uploadOperation['url'] - requestHeaderType = uploadOperation['requestHeaders'][0]['name'] - requestHeaderImage = uploadOperation['requestHeaders'][0]['value'] - - return requests.put(url=url, data = binary, headers = {requestHeaderType:requestHeaderImage}) + headerString = "{" + for x, y in enumerate(uploadOperation['requestHeaders']): + name = uploadOperation['requestHeaders'][x]['name'] + value = uploadOperation['requestHeaders'][x]['value'] + headerString = headerString +str(name)+" : "+str(value) + if x < len(uploadOperation['requestHeaders']) -1: + headerString = headerString + ", " + headerString = headerString + "}" + + heders = yaml.load(headerString) + return requests.put(url=url, data = binary, headers = heders) def read_app_screenshot_information(self, appScreenshot_id): """ From bab9092fe41754960d0314fa0ef87158a6325094 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 26 Jan 2021 14:28:42 +0100 Subject: [PATCH 37/59] change variable heders to headers --- appstoreconnect/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index ed146a8..d09e273 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -528,8 +528,8 @@ def upload_the_asset(self, uploadOperation, binary): headerString = headerString + ", " headerString = headerString + "}" - heders = yaml.load(headerString) - return requests.put(url=url, data = binary, headers = heders) + headers = yaml.load(headerString) + return requests.put(url=url, data = binary, headers = headers) def read_app_screenshot_information(self, appScreenshot_id): """ From ab8bbc92b961f5048e00ef9fe6fed6dd7d74d8a6 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Fri, 29 Jan 2021 14:32:59 +0100 Subject: [PATCH 38/59] change headers construction in upload_the_asset and change some variables --- appstoreconnect/api.py | 45 ++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d09e273..6e8aecb 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -8,7 +8,6 @@ from datetime import datetime, timedelta import time import json -import yaml from enum import Enum from resources import * @@ -472,29 +471,29 @@ def modify_age_rating_declarations(self, Resource, args): """ return self._modify_resource(Resource, args) - def read_age_rating_declarations_info(self, appstoreversion_id): + def read_age_rating_declarations_info(self, app_store_version_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_the_age_rating_declaration_information_of_an_app_store_version :return: an iterator over AgeRatingDeclarations resources """ - url = BASE_API + "/v1/appStoreVersions/" + appstoreversion_id + "/ageRatingDeclaration" + url = BASE_API + "/v1/appStoreVersions/" + app_store_version_id + "/ageRatingDeclaration" payload = self._api_call(url) return AgeRatingDeclarations(payload.get('data', {}), self) - def list_all_app_screenshots_sets_for_an_app_store_version_localization(self, appstoreversionlocalization_id): + def list_all_app_screenshots_sets_for_an_app_store_version_localization(self, app_store_version_localization_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_screenshot_sets_for_an_app_store_version_localization :return: an iterator over AppScreenshotSet resources """ - url = BASE_API + "/v1/appStoreVersionLocalizations/" + appstoreversionlocalization_id + "/appScreenshotSets" + url = BASE_API + "/v1/appStoreVersionLocalizations/" + app_store_version_localization_id + "/appScreenshotSets" return self._get_resources(AppScreenshotSet, None, None, url) - def list_all_app_screenshots_for_an_app_screenshot_set(self, appscreenshotsets_id): + def list_all_app_screenshots_for_an_app_screenshot_set(self, app_screen_shot_sets_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_screenshots_for_an_app_screenshot_set :return: an iterator over AppScreenshot resources """ - url = BASE_API + "/v1/appScreenshotSets/" + appscreenshotsets_id + "/appScreenshots" + url = BASE_API + "/v1/appScreenshotSets/" + app_screen_shot_sets_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) def modify_an_app_screenshot(self, app_screenshot: AppScreenshot, sourceFileChecksum: str, uploaded: bool): @@ -512,31 +511,25 @@ def create_an_asset_reservation(self, appScreenshotSet: AppScreenshotSet, fileSi """ return self._create_resource(AppScreenshot, locals()) - def upload_the_asset(self, uploadOperation, binary): + def upload_the_asset(self, upload_operation, binary): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/uploading_assets_to_app_store_connect :return: an json answer """ - method = uploadOperation['method'] - url = uploadOperation['url'] - headerString = "{" - for x, y in enumerate(uploadOperation['requestHeaders']): - name = uploadOperation['requestHeaders'][x]['name'] - value = uploadOperation['requestHeaders'][x]['value'] - headerString = headerString +str(name)+" : "+str(value) - if x < len(uploadOperation['requestHeaders']) -1: - headerString = headerString + ", " - headerString = headerString + "}" - - headers = yaml.load(headerString) + headers = {} + url = upload_operation['url'] + + for header in upload_operation['requestHeaders']: + headers[header['name']] = header['value'] + return requests.put(url=url, data = binary, headers = headers) - def read_app_screenshot_information(self, appScreenshot_id): + def read_app_screenshot_information(self, app_screenshot_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_screenshot_information :return: an iterator over AppScreenshot resource """ - return self._get_resource(AppScreenshot, appScreenshot_id) + return self._get_resource(AppScreenshot, app_screenshot_id) def delete_an_app_screenshot(self, app_screenshot): """ @@ -545,13 +538,13 @@ def delete_an_app_screenshot(self, app_screenshot): """ return self._delete_resource(app_screenshot) - def replace_all_app_screenshots_for_an_app_screenshot_set(self, appScreenshot_Set, data): + def replace_all_app_screenshots_for_an_app_screenshot_set(self, app_screenshot_set, data): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/replace_all_app_screenshots_for_an_app_screenshot_set :return: an iterator over AppScreenshotSet resource """ post_data = {"data": data } - return self._api_call(BASE_API + "/v1/appScreenshotSets/" + appScreenshot_Set.id + "/relationships/appScreenshots", HttpMethod.PATCH, post_data) + return self._api_call(BASE_API + "/v1/appScreenshotSets/" + app_screenshot_set.id + "/relationships/appScreenshots", HttpMethod.PATCH, post_data) # Build Resources @@ -680,12 +673,12 @@ def get_build_info(self, build_id): return self._get_resource(Build, build_id) # appStoreVersions localization - def list_app_store_version_localizations(self, appstoreversion): + def list_app_store_version_localizations(self, app_store_version): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_store_version_localizations_for_an_app_store_version :return: an iterator over AppStoreVersionLocalization resources """ - full_url = BASE_API + f"/v1/appStoreVersions/{appstoreversion.id}/appStoreVersionLocalizations" + full_url = BASE_API + f"/v1/appStoreVersions/{app_store_version.id}/appStoreVersionLocalizations" return self._get_resources(AppStoreVersionLocalization, None, None, full_url) def modify_app_store_version_localization(self, app_store_version_localization: AppStoreVersionLocalization, description: str, keywords: str, marketingUrl: str, promotionalText: str, supportUrl: str, whatsNew: str ): From 87af4da38a48511cb5b96b0873dbae1ba3ce3e88 Mon Sep 17 00:00:00 2001 From: dmytrolutsyk <49659495+dmytrolutsyk@users.noreply.github.com> Date: Fri, 29 Jan 2021 16:37:55 +0100 Subject: [PATCH 39/59] Update api.py --- appstoreconnect/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 6e8aecb..83f8852 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -488,12 +488,12 @@ def list_all_app_screenshots_sets_for_an_app_store_version_localization(self, ap url = BASE_API + "/v1/appStoreVersionLocalizations/" + app_store_version_localization_id + "/appScreenshotSets" return self._get_resources(AppScreenshotSet, None, None, url) - def list_all_app_screenshots_for_an_app_screenshot_set(self, app_screen_shot_sets_id): + def list_all_app_screenshots_for_an_app_screenshot_set(self, app_screen_shot_set_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_screenshots_for_an_app_screenshot_set :return: an iterator over AppScreenshot resources """ - url = BASE_API + "/v1/appScreenshotSets/" + app_screen_shot_sets_id + "/appScreenshots" + url = BASE_API + "/v1/appScreenshotSets/" + app_screen_shot_set_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) def modify_an_app_screenshot(self, app_screenshot: AppScreenshot, sourceFileChecksum: str, uploaded: bool): From 4b57f6ae3d0e6703829012e8c804072e0a2a548b Mon Sep 17 00:00:00 2001 From: Fehmi Toumi Date: Mon, 1 Feb 2021 22:06:49 +0100 Subject: [PATCH 40/59] make _get_resource function to return None is no resource --- appstoreconnect/api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 1bdb590..a7f55f5 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -66,11 +66,17 @@ def _generate_token(self): def _get_resource(self, Resource, resource_id): url = "%s%s/%s" % (BASE_API, Resource.endpoint, resource_id) payload = self._api_call(url) - return Resource(payload.get('data', {}), self) + payloadData = payload.get('data', {}) + if not payloadData: + return None + return Resource(payloadData, self) def _get_related_resource(self, Resource, full_url): payload = self._api_call(full_url) - return Resource(payload.get('data', {}), self) + payloadData = payload.get('data', {}) + if not payloadData: + return None + return Resource(payloadData, self) def _create_resource(self, Resource, args): attributes = {} From 17b5bf021466101724ac7d5b1daf83261d1d630d Mon Sep 17 00:00:00 2001 From: dmytrolutsyk Date: Tue, 16 Feb 2021 17:53:37 +0100 Subject: [PATCH 41/59] add create and delete methode for screenshotSet --- appstoreconnect/api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 83f8852..4c9e3fb 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -496,6 +496,21 @@ def list_all_app_screenshots_for_an_app_screenshot_set(self, app_screen_shot_set url = BASE_API + "/v1/appScreenshotSets/" + app_screen_shot_set_id + "/appScreenshots" return self._get_resources(AppScreenshot, None, None, url) + def create_an_app_screenshot_set(self, screenshotDisplayType: str, appStoreVersionLocalization: AppStoreVersionLocalization): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_screenshot_set + :return: an iterator over AppScreenshotSet resources + """ + return self._create_resource(AppScreenshotSet, locals()) + + def delete_an_app_screenshot_set(self, appScreenshotSet: AppScreenshotSet): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_an_app_screenshot_set + :return: an iterator over AppScreenshotSet resources + """ + return self._delete_resource(appScreenshotSet) + + def modify_an_app_screenshot(self, app_screenshot: AppScreenshot, sourceFileChecksum: str, uploaded: bool): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_screenshot From fd43963b09bcd7eeae34ec3030aac10648e2fb5e Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Fri, 23 Apr 2021 17:30:06 +0200 Subject: [PATCH 42/59] update profile ressource --- appstoreconnect/resources.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 1bc6313..12b5359 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -301,7 +301,13 @@ class Device(Resource): class Profile(Resource): endpoint = '/v1/profiles' documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/profile/attributes' - + type = 'profiles' + attributes = ['name', 'platform', 'profileContent', 'uuid', 'createdDate', 'profileState', 'profileType', 'expirationDate'] + relationships = { + 'certificates': {'multiple': True}, + 'devices': {'multiple': True}, + 'bundleId': {'multiple': False}, + } # Reporting From 495c60b012b4aade3372b9a3d7e059ea791a9d8d Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Tue, 27 Apr 2021 12:40:54 +0200 Subject: [PATCH 43/59] update BundleId ressource comment in Ressouce Class relationships bullshit and add read_profile, get_profiles_with_budleId in api.py --- appstoreconnect/api.py | 25 ++++++++++++++++++++++++- appstoreconnect/resources.py | 12 ++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 83f8852..d783be1 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -34,7 +34,7 @@ def __init__(self, error_string, status_code): class Api: - def __init__(self, key_id, key_file, issuer_id, submit_stats=True): + def __init__(self, key_id, key_file, issuer_id, submit_stats=False): self._token = None self.token_gen_date = None self.exp = None @@ -165,6 +165,10 @@ def __init__(self, api, url): self.total_length = None self.payload = None + def __getitem__(self, item): + items = list(self) + return items[item] + def __iter__(self): return self @@ -259,6 +263,7 @@ def _api_call(self, url, method=HttpMethod.GET, post_data=None): return data.decode("utf-8") else: if not 200 <= r.status_code <= 299: + print(r.status_code) raise APIError("HTTP error [%d][%s]" % (r.status_code, r.content)) return r @@ -663,8 +668,25 @@ def list_profiles(self, filters=None, sort=None): :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_and_download_profiles :return: an iterator over Profile resources """ + #return self._get_resources(Profile, filters, sort, locals()) return self._get_resources(Profile, filters, sort) + def read_profile(self, profileId): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_and_download_profile_information + :return: an iterator over Profile resources + """ + return self._get_resource(Profile, profileId) + + def get_profiles_with_budleId(self, url): + """" + :reference: Dmytro's brain + :return: an iterator over profile resource + """ + payload = self._api_call(url, HttpMethod.GET) + return payload + #return Profile(payload.get('data'), {}) + def get_build_info(self, build_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_build_information @@ -757,6 +779,7 @@ def modify_app_info(self, app_information: AppInfo, primaryCategory: str = None, """ return self._modify_resource(app_information, locals()) + # Reporting def download_finance_reports(self, filters=None, split_response=False, save_to=None): # setup required filters if not provided diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 12b5359..afcb454 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -13,7 +13,8 @@ def __getattr__(self, item): if item in self._data.get('attributes', {}): return self._data.get('attributes', {})[item] if item in self._data.get('relationships', {}): - def callable(): + return self._data.get('relationships', {})[item] + '''def callable(): # Try to fetch relationship nonlocal item is_resources = item[-1] == 's' @@ -27,7 +28,7 @@ def callable(): return self._api._get_resources(item_cls, full_url=url) else: return self._api._get_related_resource(item_cls, full_url=url) - return callable + return callable''' raise AttributeError('%s have no attributes %s' % (self.type_name, item)) @@ -281,6 +282,13 @@ class UserInvitation(Resource): # Provisioning class BundleId(Resource): endpoint = '/v1/bundleIds' + type = 'bundleIds' + attributes = ['identifier', 'name', 'platform', 'seedId'] + relationships = { + 'profiles': {'multiple': True}, + 'bundleIdCapabilities': {'multiple': True}, + 'app': {'multiple': False}, + } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/bundleid/attributes' From 59e5dd56bd454be5f5fc2af83e9de8a0bb46b2c0 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Tue, 27 Apr 2021 16:29:10 +0200 Subject: [PATCH 44/59] clean up and reuse callable on relationships --- appstoreconnect/api.py | 11 ----------- appstoreconnect/resources.py | 5 ++--- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d783be1..20178dc 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -263,7 +263,6 @@ def _api_call(self, url, method=HttpMethod.GET, post_data=None): return data.decode("utf-8") else: if not 200 <= r.status_code <= 299: - print(r.status_code) raise APIError("HTTP error [%d][%s]" % (r.status_code, r.content)) return r @@ -668,7 +667,6 @@ def list_profiles(self, filters=None, sort=None): :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_and_download_profiles :return: an iterator over Profile resources """ - #return self._get_resources(Profile, filters, sort, locals()) return self._get_resources(Profile, filters, sort) def read_profile(self, profileId): @@ -678,15 +676,6 @@ def read_profile(self, profileId): """ return self._get_resource(Profile, profileId) - def get_profiles_with_budleId(self, url): - """" - :reference: Dmytro's brain - :return: an iterator over profile resource - """ - payload = self._api_call(url, HttpMethod.GET) - return payload - #return Profile(payload.get('data'), {}) - def get_build_info(self, build_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_build_information diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index afcb454..27a9895 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -13,8 +13,7 @@ def __getattr__(self, item): if item in self._data.get('attributes', {}): return self._data.get('attributes', {})[item] if item in self._data.get('relationships', {}): - return self._data.get('relationships', {})[item] - '''def callable(): + def callable(): # Try to fetch relationship nonlocal item is_resources = item[-1] == 's' @@ -28,7 +27,7 @@ def __getattr__(self, item): return self._api._get_resources(item_cls, full_url=url) else: return self._api._get_related_resource(item_cls, full_url=url) - return callable''' + return callable raise AttributeError('%s have no attributes %s' % (self.type_name, item)) From db9c2072469c039d6de74542dfc14214c0d0a119 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Tue, 13 Jul 2021 19:22:00 +0200 Subject: [PATCH 45/59] fix bad references --- appstoreconnect/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index a7f55f5..482e687 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -691,7 +691,7 @@ def create_an_app_store_version_phased_release(self, phased_release_state: str, def read_the_app_store_version_phased_release_information_of_an_app_store_version(self, resource_id): """ - :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_the_app_store_version_phased_release_information_of_an_app_store_version :return: an iterator over AppStoreVersionPhasedRelease resources """ url = f"https://api.appstoreconnect.apple.com/v1/appStoreVersions/{resource_id}/appStoreVersionPhasedRelease" @@ -699,14 +699,14 @@ def read_the_app_store_version_phased_release_information_of_an_app_store_versio def delete_an_app_store_version_phased_release(self, appStoreVersionPhasedRelease: AppStoreVersionPhasedRelease ): """ - :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_an_app_store_version_phased_release :return: an empty iterator over a list of AppStoreVersionPhasedRelease resources """ return self._delete_resource(appStoreVersionPhasedRelease) def modify_an_app_store_version_phased_release(self, appStoreVersionPhasedRelease: AppStoreVersionPhasedRelease, phasedReleaseState: str): """ - :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_phased_release + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version_phased_release :return: an iterator over a list of AppStoreVersionPhasedRelease resources """ return self._modify_resource(appStoreVersionPhasedRelease, locals()) From db3c67452866f9fdf0c60318640ede7a7262e448 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Fri, 16 Jul 2021 17:57:00 +0200 Subject: [PATCH 46/59] app create_an_app_store_version_submission() method --- appstoreconnect/api.py | 8 ++++++++ appstoreconnect/resources.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 466fdc1..cef6b8a 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -647,6 +647,14 @@ def read_beta_app_review_submission_information(self, beta_app_id: str): """ return self._get_resource(BetaAppReviewSubmission, beta_app_id) + def create_an_app_store_version_submission(self, appStoreVersion: AppStoreVersion): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_submission + :return: an AppStoreVersionSubmission resource + """ + return self._create_resource(AppStoreVersionSubmission, locals()) + + # Provisioning def list_bundle_ids(self, filters=None, sort=None): """ diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index e35baa1..9030b7d 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -158,6 +158,10 @@ class AppStoreVersionLocalizations(Resource): class AppStoreVersionSubmission(Resource): endpoint = '/v1/appStoreVersionSubmissions' type = "appStoreVersionSubmissions" + attributes = [] + relationships = { + 'appStoreVersion': {'multiple': False} + } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstoreversionsubmission' class IdfaDeclaration(Resource): From 98a5f8b61baf3c9c46826c2c8de5a3d0cd6aa80f Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Fri, 1 Oct 2021 18:15:03 +0200 Subject: [PATCH 47/59] implement AppStoreReviewDetail ressource and create, modify, read methods too --- appstoreconnect/api.py | 23 +++++++++++++++++++++++ appstoreconnect/resources.py | 5 +++++ 2 files changed, 28 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index cef6b8a..76d0792 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -654,6 +654,29 @@ def create_an_app_store_version_submission(self, appStoreVersion: AppStoreVersio """ return self._create_resource(AppStoreVersionSubmission, locals()) + def create_an_app_store_review_detail(self, appStoreVersion: AppStoreVersion, contactEmail: str = None, contactFirstName: str = None, contactLastName: str = None, contactPhone: str = None, demoAccountName: str = None, demoAccountPassword: str = None, demoAccountRequired: bool = None, notes: str = None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_review_detail + :return: an AppStoreReviewDetail resource + """ + return self._create_resource(AppStoreReviewDetail, locals()) + + def read_app_store_review_detail_information(self, app_store_version_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_store_review_detail_information + :param app_store_version_id: + :return: an AppStoreReviewDetail resource + """ + return self._get_resource(AppStoreReviewDetail, app_store_version_id) + + def modify_an_app_store_review_detail(self, appStoreReviewDetail: AppStoreReviewDetail, contactEmail: str = None, contactFirstName: str = None, contactLastName: str = None, contactPhone: str = None, demoAccountName: str = None, demoAccountPassword: str = None, demoAccountRequired: bool = None, notes: str = None ) -> AppStoreReviewDetail: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_review_detail + :return: a AppStoreReviewDetail resource + """ + attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword} + print(attributes) + return self._modify_resource(appStoreReviewDetail, attributes) # Provisioning def list_bundle_ids(self, filters=None, sort=None): diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 9030b7d..72b53e6 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -148,6 +148,11 @@ class Territory(Resource): class AppStoreReviewDetail(Resource): endpoint = '/v1/appStoreReviewDetails' type = "appStoreReviewDetails" + attributes = ['contactEmail', 'contactFirstName', 'contactLastName', 'contactPhone', 'demoAccountName', 'demoAccountPassword', 'demoAccountRequired', 'notes'] + relationships = { + 'appStoreVersion': {'multiple': False}, + 'appStoreReviewAttachments': {'multiple': True} + } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appstorereviewdetail' class AppStoreVersionLocalizations(Resource): From 89deb2d342e6970687ebe328de9fb7f64cdcf56b Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Mon, 4 Oct 2021 11:21:12 +0200 Subject: [PATCH 48/59] update login/pwd in review detail works --- appstoreconnect/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 76d0792..71f4177 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -654,7 +654,7 @@ def create_an_app_store_version_submission(self, appStoreVersion: AppStoreVersio """ return self._create_resource(AppStoreVersionSubmission, locals()) - def create_an_app_store_review_detail(self, appStoreVersion: AppStoreVersion, contactEmail: str = None, contactFirstName: str = None, contactLastName: str = None, contactPhone: str = None, demoAccountName: str = None, demoAccountPassword: str = None, demoAccountRequired: bool = None, notes: str = None): + def create_an_app_store_review_detail(self, appStoreVersion: AppStoreVersion, demoAccountName: str, demoAccountPassword: str , demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_review_detail :return: an AppStoreReviewDetail resource @@ -669,12 +669,12 @@ def read_app_store_review_detail_information(self, app_store_version_id): """ return self._get_resource(AppStoreReviewDetail, app_store_version_id) - def modify_an_app_store_review_detail(self, appStoreReviewDetail: AppStoreReviewDetail, contactEmail: str = None, contactFirstName: str = None, contactLastName: str = None, contactPhone: str = None, demoAccountName: str = None, demoAccountPassword: str = None, demoAccountRequired: bool = None, notes: str = None ) -> AppStoreReviewDetail: + def modify_an_app_store_review_detail(self, appStoreReviewDetail: AppStoreReviewDetail, demoAccountName: str, demoAccountPassword: str, demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str) -> AppStoreReviewDetail: """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_review_detail :return: a AppStoreReviewDetail resource """ - attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword} + attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword, 'demoAccountRequired':demoAccountRequired} print(attributes) return self._modify_resource(appStoreReviewDetail, attributes) From 739c87153bc8615f0fc5303ef5e447778f9ecc97 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Mon, 4 Oct 2021 14:10:36 +0200 Subject: [PATCH 49/59] update modify_an_app_store_review_detail --- appstoreconnect/api.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 71f4177..d28f653 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -654,7 +654,7 @@ def create_an_app_store_version_submission(self, appStoreVersion: AppStoreVersio """ return self._create_resource(AppStoreVersionSubmission, locals()) - def create_an_app_store_review_detail(self, appStoreVersion: AppStoreVersion, demoAccountName: str, demoAccountPassword: str , demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str): + def create_an_app_store_review_detail(self, appStoreVersion: AppStoreVersion, demoAccountName: str, demoAccountPassword: str, demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str = None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_review_detail :return: an AppStoreReviewDetail resource @@ -669,13 +669,12 @@ def read_app_store_review_detail_information(self, app_store_version_id): """ return self._get_resource(AppStoreReviewDetail, app_store_version_id) - def modify_an_app_store_review_detail(self, appStoreReviewDetail: AppStoreReviewDetail, demoAccountName: str, demoAccountPassword: str, demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str) -> AppStoreReviewDetail: + def modify_an_app_store_review_detail(self, appStoreReviewDetail: AppStoreReviewDetail, demoAccountName: str, demoAccountPassword: str, demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str = None) -> AppStoreReviewDetail: """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_review_detail :return: a AppStoreReviewDetail resource """ - attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword, 'demoAccountRequired':demoAccountRequired} - print(attributes) + attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword, 'demoAccountRequired':demoAccountRequired, 'contactFirstName':contactFirstName, 'contactLastName':contactLastName, 'contactEmail': contactEmail, 'contactPhone':contactPhone} return self._modify_resource(appStoreReviewDetail, attributes) # Provisioning From ffa1d772862cc7a5f8cc0e01d406c223db91a6e0 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Wed, 2 Feb 2022 16:41:16 +0100 Subject: [PATCH 50/59] first commit --- appstoreconnect/api.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d28f653..7a26f90 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -357,6 +357,16 @@ def list_beta_testers(self, filters=None, sort=None): """ return self._get_resources(BetaTester, filters, sort) + def list_all_beta_testers_in_a_beta_group(self, betaGroup, filters=None, sort=None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_beta_testers_in_a_beta_group + :return: an iterator over BetaTester resources + """ + full_url = BASE_API + "/v1/betaGroups/" + betaGroup.id + "/betaTesters" + return self._get_resources(BetaGroup, None, None, full_url) + #return self._api_call(BASE_API + "/v1/betaGroups/" + betaGroup.id + "/betaTesters", HttpMethod.GET) + #return self._get_resources(BetaGroup, filters, sort) + def read_beta_tester_information(self, beta_tester_id: str): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_tester_information @@ -364,6 +374,31 @@ def read_beta_tester_information(self, beta_tester_id: str): """ return self._get_resource(BetaTester, beta_tester_id) + def add_beta_testers_to_a_beta_group(self, betaGroup: BetaGroup, betaTesters: list): #betaTesters list of BetaTester + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/add_beta_testers_to_a_beta_group + :return: a BetaTester resource + """ + return self._create_resource(BetaGroup, locals()) + + def remove_beta_testers_from_a_beta_group(self, betaGroup: BetaGroup, betaTesters: list): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/remove_beta_testers_from_a_beta_group + :return: a BetaTester resource + """ + headers = {"Authorization": "Bearer %s" % self.token} + headers["Content-Type"] = "application/json" + url = BASE_API + "/v1/betaGroups/" + betaGroup.id + "/relationships/betaTesters" + post_data = { 'data': []} + for betaTester in betaTesters: + data = { 'id': betaTester.id, 'type': 'betaTesters'} + post_data["data"].append(data) + + return requests.delete(url=url, data = post_data, headers = headers) + #post_data = [{'data': [{ 'id': build_id, 'type': 'builds'}]}] + #return self._api_call(BASE_API + "/v1/betaGroups/" + betaGroup.id + "/relationships/betaTesters", HttpMethod.DELETE, post_data) + + def create_beta_group(self, app: App, name: str, publicLinkEnabled: bool = None, publicLinkLimit: int = None, publicLinkLimitEnabled: bool = None) -> BetaGroup: """ :reference:https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_group @@ -396,6 +431,10 @@ def read_beta_group_information(self, beta_group_ip): return self._get_resource(BetaGroup, beta_group_ip) def add_build_to_beta_group(self, beta_group_id, build_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/add_builds_to_a_beta_group + :return: an BetaGroup resource + """ post_data = {'data': [{ 'id': build_id, 'type': 'builds'}]} self._api_call(BASE_API + "/v1/betaGroups/" + beta_group_id + "/relationships/builds", HttpMethod.POST, post_data) From e3f9be01f1ba4989d083d92fac03cb155c3b5e9a Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Fri, 4 Feb 2022 15:51:00 +0100 Subject: [PATCH 51/59] send_an_invitation_to_a_beta_tester --- appstoreconnect/api.py | 14 ++++++++++---- appstoreconnect/resources.py | 9 +++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 7a26f90..9dfa3ce 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -114,6 +114,7 @@ def _create_resource(self, Resource, args): url = "%s%s" % (BASE_API, Resource.endpoint) if self._debug: print(post_data) + payload = self._api_call(url, HttpMethod.POST, post_data) return Resource(payload.get('data', {}), self) @@ -248,6 +249,7 @@ def _api_call(self, url, method=HttpMethod.GET, post_data=None): else: raise APIError("Unknown HTTP method") + content_type = r.headers['content-type'] if content_type in [ "application/json", "application/vnd.api+json" ]: @@ -381,6 +383,13 @@ def add_beta_testers_to_a_beta_group(self, betaGroup: BetaGroup, betaTesters: li """ return self._create_resource(BetaGroup, locals()) + def send_an_invitation_to_a_beta_tester(self, app: App, betaTester: BetaTester): #betaTesters list of BetaTester + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/send_an_invitation_to_a_beta_tester + :return: a BetaTester resource + """ + return self._create_resource(BetaTesterInvitation, locals()) + def remove_beta_testers_from_a_beta_group(self, betaGroup: BetaGroup, betaTesters: list): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/remove_beta_testers_from_a_beta_group @@ -393,10 +402,7 @@ def remove_beta_testers_from_a_beta_group(self, betaGroup: BetaGroup, betaTester for betaTester in betaTesters: data = { 'id': betaTester.id, 'type': 'betaTesters'} post_data["data"].append(data) - - return requests.delete(url=url, data = post_data, headers = headers) - #post_data = [{'data': [{ 'id': build_id, 'type': 'builds'}]}] - #return self._api_call(BASE_API + "/v1/betaGroups/" + betaGroup.id + "/relationships/betaTesters", HttpMethod.DELETE, post_data) + return requests.delete(url=url, data = json.dumps(post_data), headers = headers) def create_beta_group(self, app: App, name: str, publicLinkEnabled: bool = None, publicLinkLimit: int = None, publicLinkLimitEnabled: bool = None) -> BetaGroup: diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 72b53e6..50d68dd 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -60,6 +60,15 @@ class BetaTester(Resource): } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betatester' +class BetaTesterInvitation(Resource): + endpoint = '/v1/betaTesterInvitations' + type = 'betaTesterInvitations' + attributes = [] + relationships = { + 'app': {'multiple': False}, + 'betaTester': {'multiple': False} + } + documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/BetaTesterInvitation' class BetaGroup(Resource): endpoint = '/v1/betaGroups' From 9f74e2c60bdf632b20e668661f92f3cc769b609a Mon Sep 17 00:00:00 2001 From: Runner OMA Date: Fri, 4 Feb 2022 17:29:20 +0100 Subject: [PATCH 52/59] reduced token expiration so it does not fail cause of local machine misconfiguration --- appstoreconnect/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d28f653..68f83ee 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -59,7 +59,7 @@ def _generate_token(self): except IOError as e: key = self.key_file self.token_gen_date = datetime.now() - exp = int(time.mktime((self.token_gen_date + timedelta(minutes=20)).timetuple())) + exp = int(time.mktime((self.token_gen_date + timedelta(minutes=15)).timetuple())) return jwt.encode({'iss': self.issuer_id, 'exp': exp, 'aud': 'appstoreconnect-v1'}, key, headers={'kid': self.key_id, 'typ': 'JWT'}, algorithm=ALGORITHM).decode('ascii') From 363ead1c758fbbc303e17ac401f7835bcafe9908 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Thu, 21 Apr 2022 14:54:49 +0200 Subject: [PATCH 53/59] add read_app_store_version_localization_information and read_app_store_version_localization_information --- appstoreconnect/api.py | 16 +++++++++++++++- appstoreconnect/resources.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 7ef20ec..d0416c5 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -791,10 +791,24 @@ def list_app_store_version_localizations(self, app_store_version): def modify_app_store_version_localization(self, app_store_version_localization: AppStoreVersionLocalization, description: str, keywords: str, marketingUrl: str, promotionalText: str, supportUrl: str, whatsNew: str ): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_an_app_store_version_localization - :return: an iterator over AppInfoLocalization resources + :return: an iterator over AppStoreVersionLocalization resources """ return self._modify_resource(app_store_version_localization, locals()) + def read_app_store_version_localization_information(self, app_store_version_localization_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_store_version_localization_information + :return: an iterator over AppStoreVersionLocalization resources + """ + return self._get_resource(AppStoreVersionLocalization, app_store_version_localization_id) + + def read_app_info_localization_information(self, app_info_localization_id): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_info_localization_information + :return: an iterator over AppInfoLocalization resources + """ + return self._get_resource(AppInfoLocalization, app_info_localization_id) + # appStoreInfo localization def list_app_store_info_localizations(self, app_information): """ diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index 50d68dd..c4d1643 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -191,7 +191,7 @@ class RoutingAppCoverage(Resource): class AppInfoLocalization(Resource): endpoint = '/v1/appInfoLocalizations' type = 'appInfoLocalizations' - attributes = ['locale', 'name', 'privacyPolicyText', 'privacyPolicyUrl', 'subtitle'] + attributes = ['locale', 'name', 'privacyPolicyText', 'privacyPolicyUrl', 'subtitle', 'privacyChoicesUrl'] relationships = { 'appInfo': {'multiple': False}, } From 6d5fb63067a972221844dd6a8a077aa7be622f0b Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Fri, 3 Jun 2022 12:19:20 +0200 Subject: [PATCH 54/59] add modify_a_beta_app_review_detail methode --- appstoreconnect/api.py | 8 ++++++++ appstoreconnect/resources.py | 1 + 2 files changed, 9 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index d0416c5..521de2e 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -692,6 +692,14 @@ def read_beta_app_review_submission_information(self, beta_app_id: str): """ return self._get_resource(BetaAppReviewSubmission, beta_app_id) + def modify_a_beta_app_review_detail(self, beta_app_review_detail: BetaAppReviewDetail, demoAccountName: str, demoAccountPassword: str, demoAccountRequired: bool, contactFirstName: str, contactLastName: str, contactEmail: str, contactPhone: str, notes: str = None) -> AppStoreReviewDetail: + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_app_review_detail + :return: an BetaAppReviewDetail resource + """ + attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword, 'demoAccountRequired':demoAccountRequired, 'contactFirstName':contactFirstName, 'contactLastName':contactLastName, 'contactEmail': contactEmail, 'contactPhone':contactPhone, 'notes': notes} + return self._modify_resource(BetaAppReviewDetail, attributes) + def create_an_app_store_version_submission(self, appStoreVersion: AppStoreVersion): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/create_an_app_store_version_submission diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index c4d1643..d2f1469 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -280,6 +280,7 @@ class BetaBuildLocalization(Resource): class BetaAppReviewDetail(Resource): endpoint = '/v1/betaAppReviewDetails' type = 'betaAppReviewDetails' + attributes = ['contactEmail', 'contactFirstName', 'contactLastName', 'contactPhone', 'demoAccountName', 'demoAccountPassword', 'demoAccountRequired', 'notes'] documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppReviewDetail/attributes' From 46e3fb40569d56a32774af4a935d4dacc390fb8e Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Wed, 15 Jun 2022 13:31:34 +0200 Subject: [PATCH 55/59] betaAppReviewDetail ok need to do beta app localization --- appstoreconnect/api.py | 2 +- appstoreconnect/resources.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 521de2e..1490aa9 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -698,7 +698,7 @@ def modify_a_beta_app_review_detail(self, beta_app_review_detail: BetaAppReviewD :return: an BetaAppReviewDetail resource """ attributes = {'demoAccountName':demoAccountName, 'demoAccountPassword':demoAccountPassword, 'demoAccountRequired':demoAccountRequired, 'contactFirstName':contactFirstName, 'contactLastName':contactLastName, 'contactEmail': contactEmail, 'contactPhone':contactPhone, 'notes': notes} - return self._modify_resource(BetaAppReviewDetail, attributes) + return self._modify_resource(beta_app_review_detail, attributes) def create_an_app_store_version_submission(self, appStoreVersion: AppStoreVersion): """ diff --git a/appstoreconnect/resources.py b/appstoreconnect/resources.py index d2f1469..b440fe9 100644 --- a/appstoreconnect/resources.py +++ b/appstoreconnect/resources.py @@ -258,11 +258,28 @@ class Build(Resource): endpoint = '/v1/builds' type = 'builds' attributes = ['expired', 'iconAssetToken', 'minOsVersion', 'processingState', 'version', 'usesNonExemptEncryption', 'uploadedDate', 'expirationDate'] + relationships = { + 'app': {'multiple': False}, + 'appEncryptionDeclaration': {'multiple': False}, + 'individualTesters': {'multiple': True}, + 'preReleaseVersion': {'multiple': False}, + 'betaBuildLocalizations': {'multiple': True}, + 'buildBetaDetail': {'multiple': False}, + 'betaAppReviewSubmission': {'multiple': False}, + 'appStoreVersion': {'multiple': False}, + 'icons': {'multiple': True}, + 'buildBundles': {'multiple': True}, + 'betaGroups': {'multiple': True}, + } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/build/attributes' class BuildBetaDetail(Resource): endpoint = '/v1/buildBetaDetails' + attributes = ['autoNotifyEnabled', 'externalBuildState', 'internalBuildState'] + relationships = { + 'build': {'multiple': False}, + } type = 'buildBetaDetails' documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/buildBetaDetail/attributes' @@ -281,6 +298,9 @@ class BetaAppReviewDetail(Resource): endpoint = '/v1/betaAppReviewDetails' type = 'betaAppReviewDetails' attributes = ['contactEmail', 'contactFirstName', 'contactLastName', 'contactPhone', 'demoAccountName', 'demoAccountPassword', 'demoAccountRequired', 'notes'] + relationships = { + 'app': {'multiple': False}, + } documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppReviewDetail/attributes' From d5401b765405db8bf3eaae735a10e3cc0010c9a1 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Wed, 15 Jun 2022 17:45:46 +0200 Subject: [PATCH 56/59] beta app info localization blocked --- appstoreconnect/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 1490aa9..52274d8 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -496,6 +496,13 @@ def create_beta_app_localization(self, app: App, locale: str, description: str = """ return self._create_resource(BetaAppLocalization, locals()) + def modify_a_beta_app_localization(self, betaAppLocalization: BetaAppLocalization, locale: str, description: str = None, feedbackEmail: str = None, marketingUrl: str = None, privacyPolicyUrl: str = None, tvOsPrivacyPolicy: str = None): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_app_localization + :return: an BetaAppLocalization resource + """ + return self._create_resource(betaAppLocalization, locals()) + def list_app_encryption_declarations(self, filters=None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_app_encryption_declarations From b7fa52063cddfdc20e208d27c2d187d5a8e74089 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Thu, 16 Jun 2022 11:20:31 +0200 Subject: [PATCH 57/59] ready to merge --- appstoreconnect/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 52274d8..a23f256 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -496,12 +496,12 @@ def create_beta_app_localization(self, app: App, locale: str, description: str = """ return self._create_resource(BetaAppLocalization, locals()) - def modify_a_beta_app_localization(self, betaAppLocalization: BetaAppLocalization, locale: str, description: str = None, feedbackEmail: str = None, marketingUrl: str = None, privacyPolicyUrl: str = None, tvOsPrivacyPolicy: str = None): + def modify_a_beta_app_localization(self, betaAppLocalization: BetaAppLocalization, description: str = None, feedbackEmail: str = None, marketingUrl: str = None, privacyPolicyUrl: str = None, tvOsPrivacyPolicy: str = None): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_app_localization :return: an BetaAppLocalization resource """ - return self._create_resource(betaAppLocalization, locals()) + return self._modify_resource(betaAppLocalization, locals()) def list_app_encryption_declarations(self, filters=None): """ From 3f4bf8d45e9f3dade9c73ebd2b8faa157b820da2 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Thu, 16 Jun 2022 17:38:59 +0200 Subject: [PATCH 58/59] fix modify method --- appstoreconnect/api.py | 62 ++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index 2d3973a..df4a4ab 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -11,8 +11,8 @@ from typing import List from enum import Enum, auto -from .resources import * -from .__version__ import __version__ as version +#from .resources import * +#from .__version__ import __version__ as version from resources import * from __version__ import __version__ as version @@ -151,49 +151,45 @@ def _create_resource(self, Resource, args): return Resource(payload.get('data', {}), self) - def _modify_resource(self, resource, args): + def _modify_resource(self, Resource, args): attributes = {} - - for attribute in resource.attributes: + for attribute in Resource.attributes: if attribute in args and args[attribute] is not None: - if type(args[attribute]) == list: - value = list(map(lambda e: e.name if isinstance(e, Enum) else e, args[attribute])) - elif isinstance(args[attribute], Enum): - value = args[attribute].name - else: - value = args[attribute] - attributes[attribute] = value - - relationships = {} - if hasattr(resource, 'relationships'): - for relationship in resource.relationships: - if relationship in args and args[relationship] is not None: - relationships[relationship] = {} - relationships[relationship]['data'] = [] - for relationship_object in args[relationship]: - relationships[relationship]['data'].append( - { - 'id': relationship_object.id, - 'type': relationship_object.type - } - ) + attributes[attribute] = args[attribute] + relationships_dict = {} + for relation in Resource.relationships.keys(): + if relation in args and args[relation] is not None: + relationships_dict[relation] = {} + if Resource.relationships[relation].get('multiple', False): + relationships_dict[relation]['data'] = [] + relationship_objects = args[relation] + if type(relationship_objects) is not list: + relationship_objects = [relationship_objects] + for relationship_object in relationship_objects: + relationships_dict[relation]['data'].append({ + 'id': relationship_object.id, + 'type': relationship_object.type + }) + else: + relationships_dict[relation]['data'] = { + 'id': args[relation].id, + 'type': args[relation].type + } post_data = { 'data': { 'attributes': attributes, - 'id': resource.id, - 'type': resource.type + 'relationships': relationships_dict, + 'id': Resource.id, + 'type': Resource.type } } - if len(relationships): - post_data['data']['relationships'] = relationships - - url = "%s%s/%s" % (BASE_API, resource.endpoint, resource.id) + url = "%s%s/%s" % (BASE_API, Resource.endpoint, Resource.id) if self._debug: print(post_data) payload = self._api_call(url, HttpMethod.PATCH, post_data) - return type(resource)(payload.get('data', {}), self) + return type(Resource)(payload.get('data', {}), self) def _delete_resource(self, resource: Resource): url = "%s%s/%s" % (BASE_API, resource.endpoint, resource.id) From f2a96cc6bc1a9218bd6a5966a2ccbc65521a0499 Mon Sep 17 00:00:00 2001 From: Dmytro Lutsyk Date: Fri, 17 Jun 2022 10:14:58 +0200 Subject: [PATCH 59/59] fix missings things --- appstoreconnect/api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/appstoreconnect/api.py b/appstoreconnect/api.py index df4a4ab..8ad44de 100644 --- a/appstoreconnect/api.py +++ b/appstoreconnect/api.py @@ -472,6 +472,10 @@ def modify_beta_group(self, betaGroup: BetaGroup, name: str = None, publicLinkEn return self._modify_resource(betaGroup, locals()) def delete_beta_group(self, betaGroup: BetaGroup): + """ + :reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_a_beta_group + :return: a BetaGroup resources + """ return self._delete_resource(betaGroup) def list_beta_groups(self, filters=None, sort=None): @@ -870,14 +874,14 @@ def read_app_store_version_localization_information(self, app_store_version_loca """ return self._get_resource(AppStoreVersionLocalization, app_store_version_localization_id) + # appStoreInfo localization def read_app_info_localization_information(self, app_info_localization_id): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_info_localization_information :return: an iterator over AppInfoLocalization resources """ return self._get_resource(AppInfoLocalization, app_info_localization_id) - - # appStoreInfo localization + def list_app_store_info_localizations(self, app_information): """ :reference: https://developer.apple.com/documentation/appstoreconnectapi/list_all_app_info_localizations_for_an_app_info