Skip to content

Commit dc923db

Browse files
committed
Add ProjectRateV2 and tests
1 parent 2fab8c9 commit dc923db

3 files changed

Lines changed: 267 additions & 0 deletions

File tree

libs/labelbox/src/labelbox/alignerr/schema/__init__.py

Whitespace-only changes.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
from enum import Enum
2+
from typing import Optional
3+
from labelbox.orm.db_object import DbObject, Deletable
4+
from labelbox.orm.model import Relationship, Field
5+
from pydantic import BaseModel, model_validator
6+
7+
8+
class BillingMode(Enum):
9+
BY_TASK = "BY_TASK"
10+
BY_HOUR = "BY_HOUR"
11+
BY_TASK_PER_TURN = "BY_TASK_PER_TURN"
12+
BY_ACCEPTED_TASK = "BY_ACCEPTED_TASK"
13+
14+
15+
class ProjectRateInput(BaseModel):
16+
rateForId: str
17+
isBillRate: bool
18+
billingMode: BillingMode
19+
rate: float
20+
effectiveSince: str # DateTime as string
21+
effectiveUntil: Optional[str] = None # Optional DateTime as string
22+
23+
@model_validator(mode="after")
24+
def validate_fields(self):
25+
if self.rate < 0:
26+
raise ValueError("Rate must be greater than or equal to 0")
27+
28+
if self.isBillRate and self.rateForId != "":
29+
raise ValueError(
30+
"isBillRate indicates that this is a customer bill rate. rateForId must be empty if isBillRate is true"
31+
)
32+
33+
if not self.isBillRate and self.rateForId == "":
34+
raise ValueError(
35+
"rateForId must be set to the id of the Alignerr Role"
36+
)
37+
38+
return self
39+
40+
41+
class ProjectRateV2(DbObject, Deletable):
42+
# Relationships
43+
userRole = Relationship.ToOne("UserRole", False)
44+
updatedBy = Relationship.ToOne("User", False)
45+
46+
# Fields matching the GraphQL schema
47+
isBillRate = Field.Boolean("isBillRate")
48+
billingMode = Field.Enum(BillingMode, "billingMode")
49+
rate = Field.Float("rate")
50+
createdAt = Field.DateTime("createdAt")
51+
updatedAt = Field.DateTime("updatedAt")
52+
effectiveSince = Field.DateTime("effectiveSince")
53+
effectiveUntil = Field.DateTime("effectiveUntil")
54+
55+
@classmethod
56+
def get_by_project_id(cls, client, project_id: str) -> list["ProjectRateV2"]:
57+
query_str = """
58+
query GetAllProjectRatesPyApi($projectId: ID!) {
59+
project(where: { id: $projectId }) {
60+
id
61+
ratesV2 {
62+
id
63+
userRole {
64+
id
65+
name
66+
}
67+
isBillRate
68+
billingMode
69+
rate
70+
effectiveSince
71+
effectiveUntil
72+
createdAt
73+
updatedAt
74+
updatedBy {
75+
id
76+
email
77+
name
78+
}
79+
}
80+
}
81+
}
82+
"""
83+
result = client.execute(query_str, {"projectId": project_id})
84+
rates_data = result["project"]["ratesV2"]
85+
86+
if not rates_data:
87+
return []
88+
89+
# Return all rates as ProjectRateV2 objects
90+
return [cls(client, rate_data) for rate_data in rates_data]
91+
92+
@classmethod
93+
def set_project_rate(
94+
cls, client, project_id: str, project_rate_input: ProjectRateInput
95+
):
96+
mutation_str = """mutation SetProjectRateV2PyApi($input: SetProjectRateV2Input!) {
97+
setProjectRateV2(input: $input) {
98+
success
99+
}
100+
}"""
101+
102+
params = {
103+
"projectId": project_id,
104+
"input": {
105+
"projectId": project_id,
106+
"userRoleId": project_rate_input.rateForId,
107+
"isBillRate": project_rate_input.isBillRate,
108+
"billingMode": project_rate_input.billingMode.value
109+
if hasattr(project_rate_input.billingMode, "value")
110+
else project_rate_input.billingMode,
111+
"rate": project_rate_input.rate,
112+
"effectiveSince": project_rate_input.effectiveSince,
113+
"effectiveUntil": project_rate_input.effectiveUntil,
114+
},
115+
}
116+
117+
result = client.execute(mutation_str, params)
118+
119+
return result["setProjectRateV2"]["success"]
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""Integration tests for ProjectRateV2 functionality."""
2+
3+
import datetime
4+
import uuid
5+
6+
import pytest
7+
from labelbox.alignerr.schema.project_rate import (
8+
BillingMode,
9+
ProjectRateInput,
10+
ProjectRateV2,
11+
)
12+
from labelbox.schema.media_type import MediaType
13+
14+
15+
@pytest.fixture
16+
def test_project(client):
17+
"""Create a test project for ProjectRateV2 testing."""
18+
project_name = f"Test ProjectRateV2 {uuid.uuid4()}"
19+
project = client.create_project(
20+
name=project_name, media_type=MediaType.Image
21+
)
22+
23+
yield project
24+
25+
# Cleanup
26+
try:
27+
project.delete()
28+
except Exception:
29+
pass # Project may already be deleted
30+
31+
32+
def test_project_rate_input_validation():
33+
"""Test ProjectRateInput validation logic."""
34+
# Test negative rate validation
35+
with pytest.raises(ValueError, match="Rate must be greater than or equal to 0"):
36+
ProjectRateInput(
37+
rateForId="",
38+
isBillRate=True,
39+
billingMode=BillingMode.BY_HOUR,
40+
rate=-10.0,
41+
effectiveSince=datetime.datetime.now().isoformat(),
42+
)
43+
44+
# Test isBillRate=True with non-empty rateForId
45+
with pytest.raises(
46+
ValueError,
47+
match="isBillRate indicates that this is a customer bill rate. rateForId must be empty if isBillRate is true"
48+
):
49+
ProjectRateInput(
50+
rateForId="some-id",
51+
isBillRate=True,
52+
billingMode=BillingMode.BY_HOUR,
53+
rate=25.0,
54+
effectiveSince=datetime.datetime.now().isoformat(),
55+
)
56+
57+
58+
def test_get_by_project_id_no_rates(client, test_project):
59+
"""Test get_by_project_id when no rates are set."""
60+
rates = ProjectRateV2.get_by_project_id(client, test_project.uid)
61+
assert rates == []
62+
63+
64+
def test_set_and_get_project_rate_customer(client, test_project):
65+
"""Test setting and getting a customer project rate."""
66+
# Create customer rate input
67+
rate_input = ProjectRateInput(
68+
rateForId="", # Empty string for customer rate
69+
isBillRate=True,
70+
billingMode=BillingMode.BY_HOUR,
71+
rate=25.0,
72+
effectiveSince=datetime.datetime.now().isoformat(),
73+
)
74+
75+
# Set the project rate
76+
result = ProjectRateV2.set_project_rate(
77+
client, test_project.uid, rate_input
78+
)
79+
assert result is True
80+
81+
# Get the project rates back
82+
rates = ProjectRateV2.get_by_project_id(client, test_project.uid)
83+
assert isinstance(rates, list)
84+
assert len(rates) >= 1
85+
86+
# Find the customer rate
87+
customer_rate = None
88+
for rate in rates:
89+
if rate.isBillRate:
90+
customer_rate = rate
91+
break
92+
93+
assert customer_rate is not None
94+
assert customer_rate.isBillRate is True
95+
assert customer_rate.billingMode == BillingMode.BY_HOUR
96+
assert customer_rate.rate == 25.0
97+
98+
99+
def test_multiple_project_rates(client, test_project):
100+
"""Test setting multiple project rates for the same project."""
101+
# Set customer rate
102+
customer_rate_input = ProjectRateInput(
103+
rateForId="",
104+
isBillRate=True,
105+
billingMode=BillingMode.BY_HOUR,
106+
rate=30.0,
107+
effectiveSince=datetime.datetime.now().isoformat(),
108+
)
109+
110+
result1 = ProjectRateV2.set_project_rate(
111+
client, test_project.uid, customer_rate_input
112+
)
113+
assert result1 is True
114+
115+
# Get available roles for role rate
116+
roles = client.get_roles()
117+
role_id = None
118+
for role in roles.values():
119+
if role.name == "REVIEWER":
120+
role_id = role.uid
121+
break
122+
123+
if role_id:
124+
# Set role rate
125+
role_rate_input = ProjectRateInput(
126+
rateForId=role_id,
127+
isBillRate=False,
128+
billingMode=BillingMode.BY_TASK,
129+
rate=1.25,
130+
effectiveSince=datetime.datetime.now().isoformat(),
131+
)
132+
133+
result2 = ProjectRateV2.set_project_rate(
134+
client, test_project.uid, role_rate_input
135+
)
136+
assert result2 is True
137+
138+
# Get all project rates
139+
rates = ProjectRateV2.get_by_project_id(client, test_project.uid)
140+
assert isinstance(rates, list)
141+
assert len(rates) >= 2
142+
143+
# Verify we have both customer and role rates
144+
customer_rates = [r for r in rates if r.isBillRate]
145+
role_rates = [r for r in rates if not r.isBillRate]
146+
147+
assert len(customer_rates) >= 1
148+
assert len(role_rates) >= 1

0 commit comments

Comments
 (0)