strawberry-graphql 0.168.2__py3-none-any.whl → 0.170.0__py3-none-any.whl

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.
strawberry/flask/views.py CHANGED
@@ -1,33 +1,59 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
- from typing import TYPE_CHECKING, Dict
3
+ from typing import TYPE_CHECKING, Any, List, Mapping, Optional, Union, cast
5
4
 
6
- from flask import Response, render_template_string, request
5
+ from flask import Request, Response, render_template_string, request
6
+ from flask.views import View
7
+ from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
8
+ from strawberry.http.exceptions import HTTPException
9
+ from strawberry.http.sync_base_view import (
10
+ SyncBaseHTTPView,
11
+ SyncHTTPRequestAdapter,
12
+ )
13
+ from strawberry.http.types import FormData, HTTPMethod, QueryParams
14
+ from strawberry.http.typevars import Context, RootValue
15
+ from strawberry.utils.graphiql import get_graphiql_html
7
16
 
8
17
  if TYPE_CHECKING:
9
18
  from flask.typing import ResponseReturnValue
10
19
  from strawberry.http import GraphQLHTTPResponse
11
20
  from strawberry.schema.base import BaseSchema
12
- from strawberry.types import ExecutionResult
13
21
 
14
- from flask.views import View
15
- from strawberry.exceptions import MissingQueryError
16
- from strawberry.file_uploads.utils import replace_placeholders_with_files
17
- from strawberry.flask.graphiql import should_render_graphiql
18
- from strawberry.http import (
19
- parse_query_params,
20
- parse_request_data,
21
- process_result,
22
- )
23
- from strawberry.schema.exceptions import InvalidOperationTypeError
24
- from strawberry.types.graphql import OperationType
25
- from strawberry.utils.graphiql import get_graphiql_html
26
22
 
23
+ class FlaskHTTPRequestAdapter(SyncHTTPRequestAdapter):
24
+ def __init__(self, request: Request):
25
+ self.request = request
27
26
 
28
- class BaseGraphQLView(View):
29
- methods = ["GET", "POST"]
27
+ @property
28
+ def query_params(self) -> Mapping[str, Union[str, Optional[List[str]]]]:
29
+ return self.request.args.to_dict()
30
+
31
+ @property
32
+ def body(self) -> Union[str, bytes]:
33
+ return self.request.data.decode()
34
+
35
+ @property
36
+ def method(self) -> HTTPMethod:
37
+ return cast(HTTPMethod, self.request.method.upper())
30
38
 
39
+ @property
40
+ def headers(self) -> Mapping[str, str]:
41
+ return self.request.headers
42
+
43
+ @property
44
+ def post_data(self) -> Mapping[str, Union[str, bytes]]:
45
+ return self.request.form
46
+
47
+ @property
48
+ def files(self) -> Mapping[str, Any]:
49
+ return self.request.files
50
+
51
+ @property
52
+ def content_type(self) -> Optional[str]:
53
+ return self.request.content_type
54
+
55
+
56
+ class BaseGraphQLView:
31
57
  def __init__(
32
58
  self,
33
59
  schema: BaseSchema,
@@ -38,187 +64,100 @@ class BaseGraphQLView(View):
38
64
  self.graphiql = graphiql
39
65
  self.allow_queries_via_get = allow_queries_via_get
40
66
 
41
- def render_template(self, template: str) -> str:
42
- return render_template_string(template)
67
+ def render_graphiql(self, request: Request) -> Response:
68
+ template = get_graphiql_html(False)
43
69
 
44
- def encode_json(self, response_data: GraphQLHTTPResponse) -> str:
45
- return json.dumps(response_data)
70
+ return render_template_string(template) # type: ignore
46
71
 
72
+ def create_response(
73
+ self, response_data: GraphQLHTTPResponse, sub_response: Response
74
+ ) -> Response:
75
+ sub_response.set_data(self.encode_json(response_data)) # type: ignore
47
76
 
48
- class GraphQLView(BaseGraphQLView):
49
- def get_root_value(self) -> object:
50
- return None
77
+ return sub_response
51
78
 
52
- def get_context(self, response: Response) -> Dict[str, object]:
53
- return {"request": request, "response": response}
54
79
 
55
- def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse:
56
- return process_result(result)
80
+ class GraphQLView(
81
+ BaseGraphQLView,
82
+ SyncBaseHTTPView[Request, Response, Response, Context, RootValue],
83
+ View,
84
+ ):
85
+ methods = ["GET", "POST"]
86
+ allow_queries_via_get: bool = True
87
+ request_adapter_class = FlaskHTTPRequestAdapter
57
88
 
58
- def dispatch_request(self) -> ResponseReturnValue:
59
- method = request.method
60
- content_type = request.content_type or ""
89
+ def get_context(self, request: Request, response: Response) -> Context:
90
+ return {"request": request, "response": response} # type: ignore
61
91
 
62
- if request.method not in {"POST", "GET"}:
63
- return Response(
64
- "Unsupported method, must be of request type POST or GET", 405
65
- )
92
+ def get_root_value(self, request: Request) -> Optional[RootValue]:
93
+ return None
66
94
 
67
- if "application/json" in content_type:
68
- try:
69
- data = json.loads(request.data)
70
- except json.JSONDecodeError:
71
- return Response(
72
- status=400, response="Unable to parse request body as JSON"
73
- )
74
- elif content_type.startswith("multipart/form-data"):
75
- try:
76
- operations = json.loads(request.form.get("operations", "{}"))
77
- files_map = json.loads(request.form.get("map", "{}"))
78
- except json.JSONDecodeError:
79
- return Response(
80
- status=400, response="Unable to parse request body as JSON"
81
- )
82
-
83
- try:
84
- data = replace_placeholders_with_files(
85
- operations, files_map, request.files
86
- )
87
- except KeyError:
88
- return Response(status=400, response="File(s) missing in form data")
89
- elif method == "GET" and request.args:
90
- try:
91
- data = parse_query_params(request.args.to_dict())
92
- except json.JSONDecodeError:
93
- return Response(
94
- status=400, response="Unable to parse request body as JSON"
95
- )
96
- elif method == "GET" and should_render_graphiql(self.graphiql, request):
97
- template = get_graphiql_html(False)
98
-
99
- return self.render_template(template=template)
100
- elif method == "GET":
101
- return Response(status=404)
102
- else:
103
- return Response("Unsupported Media Type", 415)
104
-
105
- request_data = parse_request_data(data)
106
-
107
- response = Response(status=200, content_type="application/json")
108
- context = self.get_context(response)
109
-
110
- allowed_operation_types = OperationType.from_http(method)
111
-
112
- if not self.allow_queries_via_get and method == "GET":
113
- allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
95
+ def get_sub_response(self, request: Request) -> Response:
96
+ return Response(status=200, content_type="application/json")
114
97
 
98
+ def dispatch_request(self) -> ResponseReturnValue:
115
99
  try:
116
- result = self.schema.execute_sync(
117
- request_data.query,
118
- variable_values=request_data.variables,
119
- context_value=context,
120
- operation_name=request_data.operation_name,
121
- root_value=self.get_root_value(),
122
- allowed_operation_types=allowed_operation_types,
100
+ return self.run(request=request)
101
+ except HTTPException as e:
102
+ return Response(
103
+ response=e.reason,
104
+ status=e.status_code,
123
105
  )
124
- except InvalidOperationTypeError as e:
125
- return Response(e.as_http_error_reason(method), 400)
126
- except MissingQueryError:
127
- return Response("No GraphQL query found in the request", 400)
128
106
 
129
- response_data = self.process_result(result)
130
- response.set_data(self.encode_json(response_data))
131
107
 
132
- return response
108
+ class AsyncFlaskHTTPRequestAdapter(AsyncHTTPRequestAdapter):
109
+ def __init__(self, request: Request):
110
+ self.request = request
133
111
 
112
+ @property
113
+ def query_params(self) -> QueryParams:
114
+ return self.request.args.to_dict()
134
115
 
135
- class AsyncGraphQLView(BaseGraphQLView):
136
- methods = ["GET", "POST"]
116
+ @property
117
+ def method(self) -> HTTPMethod:
118
+ return cast(HTTPMethod, self.request.method.upper())
137
119
 
138
- async def get_root_value(self) -> object:
139
- return None
120
+ @property
121
+ def content_type(self) -> Optional[str]:
122
+ return self.request.content_type
140
123
 
141
- async def get_context(self, response: Response) -> Dict[str, object]:
142
- return {"request": request, "response": response}
124
+ @property
125
+ def headers(self) -> Mapping[str, str]:
126
+ return self.request.headers
143
127
 
144
- async def process_result(self, result: ExecutionResult) -> GraphQLHTTPResponse:
145
- return process_result(result)
128
+ async def get_body(self) -> str:
129
+ return self.request.data.decode()
146
130
 
147
- async def dispatch_request(self) -> ResponseReturnValue: # type: ignore[override]
148
- method = request.method
149
- content_type = request.content_type or ""
131
+ async def get_form_data(self) -> FormData:
132
+ return FormData(
133
+ files=self.request.files,
134
+ form=self.request.form,
135
+ )
150
136
 
151
- if request.method not in {"POST", "GET"}:
152
- return Response(
153
- "Unsupported method, must be of request type POST or GET", 405
154
- )
155
137
 
156
- if "application/json" in content_type:
157
- try:
158
- data = json.loads(request.data)
159
- except json.JSONDecodeError:
160
- return Response(
161
- status=400, response="Unable to parse request body as JSON"
162
- )
163
- elif content_type.startswith("multipart/form-data"):
164
- try:
165
- operations = json.loads(request.form.get("operations", "{}"))
166
- files_map = json.loads(request.form.get("map", "{}"))
167
- except json.JSONDecodeError:
168
- return Response(
169
- status=400, response="Unable to parse request body as JSON"
170
- )
171
-
172
- try:
173
- data = replace_placeholders_with_files(
174
- operations, files_map, request.files
175
- )
176
- except KeyError:
177
- return Response(status=400, response="File(s) missing in form data")
178
- elif method == "GET" and request.args:
179
- try:
180
- data = parse_query_params(request.args.to_dict())
181
- except json.JSONDecodeError:
182
- return Response(
183
- status=400, response="Unable to parse request body as JSON"
184
- )
185
-
186
- elif method == "GET" and should_render_graphiql(self.graphiql, request):
187
- template = get_graphiql_html(False)
188
-
189
- return self.render_template(template=template)
190
- elif method == "GET":
191
- return Response(status=404)
192
- else:
193
- return Response("Unsupported Media Type", 415)
194
-
195
- request_data = parse_request_data(data)
196
-
197
- response = Response(status=200, content_type="application/json")
198
- context = await self.get_context(response)
199
-
200
- allowed_operation_types = OperationType.from_http(method)
201
-
202
- if not self.allow_queries_via_get and method == "GET":
203
- allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
204
-
205
- root_value = await self.get_root_value()
138
+ class AsyncGraphQLView(
139
+ BaseGraphQLView,
140
+ AsyncBaseHTTPView[Request, Response, Response, Context, RootValue],
141
+ View,
142
+ ):
143
+ methods = ["GET", "POST"]
144
+ allow_queries_via_get: bool = True
145
+ request_adapter_class = AsyncFlaskHTTPRequestAdapter
206
146
 
207
- try:
208
- result = await self.schema.execute(
209
- request_data.query,
210
- variable_values=request_data.variables,
211
- context_value=context,
212
- operation_name=request_data.operation_name,
213
- root_value=root_value,
214
- allowed_operation_types=allowed_operation_types,
215
- )
216
- except InvalidOperationTypeError as e:
217
- return Response(e.as_http_error_reason(method), 400)
218
- except MissingQueryError:
219
- return Response("No GraphQL query found in the request", 400)
147
+ async def get_context(self, request: Request, response: Response) -> Context:
148
+ return {"request": request, "response": response} # type: ignore
149
+
150
+ async def get_root_value(self, request: Request) -> Optional[RootValue]:
151
+ return None
220
152
 
221
- response_data = await self.process_result(result)
222
- response.set_data(self.encode_json(response_data))
153
+ async def get_sub_response(self, request: Request) -> Response:
154
+ return Response(status=200, content_type="application/json")
223
155
 
224
- return response
156
+ async def dispatch_request(self) -> ResponseReturnValue: # type: ignore
157
+ try:
158
+ return await self.run(request=request)
159
+ except HTTPException as e:
160
+ return Response(
161
+ response=e.reason,
162
+ status=e.status_code,
163
+ )
@@ -0,0 +1,215 @@
1
+ import abc
2
+ import json
3
+ from typing import (
4
+ Callable,
5
+ Dict,
6
+ Generic,
7
+ Mapping,
8
+ Optional,
9
+ Union,
10
+ )
11
+
12
+ from strawberry import UNSET
13
+ from strawberry.exceptions import MissingQueryError
14
+ from strawberry.file_uploads.utils import replace_placeholders_with_files
15
+ from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData, process_result
16
+ from strawberry.schema.base import BaseSchema
17
+ from strawberry.schema.exceptions import InvalidOperationTypeError
18
+ from strawberry.types import ExecutionResult
19
+ from strawberry.types.graphql import OperationType
20
+
21
+ from .base import BaseView
22
+ from .exceptions import HTTPException
23
+ from .types import FormData, HTTPMethod, QueryParams
24
+ from .typevars import Context, Request, Response, RootValue, SubResponse
25
+
26
+
27
+ class AsyncHTTPRequestAdapter(abc.ABC):
28
+ @property
29
+ @abc.abstractmethod
30
+ def query_params(self) -> QueryParams:
31
+ ...
32
+
33
+ @property
34
+ @abc.abstractmethod
35
+ def method(self) -> HTTPMethod:
36
+ ...
37
+
38
+ @property
39
+ @abc.abstractmethod
40
+ def headers(self) -> Mapping[str, str]:
41
+ ...
42
+
43
+ @property
44
+ @abc.abstractmethod
45
+ def content_type(self) -> Optional[str]:
46
+ ...
47
+
48
+ @abc.abstractmethod
49
+ async def get_body(self) -> Union[str, bytes]:
50
+ ...
51
+
52
+ @abc.abstractmethod
53
+ async def get_form_data(self) -> FormData:
54
+ ...
55
+
56
+
57
+ class AsyncBaseHTTPView(
58
+ abc.ABC,
59
+ BaseView[Request],
60
+ Generic[Request, Response, SubResponse, Context, RootValue],
61
+ ):
62
+ schema: BaseSchema
63
+ graphiql: bool
64
+ request_adapter_class: Callable[[Request], AsyncHTTPRequestAdapter]
65
+
66
+ @property
67
+ @abc.abstractmethod
68
+ def allow_queries_via_get(self) -> bool:
69
+ ...
70
+
71
+ @abc.abstractmethod
72
+ async def get_sub_response(self, request: Request) -> SubResponse:
73
+ ...
74
+
75
+ @abc.abstractmethod
76
+ async def get_context(self, request: Request, response: SubResponse) -> Context:
77
+ ...
78
+
79
+ @abc.abstractmethod
80
+ async def get_root_value(self, request: Request) -> Optional[RootValue]:
81
+ ...
82
+
83
+ @abc.abstractmethod
84
+ def render_graphiql(self, request: Request) -> Response:
85
+ # TODO: this could be non abstract
86
+ # maybe add a get template function?
87
+ ...
88
+
89
+ @abc.abstractmethod
90
+ def create_response(
91
+ self, response_data: GraphQLHTTPResponse, sub_response: SubResponse
92
+ ) -> Response:
93
+ ...
94
+
95
+ async def execute_operation(
96
+ self, request: Request, context: Context, root_value: Optional[RootValue]
97
+ ) -> ExecutionResult:
98
+ request_adapter = self.request_adapter_class(request)
99
+
100
+ try:
101
+ request_data = await self.parse_http_body(request_adapter)
102
+ except json.decoder.JSONDecodeError as e:
103
+ raise HTTPException(400, "Unable to parse request body as JSON") from e
104
+ # DO this only when doing files
105
+ except KeyError as e:
106
+ raise HTTPException(400, "File(s) missing in form data") from e
107
+
108
+ allowed_operation_types = OperationType.from_http(request_adapter.method)
109
+
110
+ if not self.allow_queries_via_get and request_adapter.method == "GET":
111
+ allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
112
+
113
+ assert self.schema
114
+
115
+ return await self.schema.execute(
116
+ request_data.query,
117
+ root_value=root_value,
118
+ variable_values=request_data.variables,
119
+ context_value=context,
120
+ operation_name=request_data.operation_name,
121
+ allowed_operation_types=allowed_operation_types,
122
+ )
123
+
124
+ async def parse_multipart(self, request: AsyncHTTPRequestAdapter) -> Dict[str, str]:
125
+ try:
126
+ form_data = await request.get_form_data()
127
+ except ValueError as e:
128
+ raise HTTPException(400, "Unable to parse the multipart body") from e
129
+
130
+ operations = form_data["form"].get("operations", "{}")
131
+ files_map = form_data["form"].get("map", "{}")
132
+
133
+ if isinstance(operations, (bytes, str)):
134
+ operations = self.parse_json(operations)
135
+
136
+ if isinstance(files_map, (bytes, str)):
137
+ files_map = self.parse_json(files_map)
138
+
139
+ try:
140
+ return replace_placeholders_with_files(
141
+ operations, files_map, form_data["files"]
142
+ )
143
+ except KeyError as e:
144
+ raise HTTPException(400, "File(s) missing in form data") from e
145
+
146
+ async def run(
147
+ self,
148
+ request: Request,
149
+ context: Optional[Context] = UNSET,
150
+ root_value: Optional[RootValue] = UNSET,
151
+ ) -> Response:
152
+ request_adapter = self.request_adapter_class(request)
153
+
154
+ if not self.is_request_allowed(request_adapter):
155
+ raise HTTPException(405, "GraphQL only supports GET and POST requests.")
156
+
157
+ if self.should_render_graphiql(request_adapter):
158
+ if self.graphiql:
159
+ return self.render_graphiql(request)
160
+ else:
161
+ raise HTTPException(404, "Not Found")
162
+
163
+ sub_response = await self.get_sub_response(request)
164
+ context = (
165
+ await self.get_context(request, response=sub_response)
166
+ if context is UNSET
167
+ else context
168
+ )
169
+ root_value = (
170
+ await self.get_root_value(request) if root_value is UNSET else root_value
171
+ )
172
+
173
+ assert context
174
+
175
+ try:
176
+ result = await self.execute_operation(
177
+ request=request, context=context, root_value=root_value
178
+ )
179
+ except InvalidOperationTypeError as e:
180
+ raise HTTPException(
181
+ 400, e.as_http_error_reason(request_adapter.method)
182
+ ) from e
183
+ except MissingQueryError as e:
184
+ raise HTTPException(400, "No GraphQL query found in the request") from e
185
+
186
+ response_data = await self.process_result(request=request, result=result)
187
+
188
+ return self.create_response(
189
+ response_data=response_data, sub_response=sub_response
190
+ )
191
+
192
+ async def parse_http_body(
193
+ self, request: AsyncHTTPRequestAdapter
194
+ ) -> GraphQLRequestData:
195
+ content_type = request.content_type or ""
196
+
197
+ if "application/json" in content_type:
198
+ data = self.parse_json(await request.get_body())
199
+ elif content_type.startswith("multipart/form-data"):
200
+ data = await self.parse_multipart(request)
201
+ elif request.method == "GET":
202
+ data = self.parse_query_params(request.query_params)
203
+ else:
204
+ raise HTTPException(400, "Unsupported content type")
205
+
206
+ return GraphQLRequestData(
207
+ query=data.get("query"),
208
+ variables=data.get("variables"), # type: ignore
209
+ operation_name=data.get("operationName"),
210
+ )
211
+
212
+ async def process_result(
213
+ self, request: Request, result: ExecutionResult
214
+ ) -> GraphQLHTTPResponse:
215
+ return process_result(result)
@@ -0,0 +1,63 @@
1
+ import json
2
+ from typing import Any, Dict, Generic, List, Mapping, Optional, Union
3
+ from typing_extensions import Protocol
4
+
5
+ from strawberry.http import GraphQLHTTPResponse
6
+ from strawberry.http.types import HTTPMethod
7
+
8
+ from .exceptions import HTTPException
9
+ from .typevars import Request
10
+
11
+
12
+ class BaseRequestProtocol(Protocol):
13
+ @property
14
+ def query_params(self) -> Mapping[str, Optional[Union[str, List[str]]]]:
15
+ ...
16
+
17
+ @property
18
+ def method(self) -> HTTPMethod:
19
+ ...
20
+
21
+ @property
22
+ def headers(self) -> Mapping[str, str]:
23
+ ...
24
+
25
+
26
+ class BaseView(Generic[Request]):
27
+ def should_render_graphiql(self, request: BaseRequestProtocol) -> bool:
28
+ return (
29
+ request.method == "GET"
30
+ and request.query_params.get("query") is None
31
+ and any(
32
+ supported_header in request.headers.get("accept", "")
33
+ for supported_header in ("text/html", "*/*")
34
+ )
35
+ )
36
+
37
+ def is_request_allowed(self, request: BaseRequestProtocol) -> bool:
38
+ return request.method in ("GET", "POST")
39
+
40
+ def parse_json(self, data: Union[str, bytes]) -> Dict[str, str]:
41
+ try:
42
+ return json.loads(data)
43
+ except json.JSONDecodeError as e:
44
+ raise HTTPException(400, "Unable to parse request body as JSON") from e
45
+
46
+ def encode_json(self, response_data: GraphQLHTTPResponse) -> str:
47
+ return json.dumps(response_data)
48
+
49
+ def parse_query_params(
50
+ self, params: Mapping[str, Optional[Union[str, List[str]]]]
51
+ ) -> Dict[str, Any]:
52
+ params = dict(params)
53
+
54
+ if "variables" in params:
55
+ variables = params["variables"]
56
+
57
+ if isinstance(variables, list):
58
+ variables = variables[0]
59
+
60
+ if variables:
61
+ params["variables"] = json.loads(variables)
62
+
63
+ return params
@@ -0,0 +1,4 @@
1
+ class HTTPException(Exception):
2
+ def __init__(self, status_code: int, reason: str):
3
+ self.status_code = status_code
4
+ self.reason = reason