@@ -493,8 +493,8 @@ def delete_workspace(
493493
494494@app .command (name = "add-user" )
495495def 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" )
862907def 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