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

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

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

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):
70class RemoteError(Exception):
71    """An error occurred on the other end of some connection.
72
73    This occurs when communication succeeds but another type of error
74    occurs remotely. The error string can consist of a remote stack
75    trace or a simple message depending on the context.
76
77    Communication systems should raise more specific error types locally
78    when more introspection/control is needed; this is intended somewhat
79    as a catch-all.
80    """
81
82    def __init__(self, msg: str, peer_desc: str):
83        super().__init__(msg)
84        self._peer_desc = peer_desc
85
86    def __str__(self) -> str:
87        s = ''.join(str(arg) for arg in self.args)
88        # Indent so we can more easily tell what is the remote part when
89        # this is in the middle of a long exception chain.
90        padding = '  '
91        s = ''.join(padding + line for line in s.splitlines(keepends=True))
92        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)
82    def __init__(self, msg: str, peer_desc: str):
83        super().__init__(msg)
84        self._peer_desc = peer_desc
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class IntegrityError(builtins.ValueError):
95class IntegrityError(ValueError):
96    """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):
 99class AuthenticationError(Exception):
100    """Authentication has failed for some operation.
101
102    This can be raised if server-side-verification does not match
103    client-supplied credentials, if an invalid password is supplied
104    for a sign-in attempt, etc.
105    """

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

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

def is_udp_communication_error(exc: BaseException) -> bool:
172def is_udp_communication_error(exc: BaseException) -> bool:
173    """Should this udp-related exception be considered a communication error?
174
175    This should be passed an exception which resulted from creating and
176    using a socket.SOCK_DGRAM type socket. It should return True for any
177    errors that could conceivably arise due to unavailable/poor network
178    conditions, firewall/connectivity issues, etc. These issues can often
179    be safely ignored or presented to the user as general
180    'network-unavailable' states.
181    """
182    if isinstance(exc, ConnectionRefusedError | TimeoutError):
183        return True
184    if isinstance(exc, OSError):
185        if exc.errno == 10051:  # Windows unreachable network error.
186            return True
187        if exc.errno in {
188            errno.EADDRNOTAVAIL,
189            errno.ETIMEDOUT,
190            errno.EHOSTUNREACH,
191            errno.ENETUNREACH,
192            errno.EINVAL,
193            errno.EPERM,
194            errno.EACCES,
195            # Windows 'invalid argument' error.
196            10022,
197            # Windows 'a socket operation was attempted to'
198            #         'an unreachable network' error.
199            10051,
200        }:
201            return True
202    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:
205def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
206    """Should this streams error be considered a communication error?
207
208    This should be passed an exception which resulted from creating and
209    using asyncio streams. It should return True for any errors that could
210    conceivably arise due to unavailable/poor network connections,
211    firewall/connectivity issues, etc. These issues can often be safely
212    ignored or presented to the user as general 'connection-lost' events.
213    """
214    # pylint: disable=too-many-return-statements
215    import ssl
216
217    if isinstance(
218        exc,
219        (
220            ConnectionError,
221            TimeoutError,
222            EOFError,
223        ),
224    ):
225        return True
226
227    # Also some specific errno ones.
228    if isinstance(exc, OSError):
229        if exc.errno == 10051:  # Windows unreachable network error.
230            return True
231        if exc.errno in {
232            errno.ETIMEDOUT,
233            errno.EHOSTUNREACH,
234            errno.ENETUNREACH,
235        }:
236            return True
237
238    # Am occasionally getting a specific SSL error on shutdown which I
239    # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY).
240    # It sounds like it may soon be ignored by Python (as of March 2022).
241    # Let's still complain, however, if we get any SSL errors besides
242    # this one. https://bugs.python.org/issue39951
243    if isinstance(exc, ssl.SSLError):
244        excstr = str(exc)
245        if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr:
246            return True
247
248        # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors;
249        # Assuming this just means client is attempting to connect from some
250        # outdated browser or whatnot.
251        if 'SSL: WRONG_VERSION_NUMBER' in excstr:
252            return True
253
254        # And seeing this very rarely; assuming its just data corruption?
255        if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
256            return True
257
258    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.