Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions libs/labelbox/src/labelbox/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
57 changes: 53 additions & 4 deletions libs/labelbox/src/labelbox/schema/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,28 @@ 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:
datasets (iterable of Dataset): Optional collection of Datasets
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

Expand All @@ -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 = ""

Expand Down Expand Up @@ -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,
Expand Down
233 changes: 233 additions & 0 deletions libs/lbox-alignerr/src/alignerr/alignerr_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ProjectBoostWorkforce,
)
from labelbox.pagination import PaginatedCollection
from labelbox.orm.model import Entity

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -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": "[email protected]"}
>>> )
"""
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"):
Expand Down
Loading