bedrock-fastapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: bedrock-fastapi
3
+ Version: 0.1.0
4
+ Summary: FastAPI integration for Bedrock: request DI scopes, request-bound database sessions, and BedrockExc responses.
5
+ Keywords: bedrock,fastapi,starlette,asgi,middleware
6
+ Author: maacck
7
+ Author-email: maacck <c.mai@madainchina.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: anyio>=3.4.0
18
+ Requires-Dist: bedrock-core>=0.3.1
19
+ Requires-Dist: starlette>=0.40.0
20
+ Requires-Python: >=3.12
21
+ Project-URL: Bug Tracker, https://github.com/maacck/bedrock-py/issues
22
+ Project-URL: Documentation, https://bedrock-py.com
23
+ Project-URL: Homepage, https://bedrock-py.com
24
+ Project-URL: Repository, https://github.com/maacck/bedrock-py
25
+ Description-Content-Type: text/markdown
26
+
27
+ # bedrock-fastapi
28
+
29
+ FastAPI integration for [Bedrock](https://bedrock-py.com). Your application creates and owns the FastAPI app;
30
+ this package supplies pieces you add as needed:
31
+
32
+ - `RequestScopeMiddleware`: one Bedrock DI scope per request.
33
+ - `DbSessionMiddleware` / `AsyncDbSessionMiddleware`: each request runs inside `db.session_scope()` /
34
+ `db.asession_scope()`. Services commit; uncommitted work is rolled back when the request ends.
35
+ - `bedrock_exception_handler(status_codes)`: answers mapped `BedrockExc` subclasses with `{"detail": ...}`.
36
+
37
+ ```python
38
+ from bedrock.exc import BedrockExc
39
+ from bedrock_fastapi import AsyncDbSessionMiddleware, RequestScopeMiddleware, bedrock_exception_handler
40
+
41
+ app.add_middleware(AsyncDbSessionMiddleware)
42
+ app.add_middleware(RequestScopeMiddleware)
43
+ app.add_exception_handler(BedrockExc, bedrock_exception_handler({ItemNotFound: 404}))
44
+ ```
45
+
46
+ See the FastAPI integration docs at https://bedrock-py.com/docs/integrations/fastapi for installation, the
47
+ transaction convention, and a complete example.
@@ -0,0 +1,21 @@
1
+ # bedrock-fastapi
2
+
3
+ FastAPI integration for [Bedrock](https://bedrock-py.com). Your application creates and owns the FastAPI app;
4
+ this package supplies pieces you add as needed:
5
+
6
+ - `RequestScopeMiddleware`: one Bedrock DI scope per request.
7
+ - `DbSessionMiddleware` / `AsyncDbSessionMiddleware`: each request runs inside `db.session_scope()` /
8
+ `db.asession_scope()`. Services commit; uncommitted work is rolled back when the request ends.
9
+ - `bedrock_exception_handler(status_codes)`: answers mapped `BedrockExc` subclasses with `{"detail": ...}`.
10
+
11
+ ```python
12
+ from bedrock.exc import BedrockExc
13
+ from bedrock_fastapi import AsyncDbSessionMiddleware, RequestScopeMiddleware, bedrock_exception_handler
14
+
15
+ app.add_middleware(AsyncDbSessionMiddleware)
16
+ app.add_middleware(RequestScopeMiddleware)
17
+ app.add_exception_handler(BedrockExc, bedrock_exception_handler({ItemNotFound: 404}))
18
+ ```
19
+
20
+ See the FastAPI integration docs at https://bedrock-py.com/docs/integrations/fastapi for installation, the
21
+ transaction convention, and a complete example.
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "bedrock-fastapi"
3
+ version = "0.1.0"
4
+ description = "FastAPI integration for Bedrock: request DI scopes, request-bound database sessions, and BedrockExc responses."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "maacck", email = "c.mai@madainchina.com" }
9
+ ]
10
+ keywords = ["bedrock", "fastapi", "starlette", "asgi", "middleware"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Framework :: FastAPI",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Typing :: Typed",
20
+ ]
21
+ requires-python = ">=3.12"
22
+ dependencies = [
23
+ "anyio>=3.4.0",
24
+ "bedrock-core>=0.3.1",
25
+ "starlette>=0.40.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://bedrock-py.com"
30
+ Repository = "https://github.com/maacck/bedrock-py"
31
+ Documentation = "https://bedrock-py.com"
32
+ "Bug Tracker" = "https://github.com/maacck/bedrock-py/issues"
33
+
34
+ [build-system]
35
+ requires = ["uv_build>=0.8.15,<0.9.0"]
36
+ build-backend = "uv_build"
37
+
38
+ [tool.uv.build-backend]
39
+ module-root = "src"
40
+ module-name = "bedrock_fastapi"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.4.2",
45
+ "fastapi>=0.115.0",
46
+ "httpx2>=0.1.0",
47
+ "aiosqlite>=0.20.0",
48
+ ]
@@ -0,0 +1,14 @@
1
+ """FastAPI integration for Bedrock.
2
+
3
+ The host application creates and owns its FastAPI app. Add only the pieces it needs.
4
+ """
5
+
6
+ from .exceptions import bedrock_exception_handler
7
+ from .middleware import AsyncDbSessionMiddleware, DbSessionMiddleware, RequestScopeMiddleware
8
+
9
+ __all__ = [
10
+ "AsyncDbSessionMiddleware",
11
+ "DbSessionMiddleware",
12
+ "RequestScopeMiddleware",
13
+ "bedrock_exception_handler",
14
+ ]
@@ -0,0 +1,45 @@
1
+ """Translate Bedrock exceptions into HTTP responses."""
2
+
3
+ from collections.abc import Awaitable, Callable, Mapping
4
+
5
+ from bedrock.exc import BedrockExc
6
+ from starlette.requests import Request
7
+ from starlette.responses import JSONResponse, Response
8
+
9
+
10
+ def bedrock_exception_handler(
11
+ status_codes: Mapping[type[BedrockExc], int],
12
+ ) -> Callable[[Request, Exception], Awaitable[Response]]:
13
+ """Build an exception handler that answers mapped Bedrock exceptions with ``{"detail": exc.detail}``.
14
+
15
+ The lookup walks the raised exception's MRO, so mapping a module's base exception covers its subclasses
16
+ and the most specific mapping wins. An unmapped exception is re-raised: the server-error middleware logs
17
+ it and returns a generic 500 without exposing ``detail``, because ``BedrockExc`` is also the base of
18
+ runtime errors such as ``ImproperlyConfigured``. WebSocket connections are never answered: an exception there
19
+ is re-raised, because an HTTP response cannot be sent once the socket is accepted.
20
+
21
+ Example::
22
+
23
+ app.add_exception_handler(
24
+ BedrockExc,
25
+ bedrock_exception_handler(
26
+ {ItemNotFound: 404, ItemConflict: 409}
27
+ ),
28
+ )
29
+
30
+ Args:
31
+ status_codes: HTTP status code per exception class.
32
+
33
+ Returns:
34
+ An async exception handler for ``app.add_exception_handler``.
35
+ """
36
+
37
+ async def handle(request: Request, exc: Exception) -> Response:
38
+ if request.scope["type"] == "http" and isinstance(exc, BedrockExc):
39
+ matches = [cls for cls in status_codes if isinstance(exc, cls)]
40
+ if matches:
41
+ most_specific = min(matches, key=type(exc).__mro__.index)
42
+ return JSONResponse({"detail": exc.detail}, status_code=status_codes[most_specific])
43
+ raise exc
44
+
45
+ return handle
@@ -0,0 +1,126 @@
1
+ """Pure ASGI middleware that give each request its own Bedrock DI scope and database sessions.
2
+
3
+ The middleware enter and leave request state in the request's own context. FastAPI runs sync (``def``)
4
+ dependencies and endpoints in worker threads with copied contexts, so state entered there cannot be reset
5
+ or cleaned up reliably at the end of the request.
6
+ """
7
+
8
+ from anyio import CancelScope
9
+ from bedrock.database import db
10
+ from bedrock.di import container
11
+ from starlette.concurrency import run_in_threadpool
12
+ from starlette.types import ASGIApp, Receive, Scope, Send
13
+
14
+ _REQUEST_TYPES = frozenset({"http", "websocket"})
15
+
16
+
17
+ class RequestScopeMiddleware:
18
+ """Run each HTTP or WebSocket connection inside ``container.scope("request")``.
19
+
20
+ ``SCOPED`` services resolved while the connection is handled share one instance, and their ``close()``
21
+ runs when it ends. Lifespan events pass through.
22
+ """
23
+
24
+ def __init__(self, app: ASGIApp) -> None:
25
+ """Wrap the downstream application.
26
+
27
+ Args:
28
+ app: The downstream ASGI application.
29
+ """
30
+ self.app = app
31
+
32
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
33
+ """Handle one ASGI connection.
34
+
35
+ Args:
36
+ scope: The ASGI connection scope.
37
+ receive: The ASGI receive channel.
38
+ send: The ASGI send channel.
39
+ """
40
+ if scope["type"] not in _REQUEST_TYPES:
41
+ await self.app(scope, receive, send)
42
+ return
43
+ with container.scope("request"):
44
+ await self.app(scope, receive, send)
45
+
46
+
47
+ class DbSessionMiddleware:
48
+ """Run each HTTP request inside ``db.session_scope()`` without committing on its behalf.
49
+
50
+ Services work in ``db.session`` and commit, as they do outside HTTP. Whatever is still uncommitted when the
51
+ request ends, including work done by background tasks, is rolled back, so a write nobody commits is lost
52
+ while the response may still be a success.
53
+
54
+ Because a session is already bound, ``db.session_scope()`` raises ``RuntimeError`` inside a request; use
55
+ ``db.independent_session()`` for side transactions. WebSocket and lifespan events pass through.
56
+ """
57
+
58
+ def __init__(self, app: ASGIApp) -> None:
59
+ """Wrap the downstream application.
60
+
61
+ Args:
62
+ app: The downstream ASGI application.
63
+ """
64
+ self.app = app
65
+
66
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
67
+ """Handle one ASGI connection.
68
+
69
+ Args:
70
+ scope: The ASGI connection scope.
71
+ receive: The ASGI receive channel.
72
+ send: The ASGI send channel.
73
+
74
+ Raises:
75
+ DatabaseNotConfiguredError: When ``db.init()`` has not been called.
76
+ """
77
+ if scope["type"] != "http":
78
+ await self.app(scope, receive, send)
79
+ return
80
+ with db.session_scope() as session:
81
+ try:
82
+ await self.app(scope, receive, send)
83
+ finally:
84
+ # Discard uncommitted work so the scope's own commit has nothing left. Shielded so a cancelled
85
+ # request still returns its connection, and in a worker thread because the rollback is blocking I/O.
86
+ with CancelScope(shield=True):
87
+ await run_in_threadpool(session.rollback)
88
+
89
+
90
+ class AsyncDbSessionMiddleware:
91
+ """Run each HTTP request inside ``db.asession_scope()`` without committing on its behalf.
92
+
93
+ Same contract as :class:`DbSessionMiddleware`: services commit with ``await db.asession.commit()``,
94
+ uncommitted work is rolled back when the request ends, and ``db.asession_scope()`` raises ``RuntimeError``
95
+ inside a request (use ``db.aindependent_session()``). The sync and async sessions use separate context
96
+ variables, so both middleware can be installed together; each has its own transaction.
97
+ """
98
+
99
+ def __init__(self, app: ASGIApp) -> None:
100
+ """Wrap the downstream application.
101
+
102
+ Args:
103
+ app: The downstream ASGI application.
104
+ """
105
+ self.app = app
106
+
107
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
108
+ """Handle one ASGI connection.
109
+
110
+ Args:
111
+ scope: The ASGI connection scope.
112
+ receive: The ASGI receive channel.
113
+ send: The ASGI send channel.
114
+
115
+ Raises:
116
+ DatabaseNotConfiguredError: When ``db.init()`` has not been called.
117
+ """
118
+ if scope["type"] != "http":
119
+ await self.app(scope, receive, send)
120
+ return
121
+ async with db.asession_scope() as session:
122
+ try:
123
+ await self.app(scope, receive, send)
124
+ finally:
125
+ with CancelScope(shield=True):
126
+ await session.rollback()
File without changes