Skip to main content

Python pydantic models wrapper

Project description

ElasticModel

ElasticModel is a wrapper around pydantic.BaseModel(v2) designed to simplify working with partial (projected) data from databases or APIs.

The key advantage is support for nested models ("model within a model") without requiring all fields to be loaded. Despite this, model methods remain accessible at every level and operate on the data that has been loaded.

⚠️This behavior wouldn't be possible with Pydantic, which requires all fields to be present.

Core features of ElasticModel:

  • ✅ Fully inherits the behavior of pydantic.BaseModel: all methods and functionality. Just replace BaseModel with ElasticModel.
  • ✅ Allows creating models from incomplete data and provides full access — read fields, call methods, even on nested models.
  • ✅ Avoids the need for declaring a bunch of Optional fields.
  • ✅ Supports dynamic (undeclared) fields — stored in self.elastic_extra, a dictionary for unprocessed data.
  • ✅ Ensures strict access: only loaded fields can be accessed. Trying to access an unloaded field will raise NotLoadedFieldError.
  • ✅ Supports recursive creation of nested models.
  • ✅ Provides shallow or deep checks for required fields at any time (on demand).

ElasticModel combines the best of BaseModel.model_validate (structured/nested models) and BaseModel.model_construct (creation without validation), adding new flexibility for working with partial data — without overloading your codebase with Optional fields.


🌐Переклад тут🔱 ElasticModel — це `pydantic.BaseModel`(v2), який надає ... та спрощує... ... спрощує роботу з частковими (проекційними) даними з баз даних або API.

Основна перевага — підтримка вкладених моделей ("модель у моделі") без необхідності завантаження всіх полів. Незважаючи на це, методи моделі залишаються доступними на всіх рівнях і працюють із тими даними, які були завантажені.

⚠️У Pydantic така поведінка була б неможливою, оскільки він вимагає наявності всіх полів.

Основні можливості ElasticModel:

  • ✅ Повністю наслідує поведінку pydantic.BaseModel: усі методи та функціональність. Просто заміни BaseModel на ElasticModel.
  • ✅ Дозволяє створювати моделі з неповними даними й отримувати повний доступ — читати поля, викликати методи, навіть на вкладених моделях.
  • ✅ Не потребує великої кількості полів типу Optional.
  • ✅ Підтримує динамічні (неописані) поля — вони зберігаються в self.elastic_extra, словнику для необроблених даних.
  • ✅ Гарантує контроль доступу: можна звертатись лише до завантажених полів. Спроба доступу до незавантаженого поля викличе NotLoadedFieldError.
  • ✅ Підтримує рекурсивне створення вкладених моделей.
  • ✅ Дозволяє виконувати поверхневу або глибоку перевірку обов’язкових полів у будь-який момент (на вимогу користувача).

ElasticModel поєднує найкраще з BaseModel.model_validate (структуровані/вкладені моделі) та BaseModel.model_construct (створення без валідації), додаючи нову гнучкість у роботі з частковими даними — без перевантаження кодової бази полями Optional.

---

Install

pip install gostmodels

https://pypi.org/project/gostmodels/


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
    created: Created
    updated: Created
    
    def welcome(self) -> str:
        # Uses only fields present in the example payload below
        return f"Hi {self.first_name}! Joined at {self.created.datetime_from_at()}"

# Build from a projection (partial dict)
doc = {
    "_id": "u1",
    "first_name": "Ann",
    "email": "ann@example.com",
    "created": {
        "at": "2025-08-15"
        # "by": missing 
    },
    "updated": {
        "at": "2099-01-10"
        # "by": missing 
    },
    "external_value": 1,   # unknown key → goes to .extra
}
# --------MAIN CONSTRUCTOR-------
u = User.elastic_create(doc)        # ✅ -> ElasticModel
# -------------------------------
assert u.id == "u1"   # Alias works; unknown keys preserved without validation

# 💡 .elastic_extra is a simple dict that stores all unknown field models 💡
print(u.elastic_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
print(u.created.by)                 # ❌ -> ERROR NotLoadedFieldError


# .is_loaded(key) - Safe verification of field presence in the model
assert u.created.elastic_is_loaded("by") == False
u.created.by = "system"  # Mark fields as loaded by assigning to them
assert u.created.elastic_is_loaded("by") == True


# 💡 Choose validation depth when you need it
# shallow (recursive=False): do not descend into nested models
ok_shallow, bad_paths = u.elastic_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.elastic_is_valid(recursive=True)
print(ok_deep, bad_paths)       # ⚠️ -> False, ['updated.by']


# Produce a fully validated pydantic.BaseModel instance (or raise ValidationError)
u.updated.by = "user"  # Before making the pydantic model, we fill in the missing field to avoid getting a ValidationError
validated = u.elastic_get_validated_model(recursive=True)   # ✅ -> pydantic.BaseModel

Comparing .elastic_create to .model_validate and .model_construct from pydantic

from datetime import datetime
from pydantic import BaseModel, EmailStr, ValidationError
from gostmodels import ElasticModel

# Compare the methods of creating objects using different approaches:
# 1. pydantic.BaseModel.model_validate
# 2. pydantic.BaseModel.model_construct
# 3. gostmodels.ElasticModel.elastic_create

# Let's create identical BaseModel and ElasticModel model:
# - 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
# -------------------------------

# Equally limited data, but enough for the actions we need
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 partials
  • model_construct: partials OK, but nested dicts remain dicts
  • elastic_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 TypeAdapter using your type hints (including Annotated[..., Field(...)])
    • Unknown keys are captured in model.extra
    • Tracks actually loaded fields in ._loaded_fields
    • apply_defaults=True applies default/default_factory to missing fields and marks them as loaded
  • Strict read access

    • Accessing an unloaded declared field raises NotLoadedFieldError
    • System attributes and dunders are not intercepted
  • Shallow vs Deep validation

    • Shallow: keep existing nested instances, fast
    • Deep: fully materialize to plain structures and validate everything
  • Nested models and containers

    • Nested ElasticModel fields are built via elastic_create
    • list/set/tuple items are coerced recursively (when validate=True)
    • dict[K, V] keys and values are validated (when validate=True)

API snapshot

  • ElasticModel.elastic_create(data: dict, *, validate: bool = True, apply_defaults: bool = False) -> Self
  • model.elastic_extra -> dict[str, any]
  • model.elastic_is_loaded(name: str) -> bool
  • model.elastic_is_valid(*, recursive: bool = True) -> tuple[bool, list[str]]
  • model.elastic_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 .extra
  • populate_by_name=True — supports both field names and aliases
  • revalidate_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

gostmodels-0.2.0.tar.gz (20.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gostmodels-0.2.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file gostmodels-0.2.0.tar.gz.

File metadata

  • Download URL: gostmodels-0.2.0.tar.gz
  • Upload date:
  • Size: 20.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.11

File hashes

Hashes for gostmodels-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f1bf3c9d2b4abf791c4179c9b8a49079e7fed39af2fce76d2cabeeb6088fd124
MD5 a733284a8dc7085f55840cdff01db5ec
BLAKE2b-256 fc161888c31c983f5a72f1d094eec8a9c78e82c97e3d9188192fdaef4c43e895

See more details on using hashes here.

File details

Details for the file gostmodels-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: gostmodels-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.11

File hashes

Hashes for gostmodels-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8a37589a74bd66b301c3e0f5535f227551ebd6545fbb84a3dfa4b30491d331f3
MD5 18a8bdef66bb0f1b5eaf0a0b9c5ad834
BLAKE2b-256 3fb2e8928326c8e7c05ca027b5f7b59362a7007136620d5649468f0eba8f1f4f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page