MambuPy 2.2.1__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/__init__.py +1 -1
  2. mambupy-2.3.0/MambuPy/utils/update_user_roles.py +432 -0
  3. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/PKG-INFO +1 -1
  4. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/SOURCES.txt +2 -0
  5. {mambupy-2.2.1 → mambupy-2.3.0}/PKG-INFO +1 -1
  6. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/__init__.py +1 -1
  7. mambupy-2.3.0/mambupy/utils/update_user_roles.py +432 -0
  8. {mambupy-2.2.1 → mambupy-2.3.0}/LICENSE +0 -0
  9. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/__init__.py +0 -0
  10. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/classes.py +0 -0
  11. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/connector/__init__.py +0 -0
  12. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/connector/mambuconnector.py +0 -0
  13. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/connector/rest.py +0 -0
  14. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/entities.py +0 -0
  15. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/interfaces.py +0 -0
  16. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambubranch.py +0 -0
  17. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambucentre.py +0 -0
  18. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuclient.py +0 -0
  19. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambucustomfield.py +0 -0
  20. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambugroup.py +0 -0
  21. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuloan.py +0 -0
  22. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuproduct.py +0 -0
  23. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mamburole.py +0 -0
  24. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambustruct.py +0 -0
  25. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambutask.py +0 -0
  26. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambutransaction.py +0 -0
  27. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuuser.py +0 -0
  28. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/vos.py +0 -0
  29. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/mambuconfig.py +0 -0
  30. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/mambugeturl.py +0 -0
  31. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/mambuutil.py +0 -0
  32. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/__init__.py +0 -0
  33. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_activities.py +0 -0
  34. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_addresses.py +0 -0
  35. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_branches.py +0 -0
  36. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_centres.py +0 -0
  37. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_clients.py +0 -0
  38. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_customfields.py +0 -0
  39. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_dummies.py +0 -0
  40. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_groups.py +0 -0
  41. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_loans.py +0 -0
  42. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_mambu.py +0 -0
  43. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_orm.py +0 -0
  44. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_tasks.py +0 -0
  45. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_users.py +0 -0
  46. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/rest/__init__.py +0 -0
  47. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/rest/mambuactivity.py +0 -0
  48. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/utils/__init__.py +0 -0
  49. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/utils/userdeactivate.py +0 -0
  50. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/dependency_links.txt +0 -0
  51. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/requires.txt +0 -0
  52. {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/top_level.txt +0 -0
  53. {mambupy-2.2.1 → mambupy-2.3.0}/README.md +0 -0
  54. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/__init__.py +0 -0
  55. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/classes.py +0 -0
  56. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/connector/__init__.py +0 -0
  57. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/connector/mambuconnector.py +0 -0
  58. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/connector/rest.py +0 -0
  59. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/entities.py +0 -0
  60. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/interfaces.py +0 -0
  61. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambubranch.py +0 -0
  62. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambucentre.py +0 -0
  63. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuclient.py +0 -0
  64. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambucustomfield.py +0 -0
  65. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambugroup.py +0 -0
  66. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuloan.py +0 -0
  67. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuproduct.py +0 -0
  68. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mamburole.py +0 -0
  69. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambustruct.py +0 -0
  70. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambutask.py +0 -0
  71. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambutransaction.py +0 -0
  72. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuuser.py +0 -0
  73. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/vos.py +0 -0
  74. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/mambuconfig.py +0 -0
  75. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/mambugeturl.py +0 -0
  76. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/mambuutil.py +0 -0
  77. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/__init__.py +0 -0
  78. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_activities.py +0 -0
  79. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_addresses.py +0 -0
  80. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_branches.py +0 -0
  81. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_centres.py +0 -0
  82. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_clients.py +0 -0
  83. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_customfields.py +0 -0
  84. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_dummies.py +0 -0
  85. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_groups.py +0 -0
  86. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_loans.py +0 -0
  87. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_mambu.py +0 -0
  88. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_orm.py +0 -0
  89. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_tasks.py +0 -0
  90. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_users.py +0 -0
  91. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/rest/__init__.py +0 -0
  92. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/rest/mambuactivity.py +0 -0
  93. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/utils/__init__.py +0 -0
  94. {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/utils/userdeactivate.py +0 -0
  95. {mambupy-2.2.1 → mambupy-2.3.0}/pyproject.toml +0 -0
  96. {mambupy-2.2.1 → mambupy-2.3.0}/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.2.1"
40
+ __version__ = "2.3.0"
41
41
  """The version of this module."""
42
42
 
43
43
 
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env python3
2
+ """Update user roles from a CSV file.
3
+
4
+ This script reads a CSV file containing usernames and role identifiers, validates
5
+ the data, and updates user roles in Mambu accordingly.
6
+
7
+ CSV Format:
8
+ The CSV file should contain at least two columns: username and role.
9
+ - Lines starting with '#' are treated as comments and ignored
10
+ - Extra columns are allowed but will be ignored
11
+ - The file can have a header row (will be validated like any other row)
12
+ - Role can be specified by name OR by ID
13
+
14
+ Example CSV:
15
+ username,role
16
+ john.doe,Credit Officer
17
+ jane.smith,BRANCH_MANAGER_ID
18
+ # This is a comment line
19
+ bob.johnson,Accountant
20
+
21
+ Requirements:
22
+ - All specified roles must exist in Mambu (validated at startup)
23
+ - Roles can be identified by name or ID
24
+ - Users that don't exist will be skipped with a warning
25
+ - The script uses detailsLevel="FULL" for comprehensive data retrieval
26
+
27
+ Usage:
28
+ python update_user_roles.py users_roles.csv
29
+
30
+ With custom CSV format:
31
+ python update_user_roles.py users_roles.csv --username-col 0 --role-col 1
32
+
33
+ Author: MambuPy Contributors
34
+ License: Apache 2.0
35
+ """
36
+
37
+ import argparse
38
+ import csv
39
+ import logging
40
+ import sys
41
+ from typing import Dict, List, Tuple, Set
42
+
43
+ from mambupy.api.mambuuser import MambuUser
44
+ from mambupy.api.mamburole import MambuRole
45
+ from mambupy.mambuutil import MambuError, MambuPyError, setup_logging
46
+
47
+
48
+ # Configure logging
49
+ logger = setup_logging(__name__)
50
+
51
+
52
+ class UserRoleUpdater:
53
+ """Handles the process of updating user roles from a CSV file."""
54
+
55
+ def __init__(self, csv_file: str, username_col: int = 0, role_col: int = 1,
56
+ delimiter: str = ',', dry_run: bool = False):
57
+ """Initialize the UserRoleUpdater.
58
+
59
+ Args:
60
+ csv_file: Path to the CSV file containing username and role data
61
+ username_col: Zero-indexed column number for usernames (default: 0)
62
+ role_col: Zero-indexed column number for roles (default: 1)
63
+ delimiter: CSV delimiter character (default: ',')
64
+ dry_run: If True, validate but don't actually update users
65
+ """
66
+ self.csv_file = csv_file
67
+ self.username_col = username_col
68
+ self.role_col = role_col
69
+ self.delimiter = delimiter
70
+ self.dry_run = dry_run
71
+ self.roles_cache: Dict[str, MambuRole] = {} # Cache by name OR id
72
+ self.stats = {
73
+ 'processed': 0,
74
+ 'updated': 0,
75
+ 'skipped': 0,
76
+ 'errors': 0,
77
+ 'comments': 0
78
+ }
79
+
80
+ def validate_roles(self, role_identifiers: Set[str]) -> bool:
81
+ """Validate that all specified roles exist in Mambu.
82
+
83
+ This method retrieves all roles from Mambu and checks that each role
84
+ identifier (name OR id) specified in the CSV exists. It also caches
85
+ the role objects for later use during updates.
86
+
87
+ Args:
88
+ role_identifiers: Set of role names or IDs to validate
89
+
90
+ Returns:
91
+ True if all roles exist, False otherwise
92
+
93
+ Raises:
94
+ MambuError: If there's an error communicating with Mambu
95
+ MambuPyError: If there's a general MambuPy error
96
+ """
97
+ logger.info("Retrieving all roles from Mambu for validation...")
98
+
99
+ try:
100
+ all_roles = MambuRole.get_all(detailsLevel="FULL")
101
+ except (MambuError, MambuPyError) as e:
102
+ logger.error(f"Failed to retrieve roles from Mambu: {e}")
103
+ raise
104
+
105
+ # Build cache of role name/id -> role object
106
+ # Store by both name AND id so either can be used for lookup
107
+ for role in all_roles:
108
+ role_name = role._attrs.get('name')
109
+ role_id = role._attrs.get('id')
110
+
111
+ if role_name:
112
+ self.roles_cache[role_name] = role
113
+ if role_id:
114
+ self.roles_cache[role_id] = role
115
+
116
+ logger.info(f"Found {len(all_roles)} roles in Mambu")
117
+
118
+ # Check if all requested roles exist
119
+ missing_roles = role_identifiers - set(self.roles_cache.keys())
120
+
121
+ if missing_roles:
122
+ logger.error("The following roles do not exist in Mambu:")
123
+ for role in sorted(missing_roles):
124
+ logger.error(f" - {role}")
125
+ return False
126
+
127
+ logger.info("All specified roles validated successfully")
128
+ return True
129
+
130
+ def parse_csv(self) -> List[Tuple[str, str]]:
131
+ """Parse the CSV file and extract username-role pairs.
132
+
133
+ Processes the CSV file, ignoring comment lines (starting with '#') and
134
+ extracting username and role identifier (name or ID) from the specified columns.
135
+
136
+ Returns:
137
+ List of (username, role_identifier) tuples
138
+
139
+ Raises:
140
+ FileNotFoundError: If the CSV file doesn't exist
141
+ ValueError: If the CSV format is invalid
142
+ """
143
+ user_role_pairs = []
144
+
145
+ try:
146
+ with open(self.csv_file, 'r', encoding='utf-8') as f:
147
+ reader = csv.reader(f, delimiter=self.delimiter)
148
+
149
+ for line_num, row in enumerate(reader, start=1):
150
+ # Skip empty lines
151
+ if not row or not any(row):
152
+ continue
153
+
154
+ # Skip comment lines
155
+ if row[0].strip().startswith('#'):
156
+ self.stats['comments'] += 1
157
+ logger.debug(f"Line {line_num}: Skipping comment")
158
+ continue
159
+
160
+ # Validate that the row has enough columns
161
+ required_cols = max(self.username_col, self.role_col) + 1
162
+ if len(row) < required_cols:
163
+ logger.warning(
164
+ f"Line {line_num}: Insufficient columns "
165
+ f"(found {len(row)}, need {required_cols}). Skipping."
166
+ )
167
+ self.stats['skipped'] += 1
168
+ continue
169
+
170
+ # Extract username and role identifier (name or ID)
171
+ username = row[self.username_col].strip()
172
+ role_identifier = row[self.role_col].strip()
173
+
174
+ # Skip if either field is empty
175
+ if not username or not role_identifier:
176
+ logger.warning(
177
+ f"Line {line_num}: Empty username or role. Skipping."
178
+ )
179
+ self.stats['skipped'] += 1
180
+ continue
181
+
182
+ user_role_pairs.append((username, role_identifier))
183
+ logger.debug(
184
+ f"Line {line_num}: Found user '{username}' -> role '{role_identifier}'"
185
+ )
186
+
187
+ except FileNotFoundError:
188
+ logger.error(f"CSV file not found: {self.csv_file}")
189
+ raise
190
+ except Exception as e:
191
+ logger.error(f"Error reading CSV file: {e}")
192
+ raise ValueError(f"Invalid CSV format: {e}")
193
+
194
+ logger.info(
195
+ f"Parsed CSV: {len(user_role_pairs)} user-role pairs, "
196
+ f"{self.stats['comments']} comments, "
197
+ f"{self.stats['skipped']} invalid rows"
198
+ )
199
+
200
+ return user_role_pairs
201
+
202
+ def update_user_role(self, username: str, role_identifier: str) -> bool:
203
+ """Update a single user's role.
204
+
205
+ Retrieves the user from Mambu, updates their role, and patches the
206
+ changes back to Mambu.
207
+
208
+ Args:
209
+ username: The username to update
210
+ role_identifier: The new role name or ID to assign
211
+
212
+ Returns:
213
+ True if the update was successful, False otherwise
214
+ """
215
+ try:
216
+ # Retrieve the user
217
+ logger.debug(f"Retrieving user: {username}")
218
+ user = MambuUser().get(username, detailsLevel="FULL")
219
+
220
+ # Get the role object from cache (works with name OR id)
221
+ new_role = self.roles_cache[role_identifier]
222
+ new_role_name = new_role._attrs.get('name', 'N/A')
223
+ new_role_id = new_role._attrs.get('id', 'N/A')
224
+
225
+ # Check if user already has this role (compare by encodedKey for accuracy)
226
+ current_role_key = user._attrs.get('role', {}).get('encodedKey')
227
+ new_role_key = new_role._attrs.get('encodedKey')
228
+
229
+ if current_role_key == new_role_key:
230
+ logger.info(
231
+ f"User '{username}' already has role '{new_role_name}' ({new_role_id}). "
232
+ "No update needed."
233
+ )
234
+ return True
235
+
236
+ current_role_name = user._attrs.get('role', {}).get('name', 'N/A')
237
+ logger.info(
238
+ f"Updating user '{username}': '{current_role_name}' -> '{new_role_name}' ({new_role_id})"
239
+ )
240
+
241
+ if not self.dry_run:
242
+ # Update the role in the user object
243
+ user._attrs['role'] = {
244
+ 'encodedKey': new_role._attrs['encodedKey'],
245
+ 'id': new_role._attrs['id']
246
+ }
247
+
248
+ # Patch the user to Mambu
249
+ user.patch(['role'])
250
+ logger.info(f"Successfully updated user '{username}'")
251
+ else:
252
+ logger.info(f"[DRY RUN] Would update user '{username}'")
253
+
254
+ return True
255
+
256
+ except MambuError as e:
257
+ if "INVALID_PARAMETERS" in str(e) or "NOT_FOUND" in str(e):
258
+ logger.warning(f"User '{username}' not found in Mambu. Skipping.")
259
+ else:
260
+ logger.error(f"Mambu error updating user '{username}': {e}")
261
+ return False
262
+
263
+ except MambuPyError as e:
264
+ logger.error(f"MambuPy error updating user '{username}': {e}")
265
+ return False
266
+
267
+ except Exception as e:
268
+ logger.error(f"Unexpected error updating user '{username}': {e}")
269
+ return False
270
+
271
+ def process(self) -> int:
272
+ """Process the CSV file and update all user roles.
273
+
274
+ This is the main processing method that:
275
+ 1. Parses the CSV file
276
+ 2. Validates all roles exist
277
+ 3. Updates each user's role
278
+
279
+ Returns:
280
+ 0 if successful, 1 if there were errors
281
+
282
+ Raises:
283
+ SystemExit: If role validation fails
284
+ """
285
+ # Parse CSV
286
+ try:
287
+ user_role_pairs = self.parse_csv()
288
+ except Exception as e:
289
+ logger.error(f"Failed to parse CSV: {e}")
290
+ return 1
291
+
292
+ if not user_role_pairs:
293
+ logger.warning("No valid user-role pairs found in CSV file")
294
+ return 0
295
+
296
+ # Extract unique role identifiers for validation
297
+ unique_roles = {role_identifier for _, role_identifier in user_role_pairs}
298
+ logger.info(f"Found {len(unique_roles)} unique roles to validate")
299
+
300
+ # Validate all roles exist
301
+ try:
302
+ if not self.validate_roles(unique_roles):
303
+ logger.error(
304
+ "Role validation failed. Please ensure all roles exist in Mambu."
305
+ )
306
+ return 1
307
+ except Exception as e:
308
+ logger.error(f"Role validation error: {e}")
309
+ return 1
310
+
311
+ # Process each user
312
+ logger.info(f"Processing {len(user_role_pairs)} user updates...")
313
+
314
+ for username, role_identifier in user_role_pairs:
315
+ self.stats['processed'] += 1
316
+
317
+ if self.update_user_role(username, role_identifier):
318
+ self.stats['updated'] += 1
319
+ else:
320
+ self.stats['errors'] += 1
321
+
322
+ # Print summary
323
+ self._print_summary()
324
+
325
+ return 0 if self.stats['errors'] == 0 else 1
326
+
327
+ def _print_summary(self):
328
+ """Print a summary of the processing results."""
329
+ logger.info("=" * 60)
330
+ logger.info("PROCESSING SUMMARY")
331
+ logger.info("=" * 60)
332
+ logger.info(f"Total rows processed: {self.stats['processed']}")
333
+ logger.info(f"Successfully updated: {self.stats['updated']}")
334
+ logger.info(f"Errors: {self.stats['errors']}")
335
+ logger.info(f"Comments skipped: {self.stats['comments']}")
336
+ logger.info(f"Invalid rows skipped: {self.stats['skipped']}")
337
+ logger.info("=" * 60)
338
+
339
+ if self.dry_run:
340
+ logger.info("DRY RUN MODE: No actual changes were made to Mambu")
341
+
342
+
343
+ def main():
344
+ """Main entry point for the CLI script."""
345
+ parser = argparse.ArgumentParser(
346
+ description='Update user roles in Mambu from a CSV file.',
347
+ epilog='''
348
+ Examples:
349
+ %(prog)s users_roles.csv
350
+ %(prog)s users_roles.csv --dry-run
351
+ %(prog)s users_roles.csv --username-col 0 --role-col 2
352
+ %(prog)s users_roles.csv --delimiter ";" --verbose
353
+
354
+ CSV Format:
355
+ The CSV file should contain at least two columns with username and role.
356
+ Roles can be specified by NAME or by ID.
357
+ Lines starting with '#' are treated as comments.
358
+
359
+ Example:
360
+ username,role
361
+ john.doe,Credit Officer
362
+ jane.smith,BRANCH_MANAGER_ID
363
+ # This user is on leave
364
+ bob.johnson,Accountant
365
+ ''',
366
+ formatter_class=argparse.RawDescriptionHelpFormatter
367
+ )
368
+
369
+ parser.add_argument(
370
+ 'csv_file',
371
+ help='Path to CSV file containing username and role columns'
372
+ )
373
+
374
+ parser.add_argument(
375
+ '--username-col',
376
+ type=int,
377
+ default=0,
378
+ metavar='N',
379
+ help='Zero-indexed column number for usernames (default: 0)'
380
+ )
381
+
382
+ parser.add_argument(
383
+ '--role-col',
384
+ type=int,
385
+ default=1,
386
+ metavar='N',
387
+ help='Zero-indexed column number for roles (default: 1)'
388
+ )
389
+
390
+ parser.add_argument(
391
+ '--delimiter',
392
+ type=str,
393
+ default=',',
394
+ metavar='CHAR',
395
+ help='CSV delimiter character (default: ",")'
396
+ )
397
+
398
+ parser.add_argument(
399
+ '--dry-run',
400
+ action='store_true',
401
+ help='Validate data but do not actually update users in Mambu'
402
+ )
403
+
404
+ parser.add_argument(
405
+ '--verbose',
406
+ action='store_true',
407
+ help='Enable verbose logging (DEBUG level)'
408
+ )
409
+
410
+ # Use parse_known_args to avoid conflicts with MambuPy's CLI args
411
+ args, _ = parser.parse_known_args()
412
+
413
+ # Configure logging level
414
+ if args.verbose:
415
+ logging.getLogger().setLevel(logging.DEBUG)
416
+ else:
417
+ logging.getLogger().setLevel(logging.INFO)
418
+
419
+ # Create updater and process
420
+ updater = UserRoleUpdater(
421
+ csv_file=args.csv_file,
422
+ username_col=args.username_col,
423
+ role_col=args.role_col,
424
+ delimiter=args.delimiter,
425
+ dry_run=args.dry_run
426
+ )
427
+
428
+ return updater.process()
429
+
430
+
431
+ if __name__ == "__main__":
432
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MambuPy
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: A python library for using Mambu APIs.
5
5
  Author-email: "Javier Novoa C." <jstitch@gmail.com>
6
6
  License: LICENSE
@@ -47,6 +47,7 @@ MambuPy/orm/schema_users.py
47
47
  MambuPy/rest/__init__.py
48
48
  MambuPy/rest/mambuactivity.py
49
49
  MambuPy/utils/__init__.py
50
+ MambuPy/utils/update_user_roles.py
50
51
  MambuPy/utils/userdeactivate.py
51
52
  mambupy/__init__.py
52
53
  mambupy/mambuconfig.py
@@ -89,4 +90,5 @@ mambupy/orm/schema_users.py
89
90
  mambupy/rest/__init__.py
90
91
  mambupy/rest/mambuactivity.py
91
92
  mambupy/utils/__init__.py
93
+ mambupy/utils/update_user_roles.py
92
94
  mambupy/utils/userdeactivate.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MambuPy
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: A python library for using Mambu APIs.
5
5
  Author-email: "Javier Novoa C." <jstitch@gmail.com>
6
6
  License: LICENSE
@@ -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.2.1"
40
+ __version__ = "2.3.0"
41
41
  """The version of this module."""
42
42
 
43
43
 
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env python3
2
+ """Update user roles from a CSV file.
3
+
4
+ This script reads a CSV file containing usernames and role identifiers, validates
5
+ the data, and updates user roles in Mambu accordingly.
6
+
7
+ CSV Format:
8
+ The CSV file should contain at least two columns: username and role.
9
+ - Lines starting with '#' are treated as comments and ignored
10
+ - Extra columns are allowed but will be ignored
11
+ - The file can have a header row (will be validated like any other row)
12
+ - Role can be specified by name OR by ID
13
+
14
+ Example CSV:
15
+ username,role
16
+ john.doe,Credit Officer
17
+ jane.smith,BRANCH_MANAGER_ID
18
+ # This is a comment line
19
+ bob.johnson,Accountant
20
+
21
+ Requirements:
22
+ - All specified roles must exist in Mambu (validated at startup)
23
+ - Roles can be identified by name or ID
24
+ - Users that don't exist will be skipped with a warning
25
+ - The script uses detailsLevel="FULL" for comprehensive data retrieval
26
+
27
+ Usage:
28
+ python update_user_roles.py users_roles.csv
29
+
30
+ With custom CSV format:
31
+ python update_user_roles.py users_roles.csv --username-col 0 --role-col 1
32
+
33
+ Author: MambuPy Contributors
34
+ License: Apache 2.0
35
+ """
36
+
37
+ import argparse
38
+ import csv
39
+ import logging
40
+ import sys
41
+ from typing import Dict, List, Tuple, Set
42
+
43
+ from mambupy.api.mambuuser import MambuUser
44
+ from mambupy.api.mamburole import MambuRole
45
+ from mambupy.mambuutil import MambuError, MambuPyError, setup_logging
46
+
47
+
48
+ # Configure logging
49
+ logger = setup_logging(__name__)
50
+
51
+
52
+ class UserRoleUpdater:
53
+ """Handles the process of updating user roles from a CSV file."""
54
+
55
+ def __init__(self, csv_file: str, username_col: int = 0, role_col: int = 1,
56
+ delimiter: str = ',', dry_run: bool = False):
57
+ """Initialize the UserRoleUpdater.
58
+
59
+ Args:
60
+ csv_file: Path to the CSV file containing username and role data
61
+ username_col: Zero-indexed column number for usernames (default: 0)
62
+ role_col: Zero-indexed column number for roles (default: 1)
63
+ delimiter: CSV delimiter character (default: ',')
64
+ dry_run: If True, validate but don't actually update users
65
+ """
66
+ self.csv_file = csv_file
67
+ self.username_col = username_col
68
+ self.role_col = role_col
69
+ self.delimiter = delimiter
70
+ self.dry_run = dry_run
71
+ self.roles_cache: Dict[str, MambuRole] = {} # Cache by name OR id
72
+ self.stats = {
73
+ 'processed': 0,
74
+ 'updated': 0,
75
+ 'skipped': 0,
76
+ 'errors': 0,
77
+ 'comments': 0
78
+ }
79
+
80
+ def validate_roles(self, role_identifiers: Set[str]) -> bool:
81
+ """Validate that all specified roles exist in Mambu.
82
+
83
+ This method retrieves all roles from Mambu and checks that each role
84
+ identifier (name OR id) specified in the CSV exists. It also caches
85
+ the role objects for later use during updates.
86
+
87
+ Args:
88
+ role_identifiers: Set of role names or IDs to validate
89
+
90
+ Returns:
91
+ True if all roles exist, False otherwise
92
+
93
+ Raises:
94
+ MambuError: If there's an error communicating with Mambu
95
+ MambuPyError: If there's a general MambuPy error
96
+ """
97
+ logger.info("Retrieving all roles from Mambu for validation...")
98
+
99
+ try:
100
+ all_roles = MambuRole.get_all(detailsLevel="FULL")
101
+ except (MambuError, MambuPyError) as e:
102
+ logger.error(f"Failed to retrieve roles from Mambu: {e}")
103
+ raise
104
+
105
+ # Build cache of role name/id -> role object
106
+ # Store by both name AND id so either can be used for lookup
107
+ for role in all_roles:
108
+ role_name = role._attrs.get('name')
109
+ role_id = role._attrs.get('id')
110
+
111
+ if role_name:
112
+ self.roles_cache[role_name] = role
113
+ if role_id:
114
+ self.roles_cache[role_id] = role
115
+
116
+ logger.info(f"Found {len(all_roles)} roles in Mambu")
117
+
118
+ # Check if all requested roles exist
119
+ missing_roles = role_identifiers - set(self.roles_cache.keys())
120
+
121
+ if missing_roles:
122
+ logger.error("The following roles do not exist in Mambu:")
123
+ for role in sorted(missing_roles):
124
+ logger.error(f" - {role}")
125
+ return False
126
+
127
+ logger.info("All specified roles validated successfully")
128
+ return True
129
+
130
+ def parse_csv(self) -> List[Tuple[str, str]]:
131
+ """Parse the CSV file and extract username-role pairs.
132
+
133
+ Processes the CSV file, ignoring comment lines (starting with '#') and
134
+ extracting username and role identifier (name or ID) from the specified columns.
135
+
136
+ Returns:
137
+ List of (username, role_identifier) tuples
138
+
139
+ Raises:
140
+ FileNotFoundError: If the CSV file doesn't exist
141
+ ValueError: If the CSV format is invalid
142
+ """
143
+ user_role_pairs = []
144
+
145
+ try:
146
+ with open(self.csv_file, 'r', encoding='utf-8') as f:
147
+ reader = csv.reader(f, delimiter=self.delimiter)
148
+
149
+ for line_num, row in enumerate(reader, start=1):
150
+ # Skip empty lines
151
+ if not row or not any(row):
152
+ continue
153
+
154
+ # Skip comment lines
155
+ if row[0].strip().startswith('#'):
156
+ self.stats['comments'] += 1
157
+ logger.debug(f"Line {line_num}: Skipping comment")
158
+ continue
159
+
160
+ # Validate that the row has enough columns
161
+ required_cols = max(self.username_col, self.role_col) + 1
162
+ if len(row) < required_cols:
163
+ logger.warning(
164
+ f"Line {line_num}: Insufficient columns "
165
+ f"(found {len(row)}, need {required_cols}). Skipping."
166
+ )
167
+ self.stats['skipped'] += 1
168
+ continue
169
+
170
+ # Extract username and role identifier (name or ID)
171
+ username = row[self.username_col].strip()
172
+ role_identifier = row[self.role_col].strip()
173
+
174
+ # Skip if either field is empty
175
+ if not username or not role_identifier:
176
+ logger.warning(
177
+ f"Line {line_num}: Empty username or role. Skipping."
178
+ )
179
+ self.stats['skipped'] += 1
180
+ continue
181
+
182
+ user_role_pairs.append((username, role_identifier))
183
+ logger.debug(
184
+ f"Line {line_num}: Found user '{username}' -> role '{role_identifier}'"
185
+ )
186
+
187
+ except FileNotFoundError:
188
+ logger.error(f"CSV file not found: {self.csv_file}")
189
+ raise
190
+ except Exception as e:
191
+ logger.error(f"Error reading CSV file: {e}")
192
+ raise ValueError(f"Invalid CSV format: {e}")
193
+
194
+ logger.info(
195
+ f"Parsed CSV: {len(user_role_pairs)} user-role pairs, "
196
+ f"{self.stats['comments']} comments, "
197
+ f"{self.stats['skipped']} invalid rows"
198
+ )
199
+
200
+ return user_role_pairs
201
+
202
+ def update_user_role(self, username: str, role_identifier: str) -> bool:
203
+ """Update a single user's role.
204
+
205
+ Retrieves the user from Mambu, updates their role, and patches the
206
+ changes back to Mambu.
207
+
208
+ Args:
209
+ username: The username to update
210
+ role_identifier: The new role name or ID to assign
211
+
212
+ Returns:
213
+ True if the update was successful, False otherwise
214
+ """
215
+ try:
216
+ # Retrieve the user
217
+ logger.debug(f"Retrieving user: {username}")
218
+ user = MambuUser().get(username, detailsLevel="FULL")
219
+
220
+ # Get the role object from cache (works with name OR id)
221
+ new_role = self.roles_cache[role_identifier]
222
+ new_role_name = new_role._attrs.get('name', 'N/A')
223
+ new_role_id = new_role._attrs.get('id', 'N/A')
224
+
225
+ # Check if user already has this role (compare by encodedKey for accuracy)
226
+ current_role_key = user._attrs.get('role', {}).get('encodedKey')
227
+ new_role_key = new_role._attrs.get('encodedKey')
228
+
229
+ if current_role_key == new_role_key:
230
+ logger.info(
231
+ f"User '{username}' already has role '{new_role_name}' ({new_role_id}). "
232
+ "No update needed."
233
+ )
234
+ return True
235
+
236
+ current_role_name = user._attrs.get('role', {}).get('name', 'N/A')
237
+ logger.info(
238
+ f"Updating user '{username}': '{current_role_name}' -> '{new_role_name}' ({new_role_id})"
239
+ )
240
+
241
+ if not self.dry_run:
242
+ # Update the role in the user object
243
+ user._attrs['role'] = {
244
+ 'encodedKey': new_role._attrs['encodedKey'],
245
+ 'id': new_role._attrs['id']
246
+ }
247
+
248
+ # Patch the user to Mambu
249
+ user.patch(['role'])
250
+ logger.info(f"Successfully updated user '{username}'")
251
+ else:
252
+ logger.info(f"[DRY RUN] Would update user '{username}'")
253
+
254
+ return True
255
+
256
+ except MambuError as e:
257
+ if "INVALID_PARAMETERS" in str(e) or "NOT_FOUND" in str(e):
258
+ logger.warning(f"User '{username}' not found in Mambu. Skipping.")
259
+ else:
260
+ logger.error(f"Mambu error updating user '{username}': {e}")
261
+ return False
262
+
263
+ except MambuPyError as e:
264
+ logger.error(f"MambuPy error updating user '{username}': {e}")
265
+ return False
266
+
267
+ except Exception as e:
268
+ logger.error(f"Unexpected error updating user '{username}': {e}")
269
+ return False
270
+
271
+ def process(self) -> int:
272
+ """Process the CSV file and update all user roles.
273
+
274
+ This is the main processing method that:
275
+ 1. Parses the CSV file
276
+ 2. Validates all roles exist
277
+ 3. Updates each user's role
278
+
279
+ Returns:
280
+ 0 if successful, 1 if there were errors
281
+
282
+ Raises:
283
+ SystemExit: If role validation fails
284
+ """
285
+ # Parse CSV
286
+ try:
287
+ user_role_pairs = self.parse_csv()
288
+ except Exception as e:
289
+ logger.error(f"Failed to parse CSV: {e}")
290
+ return 1
291
+
292
+ if not user_role_pairs:
293
+ logger.warning("No valid user-role pairs found in CSV file")
294
+ return 0
295
+
296
+ # Extract unique role identifiers for validation
297
+ unique_roles = {role_identifier for _, role_identifier in user_role_pairs}
298
+ logger.info(f"Found {len(unique_roles)} unique roles to validate")
299
+
300
+ # Validate all roles exist
301
+ try:
302
+ if not self.validate_roles(unique_roles):
303
+ logger.error(
304
+ "Role validation failed. Please ensure all roles exist in Mambu."
305
+ )
306
+ return 1
307
+ except Exception as e:
308
+ logger.error(f"Role validation error: {e}")
309
+ return 1
310
+
311
+ # Process each user
312
+ logger.info(f"Processing {len(user_role_pairs)} user updates...")
313
+
314
+ for username, role_identifier in user_role_pairs:
315
+ self.stats['processed'] += 1
316
+
317
+ if self.update_user_role(username, role_identifier):
318
+ self.stats['updated'] += 1
319
+ else:
320
+ self.stats['errors'] += 1
321
+
322
+ # Print summary
323
+ self._print_summary()
324
+
325
+ return 0 if self.stats['errors'] == 0 else 1
326
+
327
+ def _print_summary(self):
328
+ """Print a summary of the processing results."""
329
+ logger.info("=" * 60)
330
+ logger.info("PROCESSING SUMMARY")
331
+ logger.info("=" * 60)
332
+ logger.info(f"Total rows processed: {self.stats['processed']}")
333
+ logger.info(f"Successfully updated: {self.stats['updated']}")
334
+ logger.info(f"Errors: {self.stats['errors']}")
335
+ logger.info(f"Comments skipped: {self.stats['comments']}")
336
+ logger.info(f"Invalid rows skipped: {self.stats['skipped']}")
337
+ logger.info("=" * 60)
338
+
339
+ if self.dry_run:
340
+ logger.info("DRY RUN MODE: No actual changes were made to Mambu")
341
+
342
+
343
+ def main():
344
+ """Main entry point for the CLI script."""
345
+ parser = argparse.ArgumentParser(
346
+ description='Update user roles in Mambu from a CSV file.',
347
+ epilog='''
348
+ Examples:
349
+ %(prog)s users_roles.csv
350
+ %(prog)s users_roles.csv --dry-run
351
+ %(prog)s users_roles.csv --username-col 0 --role-col 2
352
+ %(prog)s users_roles.csv --delimiter ";" --verbose
353
+
354
+ CSV Format:
355
+ The CSV file should contain at least two columns with username and role.
356
+ Roles can be specified by NAME or by ID.
357
+ Lines starting with '#' are treated as comments.
358
+
359
+ Example:
360
+ username,role
361
+ john.doe,Credit Officer
362
+ jane.smith,BRANCH_MANAGER_ID
363
+ # This user is on leave
364
+ bob.johnson,Accountant
365
+ ''',
366
+ formatter_class=argparse.RawDescriptionHelpFormatter
367
+ )
368
+
369
+ parser.add_argument(
370
+ 'csv_file',
371
+ help='Path to CSV file containing username and role columns'
372
+ )
373
+
374
+ parser.add_argument(
375
+ '--username-col',
376
+ type=int,
377
+ default=0,
378
+ metavar='N',
379
+ help='Zero-indexed column number for usernames (default: 0)'
380
+ )
381
+
382
+ parser.add_argument(
383
+ '--role-col',
384
+ type=int,
385
+ default=1,
386
+ metavar='N',
387
+ help='Zero-indexed column number for roles (default: 1)'
388
+ )
389
+
390
+ parser.add_argument(
391
+ '--delimiter',
392
+ type=str,
393
+ default=',',
394
+ metavar='CHAR',
395
+ help='CSV delimiter character (default: ",")'
396
+ )
397
+
398
+ parser.add_argument(
399
+ '--dry-run',
400
+ action='store_true',
401
+ help='Validate data but do not actually update users in Mambu'
402
+ )
403
+
404
+ parser.add_argument(
405
+ '--verbose',
406
+ action='store_true',
407
+ help='Enable verbose logging (DEBUG level)'
408
+ )
409
+
410
+ # Use parse_known_args to avoid conflicts with MambuPy's CLI args
411
+ args, _ = parser.parse_known_args()
412
+
413
+ # Configure logging level
414
+ if args.verbose:
415
+ logging.getLogger().setLevel(logging.DEBUG)
416
+ else:
417
+ logging.getLogger().setLevel(logging.INFO)
418
+
419
+ # Create updater and process
420
+ updater = UserRoleUpdater(
421
+ csv_file=args.csv_file,
422
+ username_col=args.username_col,
423
+ role_col=args.role_col,
424
+ delimiter=args.delimiter,
425
+ dry_run=args.dry_run
426
+ )
427
+
428
+ return updater.process()
429
+
430
+
431
+ if __name__ == "__main__":
432
+ sys.exit(main())
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes