Skip to content

Commit e4cbc1f

Browse files
feat: enhance argilla user management commands (#79)
Enhanced list-users command: - Make --workspace flag optional to list all users when omitted - Display user roles (owner/admin/annotator) alongside usernames - Add role summary statistics - Simplify output to show only username and role columns - Support both global and workspace-specific user listing Enhanced add-user command: - Make --password optional for SSO users (e.g., Hugging Face login) - Support updating existing users' roles - Allow adding existing users to additional workspaces - Prevent duplicate workspace memberships - Provide clear warnings for password limitations These improvements enable better user management workflows: - Quickly view all users and their permission levels - Update user roles without recreating accounts - Support SSO authentication flows - Maintain idempotent workspace assignments
1 parent 33364bb commit e4cbc1f

1 file changed

Lines changed: 156 additions & 57 deletions

File tree

gsma_dataset_creation/argilla_cli.py

Lines changed: 156 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -493,8 +493,8 @@ def delete_workspace(
493493

494494
@app.command(name="add-user")
495495
def add_user(
496-
username: str = typer.Option(..., "--username", "-u", help="Username for the new user"),
497-
password: str = typer.Option(..., "--password", "-p", help="Password for the new user"),
496+
username: str = typer.Option(..., "--username", "-u", help="Username for the user"),
497+
password: str | None = typer.Option(None, "--password", "-p", help="Password for the user (optional for SSO users)"),
498498
workspaces: list[str] = typer.Option(
499499
..., "--workspace", "-w", help="Workspace name(s) to add user to (can be specified multiple times)"
500500
),
@@ -515,31 +515,38 @@ def add_user(
515515
logger_level: str = typer.Option("INFO", "--logger-level", help="Logging level"),
516516
) -> None:
517517
"""
518-
Create a new user and add them to one or more workspaces.
518+
Create a new user or update an existing user's role and workspaces.
519519
520520
This command:
521-
1. Creates a new user with specified credentials and role
522-
2. Adds the user to all specified workspaces
521+
1. If user doesn't exist: Creates a new user with specified credentials and role
522+
2. If user exists: Updates their role (if different) and adds them to specified workspaces
523+
3. Adds the user to all specified workspaces
523524
524525
Available roles:
525526
- annotator: Can annotate datasets (default)
526527
- admin: Can manage workspaces and users
527528
- owner: Full administrative access
528529
529530
Examples:
530-
# Create user and add to single workspace
531+
# Create user with password and add to single workspace
531532
uv run gsma argilla add-user -u john.doe -p mypassword123 -w mantis
532533
534+
# Create SSO user (no password needed)
535+
uv run gsma argilla add-user -u john.doe -w mantis
536+
533537
# Create user and add to multiple workspaces
534538
uv run gsma argilla add-user -u john.doe -p mypassword123 -w tsg -w fasg -w ng
535539
536540
# Create admin user
537541
uv run gsma argilla add-user -u admin.user -p securepass -w mantis -r admin
542+
543+
# Update existing user's role to owner and add to new workspace
544+
uv run gsma argilla add-user -u existing.user -w new-workspace -r owner
538545
"""
539546
logger.remove()
540547
logger.add(lambda msg: typer.echo(msg, err=False), level=logger_level)
541548

542-
logger.info("👤 Creating user...")
549+
logger.info("👤 Processing user...")
543550
logger.info(f" Username: {username}")
544551
logger.info(f" Workspaces: {', '.join(workspaces)}")
545552
logger.info(f" Role: {role}")
@@ -580,42 +587,80 @@ def add_user(
580587
ws = client.workspaces(name=ws_name)
581588
if not ws:
582589
logger.error(f"❌ Workspace '{ws_name}' not found")
583-
logger.error(" All workspaces must exist before creating user")
590+
logger.error(" All workspaces must exist before adding user")
584591
raise typer.Exit(code=1)
585592
workspace_objs.append(ws)
586593

587594
logger.info(f"✅ All {len(workspaces)} workspace(s) found")
588595

589596
# Check if user already exists
590597
existing_user = client.users(username=username)
598+
591599
if existing_user:
592-
logger.error(f"❌ User '{username}' already exists")
593-
logger.error(" Use 'add-to-workspace' command to add existing user to workspaces")
594-
raise typer.Exit(code=1)
600+
logger.info(f"ℹ️ User '{username}' already exists - updating role and workspaces")
601+
user = existing_user
602+
603+
# Update role if different
604+
current_role = str(user.role).replace("Role.", "")
605+
if current_role != role:
606+
logger.info(f"🔄 Updating role from '{current_role}' to '{role}'")
607+
user.role = role
608+
try:
609+
user.update()
610+
logger.info(f"✅ Updated user role to: {role}")
611+
except AttributeError:
612+
# Try save() if update() doesn't exist
613+
try:
614+
user.save()
615+
logger.info(f"✅ Updated user role to: {role}")
616+
except AttributeError:
617+
logger.warning(f"⚠️ Could not update role - SDK method not available")
618+
else:
619+
logger.info(f"ℹ️ User already has role '{role}'")
595620

596-
# Create user
597-
user = client.users.add(
598-
rg.User(
599-
username=username,
600-
password=password,
601-
first_name=first_name or username,
602-
last_name=last_name or "",
603-
role=role,
621+
if password:
622+
logger.warning(f"⚠️ Password parameter ignored for existing user '{username}'")
623+
logger.warning(f" Passwords can only be set during user creation")
624+
else:
625+
# Create new user
626+
logger.info(f"📝 Creating new user: {username}")
627+
628+
# Password is required for new users (unless SSO is configured)
629+
if not password:
630+
logger.warning(f"⚠️ No password provided for new user")
631+
logger.warning(f" This will only work if SSO (e.g., Hugging Face) is enabled")
632+
logger.warning(f" Otherwise, user creation will fail")
633+
634+
user = client.users.add(
635+
rg.User(
636+
username=username,
637+
password=password or "", # Use empty string if no password
638+
first_name=first_name or username,
639+
last_name=last_name or "",
640+
role=role,
641+
)
604642
)
605-
)
606-
logger.info(f"✅ Created user: {username}")
643+
logger.info(f"✅ Created user: {username}")
607644

608645
# Add user to all workspaces
609646
for ws in workspace_objs:
610-
ws.add_user(user)
611-
logger.info(f"✅ Added {username} to workspace {ws.name}")
647+
# Check if user is already in workspace
648+
existing_users = [u.username for u in ws.users]
649+
if user.username in existing_users:
650+
logger.info(f"ℹ️ {username} already in workspace {ws.name}")
651+
else:
652+
ws.add_user(user)
653+
logger.info(f"✅ Added {username} to workspace {ws.name}")
612654

613-
logger.info(f"🎉 Successfully created user and added to {len(workspaces)} workspace(s)")
655+
if existing_user:
656+
logger.info(f"🎉 Successfully updated user and ensured membership in {len(workspaces)} workspace(s)")
657+
else:
658+
logger.info(f"🎉 Successfully created user and added to {len(workspaces)} workspace(s)")
614659

615660
except typer.Exit:
616661
raise
617662
except Exception as e:
618-
logger.error(f"❌ User creation failed: {e}")
663+
logger.error(f"❌ User operation failed: {e}")
619664
raise typer.Exit(code=1) from e
620665

621666

@@ -860,8 +905,8 @@ def add_users(
860905

861906
@app.command(name="list-users")
862907
def list_users(
863-
workspace: str = typer.Option(
864-
..., "--workspace", "-w", help="Workspace name to list users for"
908+
workspace: str | None = typer.Option(
909+
None, "--workspace", "-w", help="Workspace name to list users for (if not provided, lists all users)"
865910
),
866911
output_csv: Path = typer.Option(
867912
None, "--output-csv", "-o", help="Path to save CSV file with user credentials"
@@ -875,12 +920,16 @@ def list_users(
875920
logger_level: str = typer.Option("INFO", "--logger-level", help="Logging level"),
876921
) -> None:
877922
"""
878-
List all users for a given workspace.
923+
List users and their roles.
879924
880-
This command retrieves all users associated with a workspace and displays their usernames.
925+
If --workspace is provided, lists users in that workspace with their roles.
926+
If --workspace is not provided, lists ALL users in Argilla with their roles.
881927
Note: Passwords cannot be retrieved from Argilla as they are hashed.
882928
883929
Examples:
930+
# List all users with roles
931+
uv run gsma argilla list-users
932+
884933
# List users for TSG workspace
885934
uv run gsma argilla list-users -w tsg-wg -o data/tsg_users.csv
886935
@@ -890,8 +939,12 @@ def list_users(
890939
logger.remove()
891940
logger.add(lambda msg: typer.echo(msg, err=False), level=logger_level)
892941

893-
logger.info("📋 Listing workspace users...")
894-
logger.info(f" Workspace: {workspace}")
942+
if workspace:
943+
logger.info("📋 Listing workspace users...")
944+
logger.info(f" Workspace: {workspace}")
945+
else:
946+
logger.info("📋 Listing all users...")
947+
895948
logger.debug(f"Logger level set to: {logger_level}")
896949

897950
# Import here to provide better error message if argilla not installed
@@ -916,57 +969,103 @@ def list_users(
916969
# Create client
917970
client = rg.Argilla(api_url=api_url, api_key=api_key)
918971

919-
# Get workspace
920-
workspace_obj = client.workspaces(name=workspace)
921-
if not workspace_obj:
922-
logger.error(f"❌ Workspace '{workspace}' not found")
923-
raise typer.Exit(code=1)
972+
if workspace:
973+
# Get workspace-specific users
974+
workspace_obj = client.workspaces(name=workspace)
975+
if not workspace_obj:
976+
logger.error(f"❌ Workspace '{workspace}' not found")
977+
raise typer.Exit(code=1)
924978

925-
logger.info(f"✅ Workspace '{workspace}' found")
979+
logger.info(f"✅ Workspace '{workspace}' found")
926980

927-
# Get all users in the workspace
928-
users_in_workspace = list(workspace_obj.users)
981+
# Get all users in the workspace
982+
users_list = list(workspace_obj.users)
929983

930-
if not users_in_workspace:
931-
logger.warning(f"⚠️ No users found in workspace '{workspace}'")
932-
logger.info("📄 No CSV file written (no users)")
933-
raise typer.Exit(code=0)
984+
if not users_list:
985+
logger.warning(f"⚠️ No users found in workspace '{workspace}'")
986+
logger.info("📄 No CSV file written (no users)")
987+
raise typer.Exit(code=0)
934988

935-
logger.info(f"✅ Found {len(users_in_workspace)} users in workspace")
989+
logger.info(f"✅ Found {len(users_list)} users in workspace")
936990

937-
# Collect usernames (passwords are hashed in Argilla and cannot be retrieved)
938-
usernames = [user.username for user in users_in_workspace]
991+
# Print to console in table format
992+
print(f"\n👥 Users in workspace '{workspace}' ({len(users_list)} users)\n")
939993

940-
# Print to console in table format
941-
print(f"\n👥 Users in workspace '{workspace}' ({len(usernames)} users)\n")
994+
else:
995+
# Get all users in Argilla
996+
users_list = list(client.users)
997+
998+
if not users_list:
999+
logger.warning("⚠️ No users found")
1000+
logger.info("📄 No CSV file written (no users)")
1001+
raise typer.Exit(code=0)
1002+
1003+
logger.info(f"✅ Found {len(users_list)} users")
1004+
1005+
# Print to console in table format
1006+
print(f"\n👥 All users ({len(users_list)} users)\n")
1007+
1008+
# Prepare data with role information
1009+
user_data = []
1010+
for user in users_list:
1011+
role_str = str(user.role).replace("Role.", "")
1012+
user_data.append({
1013+
"username": user.username,
1014+
"role": role_str,
1015+
})
1016+
1017+
# Sort by role (owner, admin, annotator) then by username
1018+
role_order = {"owner": 0, "admin": 1, "annotator": 2}
1019+
user_data_sorted = sorted(
1020+
user_data,
1021+
key=lambda u: (role_order.get(u["role"], 999), u["username"])
1022+
)
9421023

9431024
try:
9441025
from tabulate import tabulate
9451026

9461027
# Prepare data for table
947-
table_data = [[i+1, username] for i, username in enumerate(usernames)]
948-
print(tabulate(table_data, headers=["#", "Username"], tablefmt="simple_grid"))
1028+
table_data = [
1029+
[i+1, u["username"], u["role"]]
1030+
for i, u in enumerate(user_data_sorted)
1031+
]
1032+
print(tabulate(
1033+
table_data,
1034+
headers=["#", "Username", "Role"],
1035+
tablefmt="simple_grid"
1036+
))
9491037
print()
9501038

9511039
except ImportError:
9521040
# Fallback to simple format
953-
for i, username in enumerate(usernames, 1):
954-
print(f" {i}. {username}")
1041+
for i, u in enumerate(user_data_sorted, 1):
1042+
print(f" {i}. {u['username']:<30} {u['role']:<15}")
9551043
print()
9561044

1045+
# Print summary by role
1046+
role_counts = {}
1047+
for u in user_data:
1048+
role_counts[u["role"]] = role_counts.get(u["role"], 0) + 1
1049+
1050+
print("📊 Summary by role:")
1051+
for role in ["owner", "admin", "annotator"]:
1052+
if role in role_counts:
1053+
print(f" {role}: {role_counts[role]}")
1054+
print()
1055+
9571056
# Write CSV output if requested
9581057
if output_csv:
9591058
output_csv = Path(output_csv)
9601059
output_csv.parent.mkdir(parents=True, exist_ok=True)
9611060

9621061
with open(output_csv, "w", newline="") as f:
9631062
writer = csv.writer(f)
964-
writer.writerow(["username"])
965-
for username in usernames:
966-
writer.writerow([username])
1063+
writer.writerow(["username", "role"])
1064+
for u in user_data_sorted:
1065+
writer.writerow([u["username"], u["role"]])
9671066

968-
logger.info(f"✅ Successfully exported {len(usernames)} usernames")
969-
logger.info(f"📄 Usernames saved to: {output_csv}")
1067+
logger.info(f"✅ Successfully exported {len(user_data)} users")
1068+
logger.info(f"📄 Users saved to: {output_csv}")
9701069

9711070
logger.info(f"ℹ️ Note: Passwords are only available during user creation (add-users command)")
9721071
logger.info(f"ℹ️ Save the CSV when creating users to share passwords with annotators")

0 commit comments

Comments
 (0)