diff --git a/.gitignore b/.gitignore index ef6364a9a..b46385849 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ archive.zip # macOS .DS_Store + +# Pdm additional files +.pdm-python +pdm.lock diff --git a/fastapi/responses.py b/fastapi/responses.py index 6c8db6f33..e7e908933 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -1,3 +1,4 @@ +import json from typing import Any from starlette.responses import FileResponse as FileResponse # noqa @@ -20,6 +21,18 @@ except ImportError: # pragma: nocover orjson = None # type: ignore +try: + import bson # type: ignore[import-untyped] +except ImportError: # pragma: nocover + bson = None + + +try: + import bsonjs # type: ignore +except ImportError: # pragma: nocover + bsonjs = None + + class UJSONResponse(JSONResponse): """ JSON response using the high-performance ujson library to serialize data to JSON. @@ -46,3 +59,35 @@ class ORJSONResponse(JSONResponse): return orjson.dumps( content, option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY ) + + +class BSONResponse(Response): + """ + BSON response using the current Python bson library for data serialization. + + Note: This is a temporary solution. Soon, a custom Rust wrapper will be implemented for BSON serialization/deserialization to improve performance and reliability. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + """ + + media_type = "application/bson" + + def render(self, content: Any) -> Any: + assert bson is not None, "bson must be installed to use BSONResponse" + return bson.dumps(content) + + +class BSONJSResponse(BSONResponse): + """ + BSON response using the C-backed python-bsonjs library to serialize BSON + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + """ + + media_type = "application/bson" + + def render(self, content: Any) -> Any: + assert bsonjs is not None, "bsonjs must be installed to use BSONJSResponse" + return bsonjs.loads(json.dumps(content)) diff --git a/pyproject.toml b/pyproject.toml index 7709451ff..ad175ed22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,10 @@ all = [ "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", # For ORJSONResponse "orjson >=3.2.1", + # For BSONResponse + "bson >=0.5.10", + # For BSONJSResponse + "python-bsonjs >=0.6.0; python_version >= '3.9'", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop diff --git a/tests/test_bson_response_class.py b/tests/test_bson_response_class.py new file mode 100644 index 000000000..f23054efe --- /dev/null +++ b/tests/test_bson_response_class.py @@ -0,0 +1,20 @@ +import bson +from fastapi import FastAPI +from fastapi.responses import BSONResponse +from fastapi.testclient import TestClient + +app = FastAPI(default_response_class=BSONResponse) + + +@app.get("/bson_keys") +def get_bson_serialized_data(): + return {"key": "Hello World", 1: 1} + + +client = TestClient(app) + + +def test_bson_serialized_data(): + with client: + response = client.get("/bson_keys") + assert response.content == bson.dumps({"key": "Hello World", 1: 1}) diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py new file mode 100644 index 000000000..20b211832 --- /dev/null +++ b/tests/test_bsonjs_response_class.py @@ -0,0 +1,26 @@ +import json +import sys + +import pytest + + +@pytest.mark.skipif( + sys.version_info < (3, 9), reason="requires minimum python3.9 or higher" +) +def test_bsonjs_serialized_data(): + import bsonjs + from fastapi import FastAPI + from fastapi.responses import BSONJSResponse + from fastapi.testclient import TestClient + + app = FastAPI(default_response_class=BSONJSResponse) + + @app.get("/bsonjs_keys") + def get_bsonjs_serialized_data(): + return {"key": "Hello World"} + + client = TestClient(app) + with client: + response = client.get("/bsonjs_keys") + + assert response.content == bsonjs.loads(json.dumps({"key": "Hello World"}))