MambuPy 2.3.2__tar.gz → 2.4.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.
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/__init__.py +1 -1
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/classes.py +18 -19
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/connector/mambuconnector.py +31 -0
- {mambupy-2.3.2/mambupy → mambupy-2.4.0/MambuPy}/api/connector/rest.py +49 -48
- mambupy-2.4.0/MambuPy/api/connector/rest_helpers.py +63 -0
- {mambupy-2.3.2/mambupy → mambupy-2.4.0/MambuPy}/api/entities.py +13 -12
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambuloan.py +62 -5
- {mambupy-2.3.2/mambupy → mambupy-2.4.0/MambuPy}/api/mambustruct.py +22 -4
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/mambuutil.py +69 -9
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy.egg-info/PKG-INFO +2 -1
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy.egg-info/SOURCES.txt +2 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy.egg-info/requires.txt +1 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/PKG-INFO +2 -1
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/__init__.py +1 -1
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/classes.py +18 -19
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/connector/mambuconnector.py +31 -0
- {mambupy-2.3.2/MambuPy → mambupy-2.4.0/mambupy}/api/connector/rest.py +49 -48
- mambupy-2.4.0/mambupy/api/connector/rest_helpers.py +63 -0
- {mambupy-2.3.2/MambuPy → mambupy-2.4.0/mambupy}/api/entities.py +13 -12
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambuloan.py +62 -5
- {mambupy-2.3.2/MambuPy → mambupy-2.4.0/mambupy}/api/mambustruct.py +22 -4
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/mambuutil.py +69 -9
- {mambupy-2.3.2 → mambupy-2.4.0}/pyproject.toml +1 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/LICENSE +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/connector/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/interfaces.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambubranch.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambucentre.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambuclient.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambucustomfield.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambugroup.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambuproduct.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mamburole.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambutask.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambutransaction.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/mambuuser.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/api/vos.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/mambuconfig.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/mambugeturl.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_activities.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_addresses.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_branches.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_centres.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_clients.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_customfields.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_dummies.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_groups.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_loans.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_mambu.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_orm.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_tasks.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/orm/schema_users.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/rest/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/rest/mambuactivity.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/utils/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/utils/update_user_roles.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy/utils/userdeactivate.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy.egg-info/dependency_links.txt +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/MambuPy.egg-info/top_level.txt +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/README.md +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/connector/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/interfaces.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambubranch.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambucentre.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambuclient.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambucustomfield.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambugroup.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambuproduct.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mamburole.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambutask.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambutransaction.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/mambuuser.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/api/vos.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/mambuconfig.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/mambugeturl.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_activities.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_addresses.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_branches.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_centres.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_clients.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_customfields.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_dummies.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_groups.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_loans.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_mambu.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_orm.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_tasks.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/orm/schema_users.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/rest/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/rest/mambuactivity.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/utils/__init__.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/utils/update_user_roles.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/mambupy/utils/userdeactivate.py +0 -0
- {mambupy-2.3.2 → mambupy-2.4.0}/setup.cfg +0 -0
|
@@ -31,32 +31,31 @@ class MambuMapObj:
|
|
|
31
31
|
for key, val in kwargs.items():
|
|
32
32
|
self._attrs[key] = val
|
|
33
33
|
|
|
34
|
-
def
|
|
34
|
+
def __getattr__(self, name):
|
|
35
35
|
"""Object-like get attribute
|
|
36
36
|
|
|
37
37
|
When accessing an attribute, tries to find it in the _attrs
|
|
38
38
|
dictionary, so now MambuMapObj may act not only as a dict-like
|
|
39
39
|
structure, but as a full object-like too (this is the getter
|
|
40
40
|
side).
|
|
41
|
+
|
|
42
|
+
Implemented as __getattr__ (not __getattribute__) so it only runs
|
|
43
|
+
after the normal attribute lookup already failed: a real
|
|
44
|
+
attribute/method always wins, and successful accesses keep the
|
|
45
|
+
native fast path instead of being intercepted on every access.
|
|
41
46
|
"""
|
|
42
|
-
try
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return object.__getattribute__(self, name)
|
|
55
|
-
# all else, read the property from the _attrs dict, but with a . syntax
|
|
56
|
-
# if a cf_class, just return its value
|
|
57
|
-
if _attrs[name].__class__.__name__ == self._cf_class.__name__:
|
|
58
|
-
return _attrs[name]["value"]
|
|
59
|
-
return _attrs[name]
|
|
47
|
+
# try to read the _attrs property
|
|
48
|
+
_attrs = object.__getattribute__(self, "_attrs")
|
|
49
|
+
if isinstance(_attrs, list) or name not in _attrs:
|
|
50
|
+
# magic won't happen when not a dict-like MambuMapObj or
|
|
51
|
+
# when _attrs has not the 'name' key: this is a genuine
|
|
52
|
+
# missing attribute, so AttributeError raises by default
|
|
53
|
+
raise AttributeError(name)
|
|
54
|
+
# all else, read the property from the _attrs dict, but with a . syntax
|
|
55
|
+
# if a cf_class, just return its value
|
|
56
|
+
if _attrs[name].__class__.__name__ == self._cf_class.__name__:
|
|
57
|
+
return _attrs[name]["value"]
|
|
58
|
+
return _attrs[name]
|
|
60
59
|
|
|
61
60
|
def __setattr__(self, name, value):
|
|
62
61
|
"""Object-like set attribute
|
|
@@ -286,6 +286,37 @@ class MambuConnectorWriter(ABC):
|
|
|
286
286
|
"""
|
|
287
287
|
raise NotImplementedError
|
|
288
288
|
|
|
289
|
+
@abstractmethod
|
|
290
|
+
def mambu_loanaccount_updateSchedule(self, loanid, installments):
|
|
291
|
+
"""Updates the installments schedule of a loan account
|
|
292
|
+
|
|
293
|
+
Mambu replaces the whole schedule with the given installments, so the
|
|
294
|
+
complete list must be sent, not just the modified ones.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
loanid (str): the id or encoded key of the loan account
|
|
298
|
+
installments (list of dict): the installments of the schedule,
|
|
299
|
+
complying with Mambu's schema
|
|
300
|
+
"""
|
|
301
|
+
raise NotImplementedError
|
|
302
|
+
|
|
303
|
+
@abstractmethod
|
|
304
|
+
def mambu_loanaccount_changeDueDatesSettings(
|
|
305
|
+
self, loanid, fixedDaysOfMonth, notes=None, valueDate=None
|
|
306
|
+
):
|
|
307
|
+
"""Changes the due dates settings of a loan account
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
loanid (str): the id or encoded key of the loan account
|
|
311
|
+
fixedDaysOfMonth (list of int): the new fixed days of month to be used
|
|
312
|
+
on the loan account
|
|
313
|
+
notes (str): notes to associate to the change due dates settings
|
|
314
|
+
operation in Mambu
|
|
315
|
+
valueDate (str or :py:obj:`datetime`): the date when to change the due
|
|
316
|
+
dates settings
|
|
317
|
+
"""
|
|
318
|
+
raise NotImplementedError
|
|
319
|
+
|
|
289
320
|
@abstractmethod
|
|
290
321
|
def mambu_loantransaction_adjust(self, transactionid, notes):
|
|
291
322
|
"""Adjust a loan transaction
|
|
@@ -9,21 +9,18 @@ Currently supports REST.
|
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
11
|
import copy
|
|
12
|
-
import datetime
|
|
13
|
-
import enum
|
|
14
12
|
import json
|
|
15
13
|
import mimetypes
|
|
14
|
+
import orjson
|
|
16
15
|
import os
|
|
17
16
|
import re
|
|
18
17
|
import uuid
|
|
19
|
-
from decimal import Decimal
|
|
20
18
|
|
|
21
19
|
import requests
|
|
22
20
|
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
|
23
|
-
from requests.adapters import HTTPAdapter
|
|
24
|
-
from requests.packages.urllib3.util.retry import Retry
|
|
25
21
|
|
|
26
22
|
from .mambuconnector import MambuConnector, MambuConnectorReader, MambuConnectorWriter
|
|
23
|
+
from .rest_helpers import _MambuJSONEncoder, _configure_retry_strategy
|
|
27
24
|
from MambuPy.mambuutil import (
|
|
28
25
|
ALLOWED_UPLOAD_MIMETYPES,
|
|
29
26
|
DETAILSLEVEL,
|
|
@@ -46,46 +43,6 @@ from MambuPy.mambuutil import (
|
|
|
46
43
|
logger = setup_logging(__name__)
|
|
47
44
|
|
|
48
45
|
|
|
49
|
-
class _MambuJSONEncoder(json.JSONEncoder):
|
|
50
|
-
"""JSON encoder that handles Python types not natively serializable by json.dumps."""
|
|
51
|
-
|
|
52
|
-
def default(self, obj):
|
|
53
|
-
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
54
|
-
return obj.isoformat()
|
|
55
|
-
if isinstance(obj, enum.Enum):
|
|
56
|
-
return obj.value
|
|
57
|
-
if isinstance(obj, Decimal):
|
|
58
|
-
return float(obj)
|
|
59
|
-
return super().default(obj)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _configure_retry_strategy(session, retries=5):
|
|
63
|
-
"""Configure retry strategy for a session.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
session (requests.Session): The session to configure
|
|
67
|
-
retries (int, optional): Number of retries. Defaults to 5.
|
|
68
|
-
"""
|
|
69
|
-
retry_strategy = Retry(
|
|
70
|
-
total=retries,
|
|
71
|
-
status_forcelist=[429, 500, 502, 503, 504],
|
|
72
|
-
backoff_factor=1,
|
|
73
|
-
allowed_methods=[
|
|
74
|
-
"HEAD",
|
|
75
|
-
"GET",
|
|
76
|
-
"OPTIONS",
|
|
77
|
-
"POST",
|
|
78
|
-
"PUT",
|
|
79
|
-
"DELETE",
|
|
80
|
-
"TRACE",
|
|
81
|
-
"PATCH",
|
|
82
|
-
],
|
|
83
|
-
)
|
|
84
|
-
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
85
|
-
session.mount("https://", adapter)
|
|
86
|
-
session.mount("http://", adapter)
|
|
87
|
-
|
|
88
|
-
|
|
89
46
|
class SessionSingleton:
|
|
90
47
|
"""Singleton class to manage HTTP sessions for MambuPy.
|
|
91
48
|
|
|
@@ -224,7 +181,7 @@ url %s, params %s, data %s, headers %s",
|
|
|
224
181
|
if hasattr(resp, "content"): # pragma: no cover
|
|
225
182
|
logger.warning("HTTPError, resp content: %s", resp.content)
|
|
226
183
|
try:
|
|
227
|
-
content =
|
|
184
|
+
content = orjson.loads(resp.content.decode())
|
|
228
185
|
except ValueError:
|
|
229
186
|
# in case resp.content doesn't conforms to json
|
|
230
187
|
content = {
|
|
@@ -300,7 +257,7 @@ url %s, params %s, data %s, headers %s",
|
|
|
300
257
|
def __list_request_cat_response(self, list_resp, resp):
|
|
301
258
|
if list_resp == b"":
|
|
302
259
|
list_resp = resp
|
|
303
|
-
elif len(
|
|
260
|
+
elif len(orjson.loads(resp.decode())) > 0:
|
|
304
261
|
list_resp = list_resp[:-1] + b"," + resp[1:]
|
|
305
262
|
|
|
306
263
|
return list_resp
|
|
@@ -340,7 +297,7 @@ url %s, params %s, data %s, headers %s",
|
|
|
340
297
|
method, url, params=copy.copy(params), data=copy.copy(data)
|
|
341
298
|
)
|
|
342
299
|
|
|
343
|
-
jsonresp = list(
|
|
300
|
+
jsonresp = list(orjson.loads(resp.decode()))
|
|
344
301
|
if len(jsonresp) < limit:
|
|
345
302
|
window = False
|
|
346
303
|
|
|
@@ -765,6 +722,50 @@ url %s, params %s, data %s, headers %s",
|
|
|
765
722
|
data = {"notes": notes}
|
|
766
723
|
return self.__request("POST", url, data=data)
|
|
767
724
|
|
|
725
|
+
def mambu_loanaccount_updateSchedule(self, loanid, installments):
|
|
726
|
+
"""Updates the installments schedule of a loan account
|
|
727
|
+
|
|
728
|
+
Mambu replaces the whole schedule with the given installments, so the
|
|
729
|
+
complete list must be sent, not just the modified ones.
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
loanid (str): the id or encoded key of the loan account
|
|
733
|
+
installments (list of dict): the installments of the schedule,
|
|
734
|
+
complying with Mambu's schema
|
|
735
|
+
"""
|
|
736
|
+
url = "https://{}/api/{}/{}/schedule".format(self._tenant, "loans", loanid)
|
|
737
|
+
|
|
738
|
+
return self.__request("PUT", url, data=installments)
|
|
739
|
+
|
|
740
|
+
def mambu_loanaccount_changeDueDatesSettings(
|
|
741
|
+
self, loanid, fixedDaysOfMonth, notes=None, valueDate=None
|
|
742
|
+
):
|
|
743
|
+
"""Changes the due dates settings of a loan account
|
|
744
|
+
|
|
745
|
+
Only available for loan accounts whose product uses the
|
|
746
|
+
FIXED_DAYS_OF_MONTH repayment methodology. Mambu rejects the request
|
|
747
|
+
otherwise.
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
loanid (str): the id or encoded key of the loan account
|
|
751
|
+
fixedDaysOfMonth (list of int): the new fixed days of month to be used
|
|
752
|
+
on the loan account
|
|
753
|
+
notes (str): notes to associate to the change due dates settings
|
|
754
|
+
operation in Mambu
|
|
755
|
+
valueDate (str or :py:obj:`datetime`): the date when to change the due
|
|
756
|
+
dates settings
|
|
757
|
+
"""
|
|
758
|
+
url = "https://{}/api/{}/{}:changeDueDatesSettings".format(
|
|
759
|
+
self._tenant, "loans", loanid
|
|
760
|
+
)
|
|
761
|
+
data = {"fixedDaysOfMonth": fixedDaysOfMonth}
|
|
762
|
+
if notes is not None:
|
|
763
|
+
data["notes"] = notes
|
|
764
|
+
if valueDate is not None:
|
|
765
|
+
data["valueDate"] = valueDate
|
|
766
|
+
|
|
767
|
+
return self.__request("POST", url, data=data)
|
|
768
|
+
|
|
768
769
|
def mambu_change_state(self, entid, prefix, action, notes):
|
|
769
770
|
"""change state of mambu entity
|
|
770
771
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Helpers for the REST connector.
|
|
2
|
+
|
|
3
|
+
.. autosummary::
|
|
4
|
+
:nosignatures:
|
|
5
|
+
:toctree: _autosummary
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
import enum
|
|
10
|
+
import json
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
|
|
13
|
+
from requests.adapters import HTTPAdapter
|
|
14
|
+
from requests.packages.urllib3.util.retry import Retry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _MambuJSONEncoder(json.JSONEncoder):
|
|
18
|
+
"""JSON encoder that handles Python types not natively serializable by json.dumps."""
|
|
19
|
+
|
|
20
|
+
def default(self, obj):
|
|
21
|
+
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
22
|
+
return obj.isoformat()
|
|
23
|
+
if isinstance(obj, enum.Enum):
|
|
24
|
+
return obj.value
|
|
25
|
+
if isinstance(obj, Decimal):
|
|
26
|
+
return float(obj)
|
|
27
|
+
return super().default(obj)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _configure_retry_strategy(session, retries=5):
|
|
31
|
+
"""Configure retry strategy for a session.
|
|
32
|
+
|
|
33
|
+
El HTTPAdapter se construye con pool_maxsize=50 para que los flujos
|
|
34
|
+
que despachan multiples requests concurrentes contra Mambu (p.ej.
|
|
35
|
+
fan-out de aprobaciones, cargar_datos paralelo) no queden encolados
|
|
36
|
+
sobre el default de urllib3 de 10 conexiones por host.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
session (requests.Session): The session to configure
|
|
40
|
+
retries (int, optional): Number of retries. Defaults to 5.
|
|
41
|
+
"""
|
|
42
|
+
retry_strategy = Retry(
|
|
43
|
+
total=retries,
|
|
44
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
45
|
+
backoff_factor=1,
|
|
46
|
+
allowed_methods=[
|
|
47
|
+
"HEAD",
|
|
48
|
+
"GET",
|
|
49
|
+
"OPTIONS",
|
|
50
|
+
"POST",
|
|
51
|
+
"PUT",
|
|
52
|
+
"DELETE",
|
|
53
|
+
"TRACE",
|
|
54
|
+
"PATCH",
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
adapter = HTTPAdapter(
|
|
58
|
+
max_retries=retry_strategy,
|
|
59
|
+
pool_connections=50,
|
|
60
|
+
pool_maxsize=50,
|
|
61
|
+
)
|
|
62
|
+
session.mount("https://", adapter)
|
|
63
|
+
session.mount("http://", adapter)
|
|
@@ -10,6 +10,7 @@ import datetime
|
|
|
10
10
|
import enum
|
|
11
11
|
from importlib import import_module
|
|
12
12
|
import json
|
|
13
|
+
import orjson
|
|
13
14
|
import time
|
|
14
15
|
|
|
15
16
|
from .classes import GenericClass
|
|
@@ -240,7 +241,7 @@ class MambuEntity(MambuStruct):
|
|
|
240
241
|
|
|
241
242
|
logger.debug("request several entities %s", cls.__name__)
|
|
242
243
|
list_resp = get_func(prefix, **params)
|
|
243
|
-
jsonresp = list(
|
|
244
|
+
jsonresp = list(orjson.loads(list_resp.decode()))
|
|
244
245
|
logger.debug("%s, %s retrieved", cls.__name__, len(jsonresp))
|
|
245
246
|
|
|
246
247
|
elements = []
|
|
@@ -312,8 +313,8 @@ class MambuEntity(MambuStruct):
|
|
|
312
313
|
instance = cls.__build_object(
|
|
313
314
|
connector=connector,
|
|
314
315
|
resp=resp,
|
|
315
|
-
attrs=dict(
|
|
316
|
-
tzattrs=dict(
|
|
316
|
+
attrs=dict(orjson.loads(resp.decode())),
|
|
317
|
+
tzattrs=dict(orjson.loads(resp.decode())),
|
|
317
318
|
get_entities=get_entities,
|
|
318
319
|
detailsLevel=detailsLevel,
|
|
319
320
|
debug=debug,
|
|
@@ -355,8 +356,8 @@ class MambuEntity(MambuStruct):
|
|
|
355
356
|
self.id, prefix=self._prefix, detailsLevel=detailsLevel
|
|
356
357
|
)
|
|
357
358
|
|
|
358
|
-
self._attrs.update(dict(
|
|
359
|
-
self._tzattrs = dict(
|
|
359
|
+
self._attrs.update(dict(orjson.loads(self._resp.decode())))
|
|
360
|
+
self._tzattrs = dict(orjson.loads(self._resp.decode()))
|
|
360
361
|
self._convertDict2Attrs()
|
|
361
362
|
self._extractCustomFields()
|
|
362
363
|
self._extractVOs()
|
|
@@ -472,8 +473,8 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
|
|
|
472
473
|
self._resp = self._connector.mambu_create(
|
|
473
474
|
self._prefix, copy.deepcopy(self._attrs), **kwargs
|
|
474
475
|
)
|
|
475
|
-
self._attrs.update(dict(
|
|
476
|
-
self._tzattrs = dict(
|
|
476
|
+
self._attrs.update(dict(orjson.loads(self._resp.decode())))
|
|
477
|
+
self._tzattrs = dict(orjson.loads(self._resp.decode()))
|
|
477
478
|
self._detailsLevel = "FULL"
|
|
478
479
|
except MambuError:
|
|
479
480
|
raise
|
|
@@ -573,7 +574,7 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
|
|
|
573
574
|
# build fields_ops param with what detected using previous rules
|
|
574
575
|
# strings: (OP, PATH) (remove) or (OP, PATH, VALUE) (all else)
|
|
575
576
|
fields_ops = []
|
|
576
|
-
original_attrs = dict(
|
|
577
|
+
original_attrs = dict(orjson.loads(self._resp.decode()))
|
|
577
578
|
self._extractCustomFields(original_attrs)
|
|
578
579
|
self._updateVOs()
|
|
579
580
|
for field in fields:
|
|
@@ -709,7 +710,7 @@ class MambuEntityAttachable(MambuStruct, MambuAttachable):
|
|
|
709
710
|
notes=notes,
|
|
710
711
|
)
|
|
711
712
|
|
|
712
|
-
doc = MambuDocument(**dict(
|
|
713
|
+
doc = MambuDocument(**dict(orjson.loads(response.decode())))
|
|
713
714
|
self._attachments[str(doc["id"])] = doc
|
|
714
715
|
|
|
715
716
|
return response
|
|
@@ -742,7 +743,7 @@ class MambuEntityAttachable(MambuStruct, MambuAttachable):
|
|
|
742
743
|
paginationDetails=paginationDetails,
|
|
743
744
|
)
|
|
744
745
|
|
|
745
|
-
metadata_list =
|
|
746
|
+
metadata_list = orjson.loads(response.decode())
|
|
746
747
|
|
|
747
748
|
for metadata in metadata_list:
|
|
748
749
|
doc = MambuDocument(**metadata)
|
|
@@ -831,7 +832,7 @@ class MambuEntityCommentable(MambuStruct, MambuCommentable):
|
|
|
831
832
|
paginationDetails=paginationDetails,
|
|
832
833
|
)
|
|
833
834
|
|
|
834
|
-
comments_list =
|
|
835
|
+
comments_list = orjson.loads(response.decode())
|
|
835
836
|
|
|
836
837
|
for comment in comments_list:
|
|
837
838
|
comm = MambuComment(**comment)
|
|
@@ -854,7 +855,7 @@ class MambuEntityCommentable(MambuStruct, MambuCommentable):
|
|
|
854
855
|
owner_id=self.id, owner_type=self._ownerType, text=comment
|
|
855
856
|
)
|
|
856
857
|
|
|
857
|
-
comment = MambuComment(**dict(
|
|
858
|
+
comment = MambuComment(**dict(orjson.loads(response.decode())))
|
|
858
859
|
self._comments.insert(0, comment)
|
|
859
860
|
|
|
860
861
|
return response
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import copy
|
|
9
9
|
import datetime
|
|
10
10
|
import json
|
|
11
|
+
import orjson
|
|
11
12
|
|
|
12
13
|
from .entities import (
|
|
13
14
|
MambuEntity,
|
|
@@ -113,11 +114,13 @@ class MambuLoan(
|
|
|
113
114
|
except KeyError:
|
|
114
115
|
pass
|
|
115
116
|
|
|
116
|
-
def
|
|
117
|
-
"""
|
|
118
|
-
resp = self._connector.mambu_loanaccount_getSchedule(self.id)
|
|
117
|
+
def _hydrate_schedule(self, resp):
|
|
118
|
+
"""Builds the schedule of MambuInstallments from a Mambu response.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
Args:
|
|
121
|
+
resp (bytes): a Mambu response holding an installments list.
|
|
122
|
+
"""
|
|
123
|
+
installments = orjson.loads(resp.decode())["installments"]
|
|
121
124
|
|
|
122
125
|
self.schedule = []
|
|
123
126
|
for installment in installments:
|
|
@@ -126,6 +129,38 @@ class MambuLoan(
|
|
|
126
129
|
installment_entity._convertDict2Attrs()
|
|
127
130
|
self.schedule.append(installment_entity)
|
|
128
131
|
|
|
132
|
+
def get_schedule(self, **kwargs):
|
|
133
|
+
"""Retrieves the installments schedule."""
|
|
134
|
+
resp = self._connector.mambu_loanaccount_getSchedule(self.id)
|
|
135
|
+
|
|
136
|
+
self._hydrate_schedule(resp)
|
|
137
|
+
|
|
138
|
+
def update_schedule(self, installments=None, **kwargs):
|
|
139
|
+
"""Request to update the installments schedule of a loan account.
|
|
140
|
+
|
|
141
|
+
Mambu replaces the whole schedule with the installments sent, so the
|
|
142
|
+
complete list must be given, not just the modified ones. Sending a
|
|
143
|
+
subset means deleting the rest, which Mambu rejects for accounts on
|
|
144
|
+
non dynamic term products.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
installments (list of :py:obj:`MambuInstallment`): the schedule to
|
|
148
|
+
send. Defaults to the schedule currently held by this
|
|
149
|
+
account, as retrieved by `get_schedule`.
|
|
150
|
+
"""
|
|
151
|
+
if installments is None:
|
|
152
|
+
installments = self.schedule
|
|
153
|
+
|
|
154
|
+
payload = []
|
|
155
|
+
for installment in installments:
|
|
156
|
+
serialized = copy.deepcopy(installment)
|
|
157
|
+
serialized._serializeFields()
|
|
158
|
+
payload.append(serialized._attrs)
|
|
159
|
+
|
|
160
|
+
resp = self._connector.mambu_loanaccount_updateSchedule(self.id, payload)
|
|
161
|
+
|
|
162
|
+
self._hydrate_schedule(resp)
|
|
163
|
+
|
|
129
164
|
def get_transactions(self, **kwargs):
|
|
130
165
|
"""Retrieves the transactions of the loan"""
|
|
131
166
|
self.transactions = MambuTransaction.get_all(self.id)
|
|
@@ -144,7 +179,7 @@ class MambuLoan(
|
|
|
144
179
|
resp = self._connector.mambu_change_state(
|
|
145
180
|
entid=self.id, prefix=self._prefix, action=action, notes=notes,
|
|
146
181
|
)
|
|
147
|
-
resp =
|
|
182
|
+
resp = orjson.loads(resp)
|
|
148
183
|
self.accountState = resp["accountState"]
|
|
149
184
|
else:
|
|
150
185
|
raise MambuPyError(
|
|
@@ -240,6 +275,28 @@ class MambuLoan(
|
|
|
240
275
|
self._connector.mambu_loanaccount_writeoff(self.id, notes)
|
|
241
276
|
self.refresh()
|
|
242
277
|
|
|
278
|
+
def change_due_dates_settings(
|
|
279
|
+
self, fixedDaysOfMonth, notes=None, valueDate=None, **kwargs
|
|
280
|
+
):
|
|
281
|
+
"""Request to change the due dates settings of a loan account.
|
|
282
|
+
|
|
283
|
+
Only available for loan accounts whose product uses the
|
|
284
|
+
FIXED_DAYS_OF_MONTH repayment methodology. Mambu rejects the request
|
|
285
|
+
otherwise, raising a `MambuError`.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
fixedDaysOfMonth (list of int): the new fixed days of month to be used
|
|
289
|
+
on the loan account.
|
|
290
|
+
notes (str): notes to attach to the change due dates settings
|
|
291
|
+
operation.
|
|
292
|
+
valueDate (str or :py:obj:`datetime`): the date when to change the due
|
|
293
|
+
dates settings.
|
|
294
|
+
"""
|
|
295
|
+
self._connector.mambu_loanaccount_changeDueDatesSettings(
|
|
296
|
+
self.id, fixedDaysOfMonth, notes, valueDate
|
|
297
|
+
)
|
|
298
|
+
self.refresh()
|
|
299
|
+
|
|
243
300
|
def repay(self, amount, notes, valueDate, **kwargs):
|
|
244
301
|
"""Request to repay a loan account.
|
|
245
302
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import copy
|
|
9
|
-
from datetime import datetime
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
10
|
from importlib import import_module
|
|
11
11
|
|
|
12
12
|
from .classes import MambuMapObj
|
|
@@ -101,15 +101,21 @@ class MambuStruct(MambuMapObj):
|
|
|
101
101
|
return self.__getattribute_for_loop(ent, entity.value, entity.mcf)
|
|
102
102
|
return self.__getattribute_for_one_cf(ent, entity.value, entity.mcf)
|
|
103
103
|
|
|
104
|
-
def
|
|
104
|
+
def __getattr__(self, name):
|
|
105
105
|
"""Object-like get attribute for MambuStructs.
|
|
106
106
|
|
|
107
107
|
If the attribute is not present at the _attrs dict, tries to build a
|
|
108
108
|
function that calls :py:meth:`getEntities`, which in turns will try
|
|
109
109
|
to instantiate an entity according to the requested attribute name
|
|
110
|
+
|
|
111
|
+
Implemented as __getattr__ (not __getattribute__) so it only runs
|
|
112
|
+
after the normal lookup failed. We first delegate to the base
|
|
113
|
+
__getattr__ (the _attrs resolution), preserving precedence of
|
|
114
|
+
_attrs over the dynamic get_* magic, and only fall back to building
|
|
115
|
+
a getter when that raises AttributeError.
|
|
110
116
|
"""
|
|
111
117
|
try:
|
|
112
|
-
return super().
|
|
118
|
+
return super().__getattr__(name)
|
|
113
119
|
except AttributeError as attr_err:
|
|
114
120
|
if name[0:4] == "get_" and len(name) > 4:
|
|
115
121
|
ent = name[4:]
|
|
@@ -283,7 +289,19 @@ class MambuStruct(MambuMapObj):
|
|
|
283
289
|
if isinstance(data, datetime):
|
|
284
290
|
data_asdate = data.isoformat()
|
|
285
291
|
if tzdata: # no tzdata means a date (no time) object
|
|
286
|
-
|
|
292
|
+
if self._as_utc:
|
|
293
|
+
# _convertDict2Attrs moved this datetime to UTC and dropped
|
|
294
|
+
# its tzinfo, so appending the original offset would shift
|
|
295
|
+
# the instant. Convert back to that offset instead.
|
|
296
|
+
offset_str = tzdata[3:] or "+00:00"
|
|
297
|
+
tz_info = datetime.fromisoformat(
|
|
298
|
+
"2000-01-01T00:00:00" + offset_str
|
|
299
|
+
).tzinfo
|
|
300
|
+
data_asdate = (
|
|
301
|
+
data.replace(tzinfo=timezone.utc).astimezone(tz_info).isoformat()
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
data_asdate += tzdata[-6:]
|
|
287
305
|
else:
|
|
288
306
|
data_asdate = data_asdate[:10]
|
|
289
307
|
return data_asdate
|
|
@@ -321,13 +321,46 @@ def _backup_db_previous_prep(callback, logger, kwargs):
|
|
|
321
321
|
data = {"callback": callback}
|
|
322
322
|
list_ret.append(data)
|
|
323
323
|
|
|
324
|
+
try:
|
|
325
|
+
backup_in_progress_wait = kwargs["backup_in_progress_wait"]
|
|
326
|
+
except KeyError:
|
|
327
|
+
backup_in_progress_wait = 900
|
|
328
|
+
list_ret.append(backup_in_progress_wait)
|
|
329
|
+
|
|
330
|
+
try:
|
|
331
|
+
backup_in_progress_retries = kwargs["backup_in_progress_retries"]
|
|
332
|
+
except KeyError:
|
|
333
|
+
backup_in_progress_retries = 3
|
|
334
|
+
list_ret.append(backup_in_progress_retries)
|
|
335
|
+
|
|
324
336
|
return list_ret
|
|
325
337
|
|
|
326
338
|
|
|
327
|
-
def
|
|
339
|
+
def _backup_db_in_progress(resp):
|
|
340
|
+
"""True if Mambu rejected the backup POST because one is already running.
|
|
341
|
+
|
|
342
|
+
Mambu answers with errorCode 1201 / DATABASE_BACKUP_IN_PROGRESS when a
|
|
343
|
+
database backup job is already in progress. That is a transient, retryable
|
|
344
|
+
condition (the running job finishes in ~14 min), not a real failure.
|
|
345
|
+
"""
|
|
328
346
|
try:
|
|
329
|
-
|
|
330
|
-
|
|
347
|
+
errors = json.loads(resp.content).get("errors", [])
|
|
348
|
+
return any(err.get("errorCode") == 1201 for err in errors)
|
|
349
|
+
except (ValueError, AttributeError):
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _backup_db_request(
|
|
354
|
+
justbackup, data, user, pwd, logger=None,
|
|
355
|
+
backup_in_progress_wait=900, backup_in_progress_retries=3,
|
|
356
|
+
):
|
|
357
|
+
if justbackup:
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
posturl = iri_to_uri(getmambuurl() + "database/backup")
|
|
361
|
+
attempts_left = backup_in_progress_retries
|
|
362
|
+
while True:
|
|
363
|
+
try:
|
|
331
364
|
logger.info("open url: " + posturl)
|
|
332
365
|
logger.info("data: " + str(data))
|
|
333
366
|
resp = requests.post(
|
|
@@ -343,12 +376,26 @@ def _backup_db_request(justbackup, data, user, pwd, logger=None):
|
|
|
343
376
|
logger.info(resp.request.url)
|
|
344
377
|
logger.info(resp.request.body)
|
|
345
378
|
logger.info(str([(k, v) for k, v in resp.request.headers.items() if k != "Authorization"]))
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
379
|
+
except Exception as ex:
|
|
380
|
+
mess = "Error requesting backup: %s" % repr(ex)
|
|
381
|
+
logger.exception(mess)
|
|
382
|
+
raise MambuError(mess)
|
|
383
|
+
|
|
384
|
+
if resp.status_code == 202:
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
# Only retry the POST when Mambu says a backup is already in progress.
|
|
388
|
+
# Any other non-202 is a real failure and fails fast, as before.
|
|
389
|
+
if _backup_db_in_progress(resp) and attempts_left > 0:
|
|
390
|
+
attempts_left -= 1
|
|
391
|
+
logger.warning(
|
|
392
|
+
"Mambu backup already in progress (errorCode 1201); waiting "
|
|
393
|
+
"%ss before retrying POST (%s retries left)"
|
|
394
|
+
% (backup_in_progress_wait, attempts_left)
|
|
395
|
+
)
|
|
396
|
+
sleep(backup_in_progress_wait)
|
|
397
|
+
continue
|
|
350
398
|
|
|
351
|
-
if not justbackup and resp.status_code != 202:
|
|
352
399
|
mess = "Error posting request for backup: %s" % resp.content
|
|
353
400
|
logger.error(mess)
|
|
354
401
|
raise MambuCommError(mess)
|
|
@@ -433,6 +480,13 @@ def backup_db(callback, bool_func, output_fname, *args, **kwargs):
|
|
|
433
480
|
callback is called. False to throw error if callback isn't received
|
|
434
481
|
after retries.
|
|
435
482
|
|
|
483
|
+
* backup_in_progress_wait seconds to wait before retrying the POST when
|
|
484
|
+
Mambu rejects it with errorCode 1201 (a backup is already in progress).
|
|
485
|
+
Defaults to 900 (~the observed ~14 min a backup job takes).
|
|
486
|
+
|
|
487
|
+
* backup_in_progress_retries number of times to retry the POST on
|
|
488
|
+
errorCode 1201 before giving up and raising. Defaults to 3.
|
|
489
|
+
|
|
436
490
|
* returns a dictionary with info about the download
|
|
437
491
|
-latest boolean flag, if the db downloaded was the latest or not
|
|
438
492
|
|
|
@@ -448,10 +502,16 @@ def backup_db(callback, bool_func, output_fname, *args, **kwargs):
|
|
|
448
502
|
user,
|
|
449
503
|
pwd,
|
|
450
504
|
data,
|
|
505
|
+
backup_in_progress_wait,
|
|
506
|
+
backup_in_progress_retries,
|
|
451
507
|
) = _backup_db_previous_prep(callback, logger, kwargs)
|
|
452
508
|
|
|
453
509
|
# POST to request Mambu to prepare backup of its own DB
|
|
454
|
-
_backup_db_request(
|
|
510
|
+
_backup_db_request(
|
|
511
|
+
justbackup, data, user, pwd, logger,
|
|
512
|
+
backup_in_progress_wait=backup_in_progress_wait,
|
|
513
|
+
backup_in_progress_retries=backup_in_progress_retries,
|
|
514
|
+
)
|
|
455
515
|
|
|
456
516
|
# wait & timeout mechanism
|
|
457
517
|
data["latest"] = _backup_db_timeout_mechanism(
|