summaryrefslogtreecommitdiff
path: root/gamelocker/datatypes.py
blob: b1e83cfe6aec97a381bac764e4c2c7f8cfbed04b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#!/usr/bin/python
# -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
"""
gamelocker.datatypes

(internal) Classes and utility functions to map API responses to objects.
"""

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 = attr(dict, "stats")


class Participant(DataMessage):
    type_name = "participant"
    key_id = attr(str, "id")

    actor = attr(str, "actor")
    stats = attr(dict, "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(dict, "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