diff --git a/subgrounds/client.py b/subgrounds/client.py index bd211b9..f145a51 100644 --- a/subgrounds/client.py +++ b/subgrounds/client.py @@ -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") @@ -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( @@ -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 @@ -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 diff --git a/subgrounds/errors.py b/subgrounds/errors.py new file mode 100644 index 0000000..2e01cb8 --- /dev/null +++ b/subgrounds/errors.py @@ -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""" diff --git a/subgrounds/query.py b/subgrounds/query.py index 09362d1..3e36cd2 100644 --- a/subgrounds/query.py +++ b/subgrounds/query.py @@ -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, @@ -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, @@ -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, diff --git a/subgrounds/schema.py b/subgrounds/schema.py index de978d6..8aa7a7f 100644 --- a/subgrounds/schema.py +++ b/subgrounds/schema.py @@ -13,6 +13,8 @@ from pydantic import BaseModel as PydanticBaseModel from pydantic import Field, root_validator +from .errors import SchemaError + warnings.simplefilter("default") @@ -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}" ) diff --git a/subgrounds/transform.py b/subgrounds/transform.py index 1a3df40..781afb3 100644 --- a/subgrounds/transform.py +++ b/subgrounds/transform.py @@ -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") @@ -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 @@ -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 ( @@ -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: @@ -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 @@ -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))) ) @@ -352,7 +362,8 @@ 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 ( @@ -360,8 +371,8 @@ def transform(select: Selection, data: dict) -> 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 ( @@ -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: