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