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

Sign up
Appearance settings

Commit 8774f3c

Browse files
Add WebOb integration (#129)
* Add WebOb integration * Include WebOb in integration tests
1 parent 292643d commit 8774f3c

File tree

7 files changed

+276
-1
lines changed

7 files changed

+276
-1
lines changed

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ for building GraphQL servers or integrations into existing web frameworks using
1919
| FastAPI | [fastapi](https://github.com/graphql-python/graphql-server/blob/master/docs/fastapi.md) |
2020
| Flask | [flask](https://github.com/graphql-python/graphql-server/blob/master/docs/flask.md) |
2121
| Litestar | [litestar](https://github.com/graphql-python/graphql-server/blob/master/docs/litestar.md) |
22+
| WebOb | [webob](https://github.com/graphql-python/graphql-server/blob/master/docs/webob.md) |
2223
| Quart | [quart](https://github.com/graphql-python/graphql-server/blob/master/docs/quart.md) |
2324
| Sanic | [sanic](https://github.com/graphql-python/graphql-server/blob/master/docs/sanic.md) |
2425

‎noxfile.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"django",
3939
"fastapi",
4040
"flask",
41+
"webob",
4142
"quart",
4243
"sanic",
4344
"litestar",
@@ -119,6 +120,7 @@ def tests_starlette(session: Session, gql_core: str) -> None:
119120
"channels",
120121
"fastapi",
121122
"flask",
123+
"webob",
122124
"quart",
123125
"sanic",
124126
"litestar",

‎pyproject.toml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "A library for creating GraphQL APIs"
55
authors = [{ name = "Syrus Akbary", email = "me@syrusakbary.com" }]
66
license = { text = "MIT" }
77
readme = "README.md"
8-
keywords = ["graphql", "api", "rest", "starlette", "async", "fastapi", "django", "flask", "litestar", "sanic", "channels", "aiohttp", "chalice", "pyright", "mypy", "codeflash"]
8+
keywords = ["graphql", "api", "rest", "starlette", "async", "fastapi", "django", "flask", "litestar", "sanic", "channels", "aiohttp", "chalice", "webob", "pyright", "mypy", "codeflash"]
99
classifiers = [
1010
"Development Status :: 5 - Production/Stable",
1111
"Intended Audience :: Developers",
@@ -47,6 +47,7 @@ fastapi = ["fastapi>=0.65.2", "python-multipart>=0.0.7"]
4747
chalice = ["chalice~=1.22"]
4848
litestar = ["litestar>=2; python_version~='3.10'"]
4949
pyinstrument = ["pyinstrument>=4.0.0"]
50+
webob = ["WebOb>=1.8"]
5051

5152
[tool.pytest.ini_options]
5253
# addopts = "--emoji"
@@ -64,6 +65,7 @@ markers = [
6465
"flaky",
6566
"flask",
6667
"litestar",
68+
"webob",
6769
"pydantic",
6870
"quart",
6971
"relay",

‎src/graphql_server/webob/__init__.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .views import GraphQLView
2+
3+
__all__ = ["GraphQLView"]

‎src/graphql_server/webob/views.py‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from __future__ import annotations
2+
3+
import warnings
4+
from typing import TYPE_CHECKING, Any, Mapping, Optional, Union, cast
5+
from typing_extensions import TypeGuard
6+
7+
from webob import Request, Response
8+
9+
from graphql_server.http import GraphQLRequestData
10+
from graphql_server.http.exceptions import HTTPException
11+
from graphql_server.http.sync_base_view import SyncBaseHTTPView, SyncHTTPRequestAdapter
12+
from graphql_server.http.typevars import Context, RootValue
13+
from graphql_server.http.types import HTTPMethod, QueryParams
14+
15+
if TYPE_CHECKING:
16+
from graphql.type import GraphQLSchema
17+
from graphql_server.http import GraphQLHTTPResponse
18+
from graphql_server.http.ides import GraphQL_IDE
19+
20+
21+
class WebobHTTPRequestAdapter(SyncHTTPRequestAdapter):
22+
def __init__(self, request: Request) -> None:
23+
self.request = request
24+
25+
@property
26+
def query_params(self) -> QueryParams:
27+
return dict(self.request.GET.items())
28+
29+
@property
30+
def body(self) -> Union[str, bytes]:
31+
return self.request.body
32+
33+
@property
34+
def method(self) -> HTTPMethod:
35+
return cast("HTTPMethod", self.request.method.upper())
36+
37+
@property
38+
def headers(self) -> Mapping[str, str]:
39+
return self.request.headers
40+
41+
@property
42+
def post_data(self) -> Mapping[str, Union[str, bytes]]:
43+
return self.request.POST
44+
45+
@property
46+
def files(self) -> Mapping[str, Any]:
47+
return {
48+
name: value.file
49+
for name, value in self.request.POST.items()
50+
if hasattr(value, "file")
51+
}
52+
53+
@property
54+
def content_type(self) -> Optional[str]:
55+
return self.request.content_type
56+
57+
58+
class GraphQLView(
59+
SyncBaseHTTPView[Request, Response, Response, Context, RootValue],
60+
):
61+
allow_queries_via_get: bool = True
62+
request_adapter_class = WebobHTTPRequestAdapter
63+
64+
def __init__(
65+
self,
66+
schema: GraphQLSchema,
67+
graphiql: Optional[bool] = None,
68+
graphql_ide: Optional[GraphQL_IDE] = "graphiql",
69+
allow_queries_via_get: bool = True,
70+
multipart_uploads_enabled: bool = False,
71+
) -> None:
72+
self.schema = schema
73+
self.allow_queries_via_get = allow_queries_via_get
74+
self.multipart_uploads_enabled = multipart_uploads_enabled
75+
76+
if graphiql is not None:
77+
warnings.warn(
78+
"The `graphiql` argument is deprecated in favor of `graphql_ide`",
79+
DeprecationWarning,
80+
stacklevel=2,
81+
)
82+
self.graphql_ide = "graphiql" if graphiql else None
83+
else:
84+
self.graphql_ide = graphql_ide
85+
86+
def get_root_value(self, request: Request) -> Optional[RootValue]:
87+
return None
88+
89+
def get_context(self, request: Request, response: Response) -> Context:
90+
return {"request": request, "response": response} # type: ignore
91+
92+
def get_sub_response(self, request: Request) -> Response:
93+
return Response(status=200, content_type="application/json")
94+
95+
def create_response(
96+
self,
97+
response_data: GraphQLHTTPResponse,
98+
sub_response: Response,
99+
is_strict: bool,
100+
) -> Response:
101+
sub_response.text = self.encode_json(response_data)
102+
sub_response.content_type = (
103+
"application/graphql-response+json" if is_strict else "application/json"
104+
)
105+
return sub_response
106+
107+
def render_graphql_ide(
108+
self, request: Request, request_data: GraphQLRequestData
109+
) -> Response:
110+
return Response(
111+
text=request_data.to_template_string(self.graphql_ide_html),
112+
content_type="text/html",
113+
status=200,
114+
)
115+
116+
def dispatch_request(self, request: Request) -> Response:
117+
try:
118+
return self.run(request=request)
119+
except HTTPException as e:
120+
return Response(text=e.reason, status=e.status_code)
121+
122+
123+
__all__ = ["GraphQLView"]

‎src/tests/http/clients/webob.py‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
import contextvars
5+
import functools
6+
import json
7+
import urllib.parse
8+
from io import BytesIO
9+
from typing import Any, Optional, Union
10+
from typing_extensions import Literal
11+
12+
from graphql import ExecutionResult
13+
from webob import Request, Response
14+
15+
from graphql_server.http import GraphQLHTTPResponse
16+
from graphql_server.http.ides import GraphQL_IDE
17+
from graphql_server.webob import GraphQLView as BaseGraphQLView
18+
from tests.http.context import get_context
19+
from tests.views.schema import Query, schema
20+
21+
from .base import JSON, HttpClient, Response as ClientResponse, ResultOverrideFunction
22+
23+
24+
class GraphQLView(BaseGraphQLView[dict[str, object], object]):
25+
result_override: ResultOverrideFunction = None
26+
27+
def get_root_value(self, request: Request) -> Query:
28+
super().get_root_value(request) # for coverage
29+
return Query()
30+
31+
def get_context(self, request: Request, response: Response) -> dict[str, object]:
32+
context = super().get_context(request, response)
33+
return get_context(context)
34+
35+
def process_result(
36+
self, request: Request, result: ExecutionResult, strict: bool = False
37+
) -> GraphQLHTTPResponse:
38+
if self.result_override:
39+
return self.result_override(result)
40+
return super().process_result(request, result, strict)
41+
42+
43+
class WebobHttpClient(HttpClient):
44+
def __init__(
45+
self,
46+
graphiql: Optional[bool] = None,
47+
graphql_ide: Optional[GraphQL_IDE] = "graphiql",
48+
allow_queries_via_get: bool = True,
49+
result_override: ResultOverrideFunction = None,
50+
multipart_uploads_enabled: bool = False,
51+
) -> None:
52+
self.view = GraphQLView(
53+
schema=schema,
54+
graphiql=graphiql,
55+
graphql_ide=graphql_ide,
56+
allow_queries_via_get=allow_queries_via_get,
57+
multipart_uploads_enabled=multipart_uploads_enabled,
58+
)
59+
self.view.result_override = result_override
60+
61+
async def _graphql_request(
62+
self,
63+
method: Literal["get", "post"],
64+
query: Optional[str] = None,
65+
operation_name: Optional[str] = None,
66+
variables: Optional[dict[str, object]] = None,
67+
files: Optional[dict[str, BytesIO]] = None,
68+
headers: Optional[dict[str, str]] = None,
69+
extensions: Optional[dict[str, Any]] = None,
70+
**kwargs: Any,
71+
) -> ClientResponse:
72+
body = self._build_body(
73+
query=query,
74+
operation_name=operation_name,
75+
variables=variables,
76+
files=files,
77+
method=method,
78+
extensions=extensions,
79+
)
80+
81+
data: Union[dict[str, object], str, None] = None
82+
83+
url = "/graphql"
84+
85+
if body and files:
86+
body.update({name: (file, name) for name, file in files.items()})
87+
88+
if method == "get":
89+
body_encoded = urllib.parse.urlencode(body or {})
90+
url = f"{url}?{body_encoded}"
91+
else:
92+
if body:
93+
data = body if files else json.dumps(body)
94+
kwargs["body"] = data
95+
96+
headers = self._get_headers(method=method, headers=headers, files=files)
97+
98+
return await self.request(url, method, headers=headers, **kwargs)
99+
100+
def _do_request(
101+
self,
102+
url: str,
103+
method: Literal["get", "post", "patch", "put", "delete"],
104+
headers: Optional[dict[str, str]] = None,
105+
**kwargs: Any,
106+
) -> ClientResponse:
107+
body = kwargs.get("body", None)
108+
req = Request.blank(
109+
url, method=method.upper(), headers=headers or {}, body=body
110+
)
111+
resp = self.view.dispatch_request(req)
112+
return ClientResponse(
113+
status_code=resp.status_code, data=resp.body, headers=resp.headers
114+
)
115+
116+
async def request(
117+
self,
118+
url: str,
119+
method: Literal["head", "get", "post", "patch", "put", "delete"],
120+
headers: Optional[dict[str, str]] = None,
121+
**kwargs: Any,
122+
) -> ClientResponse:
123+
loop = asyncio.get_running_loop()
124+
ctx = contextvars.copy_context()
125+
func_call = functools.partial(
126+
ctx.run, self._do_request, url=url, method=method, headers=headers, **kwargs
127+
)
128+
return await loop.run_in_executor(None, func_call) # type: ignore
129+
130+
async def get(
131+
self, url: str, headers: Optional[dict[str, str]] = None
132+
) -> ClientResponse:
133+
return await self.request(url, "get", headers=headers)
134+
135+
async def post(
136+
self,
137+
url: str,
138+
data: Optional[bytes] = None,
139+
json: Optional[JSON] = None,
140+
headers: Optional[dict[str, str]] = None,
141+
) -> ClientResponse:
142+
body = json if json is not None else data
143+
return await self.request(url, "post", headers=headers, body=body)

‎src/tests/http/conftest.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def _get_http_client_classes() -> Generator[Any, None, None]:
1818
("DjangoHttpClient", "django", [pytest.mark.django]),
1919
("FastAPIHttpClient", "fastapi", [pytest.mark.fastapi]),
2020
("FlaskHttpClient", "flask", [pytest.mark.flask]),
21+
("WebobHttpClient", "webob", [pytest.mark.webob]),
2122
("QuartHttpClient", "quart", [pytest.mark.quart]),
2223
("SanicHttpClient", "sanic", [pytest.mark.sanic]),
2324
("LitestarHttpClient", "litestar", [pytest.mark.litestar]),

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /