whenever Integration¶
activemodel has built-in support for the whenever datetime library. Five types are supported:
whenever type |
SQLAlchemy column type |
Notes |
|---|---|---|
|
|
UTC-normalized point in time |
|
|
IANA timezone name is not preserved in the DB |
|
|
No timezone; matches SQLite/naive datetime |
|
|
Calendar date with no time component |
|
|
Time of day with no date or timezone |
Defining fields on a model¶
Import and annotate fields with the bare whenever type. The SQLAlchemy column type is resolved automatically via the get_sqlalchemy_type patch that activemodel applies on import.
from whenever import Instant, PlainDateTime, ZonedDateTime
from activemodel import BaseModel
from activemodel.mixins import TypeIDPrimaryKey
class Event(BaseModel, table=True):
id: str = TypeIDPrimaryKey("event")
occurred_at: Instant
scheduled_at: ZonedDateTime | None = None
local_time: PlainDateTime | None = None
Instant and ZonedDateTime both map to TIMESTAMP WITH TIME ZONE on Postgres. The difference is that Instant is always UTC-normalized whereas ZonedDateTime carries a timezone at the application level. Neither preserves the original IANA timezone name in the database — on read, ZonedDateTime is reconstructed from the stored UTC offset.
PlainDateTime maps to TIMESTAMP WITHOUT TIME ZONE and has no timezone at all, matching the behavior of SQLite and Postgres naive datetimes.
Reading and writing¶
Values round-trip transparently through SQLAlchemy:
from whenever import Instant, ZonedDateTime
event = Event(
occurred_at=Instant.now(),
scheduled_at=ZonedDateTime.now_in_system_tz(),
).save()
fetched = Event.get(event.id)
assert isinstance(fetched.occurred_at, Instant)
assert isinstance(fetched.scheduled_at, ZonedDateTime)
Factories¶
ActiveModelFactory registers providers for all three types automatically, so any factory subclass generates valid values without extra configuration:
from activemodel.pytest.factories import ActiveModelFactory
from my_app.models import Event
class EventFactory(ActiveModelFactory[Event]):
__model__ = Event
event = EventFactory.build()
assert isinstance(event.occurred_at, Instant)
Generated values use the system timezone:
Instant→Instant.now()ZonedDateTime→ZonedDateTime.now_in_system_tz()PlainDateTime→ZonedDateTime.now_in_system_tz().to_plain()
To override a field with a specific value, pass it as a keyword argument:
from whenever import Instant
past_event = EventFactory.build(occurred_at=Instant.now().subtract(hours=24))
Pydantic JSON schema¶
Date and Time validate and serialize correctly in Pydantic models but will not carry a "format" hint in the generated JSON schema — they produce {"type": "string"} rather than {"type": "string", "format": "date"}. This is a limitation of whenever.Date and whenever.Time being Rust extension types; Python cannot add __get_pydantic_json_schema__ to them.