[
  {
    "path": ".gitignore",
    "content": "# Python bytecode:\n*.py[co]\n\n# Packaging files:\n*.egg*\ndist/*\nMANIFEST\n\n# Sphinx docs:\nbuild\n\n# SQLite3 database files:\n*.db\n\n# Logs:\n*.log\n\n# IDEs\n.project\n.pydevproject\n.settings\n.idea\n\n# Linux Editors\n*~\n\\#*\\#\n/.emacs.desktop\n/.emacs.desktop.lock\n.elc\nauto-save-list\ntramp\n.\\#*\n*.swp\n*.swo\n\n# Mac\n.DS_Store\n._*\n\n# Windows\nThumbs.db\nDesktop.ini\ngit \n\n# venv\n.venv/\n\n# Apple AuthKeys\nAuthKey_*\n\n# VSCode\n.vscode\n\n# Files used or generated during dev/testing\n*.csv\n*.p8\ntest_api_sales.py\ntest_api_finance.py\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 0.10.1\nBugfixes:\n- Relax cryptography dependencies (@conformist-mw)\n- Do not assume presence of content-type header (@jaysoffian)\n- Fix a possible RecursionError \n\n## 0.10.0\n\nFeatures:\n- Add a timeout parameter (in seconds) for requests\n- Add proxy support\n\nBugfixes:\n- Avoid a RecursionError when accessing an unknown attribute on some resources\n\n## 0.9.1\n\nBugfixes:\n- Relax required dependencies in setup.py \n- Fix APIError exception handling\n- Add relationships attribute to the Build resource\n\n## 0.9.0\n\nFeatures:\n- New endpoint: `modify_user_account`\n- Support getting more related resources, like `user.visibleApps()`\n\nBugfixes:\n- Pin dependencies versions in setup.py\n\n## 0.8.4\n\nFeatures:\n-  Expose the HTTP status code when App Store Connect API response raises APIError (@GClunies)\n\n## 0.8.3\n\nBugfixes:\n- Fix invite_user method (@AricWu)\n\n## 0.8.2\n\nFeatures:\n - New `split_response` argument in `download_finance_reports()` function that splits the response into 2 objects. Defualt value is `split_response=False`. This also\n allows the 2 responses to be saved to separate files using a syntax like `save_to=['test1.csv', 'test2.csv']`. (@GClunies)\n\n## 0.8.1\n\nBugfixes:\n - Add default versions and subtypes in download_sales_and_trends_reports\n\n## 0.8.0\n\nFeatures:\n - New endpoints:\n   - delete_beta_tester\n   - read_beta_tester_information\n   - modify_beta_group\n   - delete_beta_group\n   - read_beta_group_information\n   - read_beta_app_localization_information\n   - create_beta_app_localization\n   - modify_registered_device\n   - read_beta_app_review_submission_information\n - Collect anonymous usage statistics\n\nBreaking changes API:\n - new parameters for create_beta_tester\n - new parameters for create_beta_group\n - new parameters for submit_app_for_beta_review\n - register_device renamed to register_new_device \n\n## 0.7.0\n\nFeatures:\n - New endpoint: register_device (@BalestraPatrick)\n\n## 0.6.0\n\nFeatures:\n - New endpoints: invite_user and read_user_invitation_information (@BalestraPatrick)\n\nBugfixes:\n - Fixes create_beta_tester endpoint URL (@BalestraPatrick)\n\n## 0.5.1\n\nBugfixes:\n - Fixes token re-generation (@gsaraceno)\n\n## 0.5.0\n\nFeatures:\n -  Handle listing all resources in the provisioning section (devices thanks to @EricG-Personal)\n\n## 0.4.1\n\nFeatures:\n - Allow to query resources sorted\n - Allow passing key as a string value (@kpotehin)\n\nBugfixes:\n - Fixed sort param in reports (@kpotehin)\n\n## 0.4.0\n\nFeatures:\n - Handle fetching related resources (@WangYi)\n\nBugfixes:\n - When paging resources, fix missing resource in the first page (@WangYi)\n\n## 0.3.0\n\nFeatures:\n  - Complete API rewrite, \"list\" methods return an iterator over resources, \"get\" method returns a resource \n  - Handles all GET endpoints (except the new \"Provisioning\" section)\n  - Handle pagination\n  - Handle downloading Finance and Sales reports\n\n## 0.2.1\n\nBugfixes:\n\n  - Cryptography dependency is required\n\n## 0.2.0\n\nFeatures:\n\n  - Added more functions (@fehmitoumi)\n\n## 0.1.0\n\nFeatures:\n\n  - Initial Release\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Ponytech\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md LICENSE requirements.txt CHANGELOG.md"
  },
  {
    "path": "README.md",
    "content": "App Store Connect Api\n====\n\nThis is a Python wrapper around the **Apple App Store Api** : https://developer.apple.com/documentation/appstoreconnectapi\n\nSo far, it handles token generation / expiration, methods for listing resources and downloading reports. \n\nInstallation\n------------\n\n[![Version](http://img.shields.io/pypi/v/appstoreconnect.svg?style=flat)](https://pypi.org/project/appstoreconnect/)\n\nThe project is published on PyPI, install with: \n\n    pip install appstoreconnect\n\nUsage\n-----\n\nPlease 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.\n\nWith 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:\n\n```python\nfrom appstoreconnect import Api, UserRole\napi = Api(key_id, path_to_key_file, issuer_id)\n\n# use a proxy\napi = Api(key_id, path_to_key_file, issuer_id, proxy='http://1.2.3.4:3128')\n\n# set a timeout (in seconds) for requests\napi = Api(key_id, path_to_key_file, issuer_id, timeout=42)\n```\n\nHere 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).\n\n```python\n# list all apps\napps = api.list_apps()\nfor app in apps:\n    print(app.name, app.sku)\n\n# sort resources\napps = api.list_apps(sort='name')\n\n# filter apps\napps = api.list_apps(filters={'sku': 'DINORUSH', 'name': 'Dino Rush'})\nprint(\"%d apps found\" % len(apps))\n\n# read app information\napp = api.read_app_information('1308363336')\nprint(app.name, app.sku, app.bundleId)\n\n# get a related resource\nfor group in app.betaGroups():\n    print(group.name)\n\n# list bundle ids\nfor bundle_id in api.list_bundle_ids():\n    print(bundle_id.identifier)\n\n# list certificates\nfor certificate in api.list_certificates():\n    print(certificate.name)\n\n# modify a user\nuser = api.list_users(filters={'username': 'finance@nemoidstudio.com'})[0]\napi.modify_user_account(user, roles=[UserRole.FINANCE, UserRole.ACCESS_TO_REPORTS])\n    \n# download sales report\napi.download_sales_and_trends_reports(\n    filters={'vendorNumber': '123456789', 'frequency': 'WEEKLY', 'reportDate': '2019-06-09'}, save_to='report.csv')\n\n# download finance report\napi.download_finance_reports(filters={'vendorNumber': '123456789', 'reportDate': '2019-06'}, save_to='finance.csv')\n```\n\nDefine a timeout (in seconds) after which an exception is raised if no response is received. \n\n```python\napi = Api(key_id, path_to_key_file, issuer_id, timeout=30)\napi.list_apps()\n\nAPIError: Read timeout after 30 seconds\n```\n\n\nPlease note this is a work in progress, API is subject to change between versions.\n\nAnonymous data collection\n-------------------------\n\nStarting with version 0.8.0 this library anonymously collects its usage to help better improve its development. \nWhat we collect is:\n\n- a SHA1 hash of the issuer_id\n- the OS and Python version used\n- which enpoints had been used\n\nYou can review the [source code](https://github.com/Ponytech/appstoreconnectapi/blob/b73d4314e2a9f9098f3287f57fff687563e70b28/appstoreconnect/api.py#L238)\n\nIf you feel uncomfortable with it you can completely opt-out by initliazing the API with:\n\n```python\napi = Api(key_id, path_to_key_file, issuer_id, submit_stats=False)\n```\n\nThe 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.\n\n\nDevelopment\n-----------\n\nProject development happens on [Github](https://github.com/Ponytech/appstoreconnectapi) \n\n\nTODO\n----\n\n* [ ] Support App Store Connect API 1.2\n* [ ] Support the include parameter\n* [X] handle POST, DELETE and PATCH requests\n* [X] sales report\n* [X] handle related resources\n* [X] allow to sort resources\n* [ ] proper API documentation\n* [ ] add tests\n\n\nCredits\n-------\n\nThis project is developed by [Ponytech](https://ponytech.net)\n"
  },
  {
    "path": "appstoreconnect/__init__.py",
    "content": "from .api import Api, UserRole\n"
  },
  {
    "path": "appstoreconnect/__version__.py",
    "content": "VERSION = (0, 10, 1)\n\n__version__ = '.'.join(map(str, VERSION))\n"
  },
  {
    "path": "appstoreconnect/api.py",
    "content": "import requests\nimport jwt\nimport gzip\nimport platform\nimport hashlib\nfrom collections import defaultdict\nfrom pathlib import Path\nfrom datetime import datetime, timedelta\nimport time\nimport json\nfrom typing import List\nfrom enum import Enum, auto\n\nfrom .resources import *\nfrom .__version__ import __version__ as version\n\nALGORITHM = 'ES256'\nBASE_API = \"https://api.appstoreconnect.apple.com\"\n\n\nclass UserRole(Enum):\n\tADMIN = auto()\n\tFINANCE = auto()\n\tTECHNICAL = auto()\n\tSALES = auto()\n\tMARKETING = auto()\n\tDEVELOPER = auto()\n\tACCOUNT_HOLDER = auto()\n\tREAD_ONLY = auto()\n\tAPP_MANAGER = auto()\n\tACCESS_TO_REPORTS = auto()\n\tCUSTOMER_SUPPORT = auto()\n\n\nclass HttpMethod(Enum):\n\tGET = 1\n\tPOST = 2\n\tPATCH = 3\n\tDELETE = 4\n\n\nclass APIError(Exception):\n\tdef __init__(self, error_string, status_code=None):\n\t\ttry:\n\t\t\tself.status_code = int(status_code)\n\t\texcept (ValueError, TypeError):\n\t\t\tpass\n\t\tsuper().__init__(error_string)\n\n\nclass Api:\n\n\tdef __init__(self, key_id, key_file, issuer_id, submit_stats=True, timeout=None, proxy=None):\n\t\tself._token = None\n\t\tself.token_gen_date = None\n\t\tself.exp = None\n\t\tself.key_id = key_id\n\t\tself.key_file = key_file\n\t\tself.issuer_id = issuer_id\n\t\tself.submit_stats = submit_stats\n\t\tself.timeout = timeout\n\t\tself.proxy = proxy\n\t\tself._call_stats = defaultdict(int)\n\t\tif self.submit_stats:\n\t\t\tself._submit_stats(\"session_start\")\n\n\t\tself._debug = False\n\t\ttoken = self.token  # generate first token\n\n\tdef __del__(self):\n\t\tif self.submit_stats:\n\t\t\tself._submit_stats(\"session_end\")\n\n\tdef _generate_token(self):\n\t\ttry:\n\t\t\tkey = open(self.key_file, 'r').read()\n\t\texcept IOError as e:\n\t\t\tkey = self.key_file\n\t\tself.token_gen_date = datetime.now()\n\t\texp = int(time.mktime((self.token_gen_date + timedelta(minutes=20)).timetuple()))\n\t\treturn jwt.encode({'iss': self.issuer_id, 'exp': exp, 'aud': 'appstoreconnect-v1'}, key,\n\t\t                   headers={'kid': self.key_id, 'typ': 'JWT'}, algorithm=ALGORITHM).decode('ascii')\n\n\tdef _get_resource(self, Resource, resource_id):\n\t\turl = \"%s%s/%s\" % (BASE_API, Resource.endpoint, resource_id)\n\t\tpayload = self._api_call(url)\n\t\treturn Resource(payload.get('data', {}), self)\n\n\tdef _get_resource_from_payload_data(self, payload):\n\t\ttry:\n\t\t\tresource_type = resources[payload.get('type')]\n\t\texcept KeyError:\n\t\t\traise APIError(\"Unsupported resource type %s\" % payload.get('type'))\n\n\t\treturn resource_type(payload, self)\n\n\tdef get_related_resource(self, full_url):\n\t\tpayload = self._api_call(full_url)\n\t\tdata = payload.get('data')\n\t\tif data is None:\n\t\t\treturn None\n\t\telif type(data) == dict:\n\t\t\treturn self._get_resource_from_payload_data(data)\n\n\tdef get_related_resources(self, full_url):\n\t\tpayload = self._api_call(full_url)\n\t\tdata = payload.get('data', [])\n\t\tfor resource in data:\n\t\t\tyield self._get_resource_from_payload_data(resource)\n\n\tdef _create_resource(self, Resource, args):\n\t\tattributes = {}\n\t\tfor attribute in Resource.attributes:\n\t\t\tif attribute in args and args[attribute] is not None:\n\t\t\t\tattributes[attribute] = args[attribute]\n\n\t\trelationships_dict = {}\n\t\tfor relation in Resource.relationships.keys():\n\t\t\tif relation in args and args[relation] is not None:\n\t\t\t\trelationships_dict[relation] = {}\n\t\t\t\tif Resource.relationships[relation].get('multiple', False):\n\t\t\t\t\trelationships_dict[relation]['data'] = []\n\t\t\t\t\trelationship_objects = args[relation]\n\t\t\t\t\tif type(relationship_objects) is not list:\n\t\t\t\t\t\trelationship_objects = [relationship_objects]\n\t\t\t\t\tfor relationship_object in relationship_objects:\n\t\t\t\t\t\trelationships_dict[relation]['data'].append({\n\t\t\t\t\t\t\t'id': relationship_object.id,\n\t\t\t\t\t\t\t'type': relationship_object.type\n\t\t\t\t\t\t})\n\t\t\t\telse:\n\t\t\t\t\trelationships_dict[relation]['data'] = {\n\t\t\t\t\t\t\t'id': args[relation].id,\n\t\t\t\t\t\t\t'type': args[relation].type\n\t\t\t\t\t\t}\n\n\t\tpost_data = {\n\t\t\t'data': {\n\t\t\t\t'attributes': attributes,\n\t\t\t\t'relationships': relationships_dict,\n\t\t\t\t'type': Resource.type\n\t\t\t}\n\t\t}\n\t\turl = \"%s%s\" % (BASE_API, Resource.endpoint)\n\t\tif self._debug:\n\t\t\tprint(post_data)\n\t\tpayload = self._api_call(url, HttpMethod.POST, post_data)\n\n\t\treturn Resource(payload.get('data', {}), self)\n\n\tdef _modify_resource(self, resource, args):\n\t\tattributes = {}\n\n\t\tfor attribute in resource.attributes:\n\t\t\tif attribute in args and args[attribute] is not None:\n\t\t\t\tif type(args[attribute]) == list:\n\t\t\t\t\tvalue = list(map(lambda e: e.name if isinstance(e, Enum) else e, args[attribute]))\n\t\t\t\telif isinstance(args[attribute], Enum):\n\t\t\t\t\tvalue = args[attribute].name\n\t\t\t\telse:\n\t\t\t\t\tvalue = args[attribute]\n\t\t\t\tattributes[attribute] = value\n\n\t\trelationships = {}\n\t\tif hasattr(resource, 'relationships'):\n\t\t\tfor relationship in resource.relationships:\n\t\t\t\tif relationship in args and args[relationship] is not None:\n\t\t\t\t\trelationships[relationship] = {}\n\t\t\t\t\trelationships[relationship]['data'] = []\n\t\t\t\t\tfor relationship_object in args[relationship]:\n\t\t\t\t\t\trelationships[relationship]['data'].append(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t'id': relationship_object.id,\n\t\t\t\t\t\t\t\t'type': relationship_object.type\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t)\n\n\t\tpost_data = {\n\t\t\t'data': {\n\t\t\t\t'attributes': attributes,\n\t\t\t\t'id': resource.id,\n\t\t\t\t'type': resource.type\n\t\t\t}\n\t\t}\n\t\tif len(relationships):\n\t\t\tpost_data['data']['relationships'] = relationships\n\n\t\turl = \"%s%s/%s\" % (BASE_API, resource.endpoint, resource.id)\n\t\tif self._debug:\n\t\t\tprint(post_data)\n\t\tpayload = self._api_call(url, HttpMethod.PATCH, post_data)\n\n\t\treturn type(resource)(payload.get('data', {}), self)\n\n\tdef _delete_resource(self, resource: Resource):\n\t\turl = \"%s%s/%s\" % (BASE_API, resource.endpoint, resource.id)\n\t\tself._api_call(url, HttpMethod.DELETE)\n\n\tdef _get_resources(self, Resource, filters=None, sort=None, full_url=None):\n\t\tclass IterResource:\n\t\t\tdef __init__(self, api, url):\n\t\t\t\tself.api = api\n\t\t\t\tself.url = url\n\t\t\t\tself.index = 0\n\t\t\t\tself.total_length = None\n\t\t\t\tself.payload = None\n\n\t\t\tdef __getitem__(self, item):\n\t\t\t\titems = list(self)\n\t\t\t\treturn items[item]\n\n\t\t\tdef __iter__(self):\n\t\t\t\treturn self\n\n\t\t\tdef __repr__(self):\n\t\t\t\treturn \"Iterator over %s resource\" % Resource.__name__\n\n\t\t\tdef __len__(self):\n\t\t\t\tif not self.payload:\n\t\t\t\t\tself.fetch_page()\n\t\t\t\treturn self.total_length\n\n\t\t\tdef __next__(self):\n\t\t\t\tif not self.payload:\n\t\t\t\t\tself.fetch_page()\n\t\t\t\tif self.index < len(self.payload.get('data', [])):\n\t\t\t\t\tdata = self.payload.get('data', [])[self.index]\n\t\t\t\t\tself.index += 1\n\t\t\t\t\treturn Resource(data, self.api)\n\t\t\t\telse:\n\t\t\t\t\tself.url = self.payload.get('links', {}).get('next', None)\n\t\t\t\t\tself.index = 0\n\t\t\t\t\tif self.url:\n\t\t\t\t\t\tself.fetch_page()\n\t\t\t\t\t\tif self.index < len(self.payload.get('data', [])):\n\t\t\t\t\t\t\tdata = self.payload.get('data', [])[self.index]\n\t\t\t\t\t\t\tself.index += 1\n\t\t\t\t\t\t\treturn Resource(data, self.api)\n\t\t\t\t\traise StopIteration()\n\n\t\t\tdef fetch_page(self):\n\t\t\t\tself.payload = self.api._api_call(self.url)\n\t\t\t\tself.total_length = self.payload.get('meta', {}).get('paging', {}).get('total', 0)\n\n\t\turl = full_url if full_url else \"%s%s\" % (BASE_API, Resource.endpoint)\n\t\turl = self._build_query_parameters(url, filters, sort)\n\t\treturn IterResource(self, url)\n\n\tdef _build_query_parameters(self, url, filters, sort = None):\n\t\tseparator = '?'\n\t\tif type(filters) is dict:\n\t\t\tfor index, (filter_name, filter_value) in enumerate(filters.items()):\n\t\t\t\tfilter_name = \"filter[%s]\" % filter_name\n\t\t\t\turl = \"%s%s%s=%s\" % (url, separator, filter_name, filter_value)\n\t\t\t\tseparator = '&'\n\t\tif type(sort) is str:\n\t\t\turl = \"%s%ssort=%s\" % (url, separator, sort)\n\t\treturn url\n\n\tdef _api_call(self, url, method=HttpMethod.GET, post_data=None):\n\t\theaders = {\"Authorization\": \"Bearer %s\" % self.token}\n\t\tif self._debug:\n\t\t\tprint(\"%s %s\" % (method.value, url))\n\n\t\tif self._submit_stats:\n\t\t\tendpoint = url.replace(BASE_API, '')\n\t\t\tif method in (HttpMethod.PATCH, HttpMethod.DELETE):  # remove last bit of endpoint which is a resource id\n\t\t\t\tendpoint = \"/\".join(endpoint.split('/')[:-1])\n\t\t\trequest = \"%s %s\" % (method.name, endpoint)\n\t\t\tself._call_stats[request] += 1\n\n\t\ttry:\n\t\t\tif method == HttpMethod.GET:\n\t\t\t\tproxies = {'https': self.proxy} if self.proxy else None\n\t\t\t\tr = requests.get(url, headers=headers, timeout=self.timeout, proxies=proxies)\n\t\t\telif method == HttpMethod.POST:\n\t\t\t\theaders[\"Content-Type\"] = \"application/json\"\n\t\t\t\tr = requests.post(url=url, headers=headers, data=json.dumps(post_data), timeout=self.timeout)\n\t\t\telif method == HttpMethod.PATCH:\n\t\t\t\theaders[\"Content-Type\"] = \"application/json\"\n\t\t\t\tr = requests.patch(url=url, headers=headers, data=json.dumps(post_data), timeout=self.timeout)\n\t\t\telif method == HttpMethod.DELETE:\n\t\t\t\tr = requests.delete(url=url, headers=headers, timeout=self.timeout)\n\t\t\telse:\n\t\t\t\traise APIError(\"Unknown HTTP method\")\n\t\texcept requests.exceptions.Timeout:\n\t\t\traise APIError(f\"Read timeout after {self.timeout} seconds\")\n\n\t\tif self._debug:\n\t\t\tprint(r.status_code)\n\n\t\tcontent_type = r.headers.get('content-type')\n\n\t\tif content_type in [\"application/json\", \"application/vnd.api+json\"]:\n\t\t\tpayload = r.json()\n\t\t\tif 'errors' in payload:\n\t\t\t\traise APIError(\n\t\t\t\t\tpayload.get('errors', [])[0].get('detail', 'Unknown error'),\n\t\t\t\t \tpayload.get('errors', [])[0].get('status', None)\n\t\t\t\t)\n\t\t\treturn payload\n\t\telif content_type == 'application/a-gzip':\n\t\t\t# TODO implement stream decompress\n\t\t\tdata_gz = b\"\"\n\t\t\tfor chunk in r.iter_content(1024 * 1024):\n\t\t\t\tif chunk:\n\t\t\t\t\tdata_gz = data_gz + chunk\n\n\t\t\tdata = gzip.decompress(data_gz)\n\t\t\treturn data.decode(\"utf-8\")\n\t\telse:\n\t\t\tif not 200 <= r.status_code <= 299:\n\t\t\t\traise APIError(\"HTTP error [%d][%s]\" % (r.status_code, r.content))\n\t\t\treturn r\n\n\tdef _submit_stats(self, event_type):\n\t\t\"\"\"\n\t\tthis submits anonymous usage statistics to help us better understand how this library is used\n\t\tyou can opt-out by initializing the client with submit_stats=False\n\t\t\"\"\"\n\t\tpayload = {\n\t\t\t'project': 'appstoreconnectapi',\n\t\t\t'version': version,\n\t\t\t'type': event_type,\n\t\t\t'parameters': {\n\t\t\t\t'python_version': platform.python_version(),\n\t\t\t\t'platform': platform.platform(),\n\t\t\t\t'issuer_id_hash': hashlib.sha1(self.issuer_id.encode()).hexdigest(),  # send anonymized hash\n\t\t\t}\n\t\t}\n\t\tif event_type == 'session_end':\n\t\t\tpayload['parameters']['endpoints'] = self._call_stats\n\t\trequests.post('https://stats.ponytech.net/new-event', json.dumps(payload))\n\n\t@property\n\tdef token(self):\n\t\t# generate a new token every 15 minutes\n\t\tif (self._token is None) or (self.token_gen_date + timedelta(minutes=15) < datetime.now()):\n\t\t\tself._token = self._generate_token()\n\n\t\treturn self._token\n\n\t# Users and Roles\n\tdef modify_user_account(\n\t\t\tself,\n\t\t\tuser: User,\n\t\t\tallAppsVisible: bool = None,\n\t\t\tprovisioningAllowed: bool = None,\n\t\t\troles: List[UserRole] = None,\n\t\t\tvisibleApps: List[App] = None,\n\t):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_user_account\n\t\t:return: a User resource\n\t\t\"\"\"\n\t\treturn self._modify_resource(user, locals())\n\n\tdef list_users(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_users\n\t\t:return: an iterator over User resources\n\t\t\"\"\"\n\t\treturn self._get_resources(User, filters, sort)\n\n\tdef list_invited_users(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_invited_users\n\t\t:return: an iterator over UserInvitation resources\n\t\t\"\"\"\n\t\treturn self._get_resources(UserInvitation, filters, sort)\n\n\t# TODO: implement POST requests using Resource\n\tdef invite_user(self, all_apps_visible, email, first_name, last_name, provisioning_allowed, roles, visible_apps=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/invite_a_user\n\t\t:return: a UserInvitation resource\n\t\t\"\"\"\n\t\tpost_data = {'data': {'attributes': {'allAppsVisible': all_apps_visible, 'email': email, 'firstName': first_name, 'lastName': last_name, 'provisioningAllowed': provisioning_allowed, 'roles': roles}, 'type': 'userInvitations'}}\n\t\tif visible_apps is not None:\n\t\t\tvisible_apps_relationship = list(map(lambda a: {'id': a, 'type': 'apps'}, visible_apps))\n\t\t\tvisible_apps_data = {'visibleApps': {'data': visible_apps_relationship}}\n\t\t\tpost_data['data']['relationships'] = visible_apps_data\n\t\tpayload = self._api_call(BASE_API + \"/v1/userInvitations\", HttpMethod.POST, post_data)\n\t\treturn UserInvitation(payload.get('data'), {})\n\n\tdef read_user_invitation_information(self, user_invitation_id: str):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/read_user_invitation_information\n\t\t:return: a UserInvitation resource\n\t\t\"\"\"\n\t\treturn self._get_resource(UserInvitation, user_invitation_id)\n\n\t# Beta Testers and Groups\n\tdef create_beta_tester(self, email: str, firstName: str = None, lastName: str = None, betaGroups: BetaGroup = None, builds: Build = None) -> BetaTester:\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_tester\n\t\t:return: an BetaTester resource\n\t\t\"\"\"\n\t\treturn self._create_resource(BetaTester, locals())\n\n\tdef delete_beta_tester(self, betaTester: BetaTester) -> None:\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/delete_a_beta_tester\n\t\t:return: None\n\t\t\"\"\"\n\t\treturn self._delete_resource(betaTester)\n\n\tdef list_beta_testers(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_testers\n\t\t:return: an iterator over BetaTester resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaTester, filters, sort)\n\n\tdef read_beta_tester_information(self, beta_tester_id: str):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_tester_information\n\t\t:return: a BetaTester resource\n\t\t\"\"\"\n\t\treturn self._get_resource(BetaTester, beta_tester_id)\n\n\tdef create_beta_group(self, app: App, name: str, publicLinkEnabled: bool = None, publicLinkLimit: int = None, publicLinkLimitEnabled: bool = None) -> BetaGroup:\n\t\t\"\"\"\n\t\t:reference:https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_group\n\t\t:return: a BetaGroup resource\n\t\t\"\"\"\n\t\treturn self._create_resource(BetaGroup, locals())\n\n\tdef modify_beta_group(self, betaGroup: BetaGroup, name: str = None, publicLinkEnabled: bool = None, publicLinkLimit: int = None, publicLinkLimitEnabled: bool = None) -> BetaGroup:\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_group\n\t\t:return: a BetaGroup resource\n\t\t\"\"\"\n\t\treturn self._modify_resource(betaGroup, locals())\n\n\tdef delete_beta_group(self, betaGroup: BetaGroup):\n\t\treturn self._delete_resource(betaGroup)\n\n\tdef list_beta_groups(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_groups\n\t\t:return: an iterator over BetaGroup resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaGroup, filters, sort)\n\n\tdef read_beta_group_information(self, beta_group_ip):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_group_information\n\t\t:return: an BetaGroup resource\n\t\t\"\"\"\n\t\treturn self._get_resource(BetaGroup, beta_group_ip)\n\n\tdef add_build_to_beta_group(self, beta_group_id, build_id):\n\t\tpost_data = {'data': [{ 'id': build_id, 'type': 'builds'}]}\n\t\tpayload = self._api_call(BASE_API + \"/v1/betaGroups/\" + beta_group_id + \"/relationships/builds\", HttpMethod.POST, post_data)\n\t\treturn BetaGroup(payload.get('data'), {})\n\n\t# App Resources\n\tdef read_app_information(self, app_ip):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/read_app_information\n\t\t:param app_ip:\n\t\t:return: an App resource\n\t\t\"\"\"\n\t\treturn self._get_resource(App, app_ip)\n\n\tdef list_apps(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_apps\n\t\t:return: an iterator over App resources\n\t\t\"\"\"\n\t\treturn self._get_resources(App, filters, sort)\n\n\tdef list_prerelease_versions(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_prerelease_versions\n\t\t:return: an iterator over PreReleaseVersion resources\n\t\t\"\"\"\n\t\treturn self._get_resources(PreReleaseVersion, filters, sort)\n\n\tdef list_beta_app_localizations(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_app_localizations\n\t\t:return: an iterator over BetaAppLocalization resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaAppLocalization, filters)\n\n\tdef read_beta_app_localization_information(self, beta_app_id: str):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_app_localization_information\n\t\t:return: an BetaAppLocalization resource\n\t\t\"\"\"\n\t\treturn self._get_resource(BetaAppLocalization, beta_app_id)\n\n\tdef create_beta_app_localization(self, app: App, locale: str, description: str = None, feedbackEmail: str = None, marketingUrl: str = None, privacyPolicyUrl: str = None, tvOsPrivacyPolicy: str = None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_app_localization\n\t\t:return: an BetaAppLocalization resource\n\t\t\"\"\"\n\t\treturn self._create_resource(BetaAppLocalization, locals())\n\n\tdef list_app_encryption_declarations(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_app_encryption_declarations\n\t\t:return: an iterator over AppEncryptionDeclaration resources\n\t\t\"\"\"\n\t\treturn self._get_resources(AppEncryptionDeclaration, filters)\n\n\tdef list_beta_license_agreements(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_license_agreements\n\t\t:return: an iterator over BetaLicenseAgreement resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaLicenseAgreement, filters)\n\n\t# Build Resources\n\tdef list_builds(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_builds\n\t\t:return: an iterator over Build resources\n\t\t\"\"\"\n\t\treturn self._get_resources(Build, filters, sort)\n\n\t# TODO: handle fields on get_resources()\n\tdef build_processing_state(self, app_id, version):\n\t\treturn self._api_call(BASE_API + \"/v1/builds?filter[app]=\" + app_id + \"&filter[version]=\" + version + \"&fields[builds]=processingState\")\n\n\t# TODO: implement POST requests using Resource\n\tdef set_uses_non_encryption_exemption_setting(self, build_id, uses_non_encryption_exemption_setting):\n\t\tpost_data = {'data': {'attributes': {'usesNonExemptEncryption': uses_non_encryption_exemption_setting}, 'id': build_id, 'type': 'builds'}}\n\t\tpayload = self._api_call(BASE_API + \"/v1/builds/\" + build_id, HttpMethod.PATCH, post_data)\n\t\treturn Build(payload.get('data'), {})\n\n\tdef list_build_beta_details(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_build_beta_details\n\t\t:return: an iterator over BuildBetaDetail resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BuildBetaDetail, filters)\n\n\tdef create_beta_build_localization(self, build: Build, locale: str, whatsNew: str = None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/create_a_beta_build_localization\n\t\t:return: a BetaBuildLocalization resource\n\t\t\"\"\"\n\t\treturn self._create_resource(BetaBuildLocalization, locals())\n\n\tdef modify_beta_build_localization(self, beta_build_localization: BetaBuildLocalization, whatsNew: str):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_beta_build_localization\n\t\t:return: a BetaBuildLocalization resource\n\t\t\"\"\"\n\t\treturn self._modify_resource(beta_build_localization, locals())\n\n\tdef list_beta_build_localizations(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_build_localizations\n\t\t:return: an iterator over BetaBuildLocalization resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaBuildLocalization, filters)\n\n\tdef list_beta_app_review_details(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_app_review_details\n\t\t:return: an iterator over BetaAppReviewDetail resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaAppReviewDetail, filters)\n\n\tdef submit_app_for_beta_review(self, build: Build) -> BetaAppReviewSubmission:\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/submit_an_app_for_beta_review\n\t\t:return: a BetaAppReviewSubmission resource\n\t\t\"\"\"\n\n\t\treturn self._create_resource(BetaAppReviewSubmission, locals())\n\n\tdef list_beta_app_review_submissions(self, filters=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_beta_app_review_submissions\n\t\t:return: an iterator over BetaAppReviewSubmission resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BetaAppReviewSubmission, filters)\n\n\tdef read_beta_app_review_submission_information(self, beta_app_id: str):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/read_beta_app_review_submission_information\n\t\t:return: an BetaAppReviewSubmission resource\n\t\t\"\"\"\n\t\treturn self._get_resource(BetaAppReviewSubmission, beta_app_id)\n\n\t# Provisioning\n\tdef list_bundle_ids(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_bundle_ids\n\t\t:return: an iterator over BundleId resources\n\t\t\"\"\"\n\t\treturn self._get_resources(BundleId, filters, sort)\n\n\tdef list_certificates(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_and_download_certificates\n\t\t:return: an iterator over Certificate resources\n\t\t\"\"\"\n\t\treturn self._get_resources(Certificate, filters, sort)\n\n\tdef list_devices(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_devices\n\t\t:return: an iterator over Device resources\n\t\t\"\"\"\n\t\treturn self._get_resources(Device, filters, sort)\n\n\tdef register_new_device(self, name: str, platform: str, udid: str) -> Device:\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/register_a_new_device\n\t\t:return: a Device resource\n\t\t\"\"\"\n\t\treturn self._create_resource(Device, locals())\n\n\tdef modify_registered_device(self, device: Device, name: str = None, status: str = None) -> Device:\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/modify_a_registered_device\n\t\t:return: a Device resource\n\t\t\"\"\"\n\t\treturn self._modify_resource(device, locals())\n\n\tdef list_profiles(self, filters=None, sort=None):\n\t\t\"\"\"\n\t\t:reference: https://developer.apple.com/documentation/appstoreconnectapi/list_and_download_profiles\n\t\t:return: an iterator over Profile resources\n\t\t\"\"\"\n\t\treturn self._get_resources(Profile, filters, sort)\n\n\t# Reporting\n\tdef download_finance_reports(self, filters=None, split_response=False, save_to=None):\n\t\t# setup required filters if not provided\n\t\tfor required_key, default_value in (\n\t\t\t\t('regionCode', 'ZZ'),\n\t\t\t\t('reportType', 'FINANCIAL'),\n\t\t\t\t# vendorNumber is required but we cannot provide a default value\n\t\t\t\t# reportDate is required but we cannot provide a default value\n\t\t):\n\t\t\tif required_key not in filters:\n\t\t\t\tfilters[required_key] = default_value\n\n\t\turl = \"%s%s\" % (BASE_API, FinanceReport.endpoint)\n\t\turl = self._build_query_parameters(url, filters)\n\t\tresponse = self._api_call(url)\n\n\t\tif split_response:\n\t\t\tres1 = response.split('Total_Rows')[0]\n\t\t\tres2 = '\\n'.join(response.split('Total_Rows')[1].split('\\n')[1:])\n\n\t\t\tif save_to:\n\t\t\t\tfile1 = Path(save_to[0])\n\t\t\t\tfile1.write_text(res1, 'utf-8')\n\t\t\t\tfile2 = Path(save_to[1])\n\t\t\t\tfile2.write_text(res2, 'utf-8')\n\n\t\t\treturn res1, res2\n\n\t\tif save_to:\n\t\t\tfile = Path(save_to)\n\t\t\tfile.write_text(response, 'utf-8')\n\n\t\treturn response\n\n\tdef download_sales_and_trends_reports(self, filters=None, save_to=None):\n\t\t# setup required filters if not provided\n\t\tdefault_versions = {\n\t\t\t'SALES': '1_0',\n\t\t\t'SUBSCRIPTION': '1_2',\n\t\t\t'SUBSCRIPTION_EVENT': '1_2',\n\t\t\t'SUBSCRIBER': '1_2',\n\t\t\t'NEWSSTAND': '1_0',\n\t\t\t'PRE_ORDER': '1_0',\n\t\t}\n\t\tdefault_subtypes = {\n\t\t\t'SALES': 'SUMMARY',\n\t\t\t'SUBSCRIPTION': 'SUMMARY',\n\t\t\t'SUBSCRIPTION_EVENT': 'SUMMARY',\n\t\t\t'SUBSCRIBER': 'DETAILED',\n\t\t\t'NEWSSTAND': 'DETAILED',\n\t\t\t'PRE_ORDER': 'SUMMARY',\n\t\t}\n\t\tfor required_key, default_value in (\n\t\t\t\t('frequency', 'DAILY'),\n\t\t\t\t('reportType', 'SALES'),\n\t\t\t\t('reportSubType',  default_subtypes.get(filters.get('reportType', 'SALES'), 'SUMMARY')),\n\t\t\t\t('version', default_versions.get(filters.get('reportType', 'SALES'), '1_0')),\n\t\t\t\t# vendorNumber is required but we cannot provide a default value\n\t\t):\n\t\t\tif required_key not in filters:\n\t\t\t\tfilters[required_key] = default_value\n\n\t\turl = \"%s%s\" % (BASE_API, SalesReport.endpoint)\n\t\turl = self._build_query_parameters(url, filters)\n\t\tresponse = self._api_call(url)\n\n\t\tif save_to:\n\t\t\tfile = Path(save_to)\n\t\t\tfile.write_text(response, 'utf-8')\n\n\t\treturn response\n"
  },
  {
    "path": "appstoreconnect/resources.py",
    "content": "import inspect\nfrom abc import ABC, abstractmethod\nimport sys\n\n\nclass Resource(ABC):\n\trelationships = {}\n\n\tdef __init__(self, data, api):\n\t\tself._data = data\n\t\tself._api = api\n\n\tdef __getattr__(self, item):\n\t\tif item == 'id':\n\t\t\treturn self._data.get('id')\n\t\tif item in self._data.get('attributes', {}):\n\t\t\treturn self._data.get('attributes', {})[item]\n\t\tif item in self.relationships:\n\t\t\tdef getter():\n\t\t\t\t# Try to fetch relationship\n\t\t\t\tnonlocal item\n\t\t\t\turl = self._data.get('relationships', {})[item]['links']['related']\n\t\t\t\tif self.relationships[item]['multiple']:\n\t\t\t\t\treturn self._api.get_related_resources(full_url=url)\n\t\t\t\telse:\n\t\t\t\t\treturn self._api.get_related_resource(full_url=url)\n\t\t\treturn getter\n\n\t\traise AttributeError('%s has no attributes %s' % (self.type_name, item))\n\n\tdef __repr__(self):\n\t\treturn '%s id %s' % (self.type_name, self._data.get('id'))\n\n\tdef __dir__(self):\n\t\treturn ['id'] + list(self._data.get('attributes', {}).keys()) + list(self._data.get('relationships', {}).keys())\n\n\t@property\n\tdef type_name(self):\n\t\treturn type(self).__name__\n\n\t@property\n\t@abstractmethod\n\tdef endpoint(self):\n\t\tpass\n\n\n# Beta Testers and Groups\n\nclass BetaTester(Resource):\n\tendpoint = '/v1/betaTesters'\n\ttype = 'betaTesters'\n\tattributes = ['email', 'firstName', 'inviteType', 'lastName']\n\trelationships = {\n\t\t'apps': {'multiple': True},\n\t\t'betaGroups': {'multiple': True},\n\t\t'builds': {'multiple': True},\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betatester'\n\n\nclass BetaGroup(Resource):\n\tendpoint = '/v1/betaGroups'\n\ttype = 'betaGroups'\n\tattributes = ['isInternalGroup', 'name', 'publicLink', 'publicLinkEnabled', 'publicLinkId', 'publicLinkLimit', 'publicLinkLimitEnabled', 'createdDate']\n\trelationships = {\n\t\t'app': {'multiple': False},\n\t\t'betaTesters': {'multiple': True},\n\t\t'builds': {'multiple': True},\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betagroup'\n\n\n# App Resources\n\nclass App(Resource):\n\tendpoint = '/v1/apps'\n\ttype = 'apps'\n\tattributes = ['bundleId', 'name', 'primaryLocale', 'sku']\n\trelationships = {\n\t\t'betaLicenseAgreement': {'multiple': False},\n\t\t'preReleaseVersions': {'multiple': True},\n\t\t'betaAppLocalizations': {'multiple': True},\n\t\t'betaGroups': {'multiple': True},\n\t\t'betaTesters': {'multiple': True},\n\t\t'builds': {'multiple': True},\n\t\t'betaAppReviewDetail': {'multiple': False},\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/app'\n\n\nclass PreReleaseVersion(Resource):\n\tendpoint = '/v1/preReleaseVersions'\n\ttype = 'preReleaseVersions'\n\tattributes = ['platform', 'version']\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/preReleaseVersion/attributes'\n\n\nclass BetaAppLocalization(Resource):\n\tendpoint = '/v1/betaAppLocalizations'\n\ttype = 'betaAppLocalizations'\n\tattributes = ['description', 'feedbackEmail', 'locale', 'marketingUrl', 'privacyPolicyUrl', 'tvOsPrivacyPolicy']\n\trelationships = {\n\t\t'app': {'multiple': False}\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppLocalization/attributes'\n\n\nclass AppEncryptionDeclaration(Resource):\n\tendpoint = '/v1/appEncryptionDeclarations'\n\ttype = 'appEncryptionDeclarations'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/appEncryptionDeclaration/attributes'\n\n\nclass BetaLicenseAgreement(Resource):\n\tendpoint = '/v1/betaLicenseAgreements'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaLicenseAgreement/attributes'\n\n\n# Build Resources\n\nclass Build(Resource):\n\tendpoint = '/v1/builds'\n\ttype = 'builds'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/build/attributes'\n\trelationships = {\n\t\t'app': {'multiple': False},\n\t\t'appEncryptionDeclaration': {'multiple': False},\n\t\t'individualTesters': {'multiple': True},\n\t\t'preReleaseVersion': {'multiple': False},\n\t\t'betaBuildLocalizations': {'multiple': True},\n\t\t'buildBetaDetail': {'multiple': False},\n\t\t'betaAppReviewSubmission': {'multiple': False},\n\t\t'appStoreVersion': {'multiple': False},\n\t\t'icons': {'multiple': True},\n\t}\n\n\nclass BuildBetaDetail(Resource):\n\tendpoint = '/v1/buildBetaDetails'\n\ttype = 'buildBetaDetails'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/buildBetaDetail/attributes'\n\n\nclass BetaBuildLocalization(Resource):\n\tendpoint = '/v1/betaBuildLocalizations'\n\ttype = 'betaBuildLocalizations'\n\tattributes = ['locale', 'whatsNew']\n\trelationships = {\n\t\t'build': {'multiple': False},\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaBuildLocalization/attributes'\n\n\nclass BetaAppReviewDetail(Resource):\n\tendpoint = '/v1/betaAppReviewDetails'\n\ttype = 'betaAppReviewDetails'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppReviewDetail/attributes'\n\n\nclass BetaAppReviewSubmission(Resource):\n\tendpoint = '/v1/betaAppReviewSubmissions'\n\ttype = 'betaAppReviewSubmissions'\n\tattributes = ['betaReviewState']\n\trelationships = {\n\t\t'build': {'multiple': False},\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/betaAppReviewSubmission/attributes'\n\n\n# Users and Roles\n\nclass User(Resource):\n\tendpoint = '/v1/users'\n\ttype = 'users'\n\tattributes = ['allAppsVisible', 'provisioningAllowed', 'roles']\n\trelationships = {\n\t\t'visibleApps': {'multiple': True},\n\t}\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/user/attributes'\n\n\nclass UserInvitation(Resource):\n\tendpoint = '/v1/userInvitations'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/userinvitation/attributes'\n\n\n# Provisioning\nclass BundleId(Resource):\n\tendpoint = '/v1/bundleIds'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/bundleid/attributes'\n\n\nclass Certificate(Resource):\n\tendpoint = '/v1/certificates'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/certificate/attributes'\n\n\nclass Device(Resource):\n\tendpoint = '/v1/devices'\n\ttype = 'devices'\n\tattributes = ['name', 'platform', 'udid', 'status']\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/device/attributes'\n\n\nclass Profile(Resource):\n\tendpoint = '/v1/profiles'\n\tdocumentation = 'https://developer.apple.com/documentation/appstoreconnectapi/profile/attributes'\n\n\n# Reporting\n\nclass FinanceReport(Resource):\n\tendpoint = '/v1/financeReports'\n\tfilters = 'https://developer.apple.com/documentation/appstoreconnectapi/download_finance_reports'\n\n\nclass SalesReport(Resource):\n\tendpoint = '/v1/salesReports'\n\tfilters = 'https://developer.apple.com/documentation/appstoreconnectapi/download_sales_and_trends_reports'\n\n\n# create an index of Resources by type\nresources = {}\nfor name, obj in inspect.getmembers(sys.modules[__name__]):\n\tif inspect.isclass(obj) and issubclass(obj, Resource) and hasattr(obj, 'type') and obj != Resource:\n\t\tresources[getattr(obj, 'type')] = obj\n"
  },
  {
    "path": "example.py",
    "content": "#!/usr/bin/env python\n\nimport sys\nfrom appstoreconnect import Api, UserRole\n\n\nif __name__ == \"__main__\":\n\tkey_id = sys.argv[1]\n\tkey_file = sys.argv[2]\n\tissuer_id = sys.argv[3]\n\tapi = Api(key_id, key_file, issuer_id)\n\n\t# list all apps\n\tapps = api.list_apps()\n\tfor app in apps:\n\t\tprint(app.name, app.sku)\n\n\t# filter apps\n\tapps = api.list_apps(filters={'sku': 'DINORUSH', 'name': 'Dino Rush'})\n\tprint(\"%d apps found\" % len(apps))\n\n\t# modify a user\n\tuser = api.list_users(filters={'username': 'finance@nemoidstudio.com'})[0]\n\tapi.modify_user_account(user, roles=[UserRole.FINANCE, UserRole.APP_MANAGER, UserRole.ACCESS_TO_REPORTS])\n\n\t# download sales report\n\tapi.download_sales_and_trends_reports(\n\t\tfilters={'vendorNumber': '123456789', 'frequency': 'WEEKLY', 'reportDate': '2019-06-09'}, save_to='report.csv')\n\n\t# download finance report\n\tapi.download_finance_reports(filters={'vendorNumber': '123456789', 'reportDate': '2019-06'}, save_to='finance.csv')\n"
  },
  {
    "path": "requirements.txt",
    "content": ".\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport io\nimport os\n\nfrom setuptools import find_packages, setup\n\nNAME = 'appstoreconnect'\nDESCRIPTION = 'A Python wrapper around Apple App Store Api'\nURL = 'https://ponytech.net/projects/app-store-connect'\nEMAIL = 'contact@ponytech.net'\nAUTHOR = 'Ponytech'\nREQUIRES_PYTHON = '>=3.6.0'\nVERSION = None\n\nREQUIRED = [\n    'requests>=2.20.1,==2.*',\n    'PyJWT>=1.6.4,==1.*',\n    'cryptography>=2.6.1',\n]\n\nEXTRAS = {\n}\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\ntry:\n    with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:\n        long_description = '\\n' + f.read()\nexcept FileNotFoundError:\n    long_description = DESCRIPTION\n\nabout = {}\nif not VERSION:\n    with open(os.path.join(here, NAME, '__version__.py')) as f:\n        exec(f.read(), about)\nelse:\n    about['__version__'] = VERSION\n\nsetup(\n    name=NAME,\n    version=about['__version__'],\n    description=DESCRIPTION,\n    long_description=long_description,\n    long_description_content_type='text/markdown',\n    author=AUTHOR,\n    author_email=EMAIL,\n    python_requires=REQUIRES_PYTHON,\n    url=URL,\n    packages=find_packages(exclude=('tests',)),\n    install_requires=REQUIRED,\n    extras_require=EXTRAS,\n    include_package_data=True,\n    license='MIT',\n    classifiers=[\n        'License :: OSI Approved :: MIT License',\n        'Programming Language :: Python',\n        'Programming Language :: Python :: 3',\n        'Programming Language :: Python :: 3.6',\n        'Programming Language :: Python :: 3.7',\n        'Programming Language :: Python :: 3.8',\n        'Programming Language :: Python :: 3.9',\n    ],\n)\n"
  }
]