Skip to content

Commit

Permalink
fix: better defined error models
Browse files Browse the repository at this point in the history
Also cleans up `client` error handling to be more descriptive.
  • Loading branch information
0xMochan authored Apr 7, 2023
2 parents 36f36cd + fa853d7 commit c121132
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 37 deletions.
52 changes: 41 additions & 11 deletions subgrounds/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

import requests

from subgrounds.utils import default_header
from .errors import GraphQLError, ServerError
from .utils import default_header

logger = logging.getLogger("subgrounds")

Expand Down Expand Up @@ -112,21 +113,36 @@ def get_schema(url: str, headers: dict[str, Any]) -> dict[str, Any]:
url (str): The url of the GraphQL API
Raises:
Exception: In case of GraphQL server error
HttpError: If the request response resulted in an error
ServerError: If server responds back non-json content
GraphQLError: If the GraphQL query failed or other grapql server errors
Returns:
dict[str, Any]: The GraphQL API's schema in JSON
"""

resp = requests.post(
url,
json={"query": INTROSPECTION_QUERY},
headers=default_header() | headers,
).json()
)

resp.raise_for_status()

try:
return resp["data"]
except KeyError as exn:
raise Exception(resp["errors"]) from exn
raw_data = resp.json()

except requests.JSONDecodeError:
raise ServerError(
f"Server ({url}) did not respond with proper JSON"
f"\nDid you query a proper GraphQL endpoint?"
f"\n\n{resp.content}"
)

if (data := raw_data.get("data")) is None:
raise GraphQLError(raw_data.get("errors", "Unknown Error(s) Found"))

return data


def query(
Expand All @@ -147,7 +163,9 @@ def query(
Defaults to {}.
Raises:
Exception: GraphQL error
HttpError: If the request response resulted in an error
ServerError: If server responds back non-json content
GraphQLError: If the GraphQL query failed or other grapql server errors
Returns:
dict[str, Any]: Response data
Expand All @@ -162,9 +180,21 @@ def query(
else {"query": query_str, "variables": variables}
),
headers=default_header() | headers,
).json()
)

resp.raise_for_status()

try:
return resp["data"]
except KeyError as exn:
raise Exception(resp["errors"]) from exn
raw_data = resp.json()

except requests.JSONDecodeError:
raise ServerError(
f"Server ({url}) did not respond with proper JSON"
f"\nDid you query a proper GraphQL endpoint?"
f"\n\n{resp.content}"
)

if (data := raw_data.get("data")) is None:
raise GraphQLError(raw_data.get("errors", "Unknown Error(s) Found"))

return data
18 changes: 18 additions & 0 deletions subgrounds/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class SubgroundsError(Exception):
"""The base error for all subgrounds errors"""


class SchemaError(SubgroundsError):
"""Errors related to schema"""


class TransformError(SubgroundsError):
"""Errors related to transforms"""


class ServerError(SubgroundsError):
"""Errors returned by a server"""


class GraphQLError(ServerError):
"""Errors returned by a GraphQL server"""
9 changes: 5 additions & 4 deletions subgrounds/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@

from pipe import map, take, traverse, where

from subgrounds.schema import SchemaMeta, TypeMeta, TypeRef
from subgrounds.utils import (
from .errors import SubgroundsError
from .schema import SchemaMeta, TypeMeta, TypeRef
from .utils import (
extract_data,
filter_map,
filter_none,
Expand Down Expand Up @@ -549,7 +550,7 @@ def map(
return map_f(new_selection)

case _:
raise Exception(f"map: invalid priority {priority}")
raise SubgroundsError(f"map: invalid priority {priority}")

def map_args(
self,
Expand Down Expand Up @@ -870,7 +871,7 @@ def vardef_of_arg(arg):

def combine(self: Selection, other: Selection) -> Selection:
if self.key != other.key:
raise Exception(f"Selection.combine: {self.key} != {other.key}")
raise SubgroundsError(f"Selection.combine: {self.key} != {other.key}")

return Selection(
fmeta=self.fmeta,
Expand Down
4 changes: 3 additions & 1 deletion subgrounds/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from pydantic import BaseModel as PydanticBaseModel
from pydantic import Field, root_validator

from .errors import SchemaError

warnings.simplefilter("default")


Expand Down Expand Up @@ -161,7 +163,7 @@ def type_of_arg(self: TypeMeta.FieldMeta, argname: str) -> TypeRef.T:
| map(lambda arg: arg.type_)
)
except StopIteration:
raise Exception(
raise SchemaError(
f"TypeMeta.FieldMeta.type_of_arg: no argument named {argname} for field {self.name}"
)

Expand Down
55 changes: 34 additions & 21 deletions subgrounds/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@

from pipe import map, traverse

from subgrounds.query import DataRequest, Document, Query, Selection
from subgrounds.schema import TypeMeta, TypeRef
from subgrounds.utils import flatten, union
from .errors import TransformError
from .query import DataRequest, Document, Query, Selection
from .schema import TypeMeta, TypeRef
from .utils import flatten, union

if TYPE_CHECKING:
from subgrounds.subgraph import Subgraph
from .subgraph import Subgraph

logger = logging.getLogger("subgrounds")

Expand All @@ -55,7 +56,9 @@ def select_data(select: Selection, data: dict) -> list[Any]:
)

case (select, data):
raise Exception(f"select_data: invalid selection {select} for data {data}")
raise TransformError(
f"select_data: invalid selection {select} for data {data}"
)

assert False # Suppress mypy missing return statement warning

Expand Down Expand Up @@ -163,7 +166,8 @@ def transform_document(self: TypeTransform, doc: Document) -> Document:

def transform_response(self, doc: Document, data: dict[str, Any]) -> dict[str, Any]:
def transform(select: Selection, data: dict[str, Any]) -> None:
# TODO: Handle NonNull and List more graciously (i.e.: without using TypeRef.root_type_name)
# TODO: Handle NonNull and List more graciously
# (i.e.: without using TypeRef.root_type_name)
match (select, data):
# Type matches
case (
Expand Down Expand Up @@ -211,13 +215,15 @@ def transform(select: Selection, data: dict[str, Any]) -> None:
case None:
return None
case _:
raise Exception(
f"transform_data_type: data for selection {select} is neither list or dict {data[name]}"
raise TransformError(
f"transform_data_type: data for selection {select} is"
f" neither list or dict {data[name]}"
)

case (select, data):
raise Exception(
f"transform_data_type: invalid selection {select} for data {data}"
raise TransformError(
f"transform_data_type: invalid selection {select}"
f" for data {data}"
)

for select in doc.query.selection:
Expand Down Expand Up @@ -292,7 +298,9 @@ def transform(select: Selection) -> Selection | list[Selection]:
new_inner_select = list(inner_select | map(transform) | traverse)
return Selection(select_fmeta, alias, args, new_inner_select)
case _:
raise Exception(f"transform_document: unhandled selection {select}")
raise TransformError(
f"transform_document: unhandled selection {select}"
)

assert False # Suppress mypy missing return statement warning

Expand Down Expand Up @@ -334,10 +342,12 @@ def transform(select: Selection, data: dict) -> None:
| Selection(TypeMeta.FieldMeta(), name, _, [] | None),
dict() as data,
) if name == self.fmeta.name and name not in data:
# Case where the selection selects a the syntheticfield of the curren transform
# that is not in the data blob and there are no inner selections
# Case where the selection selects a the syntheticfield of the
# current transform that is not in the data blob and there are
# no inner selections

# Try to grab the arguments to the synthetic field transform in the data blob
# Try to grab the arguments to the synthetic field transform in
# the data blob
arg_values = flatten(
list(self.args | map(partial(select_data, data=data)))
)
Expand All @@ -352,16 +362,17 @@ def transform(select: Selection, data: dict) -> None:
| Selection(TypeMeta.FieldMeta(), name, _, [] | None),
dict() as data,
) if name not in data:
# Case where the selection selects a regular field but it is not in the data blob (caused by None value at higher selection)
# Case where the selection selects a regular field but it is not in
# the data blob (caused by None value at higher selection)
data[name] = None

case (
Selection(TypeMeta.FieldMeta(name=name), None, _, [] | None)
| Selection(TypeMeta.FieldMeta(), name, _, [] | None),
dict() as data,
):
# Case where the selection selects a regular field and there are no inner selections
# (nothing to do)
# Case where the selection selects a regular field and there are
# no inner selections (nothing to do)
pass

case (
Expand All @@ -385,13 +396,15 @@ def transform(select: Selection, data: dict) -> None:
transform(select, data[name])

case _:
raise Exception(
f"transform_response: data for selection {select} is neither list or dict {data[name]}"
raise TransformError(
f"transform_response: data for selection {select} is"
f" neither list or dict {data[name]}"
)

case (select, data):
raise Exception(
f"transform_response: invalid selection {select} for data {data}"
raise TransformError(
f"transform_response: invalid selection {select}"
f" for data {data}"
)

def transform_on_type(select: Selection, data: dict) -> None:
Expand Down

0 comments on commit c121132

Please sign in to comment.