SpaceTAP: an Elixir client for the IVOA Table Access Protocol

SpaceTAP

SpaceTAP is an Elixir client for the IVOA Table Access Protocol (TAP), the HTTP protocol astronomical archives use to expose their tables. You send a query written in ADQL, a dialect of SQL with functions for positions on the sky, and get the table back as Elixir data.

It is not related to the Test Anything Protocol, which shares the acronym.

Installation

Add space_tap to the dependencies in mix.exs:

def deps do
  [
    {:space_tap, "~> 0.1.0"}
  ]
end

In Livebook or a script:

Mix.install([{:space_tap, "~> 0.1.0"}])

Usage

SpaceTAP.query/3 takes the service URL of a TAP service and an ADQL query. This one asks the Gaia archive for the five brightest stars within one degree of the centre of the Pleiades:

adql = """
SELECT TOP 5 source_id, ra, dec, parallax, phot_g_mean_mag
FROM gaiadr3.gaia_source
WHERE CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', 56.75, 24.12, 1.0)) = 1
  AND parallax IS NOT NULL AND phot_g_mean_mag IS NOT NULL
ORDER BY phot_g_mean_mag
"""

{:ok, result} = SpaceTAP.query("https://gea.esac.esa.int/tap-server/tap", adql)

result.columns
#=> ["source_id", "ra", "dec", "parallax", "phot_g_mean_mag"]

hd(result.rows)
#=> ["66526127137440128", "57.29068893293528", "24.053211829998986",
#=>  "8.118448445799721", "3.6157947"]

The service URL is the base URL of the service, what TAP calls its access URL. SpaceTAP sends the query to its /sync endpoint, so pass the base URL even if you know the endpoint.

Use the https:// URL when the service has one. Gaia, for example, redirects http:// to https:// with a 302. Req, like curl and pyvo, follows a 301 or 302 with a GET, as RFC 9110 allows (§15.4.2, §15.4.3), and the GET carries no query. The request then fails, with an error that depends on the service: Gaia answers with its list of async jobs, which SpaceTAP returns as a :decode_error.

The result is a SpaceTAP.Result: columns holds the column names in order, and each row is a list with one value per column, in the same order. To work with maps instead:

Enum.map(result.rows, fn row -> Map.new(Enum.zip(result.columns, row)) end)

Options

  • :maxrec asks the service to return at most that many rows. The service may apply a lower limit of its own. maxrec: 0 returns the columns with no rows.

  • :req_options are passed to Req, the HTTP client. Req waits 15 seconds for a response by default; for slower queries, raise it:

    SpaceTAP.query(service_url, adql, req_options: [receive_timeout: 60_000])

    SpaceTAP sets the TAP request itself, but some Req options, such as :headers, can still change it. See SpaceTAP.query/3.

Errors

A failed query returns {:error, %SpaceTAP.Error{}}, whose :type says what went wrong:

TypeWhen
:service_errorThe service rejected the query, for example because of invalid ADQL or an unknown table. :message is the service's own message.
:http_errorAn HTTP error status without an error document from the service, such as a proxy's 502 page.
:transport_errorNo response arrived: the connection failed or timed out.
:decode_errorA successful response that can't be read as a result, such as malformed CSV.

Later versions may add types, so when you match on :type, keep a clause for the rest.

:status holds the HTTP status, and Exception.message/1 starts with it:

{:error, error} =
  SpaceTAP.query(service_url, "SELECT TOP 3 source_id FROM gaiadr3.no_such_table")

error.type
#=> :service_error

Exception.message(error)
#=> "(HTTP 400) Cannot parse query 'SELECT TOP 3 source_id FROM gaiadr3.no_such_table' ..."

Results come as CSV

SpaceTAP 0.1 asks the service for CSV. That keeps it small, but CSV carries less than VOTable, the IVOA format for tables:

  • Every value is a string. CSV has no types, so "8.118448445799721" stays a string until you convert it, with Float.parse/1 for example.
  • An empty cell is nil. No IVOA standard says how CSV represents NULL. Gaia sends an empty field, and SpaceTAP can't tell it from an empty string.
  • A truncated result looks complete. When a result reaches :maxrec or the service's own limit, the service cuts it short. A VOTable says so; CSV doesn't. If you get exactly as many rows as the limit, assume there are more. Gaia stops at 50,000 rows.
  • The service must support CSV. TAP recommends CSV output but only requires VOTable. A service that answers in another format gives a :decode_error. CSV labelled text/plain instead of text/csv is read as CSV: the NASA Exoplanet Archive and KOA send it that way.

Typed results read from VOTable are planned for a later version, along with async queries for long-running and large results.

Compatibility

SpaceTAP follows TAP 1.1 and works with TAP 1.0 services. It has been checked against these services, last on 27 October 2026:

ServiceService URLChecked byNotes
Gaia archive (ESA)https://gea.esac.esa.int/tap-server/tapIntegration and parity tests, fixtures, livebooksRedirects http:// to https://, which loses the query: use https://.
GAVO Data Centerhttps://dc.g-vo.org/tapServices and parity tests, fixtures, livebooksRuns DaCHS. Sends error documents with HTTP 500.
NASA Exoplanet Archivehttps://exoplanetarchive.ipac.caltech.edu/TAPIntegration and parity tests, fixtures, livebooksLabels CSV text/plain. Applies TOP before ORDER BY, so use :maxrec to get the first rows of a sorted result.
IRSA (IPAC)https://irsa.ipac.caltech.edu/TAPServices and parity tests, fixtures, livebook
NED (IPAC)https://ned.ipac.caltech.edu/tapServices and parity tests, fixtures, livebook
KOA (Keck)https://koa.ipac.caltech.edu/TAPServices and parity tests, fixtures, livebookLabels CSV text/plain. Sends query errors as plain text with HTTP 400, which SpaceTAP returns as a :service_error.
VizieR (CDS)https://tapvizier.cds.unistra.fr/TAPVizieR/tapServices and parity tests, fixtures, livebook
SIMBAD (CDS)https://simbad.cds.unistra.fr/simbad/sim-tapServices and parity tests, fixtures, livebook
ESOhttps://archive.eso.org/tap_obsServices and parity tests, fixtures, livebook
CADChttps://ws.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/argusServices and parity tests, fixtures, livebookRedirects each query to its result with a 303 (TAP 1.1 §2.1), which SpaceTAP follows. Sends query errors as plain text with HTTP 400, which SpaceTAP returns as a :service_error.
MASThttps://mast.stsci.edu/vo-tap/api/v0.1/caomServices tests, fixtures, livebookDoesn't work yet. It sends VOTable even when CSV is requested, so queries give a :decode_error.

Integration, services and parity tests query the live service (see Development), and fixtures are its responses, captured for the unit tests. The tour of TAP services livebook queries all of them. Services change, so one that worked here may behave differently today.

Development

Run the unit tests, which don't need network access:

mix test

Integration tests run queries against the Gaia archive and the NASA Exoplanet Archive, and are excluded by default:

mix test --include integration

Parity tests run the same queries through SpaceTAP and through pyvo, the IVOA client for Python, and compare the results. They need uv. Those for Gaia and the NASA Exoplanet Archive run with:

mix test --only pyvo

mix test.release runs the unit, integration and parity tests above. Run it before a release.

Services tests send the queries of the fixtures to the other services in Compatibility, check that they still answer the same way, and run the parity tests for those that send CSV. They are slow, and a failure is a reason to look into that service, not to hold a release, so mix test.release leaves them out:

mix test.services

The livebooks folder has Livebook notebooks that run SpaceTAP against Gaia, GAVO, the NASA Exoplanet Archive and other services, with tables and charts. They install SpaceTAP from Hex, and each one has a "Run in Livebook" button on HexDocs.

About

SpaceTAP started as an exploration of the astronomy software ecosystem from Elixir: how libraries such as Python's pyvo and Astroquery work, and what Elixir can bring to it. It is written with AI coding tools; I review, test and answer for every change.

Contributing

Contributions made with AI tools are welcome, but a person must open, supervise and answer for every issue and pull request. See AGENTS.md.

License

MIT. See LICENSE.