♻️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 typing import Optional, List
from ohmyapi.db import Model, field from ohmyapi.db import Model, field
from passlib.context import CryptContext from passlib.context import CryptContext
from tortoise.contrib.pydantic import pydantic_queryset_creator
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto") pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
@ -18,6 +19,11 @@ class User(Model):
is_staff = field.BooleanField(default=False) is_staff = field.BooleanField(default=False)
groups: Optional[List[Group]] = field.ManyToManyField("ohmyapi_auth.Group", related_name="users") 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: def set_password(self, raw_password: str) -> None:
"""Hash and store the password.""" """Hash and store the password."""
self.password_hash = pwd_context.hash(raw_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 from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
class Model(TortoiseModel): class ModelMeta(type(TortoiseModel)):
""" def __new__(cls, name, bases, attrs):
Base Tortoise model with attached Pydantic schema generators via .Schema 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: class Schema:
""" include = None
Provides convenient access to auto-generated Pydantic schemas. exclude = None
"""
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)