Skip to content

Commit

Permalink
Add custom error handling for API errors (#205)
Browse files Browse the repository at this point in the history
* Crete base DCResponse

DCResponse just wraps around the api response

* NodeResponse

* ObservationResponse

* ResolveResponse

* SparqlResponse

* fix return

* consistent naming

* Update response.py

* tests for response utilities

* Move response scripts and tests

* Improve endpoint docstrings

* Implement custom errors

* Add tests for custom errors
Move file to match history

* isort

isort imports with google profile

* move tests to right folder

* correct module structure (imports)

* isort tests

* Fix name typo
  • Loading branch information
jm-rivera authored Jan 16, 2025
1 parent fa45dcb commit c52fada
Show file tree
Hide file tree
Showing 12 changed files with 189 additions and 37 deletions.
19 changes: 11 additions & 8 deletions datacommons_client/endpoints/response.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from dataclasses import asdict, dataclass, field
from dataclasses import asdict
from dataclasses import dataclass
from dataclasses import field
from typing import Any, Dict, List

from datacommons_client.models.node import Arcs, NextToken, NodeDCID, Properties
from datacommons_client.models.observation import (
Facet,
Variable,
facetID,
variableDCID,
)
from datacommons_client.models.node import Arcs
from datacommons_client.models.node import NextToken
from datacommons_client.models.node import NodeDCID
from datacommons_client.models.node import Properties
from datacommons_client.models.observation import Facet
from datacommons_client.models.observation import facetID
from datacommons_client.models.observation import Variable
from datacommons_client.models.observation import variableDCID
from datacommons_client.models.resolve import Entity
from datacommons_client.models.sparql import Row

Expand Down
3 changes: 2 additions & 1 deletion datacommons_client/models/node.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from dataclasses import dataclass
from dataclasses import field
from typing import Any, Dict, List, Optional, TypeAlias

NextToken: TypeAlias = Optional[str]
Expand Down
3 changes: 2 additions & 1 deletion datacommons_client/models/observation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from dataclasses import dataclass
from dataclasses import field
from typing import Any, Dict, TypeAlias

variableDCID: TypeAlias = str
Expand Down
3 changes: 2 additions & 1 deletion datacommons_client/models/resolve.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from dataclasses import dataclass
from dataclasses import field
from typing import Any, Dict, List, Optional, TypeAlias

Query: TypeAlias = str
Expand Down
3 changes: 2 additions & 1 deletion datacommons_client/models/sparql.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from dataclasses import dataclass
from dataclasses import field
from typing import Any, Dict, List


Expand Down
28 changes: 12 additions & 16 deletions datacommons_client/tests/endpoints/test_response.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
from datacommons_client.models.observation import (
Facet,
Observation,
OrderedFacets,
Variable,
)
from datacommons_client.endpoints.response import (
DCResponse,
NodeResponse,
ObservationResponse,
ResolveResponse,
SparqlResponse,
_unpack_arcs,
extract_observations,
flatten_properties,
)
from datacommons_client.endpoints.response import _unpack_arcs
from datacommons_client.endpoints.response import DCResponse
from datacommons_client.endpoints.response import extract_observations
from datacommons_client.endpoints.response import flatten_properties
from datacommons_client.endpoints.response import NodeResponse
from datacommons_client.endpoints.response import ObservationResponse
from datacommons_client.endpoints.response import ResolveResponse
from datacommons_client.endpoints.response import SparqlResponse
from datacommons_client.models.observation import Facet
from datacommons_client.models.observation import Observation
from datacommons_client.models.observation import OrderedFacets
from datacommons_client.models.observation import Variable

### ----- Test DCResponse ----- ###

Expand Down
5 changes: 4 additions & 1 deletion datacommons_client/tests/models/test_node_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from datacommons_client.models.node import Arcs, Node, NodeGroup, Properties
from datacommons_client.models.node import Arcs
from datacommons_client.models.node import Node
from datacommons_client.models.node import NodeGroup
from datacommons_client.models.node import Properties


def test_node_from_json():
Expand Down
10 changes: 4 additions & 6 deletions datacommons_client/tests/models/test_observation_models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from datacommons_client.models.observation import (
Facet,
Observation,
OrderedFacets,
Variable,
)
from datacommons_client.models.observation import Facet
from datacommons_client.models.observation import Observation
from datacommons_client.models.observation import OrderedFacets
from datacommons_client.models.observation import Variable


def test_observation_from_json():
Expand Down
3 changes: 2 additions & 1 deletion datacommons_client/tests/models/test_resolve_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datacommons_client.models.resolve import Candidate, Entity
from datacommons_client.models.resolve import Candidate
from datacommons_client.models.resolve import Entity


def test_candidate_from_json():
Expand Down
3 changes: 2 additions & 1 deletion datacommons_client/tests/models/test_sparql_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datacommons_client.models.sparql import Cell, Row
from datacommons_client.models.sparql import Cell
from datacommons_client.models.sparql import Row


def test_cell_from_json():
Expand Down
68 changes: 68 additions & 0 deletions datacommons_client/tests/utils/test_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from requests import Request
from requests import Response

from datacommons_client.utils.error_handling import APIError
from datacommons_client.utils.error_handling import DataCommonsError
from datacommons_client.utils.error_handling import DCAuthenticationError
from datacommons_client.utils.error_handling import DCConnectionError
from datacommons_client.utils.error_handling import DCStatusError
from datacommons_client.utils.error_handling import InvalidDCInstanceError


def test_data_commons_error_default_message():
"""Tests that DataCommonsError uses the default message."""
error = DataCommonsError()
assert str(error) == DataCommonsError.default_message


def test_data_commons_error_custom_message():
"""Tests that DataCommonsError uses a custom message when provided."""
error = DataCommonsError("Custom message")
assert str(error) == "Custom message"


def test_api_error_without_response():
"""Tests APIError initialization without a Response object."""
error = APIError()
assert str(error) == f"\n{APIError.default_message}"


def test_api_error_with_response():
"""Tests APIError initialization with a mocked Response object.
Verifies that the string representation includes status code,
request URL, and response text.
"""
mock_request = Request("GET", "http://example.com").prepare()
mock_response = Response()
mock_response.request = mock_request
mock_response.status_code = 404
mock_response._content = b"Not Found"

error = APIError(response=mock_response)
assert "Status Code: 404" in str(error)
assert "Request URL: http://example.com" in str(error)
assert "Not Found" in str(error)


def test_subclass_default_messages():
"""Tests that subclasses use their default messages."""
connection_error = DCConnectionError()
assert DCConnectionError.default_message in str(connection_error)

status_error = DCStatusError()
assert DCStatusError.default_message in str(status_error)

auth_error = DCAuthenticationError()
assert DCAuthenticationError.default_message in str(auth_error)

instance_error = InvalidDCInstanceError()
assert InvalidDCInstanceError.default_message in str(instance_error)


def test_subclass_custom_message():
"""Tests that subclasses use custom messages when provided."""
error = DCAuthenticationError(
response=Response(), message="Custom auth error"
)
assert str(error) == "\nCustom auth error"
78 changes: 78 additions & 0 deletions datacommons_client/utils/error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from typing import Optional

from requests import Response


class DataCommonsError(Exception):
"""Base exception for all Data Commons-related errors."""

default_message = "An error occurred getting data from Data Commons API."

def __init__(self, message: Optional[str] = None):
"""Initializes a DataCommonsError with a default or custom message."""
super().__init__(message or self.default_message)


class APIError(DataCommonsError):
"""Represents an error interacting with Data Commons API."""

default_message = "An API error occurred."

def __init__(
self,
response: Optional[Response] = None,
message: Optional[str] = None,
):
"""Initializes an APIError.
Args:
response (Optional[Response]): The response, if available.
message (Optional[str]): A descriptive error message.
"""
super().__init__(message or self.default_message)
self.response = response
self.request = getattr(response, "request", None)
self.status_code = getattr(response, "status_code", None)

def __str__(self) -> str:
"""Returns a detailed string representation of the error.
Returns:
str: A string describing the error, including the request URL if available.
"""

details = f"\n{self.args[0]}"
if self.status_code:
details += f"\nStatus Code: {self.status_code}"
if getattr(self.request, "url", None):
details += f"\nRequest URL: {self.request.url}"
if getattr(self.response, "text", None):
details += f"\nResponse: {self.response.text}"

return details


class DCConnectionError(APIError):
"""Raised for network-related errors in the Data Commons API."""

default_message = (
"A network error occurred while connecting to the Data Commons API."
)


class DCStatusError(APIError):
"""Raised for non-2xx HTTP status code errors in the Data Commons API."""

default_message = "The Data Commons API returned a non-2xx status code."


class DCAuthenticationError(APIError):
"""Raised for 401 Unauthorized errors in the Data Commons API."""

default_message = "Authentication failed. Please check your API key."


class InvalidDCInstanceError(DataCommonsError):
"""Raised when an invalid Data Commons instance is provided."""

default_message = "The specified Data Commons instance is invalid."

0 comments on commit c52fada

Please sign in to comment.