Source code for hmrc.api.client

"""HMRC client"""

from dataclasses import dataclass, fields
import functools
from typing import ClassVar, List
from urllib.parse import urljoin
from requests import HTTPError
from uritemplate import URITemplate
from ..auth.session import HmrcSession
from .data import HmrcDataClass, hmrcdataclass

__all__ = [
    'HmrcNoData',
    'HmrcErrorDetail',
    'HmrcErrorResponse',
    'HmrcClientError',
    'HmrcClient',
    'HmrcEndpoint',
]


[docs]@hmrcdataclass class HmrcNoData(HmrcDataClass): """Empty HMRC data class""" pass
[docs]@hmrcdataclass class HmrcErrorDetail(HmrcDataClass): """Error description""" code: str message: str path: str = None reactivation_timestamp: int = None
[docs]@hmrcdataclass class HmrcErrorResponse(HmrcErrorDetail): """Error response An error response comprises a top-level error description plus an optional list of contributory error descriptions. """ errors: List[HmrcErrorDetail] = None
[docs]class HmrcClientError(IOError): """HMRC API exception This is used only when the server returns a recognisable HMRC error response representation. """ def __str__(self): # pylint: disable=no-member error = self.error if error.errors is None: return error.message return '%s: %s' % (error.message, '/'.join(x.message for x in error.errors)) @property def error(self): """The HMRC error response""" # pylint: disable=unsubscriptable-object return self.args[0]
[docs]@dataclass class HmrcClient: """HMRC API client""" session: HmrcSession """Requests session""" scope: ClassVar[List[str]] = [] """Authorisation scopes""" REQUEST_CONTENT_TYPE = 'application/json' RESPONSE_CONTENT_TYPE = 'application/vnd.hmrc.1.0+json' def __post_init__(self): self.session.extend_scope(self.scope)
[docs] def request(self, uri, *, method='GET', query=None, body=None, scenario=None): """Send request""" # Create required headers headers = { 'Content-Type': self.REQUEST_CONTENT_TYPE, 'Accept': self.RESPONSE_CONTENT_TYPE, } # Add test scenario header, if applicable if scenario is not None: headers['Gov-Test-Scenario'] = scenario # Construct request rsp = self.session.request(method, urljoin(self.session.uri, uri), headers=headers, params=query, data=body) # Check for errors try: rsp.raise_for_status() except HTTPError as exc: # Add response body to exception message exc.args = tuple(tuple(exc.args) + (rsp.text,)) # Try to parse error response body try: error = HmrcErrorResponse.from_json(rsp.text) except Exception: raise exc from None # Raise chained exception raise HmrcClientError(error) from exc return rsp.text
[docs]@dataclass class HmrcEndpoint: """A callable API endpoint""" uri: str """Endpoint URI""" method: str = None """HTTP method""" path: type = HmrcNoData """URI path parameter type""" query: type = HmrcNoData """URI query parameter type""" request: type = HmrcNoData """Request body type""" response: type = HmrcNoData """Response body type""" def __post_init__(self): if self.method is None: self.method = 'GET' if self.request is HmrcNoData else 'POST' self.template = URITemplate(self.uri) self.path_args = {x.name for x in fields(self.path)} def __str__(self): return self.uri def __get__(self, instance, owner, partial=functools.partial): """Allow instances to function as callable methods on the instance""" if instance is None: return self return partial(self.__call__, instance) def __call__(self, client, *args, scenario=None, **kwargs): """Call endpoint""" # Extract any path parameter arguments and construct URI path_kwargs = {k: kwargs.pop(k, None) for k in self.path_args} path_kwargs = {k: v if v is not None else getattr(client, k) for k, v in path_kwargs.items()} path = self.path(**path_kwargs).to_hmrc() uri = self.template.expand(path) # Construct query parameters from remaining arguments query = self.query(**kwargs).to_hmrc() # Construct request body, if applicable if args: (data,) = args if not hasattr(data, 'to_json'): data = self.request(**data) req = data.to_json() else: req = None # Issue request rsp = client.request(uri, method=self.method, query=query, body=req, scenario=scenario) # Construct response return self.response.from_json(rsp)