Skip to content

Commit

Permalink
adds some custom exceptions for the known ApiFlash HTTP 4xx status codes
Browse files Browse the repository at this point in the history
  • Loading branch information
jnhmcknight committed Nov 23, 2023
1 parent 35dc3fe commit 6958f0a
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 21 deletions.
28 changes: 28 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

[MESSAGES CONTROL]

# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=missing-module-docstring,
useless-object-inheritance,
logging-format-interpolation


[VARIABLES]

# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_|args|kwargs


[SIMILARITIES]

# Minimum lines number of a similarity.
min-similarity-lines=20
50 changes: 29 additions & 21 deletions apiflash/client.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,41 @@

from apiclient import (
APIClient,
endpoint,
JsonResponseHandler,
QueryParameterAuthentication,
RequestsResponseHandler,
)


# Define endpoints, using the provided decorator.
@endpoint(base_url="https://api.apiflash.com")
class Endpoint:
screenshot = "v1/urltoimage"
quota = "v1/urltoimage/quota"


class ImageFormat:
WEPB = 'webp'
PNG = 'png'
JPEG = 'jpeg'


class ResponseType:
JSON = 'json'
IMAGE = 'image'
from .constants import (
CaptureOptions,
Endpoint,
ImageFormat,
ResponseType,
)
from .error_handler import ApiFlashErrorHandler
from .exceptions import (
InvalidCaptureOptionError,
NoAccessKeyError,
)


# Extend the client for your API integration.
class ApiFlashClient(APIClient):
"""ApiFlash.com client"""

image_format = ImageFormat.PNG
response_type = ResponseType.JSON
fail_on_status = '400-599'

def __init__(self, access_key, *, image_format=None, response_type=None, fail_on_status=None):
if not access_key:
raise NoAccessKeyError()

super().__init__(
authentication_method=QueryParameterAuthentication(
'access_key',
access_key,
),
response_handler=JsonResponseHandler,
error_handler=ApiFlashErrorHandler,
)

if image_format is not None:
Expand All @@ -50,11 +46,21 @@ def __init__(self, access_key, *, image_format=None, response_type=None, fail_on
self.fail_on_status = fail_on_status

def _autoswitch_handler(self, response_type):
"""Based on the response type requested, set the request handler appropriately"""

self.set_response_handler(
RequestsResponseHandler if response_type == ResponseType.IMAGE else JsonResponseHandler
)

def capture(self, url, **kwargs):
def capture(self, url, *, ignore_unknown_options=False, **kwargs):
"""Capture a screenshot of the given URL"""

if not ignore_unknown_options:
# pylint: disable=consider-iterating-dictionary
for k in kwargs.keys():
if k not in CaptureOptions:
raise InvalidCaptureOptionError(k)

kwargs['url'] = url

if 'response_type' not in kwargs:
Expand All @@ -69,5 +75,7 @@ def capture(self, url, **kwargs):
return resp.content if kwargs['response_type'] == ResponseType.IMAGE else resp

def quota(self):
"""Get the quota information for the current access key"""

self._autoswitch_handler(ResponseType.JSON)
return self.get(Endpoint.quota)
74 changes: 74 additions & 0 deletions apiflash/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# pylint: disable=too-few-public-methods

from apiclient import endpoint


# Define endpoints, using the provided decorator.
@endpoint(base_url="https://api.apiflash.com")
class Endpoint:
"""All the available endpoints of the API"""

screenshot = "v1/urltoimage"
quota = "v1/urltoimage/quota"


class ImageFormat:
"""Supported image formats"""

WEPB = 'webp'
PNG = 'png'
JPEG = 'jpeg'


class ResponseType:
"""Allowed response types"""

JSON = 'json'
IMAGE = 'image'


# All possible options for `capture()`: https://apiflash.com/documentation#parameters
CaptureOptions = [ # pylint: disable=invalid-name
'format', # jpeg
'width', # in pixels: 1920
'height', # in pixels: 1080
'fresh', # false
'full_page', # false
'quality', # in percent: 80
'delay', # in secs: 0
'scroll_page', # false
'ttl', # in secs: 86400
'response_type', # image
'thumbnail_width', #
'crop', #
'no_cookie_banners', # false
'no_ads', # false
'no_tracking', # false
'scale_factor', # 1
'element', #
'element_overlap', # false
'user_agent', #
'extract_html', # false
'extract_text', # false
'transparent', # false
'wait_for', #
'wait_until', # network_idle
'fail_on_status', #
'accept_language', #
'css', #
'cookies', #
'proxy', #
'latitude', #
'longitude', #
'accuracy', # in secs: 0
'js', #
'headers', #
'time_zone', #
'ip_location', #
's3_access_key_id', #
's3_secret_key', #
's3_bucket', #
's3_key', #
's3_endpoint', #
's3_region', #
]
39 changes: 39 additions & 0 deletions apiflash/error_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

from apiclient.error_handlers import ErrorHandler
from apiclient.exceptions import APIRequestError
from apiclient.response import Response

from .exceptions import (
BadRequestError,
ForbiddenError,
QuotaExceededError,
RateLimitedError,
UnauthorizedError,
)


ApiExceptions = { # pylint: disable=invalid-name
'400': BadRequestError,
'401': UnauthorizedError,
'402': QuotaExceededError,
'403': ForbiddenError,
'429': RateLimitedError,
}


# pylint: disable=too-few-public-methods
class ApiFlashErrorHandler(ErrorHandler):
"""An error handler for ApiFlash specific error codes"""

@staticmethod
def get_exception(response: Response) -> APIRequestError:
status_code = response.get_status_code()

# Need these to be as strings, because a dict can't have an int key
if str(status_code) in ApiExceptions:
return ApiExceptions[str(status_code)](
status_code=status_code,
info=response.get_raw_data(),
)

return ErrorHandler.get_exception(response)
37 changes: 37 additions & 0 deletions apiflash/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

from apiclient.exceptions import (
APIClientError,
APIRequestError,
)


# Errors thrown by the client, except those for HTTP reasons

class NoAccessKeyError(APIClientError):
"""There is no API key defined for the connection"""


class InvalidCaptureOptionError(APIClientError):
"""There is no API key defined for the connection"""


# Errors specific to ApiFlash requests

class BadRequestError(APIRequestError):
"""Either the API call contains invalid parameters or the target URL cannot be captured."""


class UnauthorizedError(APIRequestError):
"""The access key used to make this API call has been revoked or is invalid."""


class QuotaExceededError(APIRequestError):
"""The monthly screenshot quota has been exceeded for the user's current plan."""


class ForbiddenError(APIRequestError):
"""The current plan does not support some of the features requested through API parameters."""


class RateLimitedError(APIRequestError):
"""Too many API calls have been made. The specific reason is included in the response body."""

0 comments on commit 6958f0a

Please sign in to comment.