Skip to content

Values API

This document describes the Values API - a declarative data fetching system for the CELINE Digital Twin runtime.

The Values API allows you to expose data queries as REST endpoints declaratively: a ValueFetcherSpec describes the query, its input schema and its output mapping, and the runtime mounts the endpoint.

Paths and declarations below are partly stale, verified against the code on 2026-08-15. The semantics — templating, payload schemas, defaults, mappers, pagination — are accurate and tested. The surface is not:

  • There is no global /values/... endpoint. Fetchers are entity-scoped and mounted per domain: /{route_prefix}/{entity_id}/values/{fetcher_id}, on port 8002. Every curl below showing localhost:8000/values/... is wrong on both counts, and all of them require a bearer token.
  • Fetchers are not declared in YAML. There is no config/values.yaml and nothing reads one. A domain returns ValueFetcherSpec objects from get_value_specs(); the YAML block under "Quick Start" and "Configuration Reference" is the shape of that dataclass, not of a file.
  • "Module-Scoped Fetchers" is superseded. There are no modules. Fetchers are namespaced {domain.name}.{id} in the registry, but the URL path takes the domain-local id.

The mounted surface is docs/domains.md. What must hold is docs/specifications/values.md and docs/specifications/query-templates.md.


Overview

Value fetchers: - Are declared in code, by a domain's get_value_specs(). There is no config/values.yaml; nothing reads one - Reference clients by key from config/clients.yaml, and startup fails on a key that file does not declare - Support parameterized queries with :param syntax - Validate inputs using JSON Schema - Transform outputs using mappers - Are exposed via REST API


Quick Start

1. Define a fetcher

# config/values.yaml
values:
  weather_forecast:
    client: dataset_api
    query: |
      SELECT * FROM weather_forecasts
      WHERE location = :location
        AND forecast_date >= :start_date
      ORDER BY forecast_date
    limit: 100
    payload:
      type: object
      required:
        - location
      properties:
        location:
          type: string
        start_date:
          type: string
          default: "2024-01-01"

2. Use the API

# GET with query parameters
curl "http://localhost:8000/values/weather_forecast?location=folgaria"

# POST with JSON body
curl -X POST http://localhost:8000/values/weather_forecast \
  -H "Content-Type: application/json" \
  -d '{"location": "folgaria", "start_date": "2024-06-01"}'

3. Response format

{
  "items": [
    {"location": "folgaria", "forecast_date": "2024-06-01", "temp": 22.5},
    {"location": "folgaria", "forecast_date": "2024-06-02", "temp": 24.0}
  ],
  "limit": 100,
  "offset": 0,
  "count": 2
}

Configuration Reference

Fetcher specification

values:
  <fetcher_id>:
    client: <client_name>        # Required: client from clients.yaml
    query: <query_template>      # Query with :param placeholders
    limit: <number>              # Default: 100
    offset: <number>             # Default: 0
    payload: <json_schema>       # Optional: input validation schema
    output_mapper: <import_path> # Optional: output transformation

Field descriptions

Field Required Default Description
client Yes - Client name from config/clients.yaml
query No - Query template (SQL or client-specific)
limit No 100 Default result limit
offset No 0 Default pagination offset
payload No - JSON Schema for input validation
output_mapper No - Import path to output mapper class

Query Templates

Parameter syntax

Use :param_name for named parameters:

query: |
  SELECT * FROM users
  WHERE department = :department
    AND status = :status
    AND created_at > :since

Parameter substitution

Parameters are safely quoted based on their type:

Type Example Quoted as
String "hello" 'hello'
Integer 42 42
Float 3.14 3.14
Boolean true TRUE
Null null NULL
List [1, 2, 3] (1, 2, 3)

String escaping

Single quotes in strings are escaped:

# Input: {"name": "O'Brien"}
# Query: WHERE name = :name
# Result: WHERE name = 'O''Brien'

Payload Schema

Define input validation using JSON Schema:

payload:
  type: object
  additionalProperties: false
  required:
    - location
  properties:
    location:
      type: string
      description: Location identifier
    start_date:
      type: string
      format: date
      default: "2024-01-01"
    limit:
      type: integer
      minimum: 1
      maximum: 1000
      default: 100
    active:
      type: boolean
      default: true

Supported types

JSON Schema Type GET coercion Example
string As-is ?name=test"test"
integer Parse int ?count=4242
number Parse float ?price=9.999.99
boolean true/false/1/0 ?active=truetrue
array Comma-separated ?ids=1,2,3[1,2,3]
null empty/"null" ?val=null

Defaults

Defaults are applied for missing parameters:

properties:
  status:
    type: string
    default: "active"  # Used if not provided

Required fields

Missing required fields return 400 Bad Request:

required:
  - location  # Must be provided

API Endpoints

List fetchers

GET /values

Response:

[
  {"id": "weather_forecast", "client": "dataset_api", "has_payload_schema": true},
  {"id": "ev_charging.solar", "client": "dataset_api", "has_payload_schema": false}
]

Describe fetcher

GET /values/{fetcher_id}/describe

Response:

{
  "id": "weather_forecast",
  "client": "dataset_api",
  "query": "SELECT * FROM weather_forecasts WHERE location = :location",
  "limit": 100,
  "offset": 0,
  "payload_schema": {
    "type": "object",
    "required": ["location"],
    "properties": {
      "location": {"type": "string"}
    }
  },
  "has_output_mapper": false
}

Fetch with GET

GET /values/{fetcher_id}?param1=value1&param2=value2&limit=10&offset=0
  • Parameters are coerced based on schema
  • limit and offset are reserved for pagination
  • Unknown parameters are passed through if additionalProperties: true

Fetch with POST

POST /values/{fetcher_id}?limit=10&offset=0
Content-Type: application/json

{"param1": "value1", "param2": 42}
  • Body is validated against payload schema
  • limit and offset can be query params

Module-Scoped Fetchers

Modules can define their own fetchers, namespaced by module name.

Definition in module config

# config/modules.yaml
modules:
  - name: ev-charging
    version: ">=1.0.0"
    import: celine.dt.modules.ev_charging.module:module
    values:
      solar_forecast:
        client: dataset_api
        query: SELECT * FROM solar WHERE lat = :lat AND lon = :lon
        payload:
          type: object
          required: [lat, lon]
          properties:
            lat:
              type: number
            lon:
              type: number

Access via API

Module fetchers are namespaced as {module_name}.{fetcher_id}:

curl "http://localhost:8000/values/ev-charging.solar_forecast?lat=45.9&lon=11.1"

Precedence

  • Root-level fetchers (from values.yaml) have no prefix
  • Module fetchers are always prefixed
  • Root-level fetchers cannot override module fetchers (different namespaces)

Output Mappers

Transform results before returning:

values:
  users:
    client: dataset_api
    query: SELECT * FROM users
    output_mapper: my.module.mappers:UserOutputMapper

Mapper implementation

# my/module/mappers.py
from celine.dt.contracts.mapper import OutputMapper


class UserOutputMapper(OutputMapper):
    output_type = dict

    def map(self, result: dict) -> dict:
        return {
            "userId": result["id"],
            "fullName": f"{result['first_name']} {result['last_name']}",
            "email": result["email"],
        }

The mapper is applied to each item in the result.


Error Handling

400 Bad Request

Returned for: - Missing required parameters - Type coercion failures - Schema validation failures - Missing query parameters

{
  "detail": "Missing required parameter: 'location'"
}

404 Not Found

Returned when fetcher doesn't exist:

{
  "detail": "Fetcher 'nonexistent' not found"
}

500 Internal Server Error

Returned for: - Client query failures - Output mapper errors - Unexpected exceptions


Best Practices

1. Use meaningful IDs

# Good
values:
  weather_forecast_hourly:
  energy_production_daily:

# Avoid
values:
  data1:
  query2:

2. Always define payload schemas

Even for simple fetchers, schemas provide: - Input validation - Type coercion for GET requests - Self-documenting API via /describe

3. Set appropriate limits

values:
  large_dataset:
    client: dataset_api
    query: SELECT * FROM events
    limit: 100  # Reasonable default, not 10000

4. Use defaults for optional parameters

payload:
  properties:
    status:
      type: string
      default: "active"  # Sensible default
    days:
      type: integer
      default: 7

5. Document with descriptions

payload:
  type: object
  properties:
    location:
      type: string
      description: "Location identifier (e.g., 'folgaria', 'trento')"
    window_hours:
      type: integer
      description: "Forecast window in hours"
      minimum: 1
      maximum: 168

6. Prefer POST for complex queries

  • GET is great for simple queries with few parameters
  • POST is better for complex payloads or sensitive data

Examples

Simple lookup

values:
  location_info:
    client: dataset_api
    query: SELECT * FROM locations WHERE id = :id
    payload:
      type: object
      required: [id]
      properties:
        id:
          type: string

Time-range query

values:
  energy_readings:
    client: dataset_api
    query: |
      SELECT timestamp, value, unit
      FROM energy_readings
      WHERE meter_id = :meter_id
        AND timestamp >= :start
        AND timestamp < :end
      ORDER BY timestamp
    limit: 1000
    payload:
      type: object
      required: [meter_id, start, end]
      properties:
        meter_id:
          type: string
        start:
          type: string
          format: date-time
        end:
          type: string
          format: date-time

Aggregation query

values:
  daily_summary:
    client: dataset_api
    query: |
      SELECT 
        date_trunc('day', timestamp) as day,
        SUM(value) as total,
        AVG(value) as average
      FROM readings
      WHERE location = :location
        AND timestamp >= :since
      GROUP BY 1
      ORDER BY 1
    payload:
      type: object
      required: [location]
      properties:
        location:
          type: string
        since:
          type: string
          format: date
          default: "2024-01-01"

Multi-value filter

values:
  filtered_items:
    client: dataset_api
    query: |
      SELECT * FROM items
      WHERE category IN :categories
        AND status = :status
    payload:
      type: object
      required: [categories]
      properties:
        categories:
          type: array
          items:
            type: string
        status:
          type: string
          default: "active"

Usage:

# GET with comma-separated array
curl "http://localhost:8000/values/filtered_items?categories=a,b,c"

# POST with JSON array
curl -X POST http://localhost:8000/values/filtered_items \
  -d '{"categories": ["a", "b", "c"]}'