Skip to content

Commit 55c37a8

Browse files
committed
Alignerr project creation
1 parent 2fab8c9 commit 55c37a8

File tree

15 files changed

+1157
-1
lines changed

15 files changed

+1157
-1
lines changed

libs/labelbox/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies = [
1212
"tqdm>=4.66.2",
1313
"geojson>=3.1.0",
1414
"lbox-clients==1.1.2",
15+
"PyYAML>=6.0",
1516
]
1617
readme = "README.md"
1718
requires-python = ">=3.9,<3.14"

libs/labelbox/src/labelbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
from labelbox.schema.ontology_kind import OntologyKind
7979
from labelbox.schema.organization import Organization
8080
from labelbox.schema.project import Project
81+
from labelbox.alignerr.schema.project_rate import ProjectRateV2 as ProjectRate
8182
from labelbox.schema.project_model_config import ProjectModelConfig
8283
from labelbox.schema.project_overview import (
8384
ProjectOverview,
@@ -98,7 +99,6 @@
9899
ResponseOption,
99100
PromptResponseClassification,
100101
)
101-
from lbox.exceptions import *
102102
from labelbox.schema.taskstatus import TaskStatus
103103
from labelbox.schema.api_key import ApiKey
104104
from labelbox.schema.timeunit import TimeUnit
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .alignerr_project import AlignerrWorkspace
2+
3+
__all__ = ['AlignerrWorkspace']
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
import datetime
2+
from enum import Enum
3+
from typing import TYPE_CHECKING, Optional
4+
import yaml
5+
from pathlib import Path
6+
7+
import logging
8+
9+
from labelbox.alignerr.schema.project_rate import BillingMode
10+
from labelbox.alignerr.schema.project_rate import ProjectRateInput
11+
from labelbox.alignerr.schema.project_rate import ProjectRateV2
12+
from labelbox.schema.media_type import MediaType
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
if TYPE_CHECKING:
18+
from labelbox import Client
19+
from labelbox.schema.project import Project
20+
21+
22+
class AlignerrRole(Enum):
23+
Labeler = "LABELER"
24+
Reviewer = "REVIEWER"
25+
Admin = "ADMIN"
26+
27+
28+
class AlignerrProject:
29+
def __init__(self, client: "Client", project: "Project"):
30+
self.client = client
31+
self.project = project
32+
33+
@property
34+
def project(self) -> Optional["Project"]:
35+
return self._project
36+
37+
@project.setter
38+
def project(self, project: "Project"):
39+
self._project = project
40+
41+
def get_project_rate(self) -> Optional["ProjectRateV2"]:
42+
query_str = """
43+
query GetAllProjectRatesPyApi($projectId: ID!) {
44+
project(where: { id: $projectId }) {
45+
id
46+
ratesV2 {
47+
id
48+
userRole {
49+
id
50+
name
51+
}
52+
isBillRate
53+
billingMode
54+
rate
55+
effectiveSince
56+
effectiveUntil
57+
createdAt
58+
updatedAt
59+
updatedBy {
60+
id
61+
email
62+
name
63+
}
64+
}
65+
}
66+
}
67+
"""
68+
result = self.client.execute(query_str, {"projectId": self.project.uid})
69+
rates_data = result["project"]["ratesV2"]
70+
71+
if not rates_data:
72+
return None
73+
74+
# Return the first rate as a ProjectRateV2 object
75+
return ProjectRateV2(self.client, rates_data[0])
76+
77+
def set_project_rate(self, project_rate_input):
78+
mutation_str = """mutation SetProjectRateV2PyApi($input: SetProjectRateV2Input!) {
79+
setProjectRateV2(input: $input) {
80+
success
81+
}
82+
}"""
83+
84+
params = {
85+
"projectId": self.project.uid,
86+
"input": {
87+
"projectId": self.project.uid,
88+
"userRoleId": project_rate_input.rateForId,
89+
"isBillRate": project_rate_input.isBillRate,
90+
"billingMode": project_rate_input.billingMode.value
91+
if hasattr(project_rate_input.billingMode, "value")
92+
else project_rate_input.billingMode,
93+
"rate": project_rate_input.rate,
94+
"effectiveSince": project_rate_input.effectiveSince,
95+
"effectiveUntil": project_rate_input.effectiveUntil,
96+
},
97+
}
98+
99+
result = self.client.execute(mutation_str, params)
100+
101+
return result["setProjectRateV2"]["success"]
102+
103+
104+
class AlignerrProjectBuilder:
105+
def __init__(self, client: "Client"):
106+
self.client = client
107+
self._alignerr_rates: dict[str, ProjectRateInput] = {}
108+
self._customer_rate: ProjectRateInput = None
109+
self.role_name_to_id = self._get_role_name_to_id()
110+
111+
def set_name(self, name: str):
112+
self.project_name = name
113+
return self
114+
115+
def set_media_type(self, media_type: "MediaType"):
116+
self.project_media_type = media_type
117+
return self
118+
119+
def set_alignerr_role_rate(
120+
self,
121+
*,
122+
role_name: AlignerrRole,
123+
rate: float,
124+
billing_mode: BillingMode,
125+
effective_since: datetime.datetime,
126+
effective_until: Optional[datetime.datetime] = None,
127+
):
128+
if role_name.value not in self.role_name_to_id:
129+
raise ValueError(f"Role {role_name} not found")
130+
131+
role_id = self.role_name_to_id[role_name.value]
132+
role_name = role_name.value
133+
134+
# Convert datetime objects to ISO format strings
135+
effective_since_str = effective_since.isoformat() if isinstance(effective_since, datetime.datetime) else effective_since
136+
effective_until_str = effective_until.isoformat() if isinstance(effective_until, datetime.datetime) else effective_until
137+
138+
self._alignerr_rates[role_name] = ProjectRateInput(
139+
rateForId=role_id,
140+
isBillRate=False,
141+
billingMode=billing_mode,
142+
rate=rate,
143+
effectiveSince=effective_since_str,
144+
effectiveUntil=effective_until_str,
145+
)
146+
return self
147+
148+
def set_customer_rate(
149+
self,
150+
*,
151+
rate: float,
152+
billing_mode: BillingMode,
153+
effective_since: datetime.datetime,
154+
effective_until: Optional[datetime.datetime] = None,
155+
):
156+
# Convert datetime objects to ISO format strings
157+
effective_since_str = effective_since.isoformat() if isinstance(effective_since, datetime.datetime) else effective_since
158+
effective_until_str = effective_until.isoformat() if isinstance(effective_until, datetime.datetime) else effective_until
159+
160+
self._customer_rate = ProjectRateInput(
161+
rateForId="", # Empty string for customer rate
162+
isBillRate=True,
163+
billingMode=billing_mode,
164+
rate=rate,
165+
effectiveSince=effective_since_str,
166+
effectiveUntil=effective_until_str,
167+
)
168+
return self
169+
170+
def create(self, skip_validation: bool = False):
171+
if not skip_validation:
172+
self._validate()
173+
logger.info("Creating project")
174+
175+
project_data = {
176+
"name": self.project_name,
177+
"media_type": self.project_media_type,
178+
}
179+
labelbox_project = self.client.create_project(**project_data)
180+
alignerr_project = AlignerrProject(self.client, labelbox_project)
181+
for alignerr_role, project_rate in self._alignerr_rates.items():
182+
logger.info(f"Setting project rate for {alignerr_role}")
183+
alignerr_project.set_project_rate(project_rate)
184+
return alignerr_project
185+
186+
def _validate_alignerr_rates(self):
187+
required_role_rates = set([AlignerrRole.Labeler.value, AlignerrRole.Reviewer.value])
188+
189+
for role_name in self._alignerr_rates.keys():
190+
required_role_rates.remove(role_name)
191+
if len(required_role_rates) > 0:
192+
raise ValueError(
193+
f"Required role rates are not set: {required_role_rates}"
194+
)
195+
196+
def _validate_customer_rate(self):
197+
if self._customer_rate is None:
198+
raise ValueError("Customer rate is not set")
199+
200+
def _validate(self):
201+
self._validate_alignerr_rates()
202+
self._validate_customer_rate()
203+
204+
def _get_role_name_to_id(self) -> dict[str, str]:
205+
roles = self.client.get_roles()
206+
return {role.name: role.uid for role in roles.values()}
207+
208+
209+
class AlignerrProjectFactory:
210+
def __init__(self, client: "Client"):
211+
self.client = client
212+
213+
def create(self, yaml_file_path: str, skip_validation: bool = False):
214+
"""
215+
Create an AlignerrProject from a YAML configuration file.
216+
217+
Args:
218+
yaml_file_path: Path to the YAML configuration file
219+
skip_validation: Whether to skip validation of required fields
220+
221+
Returns:
222+
AlignerrProject: The created project with configured rates
223+
224+
Raises:
225+
FileNotFoundError: If the YAML file doesn't exist
226+
yaml.YAMLError: If the YAML file is invalid
227+
ValueError: If required fields are missing or invalid
228+
"""
229+
logger.info(f"Creating project from YAML file: {yaml_file_path}")
230+
231+
# Load and parse YAML file
232+
yaml_path = Path(yaml_file_path)
233+
if not yaml_path.exists():
234+
raise FileNotFoundError(f"YAML file not found: {yaml_file_path}")
235+
236+
try:
237+
with open(yaml_path, 'r') as file:
238+
config = yaml.safe_load(file)
239+
except yaml.YAMLError as e:
240+
raise yaml.YAMLError(f"Invalid YAML file: {e}")
241+
242+
# Validate required fields
243+
if not config:
244+
raise ValueError("YAML file is empty")
245+
246+
required_fields = ['name', 'media_type']
247+
for field in required_fields:
248+
if field not in config:
249+
raise ValueError(f"Required field '{field}' is missing from YAML configuration")
250+
251+
# Create project builder
252+
builder = AlignerrProjectBuilder(self.client)
253+
254+
# Set basic project properties
255+
builder.set_name(config['name'])
256+
257+
# Set media type
258+
media_type_str = config['media_type']
259+
media_type = MediaType(media_type_str)
260+
261+
# Check if the media type is supported
262+
if not MediaType.is_supported(media_type):
263+
supported_members = MediaType.get_supported_members()
264+
raise ValueError(f"Invalid media_type '{media_type_str}'. Must be one of: {supported_members}")
265+
266+
builder.set_media_type(media_type)
267+
268+
# Set project rates if provided
269+
if 'rates' in config:
270+
rates_config = config['rates']
271+
if not isinstance(rates_config, dict):
272+
raise ValueError("'rates' must be a dictionary")
273+
274+
for role_name, rate_config in rates_config.items():
275+
try:
276+
alignerr_role = AlignerrRole(role_name.upper())
277+
except ValueError:
278+
raise ValueError(f"Invalid role '{role_name}'. Must be one of: {[r.value for r in AlignerrRole]}")
279+
280+
# Validate rate configuration
281+
required_rate_fields = ['rate', 'billing_mode', 'effective_since']
282+
for field in required_rate_fields:
283+
if field not in rate_config:
284+
raise ValueError(f"Required field '{field}' is missing for role '{role_name}'")
285+
286+
# Parse billing mode
287+
try:
288+
billing_mode = BillingMode(rate_config['billing_mode'])
289+
except ValueError:
290+
raise ValueError(f"Invalid billing_mode '{rate_config['billing_mode']}' for role '{role_name}'. Must be one of: {[e.value for e in BillingMode]}")
291+
292+
# Parse effective dates
293+
try:
294+
effective_since = datetime.datetime.fromisoformat(rate_config['effective_since'])
295+
except ValueError:
296+
raise ValueError(f"Invalid effective_since date format for role '{role_name}'. Use ISO format (YYYY-MM-DDTHH:MM:SS)")
297+
298+
effective_until = None
299+
if 'effective_until' in rate_config and rate_config['effective_until']:
300+
try:
301+
effective_until = datetime.datetime.fromisoformat(rate_config['effective_until'])
302+
except ValueError:
303+
raise ValueError(f"Invalid effective_until date format for role '{role_name}'. Use ISO format (YYYY-MM-DDTHH:MM:SS)")
304+
305+
# Set the rate
306+
builder.set_alignerr_role_rate(
307+
role_name=alignerr_role,
308+
rate=float(rate_config['rate']),
309+
billing_mode=billing_mode,
310+
effective_since=effective_since,
311+
effective_until=effective_until
312+
)
313+
314+
# Create the project
315+
return builder.create(skip_validation=skip_validation)
316+
317+
318+
class AlignerrWorkspace:
319+
def __init__(self, client: "Client"):
320+
self.client = client
321+
322+
def project_builder(self):
323+
return AlignerrProjectBuilder(self.client)
324+
325+
def project_prototype(self):
326+
return AlignerrProjectFactory(self.client)
327+
328+

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

Whitespace-only changes.

0 commit comments

Comments
 (0)