-
Notifications
You must be signed in to change notification settings - Fork 21
add graphql websocket subsciption tests #768
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 1.8.x
Are you sure you want to change the base?
Changes from all commits
15fe79f
1970210
2d5ea15
cea5e32
fe433bb
612ce4b
9f963f8
bfebe25
a212c89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,6 @@ | |
|
|
||
| from asyncio import iscoroutinefunction | ||
| import json | ||
| import re | ||
| import sys | ||
| import traceback | ||
| from typing import ( | ||
|
|
@@ -41,7 +40,6 @@ | |
| from tornado import web | ||
| from tornado.escape import json_encode | ||
| from tornado.escape import to_unicode | ||
| from tornado.httpclient import HTTPClientError | ||
| from tornado.log import app_log | ||
| from tornado.web import HTTPError | ||
|
|
||
|
|
@@ -60,11 +58,7 @@ | |
| from graphql.pyutils import is_awaitable | ||
| from graphql.validation import validate | ||
|
|
||
| from cylc.flow.network.graphql import ( | ||
| NULL_VALUE, | ||
| instantiate_middleware, | ||
| strip_null | ||
| ) | ||
| from cylc.flow.network.graphql import instantiate_middleware | ||
|
|
||
| if TYPE_CHECKING: | ||
| from graphene import Schema | ||
|
|
@@ -74,43 +68,10 @@ | |
| MAX_VALIDATION_ERRORS = None | ||
|
|
||
|
|
||
| def data_search_action(data, action): | ||
| if isinstance(data, dict): | ||
| return { | ||
| key: data_search_action(val, action) | ||
| for key, val in data.items() | ||
| } | ||
| if isinstance(data, list): | ||
| return [ | ||
| data_search_action(val, action) | ||
| for val in data | ||
| ] | ||
| return action(data) | ||
|
|
||
|
|
||
| def get_content_type(request: 'HTTPServerRequest') -> str: | ||
| return request.headers.get("Content-Type", "").split(";", 1)[0].lower() | ||
|
|
||
|
|
||
| def get_accepted_content_types(request: 'HTTPServerRequest') -> list: | ||
| def qualify(x): | ||
| parts = x.split(";", 1) | ||
| if len(parts) == 2: | ||
| match = re.match( | ||
| r"(^|;)q=(0(\.\d{,3})?|1(\.0{,3})?)(;|$)", parts[1]) | ||
| if match: | ||
| return parts[0].strip(), float(match.group(2)) | ||
| return parts[0].strip(), 1 | ||
|
|
||
| raw_content_types = request.headers.get("Accept", "*/*").split(",") | ||
| qualified_content_types = map(qualify, raw_content_types) | ||
| return [ | ||
| x[0] | ||
| for x in sorted( | ||
| qualified_content_types, key=lambda x: x[1], reverse=True) | ||
| ] | ||
|
|
||
|
|
||
| class ExecutionError(Exception): | ||
| def __init__(self, status_code, errors): | ||
| super().__init__(status_code, errors) | ||
|
|
@@ -184,42 +145,35 @@ async def post(self) -> None: | |
| self.handle_error(ex) | ||
|
|
||
| async def run(self, *args, **kwargs): | ||
| try: | ||
| data = self.parse_body() | ||
|
|
||
| if self.batch: | ||
| responses = [ | ||
| await self.get_response(entry) | ||
| for entry in data | ||
| ] | ||
| result = "[{}]".format( | ||
| ",".join([response[0] for response in responses]) | ||
| ) | ||
| status_code = ( | ||
| responses | ||
| and max(responses, key=lambda response: response[1])[1] | ||
| or 200 | ||
| ) | ||
| else: | ||
| result, status_code = await self.get_response(data) | ||
| data = self.parse_body() | ||
|
|
||
| if self.batch: | ||
| responses = [ | ||
| await self.get_response(entry) | ||
| for entry in data | ||
| ] | ||
| result = "[{}]".format( | ||
| ",".join([response[0] for response in responses]) | ||
| ) | ||
| status_code = ( | ||
| responses | ||
| and max(responses, key=lambda response: response[1])[1] | ||
| or 200 | ||
| ) | ||
| else: | ||
| result, status_code = await self.get_response(data) | ||
|
|
||
| if status_code == 400: | ||
| self.set_status(status_code, reason=result) | ||
| else: | ||
| self.set_status(status_code) | ||
| self.set_header("Content-Type", "application/json") | ||
| self.write(result) | ||
| await self.finish() | ||
|
|
||
| except HTTPClientError as e: | ||
| response = e.response | ||
| response["Content-Type"] = "application/json" | ||
| response.content = self.json_encode( | ||
| self.request, {"errors": [self.format_error(e)]} | ||
| ) | ||
| return response | ||
|
|
||
| self.set_header("Content-Type", "application/json") | ||
| self.write(result) | ||
| await self.finish() | ||
|
|
||
| async def get_response(self, data): | ||
| query, variables, operation_name, _id = self.get_graphql_params( | ||
| self.request, data | ||
| ) | ||
| query, variables, operation_name, _id = self.get_graphql_params(data) | ||
|
|
||
| execution_result = await self.execute_graphql_request( | ||
| data, query, variables, operation_name | ||
|
|
@@ -252,38 +206,19 @@ async def get_response(self, data): | |
| if self.batch: | ||
| response["id"] = _id | ||
| response["status"] = status_code | ||
| try: | ||
| result = self.json_encode(response) | ||
| except TypeError: | ||
| # Catch exceptions in response | ||
| errors = [] | ||
|
|
||
| def exc_to_errors(data): | ||
| if isinstance(data, Exception): | ||
| errors.append({ | ||
| 'message': ( | ||
| f'{data.value}' | ||
| if hasattr(data, 'value') else f'{data}' | ||
| ) | ||
| }) | ||
| return NULL_VALUE | ||
| return data | ||
|
|
||
| response = data_search_action( | ||
| response, | ||
| exc_to_errors | ||
| ) | ||
| response.setdefault("errors", []).extend(errors) | ||
| response = strip_null(response) | ||
|
|
||
| result = self.json_encode(response) | ||
| result = self.json_encode(response) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to wrap response should always be JSON serializable.. |
||
| else: | ||
| result = None | ||
|
|
||
| return result, status_code | ||
|
|
||
| def json_encode(self, d, pretty=False): | ||
| if (self.pretty or pretty) or self.get_query_argument("pretty", False): | ||
| def json_encode(self, d): | ||
| if ( | ||
| self.pretty | ||
| or self.get_query_argument("pretty", False) | ||
| or self.get_body_argument("pretty", False) | ||
| ): | ||
| return json.dumps( | ||
| d, sort_keys=True, indent=2, separators=(",", ": ")) | ||
|
|
||
|
|
@@ -300,17 +235,17 @@ def parse_body(self): | |
| try: | ||
| body = self.request.body | ||
| except Exception as e: | ||
| raise ExecutionError(400, e) | ||
| raise ExecutionError(400, [e]) | ||
|
|
||
| try: | ||
| request_json = json.loads(body) | ||
| if self.batch: | ||
| if not isinstance(request_json, list): | ||
| raise AssertionError( | ||
| raise AssertionError(( | ||
| "Batch requests should receive a list" | ||
| ", but received {}." | ||
| ).format(repr(request_json)) | ||
| if len(request_json <= 0): | ||
| ).format(repr(request_json))) | ||
| if len(request_json) <= 0: | ||
| raise AssertionError( | ||
| "Received an empty list in the batch request." | ||
| ) | ||
|
|
@@ -402,37 +337,19 @@ async def execute_graphql_request( | |
|
|
||
| return result | ||
| except Exception as e: | ||
| return ExecutionResult(errors=[e]) | ||
| return ExecutionResult(data=None, errors=[e]) | ||
|
|
||
| async def execute(self, *args, **kwargs): | ||
| return execute(*args, **kwargs) | ||
|
|
||
| def request_wants_html(self): | ||
| accepted = get_accepted_content_types(self.request) | ||
| accepted_length = len(accepted) | ||
| # the list will be ordered in preferred first - so we have to make | ||
| # sure the most preferred gets the highest number | ||
| html_priority = ( | ||
| accepted_length - accepted.index("text/html") | ||
| if "text/html" in accepted | ||
| else 0 | ||
| ) | ||
| json_priority = ( | ||
| accepted_length - accepted.index("application/json") | ||
| if "application/json" in accepted | ||
| else 0 | ||
| ) | ||
|
|
||
| return html_priority > json_priority | ||
|
|
||
|
Comment on lines
-410
to
-427
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note to reviewers] This is presumably something we inherited via graphene-tornado that's no longer required - https://github.com/graphql-python/graphene-tornado/blob/e4fa7d7b4c2256fa37f5ad89cbfd4d4bf6fdb606/graphene_tornado/tornado_graphql_handler.py#L379 Can't find any trace of it in Tornado.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I used Django as the starting point, and graphene-tornado as the reference (because graphene-django is more up to date).. |
||
| def get_graphql_params(self, request, data): | ||
| def get_graphql_params(self, data): | ||
| if self.graphql_params: | ||
| return self.graphql_params | ||
|
|
||
| single_args = {} | ||
| for key in request.arguments.keys(): | ||
| for key in self.request.arguments.keys(): | ||
| single_args[key] = self.decode_argument( | ||
| request.arguments.get(key)[0]) | ||
| self.request.arguments.get(key)[0]) | ||
|
|
||
| query = single_args.get("query") or data.get("query") | ||
| variables = single_args.get("variables") or data.get("variables") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note to reviewers]
Unused function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above