efro.error

Common errors and related functionality.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Common errors and related functionality."""
  4from __future__ import annotations
  5
  6from typing import TYPE_CHECKING
  7import errno
  8
  9from typing_extensions import override
 10
 11if TYPE_CHECKING:
 12    from typing import Any
 13
 14    from efro.terminal import ClrBase
 15
 16
 17class CleanError(Exception):
 18    """An error that can be presented to the user as a simple message.
 19
 20    These errors should be completely self-explanatory, to the point where
 21    a traceback or other context would not be useful.
 22
 23    A CleanError with no message can be used to inform a script to fail
 24    without printing any message.
 25
 26    This should generally be limited to errors that will *always* be
 27    presented to the user (such as those in high level tool code).
 28    Exceptions that may be caught and handled by other code should use
 29    more descriptive exception types.
 30    """
 31
 32    def pretty_print(
 33        self,
 34        flush: bool = True,
 35        prefix: str = 'Error',
 36        file: Any = None,
 37        clr: type[ClrBase] | None = None,
 38    ) -> None:
 39        """Print the error to stdout, using red colored output if available.
 40
 41        If the error has an empty message, prints nothing (not even a newline).
 42        """
 43        from efro.terminal import Clr
 44
 45        if clr is None:
 46            clr = Clr
 47
 48        if prefix:
 49            prefix = f'{prefix}: '
 50        errstr = str(self)
 51        if errstr:
 52            print(
 53                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
 54            )
 55
 56
 57class CommunicationError(Exception):
 58    """A communication related error has occurred.
 59
 60    This covers anything network-related going wrong in the sending
 61    of data or receiving of a response. Basically anything that is out
 62    of our control should get lumped in here. This error does not imply
 63    that data was not received on the other end; only that a full
 64    acknowledgement round trip was not completed.
 65
 66    These errors should be gracefully handled whenever possible, as
 67    occasional network issues are unavoidable.
 68    """
 69
 70
 71class RemoteError(Exception):
 72    """An error occurred on the other end of some connection.
 73
 74    This occurs when communication succeeds but another type of error
 75    occurs remotely. The error string can consist of a remote stack
 76    trace or a simple message depending on the context.
 77
 78    Communication systems should raise more specific error types locally
 79    when more introspection/control is needed; this is intended somewhat
 80    as a catch-all.
 81    """
 82
 83    def __init__(self, msg: str, peer_desc: str):
 84        super().__init__(msg)
 85        self._peer_desc = peer_desc
 86
 87    @override
 88    def __str__(self) -> str:
 89        s = ''.join(str(arg) for arg in self.args)
 90        # Indent so we can more easily tell what is the remote part when
 91        # this is in the middle of a long exception chain.
 92        padding = '  '
 93        s = ''.join(padding + line for line in s.splitlines(keepends=True))
 94        return f'The following occurred on {self._peer_desc}:\n{s}'
 95
 96
 97class IntegrityError(ValueError):
 98    """Data has been tampered with or corrupted in some form."""
 99
100
101class AuthenticationError(Exception):
102    """Authentication has failed for some operation.
103
104    This can be raised if server-side-verification does not match
105    client-supplied credentials, if an invalid password is supplied
106    for a sign-in attempt, etc.
107    """
108
109
110def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
111    """Is the provided exception from urllib a communication-related error?
112
113    Url, if provided, can provide extra context for when to treat an error
114    as such an error.
115
116    This should be passed an exception which resulted from opening or
117    reading a urllib Request. It returns True for any errors that could
118    conceivably arise due to unavailable/poor network connections,
119    firewall/connectivity issues, or other issues out of our control.
120    These errors can often be safely ignored or presented to the user
121    as general 'network-unavailable' states.
122    """
123    import urllib.error
124    import http.client
125    import socket
126
127    if isinstance(
128        exc,
129        (
130            urllib.error.URLError,
131            ConnectionError,
132            http.client.IncompleteRead,
133            http.client.BadStatusLine,
134            http.client.RemoteDisconnected,
135            socket.timeout,
136        ),
137    ):
138        # Special case: although an HTTPError is a subclass of URLError,
139        # we don't consider it a communication error. It generally means we
140        # have successfully communicated with the server but what we are asking
141        # for is not there/etc.
142        if isinstance(exc, urllib.error.HTTPError):
143            # Special sub-case: appspot.com hosting seems to give 403 errors
144            # (forbidden) to some countries. I'm assuming for legal reasons?..
145            # Let's consider that a communication error since its out of our
146            # control so we don't fill up logs with it.
147            if exc.code == 403 and url is not None and '.appspot.com' in url:
148                return True
149
150            return False
151
152        return True
153
154    if isinstance(exc, OSError):
155        if exc.errno == 10051:  # Windows unreachable network error.
156            return True
157        if exc.errno in {
158            errno.ETIMEDOUT,
159            errno.EHOSTUNREACH,
160            errno.ENETUNREACH,
161        }:
162            return True
163    return False
164
165
166def is_requests_communication_error(exc: BaseException) -> bool:
167    """Is the provided exception a communication-related error from requests?"""
168    import requests
169
170    # Looks like this maps pretty well onto requests' ConnectionError
171    return isinstance(exc, requests.ConnectionError)
172
173
174def is_udp_communication_error(exc: BaseException) -> bool:
175    """Should this udp-related exception be considered a communication error?
176
177    This should be passed an exception which resulted from creating and
178    using a socket.SOCK_DGRAM type socket. It should return True for any
179    errors that could conceivably arise due to unavailable/poor network
180    conditions, firewall/connectivity issues, etc. These issues can often
181    be safely ignored or presented to the user as general
182    'network-unavailable' states.
183    """
184    if isinstance(exc, ConnectionRefusedError | TimeoutError):
185        return True
186    if isinstance(exc, OSError):
187        if exc.errno == 10051:  # Windows unreachable network error.
188            return True
189        if exc.errno in {
190            errno.EADDRNOTAVAIL,
191            errno.ETIMEDOUT,
192            errno.EHOSTUNREACH,
193            errno.ENETUNREACH,
194            errno.EINVAL,
195            errno.EPERM,
196            errno.EACCES,
197            # Windows 'invalid argument' error.
198            10022,
199            # Windows 'a socket operation was attempted to'
200            #         'an unreachable network' error.
201            10051,
202        }:
203            return True
204    return False
205
206
207def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
208    """Should this streams error be considered a communication error?
209
210    This should be passed an exception which resulted from creating and
211    using asyncio streams. It should return True for any errors that could
212    conceivably arise due to unavailable/poor network connections,
213    firewall/connectivity issues, etc. These issues can often be safely
214    ignored or presented to the user as general 'connection-lost' events.
215    """
216    # pylint: disable=too-many-return-statements
217    import ssl
218
219    if isinstance(
220        exc,
221        (
222            ConnectionError,
223            TimeoutError,
224            EOFError,
225        ),
226    ):
227        return True
228
229    # Also some specific errno ones.
230    if isinstance(exc, OSError):
231        if exc.errno == 10051:  # Windows unreachable network error.
232            return True
233        if exc.errno in {
234            errno.ETIMEDOUT,
235            errno.EHOSTUNREACH,
236            errno.ENETUNREACH,
237        }:
238            return True
239
240    # Am occasionally getting a specific SSL error on shutdown which I
241    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
242    # It sounds like it may soon be ignored by Python (as of March 2022).
243    # Let's still complain, however, if we get any SSL errors besides
244    # this one. https://bugs.python.org/issue39951
245    if isinstance(exc, ssl.SSLError):
246        excstr = str(exc)
247        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
248            return True
249
250        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
251        # Assuming this just means client is attempting to connect from some
252        # outdated browser or whatnot.
253        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
254            return True
255
256        # And seeing this very rarely; assuming its just data corruption?
257        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
258            return True
259
260    return False
class CleanError(builtins.Exception):
18class CleanError(Exception):
19    """An error that can be presented to the user as a simple message.
20
21    These errors should be completely self-explanatory, to the point where
22    a traceback or other context would not be useful.
23
24    A CleanError with no message can be used to inform a script to fail
25    without printing any message.
26
27    This should generally be limited to errors that will *always* be
28    presented to the user (such as those in high level tool code).
29    Exceptions that may be caught and handled by other code should use
30    more descriptive exception types.
31    """
32
33    def pretty_print(
34        self,
35        flush: bool = True,
36        prefix: str = 'Error',
37        file: Any = None,
38        clr: type[ClrBase] | None = None,
39    ) -> None:
40        """Print the error to stdout, using red colored output if available.
41
42        If the error has an empty message, prints nothing (not even a newline).
43        """
44        from efro.terminal import Clr
45
46        if clr is None:
47            clr = Clr
48
49        if prefix:
50            prefix = f'{prefix}: '
51        errstr = str(self)
52        if errstr:
53            print(
54                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
55            )

An error that can be presented to the user as a simple message.

These errors should be completely self-explanatory, to the point where a traceback or other context would not be useful.

A CleanError with no message can be used to inform a script to fail without printing any message.

This should generally be limited to errors that will always be presented to the user (such as those in high level tool code). Exceptions that may be caught and handled by other code should use more descriptive exception types.

def pretty_print( self, flush: bool = True, prefix: str = 'Error', file: Any = None, clr: type[efro.terminal.ClrBase] | None = None) -> None:
33    def pretty_print(
34        self,
35        flush: bool = True,
36        prefix: str = 'Error',
37        file: Any = None,
38        clr: type[ClrBase] | None = None,
39    ) -> None:
40        """Print the error to stdout, using red colored output if available.
41
42        If the error has an empty message, prints nothing (not even a newline).
43        """
44        from efro.terminal import Clr
45
46        if clr is None:
47            clr = Clr
48
49        if prefix:
50            prefix = f'{prefix}: '
51        errstr = str(self)
52        if errstr:
53            print(
54                f'{clr.SRED}{prefix}{errstr}{clr.RST}', flush=flush, file=file
55            )

Print the error to stdout, using red colored output if available.

If the error has an empty message, prints nothing (not even a newline).

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class CommunicationError(builtins.Exception):
58class CommunicationError(Exception):
59    """A communication related error has occurred.
60
61    This covers anything network-related going wrong in the sending
62    of data or receiving of a response. Basically anything that is out
63    of our control should get lumped in here. This error does not imply
64    that data was not received on the other end; only that a full
65    acknowledgement round trip was not completed.
66
67    These errors should be gracefully handled whenever possible, as
68    occasional network issues are unavoidable.
69    """

A communication related error has occurred.

This covers anything network-related going wrong in the sending of data or receiving of a response. Basically anything that is out of our control should get lumped in here. This error does not imply that data was not received on the other end; only that a full acknowledgement round trip was not completed.

These errors should be gracefully handled whenever possible, as occasional network issues are unavoidable.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class RemoteError(builtins.Exception):
72class RemoteError(Exception):
73    """An error occurred on the other end of some connection.
74
75    This occurs when communication succeeds but another type of error
76    occurs remotely. The error string can consist of a remote stack
77    trace or a simple message depending on the context.
78
79    Communication systems should raise more specific error types locally
80    when more introspection/control is needed; this is intended somewhat
81    as a catch-all.
82    """
83
84    def __init__(self, msg: str, peer_desc: str):
85        super().__init__(msg)
86        self._peer_desc = peer_desc
87
88    @override
89    def __str__(self) -> str:
90        s = ''.join(str(arg) for arg in self.args)
91        # Indent so we can more easily tell what is the remote part when
92        # this is in the middle of a long exception chain.
93        padding = '  '
94        s = ''.join(padding + line for line in s.splitlines(keepends=True))
95        return f'The following occurred on {self._peer_desc}:\n{s}'

An error occurred on the other end of some connection.

This occurs when communication succeeds but another type of error occurs remotely. The error string can consist of a remote stack trace or a simple message depending on the context.

Communication systems should raise more specific error types locally when more introspection/control is needed; this is intended somewhat as a catch-all.

RemoteError(msg: str, peer_desc: str)
84    def __init__(self, msg: str, peer_desc: str):
85        super().__init__(msg)
86        self._peer_desc = peer_desc
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class IntegrityError(builtins.ValueError):
98class IntegrityError(ValueError):
99    """Data has been tampered with or corrupted in some form."""

Data has been tampered with or corrupted in some form.

Inherited Members
builtins.ValueError
ValueError
builtins.BaseException
with_traceback
add_note
args
class AuthenticationError(builtins.Exception):
102class AuthenticationError(Exception):
103    """Authentication has failed for some operation.
104
105    This can be raised if server-side-verification does not match
106    client-supplied credentials, if an invalid password is supplied
107    for a sign-in attempt, etc.
108    """

Authentication has failed for some operation.

This can be raised if server-side-verification does not match client-supplied credentials, if an invalid password is supplied for a sign-in attempt, etc.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
111def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
112    """Is the provided exception from urllib a communication-related error?
113
114    Url, if provided, can provide extra context for when to treat an error
115    as such an error.
116
117    This should be passed an exception which resulted from opening or
118    reading a urllib Request. It returns True for any errors that could
119    conceivably arise due to unavailable/poor network connections,
120    firewall/connectivity issues, or other issues out of our control.
121    These errors can often be safely ignored or presented to the user
122    as general 'network-unavailable' states.
123    """
124    import urllib.error
125    import http.client
126    import socket
127
128    if isinstance(
129        exc,
130        (
131            urllib.error.URLError,
132            ConnectionError,
133            http.client.IncompleteRead,
134            http.client.BadStatusLine,
135            http.client.RemoteDisconnected,
136            socket.timeout,
137        ),
138    ):
139        # Special case: although an HTTPError is a subclass of URLError,
140        # we don't consider it a communication error. It generally means we
141        # have successfully communicated with the server but what we are asking
142        # for is not there/etc.
143        if isinstance(exc, urllib.error.HTTPError):
144            # Special sub-case: appspot.com hosting seems to give 403 errors
145            # (forbidden) to some countries. I'm assuming for legal reasons?..
146            # Let's consider that a communication error since its out of our
147            # control so we don't fill up logs with it.
148            if exc.code == 403 and url is not None and '.appspot.com' in url:
149                return True
150
151            return False
152
153        return True
154
155    if isinstance(exc, OSError):
156        if exc.errno == 10051:  # Windows unreachable network error.
157            return True
158        if exc.errno in {
159            errno.ETIMEDOUT,
160            errno.EHOSTUNREACH,
161            errno.ENETUNREACH,
162        }:
163            return True
164    return False

Is the provided exception from urllib a communication-related error?

Url, if provided, can provide extra context for when to treat an error as such an error.

This should be passed an exception which resulted from opening or reading a urllib Request. It returns True for any errors that could conceivably arise due to unavailable/poor network connections, firewall/connectivity issues, or other issues out of our control. These errors can often be safely ignored or presented to the user as general 'network-unavailable' states.

def is_requests_communication_error(exc: BaseException) -> bool:
167def is_requests_communication_error(exc: BaseException) -> bool:
168    """Is the provided exception a communication-related error from requests?"""
169    import requests
170
171    # Looks like this maps pretty well onto requests' ConnectionError
172    return isinstance(exc, requests.ConnectionError)

Is the provided exception a communication-related error from requests?

def is_udp_communication_error(exc: BaseException) -> bool:
175def is_udp_communication_error(exc: BaseException) -> bool:
176    """Should this udp-related exception be considered a communication error?
177
178    This should be passed an exception which resulted from creating and
179    using a socket.SOCK_DGRAM type socket. It should return True for any
180    errors that could conceivably arise due to unavailable/poor network
181    conditions, firewall/connectivity issues, etc. These issues can often
182    be safely ignored or presented to the user as general
183    'network-unavailable' states.
184    """
185    if isinstance(exc, ConnectionRefusedError | TimeoutError):
186        return True
187    if isinstance(exc, OSError):
188        if exc.errno == 10051:  # Windows unreachable network error.
189            return True
190        if exc.errno in {
191            errno.EADDRNOTAVAIL,
192            errno.ETIMEDOUT,
193            errno.EHOSTUNREACH,
194            errno.ENETUNREACH,
195            errno.EINVAL,
196            errno.EPERM,
197            errno.EACCES,
198            # Windows 'invalid argument' error.
199            10022,
200            # Windows 'a socket operation was attempted to'
201            #         'an unreachable network' error.
202            10051,
203        }:
204            return True
205    return False

Should this udp-related exception be considered a communication error?

This should be passed an exception which resulted from creating and using a socket.SOCK_DGRAM type socket. It should return True for any errors that could conceivably arise due to unavailable/poor network conditions, firewall/connectivity issues, etc. These issues can often be safely ignored or presented to the user as general 'network-unavailable' states.

def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
208def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
209    """Should this streams error be considered a communication error?
210
211    This should be passed an exception which resulted from creating and
212    using asyncio streams. It should return True for any errors that could
213    conceivably arise due to unavailable/poor network connections,
214    firewall/connectivity issues, etc. These issues can often be safely
215    ignored or presented to the user as general 'connection-lost' events.
216    """
217    # pylint: disable=too-many-return-statements
218    import ssl
219
220    if isinstance(
221        exc,
222        (
223            ConnectionError,
224            TimeoutError,
225            EOFError,
226        ),
227    ):
228        return True
229
230    # Also some specific errno ones.
231    if isinstance(exc, OSError):
232        if exc.errno == 10051:  # Windows unreachable network error.
233            return True
234        if exc.errno in {
235            errno.ETIMEDOUT,
236            errno.EHOSTUNREACH,
237            errno.ENETUNREACH,
238        }:
239            return True
240
241    # Am occasionally getting a specific SSL error on shutdown which I
242    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
243    # It sounds like it may soon be ignored by Python (as of March 2022).
244    # Let's still complain, however, if we get any SSL errors besides
245    # this one. https://bugs.python.org/issue39951
246    if isinstance(exc, ssl.SSLError):
247        excstr = str(exc)
248        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
249            return True
250
251        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
252        # Assuming this just means client is attempting to connect from some
253        # outdated browser or whatnot.
254        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
255            return True
256
257        # And seeing this very rarely; assuming its just data corruption?
258        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
259            return True
260
261    return False

Should this streams error be considered a communication error?

This should be passed an exception which resulted from creating and using asyncio streams. It should return True for any errors that could conceivably arise due to unavailable/poor network connections, firewall/connectivity issues, etc. These issues can often be safely ignored or presented to the user as general 'connection-lost' events.