Skip to content
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

#71 mutations support #72

Merged
merged 5 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/plugins/pydantic_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Supported definitions are:

- `fragment`
- `query`
- `mutation` (only response data structures - input data structures are still a [TODO](https://github.com/app-sre/qenerate/issues/71))

## Opinionated Custom Scalars

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "qenerate"
version = "0.4.7"
version = "0.5.0"
description = "Code Generator for GraphQL Query and Fragment Data Classes"
authors = [
"Red Hat - Service Delivery - AppSRE <[email protected]>"
Expand Down
2 changes: 1 addition & 1 deletion qenerate/core/code_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def generate_code(self, introspection_file_path: str, dir: str):
schema=schema,
)

rendered_queries = plugin.generate_queries(
rendered_queries = plugin.generate_operations(
definitions=queries_by_plugin[plugin_name],
fragments=rendered_fragments,
schema=schema,
Expand Down
2 changes: 1 addition & 1 deletion qenerate/core/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Fragment(GeneratedFile):


class Plugin:
def generate_queries(
def generate_operations(
self,
definitions: list[GQLDefinition],
schema: GraphQLSchema,
Expand Down
33 changes: 21 additions & 12 deletions qenerate/core/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(self, message: str):
class GQLDefinitionType(Enum):
QUERY = 1
FRAGMENT = 2
MUTATION = 3


@dataclass
Expand Down Expand Up @@ -79,23 +80,31 @@ def enter_operation_definition(self, node: OperationDefinitionNode, *_):
body = self._node_body(node)
name = self._node_name(node)

if node.operation != OperationType.QUERY:
if node.operation == OperationType.QUERY:
definition = GQLDefinition(
kind=GQLDefinitionType.QUERY,
definition=body,
source_file=self._source_file_path,
feature_flags=self._feature_flags,
fragment_dependencies=set(),
name=name,
)
elif node.operation == OperationType.MUTATION:
definition = GQLDefinition(
kind=GQLDefinitionType.MUTATION,
definition=body,
source_file=self._source_file_path,
feature_flags=self._feature_flags,
fragment_dependencies=set(),
name=name,
)
else:
# TODO: logger
# TODO: raise
print(
"[WARNING] Skipping operation definition because"
f" it is not a query: \n{body}"
f" it is neither a query nor a mutation: \n{body}"
)
return

definition = GQLDefinition(
kind=GQLDefinitionType.QUERY,
definition=body,
source_file=self._source_file_path,
feature_flags=self._feature_flags,
fragment_dependencies=set(),
name=name,
)
self._stack.append(definition)

def leave_operation_definition(self, *_):
Expand Down
50 changes: 37 additions & 13 deletions qenerate/plugins/pydantic_v1/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ParsedClassNode,
)
from qenerate.core.feature_flag_parser import NamingCollisionStrategy, FeatureFlags
from qenerate.core.preprocessor import GQLDefinition
from qenerate.core.preprocessor import GQLDefinition, GQLDefinitionType

from qenerate.plugins.pydantic_v1.mapper import (
graphql_class_name_to_python,
Expand Down Expand Up @@ -66,7 +66,7 @@
)


def convenience_function(cls: str) -> str:
def query_convenience_function(cls: str) -> str:
return f"""
def query(query_func: Callable, **kwargs: Any) -> {cls}:
{INDENT}\"\"\"
Expand All @@ -88,6 +88,29 @@ def query(query_func: Callable, **kwargs: Any) -> {cls}:
"""


def mutation_convenience_function(cls: str) -> str:
return f"""
def mutate(mutation_func: Callable, **kwargs: Any) -> {cls}:
{INDENT}\"\"\"
{INDENT}This is a convenience function which executes a mutation and parses the response
{INDENT}into concrete types. It should be compatible with most GQL clients.
{INDENT}You do not have to use it to consume the generated data classes.
{INDENT}Alternatively, you can also mime and alternate the behavior
{INDENT}of this function in the caller.

{INDENT}Parameters:
{INDENT}{INDENT}mutation_func (Callable): Function which executes the mutation.
{INDENT}{INDENT}kwargs: Arguments that will be passed to the mutation function.
{INDENT}{INDENT}{INDENT}This must include the mutation parameters.

{INDENT}Returns:
{INDENT}{INDENT}{cls}: mutation response parsed into generated classes
{INDENT}\"\"\"
{INDENT}raw_data: dict[Any, Any] = mutation_func(DEFINITION, **kwargs)
{INDENT}return {cls}(**raw_data)
"""


class PydanticV1Error(Exception):
pass

Expand All @@ -97,7 +120,7 @@ def __init__(
self,
schema: GraphQLSchema,
type_info: TypeInfo,
definition: str,
definition: GQLDefinition,
feature_flags: FeatureFlags,
):
Visitor.__init__(self)
Expand Down Expand Up @@ -140,6 +163,7 @@ def enter_operation_definition(self, node: OperationDefinitionNode, *_):
current = ParsedOperationNode(
parent=self.parent,
fields=[],
operation_type=self.definition.kind,
parsed_type=ParsedFieldType(
unwrapped_python_type=node.name.value,
wrapped_python_type=node.name.value,
Expand Down Expand Up @@ -266,9 +290,9 @@ def _to_python_type(self, graphql_type: GraphQLOutputType) -> str:
class QueryParser:
@staticmethod
def parse(
definition: str, schema: GraphQLSchema, feature_flags: FeatureFlags
definition: GQLDefinition, schema: GraphQLSchema, feature_flags: FeatureFlags
) -> ParsedNode:
document_ast = parse(definition)
document_ast = parse(definition.definition)
type_info = TypeInfo(schema)
visitor = FieldToTypeMatcherVisitor(
schema=schema,
Expand Down Expand Up @@ -339,9 +363,8 @@ def generate_fragments(
result += fragment_imports
qf = definition.source_file
parser = QueryParser()
fragment_definition = definition.definition
ast = parser.parse(
definition=fragment_definition,
definition=definition,
schema=schema,
feature_flags=definition.feature_flags,
)
Expand Down Expand Up @@ -400,7 +423,7 @@ def _assemble_definition(
)
return ans

def generate_queries(
def generate_operations(
self,
definitions: list[GQLDefinition],
schema: GraphQLSchema,
Expand Down Expand Up @@ -433,17 +456,18 @@ def generate_queries(
)
result += 'DEFINITION = """\n' f"{assembled_definition}" '\n"""'
parser = QueryParser()
query = definition.definition
ast = parser.parse(
definition=query,
definition=definition,
schema=schema,
feature_flags=definition.feature_flags,
)
result += self._traverse(ast)
result += "\n\n"
result += convenience_function(
cls=f"{ast.fields[0].parsed_type.unwrapped_python_type}QueryData"
)
cls = ast.fields[0].parsed_type.unwrapped_python_type
if definition.kind == GQLDefinitionType.QUERY:
result += query_convenience_function(cls=f"{cls}QueryData")
else:
result += mutation_convenience_function(cls=f"{cls}MutationResponse")
generated_files.append(
GeneratedFile(file=qf.with_suffix(".py"), content=result)
)
Expand Down
9 changes: 8 additions & 1 deletion qenerate/plugins/pydantic_v1/typed_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from dataclasses import dataclass
from typing import Any, Optional

from qenerate.core.preprocessor import GQLDefinitionType


INDENT = " "

Expand Down Expand Up @@ -155,10 +157,15 @@ def field_type(self) -> str:

@dataclass
class ParsedOperationNode(ParsedNode):
operation_type: GQLDefinitionType

def class_code_string(self) -> str:
lines = ["\n\n"]
class_suffix = "QueryData"
if self.operation_type == GQLDefinitionType.MUTATION:
class_suffix = "MutationResponse"
lines.append(
f"class {self.parsed_type.unwrapped_python_type}QueryData(BaseModel):"
f"class {self.parsed_type.unwrapped_python_type}{class_suffix}(BaseModel):"
)
for field in self.fields:
if isinstance(field, ParsedClassNode):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

setup_kwargs = {
"name": "qenerate",
"version": "0.4.7",
"version": "0.5.0",
"description": "Code Generator for GraphQL Query and Fragment Data Classes",
"long_description": "Qenerate is a Code Generator for GraphQL Query and Fragment Data Classes. Documentation is at https://github.com/app-sre/qenerate .",
"author": "Service Delivery - AppSRE",
Expand Down
2 changes: 1 addition & 1 deletion tests/core/preprocessor/queries/mutation.gql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# qenerate: plugin=test
# This should be ignored by qenerate

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
Expand Down
2 changes: 1 addition & 1 deletion tests/core/test_code_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def generate_fragments(
) -> list[Fragment]:
return []

def generate_queries(
def generate_operations(
self,
definitions: list[GQLDefinition],
fragments: list[Fragment],
Expand Down
11 changes: 10 additions & 1 deletion tests/core/test_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,16 @@ def normalize_definition(definition: str) -> str:
# Test a file containing a mutation
[
Path("tests/core/preprocessor/queries/mutation.gql"),
[],
[
GQLDefinition(
feature_flags=FeatureFlags(plugin="test"),
kind=GQLDefinitionType.MUTATION,
name="CreateReviewForEpisode",
definition="mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) { createReview(episode: $ep, review: $review) { stars commentary } }",
fragment_dependencies=set(),
source_file="", # adjusted in test
),
],
],
# Test a file containing a single fragment
[
Expand Down
14 changes: 14 additions & 0 deletions tests/generator/definitions/github/comment_mutation.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mutation AddComment($body: String = "", $subjectId: ID = "") {
addComment(input: {subjectId: $subjectId, body: $body}) {
subject {
... on Topic {
id
name
}
... on User {
id
email
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
Generated by qenerate plugin=pydantic_v1. DO NOT MODIFY MANUALLY!
"""
from collections.abc import Callable # noqa: F401 # pylint: disable=W0611
from datetime import datetime # noqa: F401 # pylint: disable=W0611
from enum import Enum # noqa: F401 # pylint: disable=W0611
from typing import ( # noqa: F401 # pylint: disable=W0611
Any,
Optional,
Union,
)

from pydantic import ( # noqa: F401 # pylint: disable=W0611
BaseModel,
Extra,
Field,
Json,
)


DEFINITION = """
mutation AddComment($body: String = "", $subjectId: ID = "") {
addComment(input: {subjectId: $subjectId, body: $body}) {
subject {
... on Topic {
id
name
}
... on User {
id
email
}
}
}
}

"""


class Node(BaseModel):

class Config:
smart_union = True
extra = Extra.forbid


class Topic(Node):
q_id: str = Field(..., alias="id")
name: str = Field(..., alias="name")

class Config:
smart_union = True
extra = Extra.forbid


class User(Node):
q_id: str = Field(..., alias="id")
email: str = Field(..., alias="email")

class Config:
smart_union = True
extra = Extra.forbid


class AddCommentPayload(BaseModel):
subject: Optional[Union[Topic, User, Node]] = Field(..., alias="subject")

class Config:
smart_union = True
extra = Extra.forbid


class AddCommentMutationResponse(BaseModel):
add_comment: Optional[AddCommentPayload] = Field(..., alias="addComment")

class Config:
smart_union = True
extra = Extra.forbid


def mutate(mutation_func: Callable, **kwargs: Any) -> AddCommentMutationResponse:
"""
This is a convenience function which executes a mutation and parses the response
into concrete types. It should be compatible with most GQL clients.
You do not have to use it to consume the generated data classes.
Alternatively, you can also mime and alternate the behavior
of this function in the caller.

Parameters:
mutation_func (Callable): Function which executes the mutation.
kwargs: Arguments that will be passed to the mutation function.
This must include the mutation parameters.

Returns:
AddCommentMutationResponse: mutation response parsed into generated classes
"""
raw_data: dict[Any, Any] = mutation_func(DEFINITION, **kwargs)
return AddCommentMutationResponse(**raw_data)
Loading