Skip to content

Commit a0c28ad

Browse files
authored
add lbox-alignerr workspace (#2018)
2 parents 8577500 + 7fcd96f commit a0c28ad

28 files changed

+4984
-6
lines changed

.github/workflows/lbox-develop.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ jobs:
4949
steps:
5050
- uses: actions/checkout@v4
5151
with:
52-
token: ${{ secrets.ACTIONS_ACCESS_TOKEN }}
5352
ref: ${{ github.head_ref }}
5453
- uses: ./.github/actions/python-package-shared-setup
5554
with:
@@ -163,4 +162,4 @@ jobs:
163162
- name: Build and push (Pull Request) Output
164163
if: github.event_name == 'pull_request'
165164
run: |
166-
echo "ghcr.io/labelbox/${{ matrix.package }}:${{ github.sha }}" >> "$GITHUB_STEP_SUMMARY"
165+
echo "ghcr.io/labelbox/${{ matrix.package }}:${{ github.sha }}" >> "$GITHUB_STEP_SUMMARY"

.github/workflows/lbox-publish.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ jobs:
104104
steps:
105105
- uses: actions/checkout@v4
106106
with:
107-
token: ${{ secrets.ACTIONS_ACCESS_TOKEN }}
108-
# ref: ${{ inputs.tag }}
109107
ref: ${{ inputs.tag }}
110108
- uses: ./.github/actions/python-package-shared-setup
111109
with:
@@ -190,4 +188,4 @@ jobs:
190188
id: image
191189
run: |
192190
echo "ghcr.io/labelbox/${{ matrix.package }}:latest" >> "$GITHUB_STEP_SUMMARY"
193-
echo "ghcr.io/labelbox/${{ matrix.package }}:${{ inputs.tag }}" >> "$GITHUB_STEP_SUMMARY"
191+
echo "ghcr.io/labelbox/${{ matrix.package }}:${{ inputs.tag }}" >> "$GITHUB_STEP_SUMMARY"

libs/labelbox/pyproject.toml

Lines changed: 5 additions & 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"
@@ -55,6 +56,9 @@ data = [
5556
"typing-extensions>=4.10.0",
5657
"opencv-python-headless>=4.9.0.80",
5758
]
59+
alignerr = [
60+
"lbox-alignerr>=0.1.0",
61+
]
5862

5963
[build-system]
6064
requires = ["hatchling"]
@@ -68,6 +72,7 @@ dev-dependencies = [
6872
"types-python-dateutil>=2.9.0.20240316",
6973
"types-requests>=2.31.0.20240311",
7074
"types-tqdm>=4.66.0.20240106",
75+
"types-PyYAML>=6.0.12.20240311",
7176
]
7277

7378
[tool.ruff]

libs/labelbox/src/labelbox/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@
9898
ResponseOption,
9999
PromptResponseClassification,
100100
)
101-
from lbox.exceptions import *
102101
from labelbox.schema.taskstatus import TaskStatus
103102
from labelbox.schema.api_key import ApiKey
104103
from labelbox.schema.timeunit import TimeUnit

libs/labelbox/src/labelbox/schema/role.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def format_role(name: str):
2929
class Role(DbObject):
3030
name = Field.String("name")
3131

32+
@classmethod
33+
def from_name(cls, client: "Client", name: str) -> Optional["Role"]:
34+
roles = get_roles(client)
35+
return roles.get(name.upper())
36+
3237

3338
class OrgRole(Role): ...
3439

libs/lbox-alignerr/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Alignerr
2+
3+
Alignerr workspace management for Labelbox.
4+
5+
This package provides functionality for managing Alignerr projects, including:
6+
- Project creation and configuration
7+
- Rate management for labelers and reviewers
8+
- Domain and tag management
9+
- Workforce management

libs/lbox-alignerr/pyproject.toml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
[project]
2+
name = "lbox-alignerr"
3+
version = "0.1.0"
4+
description = "Alignerr workspace management for Labelbox"
5+
authors = [
6+
{ name = "Labelbox", email = "engineering@labelbox.com" }
7+
]
8+
dependencies = [
9+
"labelbox>=0.1.0",
10+
"pydantic>=2.0.0",
11+
"pyyaml>=6.0",
12+
]
13+
readme = "README.md"
14+
requires-python = ">= 3.9"
15+
16+
classifiers=[
17+
# How mature is this project?
18+
"Development Status :: 2 - Pre-Alpha",
19+
# Indicate who your project is intended for
20+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
21+
"Topic :: Software Development :: Libraries",
22+
"Intended Audience :: Developers",
23+
"Intended Audience :: Science/Research",
24+
# Pick your license as you wish
25+
"License :: OSI Approved :: Apache Software License",
26+
# Specify the Python versions you support here.
27+
"Programming Language :: Python :: 3",
28+
"Programming Language :: Python :: 3.9",
29+
"Programming Language :: Python :: 3.10",
30+
"Programming Language :: Python :: 3.11",
31+
"Programming Language :: Python :: 3.12",
32+
]
33+
keywords = ["ml", "ai", "labelbox", "labeling", "llm", "machinelearning", "alignerr", "lbox-alignerr"]
34+
35+
[project.urls]
36+
Homepage = "https://labelbox.com/"
37+
Documentation = "https://labelbox-python.readthedocs.io/en/latest/"
38+
Repository = "https://github.com/Labelbox/labelbox-python"
39+
Issues = "https://github.com/Labelbox/labelbox-python/issues"
40+
Changelog = "https://github.com/Labelbox/labelbox-python/blob/develop/libs/labelbox/CHANGELOG.md"
41+
42+
[build-system]
43+
requires = ["hatchling"]
44+
build-backend = "hatchling.build"
45+
46+
[tool.rye]
47+
managed = true
48+
dev-dependencies = [
49+
"pytest>=8.1.1",
50+
"pytest-cases>=3.8.4",
51+
"pytest-rerunfailures>=14.0",
52+
"pytest-snapshot>=0.9.0",
53+
"pytest-cov>=4.1.0",
54+
"pytest-xdist>=3.5.0",
55+
"faker>=25.5.0",
56+
"pytest-timestamper>=0.0.10",
57+
"pytest-timeout>=2.3.1",
58+
"pytest-order>=1.2.1",
59+
"pyjwt>=2.9.0",
60+
]
61+
62+
[tool.rye.scripts]
63+
unit = "pytest tests/unit"
64+
integration = "pytest tests/integration"
65+
66+
[tool.hatch.metadata]
67+
allow-direct-references = true
68+
69+
[tool.hatch.build.targets.wheel]
70+
packages = ["src/alignerr"]
71+
72+
[tool.pytest.ini_options]
73+
addopts = "-rP -vvv --durations=20 --cov=alignerr --import-mode=importlib"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from alignerr.alignerr_project import AlignerrWorkspace
2+
3+
__all__ = ["AlignerrWorkspace"]
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from enum import Enum
2+
from typing import TYPE_CHECKING, Optional
3+
4+
import logging
5+
6+
from alignerr.schema.project_rate import ProjectRateV2
7+
from alignerr.schema.project_domain import ProjectDomain
8+
from alignerr.schema.enchanced_resource_tags import (
9+
EnhancedResourceTag,
10+
ResourceTagType,
11+
)
12+
from alignerr.schema.project_boost_workforce import (
13+
ProjectBoostWorkforce,
14+
)
15+
from labelbox.pagination import PaginatedCollection
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
if TYPE_CHECKING:
21+
from labelbox import Client
22+
from labelbox.schema.project import Project
23+
from alignerr.schema.project_domain import ProjectDomain
24+
25+
26+
class AlignerrRole(Enum):
27+
Labeler = "LABELER"
28+
Reviewer = "REVIEWER"
29+
Admin = "ADMIN"
30+
ProjectCoordinator = "PROJECT_COORDINATOR"
31+
AlignerrLabeler = "ALIGNERR_LABELER"
32+
EndLabellingRole = "ENDLABELLINGROLE"
33+
34+
35+
class AlignerrProject:
36+
def __init__(self, client: "Client", project: "Project", _internal: bool = False):
37+
if not _internal:
38+
raise RuntimeError(
39+
"AlignerrProject cannot be initialized directly. "
40+
"Use AlignerrProjectBuilder or AlignerrProjectFactory to create instances."
41+
)
42+
self.client = client
43+
self.project = project
44+
45+
@property
46+
def project(self) -> "Project":
47+
return self._project
48+
49+
@project.setter
50+
def project(self, project: "Project"):
51+
self._project = project
52+
53+
def domains(self) -> PaginatedCollection:
54+
return ProjectDomain.get_by_project_id(
55+
client=self.client, project_id=self.project.uid
56+
)
57+
58+
def add_domain(self, project_domain: ProjectDomain):
59+
return ProjectDomain.connect_project_to_domains(
60+
client=self.client,
61+
project_id=self.project.uid,
62+
domain_ids=[project_domain.uid],
63+
)
64+
65+
def get_project_rates(self) -> list["ProjectRateV2"]:
66+
return ProjectRateV2.get_by_project_id(
67+
client=self.client, project_id=self.project.uid
68+
)
69+
70+
def set_project_rate(self, project_rate_input):
71+
return ProjectRateV2.set_project_rate(
72+
client=self.client,
73+
project_id=self.project.uid,
74+
project_rate_input=project_rate_input,
75+
)
76+
77+
def set_tags(self, tag_names: list[str], tag_type: ResourceTagType):
78+
# Convert tag names to tag IDs
79+
tag_ids = []
80+
for tag_name in tag_names:
81+
# Search for the tag by text to get its ID
82+
found_tags = EnhancedResourceTag.search_by_text(
83+
self.client, search_text=tag_name, tag_type=tag_type
84+
)
85+
if found_tags:
86+
tag_ids.append(found_tags[0].id)
87+
88+
# Use the existing project resource tag functionality with IDs
89+
self.project.update_project_resource_tags(tag_ids)
90+
return self
91+
92+
def get_tags(self) -> list[EnhancedResourceTag]:
93+
"""Get enhanced resource tags associated with this project.
94+
95+
Returns:
96+
List of EnhancedResourceTag instances
97+
"""
98+
# Get project resource tags and convert to EnhancedResourceTag instances
99+
project_resource_tags = self.project.get_resource_tags()
100+
enhanced_tags = []
101+
for tag in project_resource_tags:
102+
# Search for the corresponding EnhancedResourceTag by text (try different types)
103+
found_tags = []
104+
for tag_type in [ResourceTagType.Default, ResourceTagType.Billing]:
105+
found_tags = EnhancedResourceTag.search_by_text(
106+
self.client, search_text=tag.text, tag_type=tag_type
107+
)
108+
if found_tags:
109+
break
110+
if found_tags:
111+
enhanced_tags.extend(found_tags)
112+
return enhanced_tags
113+
114+
def add_tag(self, tag: EnhancedResourceTag):
115+
"""Add a single enhanced resource tag to the project.
116+
117+
Args:
118+
tag: EnhancedResourceTag instance to add
119+
120+
Returns:
121+
Self for method chaining
122+
"""
123+
current_tags = self.get_tags()
124+
current_tag_names = [t.text for t in current_tags]
125+
126+
if tag.text not in current_tag_names:
127+
current_tag_names.append(tag.text)
128+
self.set_tags(current_tag_names, tag.type)
129+
130+
return self
131+
132+
def remove_tag(self, tag: EnhancedResourceTag):
133+
"""Remove a single enhanced resource tag from the project.
134+
135+
Args:
136+
tag: EnhancedResourceTag instance to remove
137+
138+
Returns:
139+
Self for method chaining
140+
"""
141+
current_tags = self.get_tags()
142+
current_tag_names = [t.text for t in current_tags if t.uid != tag.uid]
143+
self.set_tags(current_tag_names, tag.type)
144+
return self
145+
146+
def get_project_owner(self) -> Optional[ProjectBoostWorkforce]:
147+
"""Get the ProjectBoostWorkforce for this project.
148+
149+
Returns:
150+
ProjectBoostWorkforce instance or None if not found
151+
"""
152+
return ProjectBoostWorkforce.get_by_project_id(
153+
client=self.client, project_id=self.project.uid
154+
)
155+
156+
157+
class AlignerrWorkspace:
158+
def __init__(self, client: "Client"):
159+
self.client = client
160+
161+
def project_builder(self):
162+
from alignerr.alignerr_project_builder import (
163+
AlignerrProjectBuilder,
164+
)
165+
166+
return AlignerrProjectBuilder(self.client)
167+
168+
def project_prototype(self):
169+
from alignerr.alignerr_project_factory import (
170+
AlignerrProjectFactory,
171+
)
172+
173+
return AlignerrProjectFactory(self.client)
174+
175+
@classmethod
176+
def from_labelbox(cls, client: "Client") -> "AlignerrWorkspace":
177+
return cls(client)

0 commit comments

Comments
 (0)