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, override 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 aim to communicate specific errors 77 gracefully as standard message responses when specific details are 78 needed; this is intended more 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 @override 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}' 93 94 95class IntegrityError(ValueError): 96 """Data has been tampered with or corrupted in some form.""" 97 98 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 """ 106 107 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 162 163 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) 170 171 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 203 204 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
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.
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).
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.
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 aim to communicate specific errors 78 gracefully as standard message responses when specific details are 79 needed; this is intended more 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 @override 87 def __str__(self) -> str: 88 s = ''.join(str(arg) for arg in self.args) 89 # Indent so we can more easily tell what is the remote part when 90 # this is in the middle of a long exception chain. 91 padding = ' ' 92 s = ''.join(padding + line for line in s.splitlines(keepends=True)) 93 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 aim to communicate specific errors gracefully as standard message responses when specific details are needed; this is intended more as a catch-all.
Data has been tampered with or corrupted in some form.
100class AuthenticationError(Exception): 101 """Authentication has failed for some operation. 102 103 This can be raised if server-side-verification does not match 104 client-supplied credentials, if an invalid password is supplied 105 for a sign-in attempt, etc. 106 """
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.
109def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool: 110 """Is the provided exception from urllib a communication-related error? 111 112 Url, if provided, can provide extra context for when to treat an error 113 as such an error. 114 115 This should be passed an exception which resulted from opening or 116 reading a urllib Request. It returns True for any errors that could 117 conceivably arise due to unavailable/poor network connections, 118 firewall/connectivity issues, or other issues out of our control. 119 These errors can often be safely ignored or presented to the user 120 as general 'network-unavailable' states. 121 """ 122 import urllib.error 123 import http.client 124 import socket 125 126 if isinstance( 127 exc, 128 ( 129 urllib.error.URLError, 130 ConnectionError, 131 http.client.IncompleteRead, 132 http.client.BadStatusLine, 133 http.client.RemoteDisconnected, 134 socket.timeout, 135 ), 136 ): 137 # Special case: although an HTTPError is a subclass of URLError, 138 # we don't consider it a communication error. It generally means we 139 # have successfully communicated with the server but what we are asking 140 # for is not there/etc. 141 if isinstance(exc, urllib.error.HTTPError): 142 # Special sub-case: appspot.com hosting seems to give 403 errors 143 # (forbidden) to some countries. I'm assuming for legal reasons?.. 144 # Let's consider that a communication error since its out of our 145 # control so we don't fill up logs with it. 146 if exc.code == 403 and url is not None and '.appspot.com' in url: 147 return True 148 149 return False 150 151 return True 152 153 if isinstance(exc, OSError): 154 if exc.errno == 10051: # Windows unreachable network error. 155 return True 156 if exc.errno in { 157 errno.ETIMEDOUT, 158 errno.EHOSTUNREACH, 159 errno.ENETUNREACH, 160 }: 161 return True 162 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.
165def is_requests_communication_error(exc: BaseException) -> bool: 166 """Is the provided exception a communication-related error from requests?""" 167 import requests 168 169 # Looks like this maps pretty well onto requests' ConnectionError 170 return isinstance(exc, requests.ConnectionError)
Is the provided exception a communication-related error from requests?
173def is_udp_communication_error(exc: BaseException) -> bool: 174 """Should this udp-related exception be considered a communication error? 175 176 This should be passed an exception which resulted from creating and 177 using a socket.SOCK_DGRAM type socket. It should return True for any 178 errors that could conceivably arise due to unavailable/poor network 179 conditions, firewall/connectivity issues, etc. These issues can often 180 be safely ignored or presented to the user as general 181 'network-unavailable' states. 182 """ 183 if isinstance(exc, ConnectionRefusedError | TimeoutError): 184 return True 185 if isinstance(exc, OSError): 186 if exc.errno == 10051: # Windows unreachable network error. 187 return True 188 if exc.errno in { 189 errno.EADDRNOTAVAIL, 190 errno.ETIMEDOUT, 191 errno.EHOSTUNREACH, 192 errno.ENETUNREACH, 193 errno.EINVAL, 194 errno.EPERM, 195 errno.EACCES, 196 # Windows 'invalid argument' error. 197 10022, 198 # Windows 'a socket operation was attempted to' 199 # 'an unreachable network' error. 200 10051, 201 }: 202 return True 203 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.
206def is_asyncio_streams_communication_error(exc: BaseException) -> bool: 207 """Should this streams error be considered a communication error? 208 209 This should be passed an exception which resulted from creating and 210 using asyncio streams. It should return True for any errors that could 211 conceivably arise due to unavailable/poor network connections, 212 firewall/connectivity issues, etc. These issues can often be safely 213 ignored or presented to the user as general 'connection-lost' events. 214 """ 215 # pylint: disable=too-many-return-statements 216 import ssl 217 218 if isinstance( 219 exc, 220 ( 221 ConnectionError, 222 TimeoutError, 223 EOFError, 224 ), 225 ): 226 return True 227 228 # Also some specific errno ones. 229 if isinstance(exc, OSError): 230 if exc.errno == 10051: # Windows unreachable network error. 231 return True 232 if exc.errno in { 233 errno.ETIMEDOUT, 234 errno.EHOSTUNREACH, 235 errno.ENETUNREACH, 236 }: 237 return True 238 239 # Am occasionally getting a specific SSL error on shutdown which I 240 # believe is harmless (APPLICATION_DATA_AFTER_CLOSE_NOTIFY). 241 # It sounds like it may soon be ignored by Python (as of March 2022). 242 # Let's still complain, however, if we get any SSL errors besides 243 # this one. https://bugs.python.org/issue39951 244 if isinstance(exc, ssl.SSLError): 245 excstr = str(exc) 246 if 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY' in excstr: 247 return True 248 249 # Also occasionally am getting WRONG_VERSION_NUMBER ssl errors; 250 # Assuming this just means client is attempting to connect from some 251 # outdated browser or whatnot. 252 if 'SSL: WRONG_VERSION_NUMBER' in excstr: 253 return True 254 255 # And seeing this very rarely; assuming its just data corruption? 256 if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr: 257 return True 258 259 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.