Full Code of Ponytech/appstoreconnectapi for AI

master 640d3dc02a55 cached
12 files
42.2 KB
11.2k tokens
89 symbols
1 requests
Download .txt
Repository: Ponytech/appstoreconnectapi
Branch: master
Commit: 640d3dc02a55
Files: 12
Total size: 42.2 KB

Directory structure:
gitextract_d3qjx83c/

├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── appstoreconnect/
│   ├── __init__.py
│   ├── __version__.py
│   ├── api.py
│   └── resources.py
├── example.py
├── requirements.txt
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Python bytecode:
*.py[co]

# Packaging files:
*.egg*
dist/*
MANIFEST

# Sphinx docs:
build

# SQLite3 database files:
*.db

# Logs:
*.log

# IDEs
.project
.pydevproject
.settings
.idea

# Linux Editors
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
.elc
auto-save-list
tramp
.\#*
*.swp
*.swo

# Mac
.DS_Store
._*

# Windows
Thumbs.db
Desktop.ini
git 

# venv
.venv/

# Apple AuthKeys
AuthKey_*

# VSCode
.vscode

# Files used or generated during dev/testing
*.csv
*.p8
test_api_sales.py
test_api_finance.py


================================================
FILE: CHANGELOG.md
================================================
## 0.10.1
Bugfixes:
- Relax cryptography dependencies (@conformist-mw)
- Do not assume presence of content-type header (@jaysoffian)
- Fix a possible RecursionError 

## 0.10.0

Features:
- Add a timeout parameter (in seconds) for requests
- Add proxy support

Bugfixes:
- Avoid a RecursionError when accessing an unknown attribute on some resources

## 0.9.1

Bugfixes:
- Relax required dependencies in setup.py 
- Fix APIError exception handling
- Add relationships attribute to the Build resource

## 0.9.0

Features:
- New endpoint: `modify_user_account`
- Support getting more related resources, like `user.visibleApps()`

Bugfixes:
- Pin dependencies versions in setup.py

## 0.8.4

Features:
-  Expose the HTTP status code when App Store Connect API response raises APIError (@GClunies)

## 0.8.3

Bugfixes:
- Fix invite_user method (@AricWu)

## 0.8.2

Features:
 - New `split_response` argument in `download_finance_reports()` function that splits the response into 2 objects. Defualt value is `split_response=False`. This also
 allows the 2 responses to be saved to separate files using a syntax like `save_to=['test1.csv', 'test2.csv']`. (@GClunies)

## 0.8.1

Bugfixes:
 - Add default versions and subtypes in download_sales_and_trends_reports

## 0.8.0

Features:
 - New endpoints:
   - delete_beta_tester
   - read_beta_tester_information
   - modify_beta_group
   - delete_beta_group
   - read_beta_group_information
   - read_beta_app_localization_information
   - create_beta_app_localization
   - modify_registered_device
   - read_beta_app_review_submission_information
 - Collect anonymous usage statistics

Breaking changes API:
 - new parameters for create_beta_tester
 - new parameters for create_beta_group
 - new parameters for submit_app_for_beta_review
 - register_device renamed to register_new_device 

## 0.7.0

Features:
 - New endpoint: register_device (@BalestraPatrick)

## 0.6.0

Features:
 - New endpoints: invite_user and read_user_invitation_information (@BalestraPatrick)

Bugfixes:
 - Fixes create_beta_tester endpoint URL (@BalestraPatrick)

## 0.5.1

Bugfixes:
 - Fixes token re-generation (@gsaraceno)

## 0.5.0

Features:
 -  Handle listing all resources in the provisioning section (devices thanks to @EricG-Personal)

## 0.4.1

Features:
 - Allow to query resources sorted
 - Allow passing key as a string value (@kpotehin)

Bugfixes:
 - Fixed sort param in reports (@kpotehin)

## 0.4.0

Features:
 - Handle fetching related resources (@WangYi)

Bugfixes:
 - When paging resources, fix missing resource in the first page (@WangYi)

## 0.3.0

Features:
  - Complete API rewrite, "list" methods return an iterator over resources, "get" method returns a resource 
  - Handles all GET endpoints (except the new "Provisioning" section)
  - Handle pagination
  - Handle downloading Finance and Sales reports

## 0.2.1

Bugfixes:

  - Cryptography dependency is required

## 0.2.0

Features:

  - Added more functions (@fehmitoumi)

## 0.1.0

Features:

  - Initial Release


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Ponytech

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include README.md LICENSE requirements.txt CHANGELOG.md

================================================
FILE: README.md
================================================
App Store Connect Api
====

This is a Python wrapper around the **Apple App Store Api** : https://developer.apple.com/documentation/appstoreconnectapi

So far, it handles token generation / expiration, methods for listing resources and downloading reports. 

Installation
------------

[![Version](http://img.shields.io/pypi/v/appstoreconnect.svg?style=flat)](https://pypi.org/project/appstoreconnect/)

The project is published on PyPI, install with: 

    pip install appstoreconnect

Usage
-----

Please follow instructions on [Apple documentation](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api) on how to generate an API key.

With your *key ID*, *key file* (you can either pass the path to the file or the content of it as a string) and *issuer ID* create a new API instance:

```python
from appstoreconnect import Api, UserRole
api = Api(key_id, path_to_key_file, issuer_id)

# use a proxy
api = Api(key_id, path_to_key_file, issuer_id, proxy='http://1.2.3.4:3128')

# set a timeout (in seconds) for requests
api = Api(key_id, path_to_key_file, issuer_id, timeout=42)
```

Here are a few examples of API usage. For a complete list of available methods please see [api.py](https://github.com/Ponytech/appstoreconnectapi/blob/master/appstoreconnect/api.py#L148).

```python
# list all apps
apps = api.list_apps()
for app in apps:
    print(app.name, app.sku)

# sort resources
apps = api.list_apps(sort='name')

# filter apps
apps = api.list_apps(filters={'sku': 'DINORUSH', 'name': 'Dino Rush'})
print("%d apps found" % len(apps))

# read app information
app = api.read_app_information('1308363336')
print(app.name, app.sku, app.bundleId)

# get a related resource
for group in app.betaGroups():
    print(group.name)

# list bundle ids
for bundle_id in api.list_bundle_ids():
    print(bundle_id.identifier)

# list certificates
for certificate in api.list_certificates():
    print(certificate.name)

# modify a user
user = api.list_users(filters={'username': 'finance@nemoidstudio.com'})[0]
api.modify_user_account(user, roles=[UserRole.FINANCE, UserRole.ACCESS_TO_REPORTS])
    
# download sales report
api.download_sales_and_trends_reports(
    filters={'vendorNumber': '123456789', 'frequency': 'WEEKLY', 'reportDate': '2019-06-09'}, save_to='report.csv')

# download finance report
api.download_finance_reports(filters={'vendorNumber': '123456789', 'reportDate': '2019-06'}, save_to='finance.csv')
```

Define a timeout (in seconds) after which an exception is raised if no response is received. 

```python
api = Api(key_id, path_to_key_file, issuer_id, timeout=30)
api.list_apps()

APIError: Read timeout after 30 seconds
```


Please note this is a work in progress, API is subject to change between versions.

Anonymous data collection
-------------------------

Starting with version 0.8.0 this library anonymously collects its usage to help better improve its development. 
What we collect is:

- a SHA1 hash of the issuer_id
- the OS and Python version used
- which enpoints had been used

You can review the [source code](https://github.com/Ponytech/appstoreconnectapi/blob/b73d4314e2a9f9098f3287f57fff687563e70b28/appstoreconnect/api.py#L238)

If you feel uncomfortable with it you can completely opt-out by initliazing the API with:

```python
api = Api(key_id, path_to_key_file, issuer_id, submit_stats=False)
```

The is also an [open issue](https://github.com/Ponytech/appstoreconnectapi/issues/18) about this topic where we would love to here your feedback and best practices.


Development
-----------

Project development happens on [Github](https://github.com/Ponytech/appstoreconnectapi) 


TODO
----

* [ ] Support App Store Connect API 1.2
* [ ] Support the include parameter
* [X] handle POST, DELETE and PATCH requests
* [X] sales report
* [X] handle related resources
* [X] allow to sort resources
* [ ] proper API documentation
* [ ] add tests


Credits
-------

This project is developed by [Ponytech](https://ponytech.net)


================================================
FILE: appstoreconnect/__init__.py
================================================
from .api import Api, UserRole


================================================
FILE: appstoreconnect/__version__.py
================================================
VERSION = (0, 10, 1)

__version__ = '.'.join(map(str, VERSION))


================================================
FILE: appstoreconnect/api.py
================================================
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 typing import List
from enum import Enum, auto

from .resources import *
from .__version__ import __version__ as version

ALGORITHM = 'ES256'
BASE_API = "https://api.appstoreconnect.apple.com"


class UserRole(Enum):
	ADMIN = auto()
	FINANCE = auto()
	TECHNICAL = auto()
	SALES = auto()
	MARKETING = auto()
	DEVELOPER = auto()
	ACCOUNT_HOLDER = auto()
	READ_ONLY = auto()
	APP_MANAGER = auto()
	ACCESS_TO_REPORTS = auto()
	CUSTOMER_SUPPORT = auto()


class HttpMethod(Enum):
	GET = 1
	POST = 2
	PATCH = 3
	DELETE = 4


class APIError(Exception):
	def __init__(self, error_string, status_code=None):
		try:
			self.status_code = int(status_code)
		except (ValueError, TypeError):
			pass
		super().__init__(error_string)


class Api:

	def __init__(self, key_id, key_file, issuer_id, submit_stats=True, timeout=None, proxy=None):
		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.timeout = timeout
		self.proxy = proxy
		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):
		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 _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_resource_from_payload_data(self, payload):
		try:
			resource_type = resources[payload.get('type')]
		except KeyError:
			raise APIError("Unsupported resource type %s" % payload.get('type'))

		return resource_type(payload, self)

	def get_related_resource(self, full_url):
		payload = self._api_call(full_url)
		data = payload.get('data')
		if data is None:
			return None
		elif type(data) == dict:
			return self._get_resource_from_payload_data(data)

	def get_related_resources(self, full_url):
		payload = self._api_call(full_url)
		data = payload.get('data', [])
		for resource in data:
			yield self._get_resource_from_payload_data(resource)

	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:
				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
							}
						)

		post_data = {
			'data': {
				'attributes': attributes,
				'id': resource.id,
				'type': resource.type
			}
		}
		if len(relationships):
			post_data['data']['relationships'] = relationships

		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 __getitem__(self, item):
				items = list(self)
				return items[item]

			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}
		if self._debug:
			print("%s %s" % (method.value, 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

		try:
			if method == HttpMethod.GET:
				proxies = {'https': self.proxy} if self.proxy else None
				r = requests.get(url, headers=headers, timeout=self.timeout, proxies=proxies)
			elif method == HttpMethod.POST:
				headers["Content-Type"] = "application/json"
				r = requests.post(url=url, headers=headers, data=json.dumps(post_data), timeout=self.timeout)
			elif method == HttpMethod.PATCH:
				headers["Content-Type"] = "application/json"
				r = requests.patch(url=url, headers=headers, data=json.dumps(post_data), timeout=self.timeout)
			elif method == HttpMethod.DELETE:
				r = requests.delete(url=url, headers=headers, timeout=self.timeout)
			else:
				raise APIError("Unknown HTTP method")
		except requests.exceptions.Timeout:
			raise APIError(f"Read timeout after {self.timeout} seconds")

		if self._debug:
			print(r.status_code)

		content_type = r.headers.get('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))

	@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._token

	# Users and Roles
	def modify_user_account(
			self,
			user: User,
			allAppsVisible: bool = None,
			provisioningAllowed: bool = None,
			roles: List[UserRole] = None,
			visibleApps: List[App] = None,
	):
		"""
		:reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_user_account
		:return: a User resource
		"""
		return self._modify_resource(user, locals())

	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'}]}
		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)

	# 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'}}
		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)

	# 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


================================================
FILE: appstoreconnect/resources.py
================================================
import inspect
from abc import ABC, abstractmethod
import sys


class Resource(ABC):
	relationships = {}

	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.relationships:
			def getter():
				# Try to fetch relationship
				nonlocal item
				url = self._data.get('relationships', {})[item]['links']['related']
				if self.relationships[item]['multiple']:
					return self._api.get_related_resources(full_url=url)
				else:
					return self._api.get_related_resource(full_url=url)
			return getter

		raise AttributeError('%s has 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 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 = 'preReleaseVersions'
	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'
	documentation = 'https://developer.apple.com/documentation/appstoreconnectapi/build/attributes'
	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},
	}


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'
	type = 'users'
	attributes = ['allAppsVisible', 'provisioningAllowed', 'roles']
	relationships = {
		'visibleApps': {'multiple': True},
	}
	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']
	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'


# create an index of Resources by type
resources = {}
for name, obj in inspect.getmembers(sys.modules[__name__]):
	if inspect.isclass(obj) and issubclass(obj, Resource) and hasattr(obj, 'type') and obj != Resource:
		resources[getattr(obj, 'type')] = obj


================================================
FILE: example.py
================================================
#!/usr/bin/env python

import sys
from appstoreconnect import Api, UserRole


if __name__ == "__main__":
	key_id = sys.argv[1]
	key_file = sys.argv[2]
	issuer_id = sys.argv[3]
	api = Api(key_id, key_file, issuer_id)

	# list all apps
	apps = api.list_apps()
	for app in apps:
		print(app.name, app.sku)

	# filter apps
	apps = api.list_apps(filters={'sku': 'DINORUSH', 'name': 'Dino Rush'})
	print("%d apps found" % len(apps))

	# modify a user
	user = api.list_users(filters={'username': 'finance@nemoidstudio.com'})[0]
	api.modify_user_account(user, roles=[UserRole.FINANCE, UserRole.APP_MANAGER, UserRole.ACCESS_TO_REPORTS])

	# download sales report
	api.download_sales_and_trends_reports(
		filters={'vendorNumber': '123456789', 'frequency': 'WEEKLY', 'reportDate': '2019-06-09'}, save_to='report.csv')

	# download finance report
	api.download_finance_reports(filters={'vendorNumber': '123456789', 'reportDate': '2019-06'}, save_to='finance.csv')


================================================
FILE: requirements.txt
================================================
.


================================================
FILE: setup.py
================================================
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import io
import os

from setuptools import find_packages, setup

NAME = 'appstoreconnect'
DESCRIPTION = 'A Python wrapper around Apple App Store Api'
URL = 'https://ponytech.net/projects/app-store-connect'
EMAIL = 'contact@ponytech.net'
AUTHOR = 'Ponytech'
REQUIRES_PYTHON = '>=3.6.0'
VERSION = None

REQUIRED = [
    'requests>=2.20.1,==2.*',
    'PyJWT>=1.6.4,==1.*',
    'cryptography>=2.6.1',
]

EXTRAS = {
}

here = os.path.abspath(os.path.dirname(__file__))

try:
    with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
        long_description = '\n' + f.read()
except FileNotFoundError:
    long_description = DESCRIPTION

about = {}
if not VERSION:
    with open(os.path.join(here, NAME, '__version__.py')) as f:
        exec(f.read(), about)
else:
    about['__version__'] = VERSION

setup(
    name=NAME,
    version=about['__version__'],
    description=DESCRIPTION,
    long_description=long_description,
    long_description_content_type='text/markdown',
    author=AUTHOR,
    author_email=EMAIL,
    python_requires=REQUIRES_PYTHON,
    url=URL,
    packages=find_packages(exclude=('tests',)),
    install_requires=REQUIRED,
    extras_require=EXTRAS,
    include_package_data=True,
    license='MIT',
    classifiers=[
        'License :: OSI Approved :: MIT License',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
    ],
)
Download .txt
gitextract_d3qjx83c/

├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── appstoreconnect/
│   ├── __init__.py
│   ├── __version__.py
│   ├── api.py
│   └── resources.py
├── example.py
├── requirements.txt
└── setup.py
Download .txt
SYMBOL INDEX (89 symbols across 2 files)

FILE: appstoreconnect/api.py
  class UserRole (line 21) | class UserRole(Enum):
  class HttpMethod (line 35) | class HttpMethod(Enum):
  class APIError (line 42) | class APIError(Exception):
    method __init__ (line 43) | def __init__(self, error_string, status_code=None):
  class Api (line 51) | class Api:
    method __init__ (line 53) | def __init__(self, key_id, key_file, issuer_id, submit_stats=True, tim...
    method __del__ (line 70) | def __del__(self):
    method _generate_token (line 74) | def _generate_token(self):
    method _get_resource (line 84) | def _get_resource(self, Resource, resource_id):
    method _get_resource_from_payload_data (line 89) | def _get_resource_from_payload_data(self, payload):
    method get_related_resource (line 97) | def get_related_resource(self, full_url):
    method get_related_resources (line 105) | def get_related_resources(self, full_url):
    method _create_resource (line 111) | def _create_resource(self, Resource, args):
    method _modify_resource (line 151) | def _modify_resource(self, resource, args):
    method _delete_resource (line 195) | def _delete_resource(self, resource: Resource):
    method _get_resources (line 199) | def _get_resources(self, Resource, filters=None, sort=None, full_url=N...
    method _build_query_parameters (line 249) | def _build_query_parameters(self, url, filters, sort = None):
    method _api_call (line 260) | def _api_call(self, url, method=HttpMethod.GET, post_data=None):
    method _submit_stats (line 316) | def _submit_stats(self, event_type):
    method token (line 336) | def token(self):
    method modify_user_account (line 344) | def modify_user_account(
    method list_users (line 358) | def list_users(self, filters=None, sort=None):
    method list_invited_users (line 365) | def list_invited_users(self, filters=None, sort=None):
    method invite_user (line 373) | def invite_user(self, all_apps_visible, email, first_name, last_name, ...
    method read_user_invitation_information (line 386) | def read_user_invitation_information(self, user_invitation_id: str):
    method create_beta_tester (line 394) | def create_beta_tester(self, email: str, firstName: str = None, lastNa...
    method delete_beta_tester (line 401) | def delete_beta_tester(self, betaTester: BetaTester) -> None:
    method list_beta_testers (line 408) | def list_beta_testers(self, filters=None, sort=None):
    method read_beta_tester_information (line 415) | def read_beta_tester_information(self, beta_tester_id: str):
    method create_beta_group (line 422) | def create_beta_group(self, app: App, name: str, publicLinkEnabled: bo...
    method modify_beta_group (line 429) | def modify_beta_group(self, betaGroup: BetaGroup, name: str = None, pu...
    method delete_beta_group (line 436) | def delete_beta_group(self, betaGroup: BetaGroup):
    method list_beta_groups (line 439) | def list_beta_groups(self, filters=None, sort=None):
    method read_beta_group_information (line 446) | def read_beta_group_information(self, beta_group_ip):
    method add_build_to_beta_group (line 453) | def add_build_to_beta_group(self, beta_group_id, build_id):
    method read_app_information (line 459) | def read_app_information(self, app_ip):
    method list_apps (line 467) | def list_apps(self, filters=None, sort=None):
    method list_prerelease_versions (line 474) | def list_prerelease_versions(self, filters=None, sort=None):
    method list_beta_app_localizations (line 481) | def list_beta_app_localizations(self, filters=None):
    method read_beta_app_localization_information (line 488) | def read_beta_app_localization_information(self, beta_app_id: str):
    method create_beta_app_localization (line 495) | def create_beta_app_localization(self, app: App, locale: str, descript...
    method list_app_encryption_declarations (line 502) | def list_app_encryption_declarations(self, filters=None):
    method list_beta_license_agreements (line 509) | def list_beta_license_agreements(self, filters=None):
    method list_builds (line 517) | def list_builds(self, filters=None, sort=None):
    method build_processing_state (line 525) | def build_processing_state(self, app_id, version):
    method set_uses_non_encryption_exemption_setting (line 529) | def set_uses_non_encryption_exemption_setting(self, build_id, uses_non...
    method list_build_beta_details (line 534) | def list_build_beta_details(self, filters=None):
    method create_beta_build_localization (line 541) | def create_beta_build_localization(self, build: Build, locale: str, wh...
    method modify_beta_build_localization (line 548) | def modify_beta_build_localization(self, beta_build_localization: Beta...
    method list_beta_build_localizations (line 555) | def list_beta_build_localizations(self, filters=None):
    method list_beta_app_review_details (line 562) | def list_beta_app_review_details(self, filters=None):
    method submit_app_for_beta_review (line 569) | def submit_app_for_beta_review(self, build: Build) -> BetaAppReviewSub...
    method list_beta_app_review_submissions (line 577) | def list_beta_app_review_submissions(self, filters=None):
    method read_beta_app_review_submission_information (line 584) | def read_beta_app_review_submission_information(self, beta_app_id: str):
    method list_bundle_ids (line 592) | def list_bundle_ids(self, filters=None, sort=None):
    method list_certificates (line 599) | def list_certificates(self, filters=None, sort=None):
    method list_devices (line 606) | def list_devices(self, filters=None, sort=None):
    method register_new_device (line 613) | def register_new_device(self, name: str, platform: str, udid: str) -> ...
    method modify_registered_device (line 620) | def modify_registered_device(self, device: Device, name: str = None, s...
    method list_profiles (line 627) | def list_profiles(self, filters=None, sort=None):
    method download_finance_reports (line 635) | def download_finance_reports(self, filters=None, split_response=False,...
    method download_sales_and_trends_reports (line 668) | def download_sales_and_trends_reports(self, filters=None, save_to=None):

FILE: appstoreconnect/resources.py
  class Resource (line 6) | class Resource(ABC):
    method __init__ (line 9) | def __init__(self, data, api):
    method __getattr__ (line 13) | def __getattr__(self, item):
    method __repr__ (line 31) | def __repr__(self):
    method __dir__ (line 34) | def __dir__(self):
    method type_name (line 38) | def type_name(self):
    method endpoint (line 43) | def endpoint(self):
  class BetaTester (line 49) | class BetaTester(Resource):
  class BetaGroup (line 61) | class BetaGroup(Resource):
  class App (line 75) | class App(Resource):
  class PreReleaseVersion (line 91) | class PreReleaseVersion(Resource):
  class BetaAppLocalization (line 98) | class BetaAppLocalization(Resource):
  class AppEncryptionDeclaration (line 108) | class AppEncryptionDeclaration(Resource):
  class BetaLicenseAgreement (line 114) | class BetaLicenseAgreement(Resource):
  class Build (line 121) | class Build(Resource):
  class BuildBetaDetail (line 138) | class BuildBetaDetail(Resource):
  class BetaBuildLocalization (line 144) | class BetaBuildLocalization(Resource):
  class BetaAppReviewDetail (line 154) | class BetaAppReviewDetail(Resource):
  class BetaAppReviewSubmission (line 160) | class BetaAppReviewSubmission(Resource):
  class User (line 172) | class User(Resource):
  class UserInvitation (line 182) | class UserInvitation(Resource):
  class BundleId (line 188) | class BundleId(Resource):
  class Certificate (line 193) | class Certificate(Resource):
  class Device (line 198) | class Device(Resource):
  class Profile (line 205) | class Profile(Resource):
  class FinanceReport (line 212) | class FinanceReport(Resource):
  class SalesReport (line 217) | class SalesReport(Resource):
Condensed preview — 12 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
  {
    "path": ".gitignore",
    "chars": 508,
    "preview": "# Python bytecode:\n*.py[co]\n\n# Packaging files:\n*.egg*\ndist/*\nMANIFEST\n\n# Sphinx docs:\nbuild\n\n# SQLite3 database files:\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 3014,
    "preview": "## 0.10.1\nBugfixes:\n- Relax cryptography dependencies (@conformist-mw)\n- Do not assume presence of content-type header ("
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2018 Ponytech\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "MANIFEST.in",
    "chars": 55,
    "preview": "include README.md LICENSE requirements.txt CHANGELOG.md"
  },
  {
    "path": "README.md",
    "chars": 4020,
    "preview": "App Store Connect Api\n====\n\nThis is a Python wrapper around the **Apple App Store Api** : https://developer.apple.com/do"
  },
  {
    "path": "appstoreconnect/__init__.py",
    "chars": 31,
    "preview": "from .api import Api, UserRole\n"
  },
  {
    "path": "appstoreconnect/__version__.py",
    "chars": 64,
    "preview": "VERSION = (0, 10, 1)\n\n__version__ = '.'.join(map(str, VERSION))\n"
  },
  {
    "path": "appstoreconnect/api.py",
    "chars": 24828,
    "preview": "import requests\nimport jwt\nimport gzip\nimport platform\nimport hashlib\nfrom collections import defaultdict\nfrom pathlib i"
  },
  {
    "path": "appstoreconnect/resources.py",
    "chars": 6996,
    "preview": "import inspect\nfrom abc import ABC, abstractmethod\nimport sys\n\n\nclass Resource(ABC):\n\trelationships = {}\n\n\tdef __init__("
  },
  {
    "path": "example.py",
    "chars": 953,
    "preview": "#!/usr/bin/env python\n\nimport sys\nfrom appstoreconnect import Api, UserRole\n\n\nif __name__ == \"__main__\":\n\tkey_id = sys.a"
  },
  {
    "path": "requirements.txt",
    "chars": 2,
    "preview": ".\n"
  },
  {
    "path": "setup.py",
    "chars": 1646,
    "preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport io\nimport os\n\nfrom setuptools import find_packages, setup\n\nNAME = "
  }
]

About this extraction

This page contains the full source code of the Ponytech/appstoreconnectapi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 12 files (42.2 KB), approximately 11.2k tokens, and a symbol index with 89 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!