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.
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/__init__.py +1 -1
- mambupy-2.3.0/MambuPy/utils/update_user_roles.py +432 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/PKG-INFO +1 -1
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/SOURCES.txt +2 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/PKG-INFO +1 -1
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/__init__.py +1 -1
- mambupy-2.3.0/mambupy/utils/update_user_roles.py +432 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/LICENSE +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/classes.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/connector/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/connector/mambuconnector.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/connector/rest.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/entities.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/interfaces.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambubranch.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambucentre.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuclient.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambucustomfield.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambugroup.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuloan.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuproduct.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mamburole.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambustruct.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambutask.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambutransaction.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/mambuuser.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/api/vos.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/mambuconfig.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/mambugeturl.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/mambuutil.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_activities.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_addresses.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_branches.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_centres.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_clients.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_customfields.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_dummies.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_groups.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_loans.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_mambu.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_orm.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_tasks.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/orm/schema_users.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/rest/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/rest/mambuactivity.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/utils/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy/utils/userdeactivate.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/dependency_links.txt +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/requires.txt +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/MambuPy.egg-info/top_level.txt +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/README.md +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/classes.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/connector/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/connector/mambuconnector.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/connector/rest.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/entities.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/interfaces.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambubranch.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambucentre.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuclient.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambucustomfield.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambugroup.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuloan.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuproduct.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mamburole.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambustruct.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambutask.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambutransaction.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/mambuuser.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/api/vos.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/mambuconfig.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/mambugeturl.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/mambuutil.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_activities.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_addresses.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_branches.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_centres.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_clients.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_customfields.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_dummies.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_groups.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_loans.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_mambu.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_orm.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_tasks.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/orm/schema_users.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/rest/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/rest/mambuactivity.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/utils/__init__.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/mambupy/utils/userdeactivate.py +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/pyproject.toml +0 -0
- {mambupy-2.2.1 → mambupy-2.3.0}/setup.cfg +0 -0
|
@@ -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())
|
|
@@ -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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|