MambuPy 2.3.2__py3-none-any.whl → 2.3.3__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.
MambuPy/__init__.py CHANGED
@@ -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.2"
40
+ __version__ = "2.3.3"
41
41
  """The version of this module."""
42
42
 
43
43
 
MambuPy/api/classes.py CHANGED
@@ -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
@@ -13,6 +13,7 @@ import datetime
13
13
  import enum
14
14
  import json
15
15
  import mimetypes
16
+ import orjson
16
17
  import os
17
18
  import re
18
19
  import uuid
@@ -62,6 +63,11 @@ class _MambuJSONEncoder(json.JSONEncoder):
62
63
  def _configure_retry_strategy(session, retries=5):
63
64
  """Configure retry strategy for a session.
64
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
+
65
71
  Args:
66
72
  session (requests.Session): The session to configure
67
73
  retries (int, optional): Number of retries. Defaults to 5.
@@ -81,7 +87,11 @@ def _configure_retry_strategy(session, retries=5):
81
87
  "PATCH",
82
88
  ],
83
89
  )
84
- adapter = HTTPAdapter(max_retries=retry_strategy)
90
+ adapter = HTTPAdapter(
91
+ max_retries=retry_strategy,
92
+ pool_connections=50,
93
+ pool_maxsize=50,
94
+ )
85
95
  session.mount("https://", adapter)
86
96
  session.mount("http://", adapter)
87
97
 
@@ -224,7 +234,7 @@ url %s, params %s, data %s, headers %s",
224
234
  if hasattr(resp, "content"): # pragma: no cover
225
235
  logger.warning("HTTPError, resp content: %s", resp.content)
226
236
  try:
227
- content = json.loads(resp.content.decode())
237
+ content = orjson.loads(resp.content.decode())
228
238
  except ValueError:
229
239
  # in case resp.content doesn't conforms to json
230
240
  content = {
@@ -300,7 +310,7 @@ url %s, params %s, data %s, headers %s",
300
310
  def __list_request_cat_response(self, list_resp, resp):
301
311
  if list_resp == b"":
302
312
  list_resp = resp
303
- elif len(json.loads(resp.decode())) > 0:
313
+ elif len(orjson.loads(resp.decode())) > 0:
304
314
  list_resp = list_resp[:-1] + b"," + resp[1:]
305
315
 
306
316
  return list_resp
@@ -340,7 +350,7 @@ url %s, params %s, data %s, headers %s",
340
350
  method, url, params=copy.copy(params), data=copy.copy(data)
341
351
  )
342
352
 
343
- jsonresp = list(json.loads(resp.decode()))
353
+ jsonresp = list(orjson.loads(resp.decode()))
344
354
  if len(jsonresp) < limit:
345
355
  window = False
346
356
 
MambuPy/api/entities.py CHANGED
@@ -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(json.loads(list_resp.decode()))
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(json.loads(resp.decode())),
316
- tzattrs=dict(json.loads(resp.decode())),
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(json.loads(self._resp.decode())))
359
- 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()))
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(json.loads(self._resp.decode())))
476
- 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()))
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(json.loads(self._resp.decode()))
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(json.loads(response.decode())))
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 = json.loads(response.decode())
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 = json.loads(response.decode())
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(json.loads(response.decode())))
858
+ comment = MambuComment(**dict(orjson.loads(response.decode())))
858
859
  self._comments.insert(0, comment)
859
860
 
860
861
  return response
MambuPy/api/mambuloan.py CHANGED
@@ -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:]
MambuPy/mambuutil.py CHANGED
@@ -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(
mambupy/__init__.py CHANGED
@@ -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.2"
40
+ __version__ = "2.3.3"
41
41
  """The version of this module."""
42
42
 
43
43
 
mambupy/api/classes.py CHANGED
@@ -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
@@ -13,6 +13,7 @@ import datetime
13
13
  import enum
14
14
  import json
15
15
  import mimetypes
16
+ import orjson
16
17
  import os
17
18
  import re
18
19
  import uuid
@@ -62,6 +63,11 @@ class _MambuJSONEncoder(json.JSONEncoder):
62
63
  def _configure_retry_strategy(session, retries=5):
63
64
  """Configure retry strategy for a session.
64
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
+
65
71
  Args:
66
72
  session (requests.Session): The session to configure
67
73
  retries (int, optional): Number of retries. Defaults to 5.
@@ -81,7 +87,11 @@ def _configure_retry_strategy(session, retries=5):
81
87
  "PATCH",
82
88
  ],
83
89
  )
84
- adapter = HTTPAdapter(max_retries=retry_strategy)
90
+ adapter = HTTPAdapter(
91
+ max_retries=retry_strategy,
92
+ pool_connections=50,
93
+ pool_maxsize=50,
94
+ )
85
95
  session.mount("https://", adapter)
86
96
  session.mount("http://", adapter)
87
97
 
@@ -224,7 +234,7 @@ url %s, params %s, data %s, headers %s",
224
234
  if hasattr(resp, "content"): # pragma: no cover
225
235
  logger.warning("HTTPError, resp content: %s", resp.content)
226
236
  try:
227
- content = json.loads(resp.content.decode())
237
+ content = orjson.loads(resp.content.decode())
228
238
  except ValueError:
229
239
  # in case resp.content doesn't conforms to json
230
240
  content = {
@@ -300,7 +310,7 @@ url %s, params %s, data %s, headers %s",
300
310
  def __list_request_cat_response(self, list_resp, resp):
301
311
  if list_resp == b"":
302
312
  list_resp = resp
303
- elif len(json.loads(resp.decode())) > 0:
313
+ elif len(orjson.loads(resp.decode())) > 0:
304
314
  list_resp = list_resp[:-1] + b"," + resp[1:]
305
315
 
306
316
  return list_resp
@@ -340,7 +350,7 @@ url %s, params %s, data %s, headers %s",
340
350
  method, url, params=copy.copy(params), data=copy.copy(data)
341
351
  )
342
352
 
343
- jsonresp = list(json.loads(resp.decode()))
353
+ jsonresp = list(orjson.loads(resp.decode()))
344
354
  if len(jsonresp) < limit:
345
355
  window = False
346
356
 
mambupy/api/entities.py CHANGED
@@ -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(json.loads(list_resp.decode()))
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(json.loads(resp.decode())),
316
- tzattrs=dict(json.loads(resp.decode())),
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(json.loads(self._resp.decode())))
359
- 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()))
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(json.loads(self._resp.decode())))
476
- 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()))
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(json.loads(self._resp.decode()))
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(json.loads(response.decode())))
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 = json.loads(response.decode())
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 = json.loads(response.decode())
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(json.loads(response.decode())))
858
+ comment = MambuComment(**dict(orjson.loads(response.decode())))
858
859
  self._comments.insert(0, comment)
859
860
 
860
861
  return response
mambupy/api/mambuloan.py CHANGED
@@ -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:]
mambupy/mambuutil.py CHANGED
@@ -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.2
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"
@@ -1,27 +1,27 @@
1
- MambuPy/__init__.py,sha256=Ps-C49zn-vyag70kZhy5JhIegv5l2FehezEN7LpavcM,1548
1
+ MambuPy/__init__.py,sha256=a6Mg2XoeK9HEyfcabXM2mDQqn0twIDXQsa3DS9jO9Mc,1548
2
2
  MambuPy/mambuconfig.py,sha256=6T8jT9LvB2e2HkR6DVD-BwksOgucqo617_xYIlWrc3M,7441
3
3
  MambuPy/mambugeturl.py,sha256=D7jTr3tY1nPMXER-s5Uqaoi7_L6WjHTLLnqRYYWAh2Q,23405
4
- MambuPy/mambuutil.py,sha256=tcElYEix-GYsjfuCsKxrBaEsP3IB6PXv6fop7Ln5-9c,13455
4
+ MambuPy/mambuutil.py,sha256=7svr8agXPz-a0v6x7a0yFcO4NQBP11fkmWB3zSmH8DU,15648
5
5
  MambuPy/api/__init__.py,sha256=B8lABs6zq9owokHc4VGDQ_AirzszWiu7EAYM-FsaSWE,621
6
- MambuPy/api/classes.py,sha256=0LnsG6vyB7HQk11SaL43X6ftdkv7u6KyL7ynNvLQUTg,8030
7
- MambuPy/api/entities.py,sha256=Vwv-DqmaFkjpkCWhTcvoVzVuEb34aGgjXkbcOBzrkF0,34237
6
+ MambuPy/api/classes.py,sha256=W5aDtx3kM1YOChJkkYBTGKPpbMu_YycGxTd7Syz7ew4,7943
7
+ MambuPy/api/entities.py,sha256=XRaPGhQ5FhXXwrfv9eN_3IPEydS-0yciy46pfqwuDH4,34275
8
8
  MambuPy/api/interfaces.py,sha256=vRf1NDskUum-rnyyCygCVlYvuRWu_QvsEDwekcYK308,4870
9
9
  MambuPy/api/mambubranch.py,sha256=yfpMN_OWFIaa_EGpEmOzoH-_uFkWExfuq5IDVDp61F8,778
10
10
  MambuPy/api/mambucentre.py,sha256=2ferlA_8GTAoom-jwjBKVMUHGV0Um59vSl6_pdesT-4,952
11
11
  MambuPy/api/mambuclient.py,sha256=JEtajMTzCM3lDKIzVEGS1j7wMxo6y9zryztaXoBC6Ms,2006
12
12
  MambuPy/api/mambucustomfield.py,sha256=61dEHVmB4gcchGWJ51YVrcxXVyQfqmVffVWWAaBdtpQ,2233
13
13
  MambuPy/api/mambugroup.py,sha256=uOcevAlmxJajCLsAwsgqLbTxVmwssKZmZn-q02FaHDQ,1528
14
- MambuPy/api/mambuloan.py,sha256=cammMQc7qmo9AB8mBzL-T7dy5zweKTFgs1NJIBDs6mM,11641
14
+ MambuPy/api/mambuloan.py,sha256=Apg-H8XVEAtmyFWBcaYWGpUbHJQy4j_iDRCokjpJ5lQ,11659
15
15
  MambuPy/api/mambuproduct.py,sha256=xD8BQSVuGQgjij0_u-wTR5zL_NRNmeuDK9-2GQNwknI,2871
16
16
  MambuPy/api/mamburole.py,sha256=TeeoM2MqhB1cd6TV_7nk1e6sVQRzFJehqTK8nEzqw7Y,565
17
- MambuPy/api/mambustruct.py,sha256=jqT4LCTOGA0zIICVvacVyKTTQ4DN2jAgU2m57XG5Soo,30435
17
+ MambuPy/api/mambustruct.py,sha256=RQi837WYHEf9hAfZQp2uCH04DbKaEKeGw99nPEsr6Ek,30766
18
18
  MambuPy/api/mambutask.py,sha256=pZ71_SjpNs5ZVAXuIKi84gpq56a8lbj8HZLhn_c7iWk,2552
19
19
  MambuPy/api/mambutransaction.py,sha256=epr09hmEQbyq_eS1p1NFRvqNKR0febBfL6WvcLAmUWE,1837
20
20
  MambuPy/api/mambuuser.py,sha256=PkW64XFSxRzUEOc2-D2yfJkAKwq6cNyCMecKSmaPoKY,3488
21
21
  MambuPy/api/vos.py,sha256=ppsHTITgQ_waymyV9-lxUpBTCC3TsfQpw51ZbTMlcXM,2590
22
22
  MambuPy/api/connector/__init__.py,sha256=gTRQao9uSbJ-iDO-CuH4rLGNsVgrKDVeN2nau6t5o7Q,173
23
23
  MambuPy/api/connector/mambuconnector.py,sha256=1sp9UYybtpTAUrW4RhHYQ-O-xvATwwP1VCQOFAkAmDw,9847
24
- MambuPy/api/connector/rest.py,sha256=CV151bclrnP4wrxLb_JmZlZqFnDDr6OIIFB7Dk85-HA,33705
24
+ MambuPy/api/connector/rest.py,sha256=LOSWR1nRT22qcOejVYt4LR1Hcetp8at1Z43bUNV1a_o,34068
25
25
  MambuPy/orm/__init__.py,sha256=U8zAMqm-W-5c2QaLmcbxR0l2g0o6gUw26w0TgrjRsHM,801
26
26
  MambuPy/orm/schema_activities.py,sha256=uKva4zqloesXSGJxth8w4GN-zz8PC1IIpQMs8pDalAI,3391
27
27
  MambuPy/orm/schema_addresses.py,sha256=dLvEISuZuo6faWK1i3iU56vmwnrM16X5X40IhWOq4Ec,1598
@@ -41,30 +41,30 @@ MambuPy/rest/mambuactivity.py,sha256=lpLkTO4mtn3VfsmobAlWeAWqXWCTQ4I2f7koNiDIG54
41
41
  MambuPy/utils/__init__.py,sha256=xHJUVRE3PdETa8SPaUwdgKQfx8iMOAurSmnm7ROND_k,119
42
42
  MambuPy/utils/update_user_roles.py,sha256=CUkkBv2HJ-LaczmYu9Bud269Kb9NisJDoli64KZ1BeI,14675
43
43
  MambuPy/utils/userdeactivate.py,sha256=Rjlznm2KW23bqnrR_ogB36-O2rIHcBn9xTJdVIMynZQ,3134
44
- mambupy/__init__.py,sha256=Ps-C49zn-vyag70kZhy5JhIegv5l2FehezEN7LpavcM,1548
44
+ mambupy/__init__.py,sha256=a6Mg2XoeK9HEyfcabXM2mDQqn0twIDXQsa3DS9jO9Mc,1548
45
45
  mambupy/mambuconfig.py,sha256=6T8jT9LvB2e2HkR6DVD-BwksOgucqo617_xYIlWrc3M,7441
46
46
  mambupy/mambugeturl.py,sha256=D7jTr3tY1nPMXER-s5Uqaoi7_L6WjHTLLnqRYYWAh2Q,23405
47
- mambupy/mambuutil.py,sha256=tcElYEix-GYsjfuCsKxrBaEsP3IB6PXv6fop7Ln5-9c,13455
47
+ mambupy/mambuutil.py,sha256=7svr8agXPz-a0v6x7a0yFcO4NQBP11fkmWB3zSmH8DU,15648
48
48
  mambupy/api/__init__.py,sha256=B8lABs6zq9owokHc4VGDQ_AirzszWiu7EAYM-FsaSWE,621
49
- mambupy/api/classes.py,sha256=0LnsG6vyB7HQk11SaL43X6ftdkv7u6KyL7ynNvLQUTg,8030
50
- mambupy/api/entities.py,sha256=Vwv-DqmaFkjpkCWhTcvoVzVuEb34aGgjXkbcOBzrkF0,34237
49
+ mambupy/api/classes.py,sha256=W5aDtx3kM1YOChJkkYBTGKPpbMu_YycGxTd7Syz7ew4,7943
50
+ mambupy/api/entities.py,sha256=XRaPGhQ5FhXXwrfv9eN_3IPEydS-0yciy46pfqwuDH4,34275
51
51
  mambupy/api/interfaces.py,sha256=vRf1NDskUum-rnyyCygCVlYvuRWu_QvsEDwekcYK308,4870
52
52
  mambupy/api/mambubranch.py,sha256=yfpMN_OWFIaa_EGpEmOzoH-_uFkWExfuq5IDVDp61F8,778
53
53
  mambupy/api/mambucentre.py,sha256=2ferlA_8GTAoom-jwjBKVMUHGV0Um59vSl6_pdesT-4,952
54
54
  mambupy/api/mambuclient.py,sha256=JEtajMTzCM3lDKIzVEGS1j7wMxo6y9zryztaXoBC6Ms,2006
55
55
  mambupy/api/mambucustomfield.py,sha256=61dEHVmB4gcchGWJ51YVrcxXVyQfqmVffVWWAaBdtpQ,2233
56
56
  mambupy/api/mambugroup.py,sha256=uOcevAlmxJajCLsAwsgqLbTxVmwssKZmZn-q02FaHDQ,1528
57
- mambupy/api/mambuloan.py,sha256=cammMQc7qmo9AB8mBzL-T7dy5zweKTFgs1NJIBDs6mM,11641
57
+ mambupy/api/mambuloan.py,sha256=Apg-H8XVEAtmyFWBcaYWGpUbHJQy4j_iDRCokjpJ5lQ,11659
58
58
  mambupy/api/mambuproduct.py,sha256=xD8BQSVuGQgjij0_u-wTR5zL_NRNmeuDK9-2GQNwknI,2871
59
59
  mambupy/api/mamburole.py,sha256=TeeoM2MqhB1cd6TV_7nk1e6sVQRzFJehqTK8nEzqw7Y,565
60
- mambupy/api/mambustruct.py,sha256=jqT4LCTOGA0zIICVvacVyKTTQ4DN2jAgU2m57XG5Soo,30435
60
+ mambupy/api/mambustruct.py,sha256=RQi837WYHEf9hAfZQp2uCH04DbKaEKeGw99nPEsr6Ek,30766
61
61
  mambupy/api/mambutask.py,sha256=pZ71_SjpNs5ZVAXuIKi84gpq56a8lbj8HZLhn_c7iWk,2552
62
62
  mambupy/api/mambutransaction.py,sha256=epr09hmEQbyq_eS1p1NFRvqNKR0febBfL6WvcLAmUWE,1837
63
63
  mambupy/api/mambuuser.py,sha256=PkW64XFSxRzUEOc2-D2yfJkAKwq6cNyCMecKSmaPoKY,3488
64
64
  mambupy/api/vos.py,sha256=ppsHTITgQ_waymyV9-lxUpBTCC3TsfQpw51ZbTMlcXM,2590
65
65
  mambupy/api/connector/__init__.py,sha256=gTRQao9uSbJ-iDO-CuH4rLGNsVgrKDVeN2nau6t5o7Q,173
66
66
  mambupy/api/connector/mambuconnector.py,sha256=1sp9UYybtpTAUrW4RhHYQ-O-xvATwwP1VCQOFAkAmDw,9847
67
- mambupy/api/connector/rest.py,sha256=CV151bclrnP4wrxLb_JmZlZqFnDDr6OIIFB7Dk85-HA,33705
67
+ mambupy/api/connector/rest.py,sha256=LOSWR1nRT22qcOejVYt4LR1Hcetp8at1Z43bUNV1a_o,34068
68
68
  mambupy/orm/__init__.py,sha256=U8zAMqm-W-5c2QaLmcbxR0l2g0o6gUw26w0TgrjRsHM,801
69
69
  mambupy/orm/schema_activities.py,sha256=uKva4zqloesXSGJxth8w4GN-zz8PC1IIpQMs8pDalAI,3391
70
70
  mambupy/orm/schema_addresses.py,sha256=dLvEISuZuo6faWK1i3iU56vmwnrM16X5X40IhWOq4Ec,1598
@@ -84,8 +84,8 @@ mambupy/rest/mambuactivity.py,sha256=lpLkTO4mtn3VfsmobAlWeAWqXWCTQ4I2f7koNiDIG54
84
84
  mambupy/utils/__init__.py,sha256=xHJUVRE3PdETa8SPaUwdgKQfx8iMOAurSmnm7ROND_k,119
85
85
  mambupy/utils/update_user_roles.py,sha256=CUkkBv2HJ-LaczmYu9Bud269Kb9NisJDoli64KZ1BeI,14675
86
86
  mambupy/utils/userdeactivate.py,sha256=Rjlznm2KW23bqnrR_ogB36-O2rIHcBn9xTJdVIMynZQ,3134
87
- mambupy-2.3.2.dist-info/licenses/LICENSE,sha256=8MsXxZQL8tpxeo5kLHxQ-46sKu11g-_YOu0YnceJhuA,35165
88
- mambupy-2.3.2.dist-info/METADATA,sha256=EkX4xs7VonK5AoHzaWwm410Qb_vLFlI4CF-C14Z9VlA,43259
89
- mambupy-2.3.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
90
- mambupy-2.3.2.dist-info/top_level.txt,sha256=60TnLWD6zn0VyfBdMGCHI1_RLrKyG-anEUDQeGNpn9g,16
91
- mambupy-2.3.2.dist-info/RECORD,,
87
+ mambupy-2.3.3.dist-info/licenses/LICENSE,sha256=8MsXxZQL8tpxeo5kLHxQ-46sKu11g-_YOu0YnceJhuA,35165
88
+ mambupy-2.3.3.dist-info/METADATA,sha256=9tCwdkeZMK-xrn1fwF4jVgIehuCVw7MuBJv6VPSEkVU,43288
89
+ mambupy-2.3.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
90
+ mambupy-2.3.3.dist-info/top_level.txt,sha256=60TnLWD6zn0VyfBdMGCHI1_RLrKyG-anEUDQeGNpn9g,16
91
+ mambupy-2.3.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5