115 lines
3.2 KiB
Python
115 lines
3.2 KiB
Python
#
|
|
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
|
|
#
|
|
|
|
|
|
import collections
|
|
import functools
|
|
from typing import Iterator
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
import requests
|
|
|
|
|
|
class ServerError(Exception):
|
|
"""Server respond with error"""
|
|
|
|
|
|
class APIError(Exception):
|
|
"""Base class for API errors""" ""
|
|
|
|
|
|
class ValidationError(APIError):
|
|
"""Provided data has failed validation"""
|
|
|
|
|
|
class AuthError(APIError):
|
|
"""Token is wrong or expired"""
|
|
|
|
|
|
class NotFoundError(APIError):
|
|
"""Object not found"""
|
|
|
|
|
|
class RateLimitError(APIError):
|
|
"""API calls reached allowed limit"""
|
|
|
|
|
|
def cursor_paginator(request, start_index: int = None, per_page: int = 100, params: dict = None) -> Iterator[dict]:
|
|
"""Paginator that use cursor offset to navigate"""
|
|
params = params or {}
|
|
index = start_index
|
|
while True:
|
|
result = request(params={**params, "next": index, "limit": per_page})
|
|
if isinstance(result["data"], collections.abc.Sequence):
|
|
yield from result["data"]
|
|
else:
|
|
yield result["data"]
|
|
index = result.get("pagination", {}).get("next")
|
|
if not index:
|
|
break
|
|
|
|
|
|
def next_url_paginator(request, start_index: int = None, per_page: int = 100, params: dict = None) -> Iterator[dict]:
|
|
"""Paginator that use next url to navigate"""
|
|
params = params or {}
|
|
size = per_page
|
|
index = start_index
|
|
while True:
|
|
result = request(params={**params, "index": index, "size": size})
|
|
if isinstance(result["data"], collections.abc.Sequence):
|
|
yield from result["data"]
|
|
else:
|
|
yield result["data"]
|
|
|
|
next_url = result["data"].get("next")
|
|
if not next_url:
|
|
break
|
|
|
|
# parse url to unify request command
|
|
next_url = urlparse(next_url)
|
|
next_params = parse_qs(next_url.query)
|
|
index = next_params.get("index", [None])[0]
|
|
size = next_params.get("size", [None])[0]
|
|
|
|
|
|
def exception_from_code(code: int, message: str) -> Exception:
|
|
"""Map response code to exception class"""
|
|
mapping = {
|
|
400: ValidationError,
|
|
401: AuthError,
|
|
403: AuthError,
|
|
429: RateLimitError,
|
|
404: NotFoundError,
|
|
500: ServerError,
|
|
502: ServerError,
|
|
503: ServerError,
|
|
504: ServerError,
|
|
}
|
|
|
|
return mapping.get(code, APIError)(code, message)
|
|
|
|
|
|
def _parsed_response(func):
|
|
"""Decorator to check response status and parse its body"""
|
|
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
try:
|
|
response = func(*args, **kwargs)
|
|
result = response.json() if response.text else {}
|
|
if not response.ok:
|
|
msg = result # fallback to the whole response
|
|
if "error" in result:
|
|
msg = result["error"].get("message", result)
|
|
# multiple errors? grab all of them
|
|
elif "errors" in result:
|
|
msg = result["errors"]
|
|
raise exception_from_code(response.status_code, msg)
|
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as err:
|
|
raise ServerError(err.request.status_code, "Connection Error") from err
|
|
|
|
return result
|
|
|
|
return wrapper
|