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")

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


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.

  • \$\begingroup\$ Are you looking for feedback on the internal code or the external interfacing of your library? \$\endgroup\$ Commented Jul 30, 2020 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\$ Commented Jul 30, 2020 at 1:00

1 Answer 1


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)



should just be


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


as a class member; instead store

obj_data: bool = False
  • \$\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\$ Commented Jul 30, 2020 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
    Commented Jul 30, 2020 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\$ Commented Jul 30, 2020 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
    Commented Jul 30, 2020 at 9:42
  • \$\begingroup\$ Ah, good thinking. Yeah that makes sense, like a whole ecosystem, huh? Guess I could include some relevant packages. \$\endgroup\$ Commented Jul 30, 2020 at 9:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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