Exemple #1
0
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"
            }
        }
    }
Exemple #2
0
def test_error():
    response = {
        "errors": [{
            "status": "404",
            "title": "not found",
            "detail": "Resource not found"
        }]
    }
    doc = json_api_doc.parse(response)
    assert doc == response
Exemple #3
0
 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
Exemple #4
0
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())
Exemple #5
0
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
Exemple #6
0
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"}
Exemple #7
0
 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
Exemple #8
0
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
            }
        }
    }
Exemple #9
0
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"}
Exemple #10
0
def test_simple_null_object():
    response = {"data": None}
    doc = json_api_doc.parse(response)
    assert doc is None
Exemple #11
0
def test_invalid():
    with pytest.raises(AttributeError):
        json_api_doc.parse({"a": 1})
Exemple #12
0
def test_simple_object_without_attributes():
    response = {"data": {"type": "article", "id": "1"}}
    doc = json_api_doc.parse(response)
    assert doc == {"type": "article", "id": "1"}
Exemple #13
0
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
Exemple #15
0
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