Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c156814
feat: Add CSV ingestion support for Frames
Aug 12, 2026
fd0c808
fix(csv): override get_indices for FrameDataCSV and format
Aug 12, 2026
b8efa61
fix: address review comments for Frame OM, tests, and ingest cli
Aug 12, 2026
803e0fa
test(FrameDataCSV): fix syntax error in f-string
Aug 12, 2026
cce1bc7
fix: address latest copilot review comments (docstring, unused import…
Aug 12, 2026
e0886c5
fix: use __getattr__ for lazy Frames import
Aug 12, 2026
0b77b04
fix: address review comments on FrameDataModel and formatting
Aug 12, 2026
17ee930
fix: do not override url in FrameDataModel to avoid pydantic override…
Aug 12, 2026
f393910
test: add tests for Frames OM and clean up unused import
Aug 12, 2026
c9faff2
fix: revert FrameDataModel base class to IdentityDataModel to avoid b…
Aug 13, 2026
c560a56
fix: export Frames in __all__ for import * support
Aug 13, 2026
8d99ccf
fix: ensure FrameDataModel inherits from BlobDataModel
Aug 14, 2026
83878fd
fix: cache Frames class in globals during lazy import
Aug 14, 2026
087f510
fix: address review comments on frame models and tests
Aug 14, 2026
b3d4e59
fix: address suppressed copilot review comments
Aug 15, 2026
d0beaec
fix: address review comments on FrameDataModel exception and Frames t…
Aug 15, 2026
b34b3cc
fix: address latest copilot review comments
Aug 15, 2026
a50b275
fix: address latest copilot review comments
Aug 15, 2026
401f06a
fix: address latest suppressed copilot review comments
Aug 15, 2026
413c708
fix: address latest review feedback on ImageDataCSV and Images __all__
Aug 15, 2026
a8bb224
fix: address latest review comments on docstrings, DataModels, Images…
Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion aperturedb/DataModels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -73,6 +73,8 @@ class PolygonDataModel(IdentityDataModel):
class FrameDataModel(IdentityDataModel):
"""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


Expand Down
2 changes: 2 additions & 0 deletions aperturedb/Entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions aperturedb/FrameDataCSV.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from aperturedb.ImageDataCSV import ImageDataCSV
from aperturedb.Query import ObjectType


class FrameDataCSV(ImageDataCSV):
"""
**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 {
"entity": {
ObjectType.FRAME.value: self.get_indexed_properties()
}
}
22 changes: 22 additions & 0 deletions aperturedb/Frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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)
4 changes: 2 additions & 2 deletions aperturedb/ImageDataCSV.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,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):

Expand Down Expand Up @@ -199,8 +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 = "AddImage"

def getitem(self, idx):
idx = self.df.index.start + idx

Expand Down
24 changes: 11 additions & 13 deletions aperturedb/Images.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1003,18 +1006,13 @@ def get_properties(self, prop_list: Iterable[str] = []) -> Dict[str, Any]:
return return_dictionary


class Frames(Images):
"""
**The python wrapper of frame images in ApertureDB.**
# Shim for backward compatibility
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}")

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)
2 changes: 2 additions & 0 deletions aperturedb/cli/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +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.FrameDataCSV import FrameDataCSV
from aperturedb.BBoxDataCSV import BBoxDataCSV
from aperturedb.EntityDataCSV import EntityDataCSV
from aperturedb.BlobDataCSV import BlobDataCSV
Expand All @@ -210,6 +211,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
Expand Down
15 changes: 15 additions & 0 deletions test/test_DataModels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from aperturedb.DataModels import FrameDataModel
from aperturedb.Query import ObjectType
from pydantic import ValidationError
import pytest

def test_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")
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
9 changes: 9 additions & 0 deletions test/test_Entities.py
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions test/test_FrameDataCSV.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import tempfile
import os
from aperturedb.FrameDataCSV import FrameDataCSV
from aperturedb.Query import ObjectType


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")

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}"

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)
26 changes: 26 additions & 0 deletions test/test_Frames.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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 == 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
19 changes: 19 additions & 0 deletions test/test_cli_ingest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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_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)
Loading