- Implement tests for basic and bearer authentication headers in `test_auth.py`. - Create tests for the `EasybillWebhookParser` in `test_webhooks.py`, covering JSON and form-encoded payloads, as well as a generic parse and acknowledgement method.
218 lines
7.9 KiB
Python
218 lines
7.9 KiB
Python
"""
|
|
easybill REST API
|
|
|
|
The first version of the easybill REST API. [CHANGELOG](https://api.easybill.de/rest/v1/CHANGELOG.md) ## Authentication You can choose between two available methods: `Basic Auth` or `Bearer Token`. In each HTTP request, one of the following HTTP headers is required: ``` # Basic Auth Authorization: Basic base64_encode('<email>:<api_key>') # Bearer Token Authorization: Bearer <api_key> ``` ## Limitations ### Request Limit * PLUS: 10 requests per minute * BUSINESS: 60 requests per minute If the limit is exceeded, you will receive the HTTP error: `429 Too Many Requests` ### Result Limit All result lists are limited to 100 by default. This limit can be increased by the query parameter `limit` to a maximum of 1000. ## Query filter Many list resources can be filtered. In `/documents` you can filter e.g. by number with `/documents?number=111028654`. If you want to filter multiple numbers, you can either enter them separated by commas `/documents?number=111028654,222006895` or as an array `/documents?number[]=111028654&number[]=222006895`. **Warning**: The maximum size of an HTTP request line in bytes is 4094. If this limit is exceeded, you will receive the HTTP error: `414 Request-URI Too Large` ### Escape commas in query You can escape commans in query `name=Patrick\\, Peter` if you submit the header `X-Easybill-Escape: true` in your request. ## Property login_id This is the login of your admin or employee account. ## Date and Date-Time format Please use the timezone `Europe/Berlin`. * **date** = *Y-m-d* = `2016-12-31` * **date-time** = *Y-m-d H:i:s* = `2016-12-31 03:13:37` Date or datetime can be `null` because the attributes have been added later and the entry is older.
|
|
|
|
The version of the OpenAPI document: 1.96.0
|
|
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
|
|
Do not edit the class manually.
|
|
""" # noqa: E501
|
|
|
|
|
|
from typing import Any, Optional
|
|
from typing_extensions import Self
|
|
|
|
class OpenApiException(Exception):
|
|
"""The base exception class for all OpenAPIExceptions"""
|
|
|
|
|
|
class ApiTypeError(OpenApiException, TypeError):
|
|
def __init__(self, msg, path_to_item=None, valid_classes=None,
|
|
key_type=None) -> None:
|
|
""" Raises an exception for TypeErrors
|
|
|
|
Args:
|
|
msg (str): the exception message
|
|
|
|
Keyword Args:
|
|
path_to_item (list): a list of keys an indices to get to the
|
|
current_item
|
|
None if unset
|
|
valid_classes (tuple): the primitive classes that current item
|
|
should be an instance of
|
|
None if unset
|
|
key_type (bool): False if our value is a value in a dict
|
|
True if it is a key in a dict
|
|
False if our item is an item in a list
|
|
None if unset
|
|
"""
|
|
self.path_to_item = path_to_item
|
|
self.valid_classes = valid_classes
|
|
self.key_type = key_type
|
|
full_msg = msg
|
|
if path_to_item:
|
|
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
|
|
super(ApiTypeError, self).__init__(full_msg)
|
|
|
|
|
|
class ApiValueError(OpenApiException, ValueError):
|
|
def __init__(self, msg, path_to_item=None) -> None:
|
|
"""
|
|
Args:
|
|
msg (str): the exception message
|
|
|
|
Keyword Args:
|
|
path_to_item (list) the path to the exception in the
|
|
received_data dict. None if unset
|
|
"""
|
|
|
|
self.path_to_item = path_to_item
|
|
full_msg = msg
|
|
if path_to_item:
|
|
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
|
|
super(ApiValueError, self).__init__(full_msg)
|
|
|
|
|
|
class ApiAttributeError(OpenApiException, AttributeError):
|
|
def __init__(self, msg, path_to_item=None) -> None:
|
|
"""
|
|
Raised when an attribute reference or assignment fails.
|
|
|
|
Args:
|
|
msg (str): the exception message
|
|
|
|
Keyword Args:
|
|
path_to_item (None/list) the path to the exception in the
|
|
received_data dict
|
|
"""
|
|
self.path_to_item = path_to_item
|
|
full_msg = msg
|
|
if path_to_item:
|
|
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
|
|
super(ApiAttributeError, self).__init__(full_msg)
|
|
|
|
|
|
class ApiKeyError(OpenApiException, KeyError):
|
|
def __init__(self, msg, path_to_item=None) -> None:
|
|
"""
|
|
Args:
|
|
msg (str): the exception message
|
|
|
|
Keyword Args:
|
|
path_to_item (None/list) the path to the exception in the
|
|
received_data dict
|
|
"""
|
|
self.path_to_item = path_to_item
|
|
full_msg = msg
|
|
if path_to_item:
|
|
full_msg = "{0} at {1}".format(msg, render_path(path_to_item))
|
|
super(ApiKeyError, self).__init__(full_msg)
|
|
|
|
|
|
class ApiException(OpenApiException):
|
|
|
|
def __init__(
|
|
self,
|
|
status=None,
|
|
reason=None,
|
|
http_resp=None,
|
|
*,
|
|
body: Optional[str] = None,
|
|
data: Optional[Any] = None,
|
|
) -> None:
|
|
self.status = status
|
|
self.reason = reason
|
|
self.body = body
|
|
self.data = data
|
|
self.headers = None
|
|
|
|
if http_resp:
|
|
if self.status is None:
|
|
self.status = http_resp.status
|
|
if self.reason is None:
|
|
self.reason = http_resp.reason
|
|
if self.body is None:
|
|
try:
|
|
self.body = http_resp.data.decode('utf-8')
|
|
except Exception:
|
|
pass
|
|
self.headers = http_resp.headers
|
|
|
|
@classmethod
|
|
def from_response(
|
|
cls,
|
|
*,
|
|
http_resp,
|
|
body: Optional[str],
|
|
data: Optional[Any],
|
|
) -> Self:
|
|
if http_resp.status == 400:
|
|
raise BadRequestException(http_resp=http_resp, body=body, data=data)
|
|
|
|
if http_resp.status == 401:
|
|
raise UnauthorizedException(http_resp=http_resp, body=body, data=data)
|
|
|
|
if http_resp.status == 403:
|
|
raise ForbiddenException(http_resp=http_resp, body=body, data=data)
|
|
|
|
if http_resp.status == 404:
|
|
raise NotFoundException(http_resp=http_resp, body=body, data=data)
|
|
|
|
# Added new conditions for 409 and 422
|
|
if http_resp.status == 409:
|
|
raise ConflictException(http_resp=http_resp, body=body, data=data)
|
|
|
|
if http_resp.status == 422:
|
|
raise UnprocessableEntityException(http_resp=http_resp, body=body, data=data)
|
|
|
|
if 500 <= http_resp.status <= 599:
|
|
raise ServiceException(http_resp=http_resp, body=body, data=data)
|
|
raise ApiException(http_resp=http_resp, body=body, data=data)
|
|
|
|
def __str__(self):
|
|
"""Custom error messages for exception"""
|
|
error_message = "({0})\n"\
|
|
"Reason: {1}\n".format(self.status, self.reason)
|
|
if self.headers:
|
|
error_message += "HTTP response headers: {0}\n".format(
|
|
self.headers)
|
|
|
|
if self.body:
|
|
error_message += "HTTP response body: {0}\n".format(self.body)
|
|
|
|
if self.data:
|
|
error_message += "HTTP response data: {0}\n".format(self.data)
|
|
|
|
return error_message
|
|
|
|
|
|
class BadRequestException(ApiException):
|
|
pass
|
|
|
|
|
|
class NotFoundException(ApiException):
|
|
pass
|
|
|
|
|
|
class UnauthorizedException(ApiException):
|
|
pass
|
|
|
|
|
|
class ForbiddenException(ApiException):
|
|
pass
|
|
|
|
|
|
class ServiceException(ApiException):
|
|
pass
|
|
|
|
|
|
class ConflictException(ApiException):
|
|
"""Exception for HTTP 409 Conflict."""
|
|
pass
|
|
|
|
|
|
class UnprocessableEntityException(ApiException):
|
|
"""Exception for HTTP 422 Unprocessable Entity."""
|
|
pass
|
|
|
|
|
|
def render_path(path_to_item):
|
|
"""Returns a string representation of a path"""
|
|
result = ""
|
|
for pth in path_to_item:
|
|
if isinstance(pth, int):
|
|
result += "[{0}]".format(pth)
|
|
else:
|
|
result += "['{0}']".format(pth)
|
|
return result
|