async def _observe_subscription(self, asyncgen: AsyncGenerator, operation_id: str, websocket: WebSocket) -> None: try: async for result in asyncgen: payload = {} if result.data: payload["data"] = result.data if result.errors: payload["errors"] = [ format_error(error) for error in result.errors ] await websocket.send_json({ "type": GQL_DATA, "id": operation_id, "payload": payload }) except Exception as error: if not isinstance(error, GraphQLError): error = GraphQLError(str(error), original_error=error) await websocket.send_json({ "type": GQL_DATA, "id": operation_id, "payload": { "errors": [format_error(error)] }, }) if (websocket.client_state != WebSocketState.DISCONNECTED and websocket.application_state != WebSocketState.DISCONNECTED): await websocket.send_json({ "type": GQL_COMPLETE, "id": operation_id })
def quiz_model_query(client, model_query_function, result_name, variables, expect_length=1): """ Tests a query for a model with variables that produce exactly one result :param client: Apollo client :param model_query_function: Query function expecting the client and variables :param result_name: The name of the result object in the data object :param variables: key value variables for the query :param expect_length: Default 1. Optional number items to expect :return: returns the result for further assertions """ all_result = model_query_function(client) assert not R.has('errors', all_result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', all_result))) result = model_query_function(client, variables=variables) # Check against errors assert not R.has('errors', result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', result))) # Simple assertion that the query looks good assert expect_length == R.length(R.item_path(['data', result_name], result)) return result
async def db_handler(request): """Serve GraphQL queries.""" payload = await request.json() query = payload.get('query') variables = payload.get('variables') context = _build_context(request) response = await tangoschema.execute(query, variable_values=variables, context_value=context, return_promise=True) data = {} if response.errors: if isinstance(response.errors[0].original_error, UserUnauthorizedException): return web.HTTPUnauthorized() else: data['errors'] = [format_error(e) for e in response.errors] if response.data: data['data'] = response.data jsondata = json.dumps(data,) return web.Response(text=jsondata, headers={'Content-Type': "application/json"})
async def graphql_http_server(self, request: Request) -> Response: try: query, variables, operation_name = await self.extract_data_from_request( request) document = parse(query) result = await graphql( self.schema, query, root_value=await self.root_value_for_document(document, variables), context_value=await self.context_for_request(request), variable_values=variables, operation_name=operation_name, ) except GraphQLError as error: response = {"errors": [{"message": error.message}]} return JSONResponse(response) except HttpError as error: response = error.message or error.status return Response(response, status_code=400) else: response = {"data": result.data} if result.errors: response["errors"] = [format_error(e) for e in result.errors] return JSONResponse(response)
def execution_result_to_dict(self, execution_result): result = OrderedDict() if execution_result.data: result['data'] = execution_result.data if execution_result.errors: result['errors'] = [format_error(error) for error in execution_result.errors] return result
async def send_execution_result(self, context: ConnectionContext, op_id: str, result: ExecutionResult) -> None: payload = { 'data': result.data, 'errors': [format_error(error) for error in result.errors] if result.errors else None, } await self.send_message( context, MessageType.GQL_DATA, op_id=op_id, payload=payload, )
async def send_result(self, operation_id: str, result: ExecutionResult) -> None: payload = {} if result.data: payload["data"] = result.data if result.errors: payload["errors"] = [format_error(e) for e in result.errors] await self.send_message(operation_id, "data", payload)
def return_response_from_result(self, start_response: Callable, result: ExecutionResult) -> List[bytes]: response = {"data": result.data} if result.errors: response["errors"] = [format_error(e) for e in result.errors] start_response(HTTP_STATUS_200_OK, [("Content-Type", CONTENT_TYPE_JSON)]) return [json.dumps(response).encode("utf-8")]
def execution_result_to_dict(self, execution_result): result = {} if execution_result.data: result["data"] = execution_result.data if execution_result.errors: result["errors"] = [ format_error(error) for error in execution_result.errors ] return result
def quiz_model_mutation_create(client, graphql_update_or_create_function, result_path, values, second_create_results=None, second_create_does_update=False): """ Tests a create mutation for a model :param client: The Apollo Client :param graphql_update_or_create_function: The update or create mutation function for the model. Expects client and input values :param result_path: The path to the result of the create in the data object (e.g. createRegion.region) :param values: The input values to use for the create :param second_create_results: Object, tests a second create if specified. Use to make sure that create with the same values creates a new instance or updates, depending on what you expect it to do. The values of this should be regexes that match the created instance :param second_create_does_update: Default False. If True expects a second create with the same value to update rather than create a new instance :return: Tuple with two return values. The second is null if second_create_results is False """ result = graphql_update_or_create_function(client, values=values) result_path_partial = R.item_str_path(f'data.{result_path}') assert not R.has('errors', result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', result))) # Get the created value, using underscore to make the camelcase keys match python keys created = R.map_keys(lambda key: underscore(key), result_path_partial(result)) # get all the keys in values that are in created. This should match values if created has everything we expect assert values == pick_deep(created, values) # Try creating with the same values again, unique constraints will apply to force a create or an update will occur if second_create_results: new_result = graphql_update_or_create_function(client, values) assert not R.has('errors', new_result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', new_result))) created_too = result_path_partial(new_result) if second_create_does_update: assert created['id'] == created_too['id'] if not second_create_does_update: assert created['id'] != created_too['id'] for path, value in R.flatten_dct(second_create_results, '.').items(): assert re.match(value, R.item_str_path_or(None, path, created_too)) else: new_result = None return result, new_result
async def return_response_from_result(self, result: ExecutionResult) -> None: response = {"data": result.data} if result.errors: response["errors"] = [format_error(e) for e in result.errors] await self.send_response( 200, json.dumps(response).encode("utf-8"), headers=[(b"Content-Type", CONTENT_TYPE_JSON.encode("utf-8"))], )
async def graphql_view(request): payload = await request.json() response = await schema.execute(payload.get("query", ""), return_promise=True) data = {} if response.errors: data["errors"] = [format_error(e) for e in response.errors] if response.data: data["data"] = response.data jsondata = json.dumps(data,) return web.Response(text=jsondata, headers={"Content-Type": "application/json"})
async def _handle_request(self, request: Request) -> Response: # route GET requests to the IDE if request.method == "GET" and "text/html" in request.headers.get("Accept", ""): # read the GraphiQL playground html and serve it as content graphiql = pathlib.Path(__file__).parent / "graphiql.html" raw_html = None with open(graphiql.absolute(), "r") as f: raw_html = f.read() return HTMLResponse(raw_html) # route POST requests to the graphql executor elif request.method == "POST": content_type = request.headers.get("Content-Type", "") # parse graphql according to content type if "application/json" in content_type: data = await request.json() else: return PlainTextResponse( "Unsupported Media Type", status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, ) else: return PlainTextResponse( "Method Not Allowed", status_code=status.HTTP_405_METHOD_NOT_ALLOWED ) # attempt to pull final query and vars try: query = data["query"] variables = data.get("variables") except KeyError: return PlainTextResponse( "No GraphQL query found in the request", status_code=status.HTTP_400_BAD_REQUEST, ) # construct foundation fastapi context background = BackgroundTasks() context = {"request": request, "background": background} result = await self._execute_graphql(query, variables, context) # parse graphql result response = {} if not result.invalid: response["data"] = result.data if result.errors: response["errors"] = [format_error(e) for e in result.errors] status_code = ( status.HTTP_400_BAD_REQUEST if result.errors else status.HTTP_200_OK ) return ORJSONResponse(response, status_code=status_code, background=background)
async def send_error_response(self, results, operation_id): # set default error message. payload = dict(message=dict( message="Unhandled error occoured", locations=[], path="")) # Set error message if value is available if results.errors is not None and len(results.errors) > 0: payload = dict(message=graphql.format_error(results.errors[0])) json_response: str = json.dumps( dict(type=GQLEnum.ERROR.value, id=operation_id, payload=payload)) await self.websocket.send_text(json_response)
def test_parse_error(client): with pytest.raises(Exception) as exc_info: gql(""" qeury """) error = exc_info.value assert isinstance(error, GraphQLError) formatted_error = format_error(error) assert formatted_error["locations"] == [{"column": 13, "line": 2}] assert formatted_error[ "message"] == "Syntax Error: Unexpected Name 'qeury'."
def dump_errors(result): """ Dump any errors to in the result to stderr :param result: :return: """ if R.has('errors', result): for error in result['errors']: logger.error(format_error(error)) if 'stack' in error: traceback.print_tb(error['stack'], limit=10, file=sys.stderr)
def test_parse_error(schema): query = """ qeury """ result = graphql(schema, query) assert result.invalid formatted_error = format_error(result.errors[0]) assert formatted_error["locations"] == [{"column": 9, "line": 2}] assert ('Syntax Error GraphQL (2:9) Unexpected Name "qeury"' in formatted_error["message"]) assert result.data is None
async def graphql_view(request): payload = await request.json() response = await schema.execute(payload.get('query', ''), return_promise=True) data = {} if response.errors: data['errors'] = [format_error(e) for e in response.errors] if response.data: data['data'] = response.data jsondata = json.dumps(data, ) return web.Response(text=jsondata, headers={'Content-Type': 'application/json'})
def return_response_from_result(self, start_response: Callable, result: ExecutionResult) -> List[bytes]: status = HTTP_STATUS_200_OK response = {} if result.errors: response["errors"] = [format_error(e) for e in result.errors] if result.invalid: status = HTTP_STATUS_400_BAD_REQUEST else: response["data"] = result.data start_response(status, [("Content-Type", CONTENT_TYPE_JSON)]) return [json.dumps(response).encode("utf-8")]
async def graphql_ws_server(self, websocket: WebSocket) -> None: subscriptions: Dict[str, AsyncGenerator] = {} await websocket.accept("graphql-ws") try: while True: message = await self.receive_json(websocket) operation_id = cast(str, message.get("id")) message_type = cast(str, message.get("type")) if message_type == GQL_CONNECTION_INIT: await self.send_json(websocket, {"type": GQL_CONNECTION_ACK}) elif message_type == GQL_CONNECTION_TERMINATE: break elif message_type == GQL_START: query, variables, operation_name = await self.extract_data_from_websocket( message) document = parse(query) results = await subscribe( self.schema, document, root_value=await self.root_value_for_document(document, variables), context_value=await self.context_for_request(message), variable_values=variables, operation_name=operation_name, ) if isinstance(results, ExecutionResult): payload = {"message": format_error(results.errors[0])} await self.send_json( websocket, { "type": GQL_ERROR, "id": operation_id, "payload": payload }, ) else: subscriptions[operation_id] = results asyncio.ensure_future( self.observe_async_results(results, operation_id, websocket)) elif message_type == GQL_STOP: if operation_id in subscriptions: await subscriptions[operation_id].aclose() del subscriptions[operation_id] except WebSocketDisconnect: for operation_id in subscriptions: await subscriptions[operation_id].aclose() del subscriptions[operation_id]
def quiz_model_mutation_update(client, graphql_update_or_create_function, create_path, update_path, values, update_values): """ Tests an update mutation for a model by calling a create with the given values then an update with the given update_values (plus the create id) :param client: The Apollo Client :param graphql_update_or_create_function: The update or create mutation function for the model. Expects client and input values :param create_path: The path to the result of the create in the data object (e.g. createRegion.region) :param update_path: The path to the result of the update in the data object (e.g. updateRegion.region) :param values: The input values to use for the create :param update_values: The input values to use for the update. This can be as little as one key value :return: """ result = graphql_update_or_create_function(client, values=values) assert not R.has('errors', result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', result))) # Extract the result and map the graphql keys to match the python keys created = R.compose( lambda r: R.map_keys(lambda key: underscore(key), r), lambda r: R.item_str_path(f'data.{create_path}', r))(result) # look at the users added and omit the non-determinant dateJoined assert values == pick_deep(created, values) # Update with the id and optionally key if there is one + update_values update_result = graphql_update_or_create_function( client, R.merge_all([ dict(id=created['id']), dict(key=created['key']) if R.prop_or(False, 'key', created) else {}, update_values ])) assert not R.has('errors', update_result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', update_result))) updated = R.item_str_path(f'data.{update_path}', update_result) assert created['id'] == updated['id'] assert update_values == pick_deep(update_values, updated) return result, update_result
async def _ws_on_start( self, data: Any, operation_id: str, websocket: WebSocket, subscriptions: Dict[str, AsyncGenerator], ): query = data["query"] variable_values = data.get("variables") operation_name = data.get("operationName") context_value = await self._get_context_value(websocket) errors: List[GraphQLError] = [] operation: Optional[OperationDefinitionNode] = None document: Optional[DocumentNode] = None try: document = parse(query) operation = get_operation_ast(document, operation_name) errors = validate(self.schema.graphql_schema, document) except GraphQLError as e: errors = [e] if operation and operation.operation == OperationType.SUBSCRIPTION: errors = await self._start_subscription( websocket, operation_id, subscriptions, document, context_value, variable_values, operation_name, ) else: errors = await self._handle_query_via_ws( websocket, operation_id, subscriptions, document, context_value, variable_values, operation_name, ) if errors: await websocket.send_json({ "type": GQL_ERROR, "id": operation_id, "payload": format_error(errors[0]), })
async def run(self, query: str, context: Dict[str, Any] = None) -> QueryResult: context = context or {} graphql_kwargs = context.pop('graphql', {'context_value': {}}) graphql_context = graphql_kwargs['context_value'] graphql_context.update({'dataloaders': {}}) graphql_result = await graphql(self.schema, query, **graphql_kwargs) data = graphql_result.data errors = [] for error in graphql_result.errors or []: self.logger.error(error, exc_info=error) errors.append(format_error(error)) return QueryResult(data, errors or None)
async def send_execution_result( self, connection_context: AbstractConnectionContext, op_id: str, execution_result: graphql.ExecutionResult, ) -> None: result = {} if execution_result.data: result["data"] = execution_result.data if execution_result.errors: result["errors"] = [ graphql.format_error(error) for error in execution_result.errors ] return await self.send_message( connection_context, op_id, GQLMsgType.DATA, result )
async def stream_channel(self, results, operation_id): async for result in results: resp = dict() if result and result.data: resp = dict(type=GQLEnum.DATA.value, id=operation_id, payload=dict(data=result.data)) if result and result.errors: resp = dict( errors=[graphql.format_error(e) for e in result.errors]) json_response: str = json.dumps( dict(type=GQLEnum.DATA.value, id=operation_id, payload=resp)) await self.websocket.send_text(json_response) json_response: dict = json.dumps( dict(type=GQLEnum.COMPLETE.value, id=operation_id)) await self.websocket.send_text(json_response)
async def observe_async_results(self, results: AsyncGenerator, operation_id: str, websocket: WebSocket) -> None: async for result in results: payload = {} if result.data: payload["data"] = result.data if result.errors: payload["errors"] = [format_error(e) for e in result.errors] await self.send_json(websocket, { "type": GQL_DATA, "id": operation_id, "payload": payload }) await self.send_json(websocket, { "type": GQL_COMPLETE, "id": operation_id })
def dump_errors(result): """ Dump any errors to in the result to stderr :param result: :return: """ if R.has('errors', result): for error in result['errors']: logging.exception(traceback) if hasattr(error, 'stack'): # Syncrounous calls or something # See https://github.com/graphql-python/graphql-core/issues/237 tb = error['stack'] else: # Promises tb = error.__traceback__ formatted_tb = traceback.format_tb(tb) error.stack = error.__traceback__ # This hopefully includes the traceback logger.exception(format_error(error))
async def _handle_query_via_ws( self, websocket, operation_id, subscriptions, document, context_value, variable_values, operation_name, ) -> List[GraphQLError]: result2 = execute( self.schema.graphql_schema, document, root_value=self.root_value, context_value=context_value, variable_values=variable_values, operation_name=operation_name, middleware=self.middleware, ) if isinstance(result2, ExecutionResult) and result2.errors: return result2.errors if isawaitable(result2): result2 = await cast(Awaitable[ExecutionResult], result2) result2 = cast(ExecutionResult, result2) payload: Dict[str, Any] = {} payload["data"] = result2.data if result2.errors: payload["errors"] = [ format_error(error) for error in result2.errors ] await websocket.send_json({ "type": GQL_DATA, "id": operation_id, "payload": payload }) return []
async def _handle_http_request(self, request: Request) -> JSONResponse: try: operations = await _get_operation_from_request(request) except ValueError as error: return JSONResponse({"errors": [error.args[0]]}, status_code=400) if isinstance(operations, list): return JSONResponse( {"errors": ["This server does not support batching"]}, status_code=400) else: operation = operations query = operation["query"] variable_values = operation.get("variables") operation_name = operation.get("operationName") context_value = await self._get_context_value(request) result = await graphql( self.schema.graphql_schema, source=query, context_value=context_value, root_value=self.root_value, middleware=self.middleware, variable_values=variable_values, operation_name=operation_name, ) response: Dict[str, Any] = {"data": result.data} if result.errors: response["errors"] = [ format_error(error) for error in result.errors ] status_code = 200 if not result.errors else 400 return JSONResponse(response, status_code=status_code)
def quiz_model_versioned_query(client, model_class, model_query, result_name, version_count_expected, props, omit_props): """ Tests a versioned query for a model with variables :param client: Apollo client :param model_class: Model class :param model_query: Model's query that should return one result (as a filter) number of items in the database that match props :param result_name: The name of the results in data.[result_name].objects :param version_count_expected The number of versions of the instance we expect :param props: The props to query to find a single instance. Should just be {id:...} :param omit_props: Props to omit from assertions because they are nondeterminate :return: """ result = model_query( client, variables=dict(objects=R.to_array_if_not(dict(instance=props)))) # Check against errors assert not R.has('errors', result), R.dump_json( R.map(lambda e: format_error(e), R.prop('errors', result))) assert R.compose( R.length, R.item_str_path_or( [], f'data.{result_name}.objects'))(result) == version_count_expected