From 89a29c7a2e27f7f0818525da0f09a978a117e3c9 Mon Sep 17 00:00:00 2001 From: Sushil Khairnar Date: Mon, 13 Oct 2025 20:32:59 -0400 Subject: [PATCH] Fix trailing whitespace in QUERY method implementation --- add_query_method.patch | 118 +++++++++++++++++++++++++++++++++++++ fastapi/applications.py | 14 ++--- fastapi/routing.py | 14 ++--- query_method_patch.py | 74 +++++++++++++++++++++++ test_query_method.py | 42 +++++++++++++ tests/test_query_method.py | 50 ++++++++-------- 6 files changed, 273 insertions(+), 39 deletions(-) create mode 100644 add_query_method.patch create mode 100644 query_method_patch.py create mode 100644 test_query_method.py diff --git a/add_query_method.patch b/add_query_method.patch new file mode 100644 index 000000000..c5ab08bc7 --- /dev/null +++ b/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[ diff --git a/fastapi/applications.py b/fastapi/applications.py index 81522fc33..92d18300b 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -2999,28 +2999,28 @@ class FastAPI(Starlette): ) -> 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 from pydantic import BaseModel from typing import List, Dict, Any - + app = FastAPI() - + class SearchQuery(BaseModel): terms: List[str] filters: Dict[str, Any] - + @app.query("/search/") def search_items(query: SearchQuery): return {"results": [...]} ``` - + 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 """ diff --git a/fastapi/routing.py b/fastapi/routing.py index d7c95f218..04179c8a0 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -2913,27 +2913,27 @@ class APIRouter(routing.Router): ) -> 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 from pydantic import BaseModel - + app = FastAPI() - + class SearchQuery(BaseModel): terms: List[str] filters: Dict[str, Any] - + @app.query("/search/") def search_items(query: SearchQuery): return {"results": [...]} ``` - + 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 """ diff --git a/query_method_patch.py b/query_method_patch.py new file mode 100644 index 000000000..ff5071f99 --- /dev/null +++ b/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, + ) diff --git a/test_query_method.py b/test_query_method.py new file mode 100644 index 000000000..45d7fb786 --- /dev/null +++ b/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) diff --git a/tests/test_query_method.py b/tests/test_query_method.py index e66766360..b7487634f 100644 --- a/tests/test_query_method.py +++ b/tests/test_query_method.py @@ -18,16 +18,16 @@ class SearchQuery(BaseModel): def test_query_method_basic(): """Test basic QUERY method functionality""" app = FastAPI() - + @app.query("/search/") def search_items(query: SearchQuery): return { "results": [{"id": 1, "name": "test"}], "query": query.dict() } - + client = TestClient(app) - + # Test QUERY request with body response = client.request( "QUERY", @@ -38,7 +38,7 @@ def test_query_method_basic(): "limit": 5 } ) - + assert response.status_code == 200 data = response.json() assert "results" in data @@ -51,13 +51,13 @@ def test_query_method_basic(): def test_query_method_validation(): """Test QUERY method with validation errors""" app = FastAPI() - + @app.query("/search/") def search_items(query: SearchQuery): return {"results": []} - + client = TestClient(app) - + # Test with invalid data (missing required field) response = client.request( "QUERY", @@ -67,30 +67,30 @@ def test_query_method_validation(): # Missing required 'terms' field } ) - + assert response.status_code == 422 # Validation error def test_query_method_openapi_schema(): """Test that QUERY method appears in OpenAPI schema""" app = FastAPI() - + @app.query("/search/") def search_items(query: SearchQuery): return {"results": []} - + client = TestClient(app) - + # Get OpenAPI schema response = client.get("/openapi.json") assert response.status_code == 200 - + openapi_schema = response.json() - + # Check that the QUERY method is in the schema assert "/search/" in openapi_schema["paths"] assert "query" in openapi_schema["paths"]["/search/"] - + query_operation = openapi_schema["paths"]["/search/"]["query"] assert "requestBody" in query_operation assert query_operation["requestBody"]["required"] is True @@ -99,50 +99,50 @@ def test_query_method_openapi_schema(): def test_query_method_with_dependencies(): """Test QUERY method with FastAPI dependencies""" from fastapi import Depends - + app = FastAPI() - + def get_current_user(): return {"user_id": 123} - + @app.query("/search/", dependencies=[Depends(get_current_user)]) def search_items(query: SearchQuery): return {"results": []} - + client = TestClient(app) - + response = client.request( "QUERY", "/search/", json={"terms": ["test"]} ) - + assert response.status_code == 200 def test_query_method_response_model(): """Test QUERY method with response model""" app = FastAPI() - + class SearchResponse(BaseModel): results: List[Dict[str, Any]] total: int - + @app.query("/search/", response_model=SearchResponse) def search_items(query: SearchQuery): return { "results": [{"id": 1, "name": "test"}], "total": 1 } - + client = TestClient(app) - + response = client.request( "QUERY", "/search/", json={"terms": ["test"]} ) - + assert response.status_code == 200 data = response.json() assert "results" in data