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