Python pydantic models wrapper
Project description
ElasticModel
A focused wrapper around pydantic v2 BaseModel for working with partial (projection) documents from databases and APIs.
ElasticModel enables you to:
- Build instances from incomplete data without making every field optional
- Access only what’s loaded; reading a missing field raises
NotLoadedFieldError - Preserve unknown keys in
model.extra(no validation) - Recursively build nested
ElasticModels from dicts - Validate shallowly or deeply on demand
This combines the best of BaseModel.model_validate (structured/nested models) and BaseModel.model_construct (no immediate validation), while adding strict read semantics for missing fields.
Why not just BaseModel?
-
BaseModel.model_validate(...)- Pros: validates and builds the full nested object graph
- Cons: fails immediately if required fields are missing (cannot hold partial payloads)
-
BaseModel.model_construct(...)- Pros: creates an instance without validation (can hold partial payloads)
- Cons: does not build nested models from dicts — nested values remain raw dicts, so model methods/properties that rely on nested models can break
-
ElasticModel.elastic_create(...)- Pros: accepts partial payloads while still building nested
ElasticModels; strict read access guards missing fields; unknown keys available in.extra; choose deep or shallow validation later
- Pros: accepts partial payloads while still building nested
Quick start
from typing import Annotated
from datetime import datetime
from pydantic import Field, EmailStr
from gostmodels import ElasticModel, NotLoadedFieldError
class Created(ElasticModel):
at: str
by: str
def datetime_from_at(self) -> datetime:
return datetime.strptime(self.at, "%Y-%m-%d")
class User(ElasticModel):
id: str = Field(alias="_id")
first_name: Annotated[str, Field(min_length=2)]
last_name: str
email: EmailStr
phone: str
created: Created
updated: Created
# A method that works with the subset we will actually load
def welcome(self) -> str:
# Uses only fields present in the example payload below
return f"Hi {self.first_name} {self.last_name}! Joined at {self.created.datetime_from_at()}"
# Build from a projection (partial dict)
doc = {
"_id": "u1",
"first_name": "Ann",
"last_name": "Lee",
"email": "ann@example.com",
"phone": "+12",
"created": {
"at": "2025-08-15"
# "by": missing
},
"updated": {
"at": "2099-01-10"
# "by": missing
},
"external_value": 1, # unknown key → goes to .extra
}
u = User.elastic_create(doc)
# Alias works; unknown keys preserved without validation
assert u.id == "u1"
# .extra is a simple dict
print(u.extra) # -> {'external_value': 1}
# Nested model is constructed, so methods on nested instances are available
# Model methods can operate with currently loaded data
print(u.created.datetime_from_at()) # -> "2025-08-15 00:00:00" (type <class 'datetime.datetime')
print(u.welcome()) # -> "Hi Ann Lee! Joined at 2025-08-15 00:00:00"
# Accessing a declared but not loaded field → NotLoadedFieldError
try:
_ = u.created.by
except NotLoadedFieldError:
# .is_loaded(key) - Safe verification of field presence in the model
assert u.created.is_loaded("by") == False
# Mark fields as loaded by assigning to them
u.created.by = "system"
assert u.created.is_loaded("by") == True
print("Yeah 😎") # -> "Yeah 😎"
# Choose validation depth when you need it
# shallow (recursive=False): do not descend into nested models
ok_shallow, bad_paths = u.is_valid(recursive=False)
print(ok_shallow, bad_paths) # -> True, []
# deep (recursive=True): checks nested models and finds missing required field in "updated"
ok_deep, bad_paths = u.is_valid(recursive=True)
print(ok_deep, bad_paths) # -> False, ['updated.by']
# Before making the pydantic model, we fill in the missing field to avoid getting a ValidationError
u.updated.by = "user"
# Produce a fully validated pydantic.BaseModel instance (or raise ValidationError)
validated = u.get_validated_model(recursive=True) # pydantic.BaseModel
Comparing to BaseModel.model_validate and model_construct
from datetime import datetime
from pydantic import BaseModel, EmailStr, ValidationError
from gostmodels import ElasticModel
# Порівняємо способи створення об'єктів різними підходами:
# 1. pydantic.BaseModel.model_validate
# 2. pydantic.BaseModel.model_construct
# 3. gostmodels.ElasticModel.elastic_create
# Створимо ідентичні моделі BaseModel та ElasticModel
# pydantic.BaseModel
# -------------------------------
class CreatedPydantic(BaseModel):
at: str
by: str
def datetime_from_at(self) -> datetime:
return datetime.strptime(self.at, "%Y-%m-%d")
class UserPydantic(BaseModel):
email: EmailStr
created: CreatedPydantic
# -------------------------------
# gostmodels.ElasticModel
# -------------------------------
class CreatedElastic(ElasticModel):
at: str
by: str
def datetime_from_at(self) -> datetime:
return datetime.strptime(self.at, "%Y-%m-%d")
class UserElastic(ElasticModel):
email: EmailStr
created: CreatedElastic
# -------------------------------
# Однаково обмежені дані, але їх цілком вистачить для потрібних нам маніпуляцій
partial_data = {
"email": "a@b.com",
"created": {
"at": "2025-08-15"
# "by": missing
}
}
# 1. pydantic.model_validate → raises immediately
user_validate = UserPydantic.model_validate(partial_data) # ❌ -> ERROR ValidationError: 1 validation error for UserPydantic
# 2. pydantic.model_construct → does not validate, but keeps nested dicts
user_construct = UserPydantic.model_construct(**partial_data) # ✅
assert isinstance(user_construct.created, dict) # ⚠️ -> raw dict; methods relying on CreatedPydantic would break
print(user_construct.created.datetime_from_at()) # ❌ -> ERROR AttributeError: 'dict' object has no attribute 'datetime_from_at
# 3. ElasticModel.elastic_create → no instant failures, and nested models are created
user_elastic = UserElastic.elastic_create(partial_data) # ✅
assert isinstance(user_elastic.created, CreatedElastic) # ✅
print(user_elastic.created.datetime_from_at()) # ✅ -> 2025-08-15 00:00:00
Summary:
model_validate: full validation + nested building, but no partialsmodel_construct: partials OK, but nested dicts remain dictselastic_create: partials OK + nested building + strict read access + shallow/deep validation
Key features
-
Partial construction:
elastic_create(data, validate=True, apply_defaults=False)- Accepts dicts with missing and extra keys
- Validates/coerces values via
TypeAdapterusing your type hints (includingAnnotated[..., Field(...)]) - Unknown keys are captured in
model.extra - Tracks actually loaded fields in
._loaded_fields apply_defaults=Trueappliesdefault/default_factoryto missing fields and marks them as loaded
-
Strict read access
- Accessing an unloaded declared field raises
NotLoadedFieldError - System attributes and dunders are not intercepted
- Accessing an unloaded declared field raises
-
Shallow vs Deep validation
- Shallow: keep existing nested instances, fast
- Deep: fully materialize to plain structures and validate everything
-
Nested models and containers
- Nested
ElasticModelfields are built viaelastic_create list/set/tupleitems are coerced recursively (whenvalidate=True)dict[K, V]keys and values are validated (whenvalidate=True)
- Nested
API snapshot
ElasticModel.elastic_create(data: dict, *, validate: bool = True, apply_defaults: bool = False) -> Selfmodel.extra -> dict[str, any]model.is_loaded(name: str) -> boolmodel.is_valid(*, recursive: bool = True) -> tuple[bool, list[str]]model.get_validated_model(recursive: bool = True) -> Self- Assignment marks fields as loaded:
model.field = value
Defaults and config
ElasticModel sets these pydantic.ConfigDict defaults:
extra='ignore'— extra keys are ignored by Pydantic but manually collected into.extrapopulate_by_name=True— supports both field names and aliasesrevalidate_instances='never'— nested model instances are not revalidated automatically (important for shallow validation)
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gostmodels-0.1.0.tar.gz.
File metadata
- Download URL: gostmodels-0.1.0.tar.gz
- Upload date:
- Size: 18.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d15793966e4ce2c4fe00871fd3d223d4e4f82264e75f021e55f17b0159dea97
|
|
| MD5 |
e0bc47998ecbd7478c68e04db786f64c
|
|
| BLAKE2b-256 |
f5d02c6c929f99807aeb378e27fe6524a6043c62e42bcaf5bc452abd1196688e
|
File details
Details for the file gostmodels-0.1.0-py3-none-any.whl.
File metadata
- Download URL: gostmodels-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e07e9ff54480d467b1dbf7479fb5d54f0fb9d086c1b8ae13cad96fdbe0b60f0a
|
|
| MD5 |
9b63282c5e68f5e3fae7c762fd5356d9
|
|
| BLAKE2b-256 |
bc2adb176d311375b0fbfb69af0b8073ae919fe09e5a8f253f186d9ded53bcd8
|