Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 990e48a

Browse filesBrowse files
F polish (#13)
* Disable cache for publish job * Clean up kwargs in engine classes * Improve and Clean fastapi implementation
1 parent 1d4cdf5 commit 990e48a
Copy full SHA for 990e48a

File tree

Expand file treeCollapse file tree

9 files changed

+513
-29
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

9 files changed

+513
-29
lines changed
Open diff view settings
Collapse file

‎README.md‎

Copy file name to clipboardExpand all lines: README.md
+10Lines changed: 10 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ poetry add alchemyql
4646

4747
# Using uv
4848
uv add alchemyql
49+
50+
# With FastAPI Extension
51+
uv add alchemyql[fastapi]
4952
```
5053

5154
---
@@ -160,6 +163,13 @@ Other docs can be found in: <a href="https://github.com/nicholasfelixwilliams/al
160163

161164
---
162165

166+
### 📘 Extensions
167+
168+
AlchemyQL has the following extensions:
169+
- FastAPI - sync and async pre-created routers available (see <a href="https://github.com/nicholasfelixwilliams/alchemyql/tree/main/docs/EXAMPLE-FASTAPI.md" target="_blank">Doc</a> )
170+
171+
---
172+
163173
### ℹ️ License
164174

165175
This project is licensed under the terms of the MIT license.
Collapse file

‎docs/EXAMPLE-FASTAPI.md‎

Copy file name to clipboardExpand all lines: docs/EXAMPLE-FASTAPI.md
+2-19Lines changed: 2 additions & 19 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ The following minimal example uses an async sqlalchemy connection.
44

55
```py
66
from alchemyql import AlchemyQLAsync
7+
from alchemyql.fastapi import create_alchemyql_router_async
78
from fastapi import FastAPI
8-
from pydantic import BaseModel
9-
109
from .your_db import Table, get_db_session
1110

1211
app = FastAPI()
@@ -15,22 +14,6 @@ engine = AlchemyQLAsync()
1514
engine.register(Table)
1615
engine.build_schema()
1716

18-
class GraphQLRequest(BaseModel):
19-
query: str
20-
variables: dict[str, Any] | None = None
21-
22-
@app.post("/graphql/")
23-
async def graphql(request: GraphQLRequest, db = Depends(get_db_session)):
24-
res = await engine.execute_query(
25-
request.query, variables=request.variables, db_session=db
26-
)
27-
28-
response = {}
29-
if res.errors:
30-
response["errors"] = [str(err) for err in res.errors]
31-
if res.data:
32-
response["data"] = res.data
33-
34-
return response
17+
app.include_router(create_alchemyql_router_async(engine, get_db_session))
3518

3619
```
Collapse file

‎pyproject.toml‎

Copy file name to clipboardExpand all lines: pyproject.toml
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,11 @@ dev = [
3939
"pytest-asyncio>=1.3.0",
4040
"pytest-cov>=7.0.0",
4141
"ruff>=0.14.6",
42+
"fastapi>=0.110.0",
43+
"httpx>=0.28.1",
44+
]
45+
46+
[project.optional-dependencies]
47+
fastapi = [
48+
"fastapi>=0.110.0"
4249
]
Collapse file

‎src/alchemyql/engine.py‎

Copy file name to clipboardExpand all lines: src/alchemyql/engine.py
+4-4Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ def get_schema(self) -> str:
134134

135135

136136
class AlchemyQLSync(AlchemyQL):
137-
def __init__(self, *args, **kwargs):
138-
super().__init__(*args, **kwargs)
137+
def __init__(self, max_query_depth: int | None = None, *args, **kwargs):
138+
super().__init__(max_query_depth=max_query_depth, *args, **kwargs)
139139
self.is_async = False
140140

141141
def execute_query(
@@ -175,8 +175,8 @@ def execute_query(
175175

176176

177177
class AlchemyQLAsync(AlchemyQL):
178-
def __init__(self, *args, **kwargs):
179-
super().__init__(*args, **kwargs)
178+
def __init__(self, max_query_depth: int | None = None, *args, **kwargs):
179+
super().__init__(max_query_depth=max_query_depth, *args, **kwargs)
180180
self.is_async = True
181181

182182
async def execute_query(
Collapse file

‎src/alchemyql/fastapi/__init__.py‎

Copy file name to clipboard
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .router import create_alchemyql_router_async, create_alchemyql_router_sync
2+
3+
__all__ = ["create_alchemyql_router_async", "create_alchemyql_router_sync"]
Collapse file

‎src/alchemyql/fastapi/router.py‎

Copy file name to clipboard
+116Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from typing import Any, Callable
2+
3+
# This might create import errors if fastapi/pydantic are not installed
4+
from fastapi import APIRouter, Depends, Security, status
5+
from pydantic import BaseModel, Field
6+
7+
from ..engine import AlchemyQLAsync, AlchemyQLSync
8+
9+
10+
class GraphQLRequest(BaseModel):
11+
query: str = Field(..., description="GraphQL query string")
12+
variables: dict[str, Any] | None = Field(
13+
None, description="Optional GraphQL variables"
14+
)
15+
operationName: str | None = Field(None, description="Optional operation name")
16+
17+
18+
class GraphQLResponse(BaseModel):
19+
data: Any | None = Field(None, description="GraphQL response payload")
20+
errors: list[str] | None = Field(
21+
None, description="List of error messages (if any)"
22+
)
23+
24+
25+
def create_alchemyql_router_sync(
26+
engine: AlchemyQLSync,
27+
db_dependency: Callable,
28+
auth_dependency: Callable | None = None,
29+
path="/graphql",
30+
tags=["GraphQL"],
31+
) -> APIRouter:
32+
router = APIRouter(tags=tags)
33+
34+
def auth_helper():
35+
if auth_dependency:
36+
return Security(auth_dependency)
37+
return None
38+
39+
@router.get(
40+
path,
41+
status_code=status.HTTP_200_OK,
42+
summary="Retrieve GraphQL Schema",
43+
description="Returns the full GraphQL schema in SDL format.",
44+
)
45+
def graphql_schema(_=auth_helper()):
46+
return engine.get_schema()
47+
48+
@router.post(
49+
path,
50+
status_code=status.HTTP_200_OK,
51+
summary="Execute GraphQL Query",
52+
description="Executes a GraphQL query and returns the result.",
53+
)
54+
def graphql_execute(
55+
request: GraphQLRequest, db=Depends(db_dependency), _=auth_helper()
56+
) -> GraphQLResponse:
57+
res = engine.execute_query(
58+
request.query,
59+
variables=request.variables,
60+
operation=request.operationName,
61+
db_session=db,
62+
)
63+
64+
return GraphQLResponse(
65+
data=res.data,
66+
errors=[str(err) for err in res.errors] if res.errors else None,
67+
)
68+
69+
return router
70+
71+
72+
def create_alchemyql_router_async(
73+
engine: AlchemyQLAsync,
74+
db_dependency: Callable,
75+
auth_dependency: Callable | None = None,
76+
path="/graphql",
77+
tags=["GraphQL"],
78+
) -> APIRouter:
79+
router = APIRouter(tags=tags)
80+
81+
def auth_helper():
82+
if auth_dependency:
83+
return Security(auth_dependency)
84+
return None
85+
86+
@router.get(
87+
path,
88+
status_code=status.HTTP_200_OK,
89+
summary="Retrieve GraphQL Schema",
90+
description="Returns the full GraphQL schema in SDL format.",
91+
)
92+
def graphql_schema(_=auth_helper()):
93+
return engine.get_schema()
94+
95+
@router.post(
96+
path,
97+
status_code=status.HTTP_200_OK,
98+
summary="Execute GraphQL Query",
99+
description="Executes a GraphQL query and returns the result.",
100+
)
101+
async def graphql_execute(
102+
request: GraphQLRequest, db=Depends(db_dependency), _=auth_helper()
103+
) -> GraphQLResponse:
104+
res = await engine.execute_query(
105+
request.query,
106+
variables=request.variables,
107+
operation=request.operationName,
108+
db_session=db,
109+
)
110+
111+
return GraphQLResponse(
112+
data=res.data,
113+
errors=[str(err) for err in res.errors] if res.errors else None,
114+
)
115+
116+
return router
Collapse file

‎tests/conftest.py‎

Copy file name to clipboardExpand all lines: tests/conftest.py
+12-6Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pathlib import Path
66

77
import pytest
8-
from sqlalchemy import create_engine, insert
8+
from sqlalchemy import StaticPool, create_engine, insert
99
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
1010
from sqlalchemy.orm import sessionmaker
1111

@@ -75,13 +75,17 @@ def populate_db_stmts(base, test_db: str):
7575
def db_sync():
7676
@contextmanager
7777
def _factory(db_name):
78-
engine = create_engine("sqlite:///:memory:")
78+
engine = create_engine(
79+
"sqlite:///:memory:",
80+
poolclass=StaticPool,
81+
connect_args={"check_same_thread": False},
82+
)
7983
base = TEST_DATABASES[db_name]
8084

8185
# Create DB tables
8286
base.metadata.create_all(engine)
8387

84-
session_factory = sessionmaker(bind=engine)
88+
session_factory = sessionmaker(bind=engine, expire_on_commit=False)
8589
session = session_factory()
8690

8791
for stmt in populate_db_stmts(base, db_name):
@@ -105,17 +109,19 @@ def _factory(db_name):
105109
def db_async():
106110
@asynccontextmanager
107111
async def _factory(db_name):
108-
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
112+
engine = create_async_engine(
113+
"sqlite+aiosqlite:///:memory:", poolclass=StaticPool
114+
)
109115
base = TEST_DATABASES[db_name]
110116

111117
# Create DB tables
112118
async with engine.begin() as conn:
113119
await conn.run_sync(base.metadata.create_all)
114120

115121
session_factory = sessionmaker(
116-
bind=engine,
122+
bind=engine, # type: ignore
117123
class_=AsyncSession,
118-
expire_on_commit=False, # type: ignore
124+
expire_on_commit=False,
119125
)
120126
async with session_factory() as session: # type: ignore
121127
for stmt in populate_db_stmts(base, db_name):

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.