skyfielders / python-skyfield

Elegant astronomy for Python
MIT License
1.4k stars 211 forks source link

OSError "[Errno 11001] getaddrinfo failed" on load.download("finals2000A.all") when offline #651

Closed aendie closed 2 years ago

aendie commented 2 years ago

When I have no network connection and the following code executes ...

    EOPdf  = "finals2000A.all"  # Earth Orientation Parameters data file
    dfIERS = EOPdf

    if config.useIERS:
        if compareVersion(VERSION, "1.31") >= 0:
            if path.isfile(dfIERS):
                if load.days_old(EOPdf) > float(config.ageIERS):
                    load.download(EOPdf)    # ERROR: 11001
                ts = load.timescale(builtin=False)  # timescale object
            else:
                load.download(EOPdf)
                ts = load.timescale(builtin=False)  # timescale object
        else:
            ts = load.timescale()   # timescale object with built-in UT1-tables
    else:
        ts = load.timescale()   # timescale object with built-in UT1-tables

I get the following console message (with Skyfield 1.37 and 1.39): Error 11001

Using the following try - except block didn't make the console message friendlier:

        try:
            load.download(EOPdf)    # ERROR: 11001
        except BaseException as err:
            print(f"Unexpected (err=), (type(err)=)")
            raise

I only came across this by accident (while trying to code while charging my E-car a few blocks away), and the code works fine when I have a WiFi connection. Maybe the error handling can be improved in the case that one is offline? (Fortunately my car still reached maximum charge ... :-) I'm using Python 3.9.5, by the way.

brandon-rhodes commented 2 years ago

In many cases, Python libraries are designed to allow errors through un-edited, since that gives the programmer the most information possible about the network error. Generally it's the application designer who knows better about how a transient error should be processed (displayed? replaced with another message? timeout and retry?). So my guess is that Skyfield should continue to let the real-life error through?

aendie commented 2 years ago

I can implement a ping test that I can trap myself, however I expected that a try ... except block (as above) should give me some kind of error message that I can test for. So, I'm not aware of of the best practices ... it's up to you. Thanks for the prompt response.

brandon-rhodes commented 2 years ago

As these errors are operating-system dependent — it's error 11001 on Windows, but would be a different error with a different number or message on other operating systems — I think that applications should make their own plans about how to handle I/O rather than having Skyfield try to (probably imperfectly) catch and recognize the different errors from at least 3 different operating systems.

I'll try adding a note to the documentation reminding folks that they may want to do their own downloading if they want to be in full control of the process, and noting that their operating system might raise any kind of network error during the download.

aendie commented 2 years ago

Permit me to add my "simple" solution, in case it may help another user. I just didn't want the Python code to spew out that "unfriendly" message for users that just want to run my code (maybe in the middle of an ocean). I have tested this on Windows 10 and Ubuntu Desktop 20.04 LTS (and I think it should also work on MacOS).

It has a simple network connection test - the subject of network (or internet) connection testing is quite deep in fact. I deliberately only test for http://www.iers.org on port 80 (no SSL test; no test on the actual IERS download site, which is different to the URL I chose). My code calls ts = init_sf() to initialize the Timescale object. I have slightly reduced the code here to form a MWE.

from skyfield import VERSION
from os import path
from skyfield.api import load

def compareVersion(versions1, version2):
    #versions1 = [int(v) for v in version1.split(".")]
    versions2 = [int(v) for v in version2.split(".")]
    for i in range(max(len(versions1),len(versions2))):
        v1 = versions1[i] if i < len(versions1) else 0
        v2 = versions2[i] if i < len(versions2) else 0
        if v1 > v2:
            return 1
        elif v1 < v2:
            return -1
    return 0

def isConnected():
    try:
        # connect to the host -- tells us if the host is actually reachable
        sock = socket.create_connection(("www.iers.org", 80))
        if sock is not None: sock.close
        return True
    except OSError:
        pass
    return False

def init_sf():
    EOPdf  = "finals2000A.all"  # Earth Orientation Parameters data file

    if compareVersion(VERSION, "1.31") >= 0:
        if path.isfile(EOPdf):
            if load.days_old(EOPdf) > 30.0:    # > float(config.ageIERS)
                if isConnected(): load.download(EOPdf)
                else: print("NOTE: no Internet connection... using existing '{}'".format(EOPdf))
            ts = load.timescale(builtin=False)  # timescale object
        else:
            if isConnected():
                load.download(EOPdf)
                ts = load.timescale(builtin=False)  # timescale object
            else:
                print("NOTE: no Internet connection... using built-in UT1-tables")
                ts = load.timescale()   # timescale object with built-in UT1-tables
    else:
        ts = load.timescale()   # timescale object with built-in UT1-tables

    return ts

Code is provided "as is", without a guarantee, and I am not responsible for any damage it may cause. (I know I don't need the compareVersion() function, but what works I leave.)

brandon-rhodes commented 2 years ago

Thanks for sharing example code! Hopefully it will help another user with a similar problem. I agree with you completely that “the subject of network (or internet) connection testing is quite deep in fact” — which makes me almost regret adding download() in the first place, instead of showing people how to use a Python networking library themselves and avoid having Skyfield deal with the issue. Alas! What is done in an API cannot be undone.