Skip to content

Releases: ralforion/orionbelt-runner

v0.8.1 — Arrow transport for typed exports; DECIMAL and warning fixes

Choose a tag to compare

@ralfbecher ralfbecher released this 25 Jul 22:16
52c2f75

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

Choose a tag to compare

@ralfbecher ralfbecher released this 25 Jul 21:37
625fa5b

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 / Ceph

One 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

Choose a tag to compare

@ralfbecher ralfbecher released this 21 Jul 15:45
f0c877f

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 label as an authorable OBML property; no example uses it, and /v1/measures still returns name.
  • 2.19 auto-synthesized count measures and 2.21 content-derived model_id are tolerated (extra="ignore", opaque strings).
  • query-schema.json unchanged 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

Choose a tag to compare

@ralfbecher ralfbecher released this 15 Jul 17:36
258bf29

A build and packaging release. No functional change to the runnersrc/ 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.lock is 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 with uv sync --frozen — exactly the versions CI tested.
  • The uv build tool is pinned to 0.11.28 instead of tracking :latest, so the tool installing the lockfile can't drift either.
  • CI enforces the lock with uv sync --locked, which fails if uv.lock drifts from pyproject.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-python floor, 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, and docker.

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

Choose a tag to compare

@ralfbecher ralfbecher released this 25 Jun 10:44
94d34ca

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 422 on a violation.
  • New ObslSchemaError surfaces those per-field errors with a readable message and a hint (camelCase keys, lowercase enums, numeric model version).
  • Query field order_by is now orderBy (the last snake_case query key; the query schema rejects order_by). The bundled examples/monthly-revenue.yaml was updated.
  • Re-synced the vendored OBML schema from 2.16 (camelCase-only — the snake_case max_staleness / intent_tags aliases were dropped).

Compatibility pin

  • SUPPORTED_OBSL_MINOR bumped to 16; the startup /health preflight now requires the server to be in the 2.16.x line (distinct too-old / too-new messages), with --skip-preflight to bypass.

Compatibility: requires OrionBelt Semantic Layer 2.16.x.

v0.5.0 — OBSL 2.12 auth + startup compatibility preflight

Choose a tag to compare

@ralfbecher ralfbecher released this 15 Jun 09:57
ed3fe84

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-Key by default — configurable via obsl.api_key_header / OBSL_API_KEY_HEADER (Authorization: Bearer stays a server-side fallback).
  • New obsl.api_key spec field and OBSL_API_KEY env var (matches the OBSL UI / MCP). The pre-2.12 api_token / OBSL_API_TOKEN naming has been removed.
  • 401 / 403 responses surface as ObslAuthError with OBSL's structured {code, message} and a hint about which key/header was sent.

Startup compatibility preflight

  • orionbelt-runner run calls the unauthenticated GET /health before 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 reports AUTH_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

Choose a tag to compare

@ralfbecher ralfbecher released this 26 May 09:31

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...v0.4.0

v0.3.1 — configurable PDF page size + orientation

Choose a tag to compare

@ralfbecher ralfbecher released this 24 May 15:49

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...v0.3.1

v0.3.0 — PDF reports + smarter table alignment

Choose a tag to compare

@ralfbecher ralfbecher released this 24 May 15:20

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 measuretype == "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'

Full diff

fc8a737...v0.3.0