1# -*- coding: utf-8 -*-
2
3############################ Copyrights and license ############################
4#                                                                              #
5# Copyright 2012 Vincent Jacques <vincent@vincent-jacques.net>                 #
6# Copyright 2012 Zearin <zearin@gonk.net>                                      #
7# Copyright 2013 AKFish <akfish@gmail.com>                                     #
8# Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net>                 #
9# Copyright 2014 Andrew Scheller <github@loowis.durge.org>                     #
10# Copyright 2014 Vincent Jacques <vincent@vincent-jacques.net>                 #
11# Copyright 2016 Jakub Wilk <jwilk@jwilk.net>                                  #
12# Copyright 2016 Jannis Gebauer <ja.geb@me.com>                                #
13# Copyright 2016 Peter Buckley <dx-pbuckley@users.noreply.github.com>          #
14# Copyright 2016 Sam Corbett <sam.corbett@cloudsoftcorp.com>                   #
15# Copyright 2018 sfdye <tsfdye@gmail.com>                                      #
16#                                                                              #
17# This file is part of PyGithub.                                               #
18# http://pygithub.readthedocs.io/                                              #
19#                                                                              #
20# PyGithub is free software: you can redistribute it and/or modify it under    #
21# the terms of the GNU Lesser General Public License as published by the Free  #
22# Software Foundation, either version 3 of the License, or (at your option)    #
23# any later version.                                                           #
24#                                                                              #
25# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY  #
26# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    #
27# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
28# details.                                                                     #
29#                                                                              #
30# You should have received a copy of the GNU Lesser General Public License     #
31# along with PyGithub. If not, see <http://www.gnu.org/licenses/>.             #
32#                                                                              #
33################################################################################
34
35import datetime
36from operator import itemgetter
37
38from . import Consts, GithubException
39
40
41class _NotSetType:
42    def __repr__(self):
43        return "NotSet"
44
45    value = None
46
47
48NotSet = _NotSetType()
49
50
51class _ValuedAttribute:
52    def __init__(self, value):
53        self.value = value
54
55
56class _BadAttribute:
57    def __init__(self, value, expectedType, exception=None):
58        self.__value = value
59        self.__expectedType = expectedType
60        self.__exception = exception
61
62    @property
63    def value(self):
64        raise GithubException.BadAttributeException(
65            self.__value, self.__expectedType, self.__exception
66        )
67
68
69class GithubObject(object):
70    """
71    Base class for all classes representing objects returned by the API.
72    """
73
74    """
75    A global debug flag to enable header validation by requester for all objects
76    """
77    CHECK_AFTER_INIT_FLAG = False
78
79    @classmethod
80    def setCheckAfterInitFlag(cls, flag):
81        cls.CHECK_AFTER_INIT_FLAG = flag
82
83    def __init__(self, requester, headers, attributes, completed):
84        self._requester = requester
85        self._initAttributes()
86        self._storeAndUseAttributes(headers, attributes)
87
88        # Ask requester to do some checking, for debug and test purpose
89        # Since it's most handy to access and kinda all-knowing
90        if self.CHECK_AFTER_INIT_FLAG:  # pragma no branch (Flag always set in tests)
91            requester.check_me(self)
92
93    def _storeAndUseAttributes(self, headers, attributes):
94        # Make sure headers are assigned before calling _useAttributes
95        # (Some derived classes will use headers in _useAttributes)
96        self._headers = headers
97        self._rawData = attributes
98        self._useAttributes(attributes)
99
100    @property
101    def raw_data(self):
102        """
103        :type: dict
104        """
105        self._completeIfNeeded()
106        return self._rawData
107
108    @property
109    def raw_headers(self):
110        """
111        :type: dict
112        """
113        self._completeIfNeeded()
114        return self._headers
115
116    @staticmethod
117    def _parentUrl(url):
118        return "/".join(url.split("/")[:-1])
119
120    @staticmethod
121    def __makeSimpleAttribute(value, type):
122        if value is None or isinstance(value, type):
123            return _ValuedAttribute(value)
124        else:
125            return _BadAttribute(value, type)
126
127    @staticmethod
128    def __makeSimpleListAttribute(value, type):
129        if isinstance(value, list) and all(
130            isinstance(element, type) for element in value
131        ):
132            return _ValuedAttribute(value)
133        else:
134            return _BadAttribute(value, [type])
135
136    @staticmethod
137    def __makeTransformedAttribute(value, type, transform):
138        if value is None:
139            return _ValuedAttribute(None)
140        elif isinstance(value, type):
141            try:
142                return _ValuedAttribute(transform(value))
143            except Exception as e:
144                return _BadAttribute(value, type, e)
145        else:
146            return _BadAttribute(value, type)
147
148    @staticmethod
149    def _makeStringAttribute(value):
150        return GithubObject.__makeSimpleAttribute(value, str)
151
152    @staticmethod
153    def _makeIntAttribute(value):
154        return GithubObject.__makeSimpleAttribute(value, int)
155
156    @staticmethod
157    def _makeFloatAttribute(value):
158        return GithubObject.__makeSimpleAttribute(value, float)
159
160    @staticmethod
161    def _makeBoolAttribute(value):
162        return GithubObject.__makeSimpleAttribute(value, bool)
163
164    @staticmethod
165    def _makeDictAttribute(value):
166        return GithubObject.__makeSimpleAttribute(value, dict)
167
168    @staticmethod
169    def _makeTimestampAttribute(value):
170        return GithubObject.__makeTransformedAttribute(
171            value, int, datetime.datetime.utcfromtimestamp
172        )
173
174    @staticmethod
175    def _makeDatetimeAttribute(value):
176        def parseDatetime(s):
177            if (
178                len(s) == 24
179            ):  # pragma no branch (This branch was used only when creating a download)
180                # The Downloads API has been removed. I'm keeping this branch because I have no mean
181                # to check if it's really useless now.
182                return datetime.datetime.strptime(
183                    s, "%Y-%m-%dT%H:%M:%S.000Z"
184                )  # pragma no cover (This branch was used only when creating a download)
185            elif len(s) >= 25:
186                return datetime.datetime.strptime(s[:19], "%Y-%m-%dT%H:%M:%S") + (
187                    1 if s[19] == "-" else -1
188                ) * datetime.timedelta(hours=int(s[20:22]), minutes=int(s[23:25]))
189            else:
190                return datetime.datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ")
191
192        return GithubObject.__makeTransformedAttribute(value, str, parseDatetime)
193
194    def _makeClassAttribute(self, klass, value):
195        return GithubObject.__makeTransformedAttribute(
196            value,
197            dict,
198            lambda value: klass(self._requester, self._headers, value, completed=False),
199        )
200
201    @staticmethod
202    def _makeListOfStringsAttribute(value):
203        return GithubObject.__makeSimpleListAttribute(value, str)
204
205    @staticmethod
206    def _makeListOfIntsAttribute(value):
207        return GithubObject.__makeSimpleListAttribute(value, int)
208
209    @staticmethod
210    def _makeListOfDictsAttribute(value):
211        return GithubObject.__makeSimpleListAttribute(value, dict)
212
213    @staticmethod
214    def _makeListOfListOfStringsAttribute(value):
215        return GithubObject.__makeSimpleListAttribute(value, list)
216
217    def _makeListOfClassesAttribute(self, klass, value):
218        if isinstance(value, list) and all(
219            isinstance(element, dict) for element in value
220        ):
221            return _ValuedAttribute(
222                [
223                    klass(self._requester, self._headers, element, completed=False)
224                    for element in value
225                ]
226            )
227        else:
228            return _BadAttribute(value, [dict])
229
230    def _makeDictOfStringsToClassesAttribute(self, klass, value):
231        if isinstance(value, dict) and all(
232            isinstance(key, str) and isinstance(element, dict)
233            for key, element in value.items()
234        ):
235            return _ValuedAttribute(
236                dict(
237                    (
238                        key,
239                        klass(self._requester, self._headers, element, completed=False),
240                    )
241                    for key, element in value.items()
242                )
243            )
244        else:
245            return _BadAttribute(value, {str: dict})
246
247    @property
248    def etag(self):
249        """
250        :type: str
251        """
252        return self._headers.get(Consts.RES_ETAG)
253
254    @property
255    def last_modified(self):
256        """
257        :type: str
258        """
259        return self._headers.get(Consts.RES_LAST_MODIFIED)
260
261    def get__repr__(self, params):
262        """
263        Converts the object to a nicely printable string.
264        """
265
266        def format_params(params):
267            items = list(params.items())
268            for k, v in sorted(items, key=itemgetter(0), reverse=True):
269                if isinstance(v, bytes):
270                    v = v.decode("utf-8")
271                if isinstance(v, str):
272                    v = '"{v}"'.format(v=v)
273                yield u"{k}={v}".format(k=k, v=v)
274
275        return "{class_name}({params})".format(
276            class_name=self.__class__.__name__,
277            params=", ".join(list(format_params(params))),
278        )
279
280
281class NonCompletableGithubObject(GithubObject):
282    def _completeIfNeeded(self):
283        pass
284
285
286class CompletableGithubObject(GithubObject):
287    def __init__(self, requester, headers, attributes, completed):
288        super().__init__(requester, headers, attributes, completed)
289        self.__completed = completed
290
291    def __eq__(self, other):
292        return other.__class__ is self.__class__ and other._url.value == self._url.value
293
294    def __ne__(self, other):
295        return not self == other
296
297    def _completeIfNotSet(self, value):
298        if value is NotSet:
299            self._completeIfNeeded()
300
301    def _completeIfNeeded(self):
302        if not self.__completed:
303            self.__complete()
304
305    def __complete(self):
306        if self._url.value is None:
307            raise GithubException.IncompletableObject(
308                400, "Returned object contains no URL"
309            )
310        headers, data = self._requester.requestJsonAndCheck("GET", self._url.value)
311        self._storeAndUseAttributes(headers, data)
312        self.__completed = True
313
314    def update(self, additional_headers=None):
315        """
316        Check and update the object with conditional request
317        :rtype: Boolean value indicating whether the object is changed
318        """
319        conditionalRequestHeader = dict()
320        if self.etag is not None:
321            conditionalRequestHeader[Consts.REQ_IF_NONE_MATCH] = self.etag
322        if self.last_modified is not None:
323            conditionalRequestHeader[Consts.REQ_IF_MODIFIED_SINCE] = self.last_modified
324        if additional_headers is not None:
325            conditionalRequestHeader.update(additional_headers)
326
327        status, responseHeaders, output = self._requester.requestJson(
328            "GET", self._url.value, headers=conditionalRequestHeader
329        )
330        if status == 304:
331            return False
332        else:
333            headers, data = self._requester._Requester__check(
334                status, responseHeaders, output
335            )
336            self._storeAndUseAttributes(headers, data)
337            self.__completed = True
338            return True
339