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.
@@ -0,0 +1,210 @@
1
+ import abc
2
+ import json
3
+ from typing import (
4
+ Any,
5
+ Callable,
6
+ Dict,
7
+ Generic,
8
+ Mapping,
9
+ Optional,
10
+ Union,
11
+ )
12
+
13
+ from strawberry import UNSET
14
+ from strawberry.exceptions import MissingQueryError
15
+ from strawberry.file_uploads.utils import replace_placeholders_with_files
16
+ from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData, process_result
17
+ from strawberry.schema import BaseSchema
18
+ from strawberry.schema.exceptions import InvalidOperationTypeError
19
+ from strawberry.types import ExecutionResult
20
+ from strawberry.types.graphql import OperationType
21
+
22
+ from .base import BaseView
23
+ from .exceptions import HTTPException
24
+ from .types import HTTPMethod, QueryParams
25
+ from .typevars import Context, Request, Response, RootValue, SubResponse
26
+
27
+
28
+ class SyncHTTPRequestAdapter(abc.ABC):
29
+ @property
30
+ @abc.abstractmethod
31
+ def query_params(self) -> QueryParams:
32
+ ...
33
+
34
+ @property
35
+ @abc.abstractmethod
36
+ def body(self) -> Union[str, bytes]:
37
+ ...
38
+
39
+ @property
40
+ @abc.abstractmethod
41
+ def method(self) -> HTTPMethod:
42
+ ...
43
+
44
+ @property
45
+ @abc.abstractmethod
46
+ def headers(self) -> Mapping[str, str]:
47
+ ...
48
+
49
+ @property
50
+ @abc.abstractmethod
51
+ def content_type(self) -> Optional[str]:
52
+ ...
53
+
54
+ @property
55
+ @abc.abstractmethod
56
+ def post_data(self) -> Mapping[str, Union[str, bytes]]:
57
+ ...
58
+
59
+ @property
60
+ @abc.abstractmethod
61
+ def files(self) -> Mapping[str, Any]:
62
+ ...
63
+
64
+
65
+ class SyncBaseHTTPView(
66
+ abc.ABC,
67
+ BaseView[Request],
68
+ Generic[Request, Response, SubResponse, Context, RootValue],
69
+ ):
70
+ schema: BaseSchema
71
+ graphiql: bool
72
+ request_adapter_class: Callable[[Request], SyncHTTPRequestAdapter]
73
+
74
+ # Methods that need to be implemented by individual frameworks
75
+
76
+ @property
77
+ @abc.abstractmethod
78
+ def allow_queries_via_get(self) -> bool:
79
+ ...
80
+
81
+ @abc.abstractmethod
82
+ def get_sub_response(self, request: Request) -> SubResponse:
83
+ ...
84
+
85
+ @abc.abstractmethod
86
+ def get_context(self, request: Request, response: SubResponse) -> Context:
87
+ ...
88
+
89
+ @abc.abstractmethod
90
+ def get_root_value(self, request: Request) -> Optional[RootValue]:
91
+ ...
92
+
93
+ @abc.abstractmethod
94
+ def create_response(
95
+ self, response_data: GraphQLHTTPResponse, sub_response: SubResponse
96
+ ) -> Response:
97
+ ...
98
+
99
+ @abc.abstractmethod
100
+ def render_graphiql(self, request: Request) -> Response:
101
+ # TODO: this could be non abstract
102
+ # maybe add a get template function?
103
+ ...
104
+
105
+ def execute_operation(
106
+ self, request: Request, context: Context, root_value: Optional[RootValue]
107
+ ) -> ExecutionResult:
108
+ request_adapter = self.request_adapter_class(request)
109
+
110
+ try:
111
+ request_data = self.parse_http_body(request_adapter)
112
+ except json.decoder.JSONDecodeError as e:
113
+ raise HTTPException(400, "Unable to parse request body as JSON") from e
114
+ # DO this only when doing files
115
+ except KeyError as e:
116
+ raise HTTPException(400, "File(s) missing in form data") from e
117
+
118
+ allowed_operation_types = OperationType.from_http(request_adapter.method)
119
+
120
+ if not self.allow_queries_via_get and request_adapter.method == "GET":
121
+ allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
122
+
123
+ assert self.schema
124
+
125
+ return self.schema.execute_sync(
126
+ request_data.query,
127
+ root_value=root_value,
128
+ variable_values=request_data.variables,
129
+ context_value=context,
130
+ operation_name=request_data.operation_name,
131
+ allowed_operation_types=allowed_operation_types,
132
+ )
133
+
134
+ def parse_multipart(self, request: SyncHTTPRequestAdapter) -> Dict[str, str]:
135
+ operations = self.parse_json(request.post_data.get("operations", "{}"))
136
+ files_map = self.parse_json(request.post_data.get("map", "{}"))
137
+
138
+ try:
139
+ return replace_placeholders_with_files(operations, files_map, request.files)
140
+ except KeyError as e:
141
+ raise HTTPException(400, "File(s) missing in form data") from e
142
+
143
+ def parse_http_body(self, request: SyncHTTPRequestAdapter) -> GraphQLRequestData:
144
+ content_type = request.content_type or ""
145
+
146
+ if "application/json" in content_type:
147
+ data = self.parse_json(request.body)
148
+ elif content_type.startswith("multipart/form-data"):
149
+ data = self.parse_multipart(request)
150
+ elif request.method == "GET":
151
+ data = self.parse_query_params(request.query_params)
152
+ else:
153
+ raise HTTPException(400, "Unsupported content type")
154
+
155
+ return GraphQLRequestData(
156
+ query=data.get("query"),
157
+ variables=data.get("variables"), # type: ignore
158
+ operation_name=data.get("operationName"),
159
+ )
160
+
161
+ def run(
162
+ self,
163
+ request: Request,
164
+ context: Optional[Context] = UNSET,
165
+ root_value: Optional[RootValue] = UNSET,
166
+ ) -> Response:
167
+ request_adapter = self.request_adapter_class(request)
168
+
169
+ if not self.is_request_allowed(request_adapter):
170
+ raise HTTPException(405, "GraphQL only supports GET and POST requests.")
171
+
172
+ if self.should_render_graphiql(request_adapter):
173
+ if self.graphiql:
174
+ return self.render_graphiql(request)
175
+ else:
176
+ raise HTTPException(404, "Not Found")
177
+
178
+ sub_response = self.get_sub_response(request)
179
+ context = (
180
+ self.get_context(request, response=sub_response)
181
+ if context is UNSET
182
+ else context
183
+ )
184
+ root_value = self.get_root_value(request) if root_value is UNSET else root_value
185
+
186
+ assert context
187
+
188
+ try:
189
+ result = self.execute_operation(
190
+ request=request,
191
+ context=context,
192
+ root_value=root_value,
193
+ )
194
+ except InvalidOperationTypeError as e:
195
+ raise HTTPException(
196
+ 400, e.as_http_error_reason(request_adapter.method)
197
+ ) from e
198
+ except MissingQueryError as e:
199
+ raise HTTPException(400, "No GraphQL query found in the request") from e
200
+
201
+ response_data = self.process_result(request=request, result=result)
202
+
203
+ return self.create_response(
204
+ response_data=response_data, sub_response=sub_response
205
+ )
206
+
207
+ def process_result(
208
+ self, request: Request, result: ExecutionResult
209
+ ) -> GraphQLHTTPResponse:
210
+ return process_result(result)
@@ -1,6 +1,8 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict
2
3
 
3
4
 
4
5
  @dataclass
5
6
  class TemporalResponse:
6
7
  status_code: int = 200
8
+ headers: Dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,13 @@
1
+ from typing import Any, List, Mapping, Optional, Union
2
+ from typing_extensions import Literal, TypedDict
3
+
4
+ HTTPMethod = Literal[
5
+ "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE"
6
+ ]
7
+
8
+ QueryParams = Mapping[str, Optional[Union[str, List[str]]]]
9
+
10
+
11
+ class FormData(TypedDict):
12
+ files: Mapping[str, Any]
13
+ form: Mapping[str, Any]
@@ -0,0 +1,7 @@
1
+ from typing import TypeVar
2
+
3
+ Request = TypeVar("Request", contravariant=True)
4
+ Response = TypeVar("Response")
5
+ SubResponse = TypeVar("SubResponse")
6
+ Context = TypeVar("Context")
7
+ RootValue = TypeVar("RootValue")
strawberry/sanic/utils.py CHANGED
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from io import BytesIO
4
- from typing import TYPE_CHECKING, Any, Dict, List, Union
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
5
+
6
+ from sanic.request import File
5
7
 
6
8
  if TYPE_CHECKING:
7
9
  from sanic.request import Request
@@ -23,11 +25,16 @@ def convert_request_to_files_dict(request: Request) -> Dict[str, Any]:
23
25
 
24
26
  Note that the dictionary entries are lists.
25
27
  """
26
- request_files = request.files
28
+ request_files = cast(Optional[Dict[str, List[File]]], request.files)
29
+
30
+ if not request_files:
31
+ return {}
32
+
27
33
  files_dict: Dict[str, Union[BytesIO, List[BytesIO]]] = {}
28
34
 
29
35
  for field_name, file_list in request_files.items():
30
36
  assert len(file_list) == 1
37
+
31
38
  files_dict[field_name] = BytesIO(file_list[0].body)
32
39
 
33
40
  return files_dict
strawberry/sanic/views.py CHANGED
@@ -2,37 +2,82 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import warnings
5
- from typing import TYPE_CHECKING, Any, Dict, Optional, Type, Union
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Dict,
9
+ List,
10
+ Mapping,
11
+ Optional,
12
+ Type,
13
+ cast,
14
+ )
6
15
 
7
- from sanic.exceptions import NotFound, SanicException, ServerError
16
+ from sanic.request import Request
8
17
  from sanic.response import HTTPResponse, html
9
18
  from sanic.views import HTTPMethodView
10
- from strawberry.exceptions import MissingQueryError
11
- from strawberry.file_uploads.utils import replace_placeholders_with_files
12
- from strawberry.http import (
13
- parse_query_params,
14
- parse_request_data,
15
- process_result,
16
- )
19
+ from strawberry.http.async_base_view import AsyncBaseHTTPView, AsyncHTTPRequestAdapter
20
+ from strawberry.http.exceptions import HTTPException
17
21
  from strawberry.http.temporal_response import TemporalResponse
18
- from strawberry.sanic.graphiql import should_render_graphiql
22
+ from strawberry.http.types import FormData, HTTPMethod, QueryParams
23
+ from strawberry.http.typevars import (
24
+ Context,
25
+ RootValue,
26
+ )
19
27
  from strawberry.sanic.utils import convert_request_to_files_dict
20
- from strawberry.schema.exceptions import InvalidOperationTypeError
21
- from strawberry.types.graphql import OperationType
22
28
  from strawberry.utils.graphiql import get_graphiql_html
23
29
 
24
30
  if TYPE_CHECKING:
25
- from typing_extensions import Literal
26
-
27
- from sanic.request import Request
28
- from strawberry.http import GraphQLHTTPResponse, GraphQLRequestData
31
+ from strawberry.http import GraphQLHTTPResponse
29
32
  from strawberry.schema import BaseSchema
30
- from strawberry.types import ExecutionResult
31
33
 
32
- from .context import StrawberrySanicContext
33
34
 
35
+ class SanicHTTPRequestAdapter(AsyncHTTPRequestAdapter):
36
+ def __init__(self, request: Request):
37
+ self.request = request
38
+
39
+ @property
40
+ def query_params(self) -> QueryParams:
41
+ # Just a heads up, Sanic's request.args uses urllib.parse.parse_qs
42
+ # to parse query string parameters. This returns a dictionary where
43
+ # the keys are the unique variable names and the values are lists
44
+ # of values for each variable name. To ensure consistency, we're
45
+ # enforcing the use of the first value in each list.
46
+
47
+ args = cast(
48
+ Dict[str, Optional[List[str]]],
49
+ self.request.get_args(keep_blank_values=True),
50
+ )
51
+
52
+ return {k: args.get(k, None) for k in args}
53
+
54
+ @property
55
+ def method(self) -> HTTPMethod:
56
+ return cast(HTTPMethod, self.request.method.upper())
57
+
58
+ @property
59
+ def headers(self) -> Mapping[str, str]:
60
+ return self.request.headers
61
+
62
+ @property
63
+ def content_type(self) -> Optional[str]:
64
+ return self.request.content_type
34
65
 
35
- class GraphQLView(HTTPMethodView):
66
+ async def get_body(self) -> str:
67
+ return self.request.body.decode()
68
+
69
+ async def get_form_data(self) -> FormData:
70
+ assert self.request.form is not None
71
+
72
+ files = convert_request_to_files_dict(self.request)
73
+
74
+ return FormData(form=self.request.form, files=files)
75
+
76
+
77
+ class GraphQLView(
78
+ AsyncBaseHTTPView[Request, HTTPResponse, TemporalResponse, Context, RootValue],
79
+ HTTPMethodView,
80
+ ):
36
81
  """
37
82
  Class based view to handle GraphQL HTTP Requests
38
83
 
@@ -51,6 +96,9 @@ class GraphQLView(HTTPMethodView):
51
96
  )
52
97
  """
53
98
 
99
+ allow_queries_via_get = True
100
+ request_adapter_class = SanicHTTPRequestAdapter
101
+
54
102
  def __init__(
55
103
  self,
56
104
  schema: BaseSchema,
@@ -81,57 +129,26 @@ class GraphQLView(HTTPMethodView):
81
129
 
82
130
  self.json_encoder = json.JSONEncoder
83
131
 
84
- def get_root_value(self) -> Any:
132
+ async def get_root_value(self, request: Request) -> Optional[RootValue]:
85
133
  return None
86
134
 
87
135
  async def get_context(
88
136
  self, request: Request, response: TemporalResponse
89
- ) -> StrawberrySanicContext:
90
- return {"request": request, "response": response}
91
-
92
- def render_template(self, template: str) -> HTTPResponse:
93
- return html(template)
137
+ ) -> Context:
138
+ return {"request": request, "response": response} # type: ignore
94
139
 
95
- async def process_result(
96
- self, request: Request, result: ExecutionResult
97
- ) -> GraphQLHTTPResponse:
98
- return process_result(result)
140
+ def render_graphiql(self, request: Request) -> HTTPResponse:
141
+ template = get_graphiql_html()
99
142
 
100
- async def get(self, request: Request) -> HTTPResponse:
101
- if request.args:
102
- # Sanic request.args uses urllib.parse.parse_qs
103
- # returns a dictionary where the keys are the unique variable names
104
- # and the values are a list of values for each variable name
105
- # Enforcing using the first value
106
- query_data = {
107
- variable_name: value[0] for variable_name, value in request.args.items()
108
- }
109
- try:
110
- data = parse_query_params(query_data)
111
- except json.JSONDecodeError:
112
- raise ServerError(
113
- "Unable to parse request body as JSON", status_code=400
114
- )
115
-
116
- request_data = parse_request_data(data)
117
-
118
- return await self.execute_request(
119
- request=request, request_data=request_data, method="GET"
120
- )
121
-
122
- elif should_render_graphiql(self.graphiql, request):
123
- template = get_graphiql_html(False)
124
- return self.render_template(template=template)
143
+ return html(template)
125
144
 
126
- raise NotFound()
145
+ async def get_sub_response(self, request: Request) -> TemporalResponse:
146
+ return TemporalResponse()
127
147
 
128
- async def get_response(
129
- self, response_data: GraphQLHTTPResponse, context: StrawberrySanicContext
148
+ def create_response(
149
+ self, response_data: GraphQLHTTPResponse, sub_response: TemporalResponse
130
150
  ) -> HTTPResponse:
131
- status_code = 200
132
-
133
- if "response" in context and context["response"]:
134
- status_code = context["response"].status_code
151
+ status_code = sub_response.status_code
135
152
 
136
153
  data = self.encode_json(response_data)
137
154
 
@@ -139,84 +156,17 @@ class GraphQLView(HTTPMethodView):
139
156
  data,
140
157
  status=status_code,
141
158
  content_type="application/json",
159
+ headers=sub_response.headers,
142
160
  )
143
161
 
144
- def encode_json(self, response_data: GraphQLHTTPResponse) -> str:
145
- if self.json_dumps_params:
146
- assert self.json_encoder
147
-
148
- return json.dumps(
149
- response_data, cls=self.json_encoder, **self.json_dumps_params
150
- )
151
-
152
- if self.json_encoder:
153
- return json.dumps(response_data, cls=self.json_encoder)
154
-
155
- return json.dumps(response_data)
156
-
157
162
  async def post(self, request: Request) -> HTTPResponse:
158
- request_data = self.get_request_data(request)
159
-
160
- return await self.execute_request(
161
- request=request, request_data=request_data, method="POST"
162
- )
163
-
164
- async def execute_request(
165
- self,
166
- request: Request,
167
- request_data: GraphQLRequestData,
168
- method: Union[Literal["GET"], Literal["POST"]],
169
- ) -> HTTPResponse:
170
- context = await self.get_context(request, TemporalResponse())
171
- root_value = self.get_root_value()
172
-
173
- allowed_operation_types = OperationType.from_http(method)
174
-
175
- if not self.allow_queries_via_get and method == "GET":
176
- allowed_operation_types = allowed_operation_types - {OperationType.QUERY}
177
-
178
163
  try:
179
- result = await self.schema.execute(
180
- query=request_data.query,
181
- variable_values=request_data.variables,
182
- context_value=context,
183
- root_value=root_value,
184
- operation_name=request_data.operation_name,
185
- allowed_operation_types=allowed_operation_types,
186
- )
187
- except InvalidOperationTypeError as e:
188
- raise ServerError(
189
- e.as_http_error_reason(method=method), status_code=400
190
- ) from e
191
- except MissingQueryError:
192
- raise ServerError("No GraphQL query found in the request", status_code=400)
193
-
194
- response_data = await self.process_result(request, result)
164
+ return await self.run(request)
165
+ except HTTPException as e:
166
+ return HTTPResponse(e.reason, status=e.status_code)
195
167
 
196
- return await self.get_response(response_data, context)
197
-
198
- def get_request_data(self, request: Request) -> GraphQLRequestData:
168
+ async def get(self, request: Request) -> HTTPResponse:
199
169
  try:
200
- data = self.parse_request(request)
201
- except json.JSONDecodeError:
202
- raise ServerError("Unable to parse request body as JSON", status_code=400)
203
-
204
- return parse_request_data(data)
205
-
206
- def parse_request(self, request: Request) -> Dict[str, Any]:
207
- content_type = request.content_type or ""
208
-
209
- if "application/json" in content_type:
210
- return json.loads(request.body)
211
- elif content_type.startswith("multipart/form-data"):
212
- files = convert_request_to_files_dict(request)
213
- operations = json.loads(request.form.get("operations", "{}"))
214
- files_map = json.loads(request.form.get("map", "{}"))
215
- try:
216
- return replace_placeholders_with_files(operations, files_map, files)
217
- except KeyError:
218
- raise SanicException(
219
- status_code=400, message="File(s) missing in form data"
220
- )
221
-
222
- raise ServerError("Unsupported Media Type", status_code=415)
170
+ return await self.run(request)
171
+ except HTTPException as e:
172
+ return HTTPResponse(e.reason, status=e.status_code)
@@ -5,7 +5,7 @@ from typing_extensions import Protocol
5
5
 
6
6
  from strawberry.custom_scalar import ScalarDefinition
7
7
  from strawberry.directive import StrawberryDirective
8
- from strawberry.enum import EnumDefinition
8
+ from strawberry.enum import EnumDefinition, EnumValue
9
9
  from strawberry.lazy_type import LazyType
10
10
  from strawberry.schema_directive import StrawberrySchemaDirective
11
11
  from strawberry.type import StrawberryList, StrawberryOptional
@@ -76,6 +76,9 @@ class NameConverter:
76
76
  def from_enum(self, enum: EnumDefinition) -> str:
77
77
  return enum.name
78
78
 
79
+ def from_enum_value(self, enum: EnumDefinition, enum_value: EnumValue) -> str:
80
+ return enum_value.name
81
+
79
82
  def from_directive(
80
83
  self, directive: Union[StrawberryDirective, StrawberrySchemaDirective]
81
84
  ) -> str:
@@ -150,7 +150,12 @@ class GraphQLCoreConverter:
150
150
  graphql_enum = CustomGraphQLEnumType(
151
151
  enum=enum,
152
152
  name=enum_name,
153
- values={item.name: self.from_enum_value(item) for item in enum.values},
153
+ values={
154
+ self.config.name_converter.from_enum_value(
155
+ enum, item
156
+ ): self.from_enum_value(item)
157
+ for item in enum.values
158
+ },
154
159
  description=enum.description,
155
160
  extensions={
156
161
  GraphQLCoreConverter.DEFINITION_BACKREF: enum,