feat(backend): add Pydantic schemas

This commit is contained in:
2025-04-15 22:57:23 +01:00
parent 00a5b736f9
commit 1e4ba3bb93
6 changed files with 517 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
from src.schemas.category import CategoryListResponse, CategoryResponse
from src.schemas.common import PaginatedResponse, PaginationParams
from src.schemas.pattern import (
CommonMistake,
LearningProgression,
LearningQuestion,
PatternDetailResponse,
PatternListResponse,
PatternResponse,
PatternTutorialResponse,
PatternVariation,
RelatedPattern,
VisualizationExample,
)
from src.schemas.question import (
CategoryBrief,
ExplanationResponse,
HiddenTestInput,
HiddenTestOutput,
PatternBrief,
QuestionDetail,
QuestionListItem,
QuestionListResponse,
SolutionResponse,
SubmissionRequest,
SubmissionResponse,
TestResult,
VisibleTestCase,
)
from src.schemas.stats import CategoryCount, DifficultyCount, PatternCount, StatsResponse
__all__ = [
"CategoryBrief",
"CategoryCount",
"CategoryListResponse",
"CategoryResponse",
"CommonMistake",
"DifficultyCount",
"ExplanationResponse",
"HiddenTestInput",
"HiddenTestOutput",
"LearningProgression",
"LearningQuestion",
"PaginatedResponse",
"PaginationParams",
"PatternBrief",
"PatternCount",
"PatternDetailResponse",
"PatternListResponse",
"PatternResponse",
"PatternTutorialResponse",
"PatternVariation",
"QuestionDetail",
"QuestionListItem",
"QuestionListResponse",
"RelatedPattern",
"SolutionResponse",
"StatsResponse",
"SubmissionRequest",
"SubmissionResponse",
"TestResult",
"VisibleTestCase",
"VisualizationExample",
]

View File

@@ -0,0 +1,26 @@
from uuid import UUID
from pydantic import BaseModel
class CategoryBase(BaseModel):
"""Base category schema."""
name: str
slug: str
description: str | None = None
class CategoryResponse(CategoryBase):
"""Category response schema."""
id: UUID
question_count: int = 0
model_config = {"from_attributes": True}
class CategoryListResponse(BaseModel):
"""Response for category list."""
items: list[CategoryResponse]

View File

@@ -0,0 +1,18 @@
from pydantic import BaseModel
class PaginationParams(BaseModel):
"""Pagination parameters."""
page: int = 1
limit: int = 20
class PaginatedResponse[T](BaseModel):
"""Generic paginated response."""
items: list[T]
total: int
page: int
limit: int
pages: int

View File

@@ -0,0 +1,126 @@
from typing import Any
from uuid import UUID
from pydantic import BaseModel
class PatternBase(BaseModel):
"""Base pattern schema."""
name: str
slug: str
description: str | None = None
when_to_use: str | None = None
pattern_type: str | None = None
display_order: int | None = None
class PatternResponse(PatternBase):
"""Pattern response schema."""
id: UUID
question_count: int = 0
model_config = {"from_attributes": True}
class PatternListResponse(BaseModel):
"""Response for pattern list."""
items: list[PatternResponse]
class PatternDetailResponse(PatternBase):
"""Pattern detail with related questions."""
id: UUID
question_count: int = 0
model_config = {"from_attributes": True}
# Tutorial-specific schemas
class CommonMistake(BaseModel):
"""A common mistake when using a pattern."""
title: str
description: str
fix: str | None = None
class PatternVariation(BaseModel):
"""A variation of a pattern."""
name: str
description: str
example: str | None = None
class RelatedPattern(BaseModel):
"""Brief info about a related pattern."""
slug: str
name: str
description: str | None = None
class LearningQuestion(BaseModel):
"""Question for learning progression."""
id: UUID
title: str
slug: str
difficulty: str
leetcode_id: int | None = None
is_optimal: bool = False
class LearningProgression(BaseModel):
"""Curated learning path for a pattern."""
warmup: list[LearningQuestion]
core: list[LearningQuestion]
challenge: list[LearningQuestion]
class VisualizationExample(BaseModel):
"""Interactive visualization example for a pattern."""
id: str
title: str
input: dict[str, Any] | None = None
code: str
steps: list[dict[str, Any]]
class PatternTutorialResponse(PatternBase):
"""Full pattern tutorial with all educational content."""
id: UUID
question_count: int = 0
# Tutorial content
metaphor: str | None = None
core_concept: str | None = None
visualization: str | None = None
code_template: str | None = None
# Structured data
recognition_signals: list[str] | None = None
common_mistakes: list[CommonMistake] | None = None
variations: list[PatternVariation] | None = None
related_patterns: list[RelatedPattern] | None = None
prerequisite_patterns: list[RelatedPattern] | None = None
# Difficulty level (1-5)
difficulty_level: int | None = None
# Learning progression
learning_progression: LearningProgression | None = None
# Interactive visualization examples
visualization_examples: list[VisualizationExample] | None = None
model_config = {"from_attributes": True}

View File

@@ -0,0 +1,249 @@
from datetime import datetime
from typing import Any
from uuid import UUID
from pydantic import BaseModel, Field, model_validator
from src.models.question import Difficulty
class ExplanationResponse(BaseModel):
"""Explanation schema for API responses."""
approach: str
intuition: str
common_pitfalls: list[dict[str, Any]] | None = None
key_takeaways: list[str] | None = None
time_complexity: str
space_complexity: str
complexity_explanation: str | None = None
pattern_comparison: str | None = None
model_config = {"from_attributes": True}
class SolutionResponse(BaseModel):
"""Solution schema for API responses."""
id: UUID
approach_name: str
code: str
language: str
is_optimal: bool
explanation: str | None = None
model_config = {"from_attributes": True}
class CategoryBrief(BaseModel):
"""Brief category info for question responses."""
id: UUID
name: str
slug: str
model_config = {"from_attributes": True}
class PatternBrief(BaseModel):
"""Brief pattern info for question responses."""
id: UUID
name: str
slug: str
is_optimal: bool = False
model_config = {"from_attributes": True}
class QuestionListItem(BaseModel):
"""Question summary for list views."""
id: UUID
title: str
slug: str
difficulty: Difficulty
leetcode_id: int | None = None
leetcode_url: str | None = None
categories: list[CategoryBrief] = []
patterns: list[PatternBrief] = []
created_at: datetime
model_config = {"from_attributes": True}
@model_validator(mode="before")
@classmethod
def transform_patterns(cls, data: Any) -> Any:
"""Transform question_patterns to patterns with is_optimal flag."""
if hasattr(data, "question_patterns"):
# SQLAlchemy model - build patterns from question_patterns
patterns = []
for qp in data.question_patterns:
patterns.append(
{
"id": qp.pattern.id,
"name": qp.pattern.name,
"slug": qp.pattern.slug,
"is_optimal": qp.is_optimal,
}
)
# Create a dict with all fields from data plus transformed patterns
result = {
"id": data.id,
"title": data.title,
"slug": data.slug,
"difficulty": data.difficulty,
"leetcode_id": data.leetcode_id,
"leetcode_url": data.leetcode_url,
"categories": data.categories,
"patterns": patterns,
"created_at": data.created_at,
}
return result
return data
class VisibleTestCase(BaseModel):
"""Visible test case with full input and expected output."""
id: int
input: dict[str, Any]
expected: Any
class HiddenTestInput(BaseModel):
"""Hidden test case input only (no expected value)."""
id: int
input: dict[str, Any]
class QuestionDetail(BaseModel):
"""Full question detail response."""
id: UUID
title: str
slug: str
difficulty: Difficulty
description: str
constraints: str | None = None
examples: list[dict[str, Any]] | None = None
leetcode_id: int | None = None
leetcode_url: str | None = None
function_signature: str | None = None
visible_test_cases: list[VisibleTestCase] | None = None
hidden_test_inputs: list[HiddenTestInput] | None = None
categories: list[CategoryBrief] = []
patterns: list[PatternBrief] = []
explanation: ExplanationResponse | None = None
solutions: list[SolutionResponse] = []
created_at: datetime
updated_at: datetime
model_config = {"from_attributes": True}
@model_validator(mode="before")
@classmethod
def transform_test_cases(cls, data: Any) -> Any:
"""Transform test_cases from DB format to visible/hidden split."""
if hasattr(data, "__dict__"):
# Transform patterns from question_patterns with is_optimal
patterns = []
if hasattr(data, "question_patterns"):
for qp in data.question_patterns:
patterns.append(
{
"id": qp.pattern.id,
"name": qp.pattern.name,
"slug": qp.pattern.slug,
"is_optimal": qp.is_optimal,
}
)
else:
patterns = data.patterns
# SQLAlchemy model - convert to dict
values = {
"id": data.id,
"title": data.title,
"slug": data.slug,
"difficulty": data.difficulty,
"description": data.description,
"constraints": data.constraints,
"examples": data.examples,
"leetcode_id": data.leetcode_id,
"leetcode_url": data.leetcode_url,
"function_signature": data.function_signature,
"categories": data.categories,
"patterns": patterns,
"explanation": data.explanation,
"solutions": data.solutions,
"created_at": data.created_at,
"updated_at": data.updated_at,
}
test_cases = data.test_cases
else:
values = dict(data)
test_cases = values.pop("test_cases", None)
if test_cases:
visible = test_cases.get("visible", [])
hidden = test_cases.get("hidden", [])
values["visible_test_cases"] = [
{"id": i, "input": tc["input"], "expected": tc["expected"]}
for i, tc in enumerate(visible)
]
values["hidden_test_inputs"] = [
{"id": len(visible) + i, "input": tc["input"]} for i, tc in enumerate(hidden)
]
else:
values["visible_test_cases"] = None
values["hidden_test_inputs"] = None
return values
class QuestionListResponse(BaseModel):
"""Paginated question list response."""
items: list[QuestionListItem]
total: int
page: int
limit: int
pages: int
class HiddenTestOutput(BaseModel):
"""User's output for a hidden test case."""
test_id: int
output: Any
class SubmissionRequest(BaseModel):
"""Request to submit a solution for validation."""
code: str = Field(..., min_length=1)
hidden_outputs: list[HiddenTestOutput]
class TestResult(BaseModel):
"""Result of running a single test case."""
test_id: int
passed: bool
input: dict[str, Any] | None = None
expected: Any | None = None
actual: Any
error: str | None = None
class SubmissionResponse(BaseModel):
"""Response from submitting a solution."""
passed: bool
visible_results: list[TestResult]
hidden_results: list[TestResult]
total_passed: int
total_tests: int

View File

@@ -0,0 +1,34 @@
from pydantic import BaseModel
class DifficultyCount(BaseModel):
"""Count of questions by difficulty."""
easy: int
medium: int
hard: int
class CategoryCount(BaseModel):
"""Question count for a category."""
name: str
slug: str
count: int
class PatternCount(BaseModel):
"""Question count for a pattern."""
name: str
slug: str
count: int
class StatsResponse(BaseModel):
"""Overall statistics response."""
total_questions: int
by_difficulty: DifficultyCount
by_category: list[CategoryCount]
by_pattern: list[PatternCount]