Skip to content

Commit 4434c1d

Browse files
authored
[PTDT-3002] SDK updates for PDF connections (#1951)
2 parents 156eda2 + b261357 commit 4434c1d

File tree

5 files changed

+242
-5
lines changed

5 files changed

+242
-5
lines changed

libs/labelbox/src/labelbox/data/annotation_types/relationship.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ class Type(Enum):
1414
UNIDIRECTIONAL = "unidirectional"
1515
BIDIRECTIONAL = "bidirectional"
1616

17-
source: Union[ObjectAnnotation, ClassificationAnnotation]
17+
source: Optional[Union[ObjectAnnotation, ClassificationAnnotation]] = None
18+
source_ontology_name: Optional[str] = None
1819
target: ObjectAnnotation
1920
type: Type = Type.UNIDIRECTIONAL
2021
readonly: Optional[bool] = None
@@ -27,6 +28,22 @@ def check_readonly(self):
2728
)
2829
return self
2930

31+
@model_validator(mode="after")
32+
def validate_source_fields(self):
33+
if self.source is None and self.source_ontology_name is None:
34+
raise ValueError(
35+
"Either source or source_ontology_name must be provided"
36+
)
37+
return self
38+
39+
@model_validator(mode="after")
40+
def validate_source_consistency(self):
41+
if self.source is not None and self.source_ontology_name is not None:
42+
raise ValueError(
43+
"Only one of 'source' or 'source_ontology_name' may be provided"
44+
)
45+
return self
46+
3047

3148
class RelationshipAnnotation(BaseAnnotation):
3249
value: Relationship

libs/labelbox/src/labelbox/data/serialization/ndjson/label.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ def _create_relationship_annotations(
209209
- For other target annotations: source must be ObjectAnnotation
210210
- Target:
211211
- Target must always be ObjectAnnotation
212+
ValueError: If relationship validation fails:
213+
- For PDF target annotations: either source or source_ontology_name must be provided
212214
"""
213215
for annotation in label.annotations:
214216
if isinstance(annotation, RelationshipAnnotation):
@@ -221,13 +223,20 @@ def _create_relationship_annotations(
221223
if isinstance(
222224
target.value, (DocumentRectangle, DocumentEntity)
223225
):
224-
if not isinstance(
226+
if source is not None and not isinstance(
225227
source, (ObjectAnnotation, ClassificationAnnotation)
226228
):
227229
raise TypeError(
228230
f"Unable to create relationship with invalid source. For PDF targets, "
229231
f"source must be ObjectAnnotation or ClassificationAnnotation. Got: {type(source)}"
230232
)
233+
if (
234+
source is None
235+
and annotation.value.source_ontology_name is None
236+
):
237+
raise ValueError(
238+
"Unable to create relationship - either source or source_ontology_name must be provided"
239+
)
231240
elif not isinstance(source, ObjectAnnotation):
232241
raise TypeError(
233242
f"Unable to create relationship with non ObjectAnnotation source: {type(source)}"
@@ -239,7 +248,7 @@ def _create_relationship_annotations(
239248
f"Unable to create relationship with non ObjectAnnotation target: {type(target)}"
240249
)
241250

242-
if not source._uuid:
251+
if source is not None and not source._uuid:
243252
source._uuid = uuid1
244253
if not target._uuid:
245254
target._uuid = uuid2

libs/labelbox/src/labelbox/data/serialization/ndjson/relationship.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111

1212

1313
class _Relationship(BaseModel):
14-
source: str
14+
source: Optional[str] = None
1515
target: str
1616
type: str
1717
readonly: Optional[bool] = None
18+
sourceOntologyName: Optional[str] = None
1819

1920

2021
class NDRelationship(NDAnnotation):
@@ -25,11 +26,13 @@ def to_common(
2526
annotation: "NDRelationship",
2627
source: SUPPORTED_ANNOTATIONS,
2728
target: SUPPORTED_ANNOTATIONS,
29+
source_ontology_name: Optional[str] = None,
2830
) -> RelationshipAnnotation:
2931
return RelationshipAnnotation(
3032
name=annotation.name,
3133
value=Relationship(
3234
source=source,
35+
source_ontology_name=source_ontology_name,
3336
target=target,
3437
type=Relationship.Type(annotation.relationship.type),
3538
readonly=annotation.relationship.readonly,
@@ -50,8 +53,11 @@ def from_common(
5053
name=annotation.name,
5154
dataRow=DataRow(id=data.uid, global_key=data.global_key),
5255
relationship=_Relationship(
53-
source=str(relationship.source._uuid),
56+
source=str(relationship.source._uuid)
57+
if relationship.source
58+
else None,
5459
target=str(relationship.target._uuid),
60+
sourceOntologyName=relationship.source_ontology_name,
5561
type=relationship.type.value,
5662
readonly=relationship.readonly,
5763
),

libs/labelbox/tests/data/annotation_import/test_relationships.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,3 +409,68 @@ def test_relationship_readonly_explicit_true():
409409
),
410410
)
411411
assert relationship.value.readonly is True
412+
413+
414+
def test_relationship_source_ontology_name():
415+
"""Test that relationship can be created with source_ontology_name instead of source."""
416+
target = ObjectAnnotation(
417+
name="e2",
418+
value=TextEntity(start=30, end=35),
419+
)
420+
421+
relationship = RelationshipAnnotation(
422+
name="rel",
423+
value=Relationship(
424+
source_ontology_name="test_source",
425+
target=target,
426+
type=Relationship.Type.UNIDIRECTIONAL,
427+
),
428+
)
429+
assert relationship.value.source_ontology_name == "test_source"
430+
assert relationship.value.source is None
431+
432+
433+
def test_relationship_missing_source_validation():
434+
"""Test that relationship requires either source or source_ontology_name."""
435+
target = ObjectAnnotation(
436+
name="e2",
437+
value=TextEntity(start=30, end=35),
438+
)
439+
440+
with pytest.raises(
441+
ValueError,
442+
match="Either source or source_ontology_name must be provided",
443+
):
444+
RelationshipAnnotation(
445+
name="rel",
446+
value=Relationship(
447+
target=target,
448+
type=Relationship.Type.UNIDIRECTIONAL,
449+
),
450+
)
451+
452+
453+
def test_relationship_both_sources_validation():
454+
"""Test that relationship cannot have both source and source_ontology_name."""
455+
source = ObjectAnnotation(
456+
name="e1",
457+
value=TextEntity(start=10, end=12),
458+
)
459+
target = ObjectAnnotation(
460+
name="e2",
461+
value=TextEntity(start=30, end=35),
462+
)
463+
464+
with pytest.raises(
465+
ValueError,
466+
match="Only one of 'source' or 'source_ontology_name' may be provided",
467+
):
468+
RelationshipAnnotation(
469+
name="rel",
470+
value=Relationship(
471+
source=source,
472+
source_ontology_name="test_source",
473+
target=target,
474+
type=Relationship.Type.UNIDIRECTIONAL,
475+
),
476+
)

libs/labelbox/tests/data/serialization/ndjson/test_relationship.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
RelationshipAnnotation,
66
Relationship,
77
TextEntity,
8+
DocumentRectangle,
9+
RectangleUnit,
10+
Point,
811
)
912

1013

@@ -285,3 +288,140 @@ def test_readonly_relationships():
285288
non_readonly_rel_serialized["relationship"]["type"] == "bidirectional"
286289
)
287290
assert non_readonly_rel_serialized["relationship"]["readonly"] is False
291+
292+
293+
def test_source_ontology_name_relationship():
294+
ner_source = ObjectAnnotation(
295+
name="e1",
296+
value=TextEntity(start=10, end=12),
297+
)
298+
299+
ner_target = ObjectAnnotation(
300+
name="e2",
301+
value=TextEntity(start=30, end=35),
302+
)
303+
304+
# Test relationship with source
305+
regular_relationship = RelationshipAnnotation(
306+
name="regular_rel",
307+
value=Relationship(
308+
source=ner_source,
309+
target=ner_target,
310+
type=Relationship.Type.UNIDIRECTIONAL,
311+
),
312+
)
313+
314+
# Test relationship with source_ontology_name for PDF target
315+
pdf_target = ObjectAnnotation(
316+
name="pdf_region",
317+
value=DocumentRectangle(
318+
start=Point(x=0.5, y=0.5),
319+
end=Point(x=0.7, y=0.7),
320+
page=1,
321+
unit=RectangleUnit.PERCENT,
322+
),
323+
)
324+
325+
ontology_relationship = RelationshipAnnotation(
326+
name="ontology_rel",
327+
value=Relationship(
328+
source_ontology_name="Person",
329+
target=pdf_target,
330+
type=Relationship.Type.UNIDIRECTIONAL,
331+
),
332+
)
333+
334+
label = Label(
335+
data={"uid": "clqbkpy236syk07978v3pscw1"},
336+
annotations=[
337+
ner_source,
338+
ner_target,
339+
pdf_target,
340+
regular_relationship,
341+
ontology_relationship,
342+
],
343+
)
344+
345+
serialized_label = list(NDJsonConverter.serialize([label]))
346+
347+
ner_source_serialized = next(
348+
annotation
349+
for annotation in serialized_label
350+
if annotation["name"] == ner_source.name
351+
)
352+
ner_target_serialized = next(
353+
annotation
354+
for annotation in serialized_label
355+
if annotation["name"] == ner_target.name
356+
)
357+
pdf_target_serialized = next(
358+
annotation
359+
for annotation in serialized_label
360+
if annotation["name"] == pdf_target.name
361+
)
362+
regular_rel_serialized = next(
363+
annotation
364+
for annotation in serialized_label
365+
if annotation["name"] == regular_relationship.name
366+
)
367+
ontology_rel_serialized = next(
368+
annotation
369+
for annotation in serialized_label
370+
if annotation["name"] == ontology_relationship.name
371+
)
372+
373+
# Verify regular relationship
374+
assert (
375+
regular_rel_serialized["relationship"]["source"]
376+
== ner_source_serialized["uuid"]
377+
)
378+
assert (
379+
regular_rel_serialized["relationship"]["target"]
380+
== ner_target_serialized["uuid"]
381+
)
382+
assert regular_rel_serialized["relationship"]["type"] == "unidirectional"
383+
384+
# Verify relationship with source_ontology_name
385+
assert (
386+
ontology_rel_serialized["relationship"]["sourceOntologyName"]
387+
== "Person"
388+
)
389+
assert (
390+
ontology_rel_serialized["relationship"]["target"]
391+
== pdf_target_serialized["uuid"]
392+
)
393+
assert ontology_rel_serialized["relationship"]["type"] == "unidirectional"
394+
395+
# Test that providing both source and source_ontology_name raises an error
396+
try:
397+
RelationshipAnnotation(
398+
name="invalid_rel",
399+
value=Relationship(
400+
source=ner_source,
401+
source_ontology_name="Person",
402+
target=pdf_target,
403+
type=Relationship.Type.UNIDIRECTIONAL,
404+
),
405+
)
406+
assert False, "Expected ValueError for providing both source and source_ontology_name"
407+
except Exception as e:
408+
assert (
409+
"Value error, Only one of 'source' or 'source_ontology_name' may be provided"
410+
in str(e)
411+
)
412+
413+
# Test that providing neither source nor source_ontology_name raises an error
414+
try:
415+
RelationshipAnnotation(
416+
name="invalid_rel",
417+
value=Relationship(
418+
target=pdf_target,
419+
type=Relationship.Type.UNIDIRECTIONAL,
420+
),
421+
)
422+
assert False, "Expected ValueError for providing neither source nor source_ontology_name"
423+
except Exception as e:
424+
assert (
425+
"Value error, Either source or source_ontology_name must be provided"
426+
in str(e)
427+
)

0 commit comments

Comments
 (0)