Skip to content

Commit

Permalink
Merge pull request #18 from tigattack/refactor/err_handling_etc
Browse files Browse the repository at this point in the history
Improve error handling, style, other tweaks
  • Loading branch information
tigattack authored Sep 17, 2024
2 parents 966f89f + f099241 commit cbc989d
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 175 deletions.
26 changes: 12 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
![PyPI - Python Versions](https://img.shields.io/pypi/pyversions/dvla-vehicle-enquiry-service?style=for-the-badge&logo=python&link=https%3A%2F%2Fpypi.org%2Fproject%2Fdvla-vehicle-enquiry-service)
![PyPI - Version](https://img.shields.io/pypi/v/dvla-vehicle-enquiry-service?style=for-the-badge&logo=python&link=https%3A%2F%2Fpypi.org%2Fproject%2Fdvla-vehicle-enquiry-service)

`dvla_vehicle_enquiry_service` is a Python SDK that provides a simple interface for interacting with the DVLA (Driver and Vehicle Licensing Agency) Vehicle Enquiry Service API. It allows retrieval of detailed vehicle information based on the registration number, including tax status, MOT status, and more.
`dvla_vehicle_enquiry_service` is a Python SDK providing a simple interface for interacting with the DVLA (Driver and Vehicle Licensing Agency) Vehicle Enquiry Service API. It allows retrieval of detailed vehicle information based on the registration number, including tax status, MOT status, and more.

## Installation

Expand Down Expand Up @@ -38,14 +38,12 @@ To fetch vehicle details using its registration number:

```python
import asyncio
from dvla_vehicle_enquiry_service import ErrorResponse, Vehicle
from dvla_vehicle_enquiry_service import VehicleResponse, VehicleEnquiryError

async def get_vehicle_details():
response = await client.get_vehicle("ABC123")
if isinstance(response, ErrorResponse):
print(f"Error: {response.errors[0].title}")
elif isinstance(response, Vehicle):
print(f"Vehicle Make: {response.make}, MOT Status: {response.motStatus.value}")
response = await api.get_vehicle("AA19MOT")
mot_status = response.motStatus.value if response.motStatus else "Unknown"
print(f"Vehicle Make: {response.make}, MOT Status: {mot_status}")

asyncio.run(get_vehicle_details())
```
Expand All @@ -54,24 +52,24 @@ If the request is successful, this example will print something such as: `Vehicl

### Error Handling

The SDK returns an `ErrorResponse` object when an error occurs, containing a list of `ErrorDetail` objects with specific error information.
Errors are raised as `VehicleEnquiryError` exceptions. You can catch and handle these exceptions to access detailed error information.

```python
if isinstance(response, ErrorResponse):
for error in response.errors:
print(f"Error {error.status}: {error.title} - {error.detail}")
try:
response = await client.get_vehicle("ER19NFD")
except VehicleEnquiryError as e:
print(f"Error {e.status}: {e.title} - {e.detail}")
```

## Classes and Data Structures

### Vehicle Class

- `Vehicle`: Represents detailed information about a vehicle, including tax status, MOT status, make, model, and various other attributes.
- `VehicleResponse`: Represents detailed information about a vehicle, including tax status, MOT status, make, model, and various other attributes.

### Error Handling Classes

- `ErrorResponse`: Encapsulates the error response returned by the API.
- `ErrorDetail`: Provides detailed information about individual errors.
- `VehicleEnquiryError`: Exception class that encapsulates error information, including status codes and error details.

### Enum Classes

Expand Down
11 changes: 6 additions & 5 deletions dvla_vehicle_enquiry_service/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

from .client import VehicleEnquiryAPI
from .enums import MotStatus, TaxStatus
from .models import ErrorResponse, Vehicle
from .errors import VehicleEnquiryError
from .models import VehicleResponse

__all__ = [
"VehicleEnquiryAPI",
"Vehicle",
"ErrorResponse",
"TaxStatus",
"MotStatus",
"TaxStatus",
"VehicleEnquiryAPI",
"VehicleEnquiryError",
"VehicleResponse",
]
76 changes: 41 additions & 35 deletions dvla_vehicle_enquiry_service/client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Client for DVLA Vehicle Enquiry Service API"""

from typing import Any, Literal, Optional, Union
from typing import Any, Literal, Optional

import aiohttp
from pydantic import ValidationError

from .api import BASE_URLS, VEHICLE_BY_REGISTRATION
from .models import ErrorDetail, ErrorResponse, Vehicle
from .errors import VehicleEnquiryError
from .models import VehicleResponse


class VehicleEnquiryAPI:
Expand All @@ -22,8 +23,8 @@ def __init__(

async def _make_request(
self, endpoint: str, data: dict[str, Any], correlation_id: Optional[str] = None
) -> Union[dict[str, Any], ErrorResponse]:
"""Makes a request to the API and returns the response."""
) -> dict[str, Any]:
"""Makes a request to the API and raises errors for any non-200 status."""
headers = {"x-api-key": self.api_key, "Content-Type": "application/json"}
if correlation_id:
headers["X-Correlation-Id"] = correlation_id
Expand All @@ -33,45 +34,50 @@ async def _make_request(
f"{self.base_url}{endpoint}", json=data, headers=headers
) as response:
response_json: dict[str, Any] = await response.json()

if response.status == 200:
return response_json
else:
if response_json.get("message"):
return ErrorResponse(
errors=[
ErrorDetail(
status=str(response.status),
title=response_json["message"],
)
]
)
elif response_json.get("errors"):
return ErrorResponse(
errors=[
ErrorDetail(**error)
for error in response_json.get("errors", [])
]
)
elif isinstance(response_json, list):
return ErrorResponse(
errors=[ErrorDetail(**error) for error in response_json]
)
raise ValueError(
f"Unexpected error response format (HTTP {response.status}): {response_json}"

if response_json.get("message"):
raise VehicleEnquiryError(
status=response.status,
title=response_json["message"],
)

if response_json.get("errors"):
raise VehicleEnquiryError(
status=response.status,
title="Multiple errors occurred during API request"
if len(response_json["errors"]) > 1
else "Error occurred during API request",
errors=[error for error in response_json.get("errors", [])],
)

raise VehicleEnquiryError(
status=response.status, title="Unknown error during API request"
)

async def get_vehicle(
self, registration_number: str, correlation_id: Optional[str] = None
) -> Union[Vehicle, ErrorResponse]:
"""Fetches vehicle details."""
) -> VehicleResponse:
"""Fetches vehicle details.
Args:
registration_number: The vehicle registration number
correlation_id: The correlation ID to include in the request headers
Returns:
VehicleResponse
Raises:
VehicleEnquiryError
"""
data = {"registrationNumber": registration_number}
response = await self._make_request(
VEHICLE_BY_REGISTRATION, data, correlation_id
)

if isinstance(response, dict):
try:
return Vehicle(**response)
except ValidationError as e:
raise ValueError(f"Invalid response format: {e}")
return response
try:
return VehicleResponse(**response)
except ValidationError as e:
raise VehicleEnquiryError(
title="Invalid response format", detail=str(e)
) from e
47 changes: 47 additions & 0 deletions dvla_vehicle_enquiry_service/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Errors for the DVLA Vehicle Enquiry Service API"""

from typing import Any, Optional


class VehicleEnquiryError(Exception):
"""Custom exception for vehicle enquiry errors, encapsulating error details."""

def __init__(
self,
title: str,
status: Optional[int] = None,
code: Optional[int] = None,
detail: Optional[str] = None,
errors: Optional[list[dict[str, Any]]] = None,
):
# Support both error dict and list[dict] of errors
if errors:
if len(errors) == 1:
# Handle single error presented as list
error = errors[0]
_status = error.get("status")
_code = error.get("code")
if isinstance(_status, str):
status = int(_status)
if isinstance(_code, str):
code = int(_code)

title = error.get("title") or title
detail = error.get("detail") or detail
super().__init__(f"[{error.get('status')}] {error.get('title')}: {error.get('detail') or 'No details'}")
else:
# Convert multiple error details into a string
error_messages = " ; ".join(
f"[{error.get('status')}] {error.get('title')}: {error.get('detail') or 'No details'}"
for error in errors
)
super().__init__(f"Multiple errors occurred: {error_messages}")
else:
# Handle a single error
super().__init__(f"[{status}] {title}: {detail or 'No details'}")

self.title = title
self.status = status
self.code = code
self.detail = detail
self.errors = errors or []
44 changes: 7 additions & 37 deletions dvla_vehicle_enquiry_service/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,14 @@
from datetime import date
from typing import Optional, Union

from pydantic import Field, field_validator
from pydantic import field_validator
from pydantic.dataclasses import dataclass

from .enums import MotStatus, TaxStatus


@dataclass
class ErrorDetail:
"""Represents an individual error detail returned by the API.
Attributes:
title: The error title.
status: The HTTP status code.
code: The error code.
detail: The error detail.
"""

title: str
status: Optional[str] = None
code: Optional[str] = None
detail: Optional[str] = None


@dataclass
class ErrorResponse:
"""Represents an error response from the API.
Attributes:
errors: A list of error details.
"""

errors: list[ErrorDetail] = Field(default_factory=list)


@dataclass
class Vehicle:
class VehicleResponse:
"""Represents a vehicle's details as retrieved from the DVLA Vehicle Enquiry Service API.
Attributes:
Expand Down Expand Up @@ -97,11 +69,9 @@ def parse_month(cls, value: Union[str, date]) -> Optional[date]:
if value:
if isinstance(value, date):
return value
else:
try:
year = int(value.split("-")[0])
month = int(value.split("-")[1])
return date(year, month, 1)
except ValueError:
raise ValueError(f"Invalid date format for 'YYYY-MM': {value}")
try:
year, month = map(int, value.split("-"))
return date(year, month, 1)
except ValueError:
raise ValueError(f"Invalid date format for 'YYYY-MM': {value}")
return None
Loading

0 comments on commit cbc989d

Please # to comment.