python-easyverein 0.1.0__py3-none-any.whl
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.
- easyverein/__init__.py +8 -0
- easyverein/api.py +43 -0
- easyverein/core/__init__.py +0 -0
- easyverein/core/client.py +314 -0
- easyverein/core/exceptions.py +18 -0
- easyverein/core/protocol.py +29 -0
- easyverein/core/types.py +41 -0
- easyverein/core/validators.py +14 -0
- easyverein/models/__init__.py +18 -0
- easyverein/models/base.py +17 -0
- easyverein/models/contact_details.py +137 -0
- easyverein/models/custom_field.py +93 -0
- easyverein/models/invoice.py +77 -0
- easyverein/models/invoice_item.py +56 -0
- easyverein/models/member.py +119 -0
- easyverein/models/member_custom_field.py +51 -0
- easyverein/models/mixins/__init__.py +0 -0
- easyverein/models/mixins/required_attributes.py +48 -0
- easyverein/modules/__init__.py +0 -0
- easyverein/modules/contact_details.py +20 -0
- easyverein/modules/custom_field.py +21 -0
- easyverein/modules/invoice.py +126 -0
- easyverein/modules/invoice_item.py +16 -0
- easyverein/modules/member.py +19 -0
- easyverein/modules/member_custom_field.py +119 -0
- easyverein/modules/mixins/__init__.py +0 -0
- easyverein/modules/mixins/crud.py +168 -0
- easyverein/modules/mixins/recycle_bin.py +48 -0
- python_easyverein-0.1.0.dist-info/LICENSE +7 -0
- python_easyverein-0.1.0.dist-info/METADATA +125 -0
- python_easyverein-0.1.0.dist-info/RECORD +32 -0
- python_easyverein-0.1.0.dist-info/WHEEL +4 -0
easyverein/__init__.py
ADDED
easyverein/api.py
ADDED
|
@@ -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
|
+
...
|
easyverein/core/types.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom types used for model validation
|
|
3
|
+
"""
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
from pydantic import BeforeValidator, EmailStr, Field, PlainSerializer, UrlConstraints
|
|
9
|
+
from pydantic_core import Url
|
|
10
|
+
|
|
11
|
+
from .validators import empty_string_to_none, parse_json_string
|
|
12
|
+
|
|
13
|
+
AnyHttpURL = Annotated[
|
|
14
|
+
Url,
|
|
15
|
+
UrlConstraints(allowed_schemes=["http", "https"]),
|
|
16
|
+
PlainSerializer(lambda x: str(x), return_type=str),
|
|
17
|
+
]
|
|
18
|
+
EasyVereinReference = Annotated[
|
|
19
|
+
int | AnyHttpURL | None, BeforeValidator(empty_string_to_none)
|
|
20
|
+
]
|
|
21
|
+
PositiveIntWithZero = Annotated[int, Field(ge=0)]
|
|
22
|
+
Date = Annotated[
|
|
23
|
+
datetime.date, PlainSerializer(lambda x: x.strftime("%Y-%m-%d"), return_type=str)
|
|
24
|
+
]
|
|
25
|
+
DateTime = Annotated[
|
|
26
|
+
datetime.datetime,
|
|
27
|
+
PlainSerializer(lambda x: x.strftime("%Y-%m-%dT%H:%M:%S"), return_type=str),
|
|
28
|
+
]
|
|
29
|
+
OptionsField = Annotated[
|
|
30
|
+
list[str] | None,
|
|
31
|
+
PlainSerializer(lambda x: json.dumps(x), return_type=str),
|
|
32
|
+
BeforeValidator(parse_json_string),
|
|
33
|
+
BeforeValidator(empty_string_to_none),
|
|
34
|
+
]
|
|
35
|
+
HexColor = Annotated[
|
|
36
|
+
str | None,
|
|
37
|
+
Field(min_length=7, max_length=7),
|
|
38
|
+
BeforeValidator(empty_string_to_none),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
Email = Annotated[EmailStr | None, BeforeValidator(empty_string_to_none)]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def empty_string_to_none(v: Any) -> Any:
|
|
6
|
+
if isinstance(v, str) and v == "":
|
|
7
|
+
return None
|
|
8
|
+
return v
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_json_string(v: Any) -> Any:
|
|
12
|
+
if isinstance(v, str):
|
|
13
|
+
return json.loads(v)
|
|
14
|
+
return v
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# noqa: F401
|
|
2
|
+
from .contact_details import ContactDetails, ContactDetailsCreate, ContactDetailsUpdate
|
|
3
|
+
from .custom_field import CustomField, CustomFieldCreate, CustomFieldUpdate
|
|
4
|
+
from .invoice import Invoice, InvoiceCreate, InvoiceUpdate
|
|
5
|
+
from .invoice_item import InvoiceItem, InvoiceItemCreate, InvoiceItemUpdate
|
|
6
|
+
from .member import Member, MemberCreate, MemberUpdate
|
|
7
|
+
from .member_custom_field import (
|
|
8
|
+
MemberCustomField,
|
|
9
|
+
MemberCustomFieldCreate,
|
|
10
|
+
MemberCustomFieldUpdate,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ContactDetails.model_rebuild()
|
|
14
|
+
CustomField.model_rebuild()
|
|
15
|
+
Invoice.model_rebuild()
|
|
16
|
+
InvoiceItem.model_rebuild()
|
|
17
|
+
Member.model_rebuild()
|
|
18
|
+
MemberCustomField.model_rebuild()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, PositiveInt
|
|
2
|
+
|
|
3
|
+
from ..core.types import DateTime, EasyVereinReference
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EasyVereinBase(BaseModel):
|
|
7
|
+
"""
|
|
8
|
+
Base class encapsulating common fields for all models
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
id: PositiveInt | None = None
|
|
12
|
+
org: EasyVereinReference | None = None
|
|
13
|
+
# TODO: Add reference to Organization once implemented
|
|
14
|
+
deleteAfterDate: DateTime | None = Field(default=None, alias="_deleteAfterDate")
|
|
15
|
+
"""Alias for `_deleteAfterDate` field. See [Pydantic Models](../usage.md#pydantic-models) for details."""
|
|
16
|
+
deletedBy: str | None = Field(default=None, alias="_deletedBy")
|
|
17
|
+
"""Alias for `_deletedBy` field. See [Pydantic Models](../usage.md#pydantic-models) for details."""
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contact Details related models
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from ..core.types import Date, Email
|
|
11
|
+
from .base import EasyVereinBase
|
|
12
|
+
from .mixins.required_attributes import required_mixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContactDetails(EasyVereinBase):
|
|
16
|
+
"""
|
|
17
|
+
| Representative Model Class | Update Model Class | Create Model Class |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `ContactDetails` | `ContactDetailsUpdate` | `ContactDetailsCreate` |
|
|
20
|
+
|
|
21
|
+
!!! info "Contact Details and Members"
|
|
22
|
+
Note that contact details can be created standalone (independently of members), but members
|
|
23
|
+
are required to have a contact details object linked.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
isCompany: bool | None = Field(default=None, alias="_isCompany")
|
|
27
|
+
"""Alias for `_isCompany` field. See [Pydantic Models](../usage.md#pydantic-models) for details."""
|
|
28
|
+
salutation: Literal["", "Herr", "Frau"] | None = None
|
|
29
|
+
firstName: str | None = Field(default=None, max_length=128)
|
|
30
|
+
familyName: str | None = Field(default=None, max_length=128)
|
|
31
|
+
nameAffix: str | None = Field(default=None, max_length=100)
|
|
32
|
+
dateOfBirth: Date | None = None
|
|
33
|
+
internalNote: str | None = None
|
|
34
|
+
privateEmail: Email | None = None
|
|
35
|
+
companyEmail: Email | None = None
|
|
36
|
+
companyEmailInvoice: Email | None = None
|
|
37
|
+
primaryEmail: str | None = "email"
|
|
38
|
+
preferredEmailField: Literal[0, 1, 2] | None = Field(
|
|
39
|
+
default=None, alias="_preferredEmailField"
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
Alias for `_preferredEmailField` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
|
|
43
|
+
|
|
44
|
+
Possible values:
|
|
45
|
+
|
|
46
|
+
- 0: same as login
|
|
47
|
+
- 1: private
|
|
48
|
+
- 2: company
|
|
49
|
+
"""
|
|
50
|
+
# Hint: 0 = mail, 1 = phone, 3 = no communication
|
|
51
|
+
preferredCommunicationWay: Literal[0, 1, 2] | None = None
|
|
52
|
+
companyName: str | None = None
|
|
53
|
+
invoiceCompany: bool | None = None
|
|
54
|
+
sendInvoiceCompanyMail: bool | None = None
|
|
55
|
+
addressCompany: bool | None = None
|
|
56
|
+
privatePhone: str | None = Field(default=None, max_length=100)
|
|
57
|
+
companyPhone: str | None = Field(default=None, max_length=100)
|
|
58
|
+
mobilePhone: str | None = Field(default=None, max_length=100)
|
|
59
|
+
street: str | None = Field(default=None, max_length=128)
|
|
60
|
+
city: str | None = Field(default=None, max_length=100)
|
|
61
|
+
state: str | None = Field(default=None, max_length=64)
|
|
62
|
+
additionalAdressInfo: str | None = Field(
|
|
63
|
+
default=None, max_length=128
|
|
64
|
+
) # Intentionally written wrong, as per API
|
|
65
|
+
zip: str | None = Field(default=None, max_length=20)
|
|
66
|
+
country: str | None = Field(default=None, max_length=50)
|
|
67
|
+
companyStreet: str | None = Field(default=None, max_length=100)
|
|
68
|
+
companyCity: str | None = Field(default=None, max_length=64)
|
|
69
|
+
companyState: str | None = Field(default=None, max_length=100)
|
|
70
|
+
companyZip: str | None = Field(default=None, max_length=20)
|
|
71
|
+
companyCountry: str | None = Field(default=None, max_length=50)
|
|
72
|
+
professionalRole: str | None = Field(default=None, max_length=500)
|
|
73
|
+
balance: float | None = None
|
|
74
|
+
iban: str | None = Field(default=None, max_length=50)
|
|
75
|
+
bic: str | None = Field(default=None, max_length=100)
|
|
76
|
+
bankAccountOwner: str | None = Field(default=None, max_length=128)
|
|
77
|
+
sepaMandate: str | None = Field(default=None, max_length=60)
|
|
78
|
+
sepaDate: Date | None = None
|
|
79
|
+
methodOfPayment: int | None = None
|
|
80
|
+
"""
|
|
81
|
+
Defines the method of payment preferred by the user.
|
|
82
|
+
|
|
83
|
+
Possible values:
|
|
84
|
+
|
|
85
|
+
- 0: not selected
|
|
86
|
+
- 1: direct debit
|
|
87
|
+
- 2: bank transfer
|
|
88
|
+
- 3: cash
|
|
89
|
+
- 4: other
|
|
90
|
+
"""
|
|
91
|
+
datevAccountNumber: int | None = None
|
|
92
|
+
# TODO: Refine once available from API description
|
|
93
|
+
copiedFromParent: Any | None = Field(default=None, alias="_copiedFromParent")
|
|
94
|
+
"""
|
|
95
|
+
Alias for `_copiedFromParent` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
|
|
96
|
+
"""
|
|
97
|
+
# TODO: Refine once available from API description
|
|
98
|
+
copiedFromParentStartDate: Any | None = Field(
|
|
99
|
+
default=None,
|
|
100
|
+
alias="_copiedFromParentStartDate",
|
|
101
|
+
)
|
|
102
|
+
"""
|
|
103
|
+
Alias for `_copiedFromParentStartDate` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
|
|
104
|
+
"""
|
|
105
|
+
# TODO: Refine once available from API description
|
|
106
|
+
copiedFromParentEndDate: Any | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
alias="_copiedFromParentEndDate",
|
|
109
|
+
)
|
|
110
|
+
"""
|
|
111
|
+
Alias for `_copiedFromParentEndDate` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
|
|
112
|
+
"""
|
|
113
|
+
# TODO: Refine once available from API description
|
|
114
|
+
copiedFromParentEndDateAction: Any | None = Field(
|
|
115
|
+
default=None,
|
|
116
|
+
alias="_copiedFromParentEndDateAction",
|
|
117
|
+
)
|
|
118
|
+
"""
|
|
119
|
+
Alias for `_copiedFromParentEndDateAction` field. See [Pydantic Models](../usage.md#pydantic-models) for details.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ContactDetailsUpdate(ContactDetails):
|
|
124
|
+
"""
|
|
125
|
+
Pydantic model used to update contact details
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
isCompany: bool | None = Field(default=None, serialization_alias="_isCompany")
|
|
129
|
+
preferredEmailField: Literal[0, 1, 2] | None = Field(
|
|
130
|
+
default=None, alias="_preferredEmailField"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ContactDetailsCreate(ContactDetailsUpdate, required_mixin(["isCompany"])):
|
|
135
|
+
"""
|
|
136
|
+
Pydantic model for creating new contact details
|
|
137
|
+
"""
|