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
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).
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- add_note
- args
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
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.
Inherited Members
- builtins.BaseException
- with_traceback
- add_note
- args
Data has been tampered with or corrupted in some form.
Inherited Members
- builtins.ValueError
- ValueError
- builtins.BaseException
- with_traceback
- add_note
- args
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
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.
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?
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.
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.