<filename>egnyte/exc.py
"""
Exceptions and their handlers.
"""

from http import HTTPStatus

__all__ = """
EgnyteError InvalidParameters InsufficientPermissions NotFound Redirected NotAuthorized JsonParseError DomainRequired ClientIdRequired
OAuthUsernameRequired OAuthPasswordRequired UnsupportedAuthStrategy RequestError DuplicateRecordExists FileSizeExceedsLimit ChecksumError
""".split()

class EgnyteError(Exception):
    """Base class for Egnyte SDK exceptions"""

    def __str__(self):
        """Pretty-printed version. Use repr for bare vesion instead"""
        contents = []
        for item in self.args:
            if isinstance(item, dict):
                contents.append("{%s}" % ", ".join(sorted(["%s: '%s'" % (k, v) for (k, v) in item.items()])))
            else:
                contents.append(str(item))
        return "<%s: %s>" % (self.__class__.__name__, ", ".join(contents))
        return self


class InvalidParameters(EgnyteError):
    """Invalid parameters were passed to an API request"""


class InsufficientPermissions(EgnyteError):
    """User does not have sufficient permissions to perform this action"""


class NotFound(EgnyteError):
    """Resource with name does not exist"""


class Redirected(EgnyteError):
    """Received unexpected HTTP 303 response"""


class NotAuthorized(EgnyteError):
    """Access token is required"""


class JsonParseError(EgnyteError):
    """Response from the server could not be parsed properly"""


class DomainRequired(EgnyteError):
    """Domain name is required"""


class ClientIdRequired(EgnyteError):
    """Client id is required"""


class OAuthUsernameRequired(EgnyteError):
    """Username is required for OAuth authentication"""


class OAuthPasswordRequired(EgnyteError):
    """Password is required for OAuth authentication"""


class UnsupportedAuthStrategy(EgnyteError):
    """This OAuth flow is not supported by this API key"""


class RequestError(EgnyteError):
    """Other kinds of request errors"""


class FileExpected(EgnyteError):
    """
    """


class DuplicateRecordExists(EgnyteError):
    """Existing entity conflict"""


class FileSizeExceedsLimit(EgnyteError):
    """File is too large for this operation."""


class ChecksumError(EgnyteError):
    """Checksum of the uploaded file is different than checksum calculated locally - file was corrupted during transfer."""

def extract_errors(data):
    """
    Try to extract useful information from inconsistent error data structures.
    """
    if 'errors' in data:
        data = data['errors']
    if 'inputErrors' in data:
        for err in extract_errors(data['inputErrors']):
            yield err
    elif hasattr(data, 'keys'):
        if 'code' in data:
            yield data
        else:
            for value in data.values():
                for err in extract_errors(value):
                    yield err
    elif isinstance(data, list):
        for value in data:
            for err in extract_errors(value):
                yield err
    else:
        yield data


def recursive_tuple(data):
    """Convert nested lists/dicts into tuples for structural comparing"""
    if isinstance(data, (list, tuple)):
        return tuple(recursive_tuple(x) for x in data)
    elif isinstance(data, dict):
        # We cannot use plain tuple(sorted(...)) here, because while it works giving us stable sort
        # in Python 2.7, it does not in Python 3 (results in TypeError: unorderable types
        # so we'll sort dictonaries first and then turn into tuples
        return tuple((recursive_tuple(x), recursive_tuple(y)) for (x, y) in sorted(data.items()))
    return data


class ErrorMapping(dict):
    """Maps HTTP status to EgnyteError subclasses"""
    ignored_errors = ()

    def __init__(self, values=None, ok_statuses=(HTTPStatus.OK, ), ignored_errors=None):
        super(ErrorMapping, self).__init__({
            HTTPStatus.BAD_REQUEST: RequestError,
            HTTPStatus.UNAUTHORIZED: NotAuthorized,
            HTTPStatus.FORBIDDEN: InsufficientPermissions,
            HTTPStatus.NOT_FOUND: NotFound,
            HTTPStatus.CONFLICT: DuplicateRecordExists,
            HTTPStatus.REQUEST_ENTITY_TOO_LARGE: FileSizeExceedsLimit,
            HTTPStatus.SEE_OTHER: Redirected,
        })
        if values:
            self.update(values)
        if ignored_errors:
            self.ignored_errors = recursive_tuple(ignored_errors)
        self.ok_statuses = ok_statuses

    def map_error(self, response):
        return self.get(response.status_code, RequestError)

    def check_response(self, response, *ok_statuses):
        """
        Check if HTTP response has a correct status,
        try to raise a specific EgnyteError subclass if not
        """
        if not len(ok_statuses):
            ok_statuses = self.ok_statuses
        if response.status_code not in ok_statuses:
            errors = [{'url': response.url}]
            error_type = self.map_error(response)
            try:
                data = response.json()
                for err in extract_errors(data):
                    errors.append(err)
            except Exception:
                errors.append({"http response": response.text})
            errors.append({"http status": response.status_code})
            errors.append({"headers": dict(response.headers)})
            if not self.ignore_error(errors):
                raise error_type(*errors)
        return response

    def ignore_error(self, errors):
        errors = recursive_tuple(errors[1:])
        if errors and self.ignored_errors:
            result = any((errors[:len(ignored)] == ignored) for ignored in self.ignored_errors)
            return result

    def check_json_response(self, response, *ok_statuses):
        """
        Check if HTTP response has a correct status and then parse it as JSON,
        try to raise a specific EgnyteError subclass if not
        """
        try:
            r = self.check_response(response, *ok_statuses)
            if r.status_code == HTTPStatus.NO_CONTENT:
                return None
            return r.json()
        except ValueError:
            raise JsonParseError({"http response": response.text})

    def copy(self):
        """Make a copy preserving class of self"""
        return self.__class__(self)

default = ErrorMapping()
partial = ErrorMapping(ok_statuses={HTTPStatus.PARTIAL_CONTENT})
accepted = ErrorMapping(ok_statuses={HTTPStatus.ACCEPTED})
created = ErrorMapping(ok_statuses={HTTPStatus.CREATED})
no_content_ok = ErrorMapping(ok_statuses={HTTPStatus.OK, HTTPStatus.NO_CONTENT})
created_ignore_existing = ErrorMapping(ok_statuses=(HTTPStatus.CREATED,), ignored_errors = [
    (u'Folder already exists at this location', {'http status': 403})
])

<filename>egnyte/exc.py
"""
Exceptions and their handlers.
"""

from http import HTTPStatus

__all__ = """
EgnyteError InvalidParameters InsufficientPermissions NotFound Redirected NotAuthorized JsonParseError DomainRequired ClientIdRequired
OAuthUsernameRequired OAuthPasswordRequired UnsupportedAuthStrategy RequestError DuplicateRecordExists FileSizeExceedsLimit ChecksumError
""".split()

class EgnyteError(Exception):
    """Base class for Egnyte SDK exceptions"""

    def __str__(self):
        """Pretty-printed version. Use repr for bare vesion instead"""
        contents = []
        for item in self.args:
            if isinstance(item, dict):
                contents.append("{%s}" % ", ".join(sorted(["%s: '%s'" % (k, v) for (k, v) in item.items()])))
            else:
                contents.append(str(item))
        return "<%s: %s>" % (self.__class__.__name__, ", ".join(contents))
        return self


class InvalidParameters(EgnyteError):
    """Invalid parameters were passed to an API request"""


class InsufficientPermissions(EgnyteError):
    """User does not have sufficient permissions to perform this action"""


class NotFound(EgnyteError):
    """Resource with name does not exist"""


class Redirected(EgnyteError):
    """Received unexpected HTTP 303 response"""


class NotAuthorized(EgnyteError):
    """Access token is required"""


class JsonParseError(EgnyteError):
    """Response from the server could not be parsed properly"""


class DomainRequired(EgnyteError):
    """Domain name is required"""


class ClientIdRequired(EgnyteError):
    """Client id is required"""


class OAuthUsernameRequired(EgnyteError):
    """Username is required for OAuth authentication"""


class OAuthPasswordRequired(EgnyteError):
    """Password is required for OAuth authentication"""


class UnsupportedAuthStrategy(EgnyteError):
    """This OAuth flow is not supported by this API key"""


class RequestError(EgnyteError):
    """Other kinds of request errors"""


class FileExpected(EgnyteError):
    """
    """


class DuplicateRecordExists(EgnyteError):
    """Existing entity conflict"""


class FileSizeExceedsLimit(EgnyteError):
    """File is too large for this operation."""


class ChecksumError(EgnyteError):
    """Checksum of the uploaded file is different than checksum calculated locally - file was corrupted during transfer."""

def extract_errors(data):
    """
    Try to extract useful information from inconsistent error data structures.
    """
    if 'errors' in data:
        data = data['errors']
    if 'inputErrors' in data:
        for err in extract_errors(data['inputErrors']):
            yield err
    elif hasattr(data, 'keys'):
        if 'code' in data:
            yield data
        else:
            for value in data.values():
                for err in extract_errors(value):
                    yield err
    elif isinstance(data, list):
        for value in data:
            for err in extract_errors(value):
                yield err
    else:
        yield data


def recursive_tuple(data):
    """Convert nested lists/dicts into tuples for structural comparing"""
    if isinstance(data, (list, tuple)):
        return tuple(recursive_tuple(x) for x in data)
    elif isinstance(data, dict):
        # We cannot use plain tuple(sorted(...)) here, because while it works giving us stable sort
        # in Python 2.7, it does not in Python 3 (results in TypeError: unorderable types
        # so we'll sort dictonaries first and then turn into tuples
        return tuple((recursive_tuple(x), recursive_tuple(y)) for (x, y) in sorted(data.items()))
    return data


class ErrorMapping(dict):
    """Maps HTTP status to EgnyteError subclasses"""
    ignored_errors = ()

    def __init__(self, values=None, ok_statuses=(HTTPStatus.OK, ), ignored_errors=None):
        super(ErrorMapping, self).__init__({
            HTTPStatus.BAD_REQUEST: RequestError,
            HTTPStatus.UNAUTHORIZED: NotAuthorized,
            HTTPStatus.FORBIDDEN: InsufficientPermissions,
            HTTPStatus.NOT_FOUND: NotFound,
            HTTPStatus.CONFLICT: DuplicateRecordExists,
            HTTPStatus.REQUEST_ENTITY_TOO_LARGE: FileSizeExceedsLimit,
            HTTPStatus.SEE_OTHER: Redirected,
        })
        if values:
            self.update(values)
        if ignored_errors:
            self.ignored_errors = recursive_tuple(ignored_errors)
        self.ok_statuses = ok_statuses

    def map_error(self, response):
        return self.get(response.status_code, RequestError)

    def check_response(self, response, *ok_statuses):
        """
        Check if HTTP response has a correct status,
        try to raise a specific EgnyteError subclass if not
        """
        if not len(ok_statuses):
            ok_statuses = self.ok_statuses
        if response.status_code not in ok_statuses:
            errors = [{'url': response.url}]
            error_type = self.map_error(response)
            try:
                data = response.json()
                for err in extract_errors(data):
                    errors.append(err)
            except Exception:
                errors.append({"http response": response.text})
            errors.append({"http status": response.status_code})
            errors.append({"headers": dict(response.headers)})
            if not self.ignore_error(errors):
                raise error_type(*errors)
        return response

    def ignore_error(self, errors):
        errors = recursive_tuple(errors[1:])
        if errors and self.ignored_errors:
            result = any((errors[:len(ignored)] == ignored) for ignored in self.ignored_errors)
            return result

    def check_json_response(self, response, *ok_statuses):
        """
        Check if HTTP response has a correct status and then parse it as JSON,
        try to raise a specific EgnyteError subclass if not
        """
        try:
            r = self.check_response(response, *ok_statuses)
            if r.status_code == HTTPStatus.NO_CONTENT:
                return None
            return r.json()
        except ValueError:
            raise JsonParseError({"http response": response.text})

    def copy(self):
        """Make a copy preserving class of self"""
        return self.__class__(self)

default = ErrorMapping()
partial = ErrorMapping(ok_statuses={HTTPStatus.PARTIAL_CONTENT})
accepted = ErrorMapping(ok_statuses={HTTPStatus.ACCEPTED})
created = ErrorMapping(ok_statuses={HTTPStatus.CREATED})
no_content_ok = ErrorMapping(ok_statuses={HTTPStatus.OK, HTTPStatus.NO_CONTENT})
created_ignore_existing = ErrorMapping(ok_statuses=(HTTPStatus.CREATED,), ignored_errors = [
    (u'Folder already exists at this location', {'http status': 403})
])

Exceptions and their handlers. EgnyteError InvalidParameters InsufficientPermissions NotFound Redirected NotAuthorized JsonParseError DomainRequired ClientIdRequired OAuthUsernameRequired OAuthPasswordRequired UnsupportedAuthStrategy RequestError DuplicateRecordExists FileSizeExceedsLimit ChecksumError Base class for Egnyte SDK exceptions Pretty-printed version. Use repr for bare vesion instead Invalid parameters were passed to an API request User does not have sufficient permissions to perform this action Resource with name does not exist Received unexpected HTTP 303 response Access token is required Response from the server could not be parsed properly Domain name is required Client id is required Username is required for OAuth authentication Password is required for OAuth authentication This OAuth flow is not supported by this API key Other kinds of request errors Existing entity conflict File is too large for this operation. Checksum of the uploaded file is different than checksum calculated locally - file was corrupted during transfer. Try to extract useful information from inconsistent error data structures. Convert nested lists/dicts into tuples for structural comparing # We cannot use plain tuple(sorted(...)) here, because while it works giving us stable sort # in Python 2.7, it does not in Python 3 (results in TypeError: unorderable types # so we'll sort dictonaries first and then turn into tuples Maps HTTP status to EgnyteError subclasses Check if HTTP response has a correct status,         try to raise a specific EgnyteError subclass if not Check if HTTP response has a correct status and then parse it as JSON,         try to raise a specific EgnyteError subclass if not Make a copy preserving class of self