10
\$\begingroup\$

I'm writing a tiny Python library that fetches data from Horizons, JPL's solar system data and ephemeris computation service. An overview of their batch CGI is here. While the script is simple, I would like to make it as clear and standardized as possible.

The idea is to initialize the HorizonsRequest class with a location for the observer (center), target (COMMAND), datetime, and the the data you want (quantities). The response can be retrieved as either a complete list of the lines sent back or as a dictionary of the requested values.

Example request

A request for the angle of Jupiter (599), its distance, and light travel time in relation to Earth (399) at the current time. Horizons users can find these numeric ids using the web-interface.

request = horizons.HorizonsRequest("399", "599", datetime.utcnow(), "2,20,21")
request.send()

dictionary = request.get_dictionary()
for key in dictionary:
    print(key, ":", dictionary[key])

Library

import sys
import urllib.request
import math
from datetime import datetime, timedelta

def get_julian_datetime(date):
    # Ensure correct format
    if not isinstance(date, datetime):
        raise TypeError('Invalid type for parameter "date" - expecting datetime')
    elif date.year < 1801 or date.year > 2099:
        raise ValueError('Datetime must be between year 1801 and 2099')

    # Perform the calculation
    julian_datetime = 367 * date.year - int((7 * (date.year + int((date.month + 9) 
    / 12.0))) / 4.0) + int((275 * date.month) / 9.0) + date.day + 1721013.5 
    + (date.hour + date.minute / 60.0 + date.second / math.pow(60,2)) / 24.0 
    - 0.5 * math.copysign(1, 100 * date.year + date.month - 190002.5) + 0.5

    return julian_datetime

class HorizonsRequest:
    def __init__(self, center, target, datetime, quantities):
        self.keys = {
            "CENTER": center,
            "COMMAND": target,
            "QUANTITIES": quantities,
            "TLIST": str(get_julian_datetime(datetime)),
            "MAKE_EPHEM": "YES",
            "OBJ_DATA": "NO",
            "TABLE_TYPE": "OBSERVER",
            "CAL_FORMAT": "BOTH",
            "ANG_FORMAT": "DEG",
            "CSV_FORMAT": "YES"
        }
        self.response = []
        self.dictionary = {}
    
    def set_key(self, key, value):
        self.keys[key] = str(value)

    def delete_key(self, key):
        self.keys.pop(key, None)    

    def send(self):
        # create request
        request = "https://ssd.jpl.nasa.gov/horizons_batch.cgi?batch=1"
        for key in self.keys:
            request += "&" + key + "='" + self.keys[key] + "'"

        # send request
        file = urllib.request.urlopen(request)
        for line in file:
            self.response.append(line.decode('utf-8').replace('\n', ''))

    def get_dictionary(self):
        keys = []
        values = [] 

        # locate data position in response
        i = 0
        while i < len(self.response):
            if "$$SOE" in self.response[i]:
                keys = self.response[i - 2].split(",")
                values = self.response[i + 1].split(",")
            i += 1

        # create dictionary
        i = 0
        while i < len(keys):
            self.dictionary[keys[i].strip()] = values[i].strip()
            i += 1

        return self.dictionary

    def get_response(self):
        return self.response

Any feedback would be greatly appreciated.

\$\endgroup\$
  • \$\begingroup\$ Are you looking for feedback on the internal code or the external interfacing of your library? \$\endgroup\$ – Ted Brownlow Jul 30 at 0:57
  • 1
    \$\begingroup\$ Internal code mostly, but suggestions regarding better ways to interface with it would also be helpful. The only thing that cannot be changed is the Horizons requests. \$\endgroup\$ – Pandazoic Jul 30 at 1:00
11
\$\begingroup\$

Use Requests

urllib is kind of a pain to use. Use Requests instead. Among other things, all of this code:

    for key in self.keys:
        request += "&" + key + "='" + self.keys[key] + "'"

will go away. Requests has query string formatting from a dictionary.

Julian conversion

It's doubtful that you need to be doing this yourself. Find a library such as https://pypi.org/project/julian/ .

Combined-inequality syntax

date.year < 1801 or date.year > 2099

should be

not (1800 < date.year < 2100)

Squaring

math.pow(60,2)

should just be

60**2

Late serialization

I would recommend that HorizonsRequest.keys (which, by the way, is not only a collection of keys - it's a dictionary, so it's misnamed) not be constructed until it's needed in send. Nor should you expose arbitrary set_key / delete_key. In theory you should understand enough about the request format that information to create it is stored in more strongly-typed variables that can receive better validation. For instance, do not store

"OBJ_DATA": "NO",

as a class member; instead store

obj_data: bool = False
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Very helpful, thank you! I'll make these changes. I'll check into Requests and pypi's julian library right away. Regarding the keys, because OBJ_DATA's value must be sent as "NO" should the bool be converted to this format only during request formation? \$\endgroup\$ – Pandazoic Jul 30 at 1:33
  • 1
    \$\begingroup\$ should the bool be converted to this format only during request formation? - Yes; I believe this would offer better structure to your program. \$\endgroup\$ – Reinderien Jul 30 at 1:34
  • \$\begingroup\$ I'm a little curious, regarding python, what people's thoughts are regarding when you should use a library if you can write it yourself. While popular libraries are likely to be better implementations, requiring your users to install another package seems worse sometimes, right? I become worried that someone unfamiliar with python will download my package from git and then be required to also install a bunch of extra things from pip. \$\endgroup\$ – Pandazoic Jul 30 at 9:19
  • 2
    \$\begingroup\$ @Pandazoic Python is batteries-included. Use what's available, keep a back-up in case it breaks. Reinventing the wheel is not Pythonic. If you want to make sure the packages are readily available, you can consider copying them into your own library (also takes care of version-difference troubles), make sure you read the relevant licenses first though. \$\endgroup\$ – Mast Jul 30 at 9:42
  • \$\begingroup\$ Ah, good thinking. Yeah that makes sense, like a whole ecosystem, huh? Guess I could include some relevant packages. \$\endgroup\$ – Pandazoic Jul 30 at 9:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.