1# This file is part of PRAW. 2# 3# PRAW is free software: you can redistribute it and/or modify it under the 4# terms of the GNU General Public License as published by the Free Software 5# Foundation, either version 3 of the License, or (at your option) any later 6# version. 7# 8# PRAW is distributed in the hope that it will be useful, but WITHOUT ANY 9# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 10# A PARTICULAR PURPOSE. See the GNU General Public License for more details. 11# 12# You should have received a copy of the GNU General Public License along with 13# PRAW. If not, see <http://www.gnu.org/licenses/>. 14 15""" 16Error classes. 17 18Includes two main exceptions: ClientException, when something goes 19wrong on our end, and APIExeception for when something goes wrong on the 20server side. A number of classes extend these two main exceptions for more 21specific exceptions. 22""" 23 24from __future__ import print_function, unicode_literals 25 26import inspect 27import six 28import sys 29 30 31class PRAWException(Exception): 32 """The base PRAW Exception class. 33 34 Ideally, this can be caught to handle any exception from PRAW. 35 36 """ 37 38 39class ClientException(PRAWException): 40 """Base exception class for errors that don't involve the remote API.""" 41 42 def __init__(self, message=None): 43 """Construct a ClientException. 44 45 :param message: The error message to display. 46 47 """ 48 if not message: 49 message = 'Clientside error' 50 super(ClientException, self).__init__() 51 self.message = message 52 53 def __str__(self): 54 """Return the message of the error.""" 55 return self.message 56 57 58class OAuthScopeRequired(ClientException): 59 """Indicates that an OAuth2 scope is required to make the function call. 60 61 The attribute `scope` will contain the name of the necessary scope. 62 63 """ 64 65 def __init__(self, function, scope, message=None): 66 """Contruct an OAuthScopeRequiredClientException. 67 68 :param function: The function that requires a scope. 69 :param scope: The scope required for the function. 70 :param message: A custom message to associate with the 71 exception. Default: `function` requires the OAuth2 scope `scope` 72 73 """ 74 if not message: 75 message = '`{0}` requires the OAuth2 scope `{1}`'.format(function, 76 scope) 77 super(OAuthScopeRequired, self).__init__(message) 78 self.scope = scope 79 80 81class LoginRequired(ClientException): 82 """Indicates that a logged in session is required. 83 84 This exception is raised on a preemptive basis, whereas NotLoggedIn occurs 85 in response to a lack of credentials on a privileged API call. 86 87 """ 88 89 def __init__(self, function, message=None): 90 """Construct a LoginRequired exception. 91 92 :param function: The function that requires login-based authentication. 93 :param message: A custom message to associate with the exception. 94 Default: `function` requires a logged in session 95 96 """ 97 if not message: 98 message = '`{0}` requires a logged in session'.format(function) 99 super(LoginRequired, self).__init__(message) 100 101 102class LoginOrScopeRequired(OAuthScopeRequired, LoginRequired): 103 """Indicates that either a logged in session or OAuth2 scope is required. 104 105 The attribute `scope` will contain the name of the necessary scope. 106 107 """ 108 109 def __init__(self, function, scope, message=None): 110 """Construct a LoginOrScopeRequired exception. 111 112 :param function: The function that requires authentication. 113 :param scope: The scope that is required if not logged in. 114 :param message: A custom message to associate with the exception. 115 Default: `function` requires a logged in session or the OAuth2 116 scope `scope` 117 118 """ 119 if not message: 120 message = ('`{0}` requires a logged in session or the ' 121 'OAuth2 scope `{1}`').format(function, scope) 122 super(LoginOrScopeRequired, self).__init__(function, scope, message) 123 124 125class ModeratorRequired(LoginRequired): 126 """Indicates that a moderator of the subreddit is required.""" 127 128 def __init__(self, function): 129 """Construct a ModeratorRequired exception. 130 131 :param function: The function that requires moderator access. 132 133 """ 134 message = ('`{0}` requires a moderator ' 135 'of the subreddit').format(function) 136 super(ModeratorRequired, self).__init__(message) 137 138 139class ModeratorOrScopeRequired(LoginOrScopeRequired, ModeratorRequired): 140 """Indicates that a moderator of the sub or OAuth2 scope is required. 141 142 The attribute `scope` will contain the name of the necessary scope. 143 144 """ 145 146 def __init__(self, function, scope): 147 """Construct a ModeratorOrScopeRequired exception. 148 149 :param function: The function that requires moderator authentication or 150 a moderator scope.. 151 :param scope: The scope that is required if not logged in with 152 moderator access.. 153 154 """ 155 message = ('`{0}` requires a moderator of the subreddit or the ' 156 'OAuth2 scope `{1}`').format(function, scope) 157 super(ModeratorOrScopeRequired, self).__init__(function, scope, 158 message) 159 160 161class OAuthAppRequired(ClientException): 162 """Raised when an OAuth client cannot be initialized. 163 164 This occurs when any one of the OAuth config values are not set. 165 166 """ 167 168 169class HTTPException(PRAWException): 170 """Base class for HTTP related exceptions.""" 171 172 def __init__(self, _raw, message=None): 173 """Construct a HTTPException. 174 175 :params _raw: The internal request library response object. This object 176 is mapped to attribute `_raw` whose format may change at any time. 177 178 """ 179 if not message: 180 message = 'HTTP error' 181 super(HTTPException, self).__init__() 182 self._raw = _raw 183 self.message = message 184 185 def __str__(self): 186 """Return the message of the error.""" 187 return self.message 188 189 190class Forbidden(HTTPException): 191 """Raised when the user does not have permission to the entity.""" 192 193 194class NotFound(HTTPException): 195 """Raised when the requested entity is not found.""" 196 197 198class InvalidComment(PRAWException): 199 """Indicate that the comment is no longer available on reddit.""" 200 201 ERROR_TYPE = 'DELETED_COMMENT' 202 203 def __str__(self): 204 """Return the message of the error.""" 205 return self.ERROR_TYPE 206 207 208class InvalidSubmission(PRAWException): 209 """Indicates that the submission is no longer available on reddit.""" 210 211 ERROR_TYPE = 'DELETED_LINK' 212 213 def __str__(self): 214 """Return the message of the error.""" 215 return self.ERROR_TYPE 216 217 218class InvalidSubreddit(PRAWException): 219 """Indicates that an invalid subreddit name was supplied.""" 220 221 ERROR_TYPE = 'SUBREDDIT_NOEXIST' 222 223 def __str__(self): 224 """Return the message of the error.""" 225 return self.ERROR_TYPE 226 227 228class RedirectException(PRAWException): 229 """Raised when a redirect response occurs that is not expected.""" 230 231 def __init__(self, request_url, response_url, message=None): 232 """Construct a RedirectException. 233 234 :param request_url: The url requested. 235 :param response_url: The url being redirected to. 236 :param message: A custom message to associate with the exception. 237 238 """ 239 if not message: 240 message = ('Unexpected redirect ' 241 'from {0} to {1}').format(request_url, response_url) 242 super(RedirectException, self).__init__() 243 self.request_url = request_url 244 self.response_url = response_url 245 self.message = message 246 247 def __str__(self): 248 """Return the message of the error.""" 249 return self.message 250 251 252class OAuthException(PRAWException): 253 """Base exception class for OAuth API calls. 254 255 Attribute `message` contains the error message. 256 Attribute `url` contains the url that resulted in the error. 257 258 """ 259 260 def __init__(self, message, url): 261 """Construct a OAuthException. 262 263 :param message: The message associated with the exception. 264 :param url: The url that resulted in error. 265 266 """ 267 super(OAuthException, self).__init__() 268 self.message = message 269 self.url = url 270 271 def __str__(self): 272 """Return the message along with the url.""" 273 return self.message + " on url {0}".format(self.url) 274 275 276class OAuthInsufficientScope(OAuthException): 277 """Raised when the current OAuth scope is not sufficient for the action. 278 279 This indicates the access token is valid, but not for the desired action. 280 281 """ 282 283 284class OAuthInvalidGrant(OAuthException): 285 """Raised when the code to retrieve access information is not valid.""" 286 287 288class OAuthInvalidToken(OAuthException): 289 """Raised when the current OAuth access token is not valid.""" 290 291 292class APIException(PRAWException): 293 """Base exception class for the reddit API error message exceptions. 294 295 All exceptions of this type should have their own subclass. 296 297 """ 298 299 def __init__(self, error_type, message, field='', response=None): 300 """Construct an APIException. 301 302 :param error_type: The error type set on reddit's end. 303 :param message: The associated message for the error. 304 :param field: The input field associated with the error, or ''. 305 :param response: The HTTP response that resulted in the exception. 306 307 """ 308 super(APIException, self).__init__() 309 self.error_type = error_type 310 self.message = message 311 self.field = field 312 self.response = response 313 314 def __str__(self): 315 """Return a string containing the error message and field.""" 316 if hasattr(self, 'ERROR_TYPE'): 317 return '`{0}` on field `{1}`'.format(self.message, self.field) 318 else: 319 return '({0}) `{1}` on field `{2}`'.format(self.error_type, 320 self.message, 321 self.field) 322 323 324class ExceptionList(APIException): 325 """Raised when more than one exception occurred.""" 326 327 def __init__(self, errors): 328 """Construct an ExceptionList. 329 330 :param errors: The list of errors. 331 332 """ 333 super(ExceptionList, self).__init__(None, None) 334 self.errors = errors 335 336 def __str__(self): 337 """Return a string representation for all the errors.""" 338 ret = '\n' 339 for i, error in enumerate(self.errors): 340 ret += '\tError {0}) {1}\n'.format(i, six.text_type(error)) 341 return ret 342 343 344class AlreadySubmitted(APIException): 345 """An exception to indicate that a URL was previously submitted.""" 346 347 ERROR_TYPE = 'ALREADY_SUB' 348 349 350class AlreadyModerator(APIException): 351 """Used to indicate that a user is already a moderator of a subreddit.""" 352 353 ERROR_TYPE = 'ALREADY_MODERATOR' 354 355 356class BadCSS(APIException): 357 """An exception to indicate bad CSS (such as invalid) was used.""" 358 359 ERROR_TYPE = 'BAD_CSS' 360 361 362class BadCSSName(APIException): 363 """An exception to indicate a bad CSS name (such as invalid) was used.""" 364 365 ERROR_TYPE = 'BAD_CSS_NAME' 366 367 368class BadUsername(APIException): 369 """An exception to indicate an invalid username was used.""" 370 371 ERROR_TYPE = 'BAD_USERNAME' 372 373 374class InvalidCaptcha(APIException): 375 """An exception for when an incorrect captcha error is returned.""" 376 377 ERROR_TYPE = 'BAD_CAPTCHA' 378 379 380class InvalidEmails(APIException): 381 """An exception for when invalid emails are provided.""" 382 383 ERROR_TYPE = 'BAD_EMAILS' 384 385 386class InvalidFlairTarget(APIException): 387 """An exception raised when an invalid user is passed as a flair target.""" 388 389 ERROR_TYPE = 'BAD_FLAIR_TARGET' 390 391 392class InvalidInvite(APIException): 393 """Raised when attempting to accept a nonexistent moderator invite.""" 394 395 ERROR_TYPE = 'NO_INVITE_FOUND' 396 397 398class InvalidUser(APIException): 399 """An exception for when a user doesn't exist.""" 400 401 ERROR_TYPE = 'USER_DOESNT_EXIST' 402 403 404class InvalidUserPass(APIException): 405 """An exception for failed logins.""" 406 407 ERROR_TYPE = 'WRONG_PASSWORD' 408 409 410class InsufficientCreddits(APIException): 411 """Raised when there are not enough creddits to complete the action.""" 412 413 ERROR_TYPE = 'INSUFFICIENT_CREDDITS' 414 415 416class NotLoggedIn(APIException): 417 """An exception for when a Reddit user isn't logged in.""" 418 419 ERROR_TYPE = 'USER_REQUIRED' 420 421 422class NotModified(APIException): 423 """An exception raised when reddit returns {'error': 304}. 424 425 This error indicates that the requested content was not modified and is 426 being requested too frequently. Such an error usually occurs when multiple 427 instances of PRAW are running concurrently or in rapid succession. 428 429 """ 430 431 def __init__(self, response): 432 """Construct an instance of the NotModified exception. 433 434 This error does not have an error_type, message, nor field. 435 436 """ 437 super(NotModified, self).__init__(None, None, response=response) 438 439 def __str__(self): 440 """Return: That page has not been modified.""" 441 return 'That page has not been modified.' 442 443 444class RateLimitExceeded(APIException): 445 """An exception for when something has happened too frequently. 446 447 Contains a `sleep_time` attribute for the number of seconds that must 448 transpire prior to the next request. 449 450 """ 451 452 ERROR_TYPE = 'RATELIMIT' 453 454 455class SubredditExists(APIException): 456 """An exception to indicate that a subreddit name is not available.""" 457 458 ERROR_TYPE = 'SUBREDDIT_EXISTS' 459 460 461class UsernameExists(APIException): 462 """An exception to indicate that a username is not available.""" 463 464 ERROR_TYPE = 'USERNAME_TAKEN' 465 466 467def _build_error_mapping(): 468 def predicate(obj): 469 return inspect.isclass(obj) and hasattr(obj, 'ERROR_TYPE') 470 471 tmp = {} 472 for _, obj in inspect.getmembers(sys.modules[__name__], predicate): 473 tmp[obj.ERROR_TYPE] = obj 474 return tmp 475ERROR_MAPPING = _build_error_mapping() 476