mirror of https://github.com/tiangolo/fastapi.git
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
This commit is contained in:
parent
71c441e471
commit
c5e20f6e60
|
|
@ -6,9 +6,10 @@ Example: Basic QUERY method usage in FastAPI.
|
|||
This example demonstrates how to use the QUERY HTTP method for simple queries.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -28,13 +29,12 @@ def search_items(query: SimpleQuery):
|
|||
"""
|
||||
# Simulate search logic
|
||||
results = [
|
||||
f"Item {i}: {query.search_term}"
|
||||
for i in range(1, min(query.limit + 1, 6))
|
||||
f"Item {i}: {query.search_term}" for i in range(1, min(query.limit + 1, 6))
|
||||
]
|
||||
|
||||
return {
|
||||
"query": query.search_term,
|
||||
"limit": query.limit,
|
||||
"results": results,
|
||||
"total_found": len(results)
|
||||
"total_found": len(results),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ sample_data = [
|
|||
"id": 101,
|
||||
"name": "Dr. Smith",
|
||||
"email": "smith@university.edu",
|
||||
"bio": "Mathematics professor with 20 years experience"
|
||||
"bio": "Mathematics professor with 20 years experience",
|
||||
},
|
||||
"topics": [
|
||||
{
|
||||
|
|
@ -34,8 +35,8 @@ sample_data = [
|
|||
"lessons": 15,
|
||||
"exercises": [
|
||||
{"id": 1, "title": "Linear Equations", "points": 10},
|
||||
{"id": 2, "title": "Quadratic Equations", "points": 15}
|
||||
]
|
||||
{"id": 2, "title": "Quadratic Equations", "points": 15},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
|
@ -44,13 +45,13 @@ sample_data = [
|
|||
"lessons": 20,
|
||||
"exercises": [
|
||||
{"id": 3, "title": "Derivatives", "points": 20},
|
||||
{"id": 4, "title": "Integrals", "points": 25}
|
||||
]
|
||||
}
|
||||
{"id": 4, "title": "Integrals", "points": 25},
|
||||
],
|
||||
},
|
||||
],
|
||||
"tags": ["mathematics", "algebra", "calculus"],
|
||||
"rating": 4.8,
|
||||
"enrolled_students": 245
|
||||
"enrolled_students": 245,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
|
|
@ -60,7 +61,7 @@ sample_data = [
|
|||
"id": 102,
|
||||
"name": "Dr. Johnson",
|
||||
"email": "johnson@university.edu",
|
||||
"bio": "Physics professor specializing in quantum mechanics"
|
||||
"bio": "Physics professor specializing in quantum mechanics",
|
||||
},
|
||||
"topics": [
|
||||
{
|
||||
|
|
@ -70,19 +71,20 @@ sample_data = [
|
|||
"lessons": 25,
|
||||
"exercises": [
|
||||
{"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"],
|
||||
"rating": 4.9,
|
||||
"enrolled_students": 156
|
||||
}
|
||||
"enrolled_students": 156,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class FieldSelector(BaseModel):
|
||||
"""Define which fields to include in the response."""
|
||||
|
||||
course_fields: Optional[List[str]] = None
|
||||
instructor_fields: Optional[List[str]] = None
|
||||
topic_fields: Optional[List[str]] = None
|
||||
|
|
@ -91,6 +93,7 @@ class FieldSelector(BaseModel):
|
|||
|
||||
class QueryFilter(BaseModel):
|
||||
"""Define filters for the query."""
|
||||
|
||||
min_rating: Optional[float] = None
|
||||
max_rating: Optional[float] = None
|
||||
difficulty: Optional[str] = None
|
||||
|
|
@ -100,13 +103,16 @@ class QueryFilter(BaseModel):
|
|||
|
||||
class CourseQuery(BaseModel):
|
||||
"""Complete query schema for course data."""
|
||||
|
||||
fields: Optional[FieldSelector] = None
|
||||
filters: Optional[QueryFilter] = None
|
||||
limit: Optional[int] = 10
|
||||
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."""
|
||||
if allowed_fields is None:
|
||||
return obj
|
||||
|
|
@ -121,33 +127,50 @@ def apply_filters(data: List[Dict], filters: Optional[QueryFilter]) -> List[Dict
|
|||
filtered_data = data.copy()
|
||||
|
||||
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:
|
||||
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:
|
||||
filtered_data = [
|
||||
item for item in filtered_data
|
||||
if any(topic.get("difficulty") == filters.difficulty for topic in item.get("topics", []))
|
||||
item
|
||||
for item in filtered_data
|
||||
if any(
|
||||
topic.get("difficulty") == filters.difficulty
|
||||
for topic in item.get("topics", [])
|
||||
)
|
||||
]
|
||||
|
||||
if filters.tags:
|
||||
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 filters.instructor_name:
|
||||
filtered_data = [
|
||||
item for item in filtered_data
|
||||
if filters.instructor_name.lower() in item.get("instructor", {}).get("name", "").lower()
|
||||
item
|
||||
for item in filtered_data
|
||||
if filters.instructor_name.lower()
|
||||
in item.get("instructor", {}).get("name", "").lower()
|
||||
]
|
||||
|
||||
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."""
|
||||
if not fields:
|
||||
return data
|
||||
|
|
@ -158,7 +181,9 @@ def apply_field_selection(data: List[Dict], fields: Optional[FieldSelector]) ->
|
|||
|
||||
# Filter 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
|
||||
if "topics" in item and fields.topic_fields:
|
||||
|
|
@ -229,7 +254,7 @@ def query_courses(query: CourseQuery):
|
|||
"returned_results": len(paginated_data),
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"data": paginated_data
|
||||
"data": paginated_data,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -245,12 +270,9 @@ def read_root():
|
|||
"fields": {
|
||||
"course_fields": ["id", "name", "rating"],
|
||||
"instructor_fields": ["name"],
|
||||
"topic_fields": ["title", "difficulty"]
|
||||
"topic_fields": ["title", "difficulty"],
|
||||
},
|
||||
"filters": {
|
||||
"min_rating": 4.5,
|
||||
"tags": ["mathematics"]
|
||||
"filters": {"min_rating": 4.5, "tags": ["mathematics"]},
|
||||
"limit": 5,
|
||||
},
|
||||
"limit": 5
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ This example directly addresses the use case described in the GitHub issue:
|
|||
- Provides GraphQL-like flexibility with standard HTTP
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
|
@ -69,13 +70,13 @@ sample_subject = {
|
|||
"number_of_clicks": 1,
|
||||
"number_of_votes": 1,
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -84,12 +85,15 @@ class ArbitrarySchema(BaseModel):
|
|||
The schema that clients send to specify exactly what they want in the response.
|
||||
This is the key innovation - clients can request any combination of fields.
|
||||
"""
|
||||
|
||||
# Root level fields to include
|
||||
root_fields: Optional[List[str]] = None
|
||||
|
||||
# Nested object field specifications
|
||||
tags: Optional[Dict[str, Any]] = None # {"fields": ["id", "name"], "limit": 5}
|
||||
topics: Optional[Dict[str, Any]] = None # {"fields": [...], "limit": 10, "posts": {...}}
|
||||
topics: Optional[Dict[str, Any]] = (
|
||||
None # {"fields": [...], "limit": 10, "posts": {...}}
|
||||
)
|
||||
|
||||
# Global limits
|
||||
max_depth: Optional[int] = None
|
||||
|
|
@ -123,7 +127,11 @@ def filter_by_schema(data: Dict[str, Any], schema: ArbitrarySchema) -> Dict[str,
|
|||
# Filter fields if specified
|
||||
if "fields" in tags_config:
|
||||
tags_data = [
|
||||
{field: tag.get(field) for field in tags_config["fields"] if field in tag}
|
||||
{
|
||||
field: tag.get(field)
|
||||
for field in tags_config["fields"]
|
||||
if field in tag
|
||||
}
|
||||
for tag in tags_data
|
||||
]
|
||||
|
||||
|
|
@ -149,7 +157,9 @@ def filter_by_schema(data: Dict[str, Any], schema: ArbitrarySchema) -> Dict[str,
|
|||
processed_topic[field] = topic[field]
|
||||
else:
|
||||
# Default topic fields
|
||||
processed_topic = {k: v for k, v in topic.items() if k in ["id", "name"]}
|
||||
processed_topic = {
|
||||
k: v for k, v in topic.items() if k in ["id", "name"]
|
||||
}
|
||||
|
||||
# Handle posts within topics
|
||||
if "posts" in topics_config and "posts" in topic:
|
||||
|
|
@ -169,7 +179,9 @@ def filter_by_schema(data: Dict[str, Any], schema: ArbitrarySchema) -> Dict[str,
|
|||
if field in post:
|
||||
processed_post[field] = post[field]
|
||||
else:
|
||||
processed_post = {k: v for k, v in post.items() if k in ["id", "title"]}
|
||||
processed_post = {
|
||||
k: v for k, v in post.items() if k in ["id", "title"]
|
||||
}
|
||||
|
||||
# Handle answers within posts
|
||||
if "answers" in posts_config and "answers" in post:
|
||||
|
|
@ -188,7 +200,11 @@ def filter_by_schema(data: Dict[str, Any], schema: ArbitrarySchema) -> Dict[str,
|
|||
if field in answer:
|
||||
processed_answer[field] = answer[field]
|
||||
else:
|
||||
processed_answer = {k: v for k, v in answer.items() if k in ["id", "content"]}
|
||||
processed_answer = {
|
||||
k: v
|
||||
for k, v in answer.items()
|
||||
if k in ["id", "content"]
|
||||
}
|
||||
|
||||
# Handle comments within answers
|
||||
if "comments" in answers_config and "comments" in answer:
|
||||
|
|
@ -196,16 +212,26 @@ def filter_by_schema(data: Dict[str, Any], schema: ArbitrarySchema) -> Dict[str,
|
|||
comments_data = answer["comments"]
|
||||
|
||||
if "limit" in comments_config:
|
||||
comments_data = comments_data[:comments_config["limit"]]
|
||||
comments_data = comments_data[
|
||||
: comments_config["limit"]
|
||||
]
|
||||
|
||||
if "fields" in comments_config:
|
||||
processed_answer["comments"] = [
|
||||
{field: comment.get(field) for field in comments_config["fields"] if field in comment}
|
||||
{
|
||||
field: comment.get(field)
|
||||
for field in comments_config["fields"]
|
||||
if field in comment
|
||||
}
|
||||
for comment in comments_data
|
||||
]
|
||||
else:
|
||||
processed_answer["comments"] = [
|
||||
{k: v for k, v in comment.items() if k in ["id", "content"]}
|
||||
{
|
||||
k: v
|
||||
for k, v in comment.items()
|
||||
if k in ["id", "content"]
|
||||
}
|
||||
for comment in comments_data
|
||||
]
|
||||
|
||||
|
|
@ -279,7 +305,7 @@ def query_subjects(schema: ArbitrarySchema):
|
|||
return {
|
||||
"message": "Successfully queried subjects using arbitrary schema",
|
||||
"schema_used": schema.model_dump(),
|
||||
"data": filtered_data
|
||||
"data": filtered_data,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -295,12 +321,12 @@ def get_root():
|
|||
"usage": {
|
||||
"endpoint": "/query/subjects",
|
||||
"method": "QUERY",
|
||||
"description": "Send arbitrary schema in request body to get exactly the data you need"
|
||||
"description": "Send arbitrary schema in request body to get exactly the data you need",
|
||||
},
|
||||
"examples": {
|
||||
"minimal": {
|
||||
"root_fields": ["id", "name"],
|
||||
"tags": {"fields": ["id", "name"], "limit": 1}
|
||||
"tags": {"fields": ["id", "name"], "limit": 1},
|
||||
},
|
||||
"detailed": {
|
||||
"root_fields": ["id", "name"],
|
||||
|
|
@ -308,14 +334,11 @@ def get_root():
|
|||
"fields": ["id", "name"],
|
||||
"posts": {
|
||||
"fields": ["id", "title"],
|
||||
"answers": {
|
||||
"fields": ["id", "content"],
|
||||
"limit": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"answers": {"fields": ["id", "content"], "limit": 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ This test file follows the FastAPI test patterns and should be compatible
|
|||
with the existing test suite.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, Depends
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
def test_query_method_basic():
|
||||
|
|
|
|||
Loading…
Reference in New Issue