示例#1
0
async def execute(
    schema: GraphQLSchema,
    query: str,
    context_value: typing.Any = None,
    variable_values: typing.Dict[str, typing.Any] = None,
    operation_name: str = None,
):
    schema_validation_errors = validate_schema(schema)
    if schema_validation_errors:
        return ExecutionResult(data=None, errors=schema_validation_errors)

    try:
        document = parse(query)
    except GraphQLError as error:
        return ExecutionResult(data=None, errors=[error])
    except Exception as error:
        error = GraphQLError(str(error), original_error=error)
        return ExecutionResult(data=None, errors=[error])

    validation_errors = validate(schema, document)

    if validation_errors:
        return ExecutionResult(data=None, errors=validation_errors)

    return graphql_excute(
        schema,
        parse(query),
        middleware=[DirectivesMiddleware()],
        variable_values=variable_values,
        operation_name=operation_name,
        context_value=context_value,
    )
示例#2
0
def test_format_execution_result():
    result = format_execution_result(None)
    assert result == GraphQLResponse(None, 200)
    data = {"answer": 42}
    result = format_execution_result(ExecutionResult(data, None))
    assert result == GraphQLResponse({"data": data}, 200)
    errors = [GraphQLError("bad")]
    result = format_execution_result(ExecutionResult(None, errors))
    assert result == GraphQLResponse({"errors": errors}, 400)
示例#3
0
def test_encode_execution_results_batch():
    data = {"answer": 42}
    errors = [GraphQLError("bad")]
    results = [ExecutionResult(data, None), ExecutionResult(None, errors)]
    result = encode_execution_results(results, is_batch=True)
    assert result == (
        '[{"data":{"answer":42}},'
        '{"errors":[{"message":"bad","locations":null,"path":null}]}]',
        400,
    )
示例#4
0
    async def execute(self,
                      query: str,
                      variable_values=None,
                      timeout=None) -> Tuple[int, ExecutionResult]:
        if not self.session:
            self.session = aiohttp.ClientSession()

        if isinstance(query, DocumentNode) and self.dsl:
            query = self.dsl.as_string(query)

        payload = {"query": query, "variables": variable_values or {}}

        if self.use_json:
            body = {"json": payload}
        else:
            body = {"data": payload}

        async with self.session.post(
                self.url,
                auth=self.auth,
                headers=self.headers,
                timeout=timeout or self.timeout,
                **body,
        ) as response:
            result = await response.json() if self.use_json else response.text(
            )
            if "errors" not in result and "data" not in result:
                raise ValueError(f'Received incompatible response "{result}"')

            return (
                response.status,
                ExecutionResult(errors=result.get("errors"),
                                data=result.get("data")),
            )
def try_fast_introspection(schema: GraphQLSchema,
                           query: str) -> Optional[ExecutionResult]:
    """Compute the GraphQL introspection query if query can be computed fastly.

    Args:
        schema: GraphQL schema object, obtained from the graphql library
        query: string containing the introspection query to be executed on the schema

    Returns:
        - GraphQL Execution Result with data = None: there were schema validation errors.
        - GraphQL ExecutionResult with data != None: fast introspection was successful and computed
          data can be found under data attribute.
        - None if the query does not match the set introspection query in this module: the query
          cannot be computed fastly with this module.
    """
    if _remove_whitespace_from_query(
            query) != _whitespace_free_introspection_query:
        return None

    # Schema validations
    schema_validation_errors = validate_schema(schema)
    if schema_validation_errors:
        return ExecutionResult(data=None, errors=schema_validation_errors)

    return _execute_fast_introspection_query(schema)
        async def send_events(zero_event: ZeroEvent) -> AsyncIterable[bytes]:
            LOGGER.debug('Streaming subscription started.')

            try:
                zero_event.increment()

                async for val in cancellable_aiter(result,
                                                   self.cancellation_event,
                                                   timeout=self.ping_interval):
                    yield encode(val)
                    yield nudge  # Give the ASGI server a nudge.

            except asyncio.CancelledError:
                LOGGER.debug("Streaming subscription cancelled.")
            except Exception as error:  # pylint: disable=broad-except
                LOGGER.exception("Streaming subscription failed.")
                # If the error is not caught the client fetch will fail, however
                # the status code and headers have already been sent. So rather
                # than let the fetch fail we send a GraphQL response with no
                # data and the error and close gracefully.
                if not isinstance(error, GraphQLError):
                    error = GraphQLError('Execution error',
                                         original_error=error)
                val = ExecutionResult(None, [error])
                yield encode(val)
                yield nudge  # Give the ASGI server a nudge.
            finally:
                zero_event.decrement()

            LOGGER.debug("Streaming subscription stopped.")
示例#7
0
    async def subscribe(self, query, *args, **kwargs):
        """Execute a GraphQL subscription on the schema asynchronously."""
        # Do parsing
        try:
            document = parse(query)
        except GraphQLError as error:
            return ExecutionResult(data=None, errors=[error])

        # Do validation
        validation_errors = validate(self.graphql_schema, document)
        if validation_errors:
            return ExecutionResult(data=None, errors=validation_errors)

        # Execute the query
        kwargs = normalize_execute_kwargs(kwargs)
        return await subscribe(self.graphql_schema, document, *args, **kwargs)
示例#8
0
文件: aiohttp.py 项目: tony/gql
    async def execute(
        self,
        document: DocumentNode,
        variable_values: Optional[Dict[str, str]] = None,
        operation_name: Optional[str] = None,
        extra_args: Dict[str, Any] = {},
    ) -> ExecutionResult:
        """Execute the provided document AST against the configured remote server.
        This uses the aiohttp library to perform a HTTP POST request asynchronously
        to the remote server.

        The result is sent as an ExecutionResult object.
        """

        query_str = print_ast(document)
        payload: Dict[str, Any] = {
            "query": query_str,
        }

        if variable_values:
            payload["variables"] = variable_values
        if operation_name:
            payload["operationName"] = operation_name

        post_args = {
            "json": payload,
        }

        # Pass post_args to aiohttp post method
        post_args.update(extra_args)

        if self.session is None:
            raise TransportClosed("Transport is not connected")

        async with self.session.post(self.url, ssl=self.ssl,
                                     **post_args) as resp:
            try:
                result = await resp.json()
            except Exception:
                # We raise a TransportServerError if the status code is 400 or higher
                # We raise a TransportProtocolError in the other cases

                try:
                    # Raise a ClientResponseError if response status is 400 or higher
                    resp.raise_for_status()

                except ClientResponseError as e:
                    raise TransportServerError from e

                raise TransportProtocolError(
                    "Server did not return a GraphQL result")

            if "errors" not in result and "data" not in result:
                raise TransportProtocolError(
                    "Server did not return a GraphQL result")

            return ExecutionResult(errors=result.get("errors"),
                                   data=result.get("data"))
示例#9
0
def execute_graphql_request(
    schema: GraphQLSchema,
    params: GraphQLParams,
    allow_only_query: bool = False,
    **kwargs,
):
    if not params.query:
        raise HttpQueryError(400, "Must provide query string.")

    try:
        document = parse(params.query)
    except GraphQLError as e:
        return ExecutionResult(data=None, errors=[e])
    except Exception as e:
        e = GraphQLError(str(e), original_error=e)
        return ExecutionResult(data=None, errors=[e])

    if allow_only_query:
        operation_ast = get_operation_ast(document, params.operation_name)
        if operation_ast:
            operation = operation_ast.operation.value
            if operation != "query":
                raise HttpQueryError(
                    405,
                    f"Can only perform a {operation} operation from a POST request.",
                    headers={"Allow": "POST"},
                )

    # Note: the schema is not validated here for performance reasons.
    # This should be done only once when starting the server.

    validation_errors = validate(schema, document)
    if validation_errors:
        return ExecutionResult(data=None, errors=validation_errors)

    return execute(
        schema,
        document,
        variable_values=params.variables,
        operation_name=params.operation_name,
        **kwargs,
    )
示例#10
0
async def execute(
    schema: GraphQLSchema,
    query: str,
    root_value: typing.Any = None,
    context_value: typing.Any = None,
    variable_values: typing.Dict[str, typing.Any] = None,
    middleware: typing.List[Middleware] = None,
    operation_name: str = None,
):  # pragma: no cover
    schema_validation_errors = validate_schema(schema)
    if schema_validation_errors:
        return ExecutionResult(data=None, errors=schema_validation_errors)

    try:
        document = parse(query)
    except GraphQLError as error:
        return ExecutionResult(data=None, errors=[error])
    except Exception as error:
        error = GraphQLError(str(error), original_error=error)
        return ExecutionResult(data=None, errors=[error])

    validation_errors = validate(schema, document)

    if validation_errors:
        return ExecutionResult(data=None, errors=validation_errors)

    result = graphql_execute(
        schema,
        parse(query),
        root_value=root_value,
        middleware=middleware or [],
        variable_values=variable_values,
        operation_name=operation_name,
        context_value=context_value,
    )
    if isawaitable(result):
        result = await typing.cast(typing.Awaitable[ExecutionResult], result)
    return result
示例#11
0
    async def _process_subscription(self, id_: Id,
                                    result: AsyncIterator) -> AsyncIterator:
        try:
            async for val in result:
                await self._send_execution_result(id_, val)
            await self.web_socket.send(self._to_message(GQL_COMPLETE, id_))
        except asyncio.CancelledError:
            pass
        except Exception as error:  # pylint: disable=broad-except
            if not isinstance(error, GraphQLError):
                error = GraphQLError('Execution error', original_error=error)
            await self._send_execution_result(id_,
                                              ExecutionResult(errors=[error]))
            await self.web_socket.send(self._to_message(GQL_COMPLETE, id_))

        return result
示例#12
0
    async def start(self, context: ConnectionContext, message: OperationMessage) -> None:
        if context.user and not context.user.is_authenticated:
            await self.send_error(context, message.id, {'message': 'Invalid auth credentials.'})
            return

        op_id = message.id
        # if we already have a subscription with this id, unsubscribe from it first
        if op_id in context.operations:
            await self.unsubscribe(context, op_id)

        payload = message.payload
        try:
            doc = parse(payload.query)
        except Exception as exc:
            if isinstance(exc, GraphQLError):
                await self.send_execution_result(context, op_id, ExecutionResult(data=None, errors=[exc]))
            else:
                await self.send_error(context, op_id, {'message': str(exc)})
            return

        result_or_iterator = await subscribe(
            self.schema,
            doc,
            variable_values=payload.variables,
            context_value=context,
            operation_name=payload.operation_name,
        )
        if isinstance(result_or_iterator, ExecutionResult):
            result_or_iterator = create_async_iterator([result_or_iterator])

        context.operations[op_id] = result_or_iterator

        async def iter_result():
            async for result in result_or_iterator:
                await self.send_execution_result(context, op_id, result)

            await self.send_message(context, MessageType.GQL_COMPLETE, op_id=op_id)

        asyncio.create_task(iter_result())
def _execute_fast_introspection_query(
        schema: GraphQLSchema) -> ExecutionResult:
    """Compute the GraphQL introspection query."""
    response_types = []
    for type_ in __Schema.fields["types"].resolve(schema, None):
        response_types.append(_get_full_type(type_, schema))

    response_directives = []
    for directive in __Schema.fields["directives"].resolve(schema, None):
        response_directives.append(_get_directive(directive))

    query_type = __Schema.fields["queryType"].resolve(schema, None)
    mutation_type = __Schema.fields["mutationType"].resolve(schema, None)
    subscription_type = __Schema.fields["subscriptionType"].resolve(
        schema, None)

    response: Dict[str, Any] = {
        "queryType": {
            "name": __Type.fields["name"].resolve(query_type, None)
        },
        "mutationType":
        ({
            "name": __Type.fields["name"].resolve(mutation_type, None)
        } if mutation_type else None),
        "subscriptionType":
        ({
            "name": __Type.fields["name"].resolve(subscription_type, None)
        } if subscription_type else None),
        "types":
        response_types,
        "directives":
        response_directives,
    }
    response_payload = {"__schema": response}

    return ExecutionResult(data=response_payload, errors=None)
示例#14
0
    def _parse_answer_graphqlws(
        self, json_answer: Dict[str, Any]
    ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]:
        """Parse the answer received from the server if the server supports the
        graphql-ws protocol.

        Returns a list consisting of:
            - the answer_type (between:
              'connection_ack', 'ping', 'pong', 'data', 'error', 'complete')
            - the answer id (Integer) if received or None
            - an execution Result if the answer_type is 'data' or None

        Differences with the apollo websockets protocol (superclass):
            - the "data" message is now called "next"
            - the "stop" message is now called "complete"
            - there is no connection_terminate or connection_error messages
            - instead of a unidirectional keep-alive (ka) message from server to client,
              there is now the possibility to send bidirectional ping/pong messages
            - connection_ack has an optional payload
            - the 'error' answer type returns a list of errors instead of a single error
        """

        answer_type: str = ""
        answer_id: Optional[int] = None
        execution_result: Optional[ExecutionResult] = None

        try:
            answer_type = str(json_answer.get("type"))

            if answer_type in ["next", "error", "complete"]:
                answer_id = int(str(json_answer.get("id")))

                if answer_type == "next" or answer_type == "error":

                    payload = json_answer.get("payload")

                    if answer_type == "next":

                        if not isinstance(payload, dict):
                            raise ValueError("payload is not a dict")

                        if "errors" not in payload and "data" not in payload:
                            raise ValueError(
                                "payload does not contain 'data' or 'errors' fields"
                            )

                        execution_result = ExecutionResult(
                            errors=payload.get("errors"),
                            data=payload.get("data"),
                            extensions=payload.get("extensions"),
                        )

                        # Saving answer_type as 'data' to be understood with superclass
                        answer_type = "data"

                    elif answer_type == "error":

                        if not isinstance(payload, list):
                            raise ValueError("payload is not a list")

                        raise TransportQueryError(str(payload[0]),
                                                  query_id=answer_id,
                                                  errors=payload)

            elif answer_type in ["ping", "pong", "connection_ack"]:
                self.payloads[answer_type] = json_answer.get("payload", None)

            else:
                raise ValueError

            if self.check_keep_alive_task is not None:
                self._next_keep_alive_message.set()

        except ValueError as e:
            raise TransportProtocolError(
                f"Server did not return a GraphQL result: {json_answer}"
            ) from e

        return answer_type, answer_id, execution_result
示例#15
0
    async def execute(
        self,
        document: DocumentNode,
        variable_values: Optional[Dict[str, str]] = None,
        operation_name: Optional[str] = None,
        extra_args: Dict[str, Any] = None,
        upload_files: bool = False,
    ) -> ExecutionResult:
        """Execute the provided document AST against the configured remote server
        using the current session.
        This uses the aiohttp library to perform a HTTP POST request asynchronously
        to the remote server.

        Don't call this coroutine directly on the transport, instead use
        :code:`execute` on a client or a session.

        :param document: the parsed GraphQL request
        :param variables_values: An optional Dict of variable values
        :param operation_name: An optional Operation name for the request
        :param extra_args: additional arguments to send to the aiohttp post method
        :param upload_files: Set to True if you want to put files in the variable values
        :returns: an ExecutionResult object.
        """

        query_str = print_ast(document)

        payload: Dict[str, Any] = {
            "query": query_str,
        }

        if operation_name:
            payload["operationName"] = operation_name

        if upload_files:

            # If the upload_files flag is set, then we need variable_values
            assert variable_values is not None

            # If we upload files, we will extract the files present in the
            # variable_values dict and replace them by null values
            nulled_variable_values, files = extract_files(
                variables=variable_values, file_classes=self.file_classes,
            )

            # Save the nulled variable values in the payload
            payload["variables"] = nulled_variable_values

            # Prepare aiohttp to send multipart-encoded data
            data = aiohttp.FormData()

            # Generate the file map
            # path is nested in a list because the spec allows multiple pointers
            # to the same file. But we don't support that.
            # Will generate something like {"0": ["variables.file"]}
            file_map = {str(i): [path] for i, path in enumerate(files)}

            # Enumerate the file streams
            # Will generate something like {'0': <_io.BufferedReader ...>}
            file_streams = {str(i): files[path] for i, path in enumerate(files)}

            # Add the payload to the operations field
            operations_str = json.dumps(payload)
            log.debug("operations %s", operations_str)
            data.add_field(
                "operations", operations_str, content_type="application/json"
            )

            # Add the file map field
            file_map_str = json.dumps(file_map)
            log.debug("file_map %s", file_map_str)
            data.add_field("map", file_map_str, content_type="application/json")

            # Add the extracted files as remaining fields
            for k, v in file_streams.items():
                data.add_field(k, v, filename=k)

            post_args: Dict[str, Any] = {"data": data}

        else:
            if variable_values:
                payload["variables"] = variable_values

            if log.isEnabledFor(logging.INFO):
                log.info(">>> %s", json.dumps(payload))

            post_args = {"json": payload}

        # Pass post_args to aiohttp post method
        if extra_args:
            post_args.update(extra_args)

        if self.session is None:
            raise TransportClosed("Transport is not connected")

        async with self.session.post(self.url, ssl=self.ssl, **post_args) as resp:
            try:
                result = await resp.json()

                if log.isEnabledFor(logging.INFO):
                    result_text = await resp.text()
                    log.info("<<< %s", result_text)
            except Exception:
                # We raise a TransportServerError if the status code is 400 or higher
                # We raise a TransportProtocolError in the other cases

                try:
                    # Raise a ClientResponseError if response status is 400 or higher
                    resp.raise_for_status()

                except ClientResponseError as e:
                    raise TransportServerError(str(e)) from e

                result_text = await resp.text()
                raise TransportProtocolError(
                    f"Server did not return a GraphQL result: {result_text}"
                )

            if "errors" not in result and "data" not in result:
                result_text = await resp.text()
                raise TransportProtocolError(
                    "Server did not return a GraphQL result: "
                    'No "data" or "error" keys in answer: '
                    f"{result_text}"
                )

            return ExecutionResult(errors=result.get("errors"), data=result.get("data"))
示例#16
0
    def execute(  # type: ignore
        self,
        document: DocumentNode,
        variable_values: Optional[Dict[str, Any]] = None,
        operation_name: Optional[str] = None,
        timeout: Optional[int] = None,
    ) -> ExecutionResult:
        """Execute GraphQL query.

        Execute the provided document AST against the configured remote server. This
        uses the requests library to perform a HTTP POST request to the remote server.

        :param document: GraphQL query as AST Node object.
        :param variable_values: Dictionary of input parameters (Default: None).
        :param operation_name: Name of the operation that shall be executed.
            Only required in multi-operation documents (Default: None).
        :param timeout: Specifies a default timeout for requests (Default: None).
        :return: The result of execution.
            `data` is the result of executing the query, `errors` is null
            if no errors occurred, and is a non-empty array if an error occurred.
        """

        if not self.session:
            raise TransportClosed("Transport is not connected")

        query_str = print_ast(document)
        payload: Dict[str, Any] = {"query": query_str}
        if variable_values:
            payload["variables"] = variable_values
        if operation_name:
            payload["operationName"] = operation_name

        data_key = "json" if self.use_json else "data"
        post_args = {
            "headers": self.headers,
            "auth": self.auth,
            "cookies": self.cookies,
            "timeout": timeout or self.default_timeout,
            "verify": self.verify,
            data_key: payload,
        }

        # Log the payload
        if log.isEnabledFor(logging.INFO):
            log.info(">>> %s", json.dumps(payload))

        # Pass kwargs to requests post method
        post_args.update(self.kwargs)

        # Using the created session to perform requests
        response = self.session.request(
            self.method,
            self.url,
            **post_args  # type: ignore
        )
        try:
            result = response.json()

            if log.isEnabledFor(logging.INFO):
                log.info("<<< %s", response.text)
        except Exception:
            # We raise a TransportServerError if the status code is 400 or higher
            # We raise a TransportProtocolError in the other cases

            try:
                # Raise a requests.HTTPerror if response status is 400 or higher
                response.raise_for_status()

            except requests.HTTPError as e:
                raise TransportServerError(str(e))

            raise TransportProtocolError(
                "Server did not return a GraphQL result")

        if "errors" not in result and "data" not in result:
            raise TransportProtocolError(
                "Server did not return a GraphQL result")

        return ExecutionResult(errors=result.get("errors"),
                               data=result.get("data"))
示例#17
0
    def _parse_answer(
            self, answer: str
    ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]:
        """Parse the answer received from the server

        Returns a list consisting of:
            - the answer_type (between:
              'heartbeat', 'data', 'reply', 'error', 'close')
            - the answer id (Integer) if received or None
            - an execution Result if the answer_type is 'data' or None
        """

        event: str = ""
        answer_id: Optional[int] = None
        answer_type: str = ""
        execution_result: Optional[ExecutionResult] = None

        try:
            json_answer = json.loads(answer)

            event = str(json_answer.get("event"))

            if event == "subscription:data":
                payload = json_answer.get("payload")

                if not isinstance(payload, dict):
                    raise ValueError("payload is not a dict")

                subscription_id = str(payload.get("subscriptionId"))
                try:
                    answer_id = self.subscription_ids_to_query_ids[
                        subscription_id]
                except KeyError:
                    raise ValueError(
                        f"subscription '{subscription_id}' has not been registerd"
                    )

                result = payload.get("result")

                if not isinstance(result, dict):
                    raise ValueError("result is not a dict")

                answer_type = "data"

                execution_result = ExecutionResult(
                    errors=payload.get("errors"), data=result.get("data"))

            elif event == "phx_reply":
                answer_id = int(json_answer.get("ref"))
                payload = json_answer.get("payload")

                if not isinstance(payload, dict):
                    raise ValueError("payload is not a dict")

                status = str(payload.get("status"))

                if status == "ok":

                    answer_type = "reply"
                    response = payload.get("response")

                    if isinstance(response,
                                  dict) and "subscriptionId" in response:
                        subscription_id = str(response.get("subscriptionId"))
                        self.subscription_ids_to_query_ids[
                            subscription_id] = answer_id

                elif status == "error":
                    response = payload.get("response")

                    if isinstance(response, dict):
                        if "errors" in response:
                            raise TransportQueryError(str(
                                response.get("errors")),
                                                      query_id=answer_id)
                        elif "reason" in response:
                            raise TransportQueryError(str(
                                response.get("reason")),
                                                      query_id=answer_id)
                    raise ValueError("reply error")

                elif status == "timeout":
                    raise TransportQueryError("reply timeout",
                                              query_id=answer_id)

            elif event == "phx_error":
                raise TransportServerError("Server error")
            elif event == "phx_close":
                answer_type = "close"
            else:
                raise ValueError

        except ValueError as e:
            raise TransportProtocolError(
                "Server did not return a GraphQL result") from e

        return answer_type, answer_id, execution_result
    def _parse_answer(
            self, answer: str
    ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]:
        """Parse the answer received from the server

        Returns a list consisting of:
            - the answer_type (between:
              'data', 'reply', 'complete', 'close')
            - the answer id (Integer) if received or None
            - an execution Result if the answer_type is 'data' or None
        """

        event: str = ""
        answer_id: Optional[int] = None
        answer_type: str = ""
        execution_result: Optional[ExecutionResult] = None
        subscription_id: Optional[str] = None

        def _get_value(d: Any, key: str, label: str) -> Any:
            if not isinstance(d, dict):
                raise ValueError(f"{label} is not a dict")

            return d.get(key)

        def _required_value(d: Any, key: str, label: str) -> Any:
            value = _get_value(d, key, label)
            if value is None:
                raise ValueError(f"null {key} in {label}")

            return value

        def _required_subscription_id(d: Any,
                                      label: str,
                                      must_exist: bool = False,
                                      must_not_exist=False) -> str:
            subscription_id = str(_required_value(d, "subscriptionId", label))
            if must_exist and (subscription_id not in self.subscriptions):
                raise ValueError("unregistered subscriptionId")
            if must_not_exist and (subscription_id in self.subscriptions):
                raise ValueError("previously registered subscriptionId")

            return subscription_id

        def _validate_data_response(d: Any, label: str) -> dict:
            """Make sure query, mutation or subscription answer conforms.
            The GraphQL spec says only three keys are permitted.
            """
            if not isinstance(d, dict):
                raise ValueError(f"{label} is not a dict")

            keys = set(d.keys())
            invalid = keys - {"data", "errors", "extensions"}
            if len(invalid) > 0:
                raise ValueError(f"{label} contains invalid items: " +
                                 ", ".join(invalid))
            return d

        try:
            json_answer = json.loads(answer)

            event = str(_required_value(json_answer, "event", "answer"))

            if event == "subscription:data":
                payload = _required_value(json_answer, "payload", "answer")

                subscription_id = _required_subscription_id(payload,
                                                            "payload",
                                                            must_exist=True)

                result = _validate_data_response(payload.get("result"),
                                                 "result")

                answer_type = "data"

                subscription = self.subscriptions[subscription_id]
                answer_id = subscription.listener_id

                execution_result = ExecutionResult(
                    data=result.get("data"),
                    errors=result.get("errors"),
                    extensions=result.get("extensions"),
                )

            elif event == "phx_reply":

                # Will generate a ValueError if 'ref' is not there
                # or if it is not an integer
                answer_id = int(_required_value(json_answer, "ref", "answer"))

                payload = _required_value(json_answer, "payload", "answer")

                status = _get_value(payload, "status", "payload")

                if status == "ok":
                    answer_type = "reply"

                    if answer_id in self.listeners:
                        response = _required_value(payload, "response",
                                                   "payload")

                        if isinstance(response,
                                      dict) and "subscriptionId" in response:

                            # Subscription answer
                            subscription_id = _required_subscription_id(
                                response, "response", must_not_exist=True)

                            self.subscriptions[subscription_id] = Subscription(
                                answer_id)

                        else:
                            # Query or mutation answer
                            # GraphQL spec says only three keys are permitted
                            response = _validate_data_response(
                                response, "response")

                            answer_type = "data"

                            execution_result = ExecutionResult(
                                data=response.get("data"),
                                errors=response.get("errors"),
                                extensions=response.get("extensions"),
                            )
                    else:
                        (
                            registered_subscription_id,
                            listener_id,
                        ) = self._find_subscription(answer_id)
                        if registered_subscription_id is not None:
                            # Unsubscription answer
                            response = _required_value(payload, "response",
                                                       "payload")
                            subscription_id = _required_subscription_id(
                                response, "response")

                            if subscription_id != registered_subscription_id:
                                raise ValueError(
                                    "subscription id does not match")

                            answer_type = "complete"

                            answer_id = listener_id

                elif status == "error":
                    response = payload.get("response")

                    if isinstance(response, dict):
                        if "errors" in response:
                            raise TransportQueryError(str(
                                response.get("errors")),
                                                      query_id=answer_id)
                        elif "reason" in response:
                            raise TransportQueryError(str(
                                response.get("reason")),
                                                      query_id=answer_id)
                    raise TransportQueryError("reply error",
                                              query_id=answer_id)

                elif status == "timeout":
                    raise TransportQueryError("reply timeout",
                                              query_id=answer_id)
                else:
                    # missing or unrecognized status, just continue
                    pass

            elif event == "phx_error":
                # Sent if the channel has crashed
                # answer_id will be the "join_ref" for the channel
                # answer_id = int(json_answer.get("ref"))
                raise TransportServerError("Server error")
            elif event == "phx_close":
                answer_type = "close"
            else:
                raise ValueError("unrecognized event")

        except ValueError as e:
            log.error(f"Error parsing answer '{answer}': {e!r}")
            raise TransportProtocolError(
                f"Server did not return a GraphQL result: {e!s}") from e

        return answer_type, answer_id, execution_result
示例#19
0
    def _parse_answer(
            self, answer: str
    ) -> Tuple[str, Optional[int], Optional[ExecutionResult]]:
        """Parse the answer received from the server

        Returns a list consisting of:
            - the answer_type (between:
              'connection_ack', 'ka', 'connection_error', 'data', 'error', 'complete')
            - the answer id (Integer) if received or None
            - an execution Result if the answer_type is 'data' or None
        """

        answer_type: str = ""
        answer_id: Optional[int] = None
        execution_result: Optional[ExecutionResult] = None

        try:
            json_answer = json.loads(answer)

            answer_type = str(json_answer.get("type"))

            if answer_type in ["data", "error", "complete"]:
                answer_id = int(str(json_answer.get("id")))

                if answer_type == "data" or answer_type == "error":

                    payload = json_answer.get("payload")

                    if not isinstance(payload, dict):
                        raise ValueError("payload is not a dict")

                    if answer_type == "data":

                        if "errors" not in payload and "data" not in payload:
                            raise ValueError(
                                "payload does not contain 'data' or 'errors' fields"
                            )

                        execution_result = ExecutionResult(
                            errors=payload.get("errors"),
                            data=payload.get("data"))

                    elif answer_type == "error":

                        raise TransportQueryError(str(payload),
                                                  query_id=answer_id,
                                                  errors=[payload])

            elif answer_type == "ka":
                # KeepAlive message
                pass
            elif answer_type == "connection_ack":
                pass
            elif answer_type == "connection_error":
                error_payload = json_answer.get("payload")
                raise TransportServerError(
                    f"Server error: '{repr(error_payload)}'")
            else:
                raise ValueError

        except ValueError as e:
            raise TransportProtocolError(
                "Server did not return a GraphQL result") from e

        return answer_type, answer_id, execution_result
示例#20
0
def test_encode_execution_results():
    data = {"answer": 42}
    errors = [GraphQLError("bad")]
    results = [ExecutionResult(data, None), ExecutionResult(None, errors)]
    result = encode_execution_results(results)
    assert result == ('{"data":{"answer":42}}', 400)
示例#21
0
def test_encode_execution_results_not_encoded():
    data = {"answer": 42}
    results = [ExecutionResult(data, None)]
    result = encode_execution_results(results, encode=lambda r: r)
    assert result == ({"data": data}, 200)
示例#22
0
    async def __call__(self, request: Request) -> Response:
        """
        Run the GraphQL query provided.

        :param request: aiohttp Request
        :return: aiohttp Response
        """
        request_method = request.method.lower()
        try:
            variables = json.loads(request.query.get("variables", "{}"))
        except json.decoder.JSONDecodeError:
            return self.error_response("Variables are invalid JSON.")
        operation_name = request.query.get("operationName")

        if request_method == "options":
            return self.process_preflight(request)
        elif request_method == "post":
            try:
                data = await self.parse_body(request)
            except json.decoder.JSONDecodeError:
                return self.error_response("POST body sent invalid JSON.")
            operation_name = data.get("operationName", operation_name)
        elif request_method == "get":
            data = {"query": request.query.get("query")}
        else:
            return self.error_response(
                "GraphQL only supports GET and POST requests.",
                405,
                headers={"Allow": "GET, POST"},
            )

        is_tool = self.is_tool(request)

        vars_dyn = data.get("variables", {}) or {}
        variables.update(
            vars_dyn if isinstance(vars_dyn, dict) else json.loads(vars_dyn)
        )
        query = cast(str, data.get("query"))
        context = self.get_context(request)
        invalid = False

        if is_tool:
            tool = cast(GraphQLTool, self.tool)
            return await tool.render(query, variables, operation_name)

        if not data.get("query"):
            return self.encode_response(
                request,
                ExecutionResult(
                    data=None,
                    errors=[GraphQLError(message="Must provide query string.")],
                ),
                invalid=True,
            )

        # Validate Schema
        schema_validation_errors = validate_schema(self.schema)
        if schema_validation_errors:  # pragma: no cover
            return self.encode_response(
                request,
                ExecutionResult(data=None, errors=schema_validation_errors),
                invalid=True,
            )

        # Parse
        try:
            document = parse(query)
            op = get_operation_ast(document, operation_name)
            if op is None:
                invalid = True
            else:
                if request_method == "get" and op.operation != OperationType.QUERY:
                    return self.error_response(
                        "Can only perform a {} operation from a POST request.".format(
                            op.operation.value
                        ),
                        405,
                        headers={"Allow": "POST"},
                    )
        except GraphQLError as error:
            return self.encode_response(
                request, ExecutionResult(data=None, errors=[error]), invalid=True
            )
        except Exception as error:  # pragma: no cover
            error = GraphQLError(str(error), original_error=error)
            return self.encode_response(
                request, ExecutionResult(data=None, errors=[error]), invalid=True
            )

        # Validate
        validation_errors = validate(self.schema, document)
        if validation_errors:
            return self.encode_response(
                request,
                ExecutionResult(data=None, errors=validation_errors),
                invalid=True,
            )

        if self.asynchronous:
            result = self._graphql(
                self.schema,
                document=document,
                variable_values=variables,
                operation_name=operation_name,
                root_value=self.root_value,
                context_value=context,
                middleware=self.middleware,
            )
            if isawaitable(result):  # pragma: no branch
                result = await cast(Awaitable[ExecutionResult], result)
        else:
            result = self._graphql(
                self.schema,
                document=document,
                variable_values=variables,
                operation_name=operation_name,
                root_value=self.root_value,
                context_value=context,
                middleware=self.middleware,
            )

        return self.encode_response(
            request, cast(ExecutionResult, result), invalid=invalid
        )
示例#23
0
    def execute(  # type: ignore
        self,
        document: DocumentNode,
        variable_values: Optional[Dict[str, Any]] = None,
        operation_name: Optional[str] = None,
        timeout: Optional[int] = None,
        extra_args: Dict[str, Any] = None,
        upload_files: bool = False,
    ) -> ExecutionResult:
        """Execute GraphQL query.

        Execute the provided document AST against the configured remote server. This
        uses the requests library to perform a HTTP POST request to the remote server.

        :param document: GraphQL query as AST Node object.
        :param variable_values: Dictionary of input parameters (Default: None).
        :param operation_name: Name of the operation that shall be executed.
            Only required in multi-operation documents (Default: None).
        :param timeout: Specifies a default timeout for requests (Default: None).
        :param extra_args: additional arguments to send to the requests post method
        :param upload_files: Set to True if you want to put files in the variable values
        :return: The result of execution.
            `data` is the result of executing the query, `errors` is null
            if no errors occurred, and is a non-empty array if an error occurred.
        """

        if not self.session:
            raise TransportClosed("Transport is not connected")

        query_str = print_ast(document)
        payload: Dict[str, Any] = {"query": query_str}

        if operation_name:
            payload["operationName"] = operation_name

        post_args = {
            "headers": self.headers,
            "auth": self.auth,
            "cookies": self.cookies,
            "timeout": timeout or self.default_timeout,
            "verify": self.verify,
        }

        if upload_files:
            # If the upload_files flag is set, then we need variable_values
            assert variable_values is not None

            # If we upload files, we will extract the files present in the
            # variable_values dict and replace them by null values
            nulled_variable_values, files = extract_files(
                variables=variable_values,
                file_classes=self.file_classes,
            )

            # Save the nulled variable values in the payload
            payload["variables"] = nulled_variable_values

            # Add the payload to the operations field
            operations_str = json.dumps(payload)
            log.debug("operations %s", operations_str)

            # Generate the file map
            # path is nested in a list because the spec allows multiple pointers
            # to the same file. But we don't support that.
            # Will generate something like {"0": ["variables.file"]}
            file_map = {str(i): [path] for i, path in enumerate(files)}

            # Enumerate the file streams
            # Will generate something like {'0': <_io.BufferedReader ...>}
            file_streams = {
                str(i): files[path]
                for i, path in enumerate(files)
            }

            # Add the file map field
            file_map_str = json.dumps(file_map)
            log.debug("file_map %s", file_map_str)

            fields = {"operations": operations_str, "map": file_map_str}

            # Add the extracted files as remaining fields
            for k, v in file_streams.items():
                fields[k] = (getattr(v, "name", k), v)

            # Prepare requests http to send multipart-encoded data
            data = MultipartEncoder(fields=fields)

            post_args["data"] = data

            if post_args["headers"] is None:
                post_args["headers"] = {}
            else:
                post_args["headers"] = {**post_args["headers"]}

            post_args["headers"]["Content-Type"] = data.content_type

        else:
            if variable_values:
                payload["variables"] = variable_values

            data_key = "json" if self.use_json else "data"
            post_args[data_key] = payload

        # Log the payload
        if log.isEnabledFor(logging.INFO):
            log.info(">>> %s", json.dumps(payload))

        # Pass kwargs to requests post method
        post_args.update(self.kwargs)

        # Pass post_args to requests post method
        if extra_args:
            post_args.update(extra_args)

        # Using the created session to perform requests
        response = self.session.request(
            self.method,
            self.url,
            **post_args  # type: ignore
        )
        self.response_headers = response.headers

        def raise_response_error(resp: requests.Response, reason: str):
            # We raise a TransportServerError if the status code is 400 or higher
            # We raise a TransportProtocolError in the other cases

            try:
                # Raise a HTTPError if response status is 400 or higher
                resp.raise_for_status()
            except requests.HTTPError as e:
                raise TransportServerError(str(e),
                                           e.response.status_code) from e

            result_text = resp.text
            raise TransportProtocolError(
                f"Server did not return a GraphQL result: "
                f"{reason}: "
                f"{result_text}")

        try:
            result = response.json()

            if log.isEnabledFor(logging.INFO):
                log.info("<<< %s", response.text)

        except Exception:
            raise_response_error(response, "Not a JSON answer")

        if "errors" not in result and "data" not in result:
            raise_response_error(response,
                                 'No "data" or "errors" keys in answer')

        return ExecutionResult(
            errors=result.get("errors"),
            data=result.get("data"),
            extensions=result.get("extensions"),
        )