JSON API (/api/v1)¶
The IVOA TAP endpoints speak the XML the VO standards mandate (VOTable,
UWS, VOSI) so TOPCAT, PyVO and astroquery work out of the box. For
machine-to-machine integration the service also exposes a JSON interface at
/api/v1, documented by the live OpenAPI spec at /openapi.json (Swagger
UI at /docs). Both interfaces share one engine: the same ADQL translator,
TAP_SCHEMA publication checks, MAXREC limits and UWS job store.
Queries¶
POST /api/v1/query
{"query": "SELECT TOP 5 * FROM ska.continuum_sources", "maxrec": 100}
returns {"metadata": [...], "data": [[...], ...], "status": "OK|OVERFLOW"},
with each metadata entry carrying datatype, unit, ucd and
description from TAP_SCHEMA. Set "format": "parquet" (or "arrow",
"votable", "csv", "tsv") for other output formats — Parquet responses
embed the column metadata in the Arrow schema and the DALI status in the
file metadata. Synchronous results larger than TAP_SYNC_MAX_BYTES are
refused; use a job for larger output.
Errors come back as JSON ({"error": "UsageError", "message": ...}) with
the same HTTP status codes as the DALI VOTable errors on /tap.
Jobs (asynchronous)¶
A JSON facade over the same UWS job store used by /tap/async — a job
created here is visible there and vice versa:
| Method & path | Purpose |
|---|---|
POST /api/v1/jobs {query, lang?, maxrec?, format?, run?} |
Create (and with run: true queue) a job; the query is validated up front |
GET /api/v1/jobs?phase=COMPLETED,ERROR&last=10 |
List jobs (100 by default, at most 1,000) |
GET /api/v1/jobs/{id} |
Job document (phase, timing, parameters, result/error) |
POST /api/v1/jobs/{id}/phase {"phase": "RUN"\|"ABORT"} |
Start or abort |
GET /api/v1/jobs/{id}/result |
Fetch the result file |
DELETE /api/v1/jobs/{id} |
Destroy the job |
Metadata¶
GET /api/v1/tables returns TAP_SCHEMA as JSON (schemas, tables, columns
with units/UCDs) — the machine-friendly twin of VOSI /tap/tables.
Metadata-domain plugins¶
Metadata domains are plugins: each binds an upstream pydantic model
package to a SQL schema and a mount point, and gets the same endpoint set
(full guide: Metadata plugins).
Two ship built in, and third-party model packages register through the
egernia.models entry-point group — installed alongside the services,
no changes to this codebase. TAP_MODEL_PLUGINS (Helm:
config.modelPlugins) selects what a deployment activates: all, or a
comma-separated subset for dedicated per-model systems.
| Plugin | Model package | Tables | Mount |
|---|---|---|---|
odp |
ska-src-mm-notification (Project → … → DataProduct → Artifact) |
srcnet.projects … srcnet.artifacts |
/api/v1/notifications |
software |
ska-src-sdm (Software → Artifact, embedded discovery/resources/provenance) |
srcnet.software, srcnet.software_artifacts |
/api/v1/software |
Every active plugin serves (shown for odp; identically for
/api/v1/software/{uri} etc.):
| Method & path | Purpose |
|---|---|
POST /api/v1/notifications |
Validate (with the plugin's pydantic models) and store a document; idempotent upsert |
GET /api/v1/notifications?limit=100&after={project_id} |
Root-level summaries with per-table row counts; at most 1,000, with next_after in non-final responses |
GET /api/v1/notifications/{project_id} |
Reconstruct the full nested document |
PATCH /api/v1/notifications/{project_id} |
Amend already-ingested rows: {"table", "match"?, "values"} |
DELETE /api/v1/notifications/{project_id} |
Delete the document and cascade to all generated child rows |
Invalid payloads are rejected with HTTP 422 and pydantic's structured
error list — including the library's cross-field rules (em_min <= em_max,
image products requiring s_ra/s_dec/s_fov, ...).
Amending ingested metadata¶
When a data-model release adds a field, existing tables gain the column
automatically at startup (nullable) — PATCH then backfills it, or fixes
any stored value, without re-sending whole notifications:
PATCH /api/v1/notifications/project12314
{"table": "data_products",
"match": {"product_id": "SKAO-19571257111"},
"values": {"beam_pa": 42.0}}
table is one of projects, observations, scheduling_blocks,
execution_blocks, data_products, artifacts; match narrows the rows
by column equality (omit it to update every row of the project); each
value in values is validated against the corresponding pydantic model
field, so amendments obey the same constraints as ingestion. Key columns
cannot be changed. The response reports the number of rows updated.
Re-POSTing a full notification remains the way to amend everything at
once (idempotent upsert).
DELETE /api/v1/notifications/{project_id} removes the root document; the
generated foreign keys cascade the deletion through observations,
scheduling/execution blocks, data products, and artifacts. It answers 200
with {"status": "deleted", "<id column>": "..."} — the same
report-what-happened shape as POST and PATCH — and 404 if the document
is not there, so deleting twice is not idempotent (unlike
DELETE /api/v1/jobs/{id}, which follows UWS and answers 204). Each
deletion is logged by the service.
There is no authentication layer yet, so DELETE — like every other
endpoint — is open to anyone who can reach the service, and deployments
must not expose it to untrusted networks. Package 4 of the
roadmap wires in the SRCNet flow: INDIGO IAM bearer tokens
validated against the issuer's JWKS, with authorisation delegated per route
to the SKA SRC Permissions
API
(IAM group membership via its /gms endpoint mapped onto roles). The
mutating endpoints — POST, PATCH and DELETE — are the first to be
protected, and the deletion audit record then carries the authenticated
subject.
Model-driven database schema¶
Each plugin's tables are generated from its pydantic models at startup
(egernia_core/metadata/schema_gen.py):
- each
list[Model]level becomes a child table with a composite primary key following the identity chain (*_idfields by convention, overridable per model class — the software plugin keys onuriand artifactlocation) and a cascading foreign key; - singular nested models are flattened into prefixed columns
(
resources.min_memory→resources_min_memory), reconstructed as nested objects on fetch; - pydantic types map to PostgreSQL types;
Ge/Gt/Le/Ltconstraints and enums becomeCHECKconstraints, so the database enforces the same invariants the library validates; - every generated table (and key) is registered in
TAP_SCHEMA, making the ingested metadata immediately queryable through TAP/ADQL and the JSON query endpoint, and visible in VOSI/tap/tables:
SELECT p.product_id, a.artifact_id, a.access_url
FROM srcnet.data_products AS p
JOIN srcnet.artifacts AS a
ON p.project_id = a.project_id
AND p.eb_id = a.eb_id
AND p.product_id = a.product_id
WHERE p.dataproduct_type = 'cube'
Upgrading the library to a schema version with new fields adds the new columns/tables on the next startup (existing columns are never dropped).