-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from lonelyteapot/parametrized-queries
Support for queries with parameters
- Loading branch information
Showing
17 changed files
with
261 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ Pygraphic | |
turms | ||
Semenov | ||
Pydantic | ||
camelCase |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from __future__ import annotations | ||
|
||
from datetime import date | ||
|
||
from pydantic import Field | ||
|
||
from pygraphic import GQLParameters, GQLQuery, GQLType | ||
from pygraphic.types import register_graphql_type | ||
|
||
|
||
# Register the type so that pygraphic knows how to convert it to GraphQL | ||
register_graphql_type("Date", date) | ||
|
||
|
||
class Parameters(GQLParameters): | ||
bornAfter: date | ||
|
||
|
||
class User(GQLType): | ||
username: str | ||
birthday: date | ||
friends: list[UserFriend] = Field(onlineOnly=True) | ||
|
||
|
||
class UserFriend(GQLType): | ||
username: str | ||
isOnline: bool | ||
|
||
|
||
class GetUsersBornAfter(GQLQuery, parameters=Parameters): | ||
users: list[User] = Field(bornAfter=Parameters.bornAfter) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import json | ||
from datetime import date | ||
from pprint import pprint | ||
|
||
from examples.server import server_schema | ||
|
||
import pygraphic | ||
|
||
from .get_users_born_after import GetUsersBornAfter, Parameters | ||
|
||
|
||
# We only want to get users born after this date | ||
born_after = date(year=1990, month=1, day=1) | ||
|
||
|
||
# Generate query string | ||
gql = GetUsersBornAfter.get_query_string() | ||
variables = Parameters(bornAfter=born_after) | ||
|
||
# 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 = GetUsersBornAfter.parse_obj(response.data) | ||
|
||
# Print validated data | ||
for user in result.users: | ||
print("User:", user.username) | ||
print("Birthday:", user.birthday) | ||
print("Online friends:") | ||
pprint(user.friends) | ||
print() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
query GetUsersBornAfter($bornAfter: Date) { | ||
users(bornAfter: $bornAfter) { | ||
username | ||
birthday | ||
friends(onlineOnly: true) { | ||
username | ||
isOnline | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
from . import types | ||
from ._gql_parameters import GQLParameters | ||
from ._gql_query import GQLQuery | ||
from ._gql_type import GQLType | ||
|
||
|
||
__all__ = ["GQLType", "GQLQuery"] | ||
__all__ = ["GQLParameters", "GQLType", "GQLQuery", "types"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
import pydantic | ||
import pydantic.main | ||
|
||
|
||
class ModelMetaclass(pydantic.main.ModelMetaclass): | ||
def __getattr__(cls, __name: str) -> Any: | ||
try: | ||
return cls.__fields__[__name] | ||
except KeyError: | ||
raise AttributeError( | ||
f"type object '{cls.__name__}' has no attribute '{__name}'" | ||
) | ||
|
||
|
||
class GQLParameters(pydantic.BaseModel, metaclass=ModelMetaclass): | ||
def __str__(self): | ||
return "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,47 @@ | ||
from typing import Any, Iterator, Optional | ||
|
||
from ._gql_parameters import GQLParameters | ||
from ._gql_type import GQLType | ||
from .types import class_to_graphql_type | ||
|
||
|
||
class GQLQuery(GQLType): | ||
__parameters__ = None | ||
|
||
@classmethod | ||
def get_query_string(cls, named: bool = True) -> str: | ||
if not named and cls.__parameters__ is not None: | ||
# TODO Find a better exception type | ||
raise Exception("Query with parameters must have a name") | ||
|
||
def _gen(): | ||
if named: | ||
yield "query " + cls.__name__ + " {" | ||
params = "".join(_gen_parameter_string(cls.__parameters__)) | ||
yield "query " + cls.__name__ + params + " {" | ||
else: | ||
yield "query {" | ||
for line in cls.generate_query_lines(nest_level=1): | ||
yield line | ||
yield "}" | ||
|
||
return "\n".join(_gen()) | ||
|
||
def __init_subclass__( | ||
cls, | ||
parameters: Optional[type[GQLParameters]] = None, | ||
**pydantic_kwargs: Any, | ||
) -> None: | ||
cls.__parameters__ = parameters | ||
return super().__init_subclass__(**pydantic_kwargs) | ||
|
||
|
||
def _gen_parameter_string(parameters: Optional[type[GQLParameters]]) -> Iterator[str]: | ||
if parameters is None or not parameters.__fields__: | ||
return | ||
yield "(" | ||
for name, field in parameters.__fields__.items(): | ||
yield "$" | ||
yield name | ||
yield ": " | ||
yield class_to_graphql_type(field.type_) | ||
yield ")" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
_mapping: dict[type, str] = { | ||
int: "Int", | ||
float: "Float", | ||
str: "String", | ||
bool: "Boolean", | ||
} | ||
|
||
|
||
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: | ||
try: | ||
return _mapping[python_class] | ||
except KeyError: | ||
raise KeyError( | ||
f"Type '{python_class.__name__}' could not be converted to a GraphQL type. " | ||
"See pygraphic.types.register_graphql_type" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from pathlib import Path | ||
|
||
from examples.parametrized_query.get_users_born_after import GetUsersBornAfter | ||
from examples.server import server_schema | ||
|
||
|
||
def test_query_string_generation(): | ||
expected = Path("golden_files", "query_parametrized.gql").read_text("utf-8") | ||
assert GetUsersBornAfter.get_query_string() == expected | ||
|
||
|
||
def test_local_query_execution(): | ||
query = GetUsersBornAfter.get_query_string() | ||
# variables = GetAllUsers.variables() | ||
result = server_schema.execute_sync(query) | ||
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) | ||
assert type(result.data) is dict | ||
result = GetUsersBornAfter.parse_obj(result.data) | ||
|
||
|
||
def test_example(): | ||
from examples.parametrized_query.main import born_after, result | ||
|
||
assert type(result) is GetUsersBornAfter | ||
assert all(user.birthday > born_after for user in result.users) | ||
assert all(all(friend.isOnline for friend in user.friends) for user in result.users) |
Oops, something went wrong.