1
0
mirror of synced 2026-01-02 12:02:47 -05:00
Files
airbyte/airbyte-integrations/connectors/source-drift/source_drift/client/common.py
Cole Snodgrass 2e099acc52 update headers from 2022 -> 2023 (#22594)
* It's 2023!

* 2022 -> 2023

---------

Co-authored-by: evantahler <evan@airbyte.io>
2023-02-08 13:01:16 -08:00

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