MambuPy 2.3.1__tar.gz → 2.3.3__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 (96) hide show
  1. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/__init__.py +1 -1
  2. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/classes.py +18 -19
  3. {mambupy-2.3.1/mambupy → mambupy-2.3.3/MambuPy}/api/connector/rest.py +32 -9
  4. {mambupy-2.3.1/mambupy → mambupy-2.3.3/MambuPy}/api/entities.py +19 -12
  5. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambuloan.py +3 -2
  6. {mambupy-2.3.1/mambupy → mambupy-2.3.3/MambuPy}/api/mambustruct.py +8 -2
  7. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/mambuutil.py +69 -9
  8. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy.egg-info/PKG-INFO +2 -1
  9. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy.egg-info/requires.txt +1 -0
  10. {mambupy-2.3.1 → mambupy-2.3.3}/PKG-INFO +2 -1
  11. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/__init__.py +1 -1
  12. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/classes.py +18 -19
  13. {mambupy-2.3.1/MambuPy → mambupy-2.3.3/mambupy}/api/connector/rest.py +32 -9
  14. {mambupy-2.3.1/MambuPy → mambupy-2.3.3/mambupy}/api/entities.py +19 -12
  15. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambuloan.py +3 -2
  16. {mambupy-2.3.1/MambuPy → mambupy-2.3.3/mambupy}/api/mambustruct.py +8 -2
  17. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/mambuutil.py +69 -9
  18. {mambupy-2.3.1 → mambupy-2.3.3}/pyproject.toml +1 -0
  19. {mambupy-2.3.1 → mambupy-2.3.3}/LICENSE +0 -0
  20. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/__init__.py +0 -0
  21. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/connector/__init__.py +0 -0
  22. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/connector/mambuconnector.py +0 -0
  23. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/interfaces.py +0 -0
  24. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambubranch.py +0 -0
  25. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambucentre.py +0 -0
  26. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambuclient.py +0 -0
  27. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambucustomfield.py +0 -0
  28. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambugroup.py +0 -0
  29. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambuproduct.py +0 -0
  30. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mamburole.py +0 -0
  31. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambutask.py +0 -0
  32. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambutransaction.py +0 -0
  33. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/mambuuser.py +0 -0
  34. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/api/vos.py +0 -0
  35. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/mambuconfig.py +0 -0
  36. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/mambugeturl.py +0 -0
  37. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/__init__.py +0 -0
  38. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_activities.py +0 -0
  39. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_addresses.py +0 -0
  40. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_branches.py +0 -0
  41. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_centres.py +0 -0
  42. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_clients.py +0 -0
  43. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_customfields.py +0 -0
  44. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_dummies.py +0 -0
  45. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_groups.py +0 -0
  46. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_loans.py +0 -0
  47. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_mambu.py +0 -0
  48. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_orm.py +0 -0
  49. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_tasks.py +0 -0
  50. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/orm/schema_users.py +0 -0
  51. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/rest/__init__.py +0 -0
  52. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/rest/mambuactivity.py +0 -0
  53. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/utils/__init__.py +0 -0
  54. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/utils/update_user_roles.py +0 -0
  55. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy/utils/userdeactivate.py +0 -0
  56. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy.egg-info/SOURCES.txt +0 -0
  57. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy.egg-info/dependency_links.txt +0 -0
  58. {mambupy-2.3.1 → mambupy-2.3.3}/MambuPy.egg-info/top_level.txt +0 -0
  59. {mambupy-2.3.1 → mambupy-2.3.3}/README.md +0 -0
  60. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/__init__.py +0 -0
  61. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/connector/__init__.py +0 -0
  62. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/connector/mambuconnector.py +0 -0
  63. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/interfaces.py +0 -0
  64. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambubranch.py +0 -0
  65. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambucentre.py +0 -0
  66. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambuclient.py +0 -0
  67. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambucustomfield.py +0 -0
  68. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambugroup.py +0 -0
  69. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambuproduct.py +0 -0
  70. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mamburole.py +0 -0
  71. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambutask.py +0 -0
  72. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambutransaction.py +0 -0
  73. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/mambuuser.py +0 -0
  74. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/api/vos.py +0 -0
  75. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/mambuconfig.py +0 -0
  76. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/mambugeturl.py +0 -0
  77. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/__init__.py +0 -0
  78. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_activities.py +0 -0
  79. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_addresses.py +0 -0
  80. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_branches.py +0 -0
  81. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_centres.py +0 -0
  82. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_clients.py +0 -0
  83. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_customfields.py +0 -0
  84. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_dummies.py +0 -0
  85. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_groups.py +0 -0
  86. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_loans.py +0 -0
  87. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_mambu.py +0 -0
  88. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_orm.py +0 -0
  89. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_tasks.py +0 -0
  90. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/orm/schema_users.py +0 -0
  91. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/rest/__init__.py +0 -0
  92. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/rest/mambuactivity.py +0 -0
  93. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/utils/__init__.py +0 -0
  94. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/utils/update_user_roles.py +0 -0
  95. {mambupy-2.3.1 → mambupy-2.3.3}/mambupy/utils/userdeactivate.py +0 -0
  96. {mambupy-2.3.1 → mambupy-2.3.3}/setup.cfg +0 -0
@@ -37,7 +37,7 @@ import sys
37
37
  from importlib.abc import MetaPathFinder
38
38
  from importlib.util import spec_from_file_location
39
39
 
40
- __version__ = "2.3.1"
40
+ __version__ = "2.3.3"
41
41
  """The version of this module."""
42
42
 
43
43
 
@@ -31,32 +31,31 @@ class MambuMapObj:
31
31
  for key, val in kwargs.items():
32
32
  self._attrs[key] = val
33
33
 
34
- def __getattribute__(self, name):
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
- # first, try to read 'name' as if it's a property of the object
44
- # if it doesn't exists as property, AttributeError raises
45
- return object.__getattribute__(self, name)
46
- except AttributeError:
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 last one means
52
- # that if 'name' is not a property of the object too,
53
- # AttributeError will raise by default)
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
@@ -9,11 +9,15 @@ Currently supports REST.
9
9
 
10
10
  import base64
11
11
  import copy
12
+ import datetime
13
+ import enum
12
14
  import json
13
15
  import mimetypes
16
+ import orjson
14
17
  import os
15
18
  import re
16
19
  import uuid
20
+ from decimal import Decimal
17
21
 
18
22
  import requests
19
23
  from requests_toolbelt.multipart.encoder import MultipartEncoder
@@ -43,9 +47,27 @@ from MambuPy.mambuutil import (
43
47
  logger = setup_logging(__name__)
44
48
 
45
49
 
50
+ class _MambuJSONEncoder(json.JSONEncoder):
51
+ """JSON encoder that handles Python types not natively serializable by json.dumps."""
52
+
53
+ def default(self, obj):
54
+ if isinstance(obj, (datetime.datetime, datetime.date)):
55
+ return obj.isoformat()
56
+ if isinstance(obj, enum.Enum):
57
+ return obj.value
58
+ if isinstance(obj, Decimal):
59
+ return float(obj)
60
+ return super().default(obj)
61
+
62
+
46
63
  def _configure_retry_strategy(session, retries=5):
47
64
  """Configure retry strategy for a session.
48
65
 
66
+ El HTTPAdapter se construye con pool_maxsize=50 para que los flujos
67
+ que despachan multiples requests concurrentes contra Mambu (p.ej.
68
+ fan-out de aprobaciones, cargar_datos paralelo) no queden encolados
69
+ sobre el default de urllib3 de 10 conexiones por host.
70
+
49
71
  Args:
50
72
  session (requests.Session): The session to configure
51
73
  retries (int, optional): Number of retries. Defaults to 5.
@@ -65,7 +87,11 @@ def _configure_retry_strategy(session, retries=5):
65
87
  "PATCH",
66
88
  ],
67
89
  )
68
- adapter = HTTPAdapter(max_retries=retry_strategy)
90
+ adapter = HTTPAdapter(
91
+ max_retries=retry_strategy,
92
+ pool_connections=50,
93
+ pool_maxsize=50,
94
+ )
69
95
  session.mount("https://", adapter)
70
96
  session.mount("http://", adapter)
71
97
 
@@ -154,11 +180,8 @@ class MambuConnectorREST(MambuConnector, MambuConnectorReader, MambuConnectorWri
154
180
  return params
155
181
 
156
182
  def __request_data(self, data):
157
- if data is not None:
158
- try:
159
- data = json.dumps(data)
160
- except TypeError:
161
- data = data
183
+ if isinstance(data, (dict, list)):
184
+ data = json.dumps(data, cls=_MambuJSONEncoder)
162
185
  return data
163
186
 
164
187
  def __request(self, method, url, params=None, data=None, content_type=None):
@@ -211,7 +234,7 @@ url %s, params %s, data %s, headers %s",
211
234
  if hasattr(resp, "content"): # pragma: no cover
212
235
  logger.warning("HTTPError, resp content: %s", resp.content)
213
236
  try:
214
- content = json.loads(resp.content.decode())
237
+ content = orjson.loads(resp.content.decode())
215
238
  except ValueError:
216
239
  # in case resp.content doesn't conforms to json
217
240
  content = {
@@ -287,7 +310,7 @@ url %s, params %s, data %s, headers %s",
287
310
  def __list_request_cat_response(self, list_resp, resp):
288
311
  if list_resp == b"":
289
312
  list_resp = resp
290
- elif len(json.loads(resp.decode())) > 0:
313
+ elif len(orjson.loads(resp.decode())) > 0:
291
314
  list_resp = list_resp[:-1] + b"," + resp[1:]
292
315
 
293
316
  return list_resp
@@ -327,7 +350,7 @@ url %s, params %s, data %s, headers %s",
327
350
  method, url, params=copy.copy(params), data=copy.copy(data)
328
351
  )
329
352
 
330
- jsonresp = list(json.loads(resp.decode()))
353
+ jsonresp = list(orjson.loads(resp.decode()))
331
354
  if len(jsonresp) < limit:
332
355
  window = False
333
356
 
@@ -6,8 +6,11 @@
6
6
  """
7
7
 
8
8
  import copy
9
+ import datetime
10
+ import enum
9
11
  from importlib import import_module
10
12
  import json
13
+ import orjson
11
14
  import time
12
15
 
13
16
  from .classes import GenericClass
@@ -238,7 +241,7 @@ class MambuEntity(MambuStruct):
238
241
 
239
242
  logger.debug("request several entities %s", cls.__name__)
240
243
  list_resp = get_func(prefix, **params)
241
- jsonresp = list(json.loads(list_resp.decode()))
244
+ jsonresp = list(orjson.loads(list_resp.decode()))
242
245
  logger.debug("%s, %s retrieved", cls.__name__, len(jsonresp))
243
246
 
244
247
  elements = []
@@ -310,8 +313,8 @@ class MambuEntity(MambuStruct):
310
313
  instance = cls.__build_object(
311
314
  connector=connector,
312
315
  resp=resp,
313
- attrs=dict(json.loads(resp.decode())),
314
- tzattrs=dict(json.loads(resp.decode())),
316
+ attrs=dict(orjson.loads(resp.decode())),
317
+ tzattrs=dict(orjson.loads(resp.decode())),
315
318
  get_entities=get_entities,
316
319
  detailsLevel=detailsLevel,
317
320
  debug=debug,
@@ -353,8 +356,8 @@ class MambuEntity(MambuStruct):
353
356
  self.id, prefix=self._prefix, detailsLevel=detailsLevel
354
357
  )
355
358
 
356
- self._attrs.update(dict(json.loads(self._resp.decode())))
357
- self._tzattrs = dict(json.loads(self._resp.decode()))
359
+ self._attrs.update(dict(orjson.loads(self._resp.decode())))
360
+ self._tzattrs = dict(orjson.loads(self._resp.decode()))
358
361
  self._convertDict2Attrs()
359
362
  self._extractCustomFields()
360
363
  self._extractVOs()
@@ -470,8 +473,8 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
470
473
  self._resp = self._connector.mambu_create(
471
474
  self._prefix, copy.deepcopy(self._attrs), **kwargs
472
475
  )
473
- self._attrs.update(dict(json.loads(self._resp.decode())))
474
- self._tzattrs = dict(json.loads(self._resp.decode()))
476
+ self._attrs.update(dict(orjson.loads(self._resp.decode())))
477
+ self._tzattrs = dict(orjson.loads(self._resp.decode()))
475
478
  self._detailsLevel = "FULL"
476
479
  except MambuError:
477
480
  raise
@@ -503,6 +506,10 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
503
506
  val = attrs[field]["value"]
504
507
  except (TypeError, KeyError):
505
508
  val = attrs[field]
509
+ if isinstance(val, (datetime.datetime, datetime.date)):
510
+ val = val.isoformat()
511
+ elif isinstance(val, enum.Enum):
512
+ val = val.value
506
513
  return (operation, path, val)
507
514
 
508
515
  def __patch_field_op(self, field, original_attrs):
@@ -567,7 +574,7 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
567
574
  # build fields_ops param with what detected using previous rules
568
575
  # strings: (OP, PATH) (remove) or (OP, PATH, VALUE) (all else)
569
576
  fields_ops = []
570
- original_attrs = dict(json.loads(self._resp.decode()))
577
+ original_attrs = dict(orjson.loads(self._resp.decode()))
571
578
  self._extractCustomFields(original_attrs)
572
579
  self._updateVOs()
573
580
  for field in fields:
@@ -703,7 +710,7 @@ class MambuEntityAttachable(MambuStruct, MambuAttachable):
703
710
  notes=notes,
704
711
  )
705
712
 
706
- doc = MambuDocument(**dict(json.loads(response.decode())))
713
+ doc = MambuDocument(**dict(orjson.loads(response.decode())))
707
714
  self._attachments[str(doc["id"])] = doc
708
715
 
709
716
  return response
@@ -736,7 +743,7 @@ class MambuEntityAttachable(MambuStruct, MambuAttachable):
736
743
  paginationDetails=paginationDetails,
737
744
  )
738
745
 
739
- metadata_list = json.loads(response.decode())
746
+ metadata_list = orjson.loads(response.decode())
740
747
 
741
748
  for metadata in metadata_list:
742
749
  doc = MambuDocument(**metadata)
@@ -825,7 +832,7 @@ class MambuEntityCommentable(MambuStruct, MambuCommentable):
825
832
  paginationDetails=paginationDetails,
826
833
  )
827
834
 
828
- comments_list = json.loads(response.decode())
835
+ comments_list = orjson.loads(response.decode())
829
836
 
830
837
  for comment in comments_list:
831
838
  comm = MambuComment(**comment)
@@ -848,7 +855,7 @@ class MambuEntityCommentable(MambuStruct, MambuCommentable):
848
855
  owner_id=self.id, owner_type=self._ownerType, text=comment
849
856
  )
850
857
 
851
- comment = MambuComment(**dict(json.loads(response.decode())))
858
+ comment = MambuComment(**dict(orjson.loads(response.decode())))
852
859
  self._comments.insert(0, comment)
853
860
 
854
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,
@@ -117,7 +118,7 @@ class MambuLoan(
117
118
  """Retrieves the installments schedule."""
118
119
  resp = self._connector.mambu_loanaccount_getSchedule(self.id)
119
120
 
120
- installments = json.loads(resp.decode())["installments"]
121
+ installments = orjson.loads(resp.decode())["installments"]
121
122
 
122
123
  self.schedule = []
123
124
  for installment in installments:
@@ -144,7 +145,7 @@ class MambuLoan(
144
145
  resp = self._connector.mambu_change_state(
145
146
  entid=self.id, prefix=self._prefix, action=action, notes=notes,
146
147
  )
147
- resp = json.loads(resp)
148
+ resp = orjson.loads(resp)
148
149
  self.accountState = resp["accountState"]
149
150
  else:
150
151
  raise MambuPyError(
@@ -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 __getattribute__(self, name):
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().__getattribute__(name)
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:]
@@ -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 _backup_db_request(justbackup, data, user, pwd, logger=None):
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
- if not justbackup:
330
- posturl = iri_to_uri(getmambuurl() + "database/backup")
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
- except Exception as ex:
347
- mess = "Error requesting backup: %s" % repr(ex)
348
- logger.exception(mess)
349
- raise MambuError(mess)
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(justbackup, data, user, pwd, logger)
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MambuPy
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: A python library for using Mambu APIs.
5
5
  Author-email: "Javier Novoa C." <jstitch@gmail.com>
6
6
  License: LICENSE
@@ -701,6 +701,7 @@ Requires-Dist: PyYAML==6.0.1
701
701
  Requires-Dist: requests==2.31.0
702
702
  Requires-Dist: requests_toolbelt==1.0.0
703
703
  Requires-Dist: dateutils==0.6.12
704
+ Requires-Dist: orjson==3.9.7
704
705
  Provides-Extra: full
705
706
  Requires-Dist: SQLAlchemy==1.3.6; extra == "full"
706
707
  Requires-Dist: mysqlclient==2.1.0; extra == "full"
@@ -2,6 +2,7 @@ PyYAML==6.0.1
2
2
  requests==2.31.0
3
3
  requests_toolbelt==1.0.0
4
4
  dateutils==0.6.12
5
+ orjson==3.9.7
5
6
 
6
7
  [deploy]
7
8
  build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MambuPy
3
- Version: 2.3.1
3
+ Version: 2.3.3
4
4
  Summary: A python library for using Mambu APIs.
5
5
  Author-email: "Javier Novoa C." <jstitch@gmail.com>
6
6
  License: LICENSE
@@ -701,6 +701,7 @@ Requires-Dist: PyYAML==6.0.1
701
701
  Requires-Dist: requests==2.31.0
702
702
  Requires-Dist: requests_toolbelt==1.0.0
703
703
  Requires-Dist: dateutils==0.6.12
704
+ Requires-Dist: orjson==3.9.7
704
705
  Provides-Extra: full
705
706
  Requires-Dist: SQLAlchemy==1.3.6; extra == "full"
706
707
  Requires-Dist: mysqlclient==2.1.0; extra == "full"
@@ -37,7 +37,7 @@ import sys
37
37
  from importlib.abc import MetaPathFinder
38
38
  from importlib.util import spec_from_file_location
39
39
 
40
- __version__ = "2.3.1"
40
+ __version__ = "2.3.3"
41
41
  """The version of this module."""
42
42
 
43
43
 
@@ -31,32 +31,31 @@ class MambuMapObj:
31
31
  for key, val in kwargs.items():
32
32
  self._attrs[key] = val
33
33
 
34
- def __getattribute__(self, name):
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
- # first, try to read 'name' as if it's a property of the object
44
- # if it doesn't exists as property, AttributeError raises
45
- return object.__getattribute__(self, name)
46
- except AttributeError:
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 last one means
52
- # that if 'name' is not a property of the object too,
53
- # AttributeError will raise by default)
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
@@ -9,11 +9,15 @@ Currently supports REST.
9
9
 
10
10
  import base64
11
11
  import copy
12
+ import datetime
13
+ import enum
12
14
  import json
13
15
  import mimetypes
16
+ import orjson
14
17
  import os
15
18
  import re
16
19
  import uuid
20
+ from decimal import Decimal
17
21
 
18
22
  import requests
19
23
  from requests_toolbelt.multipart.encoder import MultipartEncoder
@@ -43,9 +47,27 @@ from MambuPy.mambuutil import (
43
47
  logger = setup_logging(__name__)
44
48
 
45
49
 
50
+ class _MambuJSONEncoder(json.JSONEncoder):
51
+ """JSON encoder that handles Python types not natively serializable by json.dumps."""
52
+
53
+ def default(self, obj):
54
+ if isinstance(obj, (datetime.datetime, datetime.date)):
55
+ return obj.isoformat()
56
+ if isinstance(obj, enum.Enum):
57
+ return obj.value
58
+ if isinstance(obj, Decimal):
59
+ return float(obj)
60
+ return super().default(obj)
61
+
62
+
46
63
  def _configure_retry_strategy(session, retries=5):
47
64
  """Configure retry strategy for a session.
48
65
 
66
+ El HTTPAdapter se construye con pool_maxsize=50 para que los flujos
67
+ que despachan multiples requests concurrentes contra Mambu (p.ej.
68
+ fan-out de aprobaciones, cargar_datos paralelo) no queden encolados
69
+ sobre el default de urllib3 de 10 conexiones por host.
70
+
49
71
  Args:
50
72
  session (requests.Session): The session to configure
51
73
  retries (int, optional): Number of retries. Defaults to 5.
@@ -65,7 +87,11 @@ def _configure_retry_strategy(session, retries=5):
65
87
  "PATCH",
66
88
  ],
67
89
  )
68
- adapter = HTTPAdapter(max_retries=retry_strategy)
90
+ adapter = HTTPAdapter(
91
+ max_retries=retry_strategy,
92
+ pool_connections=50,
93
+ pool_maxsize=50,
94
+ )
69
95
  session.mount("https://", adapter)
70
96
  session.mount("http://", adapter)
71
97
 
@@ -154,11 +180,8 @@ class MambuConnectorREST(MambuConnector, MambuConnectorReader, MambuConnectorWri
154
180
  return params
155
181
 
156
182
  def __request_data(self, data):
157
- if data is not None:
158
- try:
159
- data = json.dumps(data)
160
- except TypeError:
161
- data = data
183
+ if isinstance(data, (dict, list)):
184
+ data = json.dumps(data, cls=_MambuJSONEncoder)
162
185
  return data
163
186
 
164
187
  def __request(self, method, url, params=None, data=None, content_type=None):
@@ -211,7 +234,7 @@ url %s, params %s, data %s, headers %s",
211
234
  if hasattr(resp, "content"): # pragma: no cover
212
235
  logger.warning("HTTPError, resp content: %s", resp.content)
213
236
  try:
214
- content = json.loads(resp.content.decode())
237
+ content = orjson.loads(resp.content.decode())
215
238
  except ValueError:
216
239
  # in case resp.content doesn't conforms to json
217
240
  content = {
@@ -287,7 +310,7 @@ url %s, params %s, data %s, headers %s",
287
310
  def __list_request_cat_response(self, list_resp, resp):
288
311
  if list_resp == b"":
289
312
  list_resp = resp
290
- elif len(json.loads(resp.decode())) > 0:
313
+ elif len(orjson.loads(resp.decode())) > 0:
291
314
  list_resp = list_resp[:-1] + b"," + resp[1:]
292
315
 
293
316
  return list_resp
@@ -327,7 +350,7 @@ url %s, params %s, data %s, headers %s",
327
350
  method, url, params=copy.copy(params), data=copy.copy(data)
328
351
  )
329
352
 
330
- jsonresp = list(json.loads(resp.decode()))
353
+ jsonresp = list(orjson.loads(resp.decode()))
331
354
  if len(jsonresp) < limit:
332
355
  window = False
333
356
 
@@ -6,8 +6,11 @@
6
6
  """
7
7
 
8
8
  import copy
9
+ import datetime
10
+ import enum
9
11
  from importlib import import_module
10
12
  import json
13
+ import orjson
11
14
  import time
12
15
 
13
16
  from .classes import GenericClass
@@ -238,7 +241,7 @@ class MambuEntity(MambuStruct):
238
241
 
239
242
  logger.debug("request several entities %s", cls.__name__)
240
243
  list_resp = get_func(prefix, **params)
241
- jsonresp = list(json.loads(list_resp.decode()))
244
+ jsonresp = list(orjson.loads(list_resp.decode()))
242
245
  logger.debug("%s, %s retrieved", cls.__name__, len(jsonresp))
243
246
 
244
247
  elements = []
@@ -310,8 +313,8 @@ class MambuEntity(MambuStruct):
310
313
  instance = cls.__build_object(
311
314
  connector=connector,
312
315
  resp=resp,
313
- attrs=dict(json.loads(resp.decode())),
314
- tzattrs=dict(json.loads(resp.decode())),
316
+ attrs=dict(orjson.loads(resp.decode())),
317
+ tzattrs=dict(orjson.loads(resp.decode())),
315
318
  get_entities=get_entities,
316
319
  detailsLevel=detailsLevel,
317
320
  debug=debug,
@@ -353,8 +356,8 @@ class MambuEntity(MambuStruct):
353
356
  self.id, prefix=self._prefix, detailsLevel=detailsLevel
354
357
  )
355
358
 
356
- self._attrs.update(dict(json.loads(self._resp.decode())))
357
- self._tzattrs = dict(json.loads(self._resp.decode()))
359
+ self._attrs.update(dict(orjson.loads(self._resp.decode())))
360
+ self._tzattrs = dict(orjson.loads(self._resp.decode()))
358
361
  self._convertDict2Attrs()
359
362
  self._extractCustomFields()
360
363
  self._extractVOs()
@@ -470,8 +473,8 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
470
473
  self._resp = self._connector.mambu_create(
471
474
  self._prefix, copy.deepcopy(self._attrs), **kwargs
472
475
  )
473
- self._attrs.update(dict(json.loads(self._resp.decode())))
474
- self._tzattrs = dict(json.loads(self._resp.decode()))
476
+ self._attrs.update(dict(orjson.loads(self._resp.decode())))
477
+ self._tzattrs = dict(orjson.loads(self._resp.decode()))
475
478
  self._detailsLevel = "FULL"
476
479
  except MambuError:
477
480
  raise
@@ -503,6 +506,10 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
503
506
  val = attrs[field]["value"]
504
507
  except (TypeError, KeyError):
505
508
  val = attrs[field]
509
+ if isinstance(val, (datetime.datetime, datetime.date)):
510
+ val = val.isoformat()
511
+ elif isinstance(val, enum.Enum):
512
+ val = val.value
506
513
  return (operation, path, val)
507
514
 
508
515
  def __patch_field_op(self, field, original_attrs):
@@ -567,7 +574,7 @@ class MambuEntityWritable(MambuStruct, MambuWritable):
567
574
  # build fields_ops param with what detected using previous rules
568
575
  # strings: (OP, PATH) (remove) or (OP, PATH, VALUE) (all else)
569
576
  fields_ops = []
570
- original_attrs = dict(json.loads(self._resp.decode()))
577
+ original_attrs = dict(orjson.loads(self._resp.decode()))
571
578
  self._extractCustomFields(original_attrs)
572
579
  self._updateVOs()
573
580
  for field in fields:
@@ -703,7 +710,7 @@ class MambuEntityAttachable(MambuStruct, MambuAttachable):
703
710
  notes=notes,
704
711
  )
705
712
 
706
- doc = MambuDocument(**dict(json.loads(response.decode())))
713
+ doc = MambuDocument(**dict(orjson.loads(response.decode())))
707
714
  self._attachments[str(doc["id"])] = doc
708
715
 
709
716
  return response
@@ -736,7 +743,7 @@ class MambuEntityAttachable(MambuStruct, MambuAttachable):
736
743
  paginationDetails=paginationDetails,
737
744
  )
738
745
 
739
- metadata_list = json.loads(response.decode())
746
+ metadata_list = orjson.loads(response.decode())
740
747
 
741
748
  for metadata in metadata_list:
742
749
  doc = MambuDocument(**metadata)
@@ -825,7 +832,7 @@ class MambuEntityCommentable(MambuStruct, MambuCommentable):
825
832
  paginationDetails=paginationDetails,
826
833
  )
827
834
 
828
- comments_list = json.loads(response.decode())
835
+ comments_list = orjson.loads(response.decode())
829
836
 
830
837
  for comment in comments_list:
831
838
  comm = MambuComment(**comment)
@@ -848,7 +855,7 @@ class MambuEntityCommentable(MambuStruct, MambuCommentable):
848
855
  owner_id=self.id, owner_type=self._ownerType, text=comment
849
856
  )
850
857
 
851
- comment = MambuComment(**dict(json.loads(response.decode())))
858
+ comment = MambuComment(**dict(orjson.loads(response.decode())))
852
859
  self._comments.insert(0, comment)
853
860
 
854
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,
@@ -117,7 +118,7 @@ class MambuLoan(
117
118
  """Retrieves the installments schedule."""
118
119
  resp = self._connector.mambu_loanaccount_getSchedule(self.id)
119
120
 
120
- installments = json.loads(resp.decode())["installments"]
121
+ installments = orjson.loads(resp.decode())["installments"]
121
122
 
122
123
  self.schedule = []
123
124
  for installment in installments:
@@ -144,7 +145,7 @@ class MambuLoan(
144
145
  resp = self._connector.mambu_change_state(
145
146
  entid=self.id, prefix=self._prefix, action=action, notes=notes,
146
147
  )
147
- resp = json.loads(resp)
148
+ resp = orjson.loads(resp)
148
149
  self.accountState = resp["accountState"]
149
150
  else:
150
151
  raise MambuPyError(
@@ -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 __getattribute__(self, name):
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().__getattribute__(name)
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:]
@@ -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 _backup_db_request(justbackup, data, user, pwd, logger=None):
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
- if not justbackup:
330
- posturl = iri_to_uri(getmambuurl() + "database/backup")
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
- except Exception as ex:
347
- mess = "Error requesting backup: %s" % repr(ex)
348
- logger.exception(mess)
349
- raise MambuError(mess)
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(justbackup, data, user, pwd, logger)
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(
@@ -11,6 +11,7 @@ dependencies = [
11
11
  "requests==2.31.0",
12
12
  "requests_toolbelt==1.0.0",
13
13
  "dateutils==0.6.12",
14
+ "orjson==3.9.7",
14
15
  ]
15
16
  keywords = ["mambu"]
16
17
  classifiers = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes