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.
Files changed (32) hide show
  1. python_easyverein-0.1.0/LICENSE +7 -0
  2. python_easyverein-0.1.0/PKG-INFO +125 -0
  3. python_easyverein-0.1.0/README.md +109 -0
  4. python_easyverein-0.1.0/easyverein/__init__.py +8 -0
  5. python_easyverein-0.1.0/easyverein/api.py +43 -0
  6. python_easyverein-0.1.0/easyverein/core/__init__.py +0 -0
  7. python_easyverein-0.1.0/easyverein/core/client.py +314 -0
  8. python_easyverein-0.1.0/easyverein/core/exceptions.py +18 -0
  9. python_easyverein-0.1.0/easyverein/core/protocol.py +29 -0
  10. python_easyverein-0.1.0/easyverein/core/types.py +41 -0
  11. python_easyverein-0.1.0/easyverein/core/validators.py +14 -0
  12. python_easyverein-0.1.0/easyverein/models/__init__.py +18 -0
  13. python_easyverein-0.1.0/easyverein/models/base.py +17 -0
  14. python_easyverein-0.1.0/easyverein/models/contact_details.py +137 -0
  15. python_easyverein-0.1.0/easyverein/models/custom_field.py +93 -0
  16. python_easyverein-0.1.0/easyverein/models/invoice.py +77 -0
  17. python_easyverein-0.1.0/easyverein/models/invoice_item.py +56 -0
  18. python_easyverein-0.1.0/easyverein/models/member.py +119 -0
  19. python_easyverein-0.1.0/easyverein/models/member_custom_field.py +51 -0
  20. python_easyverein-0.1.0/easyverein/models/mixins/__init__.py +0 -0
  21. python_easyverein-0.1.0/easyverein/models/mixins/required_attributes.py +48 -0
  22. python_easyverein-0.1.0/easyverein/modules/__init__.py +0 -0
  23. python_easyverein-0.1.0/easyverein/modules/contact_details.py +20 -0
  24. python_easyverein-0.1.0/easyverein/modules/custom_field.py +21 -0
  25. python_easyverein-0.1.0/easyverein/modules/invoice.py +126 -0
  26. python_easyverein-0.1.0/easyverein/modules/invoice_item.py +16 -0
  27. python_easyverein-0.1.0/easyverein/modules/member.py +19 -0
  28. python_easyverein-0.1.0/easyverein/modules/member_custom_field.py +119 -0
  29. python_easyverein-0.1.0/easyverein/modules/mixins/__init__.py +0 -0
  30. python_easyverein-0.1.0/easyverein/modules/mixins/crud.py +168 -0
  31. python_easyverein-0.1.0/easyverein/modules/mixins/recycle_bin.py +48 -0
  32. 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
+ [![Documentation Status](https://readthedocs.org/projects/python-easyverein/badge/?version=latest)](https://python-easyverein.readthedocs.io/en/latest/?badge=latest)
17
+ [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
18
+ ![GitHub issues](https://img.shields.io/github/issues/waza-ari/python-easyverein)
19
+ ![GitHub release (latest by date)](https://img.shields.io/github/v/release/waza-ari/python-easyverein)
20
+ ![GitHub top language](https://img.shields.io/github/languages/top/waza-ari/python-easyverein)
21
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/waza-ari/python-easyverein/development.svg)](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
+ [![Documentation Status](https://readthedocs.org/projects/python-easyverein/badge/?version=latest)](https://python-easyverein.readthedocs.io/en/latest/?badge=latest)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
3
+ ![GitHub issues](https://img.shields.io/github/issues/waza-ari/python-easyverein)
4
+ ![GitHub release (latest by date)](https://img.shields.io/github/v/release/waza-ari/python-easyverein)
5
+ ![GitHub top language](https://img.shields.io/github/languages/top/waza-ari/python-easyverein)
6
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/waza-ari/python-easyverein/development.svg)](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,8 @@
1
+ """
2
+ Middleware for FastAPI that supports authenticating users against Keycloak
3
+ """
4
+
5
+ __version__ = "0.0.1"
6
+
7
+ # Export EasyVerein API directly
8
+ from .api import EasyvereinAPI # noqa: F401
@@ -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
+ ...