Models
Every piece of data in CentauroDB is a Pydantic model. You define your schema as a Python class, and CentauroDB handles the rest — serialization, storage, versioning, and deserialization. This guide covers how to define models correctly and which base class to choose.
Defining your first model
Inherit from CentauroModel and set a __centauro_name__:
from centaurodb import CentauroModel
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = ""
location: str = ""
threshold: float = 100.0
That's it — this model is ready to be stored in a Collection.
The three rules
CentauroDB enforces three constraints at class-definition time. Break any of them and you get a clear error immediately — not a runtime surprise weeks later.
Every model needs a __centauro_name__
This ClassVar[str] is the model's stable identity in the database — it's stored in the name column and used to filter rows in views and name-based queries. Without it, renaming a Python class would silently change the stored name, causing views to miss old rows with no error or warning.
class SensorReading(CentauroModel):
__centauro_name__ = "Sensor" # DB identity stays "Sensor"
unit: str = ""
# Even after renaming the class, old rows still match
# because __centauro_name__ hasn't changed
All fields must have defaults
Without defaults, old rows that lack a new field would fail to load. Defaults make adding fields safe — old data simply gets the default value on read. A field without a default raises TypeError at class definition time.
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
unit: str = "" # OK — default = ""
location: str = "unknown" # OK — default = "unknown"
pages: int | None = None # OK — Optional with None
rating: float # TypeError at class definition
extra="forbid" is banned
Old rows may contain fields that no longer exist in your current model. Pydantic needs to silently ignore them, so extra="forbid" would cause read failures. CentauroDB rejects it at class definition time.
Working with stored objects
After writing or reading an object, it carries database metadata in the row attribute:
from centaurodb import Engine, Collection
engine = Engine("monitoring.db")
coll = Collection(engine, "monitoring")
obj = Sensor(unit="Celsius", location="pelion")
coll.write_object(obj)
print(obj.row.id) # int — auto-incremented primary key
print(obj.row.name) # "Sensor" — the __centauro_name__
print(obj.row.write_time) # datetime — when first written
print(obj.row.edit_time) # datetime — when last updated
See RowFields API for the complete field list.
Validation on assignment
By default, CentauroModel validates field values whenever you set an attribute. This catches type errors early:
sensor = Sensor(unit="Celsius")
sensor.threshold = "oops" # raises ValidationError immediately
For performance-critical code (e.g., ingesting thousands of objects in a tight loop), you can opt out:
from pydantic import ConfigDict
class FastSensor(CentauroModel):
__centauro_name__ = "FastSensor"
model_config = ConfigDict(validate_assignment=False, populate_by_name=True)
value: float = 0.0
populate_by_name=True when overriding model_config. Omitting it breaks renamed_from() — see the Performance guide for details.Choosing between CentauroModel and CentauroModelSeries
Use CentauroModel when your objects represent entities with metadata only — configuration, records, documents, settings:
class Config(CentauroModel):
__centauro_name__ = "Config"
theme: str = "dark"
language: str = "en"
Use CentauroModelSeries when each object also carries a series of timestamped numeric values — sensor readings, stock prices, metrics:
from centaurodb import CentauroModelSeries, CentauroValues
class WeatherStation(CentauroModelSeries):
__centauro_name__ = "WeatherStation"
city: str = ""
elevation: float = 0.0
CentauroModelSeries extends CentauroModel with a values list and a .df property for Polars DataFrame access. Each instance tracks one measurement series. Metadata lives in JSON, values live in a separate normalized table.
The values field accepts either a list of CentauroValues or a Polars DataFrame with time and value columns:
import polars as pl
df = pl.DataFrame({
"time": ["2025-06-01T08:00:00", "2025-06-01T09:00:00"],
"value": [22.5, 24.1],
})
station = WeatherStation(city="Sparta", values=df)
CentauroValues
A single time-series data point with two fields:
from centaurodb import CentauroValues
point = CentauroValues(
time="2025-06-01T08:00:00", # ISO timestamp (auto-coerced by Pydantic)
value=22.5 # numeric value
)
If you need multiple series per entity (e.g., temperature and humidity for the same station), create a separate subclass and object for each. The combination of (object_id, time) forms the composite primary key in the values table.
Nested models
Not all related data needs its own collection entry. If the nested data is small, bounded, and always loaded with its parent, embed it as a plain Pydantic model inside the JSON blob:
from pydantic import BaseModel
class Preferences(BaseModel):
theme: str = "dark"
language: str = "en"
class User(CentauroModel):
__centauro_name__ = "User"
name: str = ""
prefs: Preferences = Preferences() # embedded in the parent's JSON
When the child needs its own lifecycle — independent queries, growing over time, visible in views — use parent_ref instead.
parent_ref vs nested models for a detailed comparison and rules of thumb.Renaming fields safely
Use renamed_from() when you need to rename a field without breaking old data. See Schema Evolution for the full guide.
from centaurodb import renamed_from
class Sensor(CentauroModel):
__centauro_name__ = "Sensor"
temperature: float = renamed_from("temp", default=0.0)
# Old JSON {"temp": 42.0} reads correctly as temperature=42.0