Skip to content

Commit

Permalink
Merge pull request #4 from lonelyteapot/rework-parameters
Browse files Browse the repository at this point in the history
Support non-nullable parameters in queries
  • Loading branch information
lonelyteapot authored Jul 15, 2022
2 parents 8eef732 + 45665dd commit 561a495
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 17 deletions.
3 changes: 2 additions & 1 deletion examples/parametrized_query/get_users_born_after.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from datetime import date
from typing import Optional

from pydantic import Field

Expand All @@ -13,7 +14,7 @@


class Parameters(GQLParameters):
bornAfter: date
bornAfter: Optional[date] = None


class User(GQLType):
Expand Down
Empty file.
18 changes: 18 additions & 0 deletions examples/required_parametrized_query/get_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from __future__ import annotations

from pydantic import Field

from pygraphic import GQLParameters, GQLQuery, GQLType


class Parameters(GQLParameters):
userId: int


class User(GQLType):
id: int
username: str


class GetUser(GQLQuery, parameters=Parameters):
user: User = Field(id=Parameters.userId)
25 changes: 25 additions & 0 deletions examples/required_parametrized_query/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json

from examples.server import server_schema

from .get_user import GetUser, Parameters


# Generate query string
gql = GetUser.get_query_string()
variables = Parameters(userId=1)

# Typically you would send an HTTP Post request to a remote server.
# For simplicity, this query is processed locally.
# We dump and load json here to convert parameters from python types to strings.
response = server_schema.execute_sync(gql, json.loads(variables.json()))

# Handle errors
if response.data is None:
raise Exception("Query failed", response.errors)

# Parse the data
result = GetUser.parse_obj(response.data)

# Print validated data
print(result.user)
4 changes: 4 additions & 0 deletions examples/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def users(self, born_after: Optional[date] = None) -> list[User]:
return [user for user in _users if user.birthday > born_after]
return _users

@strawberry.field
def user(self, id: int) -> User:
return next(filter(lambda user: user.id == id, _users))


_users = [
User(
Expand Down
6 changes: 6 additions & 0 deletions golden_files/query_parametrized_required.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
query GetUser($userId: Int!) {
user(id: $userId) {
id
username
}
}
1 change: 1 addition & 0 deletions golden_files/server_schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ scalar Date

type Query {
users(bornAfter: Date = null): [User!]!
user(id: Int!): User!
}

type User {
Expand Down
3 changes: 1 addition & 2 deletions pygraphic/_gql_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,4 @@ def __getattr__(cls, __name: str) -> Any:


class GQLParameters(pydantic.BaseModel, metaclass=ModelMetaclass):
def __str__(self):
return ""
pass
2 changes: 1 addition & 1 deletion pygraphic/_gql_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ def _gen_parameter_string(parameters: Optional[type[GQLParameters]]) -> Iterator
yield "$"
yield name
yield ": "
yield class_to_graphql_type(field.type_)
yield class_to_graphql_type(field.type_, allow_none=field.allow_none)
yield ")"
8 changes: 6 additions & 2 deletions pygraphic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ def register_graphql_type(graphql_type: str, python_class: type) -> None:
_mapping[python_class] = graphql_type


def class_to_graphql_type(python_class: type) -> str:
def class_to_graphql_type(python_class: type, allow_none: bool) -> str:
try:
return _mapping[python_class]
type_ = _mapping[python_class]
if allow_none:
return type_
else:
return type_ + "!"
except KeyError:
raise KeyError(
f"Type '{python_class.__name__}' could not be converted to a GraphQL type. "
Expand Down
14 changes: 10 additions & 4 deletions tests/test_parametrized_query.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import json
from datetime import date
from pathlib import Path

from examples.parametrized_query.get_users_born_after import GetUsersBornAfter
from examples.parametrized_query.get_users_born_after import (
GetUsersBornAfter,
Parameters,
)
from examples.server import server_schema


Expand All @@ -11,15 +16,16 @@ def test_query_string_generation():

def test_local_query_execution():
query = GetUsersBornAfter.get_query_string()
# variables = GetAllUsers.variables()
result = server_schema.execute_sync(query)
variables = Parameters(bornAfter=date.fromtimestamp(0.0))
result = server_schema.execute_sync(query, json.loads(variables.json()))
assert result.errors is None
assert result.data is not None


def test_pydantic_object_parsing():
query = GetUsersBornAfter.get_query_string()
result = server_schema.execute_sync(query)
variables = Parameters(bornAfter=date.fromtimestamp(0.0))
result = server_schema.execute_sync(query, json.loads(variables.json()))
assert type(result.data) is dict
result = GetUsersBornAfter.parse_obj(result.data)

Expand Down
35 changes: 35 additions & 0 deletions tests/test_required_parametrized_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json
from pathlib import Path

from examples.required_parametrized_query.get_user import GetUser, Parameters
from examples.server import server_schema


def test_query_string_generation():
expected = Path("golden_files", "query_parametrized_required.gql").read_text(
"utf-8"
)
assert GetUser.get_query_string() == expected


def test_local_query_execution():
query = GetUser.get_query_string()
variables = Parameters(userId=1)
result = server_schema.execute_sync(query, json.loads(variables.json()))
assert result.errors is None
assert result.data is not None


def test_pydantic_object_parsing():
query = GetUser.get_query_string()
variables = Parameters(userId=1)
result = server_schema.execute_sync(query, json.loads(variables.json()))
assert type(result.data) is dict
result = GetUser.parse_obj(result.data)


def test_example():
from examples.required_parametrized_query.main import result

assert type(result) is GetUser
assert result.user.id == 1
18 changes: 11 additions & 7 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@

def test_class_to_typename_crashes_on_unknown_class():
with pytest.raises(KeyError) as e_info:
class_to_graphql_type(object)
class_to_graphql_type(object, allow_none=True)


def test_class_to_typename_with_default_scalars():
assert class_to_graphql_type(int) == "Int"
assert class_to_graphql_type(float) == "Float"
assert class_to_graphql_type(str) == "String"
assert class_to_graphql_type(int) == "Int"
# assert class_to_graphql_type(str) == 'ID'
assert class_to_graphql_type(int, allow_none=True) == "Int"
assert class_to_graphql_type(float, allow_none=True) == "Float"
assert class_to_graphql_type(str, allow_none=True) == "String"
assert class_to_graphql_type(int, allow_none=True) == "Int"
# assert class_to_graphql_type(str, allow_none=True) == 'ID'


def test_class_to_typename_with_non_nullable():
assert class_to_graphql_type(int, allow_none=False) == "Int!"


def test_class_to_typename_with_registered_type():
register_graphql_type("CustomType", object)
assert class_to_graphql_type(object) == "CustomType"
assert class_to_graphql_type(object, allow_none=True) == "CustomType"

0 comments on commit 561a495

Please sign in to comment.