From 21b52879d98d7afbc459ca6db743b2dedfcf0eaa Mon Sep 17 00:00:00 2001 From: kozikkamil Date: Wed, 26 Nov 2025 17:01:00 +0100 Subject: [PATCH 1/2] Add method to report fraud --- libs/labelbox/src/labelbox/client.py | 45 ++++ libs/labelbox/src/labelbox/schema/project.py | 57 ++++- .../src/alignerr/alignerr_project.py | 233 ++++++++++++++++++ 3 files changed, 331 insertions(+), 4 deletions(-) diff --git a/libs/labelbox/src/labelbox/client.py b/libs/labelbox/src/labelbox/client.py index b48db006e..0d8c113a3 100644 --- a/libs/labelbox/src/labelbox/client.py +++ b/libs/labelbox/src/labelbox/client.py @@ -503,6 +503,51 @@ def delete_model_config(self, id: str) -> bool: raise ResourceNotFoundError(Entity.ModelConfig, params) return result["deleteModelConfig"]["success"] + def delete_project_memberships( + self, project_id: str, user_ids: list[str] + ) -> dict: + """Deletes project memberships for one or more users. + + Args: + project_id (str): ID of the project + user_ids (list[str]): List of user IDs to remove from the project + + Returns: + dict: Result containing: + - success (bool): True if operation succeeded + - errorMessage (str or None): Error message if operation failed + + Example: + >>> result = client.delete_project_memberships( + >>> project_id="project123", + >>> user_ids=["user1", "user2"] + >>> ) + >>> if result["success"]: + >>> print("Users removed successfully") + >>> else: + >>> print(f"Error: {result['errorMessage']}") + """ + mutation = """mutation DeleteProjectMembershipsPyApi( + $projectId: ID! + $userIds: [ID!]! + ) { + deleteProjectMemberships(where: { + projectId: $projectId + userIds: $userIds + }) { + success + errorMessage + } + }""" + + params = { + "projectId": project_id, + "userIds": user_ids, + } + + result = self.execute(mutation, params) + return result["deleteProjectMemberships"] + def create_dataset( self, iam_integration=IAMIntegration._DEFAULT, **kwargs ) -> Dataset: diff --git a/libs/labelbox/src/labelbox/schema/project.py b/libs/labelbox/src/labelbox/schema/project.py index 8c89b9ae4..b5b33bb2a 100644 --- a/libs/labelbox/src/labelbox/schema/project.py +++ b/libs/labelbox/src/labelbox/schema/project.py @@ -317,7 +317,7 @@ def get_resource_tags(self) -> List[ResourceTag]: return [ResourceTag(self.client, tag) for tag in results] - def labels(self, datasets=None, order_by=None) -> PaginatedCollection: + def labels(self, datasets=None, order_by=None, created_by=None) -> PaginatedCollection: """Custom relationship expansion method to support limited filtering. Args: @@ -325,6 +325,20 @@ def labels(self, datasets=None, order_by=None) -> PaginatedCollection: whose Labels are sought. If not provided, all Labels in this Project are returned. order_by (None or (Field, Field.Order)): Ordering clause. + created_by (str or User): Optional. Filter labels by the user who created them. + Can be a user ID string or a User object. + + Returns: + PaginatedCollection of Labels matching the filters. + + Example: + >>> # Get all labels + >>> all_labels = project.labels() + >>> + >>> # Get labels by specific user + >>> user_labels = project.labels(created_by=user_id) + >>> # or + >>> user_labels = project.labels(created_by=user_object) """ Label = Entity.Label @@ -335,10 +349,20 @@ def labels(self, datasets=None, order_by=None) -> PaginatedCollection: stacklevel=2, ) + # Build where clause + where_clauses = [] + if datasets is not None: - where = " where:{dataRow: {dataset: {id_in: [%s]}}}" % ", ".join( - '"%s"' % dataset.uid for dataset in datasets - ) + dataset_ids = ", ".join('"%s"' % dataset.uid for dataset in datasets) + where_clauses.append(f"dataRow: {{dataset: {{id_in: [{dataset_ids}]}}}}") + + if created_by is not None: + # Handle both User object and user_id string + user_id = created_by.uid if hasattr(created_by, 'uid') else created_by + where_clauses.append(f'createdBy: {{id: "{user_id}"}}') + + if where_clauses: + where = " where:{" + ", ".join(where_clauses) + "}" else: where = "" @@ -370,6 +394,31 @@ def labels(self, datasets=None, order_by=None) -> PaginatedCollection: Label, ) + def delete_labels_by_user(self, user_id: str) -> int: + """Soft deletes all labels created by a specific user in this project. + + This performs a soft delete (sets deleted=true in the database). + The labels will no longer appear in queries but remain in the database. + + Args: + user_id (str): The ID of the user whose labels to delete. + + Returns: + int: Number of labels deleted. + + Example: + >>> project = client.get_project(project_id) + >>> deleted_count = project.delete_labels_by_user(user_id) + >>> print(f"Deleted {deleted_count} labels") + """ + labels_to_delete = list(self.labels(created_by=user_id)) + + if not labels_to_delete: + return 0 + + Entity.Label.bulk_delete(labels_to_delete) + return len(labels_to_delete) + def export( self, task_name: Optional[str] = None, diff --git a/libs/lbox-alignerr/src/alignerr/alignerr_project.py b/libs/lbox-alignerr/src/alignerr/alignerr_project.py index cc2460f87..f9432ff3a 100644 --- a/libs/lbox-alignerr/src/alignerr/alignerr_project.py +++ b/libs/lbox-alignerr/src/alignerr/alignerr_project.py @@ -13,6 +13,7 @@ ProjectBoostWorkforce, ) from labelbox.pagination import PaginatedCollection +from labelbox.orm.model import Entity logger = logging.getLogger(__name__) @@ -153,6 +154,238 @@ def get_project_owner(self) -> Optional[ProjectBoostWorkforce]: client=self.client, project_id=self.project.uid ) + def _get_user_labels(self, user_id: str): + """Get all labels created by a user in this project. + + Args: + user_id: ID of the user + + Returns: + List of Label objects + + Raises: + Exception: If labels cannot be retrieved + """ + labels = list(self.project.labels(created_by=user_id)) + logger.info( + "Found %d labels created by user %s in project %s", + len(labels), + user_id, + self.project.uid + ) + return labels + + def _create_trust_safety_case(self, user_id: str, event_metadata: dict) -> bool: + """Create a Trust & Safety case for a user. + + Args: + user_id: ID of the user being reported + event_metadata: JSON metadata about the event + + Returns: + True if case was created successfully + + Raises: + Exception: If T&S case creation fails + """ + mutation = """mutation CreateTrustAndSafetyCasePyApi( + $subjectUserId: String! + $eventType: CaseEventGqlType! + $severity: CaseSeverityGqlType! + $eventMetadata: Json! + ) { + createTrustAndSafetyCase(input: { + subjectUserId: $subjectUserId + eventType: $eventType + severity: $severity + eventMetadata: $eventMetadata + }) { + success + } + }""" + + params = { + "subjectUserId": user_id, + "eventType": "manual", + "severity": "high", + "eventMetadata": event_metadata, + } + + result = self.client.execute(mutation, params) + success = result["createTrustAndSafetyCase"]["success"] + + if success: + logger.info( + "Created T&S case for user %s in project %s", + user_id, + self.project.uid + ) + + return success + + def _remove_user_from_project(self, user_id: str) -> None: + """Remove a user from this project. + + Args: + user_id: ID of the user to remove + + Raises: + ValueError: If user not found in project + Exception: If removal fails + """ + # Check if user is in project members + user_found = False + for member in self.project.members(): + if member.user().uid == user_id: + user_found = True + break + + if not user_found: + logger.warning("User %s not found in project %s members", user_id, self.project.uid) + raise ValueError(f"User {user_id} not found in project members") + + # Remove user using deleteProjectMemberships mutation + result = self.client.delete_project_memberships( + project_id=self.project.uid, + user_ids=[user_id] + ) + + if not result.get("success"): + error_message = result.get("errorMessage", "Unknown error") + logger.error("Failed to remove user: %s", error_message) + raise Exception(f"Failed to remove user: {error_message}") + + logger.info( + "Removed user %s from project %s", + user_id, + self.project.uid + ) + + def _delete_user_labels(self, labels) -> int: + """Delete a list of labels. + + Args: + labels: List of Label objects to delete + + Returns: + Number of labels deleted + + Raises: + Exception: If deletion fails + """ + if not labels: + return 0 + + Entity.Label.bulk_delete(labels) + logger.info( + "Deleted %d labels in project %s", + len(labels), + self.project.uid + ) + return len(labels) + + def report_fraud( + self, + user_id: str, + reason: str, + custom_metadata: dict = None + ) -> dict: + """Report potential fraud by a user in this project. + + This method performs the following actions: + 1. Gets all labels created by the user in this project + 2. Creates a Trust & Safety case for the user (MANUAL event type, HIGH severity) + 3. Removes the user from the project (prevents creating more labels) + 4. Deletes all the user's labels + + Args: + user_id (str): The ID of the user to report for fraud. + reason (str): Reason for reporting fraud (e.g., "Spam labels", "Low quality work"). + custom_metadata (dict, optional): Additional metadata to include in the T&S case. + Will be merged with automatic metadata (project_id, reason, label_count, label_ids). + + Returns: + dict: A dictionary containing: + - ts_case_id: Status of T&S case creation ("created" if successful) + - labels_found: Number of labels found by the user + - user_removed: Whether the user was successfully removed + - labels_deleted: Number of labels deleted + - error: Any error message if any step failed + + Example: + >>> from alignerr import AlignerrWorkspace + >>> from labelbox import Client + >>> + >>> client = Client(api_key="YOUR_API_KEY") + >>> workspace = AlignerrWorkspace.from_labelbox(client) + >>> project = workspace.project_builder().from_existing(project_id) + >>> + >>> # Report fraud with reason + >>> result = project.report_fraud(user_id, reason="Spam labels detected") + >>> print(f"Removed user: {result['user_removed']}, Deleted {result['labels_deleted']} labels") + >>> + >>> # With additional custom metadata + >>> result = project.report_fraud( + >>> user_id, + >>> reason="Production quality issues", + >>> custom_metadata={"ticket_id": "TICKET-123", "reviewer": "john@example.com"} + >>> ) + """ + result = { + "ts_case_id": None, + "labels_found": 0, + "user_removed": False, + "labels_deleted": 0, + "error": None, + } + + # Step 1: Get all labels cteated by this user in this project + try: + labels_to_delete = self._get_user_labels(user_id) + result["labels_found"] = len(labels_to_delete) + except Exception as e: + logger.error("Failed to get labels: %s", str(e)) + result["error"] = f"Failed to get labels: {str(e)}" + return result + + # Step 2: Create T&S case with label information + try: + event_metadata = { + "project_id": self.project.uid, + "reason": reason, + "label_count": len(labels_to_delete), + "label_ids": [label.uid for label in labels_to_delete], + } + if custom_metadata: + event_metadata.update(custom_metadata) + + ts_case_created = self._create_trust_safety_case(user_id, event_metadata) + if ts_case_created: + result["ts_case_id"] = "created" + except Exception as e: + logger.error("Failed to create T&S case: %s", str(e)) + result["error"] = f"Failed to create T&S case: {str(e)}" + return result + + # Step 3: Remove user from project (prevent creating more labels) + try: + self._remove_user_from_project(user_id) + result["user_removed"] = True + except Exception as e: + logger.error("Failed to remove user from project: %s", str(e)) + result["error"] = f"Failed to remove user: {str(e)}" + return result + + # Step 4: Delete all labels by this user + try: + result["labels_deleted"] = self._delete_user_labels(labels_to_delete) + except Exception as e: + logger.error("Failed to delete labels: %s", str(e)) + result["error"] = f"Failed to delete labels: {str(e)}" + return result + + return result + class AlignerrWorkspace: def __init__(self, client: "Client"): From 2ce41dc5d66a305287b9233eafafa831190114d5 Mon Sep 17 00:00:00 2001 From: kozikkamil Date: Mon, 1 Dec 2025 16:30:57 +0100 Subject: [PATCH 2/2] Chunk --- libs/labelbox/src/labelbox/schema/project.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libs/labelbox/src/labelbox/schema/project.py b/libs/labelbox/src/labelbox/schema/project.py index b5b33bb2a..f00a75cb2 100644 --- a/libs/labelbox/src/labelbox/schema/project.py +++ b/libs/labelbox/src/labelbox/schema/project.py @@ -399,6 +399,7 @@ def delete_labels_by_user(self, user_id: str) -> int: This performs a soft delete (sets deleted=true in the database). The labels will no longer appear in queries but remain in the database. + Labels are deleted in chunks of 500 to avoid overwhelming the API. Args: user_id (str): The ID of the user whose labels to delete. @@ -416,8 +417,15 @@ def delete_labels_by_user(self, user_id: str) -> int: if not labels_to_delete: return 0 - Entity.Label.bulk_delete(labels_to_delete) - return len(labels_to_delete) + chunk_size = 500 + total_deleted = 0 + + for i in range(0, len(labels_to_delete), chunk_size): + chunk = labels_to_delete[i:i + chunk_size] + Entity.Label.bulk_delete(chunk) + total_deleted += len(chunk) + + return total_deleted def export( self,