♻️Support fine-grained Model.Schema configuration

- simplify Model.Schema
- introduce metaclass in favor of __sub_init__
- use metaclass to populate effective Model.Schema
This commit is contained in:
Brian Wiborg 2025-09-27 04:34:52 +02:00
parent 6f85f24232
commit 925a3b3911
No known key found for this signature in database
2 changed files with 44 additions and 72 deletions

View file

@ -1,6 +1,7 @@
from typing import Optional, List
from ohmyapi.db import Model, field
from passlib.context import CryptContext
from tortoise.contrib.pydantic import pydantic_queryset_creator
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
@ -18,6 +19,11 @@ class User(Model):
is_staff = field.BooleanField(default=False)
groups: Optional[List[Group]] = field.ManyToManyField("ohmyapi_auth.Group", related_name="users")
class Schema:
exclude = 'password_hash',
def set_password(self, raw_password: str) -> None:
"""Hash and store the password."""
self.password_hash = pwd_context.hash(raw_password)

View file

@ -3,78 +3,44 @@ from tortoise.models import Model as TortoiseModel
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
class Model(TortoiseModel):
"""
Base Tortoise model with attached Pydantic schema generators via .Schema
"""
class ModelMeta(type(TortoiseModel)):
def __new__(cls, name, bases, attrs):
new_cls = super().__new__(cls, name, bases, attrs)
schema_opts = getattr(new_cls, "Schema", None)
class BoundSchema:
@property
def one(self):
"""Return a Pydantic model class for 'one' results."""
include = getattr(schema_opts, "include", None)
exclude = getattr(schema_opts, "exclude", None)
return pydantic_model_creator(
new_cls,
name=f"{new_cls.__name__}SchemaOne",
include=include,
exclude=exclude,
exclude_readonly=True,
)
@property
def many(self):
"""Return a Pydantic queryset class for 'many' results."""
include = getattr(schema_opts, "include", None)
exclude = getattr(schema_opts, "exclude", None)
return pydantic_queryset_creator(
new_cls,
name=f"{new_cls.__name__}SchemaMany",
include=include,
exclude=exclude,
)
new_cls.Schema = BoundSchema()
return new_cls
class Model(TortoiseModel, metaclass=ModelMeta):
class Schema:
"""
Provides convenient access to auto-generated Pydantic schemas.
"""
def __init__(self, model_cls):
self.model_cls = model_cls
@property
def id(self):
# Minimal schema with just the primary key field
pk_field = self.model_cls._meta.pk_attr
return pydantic_model_creator(
self.model_cls, name=f"{self.model_cls.__name__}SchemaId", include=(pk_field,)
)
@property
def get(self):
# Full schema for reading
return pydantic_model_creator(
self.model_cls, name=f"{self.model_cls.__name__}SchemaGet"
)
@property
def post(self):
# Input schema for creation (no readonly fields like ID/PK)
return pydantic_model_creator(
self.model_cls,
name=f"{self.model_cls.__name__}SchemaPost",
exclude_readonly=True,
)
@property
def put(self):
# Input schema for updating
return pydantic_model_creator(
self.model_cls,
name=f"{self.model_cls.__name__}SchemaPut",
exclude_readonly=True,
)
@property
def delete(self):
# Schema for delete operations (just PK)
pk_field = self.model_cls._meta.pk_attr
return pydantic_model_creator(
self.model_cls, name=f"{self.model_cls.__name__}SchemaDelete", include=(pk_field,)
)
@property
def list(self):
# Schema for list endpoints
return pydantic_queryset_creator(self.model_cls)
def from_fields(self, *fields: str):
# Generate schema restricted to given fields
valid = [f for f in fields if f in self.model_cls._meta.fields_map]
return pydantic_model_creator(
self.model_cls,
name=f"{self.model_cls.__name__}SchemaFields",
include=valid,
)
def __init_subclass__(cls, **kwargs):
"""
Automatically attach .Schema to all subclasses
"""
super().__init_subclass__(**kwargs)
cls.Schema = cls.Schema(cls)
include = None
exclude = None