From d4ae2ef52750a6413c1229f75cc0cc0b2a29fc98 Mon Sep 17 00:00:00 2001 From: Pawel Kozik Date: Wed, 29 Jul 2026 15:57:14 +0200 Subject: [PATCH 1/2] [ALNR-6808] Remove ProjectRateV2 methods from lbox-alignerr SDK Delete legacy Pay By Role rate helpers and fail clearly pointing callers to the Rates UI (Pay By Activity). Bump package to 0.3.0. --- libs/lbox-alignerr/README.md | 1 - libs/lbox-alignerr/pyproject.toml | 2 +- .../src/alignerr/alignerr_project.py | 20 +- .../src/alignerr/alignerr_project_builder.py | 115 +--------- .../src/alignerr/alignerr_project_factory.py | 147 +------------ .../src/alignerr/schema/project_rate.py | 117 ----------- .../assets/test_project_comprehensive.yaml | 19 -- .../integration/test_alignerr_project.py | 48 ++--- .../test_alignerr_project_builder.py | 108 +++------- .../test_alignerr_project_factory.py | 197 +++--------------- .../tests/integration/test_project_rate.py | 144 ------------- 11 files changed, 103 insertions(+), 815 deletions(-) delete mode 100644 libs/lbox-alignerr/src/alignerr/schema/project_rate.py delete mode 100644 libs/lbox-alignerr/tests/integration/test_project_rate.py diff --git a/libs/lbox-alignerr/README.md b/libs/lbox-alignerr/README.md index 3dc34b809..0dd3c8fee 100644 --- a/libs/lbox-alignerr/README.md +++ b/libs/lbox-alignerr/README.md @@ -4,6 +4,5 @@ Alignerr workspace management for Labelbox. This package provides functionality for managing Alignerr projects, including: - Project creation and configuration -- Rate management for labelers and reviewers - Domain and tag management - Workforce management diff --git a/libs/lbox-alignerr/pyproject.toml b/libs/lbox-alignerr/pyproject.toml index 5c72fc408..5a1faa590 100644 --- a/libs/lbox-alignerr/pyproject.toml +++ b/libs/lbox-alignerr/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "lbox-alignerr" -version = "0.2.0" +version = "0.3.0" description = "Alignerr workspace management for Labelbox" authors = [ { name = "Labelbox", email = "engineering@labelbox.com" } diff --git a/libs/lbox-alignerr/src/alignerr/alignerr_project.py b/libs/lbox-alignerr/src/alignerr/alignerr_project.py index 92a119f43..0abe37cd6 100644 --- a/libs/lbox-alignerr/src/alignerr/alignerr_project.py +++ b/libs/lbox-alignerr/src/alignerr/alignerr_project.py @@ -3,7 +3,6 @@ import logging -from alignerr.schema.project_rate import ProjectRateV2 from alignerr.schema.project_domain import ProjectDomain from alignerr.schema.enchanced_resource_tags import ( EnhancedResourceTag, @@ -17,6 +16,11 @@ logger = logging.getLogger(__name__) +PAY_BY_ROLE_REMOVED_MSG = ( + "Pay By Role rates were removed. " + "Configure project rates in the Labelbox Rates UI (Pay By Activity)." +) + if TYPE_CHECKING: from labelbox import Client @@ -63,17 +67,11 @@ def add_domain(self, project_domain: ProjectDomain): domain_ids=[project_domain.uid], ) - def get_project_rates(self) -> list["ProjectRateV2"]: - return ProjectRateV2.get_by_project_id( - client=self.client, project_id=self.project.uid - ) + def get_project_rates(self, *args, **kwargs): + raise NotImplementedError(PAY_BY_ROLE_REMOVED_MSG) - def set_project_rate(self, project_rate_input): - return ProjectRateV2.set_project_rate( - client=self.client, - project_id=self.project.uid, - project_rate_input=project_rate_input, - ) + def set_project_rate(self, *args, **kwargs): + raise NotImplementedError(PAY_BY_ROLE_REMOVED_MSG) def set_tags(self, tag_names: list[str], tag_type: ResourceTagType): # Convert tag names to tag IDs diff --git a/libs/lbox-alignerr/src/alignerr/alignerr_project_builder.py b/libs/lbox-alignerr/src/alignerr/alignerr_project_builder.py index 1edf00d31..101d69931 100644 --- a/libs/lbox-alignerr/src/alignerr/alignerr_project_builder.py +++ b/libs/lbox-alignerr/src/alignerr/alignerr_project_builder.py @@ -1,10 +1,8 @@ -import datetime from enum import Enum from typing import TYPE_CHECKING, Optional, Union, List import logging -from alignerr.schema.project_rate import BillingMode -from alignerr.schema.project_rate import ProjectRateInput +from alignerr.alignerr_project import PAY_BY_ROLE_REMOVED_MSG from alignerr.schema.project_domain import ProjectDomain from alignerr.schema.enchanced_resource_tags import ( EnhancedResourceTag, @@ -28,18 +26,15 @@ class ValidationType(Enum): if TYPE_CHECKING: from labelbox import Client - from alignerr.alignerr_project import AlignerrProject, AlignerrRole + from alignerr.alignerr_project import AlignerrProject class AlignerrProjectBuilder: def __init__(self, client: "Client"): self.client = client - self._alignerr_rates: dict[str, ProjectRateInput] = {} - self._customer_rate: Optional[ProjectRateInput] = None self._domains: list[ProjectDomain] = [] self._enhanced_resource_tags: list[EnhancedResourceTag] = [] self._project_owner_email: Optional[str] = None - self.role_name_to_id = self._get_role_name_to_id() def set_name(self, name: str): self.project_name = name @@ -49,72 +44,11 @@ def set_media_type(self, media_type: "MediaType"): self.project_media_type = media_type return self - def set_alignerr_role_rate( - self, - *, - role_name: "AlignerrRole", - rate: float, - billing_mode: BillingMode, - effective_since: datetime.datetime, - effective_until: Optional[datetime.datetime] = None, - ): - if role_name.value not in self.role_name_to_id: - raise ValueError(f"Role {role_name.value} not found") - - role_id = self.role_name_to_id[role_name.value] - role_name_str = role_name.value - - # Convert datetime objects to ISO format strings - effective_since_str = ( - effective_since.isoformat() - if isinstance(effective_since, datetime.datetime) - else effective_since - ) - effective_until_str = ( - effective_until.isoformat() - if isinstance(effective_until, datetime.datetime) - else effective_until - ) - - self._alignerr_rates[role_name_str] = ProjectRateInput( - rateForId=role_id, - isBillRate=False, - billingMode=billing_mode, - rate=rate, - effectiveSince=effective_since_str, - effectiveUntil=effective_until_str, - ) - return self + def set_alignerr_role_rate(self, *args, **kwargs): + raise NotImplementedError(PAY_BY_ROLE_REMOVED_MSG) - def set_customer_rate( - self, - *, - rate: float, - billing_mode: BillingMode, - effective_since: datetime.datetime, - effective_until: Optional[datetime.datetime] = None, - ): - # Convert datetime objects to ISO format strings - effective_since_str = ( - effective_since.isoformat() - if isinstance(effective_since, datetime.datetime) - else effective_since - ) - effective_until_str = ( - effective_until.isoformat() - if isinstance(effective_until, datetime.datetime) - else effective_until - ) - - self._customer_rate = ProjectRateInput( - rateForId="", # Empty string for customer rate - isBillRate=True, - billingMode=billing_mode, - rate=rate, - effectiveSince=effective_since_str, - effectiveUntil=effective_until_str, - ) - return self + def set_customer_rate(self, *args, **kwargs): + raise NotImplementedError(PAY_BY_ROLE_REMOVED_MSG) def set_domains(self, domains: list[str]): for domain in domains: @@ -189,18 +123,12 @@ def create(self, skip_validation: Union[bool, List[ValidationType]] = False): self.client, labelbox_project, _internal=True ) - self._create_rates(alignerr_project) self._create_domains(alignerr_project) self._create_resource_tags(alignerr_project) self._create_project_owner(alignerr_project) return alignerr_project - def _create_rates(self, alignerr_project: "AlignerrProject"): - for alignerr_role, project_rate in self._alignerr_rates.items(): - logger.info(f"Setting project rate for {alignerr_role}") - alignerr_project.set_project_rate(project_rate) - def _create_domains(self, alignerr_project: "AlignerrProject"): if self._domains: logger.info(f"Setting domains: {[domain.name for domain in self._domains]}") @@ -248,30 +176,11 @@ def _create_project_owner(self, alignerr_project: "AlignerrProject"): project_owner_user_id=user_id, ) - def _validate_alignerr_rates(self): - # Import here to avoid circular imports - from alignerr.alignerr_project import AlignerrRole - - required_role_rates = set( - [AlignerrRole.Labeler.value, AlignerrRole.Reviewer.value] - ) - - for role_name in self._alignerr_rates.keys(): - required_role_rates.remove(role_name) - if len(required_role_rates) > 0: - raise ValueError(f"Required role rates are not set: {required_role_rates}") - - def _validate_customer_rate(self): - if self._customer_rate is None: - raise ValueError("Customer rate is not set") - def _validate_project_owner(self): if self._project_owner_email is None: raise ValueError("Project owner is not set") def _validate(self): - self._validate_alignerr_rates() - self._validate_customer_rate() self._validate_project_owner() def _validate_selective(self, skip_validations: List[ValidationType]): @@ -280,19 +189,11 @@ def _validate_selective(self, skip_validations: List[ValidationType]): Args: skip_validations: List of ValidationType enums to skip """ - if ValidationType.ALIGNERR_RATE not in skip_validations: - self._validate_alignerr_rates() - - if ValidationType.CUSTOMER_RATE not in skip_validations: - self._validate_customer_rate() - + # ALIGNERR_RATE / CUSTOMER_RATE are retained for callers that still pass them + # in skip_validation lists; rate validation itself has been removed. if ValidationType.PROJECT_OWNER not in skip_validations: self._validate_project_owner() - def _get_role_name_to_id(self) -> dict[str, str]: - roles = self.client.get_roles() - return {role.name: role.uid for role in roles.values()} - def _find_user_by_email(self, email: str) -> Optional[str]: """Find user ID by email in the organization. diff --git a/libs/lbox-alignerr/src/alignerr/alignerr_project_factory.py b/libs/lbox-alignerr/src/alignerr/alignerr_project_factory.py index 27d744b71..e44b27014 100644 --- a/libs/lbox-alignerr/src/alignerr/alignerr_project_factory.py +++ b/libs/lbox-alignerr/src/alignerr/alignerr_project_factory.py @@ -1,10 +1,9 @@ -import datetime from typing import TYPE_CHECKING, Union, List import yaml from pathlib import Path import logging -from alignerr.schema.project_rate import BillingMode +from alignerr.alignerr_project import PAY_BY_ROLE_REMOVED_MSG from alignerr.schema.enchanced_resource_tags import ResourceTagType from labelbox.schema.media_type import MediaType @@ -35,27 +34,21 @@ def create(self, yaml_file_path: str, skip_validation: Union[bool, List] = False Raises: FileNotFoundError: If the YAML file doesn't exist yaml.YAMLError: If the YAML file is invalid - ValueError: If required fields are missing or invalid + ValueError: If required fields are missing or invalid, or if legacy + rates / customer_rate keys are present YAML Configuration Structure: name: str (required) - Project name media_type: str (required) - Media type (e.g., "Image", "Video", "Text") - rates: dict (optional) - Alignerr role rates - role_name: - rate: float - billing_mode: str - effective_since: str (ISO datetime) - effective_until: str (optional, ISO datetime) - customer_rate: dict (optional) - Customer billing rate - rate: float - billing_mode: str - effective_since: str (ISO datetime) - effective_until: str (optional, ISO datetime) domains: list[str] (optional) - Project domain names tags: list[dict] (optional) - Enhanced resource tags - text: str type: str (ResourceTagType enum value) project_owner: str (optional) - Project owner email address + + Note: + Legacy `rates` and `customer_rate` YAML keys are no longer supported. + Configure rates in the Labelbox Rates UI (Pay By Activity). """ logger.info(f"Creating project from YAML file: {yaml_file_path}") @@ -81,11 +74,13 @@ def create(self, yaml_file_path: str, skip_validation: Union[bool, List] = False f"Required field '{field}' is missing from YAML configuration" ) + if "rates" in config or "customer_rate" in config: + raise ValueError(PAY_BY_ROLE_REMOVED_MSG) + # Import here to avoid circular imports from alignerr.alignerr_project_builder import ( AlignerrProjectBuilder, ) - from alignerr.alignerr_project import AlignerrRole # Create project builder builder = AlignerrProjectBuilder(self.client) @@ -106,128 +101,6 @@ def create(self, yaml_file_path: str, skip_validation: Union[bool, List] = False builder.set_media_type(media_type) - # Set project rates if provided - if "rates" in config: - rates_config = config["rates"] - if not isinstance(rates_config, dict): - raise ValueError("'rates' must be a dictionary") - - for role_name, rate_config in rates_config.items(): - try: - alignerr_role = AlignerrRole(role_name.upper()) - except ValueError: - raise ValueError( - f"Invalid role '{role_name}'. Must be one of: {[r.value for r in AlignerrRole]}" - ) - - # Validate rate configuration - required_rate_fields = [ - "rate", - "billing_mode", - "effective_since", - ] - for field in required_rate_fields: - if field not in rate_config: - raise ValueError( - f"Required field '{field}' is missing for role '{role_name}'" - ) - - # Parse billing mode - try: - billing_mode = BillingMode(rate_config["billing_mode"]) - except ValueError: - raise ValueError( - f"Invalid billing_mode '{rate_config['billing_mode']}' for role '{role_name}'. Must be one of: {[e.value for e in BillingMode]}" - ) - - # Parse effective dates - try: - effective_since = datetime.datetime.fromisoformat( - rate_config["effective_since"] - ) - except ValueError: - raise ValueError( - f"Invalid effective_since date format for role '{role_name}'. Use ISO format (YYYY-MM-DDTHH:MM:SS)" - ) - - effective_until = None - if "effective_until" in rate_config and rate_config["effective_until"]: - try: - effective_until = datetime.datetime.fromisoformat( - rate_config["effective_until"] - ) - except ValueError: - raise ValueError( - f"Invalid effective_until date format for role '{role_name}'. Use ISO format (YYYY-MM-DDTHH:MM:SS)" - ) - - # Set the rate - builder.set_alignerr_role_rate( - role_name=alignerr_role, - rate=float(rate_config["rate"]), - billing_mode=billing_mode, - effective_since=effective_since, - effective_until=effective_until, - ) - - # Set customer rate if provided - if "customer_rate" in config: - customer_rate_config = config["customer_rate"] - if not isinstance(customer_rate_config, dict): - raise ValueError("'customer_rate' must be a dictionary") - - # Validate customer rate configuration - required_customer_rate_fields = [ - "rate", - "billing_mode", - "effective_since", - ] - for field in required_customer_rate_fields: - if field not in customer_rate_config: - raise ValueError( - f"Required field '{field}' is missing for customer_rate" - ) - - # Parse billing mode - try: - billing_mode = BillingMode(customer_rate_config["billing_mode"]) - except ValueError: - raise ValueError( - f"Invalid billing_mode '{customer_rate_config['billing_mode']}' for customer_rate. Must be one of: {[e.value for e in BillingMode]}" - ) - - # Parse effective dates - try: - effective_since = datetime.datetime.fromisoformat( - customer_rate_config["effective_since"] - ) - except ValueError: - raise ValueError( - "Invalid effective_since date format for customer_rate. Use ISO format (YYYY-MM-DDTHH:MM:SS)" - ) - - effective_until = None - if ( - "effective_until" in customer_rate_config - and customer_rate_config["effective_until"] - ): - try: - effective_until = datetime.datetime.fromisoformat( - customer_rate_config["effective_until"] - ) - except ValueError: - raise ValueError( - "Invalid effective_until date format for customer_rate. Use ISO format (YYYY-MM-DDTHH:MM:SS)" - ) - - # Set the customer rate - builder.set_customer_rate( - rate=float(customer_rate_config["rate"]), - billing_mode=billing_mode, - effective_since=effective_since, - effective_until=effective_until, - ) - # Set domains if provided if "domains" in config: domains_config = config["domains"] diff --git a/libs/lbox-alignerr/src/alignerr/schema/project_rate.py b/libs/lbox-alignerr/src/alignerr/schema/project_rate.py deleted file mode 100644 index 1d0d25110..000000000 --- a/libs/lbox-alignerr/src/alignerr/schema/project_rate.py +++ /dev/null @@ -1,117 +0,0 @@ -from enum import Enum -from typing import Optional -from labelbox.orm.db_object import DbObject, Deletable -from labelbox.orm.model import Relationship, Field -from pydantic import BaseModel, model_validator - - -class BillingMode(Enum): - BY_TASK = "BY_TASK" - BY_HOUR = "BY_HOUR" - BY_TASK_PER_TURN = "BY_TASK_PER_TURN" - BY_ACCEPTED_TASK = "BY_ACCEPTED_TASK" - - -class ProjectRateInput(BaseModel): - rateForId: str - isBillRate: bool - billingMode: BillingMode - rate: float - effectiveSince: str # DateTime as string - effectiveUntil: Optional[str] = None # Optional DateTime as string - - @model_validator(mode="after") - def validate_fields(self): - if self.rate < 0: - raise ValueError("Rate must be greater than or equal to 0") - - if self.isBillRate and self.rateForId != "": - raise ValueError( - "isBillRate indicates that this is a customer bill rate. rateForId must be empty if isBillRate is true" - ) - - if not self.isBillRate and self.rateForId == "": - raise ValueError("rateForId must be set to the id of the Alignerr Role") - - return self - - -class ProjectRateV2(DbObject, Deletable): - # Relationships - userRole = Relationship.ToOne("UserRole", False) - updatedBy = Relationship.ToOne("User", False) - - # Fields matching the GraphQL schema - isBillRate = Field.Boolean("isBillRate") - billingMode = Field.Enum(BillingMode, "billingMode") - rate = Field.Float("rate") - createdAt = Field.DateTime("createdAt") - updatedAt = Field.DateTime("updatedAt") - effectiveSince = Field.DateTime("effectiveSince") - effectiveUntil = Field.DateTime("effectiveUntil") - - @classmethod - def get_by_project_id(cls, client, project_id: str) -> list["ProjectRateV2"]: - query_str = """ - query GetAllProjectRatesPyApi($projectId: ID!) { - project(where: { id: $projectId }) { - id - ratesV2 { - id - userRole { - id - name - } - isBillRate - billingMode - rate - effectiveSince - effectiveUntil - createdAt - updatedAt - updatedBy { - id - email - name - } - } - } - } - """ - result = client.execute(query_str, {"projectId": project_id}) - rates_data = result["project"]["ratesV2"] - - if not rates_data: - return [] - - # Return all rates as ProjectRateV2 objects - return [cls(client, rate_data) for rate_data in rates_data] - - @classmethod - def set_project_rate( - cls, client, project_id: str, project_rate_input: ProjectRateInput - ): - mutation_str = """mutation SetProjectRateV2PyApi($input: SetProjectRateV2Input!) { - setProjectRateV2(input: $input) { - success - } - }""" - - params = { - "projectId": project_id, - "input": { - "projectId": project_id, - "userRoleId": project_rate_input.rateForId, - "isBillRate": project_rate_input.isBillRate, - "billingMode": project_rate_input.billingMode.value - if hasattr(project_rate_input.billingMode, "value") - else project_rate_input.billingMode, - "rate": project_rate_input.rate, - "effectiveSince": project_rate_input.effectiveSince, - "effectiveUntil": project_rate_input.effectiveUntil, - }, - } - - result = client.execute(mutation_str, params) - - return result["setProjectRateV2"]["success"] diff --git a/libs/lbox-alignerr/tests/assets/test_project_comprehensive.yaml b/libs/lbox-alignerr/tests/assets/test_project_comprehensive.yaml index f341602df..9055d0f64 100644 --- a/libs/lbox-alignerr/tests/assets/test_project_comprehensive.yaml +++ b/libs/lbox-alignerr/tests/assets/test_project_comprehensive.yaml @@ -1,25 +1,6 @@ name: "TestComprehensiveProject" media_type: "Image" -# Alignerr role rates -rates: - LABELER: - rate: 15.0 - billing_mode: "BY_HOUR" - effective_since: "2024-01-01T00:00:00" - effective_until: "2024-12-31T23:59:59" - REVIEWER: - rate: 20.0 - billing_mode: "BY_HOUR" - effective_since: "2024-01-01T00:00:00" - -# Customer billing rate -customer_rate: - rate: 25.0 - billing_mode: "BY_HOUR" - effective_since: "2024-01-01T00:00:00" - effective_until: "2024-12-31T23:59:59" - # Project domains (will be created if they don't exist) domains: - "TestDomain1" diff --git a/libs/lbox-alignerr/tests/integration/test_alignerr_project.py b/libs/lbox-alignerr/tests/integration/test_alignerr_project.py index f3c38964d..70c71132d 100644 --- a/libs/lbox-alignerr/tests/integration/test_alignerr_project.py +++ b/libs/lbox-alignerr/tests/integration/test_alignerr_project.py @@ -3,12 +3,15 @@ These tests interact with the actual Labelbox API to verify AlignerrProject operations. """ -import datetime +import re import uuid import pytest -from alignerr.alignerr_project import AlignerrProject, AlignerrWorkspace -from alignerr.schema.project_rate import BillingMode, ProjectRateInput +from alignerr.alignerr_project import ( + AlignerrProject, + AlignerrWorkspace, + PAY_BY_ROLE_REMOVED_MSG, +) from labelbox.schema.media_type import MediaType @@ -72,33 +75,14 @@ def test_alignerr_project_domains(client, test_alignerr_project): # The collection might be empty for a new project, which is expected -def test_alignerr_project_get_project_rates_no_rates(client, test_alignerr_project): - """Test get_project_rates() when no rates are set.""" - # For a new project without rates, this should return an empty list - project_rates = test_alignerr_project.get_project_rates() - assert project_rates == [] - - -def test_alignerr_project_set_and_get_project_rates(client, test_alignerr_project): - """Test setting and getting project rates.""" - # Create a project rate input for a customer rate (isBillRate=True requires empty rateForId) - project_rate_input = ProjectRateInput( - rateForId="", # Empty string for customer rate - isBillRate=True, - billingMode=BillingMode.BY_HOUR, - rate=25.0, - effectiveSince=datetime.datetime.now().isoformat(), - effectiveUntil=None, - ) +def test_alignerr_project_rate_methods_removed(client, test_alignerr_project): + """Pay By Role rate helpers raise and direct callers to the Rates UI.""" + with pytest.raises( + NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) + ): + test_alignerr_project.get_project_rates() - # Set the project rate - result = test_alignerr_project.set_project_rate(project_rate_input) - assert result is True # Should return success status - - # Get the project rates back - project_rates = test_alignerr_project.get_project_rates() - # Should return a list with at least one rate - assert isinstance(project_rates, list) - assert len(project_rates) >= 1 - # Note: The actual rate retrieval might depend on the API implementation - # This test verifies the method calls work without errors + with pytest.raises( + NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) + ): + test_alignerr_project.set_project_rate(None) diff --git a/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py b/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py index 2bcdd9f08..43cf91f1e 100644 --- a/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py +++ b/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py @@ -1,9 +1,9 @@ -"""Integration tests for ProjectRateV2 functionality.""" +"""Integration tests for AlignerrProjectBuilder functionality.""" + +import re -import datetime from labelbox import Client -from alignerr.alignerr_project import AlignerrRole, AlignerrWorkspace -from alignerr.schema.project_rate import BillingMode +from alignerr.alignerr_project import AlignerrWorkspace, PAY_BY_ROLE_REMOVED_MSG from labelbox.schema.media_type import MediaType import pytest @@ -14,12 +14,6 @@ def test_skip_validation(client: Client): .project_builder() .set_name("TestAlignerrProject") .set_media_type(MediaType.Image) - .set_alignerr_role_rate( - role_name=AlignerrRole.Labeler, - rate=10.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) .create(skip_validation=True) ) assert alignerr_project is not None @@ -29,15 +23,10 @@ def test_skip_validation(client: Client): def test_create_alignerr_project_using_builder_validate_input(client: Client): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Project owner is not set"): AlignerrWorkspace.from_labelbox(client).project_builder().set_name( "TestAlignerrProject" - ).set_media_type(MediaType.Image).set_alignerr_role_rate( - role_name=AlignerrRole.Labeler, - rate=10.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ).create() + ).set_media_type(MediaType.Image).create() # Get current user for project owner current_user = client.get_user() @@ -47,23 +36,6 @@ def test_create_alignerr_project_using_builder_validate_input(client: Client): .project_builder() .set_name("TestAlignerrProject2") .set_media_type(MediaType.Image) - .set_alignerr_role_rate( - role_name=AlignerrRole.Labeler, - rate=10.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - .set_alignerr_role_rate( - role_name=AlignerrRole.Reviewer, - rate=10.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - .set_customer_rate( - rate=15.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) .set_project_owner(current_user.email) .create() ) @@ -74,6 +46,26 @@ def test_create_alignerr_project_using_builder_validate_input(client: Client): alignerr_project.project.delete() +def test_builder_rate_methods_removed(client: Client): + """Pay By Role builder rate helpers raise and direct callers to the Rates UI.""" + builder = ( + AlignerrWorkspace.from_labelbox(client) + .project_builder() + .set_name("TestAlignerrProjectRatesRemoved") + .set_media_type(MediaType.Image) + ) + + with pytest.raises( + NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) + ): + builder.set_alignerr_role_rate() + + with pytest.raises( + NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) + ): + builder.set_customer_rate() + + def test_create_alignerr_project_using_builder_add_domains(client: Client): from alignerr.schema.project_domain import ProjectDomain import uuid @@ -116,10 +108,10 @@ def test_create_alignerr_project_using_builder_add_domains(client: Client): pass -def test_create_alignerr_project_with_rates_domains_and_resource_tags( +def test_create_alignerr_project_with_domains_and_resource_tags( client: Client, ): - """Test creating an Alignerr project with rates, domains, and enhanced resource tags.""" + """Test creating an Alignerr project with domains and enhanced resource tags.""" from alignerr.schema.project_domain import ProjectDomain from alignerr.schema.enchanced_resource_tags import ( EnhancedResourceTag, @@ -159,29 +151,12 @@ def test_create_alignerr_project_with_rates_domains_and_resource_tags( # Get current user for project owner current_user = client.get_user() - # Create project with rates, domains, and resource tags + # Create project with domains and resource tags alignerr_project = ( AlignerrWorkspace.from_labelbox(client) .project_builder() .set_name("TestAlignerrProjectWithAll") .set_media_type(MediaType.Image) - .set_alignerr_role_rate( - role_name=AlignerrRole.Labeler, - rate=12.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - .set_alignerr_role_rate( - role_name=AlignerrRole.Reviewer, - rate=15.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - .set_customer_rate( - rate=20.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) .set_domains([domain1_name, domain2_name]) .set_tags([tag1_text, tag2_text], ResourceTagType.Default) .set_project_owner(current_user.email) @@ -233,23 +208,6 @@ def test_create_alignerr_project_with_project_owner(client: Client): .project_builder() .set_name("TestAlignerrProjectWithOwner") .set_media_type(MediaType.Image) - .set_alignerr_role_rate( - role_name=AlignerrRole.Labeler, - rate=10.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - .set_alignerr_role_rate( - role_name=AlignerrRole.Reviewer, - rate=12.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - .set_customer_rate( - rate=15.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) .set_project_owner(current_user.email) .create() ) @@ -287,13 +245,7 @@ def test_create_alignerr_project_selective_validation_skip_multiple( .project_builder() .set_name("TestAlignerrProjectSkipMultiple") .set_media_type(MediaType.Image) - .set_alignerr_role_rate( - role_name=AlignerrRole.Labeler, - rate=10.0, - billing_mode=BillingMode.BY_HOUR, - effective_since=datetime.datetime.now().isoformat(), - ) - # Note: Missing reviewer rate, customer rate, and project owner, but we skip those validations + # Note: Missing project owner, but we skip that validation .create( skip_validation=[ ValidationType.ALIGNERR_RATE, diff --git a/libs/lbox-alignerr/tests/integration/test_alignerr_project_factory.py b/libs/lbox-alignerr/tests/integration/test_alignerr_project_factory.py index 488bebc2d..d8c5f51ac 100644 --- a/libs/lbox-alignerr/tests/integration/test_alignerr_project_factory.py +++ b/libs/lbox-alignerr/tests/integration/test_alignerr_project_factory.py @@ -2,12 +2,14 @@ import tempfile import os +import re import yaml from pathlib import Path import pytest from labelbox import Client +from alignerr.alignerr_project import PAY_BY_ROLE_REMOVED_MSG from alignerr.alignerr_project_factory import AlignerrProjectFactory from alignerr.alignerr_project_builder import ValidationType from labelbox.schema.media_type import MediaType @@ -34,8 +36,8 @@ def test_create_alignerr_project_from_yaml_basic(client: Client): os.unlink(yaml_file_path) -def test_create_alignerr_project_from_yaml_with_rates(client: Client): - """Test creating an AlignerrProject from YAML with rate configurations.""" +def test_create_alignerr_project_from_yaml_with_rates_raises(client: Client): + """Legacy rates YAML keys raise and direct callers to the Rates UI.""" config = { "name": "TestFactoryProjectWithRates", "media_type": "IMAGE", @@ -44,12 +46,6 @@ def test_create_alignerr_project_from_yaml_with_rates(client: Client): "rate": 0.50, "billing_mode": "BY_TASK", "effective_since": "2024-01-01T00:00:00", - "effective_until": "2024-12-31T23:59:59", - }, - "reviewer": { - "rate": 0.75, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", }, }, } @@ -60,18 +56,34 @@ def test_create_alignerr_project_from_yaml_with_rates(client: Client): try: factory = AlignerrProjectFactory(client) - alignerr_project = factory.create(yaml_file_path, skip_validation=True) + with pytest.raises(ValueError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG)): + factory.create(yaml_file_path, skip_validation=True) + finally: + os.unlink(yaml_file_path) - assert alignerr_project is not None - assert alignerr_project.project.name == "TestFactoryProjectWithRates" - assert alignerr_project.project.media_type == MediaType.Image - # Verify rates were set by checking project rates - project_rates = alignerr_project.get_project_rates() - assert isinstance(project_rates, list) - assert len(project_rates) >= 1 +def test_create_alignerr_project_from_yaml_with_customer_rate_raises( + client: Client, +): + """Legacy customer_rate YAML key raises and directs callers to the Rates UI.""" + config = { + "name": "TestFactoryProjectWithCustomerRate", + "media_type": "IMAGE", + "customer_rate": { + "rate": 25.0, + "billing_mode": "BY_HOUR", + "effective_since": "2024-01-01T00:00:00", + }, + } - alignerr_project.project.delete() + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: + yaml.dump(config, f) + yaml_file_path = f.name + + try: + factory = AlignerrProjectFactory(client) + with pytest.raises(ValueError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG)): + factory.create(yaml_file_path, skip_validation=True) finally: os.unlink(yaml_file_path) @@ -121,55 +133,6 @@ def test_create_alignerr_project_from_yaml_file_not_found(client: Client): factory.create("nonexistent_file.yaml") -def test_create_alignerr_project_from_yaml_with_customer_rate(client: Client): - """Test creating an AlignerrProject from YAML with customer rate configuration.""" - config = { - "name": "TestFactoryProjectWithCustomerRate", - "media_type": "IMAGE", - "rates": { - "LABELER": { - "rate": 15.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - "REVIEWER": { - "rate": 20.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - }, - "customer_rate": { - "rate": 25.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - "effective_until": "2024-12-31T23:59:59", - }, - } - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config, f) - yaml_file_path = f.name - - try: - factory = AlignerrProjectFactory(client) - alignerr_project = factory.create( - yaml_file_path, skip_validation=[ValidationType.PROJECT_OWNER] - ) - - assert alignerr_project is not None - assert alignerr_project.project.name == "TestFactoryProjectWithCustomerRate" - assert alignerr_project.project.media_type == MediaType.Image - - # Verify rates were set - project_rates = alignerr_project.get_project_rates() - assert isinstance(project_rates, list) - assert len(project_rates) >= 2 # Should have both labeler and reviewer rates - - alignerr_project.project.delete() - finally: - os.unlink(yaml_file_path) - - def test_create_alignerr_project_from_yaml_with_domains(client: Client): """Test creating an AlignerrProject from YAML with domains configuration.""" from alignerr.schema.project_domain import ProjectDomain @@ -189,23 +152,6 @@ def test_create_alignerr_project_from_yaml_with_domains(client: Client): config = { "name": "TestFactoryProjectWithDomains", "media_type": "IMAGE", - "rates": { - "LABELER": { - "rate": 15.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - "REVIEWER": { - "rate": 20.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - }, - "customer_rate": { - "rate": 25.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, "domains": [domain1_name, domain2_name], } @@ -265,23 +211,6 @@ def test_create_alignerr_project_from_yaml_with_tags(client: Client): config = { "name": "TestFactoryProjectWithTags", "media_type": "IMAGE", - "rates": { - "LABELER": { - "rate": 15.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - "REVIEWER": { - "rate": 20.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - }, - "customer_rate": { - "rate": 25.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, "tags": [ {"text": tag1_text, "type": "Default"}, {"text": tag2_text, "type": "Billing"}, @@ -324,23 +253,6 @@ def test_create_alignerr_project_from_yaml_with_project_owner(client: Client): config = { "name": "TestFactoryProjectWithOwner", "media_type": "IMAGE", - "rates": { - "LABELER": { - "rate": 15.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - "REVIEWER": { - "rate": 20.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - }, - "customer_rate": { - "rate": 25.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, "project_owner": current_user.email, } @@ -402,11 +314,6 @@ def test_create_alignerr_project_from_yaml_comprehensive(client: Client): assert alignerr_project.project.name == "TestComprehensiveProject" assert alignerr_project.project.media_type == MediaType.Image - # Verify rates were set - project_rates = alignerr_project.get_project_rates() - assert isinstance(project_rates, list) - assert len(project_rates) >= 2 - # Verify project owner was set project_boost_workforce = alignerr_project.get_project_owner() if project_boost_workforce: @@ -422,23 +329,6 @@ def test_create_alignerr_project_from_yaml_selective_validation(client: Client): config = { "name": "TestFactoryProjectSelectiveValidation", "media_type": "IMAGE", - "rates": { - "LABELER": { - "rate": 15.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - "REVIEWER": { - "rate": 20.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, - }, - "customer_rate": { - "rate": 25.0, - "billing_mode": "BY_HOUR", - "effective_since": "2024-01-01T00:00:00", - }, # Note: No project owner set, but we skip that validation } @@ -461,35 +351,6 @@ def test_create_alignerr_project_from_yaml_selective_validation(client: Client): os.unlink(yaml_file_path) -def test_create_alignerr_project_from_yaml_invalid_customer_rate( - client: Client, -): - """Test that invalid customer rate configurations raise appropriate errors.""" - config = { - "name": "TestProject", - "media_type": "IMAGE", - "customer_rate": { - "rate": 25.0, - # Missing billing_mode and effective_since - }, - } - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(config, f) - yaml_file_path = f.name - - try: - factory = AlignerrProjectFactory(client) - - with pytest.raises( - ValueError, - match="Required field 'billing_mode' is missing for customer_rate", - ): - factory.create(yaml_file_path, skip_validation=True) - finally: - os.unlink(yaml_file_path) - - def test_create_alignerr_project_from_yaml_invalid_tags(client: Client): """Test that invalid tag configurations raise appropriate errors.""" config = { diff --git a/libs/lbox-alignerr/tests/integration/test_project_rate.py b/libs/lbox-alignerr/tests/integration/test_project_rate.py deleted file mode 100644 index 8612c9f60..000000000 --- a/libs/lbox-alignerr/tests/integration/test_project_rate.py +++ /dev/null @@ -1,144 +0,0 @@ -"""Integration tests for ProjectRateV2 functionality.""" - -import datetime -import uuid - -import pytest -from alignerr.schema.project_rate import ( - BillingMode, - ProjectRateInput, - ProjectRateV2, -) -from labelbox.schema.media_type import MediaType - - -@pytest.fixture -def test_project(client): - """Create a test project for ProjectRateV2 testing.""" - project_name = f"Test ProjectRateV2 {uuid.uuid4()}" - project = client.create_project(name=project_name, media_type=MediaType.Image) - - yield project - - # Cleanup - try: - project.delete() - except Exception: - pass # Project may already be deleted - - -def test_project_rate_input_validation(): - """Test ProjectRateInput validation logic.""" - # Test negative rate validation - with pytest.raises(ValueError, match="Rate must be greater than or equal to 0"): - ProjectRateInput( - rateForId="", - isBillRate=True, - billingMode=BillingMode.BY_HOUR, - rate=-10.0, - effectiveSince=datetime.datetime.now().isoformat(), - ) - - # Test isBillRate=True with non-empty rateForId - with pytest.raises( - ValueError, - match="isBillRate indicates that this is a customer bill rate. rateForId must be empty if isBillRate is true", - ): - ProjectRateInput( - rateForId="some-id", - isBillRate=True, - billingMode=BillingMode.BY_HOUR, - rate=25.0, - effectiveSince=datetime.datetime.now().isoformat(), - ) - - -def test_get_by_project_id_no_rates(client, test_project): - """Test get_by_project_id when no rates are set.""" - rates = ProjectRateV2.get_by_project_id(client, test_project.uid) - assert rates == [] - - -def test_set_and_get_project_rate_customer(client, test_project): - """Test setting and getting a customer project rate.""" - # Create customer rate input - rate_input = ProjectRateInput( - rateForId="", # Empty string for customer rate - isBillRate=True, - billingMode=BillingMode.BY_HOUR, - rate=25.0, - effectiveSince=datetime.datetime.now().isoformat(), - ) - - # Set the project rate - result = ProjectRateV2.set_project_rate(client, test_project.uid, rate_input) - assert result is True - - # Get the project rates back - rates = ProjectRateV2.get_by_project_id(client, test_project.uid) - assert isinstance(rates, list) - assert len(rates) >= 1 - - # Find the customer rate - customer_rate = None - for rate in rates: - if rate.isBillRate: - customer_rate = rate - break - - assert customer_rate is not None - assert customer_rate.isBillRate is True - assert customer_rate.billingMode == BillingMode.BY_HOUR - assert customer_rate.rate == 25.0 - - -def test_multiple_project_rates(client, test_project): - """Test setting multiple project rates for the same project.""" - # Set customer rate - customer_rate_input = ProjectRateInput( - rateForId="", - isBillRate=True, - billingMode=BillingMode.BY_HOUR, - rate=30.0, - effectiveSince=datetime.datetime.now().isoformat(), - ) - - result1 = ProjectRateV2.set_project_rate( - client, test_project.uid, customer_rate_input - ) - assert result1 is True - - # Get available roles for role rate - roles = client.get_roles() - role_id = None - for role in roles.values(): - if role.name == "REVIEWER": - role_id = role.uid - break - - if role_id: - # Set role rate - role_rate_input = ProjectRateInput( - rateForId=role_id, - isBillRate=False, - billingMode=BillingMode.BY_TASK, - rate=1.25, - effectiveSince=datetime.datetime.now().isoformat(), - ) - - result2 = ProjectRateV2.set_project_rate( - client, test_project.uid, role_rate_input - ) - assert result2 is True - - # Get all project rates - rates = ProjectRateV2.get_by_project_id(client, test_project.uid) - assert isinstance(rates, list) - assert len(rates) >= 2 - - # Verify we have both customer and role rates - customer_rates = [r for r in rates if r.isBillRate] - role_rates = [r for r in rates if not r.isBillRate] - - assert len(customer_rates) >= 1 - assert len(role_rates) >= 1 From 9fd23abf1c446dc25230b3e2edffaf63a5a7a66d Mon Sep 17 00:00:00 2001 From: Pawel Kozik Date: Wed, 29 Jul 2026 16:08:29 +0200 Subject: [PATCH 2/2] [ALNR-6808] Fix ruff format on rate-removal tests --- .../tests/integration/test_alignerr_project.py | 8 ++------ .../tests/integration/test_alignerr_project_builder.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/libs/lbox-alignerr/tests/integration/test_alignerr_project.py b/libs/lbox-alignerr/tests/integration/test_alignerr_project.py index 70c71132d..2e828dd7c 100644 --- a/libs/lbox-alignerr/tests/integration/test_alignerr_project.py +++ b/libs/lbox-alignerr/tests/integration/test_alignerr_project.py @@ -77,12 +77,8 @@ def test_alignerr_project_domains(client, test_alignerr_project): def test_alignerr_project_rate_methods_removed(client, test_alignerr_project): """Pay By Role rate helpers raise and direct callers to the Rates UI.""" - with pytest.raises( - NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) - ): + with pytest.raises(NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG)): test_alignerr_project.get_project_rates() - with pytest.raises( - NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) - ): + with pytest.raises(NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG)): test_alignerr_project.set_project_rate(None) diff --git a/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py b/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py index 43cf91f1e..20127aa09 100644 --- a/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py +++ b/libs/lbox-alignerr/tests/integration/test_alignerr_project_builder.py @@ -55,14 +55,10 @@ def test_builder_rate_methods_removed(client: Client): .set_media_type(MediaType.Image) ) - with pytest.raises( - NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) - ): + with pytest.raises(NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG)): builder.set_alignerr_role_rate() - with pytest.raises( - NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG) - ): + with pytest.raises(NotImplementedError, match=re.escape(PAY_BY_ROLE_REMOVED_MSG)): builder.set_customer_rate()