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:
parent
97164fc52c
commit
62871e3240
3 changed files with 147 additions and 75 deletions
|
@ -1 +1,2 @@
|
||||||
from .mega import Mega # noqa
|
from .mega import Mega # noqa
|
||||||
|
from . import errors
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue