Skip to content

Instantly share code, notes, and snippets.

@c6401
Created August 5, 2026 01:49
Show Gist options
  • Select an option

  • Save c6401/e117ef4792570dd16e5746edd426b3c7 to your computer and use it in GitHub Desktop.

Select an option

Save c6401/e117ef4792570dd16e5746edd426b3c7 to your computer and use it in GitHub Desktop.
tmp pydantic sort
import re
from enum import Enum
from typing import Annotated, Any
from fastapi import FastAPI, Query
from pydantic import BaseModel, field_validator
_CAMEL_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
# lowerCamelCase only: no underscores, no leading capital
_CAMEL_SHAPE = re.compile(r"^[a-z][a-zA-Z0-9]*$")
def camel_to_snake(value: str) -> str:
return _CAMEL_BOUNDARY.sub("_", value).lower()
def snake_to_camel(value: str) -> str:
head, *tail = value.split("_")
return head + "".join(word.capitalize() for word in tail)
def parse_sort(
value: Any,
allowed: type[Enum],
*,
strict_camel_case: bool = True,
) -> list[str]:
"""Turn a `sort` query param into a list of snake_case field names.
Accepts `"a,b"`, `["a,b"]`, or `["a", "b"]` (FastAPI hands over a list).
A leading "-" marks descending order and is preserved on the output.
Names not present in `allowed` raise ValueError -> HTTP 422.
"""
if value is None:
return []
allowed_values = {str(f.value) for f in allowed}
items = [value] if isinstance(value, str) else value
parsed: list[str] = []
unknown: list[str] = []
for chunk in items:
for raw in str(chunk).split(","):
raw = raw.strip()
if not raw:
continue
desc = raw.startswith("-")
if desc:
raw = raw[1:]
if strict_camel_case and not _CAMEL_SHAPE.match(raw):
unknown.append(raw)
continue
field = camel_to_snake(raw)
if field not in allowed_values:
unknown.append(raw)
continue
parsed.append(f"-{field}" if desc else field)
if unknown:
allowed_camel = ", ".join(snake_to_camel(str(f.value)) for f in allowed)
raise ValueError(
f"Unknown sort field(s): {', '.join(unknown)}. Allowed: {allowed_camel}"
)
return parsed
# --- one enum + one model per resource ---------------------------------------
class BookSortField(str, Enum):
NAME = "name"
PUBLISHED_YEAR = "published_year"
CREATED_AT = "created_at"
AUTHOR_FULL_NAME = "author_full_name"
BOOK_ISBN = "book_isbn"
class UserSortField(str, Enum):
EMAIL = "email"
LAST_LOGIN_AT = "last_login_at"
SIGNUP_DATE = "signup_date"
class BookSortParams(BaseModel):
sort: list[str] = []
@field_validator("sort", mode="before")
@classmethod
def _parse_sort(cls, value: Any) -> list[str]:
return parse_sort(value, BookSortField)
class UserSortParams(BaseModel):
sort: list[str] = []
@field_validator("sort", mode="before")
@classmethod
def _parse_sort(cls, value: Any) -> list[str]:
return parse_sort(value, UserSortField)
app = FastAPI()
@app.get("/books")
async def list_books(params: Annotated[BookSortParams, Query()]):
return {"sort": params.sort}
@app.get("/users")
async def list_users(params: Annotated[UserSortParams, Query()]):
return {"sort": params.sort}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment