python-easyverein 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_easyverein-0.1.0/LICENSE +7 -0
- python_easyverein-0.1.0/PKG-INFO +125 -0
- python_easyverein-0.1.0/README.md +109 -0
- python_easyverein-0.1.0/easyverein/__init__.py +8 -0
- python_easyverein-0.1.0/easyverein/api.py +43 -0
- python_easyverein-0.1.0/easyverein/core/__init__.py +0 -0
- python_easyverein-0.1.0/easyverein/core/client.py +314 -0
- python_easyverein-0.1.0/easyverein/core/exceptions.py +18 -0
- python_easyverein-0.1.0/easyverein/core/protocol.py +29 -0
- python_easyverein-0.1.0/easyverein/core/types.py +41 -0
- python_easyverein-0.1.0/easyverein/core/validators.py +14 -0
- python_easyverein-0.1.0/easyverein/models/__init__.py +18 -0
- python_easyverein-0.1.0/easyverein/models/base.py +17 -0
- python_easyverein-0.1.0/easyverein/models/contact_details.py +137 -0
- python_easyverein-0.1.0/easyverein/models/custom_field.py +93 -0
- python_easyverein-0.1.0/easyverein/models/invoice.py +77 -0
- python_easyverein-0.1.0/easyverein/models/invoice_item.py +56 -0
- python_easyverein-0.1.0/easyverein/models/member.py +119 -0
- python_easyverein-0.1.0/easyverein/models/member_custom_field.py +51 -0
- python_easyverein-0.1.0/easyverein/models/mixins/__init__.py +0 -0
- python_easyverein-0.1.0/easyverein/models/mixins/required_attributes.py +48 -0
- python_easyverein-0.1.0/easyverein/modules/__init__.py +0 -0
- python_easyverein-0.1.0/easyverein/modules/contact_details.py +20 -0
- python_easyverein-0.1.0/easyverein/modules/custom_field.py +21 -0
- python_easyverein-0.1.0/easyverein/modules/invoice.py +126 -0
- python_easyverein-0.1.0/easyverein/modules/invoice_item.py +16 -0
- python_easyverein-0.1.0/easyverein/modules/member.py +19 -0
- python_easyverein-0.1.0/easyverein/modules/member_custom_field.py +119 -0
- python_easyverein-0.1.0/easyverein/modules/mixins/__init__.py +0 -0
- python_easyverein-0.1.0/easyverein/modules/mixins/crud.py +168 -0
- python_easyverein-0.1.0/easyverein/modules/mixins/recycle_bin.py +48 -0
- python_easyverein-0.1.0/pyproject.toml +52 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2023 Daniel Herrmann
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-easyverein
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library to interact with the EasyVerein API
|
|
5
|
+
Author: Daniel Herrmann
|
|
6
|
+
Author-email: daniel.herrmann1@gmail.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Requires-Dist: email-validator (>=2,<3)
|
|
12
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
13
|
+
Requires-Dist: requests (>=2,<3)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
[](https://python-easyverein.readthedocs.io/en/latest/?badge=latest)
|
|
17
|
+
[](https://opensource.org/licenses/MIT)
|
|
18
|
+

|
|
19
|
+

|
|
20
|
+

|
|
21
|
+
[](https://results.pre-commit.ci/latest/github/waza-ari/python-easyverein/development)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Python EasyVerein
|
|
25
|
+
|
|
26
|
+
**Full documentation** is [available at Read The Docs](https://python-easyverein.readthedocs.io/en/latest/)
|
|
27
|
+
|
|
28
|
+
This package contains an unofficial API client for [EasyVerein](http://easyverein.com) written in Python. Please note that this
|
|
29
|
+
library is unofficial and therefore not supported in any way by SD Software Design GmbH. If you have issues using this
|
|
30
|
+
library, please do not open a support request within EasyVerein but report it to our GitHub repository instead.
|
|
31
|
+
|
|
32
|
+
## State of the API
|
|
33
|
+
|
|
34
|
+
This client was written against and tested against the Hexa v1.7 API version of EasyVerein. It may or may not work
|
|
35
|
+
with newer / older API versions, so please use them at your own risk. As the EasyVerein API does not expose model
|
|
36
|
+
information, the models used as part of this library are specific to this library and are based on information obtained
|
|
37
|
+
from the API responses (e.g. required fields when creating an item).
|
|
38
|
+
|
|
39
|
+
In addition to the official endpoints, the client provides some convenience functions that are not included in the
|
|
40
|
+
official API (e.g. setting a custom field of a member to certain value, no matter if it has been set before or not
|
|
41
|
+
or create an invoice with items in one go) which makes it much simpler to work with the API.
|
|
42
|
+
|
|
43
|
+
Not all endpoints offered by the EasyVerein API are supported. For now, only the following endpoints are implemented.
|
|
44
|
+
When saying CRUD, it means the library supports various methods to **C**reate, **R**ead, **U**pdate and
|
|
45
|
+
**D**elete objects. See the API reference for details on supported CRUD operations.
|
|
46
|
+
|
|
47
|
+
* `contact-details`: CRUD, Soft-Delete
|
|
48
|
+
* `custom-fields`: CRUD, Soft-Delete
|
|
49
|
+
* `invoice`: CRUD, Soft-Delete, plus some convenience methods
|
|
50
|
+
* `invoice-item`: CRUD
|
|
51
|
+
* `member`: CRUD, Soft-Delete
|
|
52
|
+
* `member/<id>/custom-fields`: CRUD, plus some convenience methods
|
|
53
|
+
* `wastebasket` (its the official name used by the EasyVerein API to reference soft-deleted objects)
|
|
54
|
+
|
|
55
|
+
In addition to that, the library supports nested queries using the query syntax, included nested model validation.
|
|
56
|
+
See the Usage section of this documentation for more details.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
Install the package using `poetry`:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
poetry add python-easyverein
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
or `pip`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install python-easyverein
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Getting Started
|
|
73
|
+
|
|
74
|
+
This simple example shows how to setup the library and retrieve all invoices:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import os
|
|
78
|
+
from easyverein import EasyvereinAPI
|
|
79
|
+
|
|
80
|
+
api_key = os.getenv('EV_API_KEY', '')
|
|
81
|
+
|
|
82
|
+
ev_client = EasyvereinAPI(api_key)
|
|
83
|
+
|
|
84
|
+
print(ev_client.invoice.get())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The result will be a list of invoice objects. All returned objects are [Pydantic](https://pydantic.dev) models under
|
|
88
|
+
the hood, so you get auto-completion and a guaranteed interface for these models. For details please refer to the usage
|
|
89
|
+
section of this documentation.
|
|
90
|
+
|
|
91
|
+
## Tests
|
|
92
|
+
|
|
93
|
+
All features of this client are automatically tested against the actual API using pytest. If you want to run the tests
|
|
94
|
+
yourself, it is advisable to create a separate demo account for that. Then, set the following environment variable to
|
|
95
|
+
your API token and simply run `pytest`:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
EV_API_KEY=<your-api-key>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
The client is written in pure Python, using `mkdocs` with `mkdocstrings` for documentation. Any changes or
|
|
104
|
+
pull requests are more than welcome, but please adhere to the code style:
|
|
105
|
+
|
|
106
|
+
- Use `black` for code formatting
|
|
107
|
+
- Use `isort` based import sorting
|
|
108
|
+
- Use `ruff` based code linting
|
|
109
|
+
|
|
110
|
+
A pre-commit hook configuration is supplied as part of the project.
|
|
111
|
+
|
|
112
|
+
Please make sure that any additions are properly tested. PRs won't get accepted if they don't have test cases to
|
|
113
|
+
cover them.
|
|
114
|
+
|
|
115
|
+
## Getting Help
|
|
116
|
+
|
|
117
|
+
Once more, this library is not officially supported by SD Software Design GmbH, the company behind EasyVerein.
|
|
118
|
+
If you have troubles, please ask yourself the following questions:
|
|
119
|
+
|
|
120
|
+
- Is your problem around API usage in general, which endpoint to call, which fields to set to achieve a certain thing?
|
|
121
|
+
Do you have questions around attribute naming, usage, or why the API behaves a certain way? Those questions should be
|
|
122
|
+
directed towards their support, as I cannot help with them.
|
|
123
|
+
- is your question around how to use this library, a mistake in the Pydantic models it's using, an error in the library
|
|
124
|
+
code or other questions around this library? Please open a GitHub issue for those questions.
|
|
125
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
[](https://python-easyverein.readthedocs.io/en/latest/?badge=latest)
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
[](https://results.pre-commit.ci/latest/github/waza-ari/python-easyverein/development)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Python EasyVerein
|
|
10
|
+
|
|
11
|
+
**Full documentation** is [available at Read The Docs](https://python-easyverein.readthedocs.io/en/latest/)
|
|
12
|
+
|
|
13
|
+
This package contains an unofficial API client for [EasyVerein](http://easyverein.com) written in Python. Please note that this
|
|
14
|
+
library is unofficial and therefore not supported in any way by SD Software Design GmbH. If you have issues using this
|
|
15
|
+
library, please do not open a support request within EasyVerein but report it to our GitHub repository instead.
|
|
16
|
+
|
|
17
|
+
## State of the API
|
|
18
|
+
|
|
19
|
+
This client was written against and tested against the Hexa v1.7 API version of EasyVerein. It may or may not work
|
|
20
|
+
with newer / older API versions, so please use them at your own risk. As the EasyVerein API does not expose model
|
|
21
|
+
information, the models used as part of this library are specific to this library and are based on information obtained
|
|
22
|
+
from the API responses (e.g. required fields when creating an item).
|
|
23
|
+
|
|
24
|
+
In addition to the official endpoints, the client provides some convenience functions that are not included in the
|
|
25
|
+
official API (e.g. setting a custom field of a member to certain value, no matter if it has been set before or not
|
|
26
|
+
or create an invoice with items in one go) which makes it much simpler to work with the API.
|
|
27
|
+
|
|
28
|
+
Not all endpoints offered by the EasyVerein API are supported. For now, only the following endpoints are implemented.
|
|
29
|
+
When saying CRUD, it means the library supports various methods to **C**reate, **R**ead, **U**pdate and
|
|
30
|
+
**D**elete objects. See the API reference for details on supported CRUD operations.
|
|
31
|
+
|
|
32
|
+
* `contact-details`: CRUD, Soft-Delete
|
|
33
|
+
* `custom-fields`: CRUD, Soft-Delete
|
|
34
|
+
* `invoice`: CRUD, Soft-Delete, plus some convenience methods
|
|
35
|
+
* `invoice-item`: CRUD
|
|
36
|
+
* `member`: CRUD, Soft-Delete
|
|
37
|
+
* `member/<id>/custom-fields`: CRUD, plus some convenience methods
|
|
38
|
+
* `wastebasket` (its the official name used by the EasyVerein API to reference soft-deleted objects)
|
|
39
|
+
|
|
40
|
+
In addition to that, the library supports nested queries using the query syntax, included nested model validation.
|
|
41
|
+
See the Usage section of this documentation for more details.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
Install the package using `poetry`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
poetry add python-easyverein
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
or `pip`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install python-easyverein
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Getting Started
|
|
58
|
+
|
|
59
|
+
This simple example shows how to setup the library and retrieve all invoices:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import os
|
|
63
|
+
from easyverein import EasyvereinAPI
|
|
64
|
+
|
|
65
|
+
api_key = os.getenv('EV_API_KEY', '')
|
|
66
|
+
|
|
67
|
+
ev_client = EasyvereinAPI(api_key)
|
|
68
|
+
|
|
69
|
+
print(ev_client.invoice.get())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The result will be a list of invoice objects. All returned objects are [Pydantic](https://pydantic.dev) models under
|
|
73
|
+
the hood, so you get auto-completion and a guaranteed interface for these models. For details please refer to the usage
|
|
74
|
+
section of this documentation.
|
|
75
|
+
|
|
76
|
+
## Tests
|
|
77
|
+
|
|
78
|
+
All features of this client are automatically tested against the actual API using pytest. If you want to run the tests
|
|
79
|
+
yourself, it is advisable to create a separate demo account for that. Then, set the following environment variable to
|
|
80
|
+
your API token and simply run `pytest`:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
EV_API_KEY=<your-api-key>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Contributing
|
|
87
|
+
|
|
88
|
+
The client is written in pure Python, using `mkdocs` with `mkdocstrings` for documentation. Any changes or
|
|
89
|
+
pull requests are more than welcome, but please adhere to the code style:
|
|
90
|
+
|
|
91
|
+
- Use `black` for code formatting
|
|
92
|
+
- Use `isort` based import sorting
|
|
93
|
+
- Use `ruff` based code linting
|
|
94
|
+
|
|
95
|
+
A pre-commit hook configuration is supplied as part of the project.
|
|
96
|
+
|
|
97
|
+
Please make sure that any additions are properly tested. PRs won't get accepted if they don't have test cases to
|
|
98
|
+
cover them.
|
|
99
|
+
|
|
100
|
+
## Getting Help
|
|
101
|
+
|
|
102
|
+
Once more, this library is not officially supported by SD Software Design GmbH, the company behind EasyVerein.
|
|
103
|
+
If you have troubles, please ask yourself the following questions:
|
|
104
|
+
|
|
105
|
+
- Is your problem around API usage in general, which endpoint to call, which fields to set to achieve a certain thing?
|
|
106
|
+
Do you have questions around attribute naming, usage, or why the API behaves a certain way? Those questions should be
|
|
107
|
+
directed towards their support, as I cannot help with them.
|
|
108
|
+
- is your question around how to use this library, a mistake in the Pydantic models it's using, an error in the library
|
|
109
|
+
code or other questions around this library? Please open a GitHub issue for those questions.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main EasyVerein API class
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from .core.client import EasyvereinClient
|
|
7
|
+
from .modules.contact_details import ContactDetailsMixin
|
|
8
|
+
from .modules.custom_field import CustomFieldMixin
|
|
9
|
+
from .modules.invoice import InvoiceMixin
|
|
10
|
+
from .modules.invoice_item import InvoiceItemMixin
|
|
11
|
+
from .modules.member import MemberMixin
|
|
12
|
+
from .modules.member_custom_field import MemberCustomFieldMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EasyvereinAPI:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
api_key,
|
|
19
|
+
api_version="v1.7",
|
|
20
|
+
base_url: str = "https://hexa.easyverein.com/api/",
|
|
21
|
+
logger: logging.Logger = None,
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Constructor setting API key and logger. Test
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
super().__init__()
|
|
28
|
+
|
|
29
|
+
if logger:
|
|
30
|
+
self.logger = logger
|
|
31
|
+
else:
|
|
32
|
+
self.logger = logging.getLogger("easyverein")
|
|
33
|
+
|
|
34
|
+
self.c = EasyvereinClient(api_key, api_version, base_url, self.logger, self)
|
|
35
|
+
|
|
36
|
+
# Add methods
|
|
37
|
+
|
|
38
|
+
self.contact_details = ContactDetailsMixin(self.c, self.logger)
|
|
39
|
+
self.custom_field = CustomFieldMixin(self.c, self.logger)
|
|
40
|
+
self.invoice = InvoiceMixin(self.c, self.logger)
|
|
41
|
+
self.invoice_item = InvoiceItemMixin(self.c, self.logger)
|
|
42
|
+
self.member = MemberMixin(self.c, self.logger)
|
|
43
|
+
self.member_custom_field = MemberCustomFieldMixin(self.c, self.logger)
|
|
File without changes
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main EasyVerein API class
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from .exceptions import EasyvereinAPIException, EasyvereinAPITooManyRetriesException
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .. import EasyvereinAPI
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EasyvereinClient:
|
|
22
|
+
"""
|
|
23
|
+
Class encapsulating common function used by all API methods
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
api_key,
|
|
29
|
+
api_version,
|
|
30
|
+
base_url,
|
|
31
|
+
logger: logging.Logger,
|
|
32
|
+
instance: EasyvereinAPI,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Constructor setting API key and logger
|
|
36
|
+
"""
|
|
37
|
+
self.api_key = api_key
|
|
38
|
+
self.base_url = base_url
|
|
39
|
+
self.api_version = api_version
|
|
40
|
+
self.logger = logger
|
|
41
|
+
self.api_instance = instance
|
|
42
|
+
|
|
43
|
+
def _get_header(self):
|
|
44
|
+
"""
|
|
45
|
+
Constructs a header for the API request
|
|
46
|
+
"""
|
|
47
|
+
return {"Authorization": "Bearer " + self.api_key}
|
|
48
|
+
|
|
49
|
+
def get_url(self, path: str, url_params: dict = None) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Constructs a URL for the API request.
|
|
52
|
+
|
|
53
|
+
:param path: Base path of the request
|
|
54
|
+
:param url_params: additional path parameters to append
|
|
55
|
+
"""
|
|
56
|
+
url = f"{self.base_url}{self.api_version}{path}"
|
|
57
|
+
|
|
58
|
+
self.logger.debug(f"Base URL is {url}")
|
|
59
|
+
|
|
60
|
+
if url_params:
|
|
61
|
+
for key, value in url_params.items():
|
|
62
|
+
if not value:
|
|
63
|
+
continue
|
|
64
|
+
self.logger.debug(f"Adding {key}={value} path parameter to URL")
|
|
65
|
+
if "?" not in url:
|
|
66
|
+
url += f"?{key}={value}"
|
|
67
|
+
else:
|
|
68
|
+
url += f"&{key}={value}"
|
|
69
|
+
|
|
70
|
+
self.logger.debug(f"Final constructed URL is {url}")
|
|
71
|
+
|
|
72
|
+
return url
|
|
73
|
+
|
|
74
|
+
def _do_request( # noqa: PLR0913
|
|
75
|
+
self, method, url, data=None, headers=None, files=None
|
|
76
|
+
):
|
|
77
|
+
"""
|
|
78
|
+
Helper method that performs an actual call against the API,
|
|
79
|
+
fetching the most common errors
|
|
80
|
+
"""
|
|
81
|
+
self.logger.debug("Performing %s request to %s", method, url)
|
|
82
|
+
if data:
|
|
83
|
+
self.logger.debug("Request data: %s", data)
|
|
84
|
+
if headers:
|
|
85
|
+
self.logger.debug("Provided request headers: %s", headers)
|
|
86
|
+
|
|
87
|
+
# Merge auth header with custom headers
|
|
88
|
+
final_headers = self._get_header() | (headers or {})
|
|
89
|
+
|
|
90
|
+
self.logger.debug("Final request headers: %s", final_headers)
|
|
91
|
+
|
|
92
|
+
func = getattr(requests, method)
|
|
93
|
+
if data:
|
|
94
|
+
res = func(url, headers=final_headers, json=data, files=files or {})
|
|
95
|
+
else:
|
|
96
|
+
res = func(url, headers=final_headers, files=files)
|
|
97
|
+
|
|
98
|
+
self.logger.debug("Request returned status code %d", res.status_code)
|
|
99
|
+
|
|
100
|
+
if res.status_code == 429:
|
|
101
|
+
retry_after = res.headers("Retry-After")
|
|
102
|
+
self.logger.warning(
|
|
103
|
+
"Request returned status code 429, too many requests. Wait %d seconds",
|
|
104
|
+
retry_after,
|
|
105
|
+
)
|
|
106
|
+
raise EasyvereinAPITooManyRetriesException(
|
|
107
|
+
retry_after,
|
|
108
|
+
f"Too many requests, please wait {retry_after} seconds and try again.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if res.status_code == 404:
|
|
112
|
+
self.logger.warning("Request returned status code 404, resource not found")
|
|
113
|
+
raise EasyvereinAPIException("Requested resource not found")
|
|
114
|
+
|
|
115
|
+
# In some cases (for example on 204 delete) the response is empty
|
|
116
|
+
if res.content == b"":
|
|
117
|
+
return res.status_code, None
|
|
118
|
+
|
|
119
|
+
# Try to parse response as JSON and return it for further processing
|
|
120
|
+
try:
|
|
121
|
+
content = res.json()
|
|
122
|
+
except ValueError:
|
|
123
|
+
self.logger.error("Unable to parse response content as JSON")
|
|
124
|
+
self.logger.debug("Response content: %s", res.content)
|
|
125
|
+
content = None
|
|
126
|
+
|
|
127
|
+
return res.status_code, content
|
|
128
|
+
|
|
129
|
+
def create(
|
|
130
|
+
self,
|
|
131
|
+
url,
|
|
132
|
+
data: BaseModel = None,
|
|
133
|
+
return_model: type[T] = None,
|
|
134
|
+
status_code: int = 201,
|
|
135
|
+
) -> T:
|
|
136
|
+
"""
|
|
137
|
+
Method to create an object in the API
|
|
138
|
+
"""
|
|
139
|
+
return self._handle_response(
|
|
140
|
+
self._do_request(
|
|
141
|
+
"post",
|
|
142
|
+
url,
|
|
143
|
+
data=data.model_dump(
|
|
144
|
+
exclude_none=True, exclude_unset=True, by_alias=True
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
return_model,
|
|
148
|
+
status_code,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def delete(self, url, status_code: int = 204):
|
|
152
|
+
"""
|
|
153
|
+
Method to delete an object in the API
|
|
154
|
+
"""
|
|
155
|
+
return self._handle_response(
|
|
156
|
+
self._do_request("delete", url), expected_status_code=status_code
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def update(
|
|
160
|
+
self, url, data: BaseModel = None, model: type[T] = None, status_code: int = 200
|
|
161
|
+
) -> T:
|
|
162
|
+
"""
|
|
163
|
+
Method to update an object in the API
|
|
164
|
+
"""
|
|
165
|
+
return self._handle_response(
|
|
166
|
+
self._do_request(
|
|
167
|
+
"patch",
|
|
168
|
+
url,
|
|
169
|
+
data=data.model_dump(
|
|
170
|
+
exclude_none=True, exclude_unset=True, by_alias=True
|
|
171
|
+
),
|
|
172
|
+
),
|
|
173
|
+
model,
|
|
174
|
+
expected_status_code=status_code,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def upload(
|
|
178
|
+
self,
|
|
179
|
+
url: str,
|
|
180
|
+
field_name: str,
|
|
181
|
+
file: Path,
|
|
182
|
+
model: type[T] = None,
|
|
183
|
+
status_code: int = 200,
|
|
184
|
+
) -> T:
|
|
185
|
+
"""
|
|
186
|
+
This method uploads a file to a certain endpoint.
|
|
187
|
+
|
|
188
|
+
Only tested with invoices so far
|
|
189
|
+
"""
|
|
190
|
+
# Check that path is a file and it exists
|
|
191
|
+
if not file.exists() or not file.is_file():
|
|
192
|
+
self.logger.error("File does not exist or is not a file.")
|
|
193
|
+
raise FileNotFoundError("File does not exist")
|
|
194
|
+
|
|
195
|
+
files = {field_name: open(file, "rb")}
|
|
196
|
+
headers = {"Content-Disposition": f'name="file"; filename="{file.name}"'}
|
|
197
|
+
|
|
198
|
+
return self._handle_response(
|
|
199
|
+
self._do_request(
|
|
200
|
+
"patch",
|
|
201
|
+
url,
|
|
202
|
+
headers=headers,
|
|
203
|
+
files=files,
|
|
204
|
+
),
|
|
205
|
+
model,
|
|
206
|
+
status_code,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def fetch(self, url, model: type[T] = None) -> list[T]:
|
|
210
|
+
"""
|
|
211
|
+
Helper method that fetches a result from an API call
|
|
212
|
+
|
|
213
|
+
Only supports GET endpoints
|
|
214
|
+
"""
|
|
215
|
+
res = self._do_request("get", url)
|
|
216
|
+
return self._handle_response(res, model, 200)
|
|
217
|
+
|
|
218
|
+
def fetch_one(self, url, model: type[T] = None) -> T | None:
|
|
219
|
+
"""
|
|
220
|
+
Helper method that fetches a result from an API call
|
|
221
|
+
|
|
222
|
+
Only supports GET endpoints
|
|
223
|
+
"""
|
|
224
|
+
reply = self.fetch(url, model)
|
|
225
|
+
if isinstance(reply, list):
|
|
226
|
+
if len(reply) == 0:
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
self.logger.warning(
|
|
230
|
+
"One object was requested, but multiple objects were returned. Returning first."
|
|
231
|
+
)
|
|
232
|
+
self.logger.debug(f"In total {len(reply)} objects where returned.")
|
|
233
|
+
return reply[0]
|
|
234
|
+
|
|
235
|
+
return reply
|
|
236
|
+
|
|
237
|
+
def fetch_paginated(self, url, model: type[T] = None, limit=100) -> list[T]:
|
|
238
|
+
"""
|
|
239
|
+
Helper method that fetches all pages of a paginated API call
|
|
240
|
+
|
|
241
|
+
Only supports GET endpoints
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
self.logger.debug("Fetching paginated API call %s, limit is %d", url, limit)
|
|
245
|
+
|
|
246
|
+
# Add limit parameter to URL
|
|
247
|
+
if "?" not in url:
|
|
248
|
+
url += f"?limit={limit}"
|
|
249
|
+
else:
|
|
250
|
+
url += f"&limit={limit}"
|
|
251
|
+
|
|
252
|
+
resources = []
|
|
253
|
+
status_code: int = 0
|
|
254
|
+
|
|
255
|
+
while url is not None:
|
|
256
|
+
self.logger.debug("Fetching page of paginated API call %s", url)
|
|
257
|
+
|
|
258
|
+
status_code, result = self._do_request("get", url)
|
|
259
|
+
self.logger.debug("Request returned status code %d", status_code)
|
|
260
|
+
|
|
261
|
+
if not status_code == 200:
|
|
262
|
+
self.logger.error(
|
|
263
|
+
"Could not fetch paginated API %s, status code %d", url, status_code
|
|
264
|
+
)
|
|
265
|
+
self.logger.debug("API response: %s", result)
|
|
266
|
+
raise EasyvereinAPIException(
|
|
267
|
+
f"Could not fetch paginated API {url}, "
|
|
268
|
+
f"status code {status_code}. API response: {result}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
resources.extend(result["results"])
|
|
272
|
+
url = result["next"]
|
|
273
|
+
|
|
274
|
+
return self._handle_response((status_code, resources), model, 200)
|
|
275
|
+
|
|
276
|
+
def _handle_response(
|
|
277
|
+
self,
|
|
278
|
+
res: tuple[int, list | dict],
|
|
279
|
+
model: type[T] = None,
|
|
280
|
+
expected_status_code=200,
|
|
281
|
+
) -> T | list[T]:
|
|
282
|
+
"""
|
|
283
|
+
Helper method that handles API responses
|
|
284
|
+
"""
|
|
285
|
+
status_code, data = res
|
|
286
|
+
if status_code != expected_status_code:
|
|
287
|
+
raise EasyvereinAPIException(
|
|
288
|
+
f"API returned status code {status_code}. API response: {data}"
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
self.logger.debug("API returned status code %d", status_code)
|
|
292
|
+
|
|
293
|
+
# if no data is expected return raw data (usually None)
|
|
294
|
+
if not model:
|
|
295
|
+
self.logger.debug("No model provided. Returning raw data: %s", data)
|
|
296
|
+
return data
|
|
297
|
+
|
|
298
|
+
self.logger.debug("Received raw data: %s", data)
|
|
299
|
+
|
|
300
|
+
# if data is a list, parse each entry
|
|
301
|
+
# fetch_paginated returns a list of result entries instead of raw data, this is why this case is here.
|
|
302
|
+
if isinstance(data, list):
|
|
303
|
+
objects = []
|
|
304
|
+
for obj in data:
|
|
305
|
+
objects.append(model.model_validate(obj))
|
|
306
|
+
elif isinstance(data, dict) and "results" in data:
|
|
307
|
+
objects = []
|
|
308
|
+
for obj in data["results"]:
|
|
309
|
+
objects.append(model.model_validate(obj))
|
|
310
|
+
else:
|
|
311
|
+
# Handle the case when data is not a list
|
|
312
|
+
objects = model.model_validate(data)
|
|
313
|
+
|
|
314
|
+
return objects
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains exceptions used by the library.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EasyvereinAPIException(Exception):
|
|
7
|
+
"""
|
|
8
|
+
Exception describing an error that occurred while interacting
|
|
9
|
+
with the easyVerein API
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EasyvereinAPITooManyRetriesException(EasyvereinAPIException):
|
|
14
|
+
"""
|
|
15
|
+
Exception if the API returns a 429 Too Many Requests error
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
retry_after = 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Protocol, Type, TypeVar
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from .client import EasyvereinClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# noinspection PyPropertyDefinition
|
|
10
|
+
class IsEVClientProtocol(Protocol):
|
|
11
|
+
@property
|
|
12
|
+
def logger(self) -> logging.Logger:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def c(self) -> EasyvereinClient:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def endpoint_name(self) -> str:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def model_class(self) -> TypeVar:
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def return_type(self) -> Type[BaseModel]:
|
|
29
|
+
...
|