From 5195ac978b8c16e3ae076ce341e3eac31986711e Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Wed, 4 Jun 2025 04:34:05 +0500 Subject: [PATCH 01/15] feat: add bson in FastAPI and update .gitignore for pdm.lock & other additional files --- .gitignore | 4 ++++ fastapi/responses.py | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+) 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..0be42e99e 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -20,6 +20,12 @@ except ImportError: # pragma: nocover orjson = None # type: ignore +try: + import bson +except ImportError: # pragma: nocover + bson = None # type: ignore + + class UJSONResponse(JSONResponse): """ JSON response using the high-performance ujson library to serialize data to JSON. @@ -46,3 +52,22 @@ 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, generator: Any | None = None, on_unknown: Any | None = None + ) -> bytes: + assert bson is not None, "bson must be installed to use BSONResponse" + return bson.dumps(content, generator=generator, on_unknown=on_unknown) From b70500ae2462f84d367d43d7dd880da1e1ab7315 Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Wed, 4 Jun 2025 04:53:32 +0500 Subject: [PATCH 02/15] feat: add bson in FastAPI and update .gitignore for pdm.lock & other additional files --- fastapi/responses.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/fastapi/responses.py b/fastapi/responses.py index 0be42e99e..65d0ba394 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -66,8 +66,6 @@ class BSONResponse(Response): media_type = "application/bson" - def render( - self, content: Any, generator: Any | None = None, on_unknown: Any | None = None - ) -> bytes: + def render(self, content: Any) -> bytes: assert bson is not None, "bson must be installed to use BSONResponse" - return bson.dumps(content, generator=generator, on_unknown=on_unknown) + return bson.dumps(content) From b22ef059e6e4e3fd79c81c13827597a78f8dda68 Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Wed, 4 Jun 2025 20:58:11 +0500 Subject: [PATCH 03/15] feat: add bson serializing in FastAPI, upd: pyproject.toml add dependency --- fastapi/responses.py | 2 +- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi/responses.py b/fastapi/responses.py index 65d0ba394..c915cc7cb 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -66,6 +66,6 @@ class BSONResponse(Response): media_type = "application/bson" - def render(self, content: Any) -> bytes: + def render(self, content: Any) -> Any: assert bson is not None, "bson must be installed to use BSONResponse" return bson.dumps(content) diff --git a/pyproject.toml b/pyproject.toml index 1c540e2f6..658a62dba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,8 @@ 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", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop From 308c762d6ebf68cfe356755d759fa0589508227d Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Wed, 4 Jun 2025 21:38:15 +0500 Subject: [PATCH 04/15] edit: bson dependency comment for mypy in responses.py & add tests for bson serialization --- fastapi/responses.py | 4 ++-- tests/test_bson_response_class.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 tests/test_bson_response_class.py diff --git a/fastapi/responses.py b/fastapi/responses.py index c915cc7cb..34fc72986 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -21,9 +21,9 @@ except ImportError: # pragma: nocover try: - import bson + import bson # type: ignore[import-untyped] except ImportError: # pragma: nocover - bson = None # type: ignore + bson = None class UJSONResponse(JSONResponse): diff --git a/tests/test_bson_response_class.py b/tests/test_bson_response_class.py new file mode 100644 index 000000000..b603c3366 --- /dev/null +++ b/tests/test_bson_response_class.py @@ -0,0 +1,21 @@ +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}) From 6b3fc525735679e8e6593419bd4eef39c2378221 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:39:36 +0000 Subject: [PATCH 05/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_bson_response_class.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_bson_response_class.py b/tests/test_bson_response_class.py index b603c3366..f23054efe 100644 --- a/tests/test_bson_response_class.py +++ b/tests/test_bson_response_class.py @@ -1,5 +1,4 @@ import bson - from fastapi import FastAPI from fastapi.responses import BSONResponse from fastapi.testclient import TestClient From d15d27e4d084c5884302459c0ed9d501bb2ff35a Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Mon, 16 Jun 2025 03:35:54 +0500 Subject: [PATCH 06/15] add: python-bsonjs response class (with C ffi), tests --- fastapi/responses.py | 23 +++++++++++++++++++++++ pyproject.toml | 3 +++ tests/test_bsonjs_response_class.py | 23 +++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 tests/test_bsonjs_response_class.py diff --git a/fastapi/responses.py b/fastapi/responses.py index 34fc72986..df2c5b595 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -1,5 +1,7 @@ from typing import Any +import json + from starlette.responses import FileResponse as FileResponse # noqa from starlette.responses import HTMLResponse as HTMLResponse # noqa from starlette.responses import JSONResponse as JSONResponse # noqa @@ -26,6 +28,12 @@ except ImportError: # pragma: nocover bson = None +try: + import bsonjs # type: ignore[import-untyped] +except ImportError: # pragma: nocover + bsonjs = None + + class UJSONResponse(JSONResponse): """ JSON response using the high-performance ujson library to serialize data to JSON. @@ -69,3 +77,18 @@ class BSONResponse(Response): 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 658a62dba..6b9902a2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dependencies = [ "starlette>=0.40.0,<0.47.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", + "python-bsonjs>=0.5.0", ] [project.urls] @@ -94,6 +95,8 @@ all = [ "orjson >=3.2.1", # For BSONResponse "bson >=0.5.10", + # For BSONJSResponse + "python-bsonjs >=0.6.0", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py new file mode 100644 index 000000000..2f391590f --- /dev/null +++ b/tests/test_bsonjs_response_class.py @@ -0,0 +1,23 @@ +import bsonjs +import json + +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", 1: 1} + + +client = TestClient(app) + + +def test_bsonjs_serialized_data(): + with client: + response = client.get("/bsonjs_keys") + + assert response.content == bsonjs.loads(json.dumps({"key": "Hello World", 1: 1})) From 8e13a97c88f39353f56031a09f74b44374c9f7fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 22:36:54 +0000 Subject: [PATCH 07/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/responses.py | 3 +-- tests/test_bsonjs_response_class.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/fastapi/responses.py b/fastapi/responses.py index df2c5b595..4c90fcaad 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -1,6 +1,5 @@ -from typing import Any - import json +from typing import Any from starlette.responses import FileResponse as FileResponse # noqa from starlette.responses import HTMLResponse as HTMLResponse # noqa diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py index 2f391590f..1a577fc8a 100644 --- a/tests/test_bsonjs_response_class.py +++ b/tests/test_bsonjs_response_class.py @@ -1,6 +1,6 @@ -import bsonjs import json +import bsonjs from fastapi import FastAPI from fastapi.responses import BSONJSResponse from fastapi.testclient import TestClient From 231dc046fb6844de8c551d562343d33697ba0291 Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Mon, 16 Jun 2025 03:46:43 +0500 Subject: [PATCH 08/15] add: python-bsonjs response class (with C ffi), tests --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6b9902a2f..ab4c67757 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,7 +96,7 @@ all = [ # For BSONResponse "bson >=0.5.10", # For BSONJSResponse - "python-bsonjs >=0.6.0", + "python-bsonjs >=0.6.0; python_version >= '3.9'", # To validate email fields "email-validator >=2.0.0", # Uvicorn with uvloop From 9fbd418aad99e2a7cff8c8207ee1645db7e17e32 Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Mon, 16 Jun 2025 03:56:34 +0500 Subject: [PATCH 09/15] add: python-bsonjs response class (with C ffi), tests --- fastapi/responses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/responses.py b/fastapi/responses.py index 4c90fcaad..e7e908933 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -28,7 +28,7 @@ except ImportError: # pragma: nocover try: - import bsonjs # type: ignore[import-untyped] + import bsonjs # type: ignore except ImportError: # pragma: nocover bsonjs = None From 6d1d2ba01a198f9cd5a7fdd4a6e01268fee1c45d Mon Sep 17 00:00:00 2001 From: Kamran Pulatov <84636918+WrldEngine@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:16:55 +0500 Subject: [PATCH 10/15] upd: pyproject.toml, remove python-bsonjs from core deps --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ab4c67757..1db29c201 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,6 @@ dependencies = [ "starlette>=0.40.0,<0.47.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", - "python-bsonjs>=0.5.0", ] [project.urls] From 3187602f65593d431dcba40d546d52757b669b55 Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Fri, 20 Jun 2025 14:37:52 +0500 Subject: [PATCH 11/15] hotfix: tests for python 3.8 --- tests/test_bsonjs_response_class.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py index 1a577fc8a..2e853f904 100644 --- a/tests/test_bsonjs_response_class.py +++ b/tests/test_bsonjs_response_class.py @@ -1,6 +1,11 @@ import json -import bsonjs +# Because of python 3.8 doesn't support this library +try: + import bsonjs # type: ignore +except ImportError: # pragma: nocover + bsonjs = None + from fastapi import FastAPI from fastapi.responses import BSONJSResponse from fastapi.testclient import TestClient @@ -20,4 +25,9 @@ def test_bsonjs_serialized_data(): with client: response = client.get("/bsonjs_keys") - assert response.content == bsonjs.loads(json.dumps({"key": "Hello World", 1: 1})) + if bsonjs is not None: + assert response.content == bsonjs.loads( + json.dumps({"key": "Hello World", 1: 1}) + ) + + assert True From cdc85ba280e356eea1ca9eb490496cbd1a9af222 Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Fri, 20 Jun 2025 15:00:23 +0500 Subject: [PATCH 12/15] hotfix: tests for python 3.8 --- tests/test_bsonjs_response_class.py | 35 +++++++++++++---------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py index 2e853f904..a1affaf68 100644 --- a/tests/test_bsonjs_response_class.py +++ b/tests/test_bsonjs_response_class.py @@ -1,33 +1,28 @@ import json +import pytest +import sys -# Because of python 3.8 doesn't support this library -try: - import bsonjs # type: ignore -except ImportError: # pragma: nocover - bsonjs = None -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", 1: 1} +@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 -client = TestClient(app) + app = FastAPI(default_response_class=BSONJSResponse) + @app.get("/bsonjs_keys") + def get_bsonjs_serialized_data(): + return {"key": "Hello World"} -def test_bsonjs_serialized_data(): + client = TestClient(app) with client: response = client.get("/bsonjs_keys") - if bsonjs is not None: assert response.content == bsonjs.loads( json.dumps({"key": "Hello World", 1: 1}) ) - - assert True From 03f615caa2b06ed8dd523aff1b76c926043da393 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:01:27 +0000 Subject: [PATCH 13/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_bsonjs_response_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py index a1affaf68..9bd4d55d7 100644 --- a/tests/test_bsonjs_response_class.py +++ b/tests/test_bsonjs_response_class.py @@ -1,14 +1,14 @@ import json -import pytest 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 From 87bb16140c78d7af364e8a152c850fcce0bc563d Mon Sep 17 00:00:00 2001 From: WrldEngine Date: Fri, 20 Jun 2025 15:04:20 +0500 Subject: [PATCH 14/15] hotfix: tests for python 3.8 --- tests/test_bsonjs_response_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py index a1affaf68..d9d81ed34 100644 --- a/tests/test_bsonjs_response_class.py +++ b/tests/test_bsonjs_response_class.py @@ -24,5 +24,5 @@ def test_bsonjs_serialized_data(): response = client.get("/bsonjs_keys") assert response.content == bsonjs.loads( - json.dumps({"key": "Hello World", 1: 1}) + json.dumps({"key": "Hello World"}) ) From 3ea368646ecfdb9ba66c211e0e9146f003c3098f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:06:29 +0000 Subject: [PATCH 15/15] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_bsonjs_response_class.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_bsonjs_response_class.py b/tests/test_bsonjs_response_class.py index a40822f31..20b211832 100644 --- a/tests/test_bsonjs_response_class.py +++ b/tests/test_bsonjs_response_class.py @@ -23,6 +23,4 @@ def test_bsonjs_serialized_data(): with client: response = client.get("/bsonjs_keys") - assert response.content == bsonjs.loads( - json.dumps({"key": "Hello World"}) - ) + assert response.content == bsonjs.loads(json.dumps({"key": "Hello World"}))