Releases: ralforion/orionbelt-runner
Release list
v0.8.1 — Arrow transport for typed exports; DECIMAL and warning fixes
Patch release on top of v0.8.0. Anyone using exports: with parquet or arrow should upgrade — 0.8.0 wrote DECIMAL columns with the wrong type.
Fixed — DECIMAL landed in Parquet as string
OBSL delivers governed DECIMAL cells in raw JSON as exact decimal strings rather than floats (so precision past float64's ~15-16 digits survives the wire) and reports the column as decimal(18, 2). The 0.8.0 exporter only special-cased datetime / binary, so those columns fell through to inference and were written as text — every money measure got the wrong Parquet schema.
Values were never lost or corrupted: the strings are exact, and re-running after upgrading rewrites the files with the right types. Reports, TSV exports, and run logs were unaffected.
Fixed — a query carrying a warning failed the run
OBSL returns structured warnings — {code, severity, message, path, hint, context} — on execute, compile, and model-load, while the runner declared warnings: list[str]. Pydantic rejected the whole response, so the query was recorded as failed and the report never rendered.
This one is not a 0.8.0 regression: StructuredWarning has been in OBSL since 2.2, so warning-carrying queries have been failing across every earlier runner release. Warnings now flatten to "code: message (hint: …)" in the run log and plain strings still parse.
Added — typed exports read OBSL's Arrow transport
Parquet and Arrow targets now request ?format=arrow and decode OBSL's result frame:
[u32 big-endian json_len][JSON envelope utf-8][gzip'd Arrow IPC stream]
The server's table is written through untouched — real decimal128 / timestamp / int64, no JSON round trip and no client-side inference. The JSON envelope is a full ExecuteResult minus rows, so the run log is unchanged.
A deployment that ignores the parameter answers JSON; that's detected by content type and handled with the inference path in the same request (no retry), which is why the DECIMAL fix above still applies there. That fallback yields double where the transport gives exact decimal128.
Compatibility
No spec changes — every 0.8.0 spec runs unmodified. The OBSL pin is unchanged: 0.8.x ↔ OBSL 2.23.x. The Arrow transport needs OBSL >= 2.21, comfortably inside the supported line.
Verified: ruff, ruff format, mypy, pytest on 3.12 and 3.14 (127 passed, 1 skipped); end-to-end against a stub speaking the real frame writes Revenue: decimal128(18, 2) with exact values and parses a structured warning from the same envelope.
Full changes: v0.8.0...v0.8.1
v0.8.0 — data exports: Parquet / Arrow / TSV to a folder or S3
Query results can now be written as Parquet, Arrow IPC, or TSV into a local folder or an S3 bucket (or any S3-compatible store), with an export-only run mode for jobs that feed a warehouse rather than a reader.
exports: — a list of destinations
exports:
- format: parquet # parquet | arrow | tsv
uri: exports/{name}/{date}/ # local folder
compression: zstd
- format: parquet
uri: s3://analytics-landing/orionbelt/{date}/
region: eu-central-1
# endpoint_override: http://localhost:9000 # MinIO / R2 / CephOne file per query per target, named after the query, written on a fully successful run. uri takes the same placeholders as report.output and names a directory prefix; local relative paths rebase under --output-dir like the report path. Several targets can run side by side.
Natively typed values, without paying twice
Parquet and Arrow are typed formats, so those targets execute with format_values=false and write double / int64 / timestamp[us] rather than locale-formatted strings. The runner executes each query only as often as the spec actually needs:
| spec | executes per query |
|---|---|
report (or a tsv target) only |
1 — formatted |
report (or tsv) + parquet / arrow |
2 — formatted for the report, raw for the export |
no_report: true with only parquet / arrow |
1 — raw |
Report runs are unchanged: they still render from the formatted call, so OBSL stays authoritative for display.
Export-only runs — no_report: true
Queries execute and exports are written, no report is rendered, and the run log still lands (that's the audit trail). report: becomes optional in that mode. See examples/revenue-export-only.yaml.
Failures are visible
An unreachable bucket or a failed raw re-execute is logged and skipped — never discarding an already-rendered report — but the CLI then prints what didn't land and exits 1, so a scheduled job whose data never arrived can't look like a clean run. A query whose raw re-execute failed is omitted from typed targets rather than written with string columns, so a downstream schema never silently changes shape.
Packaging
- New optional extra:
uv sync --extra arrow(pyarrow >= 16) covers both the file formats and the S3 client. Core markdown / HTML / TSV runs stay dependency-free. - The Docker image now bundles pyarrow, so Parquet / Arrow and
s3://destinations work out of the box in a scheduled container — the image is correspondingly larger. PDF still isn't bundled (WeasyPrint needs Pango / Cairo system libraries). - S3 credentials come only from the standard AWS chain (env vars,
~/.aws, instance / task / IRSA role) — never from the spec, so spec files stay safe to commit.
Compatibility
Backward compatible: exports: defaults to empty and report: is still required unless no_report: true, so every 0.7.0 spec behaves identically. The OBSL pin is unchanged — 0.8.x ↔ OBSL 2.23.x. This runner minor advances on its own; the two version numbers don't move in lockstep.
Verified: ruff, ruff format, mypy, pytest on 3.12 and 3.14 (108 passed, 1 skipped); end-to-end against a stub OBSL server producing typed Parquet.
Full changes: v0.7.0...v0.8.0
v0.7.0 — track OBSL 2.23.x
Bumps the runner's supported OBSL minor series from 2.16.x → 2.23.x and its own minor line from 0.6.x → 0.7.0, in lockstep per the compatibility convention.
The 2.16→2.23 REST contract is runner-compatible — only the version pin changed. Reviewed across the range:
- 2.23 DECIMAL cells now serialize as exact decimal strings in raw JSON, but the runner always sends
format_values=true, so it keeps receiving locale-formatted display strings (and raw is safe anyway). - 2.22.2 dropped
labelas an authorable OBML property; no example uses it, and/v1/measuresstill returnsname. - 2.19 auto-synthesized count measures and 2.21 content-derived
model_idare tolerated (extra="ignore", opaque strings). query-schema.jsonunchanged since 2.16.
Vendored schema/obml-schema.json re-synced from the OBSL 2.23.0 clone.
Verified: ruff, ruff format, mypy, pytest on 3.12 (63 passed, 1 skipped); Docker image published for 0.7.0.
Full changes: v0.6.1...v0.7.0
v0.6.1 — Python 3.14 image + reproducible builds
A build and packaging release. No functional change to the runner — src/ is untouched apart from __version__, and OBSL compatibility is unchanged.
⚠️ The image now runs Python 3.14
The base image moves from Python 3.12 → 3.14. Because the minor line is pinned to OBSL (0.6.x ↔ OBSL 2.16.x), this ships as a patch — but the interpreter jump is larger than a patch tag suggests.
:latest, :0.6, and :0 all now resolve to a Python 3.14 image. If you pin to any of those and depend on the interpreter, pin to :0.6.0 instead, which stays on 3.12.
The full test suite passes on both 3.12 and 3.14, and the published image was pulled and verified end-to-end.
Reproducible builds
uv.lockis now committed. Previously gitignored, so image builds re-resolved dependencies on every run and the same commit could produce different images over time. The image now installs the lockfile withuv sync --frozen— exactly the versions CI tested.- The uv build tool is pinned to
0.11.28instead of tracking:latest, so the tool installing the lockfile can't drift either. - CI enforces the lock with
uv sync --locked, which fails ifuv.lockdrifts frompyproject.toml.
Locking the baseline picked up current releases of the dev/runtime stack, including mypy 2.3.0 and structlog 26.1.0. Both verified against the full suite.
CI
- Tests now run on a 3.12 / 3.14 matrix — 3.12 is the
requires-pythonfloor, 3.14 is what the image ships. - Every PR now builds the image and smoke-tests it (entrypoint + all runtime imports), instead of Dockerfile changes going unexercised until release.
- Dependabot is configured for
uv,github-actions, anddocker.
Compatibility
Requires OrionBelt Semantic Layer 2.16.x — unchanged from 0.6.0.
Full changelog: v0.6.0...v0.6.1
v0.6.0 — OBSL 2.16 camelCase contract + schema-validation errors
Aligns the runner with OrionBelt Semantic Layer v2.16 and pins the 0.6.x line to OBSL 2.16.x.
camelCase contract
- OBSL 2.16 validates model-load and query payloads against its published JSON Schemas at the ingestion boundary and returns a structured
422on a violation. - New
ObslSchemaErrorsurfaces those per-field errors with a readable message and a hint (camelCase keys, lowercase enums, numeric modelversion). - Query field
order_byis noworderBy(the last snake_case query key; the query schema rejectsorder_by). The bundledexamples/monthly-revenue.yamlwas updated. - Re-synced the vendored OBML schema from 2.16 (camelCase-only — the snake_case
max_staleness/intent_tagsaliases were dropped).
Compatibility pin
SUPPORTED_OBSL_MINORbumped to 16; the startup/healthpreflight now requires the server to be in the 2.16.x line (distinct too-old / too-new messages), with--skip-preflightto bypass.
Compatibility: requires OrionBelt Semantic Layer 2.16.x.
v0.5.0 — OBSL 2.12 auth + startup compatibility preflight
Aligns the runner with OrionBelt Semantic Layer v2.12 unified authentication and pins the 0.5.x line to OBSL 2.12.x.
Authentication
- API key sent in
X-API-Keyby default — configurable viaobsl.api_key_header/OBSL_API_KEY_HEADER(Authorization: Bearerstays a server-side fallback). - New
obsl.api_keyspec field andOBSL_API_KEYenv var (matches the OBSL UI / MCP). The pre-2.12api_token/OBSL_API_TOKENnaming has been removed. 401/403responses surface asObslAuthErrorwith OBSL's structured{code, message}and a hint about which key/header was sent.
Startup compatibility preflight
orionbelt-runner runcalls the unauthenticatedGET /healthbefore any query and verifies the server is in the supported 2.12.x line (distinct too-old / too-new messages) and that a key is configured when the server reportsAUTH_MODE=api_key.- Bypass with
--skip-preflight.
Docs & licensing
- README: OrionBelt logo + badges, Authentication and Compatibility sections, License section with RALFORION footer.
- Added BSL 1.1
LICENSE(mirrors OBSL; converts to Apache 2.0 on 2030-03-16).
Compatibility: requires OrionBelt Semantic Layer 2.12.x.
v0.4.0 — left-align coded-integer numeric columns
Bug-fix release. Refines the table-alignment heuristic so coded integers (YYYYMM reporting periods, zero-padded IDs) stay left-aligned instead of being mistaken for measures.
What changed
Previously, any numeric column with a format pattern set in the OBML model was right-aligned. That's correct for quantities (#,##0.00, 0.00%, $#,##0) but wrong for coded integers carrying a bare "0" or "000000" format — those read as identifiers, not amounts, and should sit left alongside their text neighbours.
The new rule: right-align only when the format string contains a character outside 0 / # (i.e. a separator, decimal point, percent sign, or currency symbol).
| Column type | type |
format |
v0.3.x | v0.4.0 |
|---|---|---|---|---|
String (Country) |
string |
– | left | left |
Bare numeric ID (Order Key) |
number |
– | left | left |
Coded integer (Reporting Period) |
number |
"0" / "000000" |
right | left |
Quantity (Revenue) |
number |
#,##0.00 |
right | right |
Percentage (PD) |
number |
0.00% |
right | right |
Currency (Amount) |
number |
$#,##0 |
right | right |
Only the Coded integer row changes behaviour — all other columns render exactly as before.
Migration
No spec changes required. If you previously worked around this by stripping format: "0" from period dimensions in your OBML model, you can put it back — format_values=true still uses it, and the runner now treats it as a non-quantity format for alignment purposes.
Install
pip install orionbelt-runner==0.4.0
# with the PDF extra:
pip install 'orionbelt-runner[pdf]==0.4.0'Full diff
v0.3.1 — configurable PDF page size + orientation
Minor additive release. Two new optional fields on `ReportSpec`, both defaulting to current behaviour so existing specs render identically.
What's new
```yaml
report:
format: pdf
output: reports/{name}-{date}.pdf
title: "Monthly Revenue — {date}"
pdf_page_size: A3 # "A4" (default) or "A3"
pdf_orientation: landscape # "portrait" (default) or "landscape"
```
| Field | Values | Default |
|---|---|---|
| `pdf_page_size` | `A4`, `A3` | `A4` |
| `pdf_orientation` | `portrait`, `landscape` | `portrait` |
Reach for A3 or landscape when a table has many columns or wide cell values that wrap awkwardly in A4 portrait — the same content, just more horizontal room. Both fields are ignored for `markdown` / `html` output.
Under the hood
The previously-static `_PRINT_CSS` constant became a `_print_css(page_size, orientation)` function that builds the `@page { size: ...; }` rule dynamically (`A4` / `A4 landscape` / `A3` / `A3 landscape`). `render_pdf` reads the two new spec fields and threads the built CSS through `render_html`'s `extra_css` parameter — no new runtime dependencies, no breaking changes.
Install
```bash
pip install orionbelt-runner==0.3.1
with the PDF extra:
pip install 'orionbelt-runner[pdf]==0.3.1'
```
Full diff
v0.3.0 — PDF reports + smarter table alignment
First tagged release. Two user-visible additions on top of v0.2.0.
PDF report output
format: pdf reuses the HTML pipeline via WeasyPrint, so PDF layout stays in lockstep with HTML automatically. A print-only stylesheet adds A4 margins, a page n / total footer, and page-break hints on h2 / tr so tables don't get orphaned across pages.
report:
format: pdf
output: reports/{name}-{date}.pdf
title: "Monthly Revenue — {date}"PDF is gated behind an optional extra because WeasyPrint depends on system libraries (Pango / Cairo / GDK-Pixbuf) that not every install target carries:
uv sync --extra pdf # also: brew install pango (macOS)On Apple Silicon, Homebrew installs libs to /opt/homebrew/lib, which Python's loader doesn't search by default — prefix the runner with DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib or export it in your shell. See README → Install for full platform notes.
Sample output: examples/monthly-revenue-2026-04-29.pdf.
Right-aligned numeric columns (only when they should be)
Table cells right-align when a column is a formatted numeric measure — type == "number" and format is set on the column metadata. Bare integer IDs (Order Key, Customer ID) come back from OBSL without a format pattern and stay left-aligned with their text neighbours, so reports don't look like "DE | 52965 | 5.000,00" with a misaligned ID column.
Uses GFM ---: syntax in the markdown table separator so the alignment hint propagates to HTML and PDF automatically via Python-Markdown's tables extension. CSS adds font-variant-numeric: tabular-nums so digits line up vertically in HTML/PDF.
| Column kind | Example | Alignment |
|---|---|---|
| String | Country, Customer Name |
left |
| Datetime / binary | Order Date |
left |
Number with format |
Revenue (#,##0.00) |
right |
Number without format |
Order Key (bare integer ID) |
left |
Install
pip install orionbelt-runner==0.3.0
# or, with the PDF extra:
pip install 'orionbelt-runner[pdf]==0.3.0'