diff --git a/datacommons_client/endpoints/response.py b/datacommons_client/endpoints/response.py index da78fc73..d10e6c9f 100644 --- a/datacommons_client/endpoints/response.py +++ b/datacommons_client/endpoints/response.py @@ -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 diff --git a/datacommons_client/models/node.py b/datacommons_client/models/node.py index cd964b87..5247ad32 100644 --- a/datacommons_client/models/node.py +++ b/datacommons_client/models/node.py @@ -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] diff --git a/datacommons_client/models/observation.py b/datacommons_client/models/observation.py index a0a86fd9..22819488 100644 --- a/datacommons_client/models/observation.py +++ b/datacommons_client/models/observation.py @@ -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 diff --git a/datacommons_client/models/resolve.py b/datacommons_client/models/resolve.py index 2b0ac665..6d20aacc 100644 --- a/datacommons_client/models/resolve.py +++ b/datacommons_client/models/resolve.py @@ -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 diff --git a/datacommons_client/models/sparql.py b/datacommons_client/models/sparql.py index 1d8fd6ec..63e22b39 100644 --- a/datacommons_client/models/sparql.py +++ b/datacommons_client/models/sparql.py @@ -1,4 +1,5 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass +from dataclasses import field from typing import Any, Dict, List diff --git a/datacommons_client/tests/endpoints/test_response.py b/datacommons_client/tests/endpoints/test_response.py index e71826fc..a30cc64b 100644 --- a/datacommons_client/tests/endpoints/test_response.py +++ b/datacommons_client/tests/endpoints/test_response.py @@ -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 ----- ### diff --git a/datacommons_client/tests/models/test_node_models.py b/datacommons_client/tests/models/test_node_models.py index 9b65862f..40b6d85d 100644 --- a/datacommons_client/tests/models/test_node_models.py +++ b/datacommons_client/tests/models/test_node_models.py @@ -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(): diff --git a/datacommons_client/tests/models/test_observation_models.py b/datacommons_client/tests/models/test_observation_models.py index e25ca2c4..94535d2f 100644 --- a/datacommons_client/tests/models/test_observation_models.py +++ b/datacommons_client/tests/models/test_observation_models.py @@ -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(): diff --git a/datacommons_client/tests/models/test_resolve_models.py b/datacommons_client/tests/models/test_resolve_models.py index 2887935a..44066781 100644 --- a/datacommons_client/tests/models/test_resolve_models.py +++ b/datacommons_client/tests/models/test_resolve_models.py @@ -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(): diff --git a/datacommons_client/tests/models/test_sparql_models.py b/datacommons_client/tests/models/test_sparql_models.py index 2f6e6a87..e9fa62a9 100644 --- a/datacommons_client/tests/models/test_sparql_models.py +++ b/datacommons_client/tests/models/test_sparql_models.py @@ -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(): diff --git a/datacommons_client/tests/utils/test_error_handling.py b/datacommons_client/tests/utils/test_error_handling.py new file mode 100644 index 00000000..4ce234a4 --- /dev/null +++ b/datacommons_client/tests/utils/test_error_handling.py @@ -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" diff --git a/datacommons_client/utils/error_handling.py b/datacommons_client/utils/error_handling.py new file mode 100644 index 00000000..f7246716 --- /dev/null +++ b/datacommons_client/utils/error_handling.py @@ -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."