Add separate classes for API exceptions, raise error_for_code().

This makes the caller's life easier. They can catch
mega.errors.EFAILED instead of catching mega.errors.RequestError
and checking the message attribute.
Because these classes inherit from RequestError, and they have a code
and message attribute, anyone currently catching RequestError should
not have any backwards compatibility issues.
Furthermore, this fixes an existing issue in the codebase where
RequestError is raised with a custom string message, which was causing
IndexError since that message wasn't in CODE_TO_DESCRIPTIONS.
This commit is contained in:
Ethan Dalool 2020-03-23 18:54:29 -07:00
parent 97164fc52c
commit 62871e3240
3 changed files with 147 additions and 75 deletions

View file

@ -1 +1,2 @@
from .mega import Mega # noqa from .mega import Mega # noqa
from . import errors

View file

@ -5,83 +5,154 @@ class ValidationError(Exception):
pass pass
_CODE_TO_DESCRIPTIONS = {
-1: (
'EINTERNAL',
(
'An internal error has occurred. Please submit a bug report, '
'detailing the exact circumstances in which this error occurred'
)
),
-2: ('EARGS', 'You have passed invalid arguments to this command'),
-3: (
'EAGAIN',
(
'(always at the request level) A temporary congestion or server '
'malfunction prevented your request from being processed. '
'No data was altered. Retry. Retries must be spaced with '
'exponential backoff'
)
),
-4: (
'ERATELIMIT',
(
'You have exceeded your command weight per time quota. Please '
'wait a few seconds, then try again (this should never happen '
'in sane real-life applications)'
)
),
-5: ('EFAILED', 'The upload failed. Please restart it from scratch'),
-6: (
'ETOOMANY',
'Too many concurrent IP addresses are accessing this upload target URL'
),
-7: (
'ERANGE',
(
'The upload file packet is out of range or not starting and '
'ending on a chunk boundary'
)
),
-8: (
'EEXPIRED',
(
'The upload target URL you are trying to access has expired. '
'Please request a fresh one'
)
),
-9: ('ENOENT', 'Object (typically, node or user) not found'),
-10: ('ECIRCULAR', 'Circular linkage attempted'),
-11: (
'EACCESS',
'Access violation (e.g., trying to write to a read-only share)'
),
-12: ('EEXIST', 'Trying to create an object that already exists'),
-13: ('EINCOMPLETE', 'Trying to access an incomplete resource'),
-14: ('EKEY', 'A decryption operation failed (never returned by the API)'),
-15: ('ESID', 'Invalid or expired user session, please relogin'),
-16: ('EBLOCKED', 'User blocked'),
-17: ('EOVERQUOTA', 'Request over quota'),
-18: (
'ETEMPUNAVAIL',
'Resource temporarily not available, please try again later'
),
-19: ('ETOOMANYCONNECTIONS', 'many connections on this resource'),
-20: ('EWRITE', 'Write failed'),
-21: ('EREAD', 'Read failed'),
-22: ('EAPPKEY', 'Invalid application key; request not processed'),
}
class RequestError(Exception): class RequestError(Exception):
""" """
Error in API request Error in API request
""" """
def __init__(self, message): def __init__(self, message=None):
code = message # If you need to raise a generic RequestError with a custom message,
self.code = code # use this constructor. Otherwise you can use error_for_code.
code_desc, long_desc = _CODE_TO_DESCRIPTIONS[code] if message is not None:
self.message = f'{code_desc}, {long_desc}' self.message = message
def __str__(self): def __str__(self):
return self.message return self.message
class EINTERNAL(RequestError):
code = -1
message = (
'An internal error has occurred. Please submit a bug report, detailing '
'the exact circumstances in which this error occurred'
)
class EARGS(RequestError):
code = -2
message = 'You have passed invalid arguments to this command'
class EAGAIN(RequestError):
code = -3
message = (
'(always at the request level) A temporary congestion or server '
'malfunction prevented your request from being processed. No data was '
'altered. Retry. Retries must be spaced with exponential backoff'
)
class ERATELIMIT(RequestError):
code = -4
message = (
'You have exceeded your command weight per time quota. Please wait a '
'few seconds, then try again (this should never happen in sane '
'real-life applications)'
)
class EFAILED(RequestError):
code = -5
message = 'The upload failed. Please restart it from scratch'
class ETOOMANY(RequestError):
code = -6
message = (
'Too many concurrent IP addresses are accessing this upload target URL'
)
class ERANGE(RequestError):
code = -7
message = (
'The upload file packet is out of range or not starting and ending on '
'a chunk boundary'
)
class EEXPIRED(RequestError):
code = -8
message = (
'The upload target URL you are trying to access has expired. Please '
'request a fresh one'
)
class ENOENT(RequestError):
code = -9
message = 'Object (typically, node or user) not found'
class ECIRCULAR(RequestError):
code = -10
message = 'Circular linkage attempted'
class EACCESS(RequestError):
code = -11
message = 'Access violation (e.g., trying to write to a read-only share)'
class EEXIST(RequestError):
code = -12
message = 'Trying to create an object that already exists'
class EINCOMPLETE(RequestError):
code = -13
message = 'Trying to access an incomplete resource'
class EKEY(RequestError):
code = -14
message = 'A decryption operation failed (never returned by the API)'
class ESID(RequestError):
code = -15
message = 'Invalid or expired user session, please relogin'
class EBLOCKED(RequestError):
code = -16
message = 'User blocked'
class EOVERQUOTA(RequestError):
code = -17
message = 'Request over quota'
class ETEMPUNAVAIL(RequestError):
code = -18
message = 'Resource temporarily not available, please try again later'
class ETOOMANYCONNECTIONS(RequestError):
code = -19
message = 'many connections on this resource'
class EWRITE(RequestError):
code = -20
message = 'Write failed'
class EREAD(RequestError):
code = -21
message = 'Read failed'
class EAPPKEY(RequestError):
code = -22
message = 'Invalid application key; request not processed'
_CODE_TO_CLASSES = {
-1: EINTERNAL,
-2: EARGS,
-3: EAGAIN,
-4: ERATELIMIT,
-5: EFAILED,
-6: ETOOMANY,
-7: ERANGE,
-8: EEXPIRED,
-9: ENOENT,
-10: ECIRCULAR,
-11: EACCESS,
-12: EEXIST,
-13: EINCOMPLETE,
-14: EKEY,
-15: ESID,
-16: EBLOCKED,
-17: EOVERQUOTA,
-18: ETEMPUNAVAIL,
-19: ETOOMANYCONNECTIONS,
-20: EWRITE,
-21: EREAD,
-22: EAPPKEY,
}
def error_for_code(code):
cls = _CODE_TO_CLASSES[code]
return cls()

View file

@ -17,7 +17,7 @@ import shutil
import requests import requests
from tenacity import retry, wait_exponential, retry_if_exception_type from tenacity import retry, wait_exponential, retry_if_exception_type
from .errors import ValidationError, RequestError from .errors import ValidationError, RequestError, error_for_code
from .crypto import ( from .crypto import (
a32_to_base64, encrypt_key, base64_url_encode, encrypt_attr, base64_to_a32, a32_to_base64, encrypt_key, base64_url_encode, encrypt_attr, base64_to_a32,
base64_url_decode, decrypt_attr, a32_to_str, get_chunks, str_to_a32, base64_url_decode, decrypt_attr, a32_to_str, get_chunks, str_to_a32,
@ -186,7 +186,7 @@ class Mega:
msg = 'Request failed, retrying' msg = 'Request failed, retrying'
logger.info(msg) logger.info(msg)
raise RuntimeError(msg) raise RuntimeError(msg)
raise RequestError(json_resp) raise error_for_code(json_resp)
return json_resp[0] return json_resp[0]
def _parse_url(self, url): def _parse_url(self, url):