diff options
| author | schneefux <schneefux+commit@schneefux.xyz> | 2017-01-14 21:19:48 +0100 |
|---|---|---|
| committer | schneefux <schneefux+commit@schneefux.xyz> | 2017-01-14 21:19:48 +0100 |
| commit | 814c7262ed1c9863ea6106840c217a69182eced3 (patch) | |
| tree | a82a9baf1a34287080808b9404069633040bee06 /gamelocker | |
| parent | e3337188804e1521cc0a9ae72a172a71657c5f45 (diff) | |
| download | python-gamelocker-814c7262ed1c9863ea6106840c217a69182eced3.tar.gz python-gamelocker-814c7262ed1c9863ea6106840c217a69182eced3.zip | |
rewrite API to parse with xamoom-janus
Diffstat (limited to 'gamelocker')
| -rw-r--r-- | gamelocker/__init__.py | 1 | ||||
| -rw-r--r-- | gamelocker/api.py | 376 | ||||
| -rw-r--r-- | gamelocker/datatypes.py | 177 |
3 files changed, 245 insertions, 309 deletions
diff --git a/gamelocker/__init__.py b/gamelocker/__init__.py index c6798dd..acd0bd8 100644 --- a/gamelocker/__init__.py +++ b/gamelocker/__init__.py @@ -8,3 +8,4 @@ Dead-simple Python wrapper for the Gamelocker API. """ from .api import * +from .janus import * diff --git a/gamelocker/api.py b/gamelocker/api.py index 4595ce5..dee307d 100644 --- a/gamelocker/api.py +++ b/gamelocker/api.py @@ -1,6 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# pylint: disable=too-few-public-methods,fixme # TODO: generate documentation """ @@ -9,310 +8,10 @@ requests.api This module implements the Gamelocker API. """ +import inspect import requests - - -class Utils(object): - """Utility functions.""" - - def search_dict(self, dic, attr, val): - """Find a key-value pair recursively in a dictionary. - - :param dic: Dictionary to search. - :type dic: dict - :param attr: Key to look for. - :type attr: str - :param val: Value the key needs to have. - :param val: object - :returns: (bool) Whether the dictionary contains the pair. - :rtype: bool - """ - for key in dic: - if isinstance(dic[key], dict): - if self.search_dict(dic[key], attr, val): - return True - else: - if key == attr and dic[key] == val: - return True - - return False - - -class Response(object): - """A generic API response object. - - :param errors: `errors` as returned by the API. - :param data: `errors` as returned by the API. - :param links: `links` as returned by the API. - :param included: `included` as returned by the API. - :param raw: Raw response. - :type raw: dict - """ - - def __init__(self, data): - """Constructs a :class:`Response <Response>`. - - :param data: Dictionary from API response JSON. - :type data: dict - :return: :class:`Response <Response>` object - :rtype: gamelocker.Response - """ - if "errors" in data: - self.errors = data["errors"] - else: - self.errors = None - if "data" in data: - self.data = data["data"] - else: - self.data = None - if "links" in data: - self.links = data["links"] - else: - self.links = None - if "included" in data: - self.included = data["included"] - else: - self.included = None - self.raw = data - if self.errors: - raise AttributeError( - "API returned errors: {errors}".format( - errors=repr(self.errors))) - - def filter(self, fid, ftype): - """Returns an element from `data` matching the criteria. - - :param fid: ID to look for. - :type fid: str - :param ftype: Obect type to look for. - :type ftype: str - :return: Element matching the filter criteria - :rtype: dict - """ - for include in self.included: - if include["id"] == fid and include["type"] == ftype: - return include - for datum in self.data: - if datum["id"] == fid and datum["type"] == ftype: - return datum - - return None - - -# TODO: implement a Matches.players() -class Matches(object): - """A collection of :class:`Match <Match>` objects. - - :param matches: A list of matches. - :type matches: list - :param length: The number of matches. - :type length: int - """ - - def __init__(self, data, matches=None): - """Constructs a :class:`Matches <Matches>`. - - :param data: API data. - :type data: gamelocker.Response - :param matches: (optional) List of matches to construct - collection from. Overrides `data` matches. - :type matches: list of :class:`Matches` - :return: :class:`Matches <Matches>` object - :rtype: gamelocker.Matches - """ - self._data = data - self.matches = [] - if matches is not None: - self.matches = matches - else: - for match in self._data.data: - self.matches.append(Match(self._data, match["id"])) - self.length = len(self.matches) - - def __getitem__(self, key): - return self.matches[key] - - def where(self, attribute, value): - """Searches for matches where the condition is met. - - :param attribute: Attribute to look for. - :type attribute: str - :param value: Requested value for that attribute. - :type value: str, int, object - :return: Collection of :class:`Match` matching the criteria. - :rtype: gamelocker.Matches - """ - matches = [] - for match in self.matches: - if match.has(attribute, value): - matches.append(match) - return Matches(self._data, matches) - - -class Match(object): - """A Match record. - - :param attributes: `attributes` as returned by the API. - :param rosters: List of :class:`Roster <Roster>` objects - related to the match. - :type rosters: list - """ - - def __init__(self, data, mid): - """Constructs a :class:`Match <Match>` from a :class:`Response`. - - :param data: API data. - :type data: gamelocker.Response - :param mid: Match ID. - :type mid: str - :return: :class:`Match <Match>` object - :rtype: gamelocker.Match - """ - self._data = data - self._id = mid - match = self._data.filter(mid, "match") - self.attributes = match["attributes"] - self.rosters = [Roster(self._data, r["id"]) - for r in match["relationships"]["rosters"]["data"]] - - # TODO refactor duplicated code - def has(self, attribute, value, recurse=True): - """Checks whether Match or (optionally) a child owns an `attribute` - with `value`. - - :param attribute: Attribute to look for. - :type attribute: str - :param value: Requested value for that attribute. - :type value: str, int, object - :param recurse: Whether to search relationships or not. - :type recurse: bool - :return: bool - :rtype: bool - """ - if Utils().search_dict(self.attributes, attribute, value): - return True - if recurse: - for roster in self.rosters: - if roster.has(attribute, value): - return True - - return False - - -class Roster(object): - """A Roster object. - - :param attributes: `attributes` as returned by the API. - :param participants: List of :class:`Participant <Participant>` objects - related to the roster. - :type rosters: list - """ - - def __init__(self, data, rid): - """Constructs a specified :class:`Roster <Roster>` from a :class:`Response`. - - :param data: API data. - :type data: gamelocker.Response - :param rid: Roster ID. - :type rid: str - :return: :class:`Roster <Roster>` object - :rtype: gamelocker.Roster - """ - self._data = data - self._id = rid - roster = self._data.filter(rid, "roster") - self.attributes = roster["attributes"] - self.participants = [ - Participant(self._data, p["id"]) - for p in roster["relationships"]["participants"]["data"] - ] - - def has(self, attribute, value, recurse=True): - """Checks whether Roster or (optionally) a child owns an `attribute` - with `value`. - - :param attribute: Attribute to look for. - :type attribute: str - :param value: Requested value for that attribute. - :type value: str, int, object - :param recurse: Whether to search relationships or not. - :type recurse: bool - :return: bool - :rtype: bool - """ - if Utils().search_dict(self.attributes, attribute, value): - return True - if recurse: - for participant in self.participants: - if participant.has(attribute, value): - return True - - return False - - -class Participant(object): - """A Participant object. - - :param attributes: `attributes` as returned by the API. - :param player: :class:`Player <Player>` object related to the participant. - :type player: gamelocker.Player - """ - - def __init__(self, data, pid): - """Constructs a specified :class:`Participant <Participant>` - from a :class:`Response`. - - :param data: API data. - :type data: gamelocker.Response - :param pid: Participant ID. - :type pid: str - :return: :class:`Participant <Participant>` object - :rtype: gamelocker.Participant - """ - self._data = data - self._id = pid - participant = self._data.filter(pid, "participant") - self.attributes = participant["attributes"] - self.player = Player(self._data, - participant["relationships"]["player"] - ["data"]["id"]) - - def has(self, attribute, value): - """Checks whether Participant or (optionally) owns an `attribute` - with `value`. - - :param attribute: Attribute to look for. - :type attribute: str - :param value: Requested value for that attribute. - :type value: str, int, object - :return: bool - :rtype: bool - """ - if Utils().search_dict(self.attributes, attribute, value): - return True - return False - - -class Player(object): - """A Player object. - - :param attributes: `attributes` as returned by the API. - """ - - def __init__(self, data, pid): - """Constructs a specified :class:`Player <Player>` from a :class:`Response`. - - :param data: API data. - :type data: gamelocker.Response - :param pid: Player ID. - :type pid: str - :return: :class:`Player <Player>` object - :rtype: gamelocker.Player - """ - self._data = data - self._id = pid - player = self._data.filter(pid, "player") - self.attributes = player["attributes"] +import requests_jwt +import gamelocker.datatypes class Gamelocker(object): @@ -349,7 +48,9 @@ class Gamelocker(object): """Sends a GET request to the API endpoint. :param method: Method to query. + :type method: str :param params: (optional) Parameters to send. + :type params: dict :return: Parsed JSON object. :rtype: dict """ @@ -362,7 +63,44 @@ class Gamelocker(object): headers=headers, params=params) http.raise_for_status() - return Response(http.json()) + return http.json() + + def _get(self, endpoint, elid="", params=None): + """Returns an object or a list of objects from the API. + + :param endpoint: API slug to use. + :type endpoint: str + :param elid: (optional) ID of the object to query for. + :type elid: str + :param params: (optional) Parameters to pass with the http request. + :type params: dict + :return: Data object. + :rtype: :class:`janus.DataMessage` + """ + data = self._req(endpoint + "/" + elid, params=params) + + # collect related data + includes = [] + if "included" in data: + for incl in data["included"]: + element = gamelocker.datatypes.data_to_object(incl) + includes.append(element) + + # main data object + if isinstance(data["data"], (list, tuple)): + elements = [] + for dat in data["data"]: + element = gamelocker.datatypes.data_to_object(dat) + # link related data + element = gamelocker.datatypes.link_to_object( + element, includes) + elements.append(element) + return elements + else: + element = gamelocker.datatypes.data_to_object(data["data"]) + # link related data + element = gamelocker.datatypes.link_to_object(element, includes) + return element def vainglory(self): """Sets title to Vainglory. @@ -374,12 +112,32 @@ class Gamelocker(object): return self def status(self): - """Returns the API version. + """Returns the API status JSON string. - :return: API version. + :return: API status JSON. :rtype: str """ - return self._req("status").raw["version"] + return self._req("status") + + def match(self, elid): + """Returns a match. + + :param elid: ID of the match. + :type elid: str + :return: A match with the given ID. + :rtype: :class:`Match` + """ + return self._get("matches", elid) + + def player(self, elid): + """Returns a player. + + :param elid: ID of the player. + :type elid: str + :return: A player with the given ID. + :rtype: :class:`Player` + """ + return self._get("players", elid) def matches(self, limit=None, offset=None, sort=None): """Returns a list of recent matches. @@ -401,4 +159,4 @@ class Gamelocker(object): params["page[offset]"] = offset if sort: # TODO make this nice and usable params["sort"] = sort - return Matches(self._req("matches", params)) + return self._get("matches", params=params) diff --git a/gamelocker/datatypes.py b/gamelocker/datatypes.py new file mode 100644 index 0000000..92848ad --- /dev/null +++ b/gamelocker/datatypes.py @@ -0,0 +1,177 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# pylint: disable=missing-docstring + +import inspect +import sys +from gamelocker.janus import DataMessage, Attribute + + +def attr(typ, key, relation=False): + """Convenience function to specify an attribute or a relation. + + :param typ: Allowed data type or class. + :type typ: object + :param key: Object name, mapping name and key mapping name. + :type key: str + :param link: (optional) Whether this is a relation. + :return: `DataMessage` `Attribute` class. + :rtype: :class:`Attribute` + """ + if relation: + return Attribute(value_type=typ, name=key, + mapping=key, key_mapping=key) + else: + return Attribute(value_type=typ, name=key, + mapping=key) + + +def rel(typ, key): + """Convenience function to specify a relation. + See `attr()`. + """ + return attr(typ, key, True) + + +class Player(DataMessage): + type_name = "player" + key_id = attr(str, "id") + + name = attr(str, "name") + stats = rel(dict, "stats") + + +class Participant(DataMessage): + type_name = "participant" + key_id = attr(str, "id") + + actor = attr(str, "actor") + stats = attr(str, "stats") + + player = rel(Player, "player") + + +class Team(DataMessage): + type_name = "team" + key_id = attr(str, "id") + + name = rel(str, "name") + + +class Roster(DataMessage): + type_name = "roster" + key_id = attr(str, "id") + + stats = attr(str, "stats") + + participants = rel(Participant, "participants") + team = rel(Team, "team") + + +class Match(DataMessage): + type_name = "match" + key_id = attr(str, "id") + + createdAt = attr(str, "createdAt") + duration = attr(int, "duration") + gameMode = attr(str, "gameMode") + patchVersion = attr(str, "patchVersion") + region = attr(str, "region") + stats = attr(dict, "stats") + + rosters = rel(Roster, "rosters") + + +def modulemap(): + """Returns a dictionary where `type_name`s are mapped + to corresponding classes from `gamelocker.datatypes`. + + :return: A dictionary with keys of `type_name` + and values of :class:`DataMessage` subclasses. + :rtype: :class:`gamelocker.janus.DataMessage` + """ + typemap = dict() + classes = inspect.getmembers(sys.modules[__name__], inspect.isclass) + for name, value in classes: + if name == "Attribute" or name == "DataMessage": + continue + typemap[value.type_name] = value + + return typemap + + +def data_to_object(data): + """Given a data dictionary from the API, returns mapped objects or None. + + :param data: Dictionary from API request. + :type data: dict + :return: An object. + :rtype: :class:`gamelocker.janus.DataMessage` + """ + # find the appropriate class from /datatypes.py + typemap = modulemap() + datatype = None + for key, dataclass in typemap.items(): + if key == data["type"]: + datatype = dataclass + if datatype is None: + raise NotImplementedError( + "Data type " + data["type"] + " is not implemented yet.") + + # load data into class + element = datatype() + element.map_message(data) + + return element + + +def link_to_object(obj, relations): + """Replaces references in `obj` + by their corresponding objects from `relations`. + + :param obj: Master object to populate. + :type obj: :class:`gamelocker.janus.DataMessage` + :param relations: Object pool to populate from. + :type obj: list of :class:`gamelocker.janus.DataMessage` + :return: The populated master object. + :rtype: :class:`gamelocker.janus.DataMessage` + """ + for att in dir(obj): + # loop through all relations that object could possibly have + if not(isinstance(object.__getattribute__(obj, att), Attribute) and + issubclass( + object.__getattribute__(obj, att).value_type, DataMessage) + and not att.startswith("__")): + continue + + # try to get the objects that are being linked to + children = object.__getattribute__(obj, att).value + if children is None: + # there is no reference -> master class has object + # but it was not populated with data + # so it is (hopefully) optional. + continue + + # can either be a reference or a list of references + if isinstance(children, (list, tuple)): + newlist = [] + for child in children: + # replaces every element + for relation in relations: + # see below + if relation.id == child.id: + relation = link_to_object(relation, relations) + newlist.append(relation) + break + object.__setattr__(obj, att, newlist) + else: + for relation in relations: + if relation.id == children.id: + # replace unpopulated relation + # with object from `relations` that has data + # also, replace all references in that object (recursively) + relation = link_to_object(relation, relations) + object.__setattr__(obj, att, relation) + break + + return obj |
