def test_linked_relationship(): response = { "data": { "type": "article", "id": "1", "attributes": { "title": "Article 1" }, "relationships": { "author": { "links": { "related": "/authors/9" } } } } } doc = json_api_doc.parse(response) assert doc == { "type": "article", "id": "1", "title": "Article 1", "author": { "links": { "related": "/authors/9" } } }
def test_error(): response = { "errors": [{ "status": "404", "title": "not found", "detail": "Resource not found" }] } doc = json_api_doc.parse(response) assert doc == response
async def inner(some_session): async with some_session.get(url, headers=headers) as response: response_json = await response.json() try: return json_api_doc.parse(response_json) except Exception as e: _, log_path = tempfile.mkstemp(suffix=".log") print(f"Writing problematic API response to {log_path}") with open(log_path, "w") as file: file.write(json.dumps(response_json)) raise e
def getV3(command, params={}, raw=False): """Make a GET request against the MBTA v3 API""" api_key = os.environ.get('MBTA_V3_API_KEY', '') or secrets.MBTA_V3_API_KEY headers = {'x-api-key': api_key} if api_key else {} url = BASE_URL_V3.format(command=command, parameters=urlencode(params)) print('Requesting from url: {}'.format(url)) response = requests.get(url, headers=headers) if raw: return response.json() else: return json_api_doc.parse(response.json())
def jsonapi_validator(data): with open(settings.JSONAPI_SCHEMA_PATH, 'r') as schemafile: schema = json.load(schemafile) try: jsonschema.validate(data, schema) validated = json_api_doc.parse(data) return True, validated, [] except (jsonschema.ValidationError, AttributeError) as e: errors = [t.message for t in e.context] return False, None, errors
def test_simple_object(): response = { "data": { "type": "article", "id": "1", "attributes": { "title": "Article 1" }, } } doc = json_api_doc.parse(response) assert doc == {"type": "article", "id": "1", "title": "Article 1"}
async def inner(some_session): async with some_session.get(url, headers=headers) as response: response_json = await response.json() eastern = pytz.timezone("US/Eastern") now_eastern = datetime.datetime.now(eastern) if response.status >= 400: print( f"[{now_eastern}] API returned {response.status} for {url} -- it says {response_json}" ) try: return json_api_doc.parse(response_json) except Exception as e: _, log_path = tempfile.mkstemp(suffix=".log") print(f"[{now_eastern}] Writing problematic API response to {log_path}") with open(log_path, "w") as file: file.write(json.dumps(response_json)) raise e
def test_simple_relationships_with_meta(): response = { "data": { "type": "article", "id": "1", "attributes": { "title": "Article 1" }, "relationships": { "author": { "data": { "type": "people", "id": "9", "meta": { "index": 3 } } } } }, "included": [{ "type": "people", "id": "9", "attributes": { "first-name": "Bob", "last-name": "Doe", } }] } doc = json_api_doc.parse(response) assert doc == { "type": "article", "id": "1", "title": "Article 1", "author": { "type": "people", "id": "9", "first-name": "Bob", "last-name": "Doe", "meta": { "index": 3 } } }
def test_simple_list(): response = { "data": [{ "type": "article", "id": "1", "attributes": { "title": "Article 1" }, }, { "type": "article", "id": "2", "attributes": { "title": "Article 2" }, }] } doc = json_api_doc.parse(response) assert len(doc) == 2 assert doc[0] == {"type": "article", "id": "1", "title": "Article 1"} assert doc[1] == {"type": "article", "id": "2", "title": "Article 2"}
def test_simple_null_object(): response = {"data": None} doc = json_api_doc.parse(response) assert doc is None
def test_invalid(): with pytest.raises(AttributeError): json_api_doc.parse({"a": 1})
def test_simple_object_without_attributes(): response = {"data": {"type": "article", "id": "1"}} doc = json_api_doc.parse(response) assert doc == {"type": "article", "id": "1"}
def test_resolves_deeply_without_infinite_recursion(): response = { "data": [{ "id": "O-546755D4", "relationships": { "route": { "data": { "id": "Orange", "type": "route" } }, "trip": { "data": { "id": "45616458", "type": "trip" } } }, "type": "vehicle" }, { "id": "O-546751D5", "relationships": { "route": { "data": { "id": "Orange", "type": "route" } }, "trip": { "data": { "id": "45616586", "type": "trip" } } }, "type": "vehicle" }, { "id": "O-54675162", "relationships": { "route": { "data": { "id": "Orange", "type": "route" } }, "trip": { "data": { "id": "45616587", "type": "trip" } } }, "type": "vehicle" }], "included": [{ "id": "45616586", "relationships": { "route": { "data": { "id": "Orange", "type": "route" } }, "route_pattern": { "data": { "id": "Orange-3-1", "type": "route_pattern" } } }, "type": "trip" }, { "id": "Orange-3-1", "relationships": { "token_trip": { "data": { "id": "45616458", "type": "trip" } }, "route": { "data": { "id": "Orange", "type": "route" } } }, "type": "route_pattern" }, { "id": "45616458", "relationships": { "route": { "data": { "id": "Orange", "type": "route" } }, "route_pattern": { "data": { "id": "Orange-3-1", "type": "route_pattern" } } }, "type": "trip" }, { "id": "45616587", "relationships": { "route": { "data": { "id": "Orange", "type": "route" } }, "route_pattern": { "data": { "id": "Orange-3-1", "type": "route_pattern" } } }, "type": "trip" }] } doc = json_api_doc.parse(response) trip_id = {'id': '45616458', 'type': 'trip'} route_id = {'id': 'Orange', 'type': 'route'} route_pattern_id = {'id': 'Orange-3-1', 'type': 'route_pattern'} trip0 = doc[0]['trip'] trip1 = doc[1]['trip'] assert bool(trip0 != trip_id) assert bool(trip0['route_pattern']['route'] == route_id) assert bool(trip0['route_pattern']['token_trip'] == trip_id) assert bool(trip1['route_pattern'] != route_pattern_id) assert bool(trip1['route_pattern']['route'] == route_id) assert bool(trip1['route_pattern']['token_trip']['route'] == route_id) assert bool(trip1['route_pattern']['token_trip']['route_pattern'] == route_pattern_id)
def kitsu(user_slug_or_id): """ Retrieve a users' animelist scores from Kitsu. Only anime scored > 0 will be returned, and all PTW entries are ignored, even if they are scored. :param str user_slug_or_id: Kitsu user slug or user id :return: Mapping of ``id`` to ``score`` :rtype: dict """ if not user_slug_or_id.isdigit(): # Username is the "slug". The API is incapable of letting us pass # a slug filter to the `library-entries` endpoint, so we need to # get the user id first... # TODO: Tidy this up user_id = requests.request("GET", "https://kitsu.io/api/edge/users", params={ "filter[slug]": user_slug_or_id }).json()["data"] if not user_id: raise InvalidUserError( "User `{}` does not exist on Kitsu".format(user_slug_or_id)) user_id = user_id[0]["id"] # assume it's the first one, idk else: # Assume that if the username is all digits, then the user id is # passed so we can just send this straight into `library-entries` user_id = user_slug_or_id params = { "fields[anime]": "id,mappings", # TODO: Find a way to specify username instead of user_id. "filter[user_id]": user_id, "filter[kind]": "anime", "filter[status]": "completed,current,dropped,on_hold", "include": "anime,anime.mappings", "page[offset]": "0", "page[limit]": "500" } entries = [] next_url = ENDPOINT_URLS.KITSU while next_url: resp = requests.request("GET", next_url, params=params) # TODO: Handle invalid username, other exceptions, etc if resp.status_code == TOO_MANY_REQUESTS: # pragma: no cover raise RateLimitExceededError("Kitsu rate limit exceeded") json = resp.json() # The API silently fails if the user id is invalid, # which is a PITA, but hey... if not json["data"]: raise InvalidUserError( "User `{}` does not exist on Kitsu".format(user_slug_or_id)) entries += json_api_doc.parse(json) # HACKISH # params built into future `next_url`s, bad idea to keep existing ones params = {} next_url = json["links"].get("next") scores = {} for entry in entries: # Our request returns mappings with various services, we need # to find the MAL one to get the MAL id to use. mappings = entry["anime"]["mappings"] for mapping in mappings: if mapping["externalSite"] == "myanimelist/anime": id = mapping["externalId"] break else: # Eh, if there isn't a MAL mapping, then the entry probably # doesn't exist there. Not much we can do if that's the case... continue score = entry["ratingTwenty"] # Why does this API do `score == None` when it's not rated? # Whatever happened to 0? if score is not None: scores[id] = score if not len(scores): raise NoAffinityError( "User `{}` hasn't rated any anime on Kitsu".format( user_slug_or_id)) return scores
def kitsu(user_slug_or_id, **kws): """ Retrieve a users' animelist scores from Kitsu. Only anime scored > 0 will be returned, and all PTW entries are ignored, even if they are scored. :param str user_slug_or_id: Kitsu user slug or user id :return: Mapping of ``id`` to ``score`` :rtype: dict """ # TODO: Move this somewhere else? def get_pages(params): session = requests.Session() # Convert params dict to url string and add it onto the URL, # as the `next_url` pagination links include the updated params # already, so we want to avoid either duplicating the params, # updating the params ourselves, or clearing the params after # the first run. next_url = ENDPOINT_URLS.KITSU + "?" + urllib.parse.urlencode(params) while next_url: # Kitsu's API doesn't really need the limiting, but just in case.. time.sleep(kws.get("wait_time", 0)) resp = session.request("GET", next_url) # TODO: Handle other exceptions, etc if resp.status_code == TOO_MANY_REQUESTS: # pragma: no cover raise RateLimitExceededError("Kitsu rate limit exceeded") json = resp.json() # The API silently fails if the user id is invalid, # which is a PITA, but hey... if not json["data"]: raise InvalidUserError( "User `{}` does not exist on Kitsu".format( user_slug_or_id)) yield json next_url = json["links"].get("next") if not user_slug_or_id.isdigit(): # Username is the "slug". The API is incapable of letting us pass # a slug filter to the `library-entries` endpoint, so we need to # get the user id first... # TODO: Tidy this up user_id = requests.request("GET", "https://kitsu.io/api/edge/users", params={ "filter[slug]": user_slug_or_id }).json()["data"] if not user_id: raise InvalidUserError( "User `{}` does not exist on Kitsu".format(user_slug_or_id)) user_id = user_id[0]["id"] # assume it's the first one, idk else: # Assume that if the username is all digits, then the user id is # passed so we can just send this straight into `library-entries` user_id = user_slug_or_id params = { "fields[anime]": "id,mappings", # TODO: Find a way to specify username instead of user_id. "filter[user_id]": user_id, "filter[kind]": "anime", "filter[status]": "completed,current,dropped,on_hold", "include": "anime,anime.mappings", "page[offset]": "0", "page[limit]": "500" } scores = {} for page in get_pages(params): for entry in json_api_doc.parse(page): # Our request returns mappings with various services, we need # to find the MAL one to get the MAL id to use. for mapping in entry["anime"]["mappings"]: if mapping["externalSite"] == "myanimelist/anime": id = mapping["externalId"] break else: # Eh, if there isn't a MAL mapping, then the entry probably # doesn't exist there. Not much we can do if that's the case.. continue score = entry["ratingTwenty"] # Why does this API do `score == None` when it's not rated? # Whatever happened to 0? if score is not None: scores[id] = score if not len(scores): raise NoAffinityError( "User `{}` hasn't rated any anime on Kitsu".format( user_slug_or_id)) return scores