This repository has been archived by the owner on Dec 9, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial implementation of the API client added
- Loading branch information
1 parent
5c28be0
commit 793ebb2
Showing
1 changed file
with
211 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
''' | ||
ComicVine API Information & Documentation: | ||
https://comicvine.gamespot.com/api/ | ||
https://comicvine.gamespot.com/api/documentation | ||
''' | ||
|
||
import requests | ||
import requests_cache | ||
|
||
from .exceptions import ( | ||
ComicVineApiError, ComicVineUnauthorizedError, ComicVineForbiddenError | ||
) | ||
|
||
|
||
class ComicVineClient(object): | ||
''' | ||
Interacts with the ``search`` resource of the ComicVine API. Requires an | ||
account on https://comicvine.gamespot.com/ in order to obtain an API key. | ||
''' | ||
|
||
# All API requests made by this client will be made to this URL. | ||
API_URL = 'https://www.comicvine.com/api/search/' | ||
|
||
# A valid User-Agent header must be set in order for our API requests to | ||
# be accepted, otherwise our request will be rejected with a | ||
# **403 - Forbidden** error. | ||
HEADERS = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:7.0) ' | ||
'Gecko/20130825 Firefox/36.0'} | ||
|
||
# A set of valid resource types to return in results. | ||
RESOURCE_TYPES = { | ||
'character', 'issue', 'location', 'object', 'person', 'publisher', | ||
'story_arc', 'team', 'volume', | ||
} | ||
|
||
def __init__(self, api_key, expire_after=300): | ||
''' | ||
Store the API key in a class variable, and install the requests cache, | ||
configuring it using the ``expire_after`` parameter. | ||
:param api_key: Your personal ComicVine API key. | ||
:type api_key: str | ||
:param expire_after: The number of seconds to retain an entry in cache. | ||
:type expire_after: int or None | ||
''' | ||
|
||
self.api_key = api_key | ||
self.install_requests_cache(expire_after) | ||
|
||
def install_requests_cache(self, expire_after): | ||
''' | ||
Monkey patch Requests to use requests_cache.CachedSession rather than | ||
requests.Session. Responses will have the `from_cache` attribute set | ||
to True if the value being returned is a cached value. | ||
Responses will be held in cache for the number of seconds assigned to | ||
the ``expire_after`` class variable. | ||
:param expire_after: The number of seconds to retain an entry in cache. | ||
:type expire_after: int | ||
''' | ||
|
||
requests_cache.install_cache( | ||
__name__, | ||
backend='memory', | ||
expire_after=expire_after | ||
) | ||
|
||
def search(self, query, offset=0, limit=10, resources=None, | ||
use_cache=True): | ||
''' | ||
Perform a search against the API, using the provided query term. If | ||
required, a list of resource types to filter search results to can | ||
be included. Return the JSON contained in the response. | ||
:param query: The search query with which to make the request. | ||
:type query: str | ||
:param offset: The index of the first record returned. | ||
:type offset: int or None | ||
:param limit: How many records to return **(max 10)** | ||
:type limit: int or None | ||
:param resources: A list of resources to include in the search results. | ||
:type resources: list or None | ||
:param use_cache: Toggle the use of requests_cache. | ||
:type use_cache: bool | ||
:return: The JSON contained in the HTTP response following the search. | ||
:rtype: dict | ||
''' | ||
|
||
params = self.request_params(query, offset, limit, resources) | ||
response = self.query_api(params, use_cache=use_cache) | ||
|
||
return response.json() | ||
|
||
def request_params(self, query, offset, limit, resources): | ||
''' | ||
Construct a dict containing the required key-value pairs of parameters | ||
required in order to make the API request. | ||
The documentation for the ``search`` resource can be found at | ||
https://comicvine.gamespot.com/api/documentation#toc-0-30. | ||
Regarding 'limit', as per the documentation: | ||
The number of results to display per page. This value defaults to | ||
10 and can not exceed this number. | ||
:param query: The search query with which to make the request. | ||
:type query: str | ||
:param offset: The index of the first record returned. | ||
:type offset: int | ||
:param limit: How many records to return **(max 10)** | ||
:type limit: int | ||
:param resources: A list of resources to include in the search results. | ||
:type resources: list or None | ||
:return: A dictionary of request parameters. | ||
:rtype: dict | ||
''' | ||
|
||
return {'api_key': self.api_key, | ||
'format': 'json', | ||
'limit': min(10, limit), | ||
'offset': max(0, offset), | ||
'query': query, | ||
'resources': self.validate_resources(resources)} | ||
|
||
def validate_resources(self, resources): | ||
''' | ||
Provided a list of resources, first convert it to a set and perform an | ||
intersection with the set of valid resource types, ``RESOURCE_TYPES``. | ||
Return a comma-separted string of the remaining valid resources, or | ||
None if the set is empty. | ||
:param resources: A list of resources to include in the search results. | ||
:type resources: list or None | ||
:return: A comma-separated string of valid resources. | ||
:rtype: str or None | ||
''' | ||
|
||
if not resources: | ||
return None | ||
|
||
valid_resources = self.RESOURCE_TYPES & set(resources) | ||
return ','.join(valid_resources) if valid_resources else None | ||
|
||
def query_api(self, params, use_cache): | ||
''' | ||
Query the ComicVine API's ``search`` resource, providing the required | ||
headers and parameters with the request. Optionally allow the caller | ||
of the function to specify *not* to use the request cache. | ||
:param params: Parameters to include with the request. | ||
:type params: dict | ||
:param use_cache: Toggle the use of requests_cache. | ||
:type use_cache: bool | ||
:return: The requests.Response object returned by the HTTP request. | ||
:rtype: requests.Response | ||
''' | ||
|
||
# Since we're performing the identical action regardless of whether | ||
# or not the request cache is to be used, store the procedure in a | ||
# local function to avoid repetition. | ||
def __httpget(): | ||
response = requests.get( | ||
self.API_URL, headers=self.HEADERS, params=params) | ||
|
||
if not response.ok: | ||
self.handle_http_error(response) | ||
|
||
return response | ||
|
||
# To disable the use of the request cache, we simply must make our | ||
# HTTP requests from within the `requests_cache.disabled` context. | ||
if not use_cache: | ||
with requests_cache.disabled(): | ||
return __httpget() | ||
|
||
return __httpget() | ||
|
||
def handle_http_error(self, response): | ||
''' | ||
Given a response to an HTTP request, represented by a | ||
``requests.Response`` object, if the response's status code is | ||
anything other than **200**, we will treat it as an error. | ||
Using the response's status code, determine which type of exception to | ||
raise. Construct an exception message from the response's status code | ||
and reason properties before raising the exception. | ||
:param response: The requests.Response object returned by the HTTP | ||
request. | ||
:type response: requests.Response | ||
:raises ComicVineUnauthorizedException: if no API key provided. | ||
:raises ComicVineForbiddenException: if no User-Agent header provided. | ||
:raises ComicVineApiException: if an unidentified error occurs. | ||
''' | ||
|
||
exception = { | ||
401: ComicVineUnauthorizedError, | ||
403: ComicVineForbiddenError | ||
}.get(response.status_code, ComicVineApiError) | ||
message = f'{response.status_code} {response.reason}' | ||
|
||
raise exception(message) |