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.
@@ -2,12 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import json
5
- import warnings
6
- from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Callable,
9
+ Mapping,
10
+ Optional,
11
+ Union,
12
+ cast,
13
+ )
7
14
 
8
- from django.core.exceptions import BadRequest, SuspiciousOperation
9
- from django.core.serializers.json import DjangoJSONEncoder
10
- from django.http import Http404, HttpResponseNotAllowed, JsonResponse
15
+ from django.http import HttpRequest, HttpResponseNotAllowed, JsonResponse
11
16
  from django.http.response import HttpResponse
12
17
  from django.template import RequestContext, Template
13
18
  from django.template.exceptions import TemplateDoesNotExist
@@ -17,30 +22,27 @@ from django.utils.decorators import classonlymethod, method_decorator
17
22
  from django.views.decorators.csrf import csrf_exempt
18
23
  from django.views.generic import View
19
24
 
20
- from strawberry.exceptions import MissingQueryError
21
- from strawberry.file_uploads.utils import replace_placeholders_with_files
22
- from strawberry.http import (
23
- parse_query_params,
24
- parse_request_data,
25
- process_result,
25
+ from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
26
+ from strawberry.http.exceptions import HTTPException
27
+ from strawberry.http.sync_base_view import SyncBaseHTTPView, SyncHTTPRequestAdapter
28
+ from strawberry.http.types import FormData, HTTPMethod, QueryParams
29
+ from strawberry.http.typevars import (
30
+ Context,
31
+ RootValue,
26
32
  )
27
- from strawberry.schema.exceptions import InvalidOperationTypeError
28
- from strawberry.types.graphql import OperationType
29
33
  from strawberry.utils.graphiql import get_graphiql_html
30
34
 
31
35
  from .context import StrawberryDjangoContext
32
36
 
33
37
  if TYPE_CHECKING:
34
- from django.http import HttpRequest
35
-
36
- from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData
37
- from strawberry.types import ExecutionResult
38
+ from strawberry.http import GraphQLHTTPResponse
38
39
 
39
40
  from ..schema import BaseSchema
40
41
 
41
42
 
43
+ # TODO: remove this and unify temporal responses
42
44
  class TemporalHttpResponse(JsonResponse):
43
- status_code = None
45
+ status_code: Optional[int] = None # pyright: ignore
44
46
 
45
47
  def __init__(self) -> None:
46
48
  super().__init__({})
@@ -49,101 +51,99 @@ class TemporalHttpResponse(JsonResponse):
49
51
  """Adopted from Django to handle `status_code=None`."""
50
52
  if self.status_code is not None:
51
53
  return super().__repr__()
54
+
52
55
  return "<{cls} status_code={status_code}{content_type}>".format(
53
56
  cls=self.__class__.__name__,
54
57
  status_code=self.status_code,
55
- content_type=self._content_type_for_repr,
58
+ content_type=self._content_type_for_repr, # pyright: ignore
56
59
  )
57
60
 
58
61
 
59
- class BaseView(View):
60
- subscriptions_enabled = False
61
- graphiql = True
62
- allow_queries_via_get = True
63
- schema: Optional[BaseSchema] = None
64
- json_encoder: Optional[Type[json.JSONEncoder]] = None
65
- json_dumps_params: Optional[Dict[str, Any]] = None
62
+ class DjangoHTTPRequestAdapter(SyncHTTPRequestAdapter):
63
+ def __init__(self, request: HttpRequest):
64
+ self.request = request
66
65
 
67
- def __init__(
68
- self,
69
- schema: BaseSchema,
70
- graphiql: bool = True,
71
- allow_queries_via_get: bool = True,
72
- subscriptions_enabled: bool = False,
73
- **kwargs: Any,
74
- ):
75
- self.schema = schema
76
- self.graphiql = graphiql
77
- self.allow_queries_via_get = allow_queries_via_get
78
- self.subscriptions_enabled = subscriptions_enabled
66
+ @property
67
+ def query_params(self) -> QueryParams:
68
+ return self.request.GET.dict()
79
69
 
80
- super().__init__(**kwargs)
70
+ @property
71
+ def body(self) -> Union[str, bytes]:
72
+ return self.request.body.decode()
81
73
 
82
- self.json_dumps_params = kwargs.pop("json_dumps_params", self.json_dumps_params)
74
+ @property
75
+ def method(self) -> HTTPMethod:
76
+ assert self.request.method is not None
83
77
 
84
- if self.json_dumps_params:
85
- warnings.warn(
86
- "json_dumps_params is deprecated, override encode_json instead",
87
- DeprecationWarning,
88
- stacklevel=2,
89
- )
78
+ return cast(HTTPMethod, self.request.method.upper())
90
79
 
91
- self.json_encoder = DjangoJSONEncoder
80
+ @property
81
+ def headers(self) -> Mapping[str, str]:
82
+ return self.request.headers
92
83
 
93
- self.json_encoder = kwargs.pop("json_encoder", self.json_encoder)
84
+ @property
85
+ def post_data(self) -> Mapping[str, Union[str, bytes]]:
86
+ return self.request.POST
94
87
 
95
- if self.json_encoder is not None:
96
- warnings.warn(
97
- "json_encoder is deprecated, override encode_json instead",
98
- DeprecationWarning,
99
- stacklevel=2,
100
- )
88
+ @property
89
+ def files(self) -> Mapping[str, Any]:
90
+ return self.request.FILES
91
+
92
+ @property
93
+ def content_type(self) -> Optional[str]:
94
+ return self.request.content_type
101
95
 
102
- def parse_body(self, request: HttpRequest) -> Dict[str, Any]:
103
- content_type = request.content_type or ""
104
96
 
105
- if "application/json" in content_type:
106
- return json.loads(request.body)
107
- elif content_type.startswith("multipart/form-data"):
108
- data = json.loads(request.POST.get("operations", "{}"))
109
- files_map = json.loads(request.POST.get("map", "{}"))
97
+ class AsyncDjangoHTTPRequestAdapter(AsyncHTTPRequestAdapter):
98
+ def __init__(self, request: HttpRequest):
99
+ self.request = request
110
100
 
111
- data = replace_placeholders_with_files(data, files_map, request.FILES)
101
+ @property
102
+ def query_params(self) -> QueryParams:
103
+ return self.request.GET.dict()
112
104
 
113
- return data
114
- elif request.method.lower() == "get" and request.META.get("QUERY_STRING"):
115
- return parse_query_params(request.GET.copy())
105
+ @property
106
+ def method(self) -> HTTPMethod:
107
+ assert self.request.method is not None
116
108
 
117
- return json.loads(request.body)
109
+ return cast(HTTPMethod, self.request.method.upper())
118
110
 
119
- def is_request_allowed(self, request: HttpRequest) -> bool:
120
- return request.method.lower() in ("get", "post")
111
+ @property
112
+ def headers(self) -> Mapping[str, str]:
113
+ return self.request.headers
121
114
 
122
- def should_render_graphiql(self, request: HttpRequest) -> bool:
123
- if request.method.lower() != "get":
124
- return False
115
+ @property
116
+ def content_type(self) -> Optional[str]:
117
+ return self.request.content_type
125
118
 
126
- if self.allow_queries_via_get and request.META.get("QUERY_STRING"):
127
- return False
119
+ async def get_body(self) -> str:
120
+ return self.request.body.decode()
128
121
 
129
- return any(
130
- supported_header in request.META.get("HTTP_ACCEPT", "")
131
- for supported_header in ("text/html", "*/*")
122
+ async def get_form_data(self) -> FormData:
123
+ return FormData(
124
+ files=self.request.FILES,
125
+ form=self.request.POST,
132
126
  )
133
127
 
134
- def get_request_data(self, request: HttpRequest) -> GraphQLRequestData:
135
- try:
136
- data = self.parse_body(request)
137
- except json.decoder.JSONDecodeError:
138
- raise SuspiciousOperation("Unable to parse request body as JSON")
139
- except KeyError:
140
- raise BadRequest("File(s) missing in form data")
141
128
 
142
- return parse_request_data(data)
129
+ class BaseView:
130
+ def __init__(
131
+ self,
132
+ schema: BaseSchema,
133
+ graphiql: bool = True,
134
+ allow_queries_via_get: bool = True,
135
+ subscriptions_enabled: bool = False,
136
+ **kwargs: Any,
137
+ ):
138
+ self.schema = schema
139
+ self.graphiql = graphiql
140
+ self.allow_queries_via_get = allow_queries_via_get
141
+ self.subscriptions_enabled = subscriptions_enabled
142
+
143
+ super().__init__(**kwargs)
143
144
 
144
- def _render_graphiql(self, request: HttpRequest, context=None) -> TemplateResponse:
145
- if not self.graphiql:
146
- raise Http404()
145
+ def render_graphiql(self, request: HttpRequest) -> HttpResponse:
146
+ context = None # TODO?
147
147
 
148
148
  try:
149
149
  template = Template(render_to_string("graphql/graphiql.html"))
@@ -158,10 +158,10 @@ class BaseView(View):
158
158
 
159
159
  return response
160
160
 
161
- def _create_response(
161
+ def create_response(
162
162
  self, response_data: GraphQLHTTPResponse, sub_response: HttpResponse
163
163
  ) -> HttpResponse:
164
- data = self.encode_json(response_data)
164
+ data = self.encode_json(response_data) # type: ignore
165
165
 
166
166
  response = HttpResponse(
167
167
  data,
@@ -171,7 +171,7 @@ class BaseView(View):
171
171
  for name, value in sub_response.items():
172
172
  response[name] = value
173
173
 
174
- if sub_response.status_code is not None:
174
+ if sub_response.status_code:
175
175
  response.status_code = sub_response.status_code
176
176
 
177
177
  for name, value in sub_response.cookies.items():
@@ -179,82 +179,57 @@ class BaseView(View):
179
179
 
180
180
  return response
181
181
 
182
- def encode_json(self, response_data: GraphQLHTTPResponse) -> str:
183
- if self.json_dumps_params:
184
- assert self.json_encoder
185
-
186
- return json.dumps(
187
- response_data, cls=self.json_encoder, **self.json_dumps_params
188
- )
189
-
190
- if self.json_encoder:
191
- return json.dumps(response_data, cls=self.json_encoder)
192
-
193
- return json.dumps(response_data)
194
182
 
183
+ class GraphQLView(
184
+ BaseView,
185
+ SyncBaseHTTPView[
186
+ HttpRequest, HttpResponse, TemporalHttpResponse, Context, RootValue
187
+ ],
188
+ View,
189
+ ):
190
+ subscriptions_enabled = False
191
+ graphiql = True
192
+ allow_queries_via_get = True
193
+ schema: BaseSchema = None # type: ignore
194
+ request_adapter_class = DjangoHTTPRequestAdapter
195
195
 
196
- class GraphQLView(BaseView):
197
- def get_root_value(self, request: HttpRequest) -> Any:
196
+ def get_root_value(self, request: HttpRequest) -> Optional[RootValue]:
198
197
  return None
199
198
 
200
199
  def get_context(self, request: HttpRequest, response: HttpResponse) -> Any:
201
200
  return StrawberryDjangoContext(request=request, response=response)
202
201
 
203
- def process_result(
204
- self, request: HttpRequest, result: ExecutionResult
205
- ) -> GraphQLHTTPResponse:
206
- return process_result(result)
202
+ def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse:
203
+ return TemporalHttpResponse()
207
204
 
208
205
  @method_decorator(csrf_exempt)
209
206
  def dispatch(
210
- self, request, *args, **kwargs
207
+ self, request: HttpRequest, *args: Any, **kwargs: Any
211
208
  ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponse]:
212
- if not self.is_request_allowed(request):
213
- return HttpResponseNotAllowed(
214
- ["GET", "POST"], "GraphQL only supports GET and POST requests."
215
- )
216
-
217
- if self.should_render_graphiql(request):
218
- return self._render_graphiql(request)
219
-
220
- request_data = self.get_request_data(request)
221
-
222
- sub_response = TemporalHttpResponse()
223
- context = self.get_context(request, response=sub_response)
224
- root_value = self.get_root_value(request)
225
-
226
- method = request.method
227
- allowed_operation_types = OperationType.from_http(method)
228
-
229
- if not self.allow_queries_via_get and method == "GET":
230
- allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
231
-
232
- assert self.schema
233
-
234
209
  try:
235
- result = self.schema.execute_sync(
236
- request_data.query,
237
- root_value=root_value,
238
- variable_values=request_data.variables,
239
- context_value=context,
240
- operation_name=request_data.operation_name,
241
- allowed_operation_types=allowed_operation_types,
210
+ return self.run(request=request)
211
+ except HTTPException as e:
212
+ return HttpResponse(
213
+ content=e.reason,
214
+ status=e.status_code,
242
215
  )
243
- except InvalidOperationTypeError as e:
244
- raise BadRequest(e.as_http_error_reason(method)) from e
245
- except MissingQueryError:
246
- raise SuspiciousOperation("No GraphQL query found in the request")
247
216
 
248
- response_data = self.process_result(request=request, result=result)
249
-
250
- return self._create_response(
251
- response_data=response_data, sub_response=sub_response
252
- )
253
217
 
218
+ class AsyncGraphQLView(
219
+ BaseView,
220
+ AsyncBaseHTTPView[
221
+ HttpRequest, HttpResponse, TemporalHttpResponse, Context, RootValue
222
+ ],
223
+ View,
224
+ ):
225
+ subscriptions_enabled = False
226
+ graphiql = True
227
+ allow_queries_via_get = True
228
+ schema: BaseSchema = None # type: ignore
229
+ request_adapter_class = AsyncDjangoHTTPRequestAdapter
254
230
 
255
- class AsyncGraphQLView(BaseView):
256
231
  @classonlymethod
257
- def as_view(cls, **initkwargs) -> Callable[..., HttpResponse]:
232
+ def as_view(cls, **initkwargs: Any) -> Callable[..., HttpResponse]:
258
233
  # This code tells django that this view is async, see docs here:
259
234
  # https://docs.djangoproject.com/en/3.1/topics/async/#async-views
260
235
 
@@ -262,60 +237,23 @@ class AsyncGraphQLView(BaseView):
262
237
  view._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] # noqa: E501
263
238
  return view
264
239
 
265
- @method_decorator(csrf_exempt)
266
- async def dispatch(
267
- self, request, *args, **kwargs
268
- ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponse]:
269
- if not self.is_request_allowed(request):
270
- return HttpResponseNotAllowed(
271
- ["GET", "POST"], "GraphQL only supports GET and POST requests."
272
- )
273
-
274
- if self.should_render_graphiql(request):
275
- return self._render_graphiql(request)
276
-
277
- request_data = self.get_request_data(request)
278
-
279
- sub_response = TemporalHttpResponse()
280
- context = await self.get_context(request, response=sub_response)
281
- root_value = await self.get_root_value(request)
282
-
283
- method = request.method
284
-
285
- allowed_operation_types = OperationType.from_http(method)
286
-
287
- if not self.allow_queries_via_get and method == "GET":
288
- allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
289
-
290
- assert self.schema
291
-
292
- try:
293
- result = await self.schema.execute(
294
- request_data.query,
295
- root_value=root_value,
296
- variable_values=request_data.variables,
297
- context_value=context,
298
- operation_name=request_data.operation_name,
299
- allowed_operation_types=allowed_operation_types,
300
- )
301
- except InvalidOperationTypeError as e:
302
- raise BadRequest(e.as_http_error_reason(method)) from e
303
- except MissingQueryError:
304
- raise SuspiciousOperation("No GraphQL query found in the request")
305
-
306
- response_data = await self.process_result(request=request, result=result)
307
-
308
- return self._create_response(
309
- response_data=response_data, sub_response=sub_response
310
- )
311
-
312
240
  async def get_root_value(self, request: HttpRequest) -> Any:
313
241
  return None
314
242
 
315
243
  async def get_context(self, request: HttpRequest, response: HttpResponse) -> Any:
316
244
  return StrawberryDjangoContext(request=request, response=response)
317
245
 
318
- async def process_result(
319
- self, request: HttpRequest, result: ExecutionResult
320
- ) -> GraphQLHTTPResponse:
321
- return process_result(result)
246
+ async def get_sub_response(self, request: HttpRequest) -> TemporalHttpResponse:
247
+ return TemporalHttpResponse()
248
+
249
+ @method_decorator(csrf_exempt)
250
+ async def dispatch( # pyright: ignore
251
+ self, request: HttpRequest, *args: Any, **kwargs: Any
252
+ ) -> Union[HttpResponseNotAllowed, TemplateResponse, HttpResponse]:
253
+ try:
254
+ return await self.run(request=request)
255
+ except HTTPException as e:
256
+ return HttpResponse(
257
+ content=e.reason,
258
+ status=e.status_code,
259
+ )