зеркало из https://github.com/microsoft/asconnect.git
Add retry logic to get for 401 error (#8)
This commit is contained in:
Родитель
1a3b3335af
Коммит
0892f3f48d
|
@ -11,6 +11,13 @@
|
|||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Python: Test",
|
||||
"type": "python",
|
||||
"request": "test",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
}
|
||||
]
|
||||
}
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class AppStoreConnectError(Exception):
|
||||
"""An error response from the API."""
|
||||
|
@ -15,15 +17,20 @@ class AppStoreConnectError(Exception):
|
|||
title: str
|
||||
detail: str
|
||||
source: Any
|
||||
response: requests.Response
|
||||
|
||||
def __init__(self, data: Any):
|
||||
def __init__(self, response: requests.Response):
|
||||
"""Create a new instance.
|
||||
|
||||
:param data: The raw data from the response
|
||||
:param response: The HTTP response
|
||||
|
||||
:raises ValueError: If we can't decode the data
|
||||
"""
|
||||
|
||||
self.response = response
|
||||
|
||||
data = response.json()
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Could not decode App Store Connect error: {data}")
|
||||
|
||||
|
|
|
@ -17,11 +17,12 @@ from asconnect.exceptions import AppStoreConnectError
|
|||
class HttpClient:
|
||||
"""Base HTTP client for the ASC API."""
|
||||
|
||||
key_id: str
|
||||
key_contents: str
|
||||
issuer_id: str
|
||||
_key_id: str
|
||||
_key_contents: str
|
||||
_issuer_id: str
|
||||
log: logging.Logger
|
||||
|
||||
_credentials_valid: bool
|
||||
_cached_token_info: Optional[Tuple[str, datetime.datetime]]
|
||||
|
||||
def __init__(
|
||||
|
@ -40,9 +41,10 @@ class HttpClient:
|
|||
:param log: Any base logger to be used (one will be created if not supplied)
|
||||
"""
|
||||
|
||||
self.key_id = key_id
|
||||
self.key_contents = key_contents
|
||||
self.issuer_id = issuer_id
|
||||
self._key_id = key_id
|
||||
self._key_contents = key_contents
|
||||
self._issuer_id = issuer_id
|
||||
self._credentials_valid = False
|
||||
|
||||
if log is None:
|
||||
self.log = logging.getLogger("asconnect")
|
||||
|
@ -51,6 +53,57 @@ class HttpClient:
|
|||
|
||||
self._cached_token_info = None
|
||||
|
||||
@property
|
||||
def key_contents(self) -> str:
|
||||
"""Get key contents.
|
||||
|
||||
:returns: Key contents
|
||||
"""
|
||||
return self._key_contents
|
||||
|
||||
@key_contents.setter
|
||||
def key_contents(self, value: str) -> None:
|
||||
"""Set key contents.
|
||||
|
||||
:param value: The new value to set to
|
||||
"""
|
||||
self._key_contents = value
|
||||
self._credentials_valid = False
|
||||
|
||||
@property
|
||||
def key_id(self) -> str:
|
||||
"""Get key ID.
|
||||
|
||||
:returns: Key ID
|
||||
"""
|
||||
return self._key_id
|
||||
|
||||
@key_id.setter
|
||||
def key_id(self, value: str) -> None:
|
||||
"""Set key ID.
|
||||
|
||||
:param value: The new value to set to
|
||||
"""
|
||||
self._key_id = value
|
||||
self._credentials_valid = False
|
||||
|
||||
@property
|
||||
def issuer_id(self) -> str:
|
||||
"""Get issuer ID.
|
||||
|
||||
:returns: Issuer ID
|
||||
"""
|
||||
return self._issuer_id
|
||||
|
||||
@issuer_id.setter
|
||||
def issuer_id(self, value: str) -> None:
|
||||
"""Set issuer ID.
|
||||
|
||||
:param value: The new value to set to
|
||||
"""
|
||||
self._issuer_id = value
|
||||
self._credentials_valid = False
|
||||
|
||||
def generate_token(self) -> str:
|
||||
"""Generate a new JWT token.
|
||||
|
||||
|
@ -93,12 +146,21 @@ class HttpClient:
|
|||
_ = self
|
||||
return f"https://api.appstoreconnect.apple.com/v1/{endpoint}"
|
||||
|
||||
def verify_response(self, response: requests.Response) -> None:
|
||||
"""Perform some checks on the response.
|
||||
|
||||
:param response: The response to check
|
||||
"""
|
||||
if response.status_code >= 200 and response.status_code < 300:
|
||||
self._credentials_valid = True
|
||||
|
||||
def get(
|
||||
self,
|
||||
*,
|
||||
data_type: Type,
|
||||
endpoint: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
attempts: int = 3,
|
||||
) -> Iterator[Any]:
|
||||
"""Perform a GET to the endpoint specified.
|
||||
|
||||
|
@ -108,8 +170,10 @@ class HttpClient:
|
|||
:param Type data_type: The class to deserialize the data of the response to
|
||||
:param Optional[str] endpoint: The endpoint to perform the GET on
|
||||
:param Optional[str] url: The full URL to perform the GET on
|
||||
:param int attempts: Number of attempts remaining to try this call
|
||||
|
||||
:raises ValueError: If neither url or endpoint are specified
|
||||
:raises AppStoreConnectError: If an error with the API occurs
|
||||
|
||||
:returns: The raw response
|
||||
"""
|
||||
|
@ -125,7 +189,22 @@ class HttpClient:
|
|||
url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
response_data = self.extract_data(raw_response)
|
||||
|
||||
self.verify_response(raw_response)
|
||||
|
||||
try:
|
||||
response_data = self.extract_data(raw_response)
|
||||
except AppStoreConnectError as ex:
|
||||
if attempts > 1 and (
|
||||
ex.response.status_code >= 500
|
||||
or (ex.response.status_code == 401 and self._credentials_valid)
|
||||
):
|
||||
yield from self.get(
|
||||
data_type=data_type, endpoint=endpoint, url=url, attempts=attempts - 1
|
||||
)
|
||||
return
|
||||
|
||||
raise
|
||||
|
||||
if response_data["data"] is None:
|
||||
yield from []
|
||||
|
@ -182,11 +261,13 @@ class HttpClient:
|
|||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
self.verify_response(raw_response)
|
||||
|
||||
if raw_response.status_code == 204:
|
||||
return None
|
||||
|
||||
if raw_response.status_code != 200:
|
||||
raise AppStoreConnectError(raw_response.json())
|
||||
raise AppStoreConnectError(raw_response)
|
||||
|
||||
response_data = self.extract_data(raw_response)
|
||||
|
||||
|
@ -231,6 +312,8 @@ class HttpClient:
|
|||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
self.verify_response(raw_response)
|
||||
|
||||
if raw_response.status_code == 201:
|
||||
|
||||
if data_type is None:
|
||||
|
@ -243,7 +326,7 @@ class HttpClient:
|
|||
if raw_response.status_code >= 200 and raw_response.status_code < 300:
|
||||
return None
|
||||
|
||||
raise AppStoreConnectError(raw_response.json())
|
||||
raise AppStoreConnectError(raw_response)
|
||||
|
||||
def delete(
|
||||
self, *, endpoint: Optional[str] = None, url: Optional[str] = None
|
||||
|
@ -267,11 +350,15 @@ class HttpClient:
|
|||
raise ValueError("Either `endpoint` or `url` must be set")
|
||||
url = self.generate_url(endpoint)
|
||||
|
||||
return requests.delete(
|
||||
raw_response = requests.delete(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
self.verify_response(raw_response)
|
||||
|
||||
return raw_response
|
||||
|
||||
def put_chunk(
|
||||
self, *, url: str, additional_headers: Dict[str, str], data: bytes
|
||||
) -> requests.Response:
|
||||
|
@ -290,7 +377,11 @@ class HttpClient:
|
|||
**additional_headers,
|
||||
}
|
||||
|
||||
return requests.put(url=url, data=data, headers=headers)
|
||||
raw_response = requests.put(url=url, data=data, headers=headers)
|
||||
|
||||
self.verify_response(raw_response)
|
||||
|
||||
return raw_response
|
||||
|
||||
def extract_data(self, response: requests.Response) -> Any:
|
||||
"""Validate a response from the API and extract the data
|
||||
|
@ -304,6 +395,6 @@ class HttpClient:
|
|||
_ = self
|
||||
|
||||
if not response.ok:
|
||||
raise AppStoreConnectError(response.json())
|
||||
raise AppStoreConnectError(response)
|
||||
|
||||
return response.json()
|
||||
|
|
|
@ -69,7 +69,7 @@ class ScreenshotClient:
|
|||
raw_response = self.http_client.delete(url=url)
|
||||
|
||||
if raw_response.status_code != 204:
|
||||
raise AppStoreConnectError(raw_response.json())
|
||||
raise AppStoreConnectError(raw_response)
|
||||
|
||||
def get_screenshots(
|
||||
self,
|
||||
|
@ -98,7 +98,7 @@ class ScreenshotClient:
|
|||
raw_response = self.http_client.delete(url=url)
|
||||
|
||||
if raw_response.status_code != 204:
|
||||
raise AppStoreConnectError(raw_response.json())
|
||||
raise AppStoreConnectError(raw_response)
|
||||
|
||||
def delete_screenshots_in_set(self, *, screenshot_set_id: str) -> None:
|
||||
"""Delete all screenshots in set.
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
import logging
|
||||
from typing import Iterator, List, Optional
|
||||
|
||||
from tenacity import retry, stop_after_delay, wait_random_exponential
|
||||
|
||||
from asconnect.httpclient import HttpClient
|
||||
from asconnect.models import (
|
||||
AppStoreVersion,
|
||||
|
@ -313,7 +311,6 @@ class VersionClient:
|
|||
data_type=AppStoreReviewDetails,
|
||||
)
|
||||
|
||||
@retry(wait=wait_random_exponential(max=100), stop=stop_after_delay(500))
|
||||
def get_idfa(self, *, version_id: str) -> Optional[IdfaDeclaration]:
|
||||
"""Get the advertising ID declaration.
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче