Add raw output format to the API - #17
Conversation
_format=raw returns just the contents of the columns requested via _fields, one record per line with no ids or metadata wrapper, so that scripts (in particular PARI/GP, the original request) can consume query results directly. Values are JSON-encoded, making each single-field line a valid GP expression readable with readvec; multiple fields are joined by _delim. Requesting raw without _fields gives a 400 error. Documented on the API index page and linked from table pages when _fields is present. Verified with a new test_api_raw in lmfdb/api/test_api.py (all 7 API tests pass against devmirror) and by reading saved raw responses back into sage --gp with readvec/readstr. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DB#1010) The raw format previously joined individually-JSON-encoded fields with the query delimiter, producing lines like `[0,-1,1,-10,-20],11` where commas appear both inside and between fields: not valid JSON, not a single GP expression, and unparseable when a string value contains the delimiter. Multiple fields are now emitted as one JSON array per line (JSON Lines), so every record is self-delimiting and round-trips through json.loads even when a value contains the delimiter. Single-field output is unchanged (one JSON value per line), preserving the PARI/GP readvec round-trip; the docs now scope the GP-compatibility claim to the single-field case. Tests add json.loads round-trips and a delimiter-in-value case; the single-field GP round-trip was re-verified in sage --gp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the external review's P2 finding (multi-field raw records not parseable): commit fa953ee makes multi-field raw output emit one JSON array per line (JSON Lines), so each record is self-delimiting regardless of delimiters inside values; single-field output is unchanged. Stayed within this PR's hunks — no overlap with #18/#22/#40 regions. |
Raw records were rendered from Json.prep's output, so a Postgres numeric
arrived as the extended-JSON object that records how to rebuild the Sage
real, e.g. {"__RealLiteral__": 0, "data": "-0.308...", "prec": 97} for
ec_curvedata.faltings_height. That is the wrong rendering for a format
that promises the contents of the requested columns, and it is not what a
PARI/GP client expects.
Records are now rendered from the database values, by a small serializer
that copies a real number's stored decimal verbatim (never through a
float, so nothing is rounded) once it has checked the literal against the
JSON number syntax; everything else is JSON-encoded as before, and values
with no plain JSON rendering keep psycodict's extended encoding. Lists
recurse, so a numeric[] column is exact too. A raw request also projects
exactly the requested fields rather than prepending id, which the body
never showed; a record whose projected columns are all NULL is therefore
an empty dictionary, so the by-id route now tests lucky's result against
None rather than for emptiness. Absent keys are still read with .get:
most tables set include_nones off, so psycodict omits a NULL column
rather than storing None, and an unknown field is rejected before the
query runs.
The docs were stale in two ways: the PR description still described
delimiter-joined multi-field output, and the code comments, test
docstring and API page claimed PARI/GP readvec could consume any
single-field response. readvec evaluates lines as GP code, so `true` and
`false` read as symbolic variables rather than booleans (checked in
sage --gp); the claim is now scoped to integers and nested arrays of
integers, which do round-trip. The API page also notes that raw bodies
carry no next entry and that _offset advances the page, and _delim is
described as parsing request parameters only. The Raw link on table pages
now requires a nonempty _fields, since _fields= gives a 400.
Verified: sage -python -m pytest lmfdb/api/test_api.py -> 7 passed;
pyflakes and ruff clean; test_api_raw gains exact-numeric, boolean and
NULL cases; raw numeric, boolean, string, integer-array, numeric-array,
dotted-path, by-id and multi-field responses inspected by hand, every
line parsing as JSON; readvec re-checked in sage --gp on 100 ainvs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 182d10b addressing the review. 1. Exact numerics. Raw records are now rendered from the database rows, before instead of the 2. Docs and the GP claim. The code comments, 3. Raw link condition. One deviation, on point 5. Projecting exactly the requested fields (point 6) exposed a related edge case: a record whose projected columns are all NULL comes back from Verification. The remaining checklist item I cannot tick off is the Actions run: this fork's queue is currently around 85 runs deep and the run for 182d10b has not started yet. |
raw_json_dumps special-cased RealNumber, but psycodict does not hand back
a numeric that is exactly zero as one: numeric_converter returns an
LmfdbDecimalZero, an integer wrapper keeping the literal in .literal.
Such a value fell through to Json.prep, which sees a Sage integer and
emits 0, dropping the stored scale and, for -0.000, the sign, whether the
zero stood alone or sat inside a numeric[], a list or a dictionary. That
contradicted the format's promise that a numeric is the decimal Postgres
sent, copied verbatim.
The literal lookup moves into exact_decimal(), which handles the integer
wrapper alongside RealLiteral and RealNumber and validates the literal
against the JSON number syntax once, so nothing is rebuilt from a float
or an int. A numeric with no decimal point is still a Sage integer and is
unaffected; NaN and the infinities still fall back to psycodict's
extended encoding rather than emit an unparseable token.
test_api.py gains test_raw_json_dumps, calling the serializer directly so
the behaviour does not depend on finding a zero-valued row: 0.000, -0.000
and 1.250 come back verbatim, as do zeros nested in an array and in an
object, and each result is re-parsed as JSON. The exact strings are the
point, since Decimal("-0.000") == Decimal("0.000").
Verified: sage -python -m pytest lmfdb/api/test_api.py -> 8 passed;
pyflakes and ruff clean; /api/lfunc_lfunctions/?_format=raw&_fields=
mu_imag,analytic_normalization now gives [[0.0], 0] rather than [[0], 0].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 30b24a7 for the decimal-zero case. You are right, and I had made exactly the wrong call on it: I saw The literal lookup now lives in >>> raw_json_dumps(numeric_converter("0.000"))
'0.000'
>>> raw_json_dumps(numeric_converter("-0.000"))
'-0.000'
>>> raw_json_dumps([numeric_converter("1.250"), numeric_converter("0.000")])
'[1.250, 0.000]'A
On the last checklist item: the Actions runs for |
|
GPT signed off. |
|
Superseded by LMFDB#7153, opened upstream from this same branch. Closing here; review continues upstream. |
This adds
_format=rawto the API (alongside html/json/yaml), returning only the contents of the columns requested via_fields, as requested by Bill Allombert for consumption from PARI/GP scripts.Raw output is newline-delimited JSON without the usual response envelope or implicit
id. With one requested field, each line is that field's JSON value. With several fields, each line is an ordered JSON array containing those field values._delimis used only to parse parameters such as_fieldsand_sort; it does not delimit output records or fields, so a record stays unambiguous even when a value contains the delimiter or a newline.Records are rendered from the database rows rather than from
Json.prep's output, so a Postgresnumericis a JSON number carrying every stored digit:ec_curvedata.faltings_heightfor11a1comes out as-0.30800984111840306468901426146, not as the__RealLiteral__object that records how to rebuild the Sage real. The decimal is copied verbatim, never through a float. Values with no plain JSON rendering (rationals, number field elements, dates) keep psycodict's extended encoding.For fields whose emitted JSON notation is also valid GP syntax, notably integers and nested arrays of integers, the response can be read directly with PARI/GP
readvec(verified in gp). Other values should be parsed as JSON; a JSON boolean in particular is not a GP boolean.Raw requests remain limited to 100 records and omit pagination metadata, so
_offsetis advanced to retrieve subsequent pages; requesting raw output without a nonempty_fieldsgives a 400 error. The format is documented on the API index page, table pages link to it whenever_fieldsis nonempty, andtest_api.pygains exact-output tests covering numerics, booleans, nulls, integer arrays, multi-field records, a delimiter inside a value and the error case. Addresses LMFDB#1010.🤖 Generated with Claude Code