♻️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:
parent
6f85f24232
commit
925a3b3911
2 changed files with 44 additions and 72 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue