Browse Source

Fix trailing whitespace in QUERY method implementation

pull/14182/head
Sushil Khairnar 9 months ago
parent
commit
89a29c7a2e
  1. 118
      add_query_method.patch
  2. 14
      fastapi/applications.py
  3. 14
      fastapi/routing.py
  4. 74
      query_method_patch.py
  5. 42
      test_query_method.py
  6. 50
      tests/test_query_method.py

118
add_query_method.patch

@ -0,0 +1,118 @@
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -2581,6 +2581,89 @@ class APIRouter:
generate_unique_id_function=generate_unique_id_function,
)
+ def query(
+ self,
+ path: Annotated[
+ str,
+ Doc(
+ """
+ The URL path to be used for this *path operation*.
+
+ For example, in `http://example.com/items`, the path is `/items`.
+ """
+ ),
+ ],
+ *,
+ response_model: Annotated[
+ Any,
+ Doc(
+ """
+ The type to use for the response.
+
+ It could be any valid Pydantic *field* type. So, it doesn't have to
+ be a Pydantic model, it could be other things, like a `list`, `dict`,
+ etc.
+
+ It will be used for:
+
+ * Documentation: the generated OpenAPI (and the UI at `/docs`) will
+ show it as the response (JSON Schema).
+ * Serialization: you could return an arbitrary object and the
+ `response_model` would be used to serialize that object into the
+ corresponding JSON.
+ * Filtering: the JSON sent to the client will only contain the data
+ (fields) defined in the `response_model`. If you returned an object
+ that contains an attribute `password` but the `response_model` does
+ not include that field, the JSON sent to the client would not have
+ that `password`.
+ * Validation: whatever you return will be serialized with the
+ `response_model`, converting any data as necessary to generate the
+ corresponding JSON. But if the data in the object returned is not
+ valid, that would mean a violation of the contract with the client,
+ so it's an error from the API developer. So, FastAPI will raise an
+ error and return a 500 error code (Internal Server Error).
+
+ Read more about it in the
+ [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/).
+ """
+ ),
+ ] = Default(None),
+ status_code: Annotated[Optional[int], Doc("HTTP status code for the response")] = None,
+ tags: Annotated[Optional[List[Union[str, Enum]]], Doc("OpenAPI tags")] = None,
+ dependencies: Annotated[Optional[Sequence[params.Depends]], Doc("Dependencies")] = None,
+ summary: Annotated[Optional[str], Doc("OpenAPI summary")] = None,
+ description: Annotated[Optional[str], Doc("OpenAPI description")] = None,
+ response_description: Annotated[str, Doc("OpenAPI response description")] = "Successful Response",
+ responses: Annotated[Optional[Dict[Union[int, str], Dict[str, Any]]], Doc("Additional responses")] = None,
+ deprecated: Annotated[Optional[bool], Doc("Mark as deprecated")] = None,
+ operation_id: Annotated[Optional[str], Doc("OpenAPI operation ID")] = None,
+ response_model_include: Annotated[Optional[IncEx], Doc("Fields to include")] = None,
+ response_model_exclude: Annotated[Optional[IncEx], Doc("Fields to exclude")] = None,
+ response_model_by_alias: Annotated[bool, Doc("Use field aliases")] = True,
+ response_model_exclude_unset: Annotated[bool, Doc("Exclude unset fields")] = False,
+ response_model_exclude_defaults: Annotated[bool, Doc("Exclude default values")] = False,
+ response_model_exclude_none: Annotated[bool, Doc("Exclude None values")] = False,
+ include_in_schema: Annotated[bool, Doc("Include in OpenAPI schema")] = True,
+ response_class: Annotated[
+ Union[Type[Response], DefaultPlaceholder],
+ Doc("Response class to use"),
+ ] = Default(JSONResponse),
+ name: Annotated[Optional[str], Doc("Name for the operation")] = None,
+ callbacks: Annotated[Optional[List[BaseRoute]], Doc("OpenAPI callbacks")] = None,
+ openapi_extra: Annotated[Optional[Dict[str, Any]], Doc("Extra OpenAPI data")] = None,
+ generate_unique_id_function: Annotated[
+ Callable[[APIRoute], str], Doc("Function to generate unique IDs")
+ ] = Default(generate_unique_id),
+ ) -> Callable[[DecoratedCallable], DecoratedCallable]:
+ """
+ Add a *path operation* using an HTTP QUERY operation.
+
+ The QUERY method is a safe HTTP method that allows request bodies,
+ useful for complex queries that exceed URL length limits.
+
+ See: https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html
+ """
+ return self.api_route(
+ path=path,
+ response_model=response_model,
+ status_code=status_code,
+ tags=tags,
+ dependencies=dependencies,
+ summary=summary,
+ description=description,
+ response_description=response_description,
+ responses=responses,
+ deprecated=deprecated,
+ methods=["QUERY"],
+ operation_id=operation_id,
+ response_model_include=response_model_include,
+ response_model_exclude=response_model_exclude,
+ response_model_by_alias=response_model_by_alias,
+ response_model_exclude_unset=response_model_exclude_unset,
+ response_model_exclude_defaults=response_model_exclude_defaults,
+ response_model_exclude_none=response_model_exclude_none,
+ include_in_schema=include_in_schema,
+ response_class=response_class,
+ name=name,
+ callbacks=callbacks,
+ openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
+ )
+
def delete(
self,
path: Annotated[

14
fastapi/applications.py

@ -2999,28 +2999,28 @@ class FastAPI(Starlette):
) -> Callable[[DecoratedCallable], DecoratedCallable]: ) -> Callable[[DecoratedCallable], DecoratedCallable]:
""" """
Add a *path operation* using an HTTP QUERY operation. Add a *path operation* using an HTTP QUERY operation.
The QUERY method is a safe HTTP method that allows request bodies, The QUERY method is a safe HTTP method that allows request bodies,
useful for complex queries that exceed URL length limits. useful for complex queries that exceed URL length limits.
## Example ## Example
```python ```python
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Dict, Any from typing import List, Dict, Any
app = FastAPI() app = FastAPI()
class SearchQuery(BaseModel): class SearchQuery(BaseModel):
terms: List[str] terms: List[str]
filters: Dict[str, Any] filters: Dict[str, Any]
@app.query("/search/") @app.query("/search/")
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return {"results": [...]} return {"results": [...]}
``` ```
Read more about the QUERY method in the IETF draft: Read more about the QUERY method in the IETF draft:
https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html
""" """

14
fastapi/routing.py

@ -2913,27 +2913,27 @@ class APIRouter(routing.Router):
) -> Callable[[DecoratedCallable], DecoratedCallable]: ) -> Callable[[DecoratedCallable], DecoratedCallable]:
""" """
Add a *path operation* using an HTTP QUERY operation. Add a *path operation* using an HTTP QUERY operation.
The QUERY method is a safe HTTP method that allows request bodies, The QUERY method is a safe HTTP method that allows request bodies,
useful for complex queries that exceed URL length limits. useful for complex queries that exceed URL length limits.
## Example ## Example
```python ```python
from fastapi import FastAPI from fastapi import FastAPI
from pydantic import BaseModel from pydantic import BaseModel
app = FastAPI() app = FastAPI()
class SearchQuery(BaseModel): class SearchQuery(BaseModel):
terms: List[str] terms: List[str]
filters: Dict[str, Any] filters: Dict[str, Any]
@app.query("/search/") @app.query("/search/")
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return {"results": [...]} return {"results": [...]}
``` ```
Read more about the QUERY method in the IETF draft: Read more about the QUERY method in the IETF draft:
https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html https://www.ietf.org/archive/id/draft-ietf-httpbis-safe-method-w-body-02.html
""" """

74
query_method_patch.py

@ -0,0 +1,74 @@
# Minimal implementation for QUERY method support in FastAPI
# This is the method to add to the FastAPI class in applications.py after the post method
def query(
self,
path: str,
*,
response_model: Any = Default(None),
status_code: Optional[int] = None,
tags: Optional[List[Union[str, Enum]]] = None,
dependencies: Optional[Sequence[params.Depends]] = None,
summary: Optional[str] = None,
description: Optional[str] = None,
response_description: str = "Successful Response",
responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
deprecated: Optional[bool] = None,
operation_id: Optional[str] = None,
response_model_include: Optional[IncEx] = None,
response_model_exclude: Optional[IncEx] = None,
response_model_by_alias: bool = True,
response_model_exclude_unset: bool = False,
response_model_exclude_defaults: bool = False,
response_model_exclude_none: bool = False,
include_in_schema: bool = True,
response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(generate_unique_id),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
"""
Add a *path operation* using an HTTP QUERY operation.
The QUERY method is a safe HTTP method that allows request bodies,
useful for complex queries that exceed URL length limits.
## Example
```python
from fastapi import FastAPI
app = FastAPI()
@app.query("/search/")
def search_items(query: SearchQuery):
return {"results": [...]}
```
"""
return self.router.query(
path,
response_model=response_model,
status_code=status_code,
tags=tags,
dependencies=dependencies,
summary=summary,
description=description,
response_description=response_description,
responses=responses,
deprecated=deprecated,
operation_id=operation_id,
response_model_include=response_model_include,
response_model_exclude=response_model_exclude,
response_model_by_alias=response_model_by_alias,
response_model_exclude_unset=response_model_exclude_unset,
response_model_exclude_defaults=response_model_exclude_defaults,
response_model_exclude_none=response_model_exclude_none,
include_in_schema=include_in_schema,
response_class=response_class,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=generate_unique_id_function,
)

42
test_query_method.py

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
Test script for the new QUERY method in FastAPI
"""
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Dict, Any
app = FastAPI()
class SearchQuery(BaseModel):
terms: List[str]
filters: Dict[str, Any] = {}
limit: int = 10
@app.query("/search/")
def search_items(query: SearchQuery):
"""
Search for items using the QUERY method.
This demonstrates the new QUERY HTTP method that allows
complex request bodies for search operations.
"""
return {
"results": [
{"id": 1, "name": "Item 1", "matches": query.terms},
{"id": 2, "name": "Item 2", "matches": query.terms}
],
"query": query.dict(),
"total": 2
}
@app.get("/")
def read_root():
return {"message": "FastAPI with QUERY method support"}
if __name__ == "__main__":
import uvicorn
print("Testing FastAPI QUERY method implementation...")
print("Visit http://localhost:8000/docs to see the QUERY endpoint in action!")
uvicorn.run(app, host="0.0.0.0", port=8000)

50
tests/test_query_method.py

@ -18,16 +18,16 @@ class SearchQuery(BaseModel):
def test_query_method_basic(): def test_query_method_basic():
"""Test basic QUERY method functionality""" """Test basic QUERY method functionality"""
app = FastAPI() app = FastAPI()
@app.query("/search/") @app.query("/search/")
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return { return {
"results": [{"id": 1, "name": "test"}], "results": [{"id": 1, "name": "test"}],
"query": query.dict() "query": query.dict()
} }
client = TestClient(app) client = TestClient(app)
# Test QUERY request with body # Test QUERY request with body
response = client.request( response = client.request(
"QUERY", "QUERY",
@ -38,7 +38,7 @@ def test_query_method_basic():
"limit": 5 "limit": 5
} }
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert "results" in data assert "results" in data
@ -51,13 +51,13 @@ def test_query_method_basic():
def test_query_method_validation(): def test_query_method_validation():
"""Test QUERY method with validation errors""" """Test QUERY method with validation errors"""
app = FastAPI() app = FastAPI()
@app.query("/search/") @app.query("/search/")
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return {"results": []} return {"results": []}
client = TestClient(app) client = TestClient(app)
# Test with invalid data (missing required field) # Test with invalid data (missing required field)
response = client.request( response = client.request(
"QUERY", "QUERY",
@ -67,30 +67,30 @@ def test_query_method_validation():
# Missing required 'terms' field # Missing required 'terms' field
} }
) )
assert response.status_code == 422 # Validation error assert response.status_code == 422 # Validation error
def test_query_method_openapi_schema(): def test_query_method_openapi_schema():
"""Test that QUERY method appears in OpenAPI schema""" """Test that QUERY method appears in OpenAPI schema"""
app = FastAPI() app = FastAPI()
@app.query("/search/") @app.query("/search/")
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return {"results": []} return {"results": []}
client = TestClient(app) client = TestClient(app)
# Get OpenAPI schema # Get OpenAPI schema
response = client.get("/openapi.json") response = client.get("/openapi.json")
assert response.status_code == 200 assert response.status_code == 200
openapi_schema = response.json() openapi_schema = response.json()
# Check that the QUERY method is in the schema # Check that the QUERY method is in the schema
assert "/search/" in openapi_schema["paths"] assert "/search/" in openapi_schema["paths"]
assert "query" in openapi_schema["paths"]["/search/"] assert "query" in openapi_schema["paths"]["/search/"]
query_operation = openapi_schema["paths"]["/search/"]["query"] query_operation = openapi_schema["paths"]["/search/"]["query"]
assert "requestBody" in query_operation assert "requestBody" in query_operation
assert query_operation["requestBody"]["required"] is True assert query_operation["requestBody"]["required"] is True
@ -99,50 +99,50 @@ def test_query_method_openapi_schema():
def test_query_method_with_dependencies(): def test_query_method_with_dependencies():
"""Test QUERY method with FastAPI dependencies""" """Test QUERY method with FastAPI dependencies"""
from fastapi import Depends from fastapi import Depends
app = FastAPI() app = FastAPI()
def get_current_user(): def get_current_user():
return {"user_id": 123} return {"user_id": 123}
@app.query("/search/", dependencies=[Depends(get_current_user)]) @app.query("/search/", dependencies=[Depends(get_current_user)])
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return {"results": []} return {"results": []}
client = TestClient(app) client = TestClient(app)
response = client.request( response = client.request(
"QUERY", "QUERY",
"/search/", "/search/",
json={"terms": ["test"]} json={"terms": ["test"]}
) )
assert response.status_code == 200 assert response.status_code == 200
def test_query_method_response_model(): def test_query_method_response_model():
"""Test QUERY method with response model""" """Test QUERY method with response model"""
app = FastAPI() app = FastAPI()
class SearchResponse(BaseModel): class SearchResponse(BaseModel):
results: List[Dict[str, Any]] results: List[Dict[str, Any]]
total: int total: int
@app.query("/search/", response_model=SearchResponse) @app.query("/search/", response_model=SearchResponse)
def search_items(query: SearchQuery): def search_items(query: SearchQuery):
return { return {
"results": [{"id": 1, "name": "test"}], "results": [{"id": 1, "name": "test"}],
"total": 1 "total": 1
} }
client = TestClient(app) client = TestClient(app)
response = client.request( response = client.request(
"QUERY", "QUERY",
"/search/", "/search/",
json={"terms": ["test"]} json={"terms": ["test"]}
) )
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert "results" in data assert "results" in data

Loading…
Cancel
Save