|
@ -7,9 +7,10 @@ This example demonstrates the power of the QUERY method for complex data filteri |
|
|
and field selection, similar to GraphQL but using standard HTTP. |
|
|
and field selection, similar to GraphQL but using standard HTTP. |
|
|
""" |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
from typing import Any, Dict, List, Optional |
|
|
|
|
|
|
|
|
from fastapi import FastAPI |
|
|
from fastapi import FastAPI |
|
|
from pydantic import BaseModel |
|
|
from pydantic import BaseModel |
|
|
from typing import List, Optional, Dict, Any |
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI() |
|
|
app = FastAPI() |
|
|
|
|
|
|
|
@ -24,7 +25,7 @@ sample_data = [ |
|
|
"id": 101, |
|
|
"id": 101, |
|
|
"name": "Dr. Smith", |
|
|
"name": "Dr. Smith", |
|
|
"email": "smith@university.edu", |
|
|
"email": "smith@university.edu", |
|
|
"bio": "Mathematics professor with 20 years experience" |
|
|
"bio": "Mathematics professor with 20 years experience", |
|
|
}, |
|
|
}, |
|
|
"topics": [ |
|
|
"topics": [ |
|
|
{ |
|
|
{ |
|
@ -34,8 +35,8 @@ sample_data = [ |
|
|
"lessons": 15, |
|
|
"lessons": 15, |
|
|
"exercises": [ |
|
|
"exercises": [ |
|
|
{"id": 1, "title": "Linear Equations", "points": 10}, |
|
|
{"id": 1, "title": "Linear Equations", "points": 10}, |
|
|
{"id": 2, "title": "Quadratic Equations", "points": 15} |
|
|
{"id": 2, "title": "Quadratic Equations", "points": 15}, |
|
|
] |
|
|
], |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
"id": 2, |
|
|
"id": 2, |
|
@ -44,13 +45,13 @@ sample_data = [ |
|
|
"lessons": 20, |
|
|
"lessons": 20, |
|
|
"exercises": [ |
|
|
"exercises": [ |
|
|
{"id": 3, "title": "Derivatives", "points": 20}, |
|
|
{"id": 3, "title": "Derivatives", "points": 20}, |
|
|
{"id": 4, "title": "Integrals", "points": 25} |
|
|
{"id": 4, "title": "Integrals", "points": 25}, |
|
|
] |
|
|
], |
|
|
} |
|
|
}, |
|
|
], |
|
|
], |
|
|
"tags": ["mathematics", "algebra", "calculus"], |
|
|
"tags": ["mathematics", "algebra", "calculus"], |
|
|
"rating": 4.8, |
|
|
"rating": 4.8, |
|
|
"enrolled_students": 245 |
|
|
"enrolled_students": 245, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
"id": 2, |
|
|
"id": 2, |
|
@ -60,7 +61,7 @@ sample_data = [ |
|
|
"id": 102, |
|
|
"id": 102, |
|
|
"name": "Dr. Johnson", |
|
|
"name": "Dr. Johnson", |
|
|
"email": "johnson@university.edu", |
|
|
"email": "johnson@university.edu", |
|
|
"bio": "Physics professor specializing in quantum mechanics" |
|
|
"bio": "Physics professor specializing in quantum mechanics", |
|
|
}, |
|
|
}, |
|
|
"topics": [ |
|
|
"topics": [ |
|
|
{ |
|
|
{ |
|
@ -70,19 +71,20 @@ sample_data = [ |
|
|
"lessons": 25, |
|
|
"lessons": 25, |
|
|
"exercises": [ |
|
|
"exercises": [ |
|
|
{"id": 5, "title": "Wave Functions", "points": 30}, |
|
|
{"id": 5, "title": "Wave Functions", "points": 30}, |
|
|
{"id": 6, "title": "Uncertainty Principle", "points": 35} |
|
|
{"id": 6, "title": "Uncertainty Principle", "points": 35}, |
|
|
] |
|
|
], |
|
|
} |
|
|
} |
|
|
], |
|
|
], |
|
|
"tags": ["physics", "quantum", "mechanics"], |
|
|
"tags": ["physics", "quantum", "mechanics"], |
|
|
"rating": 4.9, |
|
|
"rating": 4.9, |
|
|
"enrolled_students": 156 |
|
|
"enrolled_students": 156, |
|
|
} |
|
|
}, |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FieldSelector(BaseModel): |
|
|
class FieldSelector(BaseModel): |
|
|
"""Define which fields to include in the response.""" |
|
|
"""Define which fields to include in the response.""" |
|
|
|
|
|
|
|
|
course_fields: Optional[List[str]] = None |
|
|
course_fields: Optional[List[str]] = None |
|
|
instructor_fields: Optional[List[str]] = None |
|
|
instructor_fields: Optional[List[str]] = None |
|
|
topic_fields: Optional[List[str]] = None |
|
|
topic_fields: Optional[List[str]] = None |
|
@ -91,6 +93,7 @@ class FieldSelector(BaseModel): |
|
|
|
|
|
|
|
|
class QueryFilter(BaseModel): |
|
|
class QueryFilter(BaseModel): |
|
|
"""Define filters for the query.""" |
|
|
"""Define filters for the query.""" |
|
|
|
|
|
|
|
|
min_rating: Optional[float] = None |
|
|
min_rating: Optional[float] = None |
|
|
max_rating: Optional[float] = None |
|
|
max_rating: Optional[float] = None |
|
|
difficulty: Optional[str] = None |
|
|
difficulty: Optional[str] = None |
|
@ -100,13 +103,16 @@ class QueryFilter(BaseModel): |
|
|
|
|
|
|
|
|
class CourseQuery(BaseModel): |
|
|
class CourseQuery(BaseModel): |
|
|
"""Complete query schema for course data.""" |
|
|
"""Complete query schema for course data.""" |
|
|
|
|
|
|
|
|
fields: Optional[FieldSelector] = None |
|
|
fields: Optional[FieldSelector] = None |
|
|
filters: Optional[QueryFilter] = None |
|
|
filters: Optional[QueryFilter] = None |
|
|
limit: Optional[int] = 10 |
|
|
limit: Optional[int] = 10 |
|
|
offset: Optional[int] = 0 |
|
|
offset: Optional[int] = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filter_object(obj: Dict[str, Any], allowed_fields: Optional[List[str]] = None) -> Dict[str, Any]: |
|
|
def filter_object( |
|
|
|
|
|
obj: Dict[str, Any], allowed_fields: Optional[List[str]] = None |
|
|
|
|
|
) -> Dict[str, Any]: |
|
|
"""Filter an object to only include specified fields.""" |
|
|
"""Filter an object to only include specified fields.""" |
|
|
if allowed_fields is None: |
|
|
if allowed_fields is None: |
|
|
return obj |
|
|
return obj |
|
@ -121,33 +127,50 @@ def apply_filters(data: List[Dict], filters: Optional[QueryFilter]) -> List[Dict |
|
|
filtered_data = data.copy() |
|
|
filtered_data = data.copy() |
|
|
|
|
|
|
|
|
if filters.min_rating is not None: |
|
|
if filters.min_rating is not None: |
|
|
filtered_data = [item for item in filtered_data if item.get("rating", 0) >= filters.min_rating] |
|
|
filtered_data = [ |
|
|
|
|
|
item |
|
|
|
|
|
for item in filtered_data |
|
|
|
|
|
if item.get("rating", 0) >= filters.min_rating |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
if filters.max_rating is not None: |
|
|
if filters.max_rating is not None: |
|
|
filtered_data = [item for item in filtered_data if item.get("rating", 0) <= filters.max_rating] |
|
|
filtered_data = [ |
|
|
|
|
|
item |
|
|
|
|
|
for item in filtered_data |
|
|
|
|
|
if item.get("rating", 0) <= filters.max_rating |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
if filters.difficulty: |
|
|
if filters.difficulty: |
|
|
filtered_data = [ |
|
|
filtered_data = [ |
|
|
item for item in filtered_data |
|
|
item |
|
|
if any(topic.get("difficulty") == filters.difficulty for topic in item.get("topics", [])) |
|
|
for item in filtered_data |
|
|
|
|
|
if any( |
|
|
|
|
|
topic.get("difficulty") == filters.difficulty |
|
|
|
|
|
for topic in item.get("topics", []) |
|
|
|
|
|
) |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
if filters.tags: |
|
|
if filters.tags: |
|
|
filtered_data = [ |
|
|
filtered_data = [ |
|
|
item for item in filtered_data |
|
|
item |
|
|
|
|
|
for item in filtered_data |
|
|
if any(tag in item.get("tags", []) for tag in filters.tags) |
|
|
if any(tag in item.get("tags", []) for tag in filters.tags) |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
if filters.instructor_name: |
|
|
if filters.instructor_name: |
|
|
filtered_data = [ |
|
|
filtered_data = [ |
|
|
item for item in filtered_data |
|
|
item |
|
|
if filters.instructor_name.lower() in item.get("instructor", {}).get("name", "").lower() |
|
|
for item in filtered_data |
|
|
|
|
|
if filters.instructor_name.lower() |
|
|
|
|
|
in item.get("instructor", {}).get("name", "").lower() |
|
|
] |
|
|
] |
|
|
|
|
|
|
|
|
return filtered_data |
|
|
return filtered_data |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_field_selection(data: List[Dict], fields: Optional[FieldSelector]) -> List[Dict]: |
|
|
def apply_field_selection( |
|
|
|
|
|
data: List[Dict], fields: Optional[FieldSelector] |
|
|
|
|
|
) -> List[Dict]: |
|
|
"""Apply field selection to shape the response.""" |
|
|
"""Apply field selection to shape the response.""" |
|
|
if not fields: |
|
|
if not fields: |
|
|
return data |
|
|
return data |
|
@ -158,7 +181,9 @@ def apply_field_selection(data: List[Dict], fields: Optional[FieldSelector]) -> |
|
|
|
|
|
|
|
|
# Filter instructor fields |
|
|
# Filter instructor fields |
|
|
if "instructor" in item and fields.instructor_fields: |
|
|
if "instructor" in item and fields.instructor_fields: |
|
|
filtered_item["instructor"] = filter_object(item["instructor"], fields.instructor_fields) |
|
|
filtered_item["instructor"] = filter_object( |
|
|
|
|
|
item["instructor"], fields.instructor_fields |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
# Filter topic fields |
|
|
# Filter topic fields |
|
|
if "topics" in item and fields.topic_fields: |
|
|
if "topics" in item and fields.topic_fields: |
|
@ -217,7 +242,7 @@ def query_courses(query: CourseQuery): |
|
|
# Apply pagination |
|
|
# Apply pagination |
|
|
offset = query.offset or 0 |
|
|
offset = query.offset or 0 |
|
|
limit = query.limit or 10 |
|
|
limit = query.limit or 10 |
|
|
paginated_data = filtered_data[offset:offset + limit] |
|
|
paginated_data = filtered_data[offset : offset + limit] |
|
|
|
|
|
|
|
|
# Apply field selection |
|
|
# Apply field selection |
|
|
if query.fields: |
|
|
if query.fields: |
|
@ -229,7 +254,7 @@ def query_courses(query: CourseQuery): |
|
|
"returned_results": len(paginated_data), |
|
|
"returned_results": len(paginated_data), |
|
|
"offset": offset, |
|
|
"offset": offset, |
|
|
"limit": limit, |
|
|
"limit": limit, |
|
|
"data": paginated_data |
|
|
"data": paginated_data, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -245,12 +270,9 @@ def read_root(): |
|
|
"fields": { |
|
|
"fields": { |
|
|
"course_fields": ["id", "name", "rating"], |
|
|
"course_fields": ["id", "name", "rating"], |
|
|
"instructor_fields": ["name"], |
|
|
"instructor_fields": ["name"], |
|
|
"topic_fields": ["title", "difficulty"] |
|
|
"topic_fields": ["title", "difficulty"], |
|
|
}, |
|
|
|
|
|
"filters": { |
|
|
|
|
|
"min_rating": 4.5, |
|
|
|
|
|
"tags": ["mathematics"] |
|
|
|
|
|
}, |
|
|
}, |
|
|
"limit": 5 |
|
|
"filters": {"min_rating": 4.5, "tags": ["mathematics"]}, |
|
|
} |
|
|
"limit": 5, |
|
|
|
|
|
}, |
|
|
} |
|
|
} |
|
|