Source code for spantools._models
import uuid
import json
from dataclasses import dataclass, field, fields
from typing import Optional, Tuple, Mapping, Dict, Type, MutableMapping
from .errors_api import APIError, ERRORS_INDEXED
from ._errors import NoErrorReturnedError, InvalidAPIErrorCodeError
[docs]@dataclass
class Error:
"""Model for error information."""
name: str
"""Name of error class."""
message: str
"""Description of error."""
code: int
"""API error code. NOT the http code."""
data: Optional[dict] = None
"""Arbitrary data dict with information about the error."""
id: uuid.UUID = field(default_factory=uuid.uuid4)
"""UUID for specific instance of raised error for bug fixing."""
[docs] @classmethod
def from_exception(cls, exc: BaseException) -> Tuple["Error", APIError]:
"""
Creates Error object from mapping of response headers, and handles generation
of :class:`APIError` from Non-APIError exceptions.
:param exc: Instance of an BaseException
:return:
If ``exc`` does not inherit from APIError, a general APIError will be generated
and returned alongside the Error dataclass.
If ``exc`` does inherit from APIError, it will be passed through.
"""
if not isinstance(exc, APIError):
exc = APIError("An unknown error occurred.")
error_data = cls(
name=exc.__class__.__name__,
message=str(exc),
code=exc.api_code,
data=exc.error_data,
id=exc.id,
)
return error_data, exc
[docs] @classmethod
def from_headers(cls, headers: Mapping[str, str]) -> "Error":
"""
Creates Error object from mapping of response headers.
:param headers:
:return:
"""
data = headers.get("error-data", None)
if data is not None:
data_loaded = json.loads(data)
else:
data_loaded = None
try:
error_data = cls(
name=headers["error-name"],
message=headers["error-message"],
code=int(headers["error-code"]),
data=data_loaded,
id=uuid.UUID(headers["error-id"]),
)
except KeyError:
raise NoErrorReturnedError("No or incomplete error data found in headers.")
return error_data
[docs] def to_exception(
self, errors_additional: Optional[Dict[int, Type[APIError]]] = None
) -> APIError:
"""
Generate APIError object for given error information.
:param errors_additional: Additional custom APIError classes for possible raise.
:return: BaseException instance with message, .api_code, .error_data and
.error_id
:raises InvalidErrorCodeError: when error class with correct api_code not
supplied.
"""
if errors_additional is None:
errors_additional = dict()
errors_additional.update(ERRORS_INDEXED)
try:
error_class = errors_additional[self.code]
except KeyError:
raise InvalidAPIErrorCodeError(
f"Error class with code {self.code} not supplied."
)
error = error_class(self.message, error_data=self.data, error_id=self.id)
return error
[docs] def to_headers(self, headers: MutableMapping) -> None:
"""
Adds error info to response headers in-place.
:param headers:
:return:
All values are converted to strings before being added. Optional fields whose
values are None are skipped.
All header keys are prefixed with ``"error-"``.
"""
# Set the header error info
headers["error-name"] = self.name
headers["error-message"] = self.message
headers["error-id"] = str(self.id)
headers["error-code"] = str(self.code)
if self.data:
headers["error-data"] = json.dumps(self.data)
[docs]@dataclass
class PagingReq:
"""Paging info for requests."""
offset: int
"""Offset sent to params of request."""
limit: int
"""Limit sent to params of request."""
[docs] def to_params(self, params: MutableMapping[str, str]) -> None:
"""
Adds paging info to URL params for request.
:param params:
:return:
All values are converted to strings before being added.
All param names are prefixed with ``"paging-"`` and underscores are replaces
with hyphens.
"""
params["paging-offset"] = str(self.offset)
params["paging-limit"] = str(self.limit)
[docs] @classmethod
def from_params(
cls,
params: MutableMapping[str, str],
default_offset: Optional[int] = None,
default_limit: Optional[int] = None,
) -> "PagingReq":
"""
Creates PagingReq object from mapping of request url params.
:param params:
:param default_offset: offset to use if None is supplied
:param default_limit: limit to use if None is supplied
:return:
:raises KeyError: If offset or limit is not supplied and no default is given.
"""
try:
offset = int(params["paging-offset"])
except KeyError as error:
if default_offset is None:
raise error
else:
offset = default_offset
try:
limit = int(params["paging-limit"])
except KeyError as error:
if default_limit is None:
raise error
else:
limit = default_limit
return cls(offset=offset, limit=limit)
[docs]@dataclass
class PagingResp(PagingReq):
"""Paging info for responses."""
total_items: Optional[int]
"""Total number of returnable items."""
current_page: int
"""Current page index (starting at one)."""
previous: Optional[str]
"""Previous page url."""
next: Optional[str]
"""Next page url"""
total_pages: Optional[int]
"""Number of total pages."""
[docs] def to_headers(self, headers: MutableMapping[str, str]) -> None:
"""
Adds paging info to request headers in-place.
:param headers:
:return:
All values are converted to strings before being added. Optional fields whose
values are None are skipped.
All header keys are prefixed with ``"paging-"`` and underscores are replaces
with hyphens.
"""
for paging_field in fields(self):
value = getattr(self, paging_field.name)
key = f"paging-{paging_field.name.replace('_', '-')}"
if value is None:
continue
headers[key] = str(value)
[docs] @classmethod
def from_headers(cls, headers: Mapping[str, str]) -> "PagingResp":
"""
Creates PagingResp object from mapping of response headers.
:param headers:
:return:
"""
previous_url = headers.get("paging-previous")
next_url = headers.get("paging-next")
total_pages = headers.get("paging-total-pages")
if total_pages is not None:
total_pages_loaded: Optional[int] = int(total_pages)
else:
total_pages_loaded = None
total_items = headers.get("paging-total-items")
if total_items is not None:
total_items_loaded: Optional[int] = int(total_items)
else:
total_items_loaded = None
paging_data = cls(
previous=previous_url,
next=next_url,
current_page=int(headers["paging-current-page"]),
offset=int(headers["paging-offset"]),
limit=int(headers["paging-limit"]),
total_pages=total_pages_loaded,
total_items=total_items_loaded,
)
return paging_data