From c1568140bc60429fdb17e3ffb82f343795728c28 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 14:02:57 +0000 Subject: [PATCH 01/27] feat: Add CSV ingestion support for Frames Closes #70 This adds which subclasses and registers it with the CLI so can use . --- aperturedb/FrameDataCSV.py | 6 ++++++ aperturedb/ImageDataCSV.py | 2 +- aperturedb/cli/ingest.py | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 aperturedb/FrameDataCSV.py diff --git a/aperturedb/FrameDataCSV.py b/aperturedb/FrameDataCSV.py new file mode 100644 index 00000000..b41a4660 --- /dev/null +++ b/aperturedb/FrameDataCSV.py @@ -0,0 +1,6 @@ +from aperturedb.ImageDataCSV import ImageDataCSV + +class FrameDataCSV(ImageDataCSV): + def __init__(self, *args, **kwargs): + self.command = "AddFrame" + super().__init__(*args, **kwargs) diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index bbfaa797..9b22b651 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -199,7 +199,7 @@ def __init__(self, filename: str, check_image: bool = True, n_download_retries: self.relative_path_prefix = os.path.dirname(self.filename) \ if self.source_type == HEADER_PATH and self.blobs_relative_to_csv else "" - self.command = "AddImage" + self.command = getattr(self, "command", "AddImage") def getitem(self, idx): idx = self.df.index.start + idx diff --git a/aperturedb/cli/ingest.py b/aperturedb/cli/ingest.py index fc953056..2f9d40c8 100644 --- a/aperturedb/cli/ingest.py +++ b/aperturedb/cli/ingest.py @@ -194,7 +194,7 @@ def from_csv(filepath: Annotated[str, typer.Argument( Ingest data from a pre generated CSV file. """ from aperturedb.ImageDataCSV import ImageDataCSV - from aperturedb.BBoxDataCSV import BBoxDataCSV + from aperturedb.FrameDataCSV import FrameDataCSV from aperturedb.EntityDataCSV import EntityDataCSV from aperturedb.BlobDataCSV import BlobDataCSV from aperturedb.ConnectionDataCSV import ConnectionDataCSV @@ -210,6 +210,7 @@ def from_csv(filepath: Annotated[str, typer.Argument( IngestType.DESCRIPTOR: DescriptorDataCSV, IngestType.DESCRIPTORSET: DescriptorSetDataCSV, IngestType.ENTITY: EntityDataCSV, + IngestType.FRAME: FrameDataCSV, IngestType.IMAGE: ImageDataCSV, IngestType.POLYGON: PolygonDataCSV, IngestType.VIDEO: VideoDataCSV From fd0c808fce1ef53921b2be946fb20fdc91383923 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 14:20:59 +0000 Subject: [PATCH 02/27] fix(csv): override get_indices for FrameDataCSV and format --- aperturedb/FrameDataCSV.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aperturedb/FrameDataCSV.py b/aperturedb/FrameDataCSV.py index b41a4660..1a569ff9 100644 --- a/aperturedb/FrameDataCSV.py +++ b/aperturedb/FrameDataCSV.py @@ -1,6 +1,14 @@ from aperturedb.ImageDataCSV import ImageDataCSV + class FrameDataCSV(ImageDataCSV): def __init__(self, *args, **kwargs): self.command = "AddFrame" super().__init__(*args, **kwargs) + + def get_indices(self): + return { + "entity": { + "_Frame": self.get_indexed_properties() + } + } From b8efa613c509e4f2f9fb9aee0fe113404a1d49eb Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 15:21:17 +0000 Subject: [PATCH 03/27] fix: address review comments for Frame OM, tests, and ingest cli --- aperturedb/DataModels.py | 2 +- aperturedb/Entities.py | 2 ++ aperturedb/Frames.py | 21 +++++++++++++++++++++ aperturedb/Images.py | 17 ----------------- aperturedb/cli/ingest.py | 1 + test/test_FrameDataCSV.py | 25 +++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 18 deletions(-) create mode 100644 aperturedb/Frames.py create mode 100644 test/test_FrameDataCSV.py diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index d5fcc51b..969bd483 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -70,7 +70,7 @@ class PolygonDataModel(IdentityDataModel): type = ObjectType.POLYGON -class FrameDataModel(IdentityDataModel): +class FrameDataModel(BlobDataModel): """Frame data model for ApertureDB. """ type = ObjectType.FRAME diff --git a/aperturedb/Entities.py b/aperturedb/Entities.py index 389a0acd..5d824e44 100644 --- a/aperturedb/Entities.py +++ b/aperturedb/Entities.py @@ -278,6 +278,7 @@ def get_blob(self, entity) -> Any: def load_entities_registry(custom_entities: List[str] = None) -> dict: from aperturedb.Polygons import Polygons from aperturedb.Images import Images + from aperturedb.Frames import Frames from aperturedb.Blobs import Blobs from aperturedb.BoundingBoxes import BoundingBoxes from aperturedb.Videos import Videos @@ -287,6 +288,7 @@ def load_entities_registry(custom_entities: List[str] = None) -> dict: known_entities = { ObjectType.POLYGON.value: Polygons, ObjectType.IMAGE.value: Images, + ObjectType.FRAME.value: Frames, ObjectType.VIDEO.value: Videos, ObjectType.BOUNDING_BOX.value: BoundingBoxes, ObjectType.BLOB.value: Blobs, diff --git a/aperturedb/Frames.py b/aperturedb/Frames.py new file mode 100644 index 00000000..f000874c --- /dev/null +++ b/aperturedb/Frames.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from aperturedb.Images import Images +from aperturedb.Query import ObjectType + + +class Frames(Images): + """ + **The python wrapper of frame images in ApertureDB.** + + Frames in ApertureDB are quite similar to images and so + are modeled in python as a subclass. + + + Args: + client: The database connector, perhaps as returned by `CommonLibrary.create_connector` + """ + db_object = ObjectType.FRAME + + def __init__(self, client, batch_size=100, response=None, **kwargs): + super().__init__(client, batch_size=batch_size, response=response, **kwargs) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 1c5cc3a0..109515e7 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1001,20 +1001,3 @@ def get_properties(self, prop_list: Iterable[str] = []) -> Dict[str, Any]: print("Cannot retrieved properties") return return_dictionary - - -class Frames(Images): - """ - **The python wrapper of frame images in ApertureDB.** - - Frames in ApertureDB are quite similar to images and so - are modeled in python as a subclass. - - - Args: - client: The database connector, perhaps as returned by `CommonLibrary.create_connector` - """ - db_object = ObjectType.FRAME - - def __init__(self, client, batch_size=100, response=None, **kwargs): - super().__init__(client, batch_size=batch_size, response=response, **kwargs) diff --git a/aperturedb/cli/ingest.py b/aperturedb/cli/ingest.py index 2f9d40c8..13de9f12 100644 --- a/aperturedb/cli/ingest.py +++ b/aperturedb/cli/ingest.py @@ -195,6 +195,7 @@ def from_csv(filepath: Annotated[str, typer.Argument( """ from aperturedb.ImageDataCSV import ImageDataCSV from aperturedb.FrameDataCSV import FrameDataCSV + from aperturedb.BBoxDataCSV import BBoxDataCSV from aperturedb.EntityDataCSV import EntityDataCSV from aperturedb.BlobDataCSV import BlobDataCSV from aperturedb.ConnectionDataCSV import ConnectionDataCSV diff --git a/test/test_FrameDataCSV.py b/test/test_FrameDataCSV.py new file mode 100644 index 00000000..821a4f57 --- /dev/null +++ b/test/test_FrameDataCSV.py @@ -0,0 +1,25 @@ +import pytest +import pandas as pd +import tempfile +import os +from aperturedb.FrameDataCSV import FrameDataCSV + + +def test_FrameDataCSV_command(): + with tempfile.NamedTemporaryFile(suffix=".csv", mode="w", delete=False) as f: + f.write("url,id\nhttp://example.com/frame.jpg,1\n") + f.close() + + try: + # We don't actually need the image since check_image=False + frame_data = FrameDataCSV(f.name, check_image=False) + + assert frame_data.command == "AddFrame", f"Expected AddFrame, got { + frame_data.command}" + + indices = frame_data.get_indices() + assert "entity" in indices + assert "_Frame" in indices["entity"] + assert indices["entity"]["_Frame"] == frame_data.get_indexed_properties() + finally: + os.remove(f.name) From 803e0faf5f29154e61a12d6c521a5003e46ad09e Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 15:45:15 +0000 Subject: [PATCH 04/27] test(FrameDataCSV): fix syntax error in f-string --- test/test_FrameDataCSV.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_FrameDataCSV.py b/test/test_FrameDataCSV.py index 821a4f57..6e38b2f5 100644 --- a/test/test_FrameDataCSV.py +++ b/test/test_FrameDataCSV.py @@ -14,8 +14,8 @@ def test_FrameDataCSV_command(): # We don't actually need the image since check_image=False frame_data = FrameDataCSV(f.name, check_image=False) - assert frame_data.command == "AddFrame", f"Expected AddFrame, got { - frame_data.command}" + cmd = frame_data.command + assert cmd == "AddFrame", f"Expected AddFrame, got {cmd}" indices = frame_data.get_indices() assert "entity" in indices From cce1bc7cbb2c7879dcd5fd47ad17dbb126fb7707 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 16:13:28 +0000 Subject: [PATCH 05/27] fix: address latest copilot review comments (docstring, unused imports, backward-compat shim) --- aperturedb/FrameDataCSV.py | 10 +++++++--- aperturedb/Images.py | 4 ++++ test/test_FrameDataCSV.py | 2 -- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/aperturedb/FrameDataCSV.py b/aperturedb/FrameDataCSV.py index 1a569ff9..62a16ddf 100644 --- a/aperturedb/FrameDataCSV.py +++ b/aperturedb/FrameDataCSV.py @@ -2,9 +2,13 @@ class FrameDataCSV(ImageDataCSV): - def __init__(self, *args, **kwargs): - self.command = "AddFrame" - super().__init__(*args, **kwargs) + """ + **Helper class to ingest Frame data from a CSV file.** + + This class extends ImageDataCSV and sets the insertion command to "AddFrame", + allowing frame files to be batch ingested from CSVs just like images. + """ + command = "AddFrame" def get_indices(self): return { diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 109515e7..580c70d7 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1001,3 +1001,7 @@ def get_properties(self, prop_list: Iterable[str] = []) -> Dict[str, Any]: print("Cannot retrieved properties") return return_dictionary + + +# Shim for backward compatibility +from aperturedb.Frames import Frames # noqa: F401 diff --git a/test/test_FrameDataCSV.py b/test/test_FrameDataCSV.py index 6e38b2f5..b77e2cbc 100644 --- a/test/test_FrameDataCSV.py +++ b/test/test_FrameDataCSV.py @@ -1,5 +1,3 @@ -import pytest -import pandas as pd import tempfile import os from aperturedb.FrameDataCSV import FrameDataCSV From e0886c551b7da6595320df483682d7134ae0a6f4 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 16:39:34 +0000 Subject: [PATCH 06/27] fix: use __getattr__ for lazy Frames import --- aperturedb/Images.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 580c70d7..5a24cf29 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1004,4 +1004,8 @@ def get_properties(self, prop_list: Iterable[str] = []) -> Dict[str, Any]: # Shim for backward compatibility -from aperturedb.Frames import Frames # noqa: F401 +def __getattr__(name: str): + if name == "Frames": + from aperturedb.Frames import Frames + return Frames + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From 0b77b0427454557d92bc1120d5dbe99fb93ccadd Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 17:44:21 +0000 Subject: [PATCH 07/27] fix: address review comments on FrameDataModel and formatting --- aperturedb/DataModels.py | 4 +++- aperturedb/Frames.py | 3 ++- test/test_FrameDataCSV.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index 969bd483..b610a140 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -4,7 +4,7 @@ from __future__ import annotations from pydantic import BaseModel, Field from typing_extensions import Annotated, List -from typing import ClassVar +from typing import ClassVar, Optional from uuid import uuid4 from aperturedb.Query import ObjectType, PropertyType, RangeType @@ -73,6 +73,8 @@ class PolygonDataModel(IdentityDataModel): class FrameDataModel(BlobDataModel): """Frame data model for ApertureDB. """ + url: Annotated[Optional[str], Field( + title="URL", description="URL to file, http, s3 or gs resource")] = None type = ObjectType.FRAME diff --git a/aperturedb/Frames.py b/aperturedb/Frames.py index f000874c..060655ae 100644 --- a/aperturedb/Frames.py +++ b/aperturedb/Frames.py @@ -18,4 +18,5 @@ class Frames(Images): db_object = ObjectType.FRAME def __init__(self, client, batch_size=100, response=None, **kwargs): - super().__init__(client, batch_size=batch_size, response=response, **kwargs) + super().__init__( + client, batch_size=batch_size, response=response, **kwargs) diff --git a/test/test_FrameDataCSV.py b/test/test_FrameDataCSV.py index b77e2cbc..63d3ed85 100644 --- a/test/test_FrameDataCSV.py +++ b/test/test_FrameDataCSV.py @@ -4,7 +4,9 @@ def test_FrameDataCSV_command(): - with tempfile.NamedTemporaryFile(suffix=".csv", mode="w", delete=False) as f: + with tempfile.NamedTemporaryFile( + suffix=".csv", mode="w", delete=False + ) as f: f.write("url,id\nhttp://example.com/frame.jpg,1\n") f.close() From 17ee930829dd23e51822ce31d0f73381cc872bbe Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 18:08:11 +0000 Subject: [PATCH 08/27] fix: do not override url in FrameDataModel to avoid pydantic override error --- aperturedb/DataModels.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index b610a140..7464521d 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -73,8 +73,6 @@ class PolygonDataModel(IdentityDataModel): class FrameDataModel(BlobDataModel): """Frame data model for ApertureDB. """ - url: Annotated[Optional[str], Field( - title="URL", description="URL to file, http, s3 or gs resource")] = None type = ObjectType.FRAME From f393910dd6e00cda4c8119997075b2fa47a6ead8 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Wed, 12 Aug 2026 19:05:22 +0000 Subject: [PATCH 09/27] test: add tests for Frames OM and clean up unused import - Added `test_Frames.py` to verify the `Frames` object model correctly maps to `ObjectType.FRAME`, addressing the reviewer's request. - Removed unused `Optional` import from `DataModels.py` as suggested by Copilot. --- aperturedb/DataModels.py | 2 +- test/test_Frames.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 test/test_Frames.py diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index 7464521d..969bd483 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -4,7 +4,7 @@ from __future__ import annotations from pydantic import BaseModel, Field from typing_extensions import Annotated, List -from typing import ClassVar, Optional +from typing import ClassVar from uuid import uuid4 from aperturedb.Query import ObjectType, PropertyType, RangeType diff --git a/test/test_Frames.py b/test/test_Frames.py new file mode 100644 index 00000000..aaa310b6 --- /dev/null +++ b/test/test_Frames.py @@ -0,0 +1,15 @@ +from aperturedb.Frames import Frames +from aperturedb.Query import ObjectType + + +class MockClient: + def __init__(self): + pass + + +def test_Frames_init(): + client = MockClient() + frame = Frames(client) + assert frame.client == client + assert frame.db_object.value == "_Frame" + assert frame.db_object == ObjectType.FRAME From c9faff2a03dbdd2e0f67f3255740e8c676023df8 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 13 Aug 2026 14:18:31 +0000 Subject: [PATCH 10/27] fix: revert FrameDataModel base class to IdentityDataModel to avoid breaking changes --- aperturedb/DataModels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index 969bd483..d5fcc51b 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -70,7 +70,7 @@ class PolygonDataModel(IdentityDataModel): type = ObjectType.POLYGON -class FrameDataModel(BlobDataModel): +class FrameDataModel(IdentityDataModel): """Frame data model for ApertureDB. """ type = ObjectType.FRAME From c560a567babf86412942cf39d0e983d0e0efc97d Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Thu, 13 Aug 2026 15:10:39 +0000 Subject: [PATCH 11/27] fix: export Frames in __all__ for import * support --- aperturedb/Images.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 5a24cf29..03d7440c 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1009,3 +1009,6 @@ def __getattr__(name: str): from aperturedb.Frames import Frames return Frames raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [k for k in globals().keys() if not k.startswith('_')] + ['Frames'] From 8d99ccfb318403f5fae8685cdf9ca3dc5d2d9db1 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 14 Aug 2026 19:26:55 +0000 Subject: [PATCH 12/27] fix: ensure FrameDataModel inherits from BlobDataModel --- aperturedb/DataModels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index d5fcc51b..969bd483 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -70,7 +70,7 @@ class PolygonDataModel(IdentityDataModel): type = ObjectType.POLYGON -class FrameDataModel(IdentityDataModel): +class FrameDataModel(BlobDataModel): """Frame data model for ApertureDB. """ type = ObjectType.FRAME From 83878fdbd3b744d4436b73f212ff5570430c2612 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 14 Aug 2026 23:05:14 +0000 Subject: [PATCH 13/27] fix: cache Frames class in globals during lazy import Addressed Copilot review comment to cache the resolved class in globals() before returning it to improve backward compatibility and avoid re-imports. --- aperturedb/Images.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 03d7440c..5ad9148a 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1007,6 +1007,7 @@ def get_properties(self, prop_list: Iterable[str] = []) -> Dict[str, Any]: def __getattr__(name: str): if name == "Frames": from aperturedb.Frames import Frames + globals()[name] = Frames return Frames raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From 087f5107f6cce5b797a0b0af8bd526bb79e04b7a Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Fri, 14 Aug 2026 23:32:58 +0000 Subject: [PATCH 14/27] fix: address review comments on frame models and tests --- aperturedb/DataModels.py | 1 + aperturedb/FrameDataCSV.py | 3 ++- test/test_DataModels.py | 16 ++++++++++++++++ test/test_FrameDataCSV.py | 25 +++++++++++++------------ test/test_Frames.py | 12 ++++++++++++ 5 files changed, 44 insertions(+), 13 deletions(-) create mode 100644 test/test_DataModels.py diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index 969bd483..74f06908 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -72,6 +72,7 @@ class PolygonDataModel(IdentityDataModel): class FrameDataModel(BlobDataModel): """Frame data model for ApertureDB. + Inherits from BlobDataModel, making the `url` field required for ingestion. """ type = ObjectType.FRAME diff --git a/aperturedb/FrameDataCSV.py b/aperturedb/FrameDataCSV.py index 62a16ddf..9f012a9d 100644 --- a/aperturedb/FrameDataCSV.py +++ b/aperturedb/FrameDataCSV.py @@ -1,4 +1,5 @@ from aperturedb.ImageDataCSV import ImageDataCSV +from aperturedb.Query import ObjectType class FrameDataCSV(ImageDataCSV): @@ -13,6 +14,6 @@ class FrameDataCSV(ImageDataCSV): def get_indices(self): return { "entity": { - "_Frame": self.get_indexed_properties() + ObjectType.FRAME.value: self.get_indexed_properties() } } diff --git a/test/test_DataModels.py b/test/test_DataModels.py new file mode 100644 index 00000000..04b1869f --- /dev/null +++ b/test/test_DataModels.py @@ -0,0 +1,16 @@ +from aperturedb.DataModels import FrameDataModel +from aperturedb.Query import ObjectType +import pytest + + +def test_FrameDataModel(): + # Verify that url is a required field because it inherits from BlobDataModel + with pytest.raises(ValueError): + # This should fail because url is missing + FrameDataModel() + + # Verify that when url is provided, the model instantiates correctly + frame = FrameDataModel(url="http://example.com/frame.jpg") + assert frame.url == "http://example.com/frame.jpg" + assert frame.type == ObjectType.FRAME + assert frame.id is not None # Should have a default UUID generated diff --git a/test/test_FrameDataCSV.py b/test/test_FrameDataCSV.py index 63d3ed85..17c2a561 100644 --- a/test/test_FrameDataCSV.py +++ b/test/test_FrameDataCSV.py @@ -1,6 +1,7 @@ import tempfile import os from aperturedb.FrameDataCSV import FrameDataCSV +from aperturedb.Query import ObjectType def test_FrameDataCSV_command(): @@ -8,18 +9,18 @@ def test_FrameDataCSV_command(): suffix=".csv", mode="w", delete=False ) as f: f.write("url,id\nhttp://example.com/frame.jpg,1\n") - f.close() - try: - # We don't actually need the image since check_image=False - frame_data = FrameDataCSV(f.name, check_image=False) + try: + # We don't actually need the image since check_image=False + frame_data = FrameDataCSV(f.name, check_image=False) - cmd = frame_data.command - assert cmd == "AddFrame", f"Expected AddFrame, got {cmd}" + cmd = frame_data.command + assert cmd == "AddFrame", f"Expected AddFrame, got {cmd}" - indices = frame_data.get_indices() - assert "entity" in indices - assert "_Frame" in indices["entity"] - assert indices["entity"]["_Frame"] == frame_data.get_indexed_properties() - finally: - os.remove(f.name) + indices = frame_data.get_indices() + assert "entity" in indices + frame_type = ObjectType.FRAME.value + assert frame_type in indices["entity"] + assert indices["entity"][frame_type] == frame_data.get_indexed_properties() + finally: + os.remove(f.name) diff --git a/test/test_Frames.py b/test/test_Frames.py index aaa310b6..6b10a12c 100644 --- a/test/test_Frames.py +++ b/test/test_Frames.py @@ -13,3 +13,15 @@ def test_Frames_init(): assert frame.client == client assert frame.db_object.value == "_Frame" assert frame.db_object == ObjectType.FRAME + + +def test_Frames_backward_compatibility_import(): + # Verify that importing Frames from aperturedb.Images resolves correctly + import aperturedb.Images + from aperturedb.Images import Frames as ImagesFrames + + assert ImagesFrames is Frames + + # Verify it is cached in the module's globals + assert "Frames" in aperturedb.Images.__dict__ + assert aperturedb.Images.__dict__["Frames"] is Frames From b3d4e591f1b2758c60c2182c23bae129ce9fa084 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 00:03:29 +0000 Subject: [PATCH 15/27] fix: address suppressed copilot review comments - Fallback to 'AddImage' if getattr returns None in ImageDataCSV. - Explicitly define __all__ in Images.py instead of building dynamically. --- aperturedb/ImageDataCSV.py | 2 +- aperturedb/Images.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index 9b22b651..dac86e88 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -199,7 +199,7 @@ def __init__(self, filename: str, check_image: bool = True, n_download_retries: self.relative_path_prefix = os.path.dirname(self.filename) \ if self.source_type == HEADER_PATH and self.blobs_relative_to_csv else "" - self.command = getattr(self, "command", "AddImage") + self.command = getattr(self, "command", None) or "AddImage" def getitem(self, idx): idx = self.df.index.start + idx diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 5ad9148a..a3b685fe 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1012,4 +1012,5 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -__all__ = [k for k in globals().keys() if not k.startswith('_')] + ['Frames'] +__all__ = ["np_arr_img_to_bytes", "image_to_bytes", + "rotate", "resolve", "Images", "Frames"] From d0beaecf3b241eb03a7716708bf508173227b4ee Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 03:21:09 +0000 Subject: [PATCH 16/27] fix: address review comments on FrameDataModel exception and Frames test assertion --- test/test_DataModels.py | 3 ++- test/test_Frames.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_DataModels.py b/test/test_DataModels.py index 04b1869f..961dde47 100644 --- a/test/test_DataModels.py +++ b/test/test_DataModels.py @@ -1,11 +1,12 @@ from aperturedb.DataModels import FrameDataModel from aperturedb.Query import ObjectType +from pydantic import ValidationError import pytest def test_FrameDataModel(): # Verify that url is a required field because it inherits from BlobDataModel - with pytest.raises(ValueError): + with pytest.raises((ValueError, ValidationError)): # This should fail because url is missing FrameDataModel() diff --git a/test/test_Frames.py b/test/test_Frames.py index 6b10a12c..37b3f1c0 100644 --- a/test/test_Frames.py +++ b/test/test_Frames.py @@ -11,7 +11,6 @@ def test_Frames_init(): client = MockClient() frame = Frames(client) assert frame.client == client - assert frame.db_object.value == "_Frame" assert frame.db_object == ObjectType.FRAME From b34b3ccd25c02f9419982492e8ba2c40b84d6844 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 04:17:20 +0000 Subject: [PATCH 17/27] fix: address latest copilot review comments - Improve command fallback in ImageDataCSV.py - Add TYPE_CHECKING and __dir__ in Images.py - Add unit test for ingest.py from_csv routing for IngestType.FRAME --- aperturedb/ImageDataCSV.py | 4 +++- aperturedb/Images.py | 9 ++++++++- test/test_cli_ingest.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 test/test_cli_ingest.py diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index dac86e88..8eaf0af1 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -199,7 +199,9 @@ def __init__(self, filename: str, check_image: bool = True, n_download_retries: self.relative_path_prefix = os.path.dirname(self.filename) \ if self.source_type == HEADER_PATH and self.blobs_relative_to_csv else "" - self.command = getattr(self, "command", None) or "AddImage" + self.command = getattr(self, "command", None) + if self.command is None: + self.command = "AddImage" def getitem(self, idx): idx = self.df.index.start + idx diff --git a/aperturedb/Images.py b/aperturedb/Images.py index a3b685fe..a86eaa03 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -3,7 +3,10 @@ """ from __future__ import annotations -from typing import Any, Dict, Iterable, List, Tuple, Union +from typing import Any, Dict, Iterable, List, Tuple, Union, TYPE_CHECKING + +if TYPE_CHECKING: + from aperturedb.Frames import Frames import cv2 import math import numpy as np @@ -1012,5 +1015,9 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +def __dir__(): + return __all__ + + __all__ = ["np_arr_img_to_bytes", "image_to_bytes", "rotate", "resolve", "Images", "Frames"] diff --git a/test/test_cli_ingest.py b/test/test_cli_ingest.py new file mode 100644 index 00000000..a553aec4 --- /dev/null +++ b/test/test_cli_ingest.py @@ -0,0 +1,21 @@ +import pytest +from unittest.mock import patch, MagicMock +from aperturedb.cli.ingest import from_csv, IngestType + + +def test_from_csv_frame_type(): + import aperturedb.FrameDataCSV + + with patch("aperturedb.cli.ingest._process_data") as mock_process_data: + mock_process_data.return_value = None + + with patch.object(aperturedb.FrameDataCSV, "FrameDataCSV") as mock_csv_class: + mock_data = MagicMock() + mock_data.__len__.return_value = 10 + mock_csv_class.return_value = mock_data + + from_csv(filepath="dummy.csv", + ingest_type=IngestType.FRAME, sample_count=5) + + mock_csv_class.assert_called_once_with( + "dummy.csv", use_dask=False, blobs_relative_to_csv=True) From a50b275d04a1eb8a893f9190c2b419dde91f8a69 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 05:06:08 +0000 Subject: [PATCH 18/27] fix: address latest copilot review comments - Define __all__ before __dir__ in Images.py to avoid NameError. - Remove unused pytest import in test_cli_ingest.py. --- aperturedb/Images.py | 8 ++++---- test/test_cli_ingest.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index a86eaa03..77955101 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1015,9 +1015,9 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -def __dir__(): - return __all__ - - __all__ = ["np_arr_img_to_bytes", "image_to_bytes", "rotate", "resolve", "Images", "Frames"] + + +def __dir__(): + return __all__ diff --git a/test/test_cli_ingest.py b/test/test_cli_ingest.py index a553aec4..88d79375 100644 --- a/test/test_cli_ingest.py +++ b/test/test_cli_ingest.py @@ -1,4 +1,3 @@ -import pytest from unittest.mock import patch, MagicMock from aperturedb.cli.ingest import from_csv, IngestType From 401f06a8fdc610b1610a5a52c2b530634c00d40f Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 05:33:34 +0000 Subject: [PATCH 19/27] fix: address latest suppressed copilot review comments --- aperturedb/ImageDataCSV.py | 2 +- aperturedb/Images.py | 2 +- test/test_DataModels.py | 2 +- test/test_cli_ingest.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index 8eaf0af1..d86e4218 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -199,7 +199,7 @@ def __init__(self, filename: str, check_image: bool = True, n_download_retries: self.relative_path_prefix = os.path.dirname(self.filename) \ if self.source_type == HEADER_PATH and self.blobs_relative_to_csv else "" - self.command = getattr(self, "command", None) + self.command = getattr(type(self), "command", None) if self.command is None: self.command = "AddImage" diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 77955101..febc85cc 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1020,4 +1020,4 @@ def __getattr__(name: str): def __dir__(): - return __all__ + return sorted(set(list(globals().keys()) + __all__)) diff --git a/test/test_DataModels.py b/test/test_DataModels.py index 961dde47..d4d759d6 100644 --- a/test/test_DataModels.py +++ b/test/test_DataModels.py @@ -6,7 +6,7 @@ def test_FrameDataModel(): # Verify that url is a required field because it inherits from BlobDataModel - with pytest.raises((ValueError, ValidationError)): + with pytest.raises(ValidationError): # This should fail because url is missing FrameDataModel() diff --git a/test/test_cli_ingest.py b/test/test_cli_ingest.py index 88d79375..1894be01 100644 --- a/test/test_cli_ingest.py +++ b/test/test_cli_ingest.py @@ -10,7 +10,6 @@ def test_from_csv_frame_type(): with patch.object(aperturedb.FrameDataCSV, "FrameDataCSV") as mock_csv_class: mock_data = MagicMock() - mock_data.__len__.return_value = 10 mock_csv_class.return_value = mock_data from_csv(filepath="dummy.csv", From 413c7083a95bef57a5b42b3d6988be4d72ad0ca2 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 06:00:26 +0000 Subject: [PATCH 20/27] fix: address latest review feedback on ImageDataCSV and Images __all__ --- aperturedb/ImageDataCSV.py | 6 ++---- aperturedb/Images.py | 10 ++++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index d86e4218..27af75f5 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -119,6 +119,8 @@ def load_gs_url(self, gs_url): class ImageDataCSV(CSVParser.CSVParser, ImageDataProcessor): + command = "AddImage" + """**ApertureDB Image Data.** This class loads the Image Data which is present in a CSV file, @@ -199,10 +201,6 @@ def __init__(self, filename: str, check_image: bool = True, n_download_retries: self.relative_path_prefix = os.path.dirname(self.filename) \ if self.source_type == HEADER_PATH and self.blobs_relative_to_csv else "" - self.command = getattr(type(self), "command", None) - if self.command is None: - self.command = "AddImage" - def getitem(self, idx): idx = self.df.index.start + idx diff --git a/aperturedb/Images.py b/aperturedb/Images.py index febc85cc..ed004478 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1015,8 +1015,14 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -__all__ = ["np_arr_img_to_bytes", "image_to_bytes", - "rotate", "resolve", "Images", "Frames"] +__all__ = [ + "Any", "BytesIO", "Constraints", "DataFrame", "Dict", "Entities", + "Frames", "HTML", "Image", "Images", "Iterable", "List", + "ObjectType", "QueryBuilder", "TYPE_CHECKING", "Tuple", "Union", + "Utils", "annotations", "base64", "class_entity", "cv2", "display", + "execute_query", "image_to_bytes", "logger", "logging", "math", + "np", "np_arr_img_to_bytes", "plt", "resolve", "rotate", "widgets" +] def __dir__(): From a8bb224516b3ea46d5e9621bc418132aeee12575 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 06:33:34 +0000 Subject: [PATCH 21/27] fix: address latest review comments on docstrings, DataModels, Images, and add test for Entities --- aperturedb/DataModels.py | 7 ++++--- aperturedb/ImageDataCSV.py | 4 ++-- aperturedb/Images.py | 13 +------------ test/test_DataModels.py | 8 +++----- test/test_Entities.py | 9 +++++++++ 5 files changed, 19 insertions(+), 22 deletions(-) create mode 100644 test/test_Entities.py diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index 74f06908..54803a23 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -4,7 +4,7 @@ from __future__ import annotations from pydantic import BaseModel, Field from typing_extensions import Annotated, List -from typing import ClassVar +from typing import ClassVar, Optional from uuid import uuid4 from aperturedb.Query import ObjectType, PropertyType, RangeType @@ -70,10 +70,11 @@ class PolygonDataModel(IdentityDataModel): type = ObjectType.POLYGON -class FrameDataModel(BlobDataModel): +class FrameDataModel(IdentityDataModel): """Frame data model for ApertureDB. - Inherits from BlobDataModel, making the `url` field required for ingestion. """ + url: Annotated[Optional[str], Field( + title="URL", description="URL to file, http, s3 or gs resource")] = None type = ObjectType.FRAME diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index 27af75f5..2cd60353 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -119,8 +119,6 @@ def load_gs_url(self, gs_url): class ImageDataCSV(CSVParser.CSVParser, ImageDataProcessor): - command = "AddImage" - """**ApertureDB Image Data.** This class loads the Image Data which is present in a CSV file, @@ -173,6 +171,8 @@ class ImageDataCSV(CSVParser.CSVParser, ImageDataProcessor): id would be only inserted if it does not already exist in the database. ::: """ + command = "AddImage" + def __init__(self, filename: str, check_image: bool = True, n_download_retries: int = 3, **kwargs): diff --git a/aperturedb/Images.py b/aperturedb/Images.py index ed004478..25f111ac 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1015,15 +1015,4 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -__all__ = [ - "Any", "BytesIO", "Constraints", "DataFrame", "Dict", "Entities", - "Frames", "HTML", "Image", "Images", "Iterable", "List", - "ObjectType", "QueryBuilder", "TYPE_CHECKING", "Tuple", "Union", - "Utils", "annotations", "base64", "class_entity", "cv2", "display", - "execute_query", "image_to_bytes", "logger", "logging", "math", - "np", "np_arr_img_to_bytes", "plt", "resolve", "rotate", "widgets" -] - - -def __dir__(): - return sorted(set(list(globals().keys()) + __all__)) + diff --git a/test/test_DataModels.py b/test/test_DataModels.py index d4d759d6..fc50b8d3 100644 --- a/test/test_DataModels.py +++ b/test/test_DataModels.py @@ -3,12 +3,10 @@ from pydantic import ValidationError import pytest - def test_FrameDataModel(): - # Verify that url is a required field because it inherits from BlobDataModel - with pytest.raises(ValidationError): - # This should fail because url is missing - FrameDataModel() + # url is optional to avoid breaking existing users + frame_no_url = FrameDataModel() + assert frame_no_url.url is None # Verify that when url is provided, the model instantiates correctly frame = FrameDataModel(url="http://example.com/frame.jpg") diff --git a/test/test_Entities.py b/test/test_Entities.py new file mode 100644 index 00000000..61ec3ac1 --- /dev/null +++ b/test/test_Entities.py @@ -0,0 +1,9 @@ +from aperturedb.Entities import load_entities_registry +from aperturedb.Query import ObjectType +from aperturedb.Frames import Frames +import pytest + +def test_load_entities_registry_frames(): + registry = load_entities_registry() + assert ObjectType.FRAME.value in registry + assert registry[ObjectType.FRAME.value] is Frames From 118e8e0b5da6bb6fff4f8d6b33cd5178076bc142 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 07:01:37 +0000 Subject: [PATCH 22/27] style: fix autopep8 formatting issues --- aperturedb/ImageDataCSV.py | 1 - aperturedb/Images.py | 3 --- test/test_DataModels.py | 1 + test/test_Entities.py | 1 + 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/aperturedb/ImageDataCSV.py b/aperturedb/ImageDataCSV.py index 2cd60353..edd56f18 100644 --- a/aperturedb/ImageDataCSV.py +++ b/aperturedb/ImageDataCSV.py @@ -173,7 +173,6 @@ class ImageDataCSV(CSVParser.CSVParser, ImageDataProcessor): """ command = "AddImage" - def __init__(self, filename: str, check_image: bool = True, n_download_retries: int = 3, **kwargs): ImageDataProcessor.__init__( diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 25f111ac..7c6b149c 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1013,6 +1013,3 @@ def __getattr__(name: str): globals()[name] = Frames return Frames raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - - diff --git a/test/test_DataModels.py b/test/test_DataModels.py index fc50b8d3..b0633552 100644 --- a/test/test_DataModels.py +++ b/test/test_DataModels.py @@ -3,6 +3,7 @@ from pydantic import ValidationError import pytest + def test_FrameDataModel(): # url is optional to avoid breaking existing users frame_no_url = FrameDataModel() diff --git a/test/test_Entities.py b/test/test_Entities.py index 61ec3ac1..b3797e74 100644 --- a/test/test_Entities.py +++ b/test/test_Entities.py @@ -3,6 +3,7 @@ from aperturedb.Frames import Frames import pytest + def test_load_entities_registry_frames(): registry = load_entities_registry() assert ObjectType.FRAME.value in registry From 3a92a2f2e6c469363d912cc4657e15c0fed17f55 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 07:34:48 +0000 Subject: [PATCH 23/27] Address Copilot feedback: add __all__ to Images.py and remove unused imports --- aperturedb/Images.py | 8 ++++++++ test/test_DataModels.py | 2 -- test/test_Entities.py | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 7c6b149c..00bb76b0 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1013,3 +1013,11 @@ def __getattr__(name: str): globals()[name] = Frames return Frames raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = ["np_arr_img_to_bytes", "image_to_bytes", + "rotate", "resolve", "Images", "Frames"] + + +def __dir__(): + return sorted(list(globals().keys()) + ["Frames"]) diff --git a/test/test_DataModels.py b/test/test_DataModels.py index b0633552..50c5cd2b 100644 --- a/test/test_DataModels.py +++ b/test/test_DataModels.py @@ -1,7 +1,5 @@ from aperturedb.DataModels import FrameDataModel from aperturedb.Query import ObjectType -from pydantic import ValidationError -import pytest def test_FrameDataModel(): diff --git a/test/test_Entities.py b/test/test_Entities.py index b3797e74..0666c304 100644 --- a/test/test_Entities.py +++ b/test/test_Entities.py @@ -1,7 +1,6 @@ from aperturedb.Entities import load_entities_registry from aperturedb.Query import ObjectType from aperturedb.Frames import Frames -import pytest def test_load_entities_registry_frames(): From a576464d3e1c7dd109488a8c28a3ae2533983542 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 08:55:33 +0000 Subject: [PATCH 24/27] fix: address final review feedback on Images.__all__ and test names/assertions --- aperturedb/Images.py | 10 ++++++++-- test/test_Frames.py | 6 +++--- test/test_cli_ingest.py | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 00bb76b0..6cfe8c62 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1015,8 +1015,14 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -__all__ = ["np_arr_img_to_bytes", "image_to_bytes", - "rotate", "resolve", "Images", "Frames"] +__all__ = [ + "Any", "BytesIO", "Constraints", "DataFrame", "Dict", "Entities", + "Frames", "HTML", "Image", "Images", "Iterable", "List", + "ObjectType", "QueryBuilder", "TYPE_CHECKING", "Tuple", "Union", + "Utils", "annotations", "base64", "class_entity", "cv2", "display", + "execute_query", "image_to_bytes", "logger", "logging", "math", + "np", "np_arr_img_to_bytes", "plt", "resolve", "rotate", "widgets" +] def __dir__(): diff --git a/test/test_Frames.py b/test/test_Frames.py index 37b3f1c0..1390b3ac 100644 --- a/test/test_Frames.py +++ b/test/test_Frames.py @@ -9,9 +9,9 @@ def __init__(self): def test_Frames_init(): client = MockClient() - frame = Frames(client) - assert frame.client == client - assert frame.db_object == ObjectType.FRAME + frames = Frames(client) + assert frames.client == client + assert frames.db_object == ObjectType.FRAME def test_Frames_backward_compatibility_import(): diff --git a/test/test_cli_ingest.py b/test/test_cli_ingest.py index 1894be01..eeaf7769 100644 --- a/test/test_cli_ingest.py +++ b/test/test_cli_ingest.py @@ -17,3 +17,4 @@ def test_from_csv_frame_type(): mock_csv_class.assert_called_once_with( "dummy.csv", use_dask=False, blobs_relative_to_csv=True) + mock_process_data.assert_called_once() From f0dea519bee696f761dcf8036c174a5dc1268b39 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 12:57:28 +0000 Subject: [PATCH 25/27] feat: add Frame to Object Model, analogous to Image and Video This adds 'AddFrame' and 'FindFrame' to the blob-returning and transformer loops, so Frames are handled fully like Images and Videos throughout the SDK. --- aperturedb/CommonLibrary.py | 2 +- aperturedb/MLCroissant.py | 2 +- aperturedb/PyTorchDataset.py | 6 +++--- aperturedb/Query.py | 2 +- aperturedb/TensorFlowDataset.py | 8 ++++---- aperturedb/transformers/clip_pytorch_embeddings.py | 2 +- aperturedb/transformers/common_properties.py | 2 +- aperturedb/transformers/facenet_pytorch_embeddings.py | 2 +- aperturedb/transformers/image_properties.py | 4 ++-- aperturedb/transformers/transformer.py | 2 +- aperturedb/transformers/video_properties.py | 4 ++-- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/aperturedb/CommonLibrary.py b/aperturedb/CommonLibrary.py index ab91794a..45970450 100644 --- a/aperturedb/CommonLibrary.py +++ b/aperturedb/CommonLibrary.py @@ -384,7 +384,7 @@ def map_response_to_handler(handler, query, query_blobs, response, response_blo for req, resp in zip(query[start:end], response[start:end]): for k in req: blob_returning_commands = ["FindImage", "FindBlob", "FindVideo", - "FindDescriptor", "FindBoundingBox"] + "FindDescriptor", "FindBoundingBox", "FindFrame"] if k in blob_returning_commands and "blobs" in req[k] and req[k]["blobs"]: count = resp[k]["returned"] b_count += count diff --git a/aperturedb/MLCroissant.py b/aperturedb/MLCroissant.py index 71e8862c..27d1eee2 100644 --- a/aperturedb/MLCroissant.py +++ b/aperturedb/MLCroissant.py @@ -273,7 +273,7 @@ def getitem(self, subscript): indexes_to_create = [] for command in q: cmd = list(command.keys())[-1] - if cmd in ["AddImage", "AddBlob", "AddVideo"]: + if cmd in ["AddImage", "AddBlob", "AddVideo", "AddFrame"]: continue indexable_entity = command[list(command.keys())[-1]]["class"] if indexable_entity not in self.indexed_entities: diff --git a/aperturedb/PyTorchDataset.py b/aperturedb/PyTorchDataset.py index b7e4fdff..070757a0 100644 --- a/aperturedb/PyTorchDataset.py +++ b/aperturedb/PyTorchDataset.py @@ -37,7 +37,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm allowed_find_commands = { "FindImage", "FindVideo", "FindBlob", - "FindDescriptor", "FindBoundingBox" + "FindDescriptor", "FindBoundingBox", "FindFrame" } if self.command_idx is not None: @@ -61,7 +61,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm self.command_name = name if self.command_idx is None: - msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob). The first one encountered will be used." + msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob, FindFrame). The first one encountered will be used." logger.error(msg) raise ValueError(msg) @@ -111,7 +111,7 @@ def __getitem__(self, index): blob = self.batch_blobs[idx] label = self.batch_labels[idx] - if self.command_name == "FindImage": + if self.command_name in ("FindImage", "FindFrame"): nparr = np.frombuffer(blob, dtype=np.uint8) blob = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if blob is None: diff --git a/aperturedb/Query.py b/aperturedb/Query.py index 03923c5b..113a18f7 100644 --- a/aperturedb/Query.py +++ b/aperturedb/Query.py @@ -227,7 +227,7 @@ def generate_add_query( params.pop("properties", None) query.append( QueryBuilder.find_command(obj.type.value, params=params)) - if obj.type in [ObjectType.IMAGE, ObjectType.VIDEO, ObjectType.BLOB]: + if obj.type in [ObjectType.IMAGE, ObjectType.VIDEO, ObjectType.BLOB, ObjectType.FRAME]: # Do not send blob, if Node has been added to set of commands. if obj.id not in cached: if obj.url: diff --git a/aperturedb/TensorFlowDataset.py b/aperturedb/TensorFlowDataset.py index 7ff595c1..9600c4a7 100644 --- a/aperturedb/TensorFlowDataset.py +++ b/aperturedb/TensorFlowDataset.py @@ -35,7 +35,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm allowed_find_commands = { "FindImage", "FindVideo", "FindBlob", - "FindDescriptor", "FindBoundingBox" + "FindDescriptor", "FindBoundingBox", "FindFrame" } if self.command_idx is not None: @@ -59,7 +59,7 @@ def __init__(self, client: Connector, query, label_prop=None, batch_size=1, comm self.command_name = name if self.command_idx is None: - msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob). The first one encountered will be used." + msg = "Query error. The query must contain at least one supported blob-returning Find command (e.g., FindImage, FindVideo, FindBlob, FindFrame). The first one encountered will be used." logger.error(msg) raise ValueError(msg) @@ -177,7 +177,7 @@ def generator(self): blob = self.batch_blobs[idx] label = self.batch_labels[idx] - if self.command_name == "FindImage": + if self.command_name in ("FindImage", "FindFrame"): nparr = np.frombuffer(blob, dtype=np.uint8) blob = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if blob is None: @@ -226,7 +226,7 @@ def get_dataset(self): else: self.label_type = tf.string - if self.command_name == "FindImage": + if self.command_name in ("FindImage", "FindFrame"): tensor_shape = (None, None, 3) tensor_dtype = tf.uint8 else: diff --git a/aperturedb/transformers/clip_pytorch_embeddings.py b/aperturedb/transformers/clip_pytorch_embeddings.py index 62dedbaa..8829358a 100644 --- a/aperturedb/transformers/clip_pytorch_embeddings.py +++ b/aperturedb/transformers/clip_pytorch_embeddings.py @@ -95,7 +95,7 @@ def getitem(self, subscript): except Exception as e: logger.warning( f"Failed to generate embedding or descriptor: {e}", exc_info=True) - if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: blob_index += 1 x[0].extend(new_descriptors) diff --git a/aperturedb/transformers/common_properties.py b/aperturedb/transformers/common_properties.py index 682b1795..bdc02325 100644 --- a/aperturedb/transformers/common_properties.py +++ b/aperturedb/transformers/common_properties.py @@ -49,7 +49,7 @@ def getitem(self, subscript): if isinstance(cmd_dict, dict) and len(cmd_dict) > 0: cmd_name = next(iter(cmd_dict.keys())) - if cmd_name in ["AddImage", "AddVideo", "AddBoundingBox", "AddPolygon"]: + if cmd_name in ["AddImage", "AddVideo", "AddBoundingBox", "AddPolygon", "AddFrame"]: src_properties = cmd_dict[cmd_name].setdefault( "properties", {}) self._apply_common_properties(src_properties) diff --git a/aperturedb/transformers/facenet_pytorch_embeddings.py b/aperturedb/transformers/facenet_pytorch_embeddings.py index 35fc00ba..014eddff 100644 --- a/aperturedb/transformers/facenet_pytorch_embeddings.py +++ b/aperturedb/transformers/facenet_pytorch_embeddings.py @@ -100,7 +100,7 @@ def getitem(self, subscript): except Exception as e: logger.warning( f"Failed to generate embedding or descriptor: {e}", exc_info=True) - if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: blob_index += 1 x[0].extend(new_descriptors) diff --git a/aperturedb/transformers/image_properties.py b/aperturedb/transformers/image_properties.py index bea3e181..fadcece9 100644 --- a/aperturedb/transformers/image_properties.py +++ b/aperturedb/transformers/image_properties.py @@ -34,7 +34,7 @@ def getitem(self, subscript): if isinstance(cmd_dict, dict) and len(cmd_dict) > 0: cmd_name = next(iter(cmd_dict.keys())) - if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: if blob_index >= len(x[1]): logger.warning( "Missing blob for command %s (expected at index %d), stopping property processing for this transaction.", @@ -66,7 +66,7 @@ def getitem(self, subscript): logger.exception( "Error applying image properties", stack_info=True) - if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: blob_index += 1 return x diff --git a/aperturedb/transformers/transformer.py b/aperturedb/transformers/transformer.py index 47a29af4..669557ac 100644 --- a/aperturedb/transformers/transformer.py +++ b/aperturedb/transformers/transformer.py @@ -67,7 +67,7 @@ def __init__(self, data: Subscriptable, client=None, **kwargs) -> None: command = None if isinstance(c, dict) and len(c) > 0: command = next(iter(c.keys())) - if command in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if command in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: self._blob_index.append(i) bc += 1 # Kept for backward compatibility diff --git a/aperturedb/transformers/video_properties.py b/aperturedb/transformers/video_properties.py index ebd5325d..992747f8 100644 --- a/aperturedb/transformers/video_properties.py +++ b/aperturedb/transformers/video_properties.py @@ -32,7 +32,7 @@ def getitem(self, subscript): if isinstance(cmd_dict, dict) and len(cmd_dict) > 0: cmd_name = next(iter(cmd_dict.keys())) - if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: if blob_index >= len(x[1]): logger.warning( "Missing blob for command %s (expected at index %d), stopping property processing for this transaction.", @@ -59,7 +59,7 @@ def getitem(self, subscript): logger.exception( "Error applying video properties", stack_info=True) - if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob"]: + if cmd_name in ["AddImage", "AddDescriptor", "AddVideo", "AddBlob", "AddFrame"]: blob_index += 1 return x From 974efc7ae8546c3cc3059759884071726e854f9a Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 13:24:01 +0000 Subject: [PATCH 26/27] fix: remove annotations from __all__ in Images.py --- aperturedb/Images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperturedb/Images.py b/aperturedb/Images.py index 6cfe8c62..055a5058 100644 --- a/aperturedb/Images.py +++ b/aperturedb/Images.py @@ -1019,7 +1019,7 @@ def __getattr__(name: str): "Any", "BytesIO", "Constraints", "DataFrame", "Dict", "Entities", "Frames", "HTML", "Image", "Images", "Iterable", "List", "ObjectType", "QueryBuilder", "TYPE_CHECKING", "Tuple", "Union", - "Utils", "annotations", "base64", "class_entity", "cv2", "display", + "Utils", "base64", "class_entity", "cv2", "display", "execute_query", "image_to_bytes", "logger", "logging", "math", "np", "np_arr_img_to_bytes", "plt", "resolve", "rotate", "widgets" ] From 95ba433106c9d32ac41f462a1f9e595abfdfcc47 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sat, 15 Aug 2026 14:13:10 +0000 Subject: [PATCH 27/27] fix: ensure FrameDataModel inherits from BlobDataModel properly --- aperturedb/DataModels.py | 4 +--- test/test_DataModels.py | 8 +++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aperturedb/DataModels.py b/aperturedb/DataModels.py index 54803a23..7464521d 100644 --- a/aperturedb/DataModels.py +++ b/aperturedb/DataModels.py @@ -70,11 +70,9 @@ class PolygonDataModel(IdentityDataModel): type = ObjectType.POLYGON -class FrameDataModel(IdentityDataModel): +class FrameDataModel(BlobDataModel): """Frame data model for ApertureDB. """ - url: Annotated[Optional[str], Field( - title="URL", description="URL to file, http, s3 or gs resource")] = None type = ObjectType.FRAME diff --git a/test/test_DataModels.py b/test/test_DataModels.py index 50c5cd2b..72ccc550 100644 --- a/test/test_DataModels.py +++ b/test/test_DataModels.py @@ -1,11 +1,13 @@ +from pydantic import ValidationError from aperturedb.DataModels import FrameDataModel from aperturedb.Query import ObjectType +import pytest def test_FrameDataModel(): - # url is optional to avoid breaking existing users - frame_no_url = FrameDataModel() - assert frame_no_url.url is None + # url is required, so instantiating without it should raise a ValidationError + with pytest.raises(ValidationError): + FrameDataModel() # Verify that when url is provided, the model instantiates correctly frame = FrameDataModel(url="http://example.com/frame.jpg")