feat(backend): add Pydantic schemas
This commit is contained in:
64
backend/src/schemas/__init__.py
Normal file
64
backend/src/schemas/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
26
backend/src/schemas/category.py
Normal file
26
backend/src/schemas/category.py
Normal 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]
|
||||||
18
backend/src/schemas/common.py
Normal file
18
backend/src/schemas/common.py
Normal 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
|
||||||
126
backend/src/schemas/pattern.py
Normal file
126
backend/src/schemas/pattern.py
Normal 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}
|
||||||
249
backend/src/schemas/question.py
Normal file
249
backend/src/schemas/question.py
Normal 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
|
||||||
34
backend/src/schemas/stats.py
Normal file
34
backend/src/schemas/stats.py
Normal 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]
|
||||||
Reference in New Issue
Block a user