From 925a3b3911c17bc0a97441ee4dea74c2864124a1 Mon Sep 17 00:00:00 2001 From: Brian Wiborg Date: Sat, 27 Sep 2025 04:34:52 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8FSupport=20fine-grained=20Mode?= =?UTF-8?q?l.Schema=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - simplify Model.Schema - introduce metaclass in favor of __sub_init__ - use metaclass to populate effective Model.Schema --- src/ohmyapi/builtin/auth/models.py | 6 ++ src/ohmyapi/db/model/model.py | 110 ++++++++++------------------- 2 files changed, 44 insertions(+), 72 deletions(-) diff --git a/src/ohmyapi/builtin/auth/models.py b/src/ohmyapi/builtin/auth/models.py index 0487e61..3ca46ce 100644 --- a/src/ohmyapi/builtin/auth/models.py +++ b/src/ohmyapi/builtin/auth/models.py @@ -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) diff --git a/src/ohmyapi/db/model/model.py b/src/ohmyapi/db/model/model.py index 80e5157..9804f24 100644 --- a/src/ohmyapi/db/model/model.py +++ b/src/ohmyapi/db/model/model.py @@ -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