Repository navigation
Bug: cannot query metadata with unsupported annotations #653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
511801c
e4ace64
fd10d7e
4db51a9
e8a9cd8
d48ffc1
5f2dd76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| from dagshub.common.rich_util import get_rich_progress | ||
| from dagshub.common.util import lazy_load, multi_urljoin | ||
| from dagshub.data_engine.annotation import MetadataAnnotations | ||
| from dagshub.data_engine.annotation.metadata import UnsupportedMetadataAnnotations | ||
| from dagshub.data_engine.annotation.voxel_conversion import ( | ||
| add_ls_annotations, | ||
| add_voxel_annotations, | ||
|
|
@@ -425,8 +426,11 @@ def _get_blob_fn(dp: Datapoint, field: str, url: str, blob_path: Path): | |
| for dp in self: | ||
| for fld in document_fields: | ||
| if fld in dp.metadata: | ||
| # Override the load_into_memory flag, because we need the contents | ||
| if not load_into_memory: | ||
| # Defensive check to not mangle annotation fields by accident | ||
| if isinstance(dp.metadata[fld], MetadataAnnotations): | ||
| continue | ||
| # Force load the content into memory, even if load_into_memory was set to False | ||
| if not load_into_memory or isinstance(dp.metadata[fld], Path): | ||
| dp.metadata[fld] = Path(dp.metadata[fld]).read_bytes() | ||
| dp.metadata[fld] = dp.metadata[fld].decode("utf-8") | ||
|
Comment on lines
+429
to
435
|
||
|
|
||
|
|
@@ -435,38 +439,49 @@ def _get_blob_fn(dp: Datapoint, field: str, url: str, blob_path: Path): | |
| def _convert_annotation_fields(self, *fields, load_into_memory): | ||
| # Convert any downloaded annotation column | ||
| annotation_fields = [f for f in fields if f in self.annotation_fields] | ||
| if not annotation_fields: | ||
| return | ||
|
|
||
| # List of datapoints with annotations that couldn't be parsed | ||
| bad_annotations = defaultdict(list) | ||
|
|
||
| if annotation_fields: | ||
| # Convert them | ||
| for dp in self: | ||
| for fld in annotation_fields: | ||
| if fld in dp.metadata: | ||
| # Already loaded - skip | ||
| if isinstance(dp.metadata[fld], MetadataAnnotations): | ||
| continue | ||
| # Override the load_into_memory flag, because we need the contents | ||
| if not load_into_memory: | ||
| dp.metadata[fld] = Path(dp.metadata[fld]).read_bytes() | ||
| try: | ||
| dp.metadata[fld] = MetadataAnnotations.from_ls_task( | ||
| datapoint=dp, field=fld, ls_task=dp.metadata[fld] | ||
| ) | ||
| except ValidationError: | ||
| bad_annotations[fld].append(dp.path) | ||
| else: | ||
| dp.metadata[fld] = MetadataAnnotations(datapoint=dp, field=fld) | ||
| for dp in self: | ||
| for fld in annotation_fields: | ||
| metadata_value = dp.metadata.get(fld) | ||
| # No value - create ampty annotation container | ||
| if metadata_value is None: | ||
| dp.metadata[fld] = MetadataAnnotations(datapoint=dp, field=fld) | ||
| continue | ||
| # Already loaded - skip | ||
| elif isinstance(metadata_value, MetadataAnnotations): | ||
| continue | ||
| # Parse annotation from the content of the field | ||
| else: | ||
| # Force load the content into memory, even if load_into_memory was set to False | ||
| if not load_into_memory or isinstance(dp.metadata[fld], Path): | ||
| metadata_value = Path(metadata_value).read_bytes() | ||
| try: | ||
| dp.metadata[fld] = MetadataAnnotations.from_ls_task( | ||
| datapoint=dp, field=fld, ls_task=metadata_value | ||
| ) | ||
| except ValidationError: | ||
| dp.metadata[fld] = UnsupportedMetadataAnnotations( | ||
| datapoint=dp, field=fld, original_value=metadata_value | ||
| ) | ||
| bad_annotations[fld].append(dp.path) | ||
|
|
||
| if bad_annotations: | ||
| log_message( | ||
| "Warning: The following datapoints had invalid annotations, " | ||
| "any annotation-related operations will not work on these:" | ||
| "Warning: The following datapoints had unsupported or invalid annotations, " | ||
| "convenience functions like `add_bounding_box` won't work on these:" | ||
| ) | ||
| err_msg = "" | ||
| for fld, dps in bad_annotations.items(): | ||
| err_msg += f'Field "{fld}" in datapoints:\n\t' | ||
| err_msg += "\n\t".join(dps) | ||
| err_msg += f'\nField "{fld}" in datapoints:\n\t' | ||
| if len(dps) > 10: | ||
| err_msg += "\n\t".join(dps[:10]) + f"\n\t... and {len(dps) - 10} more" | ||
| else: | ||
| err_msg += "\n\t".join(dps) | ||
| log_message(err_msg) | ||
|
|
||
| def download_binary_columns( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| class NotImplementedMeta(type): | ||
| """ | ||
| A metaclass that replaces all parent class methods and properties that aren't overridden in the subclass | ||
| with NotImplementedError. | ||
| """ | ||
|
|
||
| def __new__(mcs, name, bases, namespace): | ||
| # Get all attributes from base classes | ||
| for base in bases: | ||
| for attr_name in dir(base): | ||
| if attr_name.startswith("_"): | ||
| continue | ||
|
kbolashev marked this conversation as resolved.
|
||
|
|
||
| # Skip if already defined in subclass | ||
| if attr_name in namespace: | ||
| continue | ||
|
|
||
| base_attr = getattr(base, attr_name) | ||
|
|
||
| # Handle properties | ||
| if isinstance(base_attr, property): | ||
| # Create a property that raises NotImplementedError | ||
| def make_not_implemented_property(prop_name): | ||
| def getter(self): | ||
| raise NotImplementedError(f"Property '{prop_name}' not implemented") | ||
|
|
||
| def setter(self, value): | ||
| raise NotImplementedError(f"Property '{prop_name}' not implemented") | ||
|
|
||
| def deleter(self): | ||
| raise NotImplementedError(f"Property '{prop_name}' not implemented") | ||
|
|
||
| return property(getter, setter, deleter) | ||
|
|
||
| namespace[attr_name] = make_not_implemented_property(attr_name) | ||
|
|
||
| # Handle regular methods | ||
| elif callable(base_attr): | ||
|
|
||
| def make_not_implemented(method_name): | ||
| def not_impl(self, *args, **kwargs): | ||
| raise NotImplementedError(f"Method '{method_name}' not implemented") | ||
|
|
||
| return not_impl | ||
|
|
||
| namespace[attr_name] = make_not_implemented(attr_name) | ||
|
|
||
| return super().__new__(mcs, name, bases, namespace) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| { | ||
| "id": 41, | ||
| "data": { | ||
| "audio": "https://example.com/some-non-existent-file.mp3", | ||
| "media type": "audio/mpeg", | ||
| "size": 111699 | ||
| }, | ||
| "meta": { | ||
| "datapoint_id": 12345678, | ||
| "datasource_id": 6565 | ||
| }, | ||
| "created_at": "2025-12-20T13:44:02.316027Z", | ||
| "updated_at": "2026-01-26T15:00:13.046967Z", | ||
| "is_labeled": true, | ||
| "project": 1, | ||
| "annotations": [ | ||
| { | ||
| "completed_by": 1, | ||
| "result": [ | ||
| { | ||
| "type": "choices", | ||
| "value": { | ||
| "choices": [ | ||
| "true" | ||
| ] | ||
| }, | ||
| "origin": "manual", | ||
| "to_name": "audio", | ||
| "from_name": "further_utterance", | ||
| "id": "deadbeef1" | ||
| }, | ||
| { | ||
| "type": "rating", | ||
| "value": { | ||
| "rating": 1 | ||
| }, | ||
| "origin": "manual", | ||
| "to_name": "audio", | ||
| "from_name": "difficulty", | ||
| "id": "deadbeef1" | ||
| }, | ||
| { | ||
| "type": "textarea", | ||
| "value": { | ||
| "text": [ | ||
| "kirill@dagshub.com" | ||
| ] | ||
| }, | ||
| "origin": "manual", | ||
| "to_name": "audio", | ||
| "from_name": "email_address", | ||
| "id": "deadbeef1" | ||
| }, | ||
| { | ||
| "to_name": "audio", | ||
| "from_name": "first_name", | ||
| "id": "Qzu1dR2RQ8", | ||
| "type": "textarea", | ||
| "value": { | ||
| "text": [ | ||
| "Kirill" | ||
| ] | ||
| }, | ||
| "origin": "manual" | ||
| }, | ||
| { | ||
| "type": "textarea", | ||
| "value": { | ||
| "text": [ | ||
| "Bolashev" | ||
| ] | ||
| }, | ||
|
kbolashev marked this conversation as resolved.
|
||
| "origin": "manual", | ||
| "to_name": "audio", | ||
| "from_name": "last_name", | ||
| "id": "deadbeef1" | ||
| } | ||
| ], | ||
| "ground_truth": false | ||
| } | ||
| ] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get the condition and the comment.
The comment says to ignore
load_into_memory- then why do you check it? I assume it's because whenload_into_memory == True, then in some earlier part of the code the field was already read as bytes. Then why not just assert that? I guess maybe because then the field content would be string or bytes and you can't know for sure whether that's the blob content or a path without relying on the assumed earlier side effects of the field (would be solved by the type safety we discussed, but out of scope in this PR).Assuming everything I've guessed is correct - I think a comment explaining your assumptions would really help to make this code understandable