Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
118e8e0
style: fix autopep8 formatting issues
Aug 15, 2026
3a92a2f
Address Copilot feedback: add __all__ to Images.py and remove unused …
Aug 15, 2026
a576464
fix: address final review feedback on Images.__all__ and test names/a…
Aug 15, 2026
f0dea51
feat: add Frame to Object Model, analogous to Image and Video
Aug 15, 2026
974efc7
fix: remove annotations from __all__ in Images.py
Aug 15, 2026
95ba433
fix: ensure FrameDataModel inherits from BlobDataModel properly
Aug 15, 2026
c007003
fix: ignore rm failure during teardown to avoid failing CI
Aug 16, 2026
8abf9b1
fix: address suppressed review comments on Images.__all__, Constants,…
Aug 16, 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
2 changes: 1 addition & 1 deletion aperturedb/CommonLibrary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions aperturedb/Constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Shared constants for the ApertureDB Python SDK.
"""

BLOB_ADD_COMMANDS = [
"AddImage",
"AddDescriptor",
"AddVideo",
"AddBlob",
"AddFrame"
]

BLOB_FIND_COMMANDS = [
"FindImage",
"FindDescriptor",
"FindVideo",
"FindBlob",
"FindFrame"
]
4 changes: 2 additions & 2 deletions 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 @@ -70,7 +70,7 @@ class PolygonDataModel(IdentityDataModel):
type = ObjectType.POLYGON


class FrameDataModel(IdentityDataModel):
class FrameDataModel(BlobDataModel):
"""Frame data model for ApertureDB.
"""
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)
3 changes: 1 addition & 2 deletions aperturedb/ImageDataCSV.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ 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 +200,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
27 changes: 13 additions & 14 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,14 @@ 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.**

Frames in ApertureDB are quite similar to images and so
are modeled in python as a subclass.

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

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)
def __dir__():
return sorted(set(list(globals().keys()) + ["Frames"]))
2 changes: 1 addition & 1 deletion aperturedb/MLCroissant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions aperturedb/PyTorchDataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion aperturedb/Query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions aperturedb/TensorFlowDataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
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
3 changes: 2 additions & 1 deletion aperturedb/transformers/clip_pytorch_embeddings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from aperturedb.Constants import BLOB_ADD_COMMANDS
import hashlib
import logging
from aperturedb.Subscriptable import Subscriptable
Expand Down Expand Up @@ -95,7 +96,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 BLOB_ADD_COMMANDS:
blob_index += 1

x[0].extend(new_descriptors)
Expand Down
2 changes: 1 addition & 1 deletion aperturedb/transformers/common_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion aperturedb/transformers/facenet_pytorch_embeddings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from aperturedb.Constants import BLOB_ADD_COMMANDS
import hashlib
import logging
from aperturedb.Subscriptable import Subscriptable
Expand Down Expand Up @@ -100,7 +101,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 BLOB_ADD_COMMANDS:
blob_index += 1

x[0].extend(new_descriptors)
Expand Down
5 changes: 3 additions & 2 deletions aperturedb/transformers/image_properties.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from aperturedb.Constants import BLOB_ADD_COMMANDS
from aperturedb.transformers.transformer import Transformer
from aperturedb.Subscriptable import Subscriptable

Expand Down Expand Up @@ -34,7 +35,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 BLOB_ADD_COMMANDS:
if blob_index >= len(x[1]):
logger.warning(
"Missing blob for command %s (expected at index %d), stopping property processing for this transaction.",
Expand Down Expand Up @@ -66,7 +67,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 BLOB_ADD_COMMANDS:
blob_index += 1

return x
3 changes: 2 additions & 1 deletion aperturedb/transformers/transformer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from aperturedb.Constants import BLOB_ADD_COMMANDS
from aperturedb.Subscriptable import Subscriptable
from aperturedb.CommonLibrary import create_connector
from aperturedb.Utils import Utils
Expand Down Expand Up @@ -67,7 +68,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 BLOB_ADD_COMMANDS:
self._blob_index.append(i)
bc += 1
# Kept for backward compatibility
Expand Down
5 changes: 3 additions & 2 deletions aperturedb/transformers/video_properties.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from aperturedb.Constants import BLOB_ADD_COMMANDS
from aperturedb.transformers.transformer import Transformer
from aperturedb.Subscriptable import Subscriptable

Expand Down Expand Up @@ -32,7 +33,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 BLOB_ADD_COMMANDS:
if blob_index >= len(x[1]):
logger.warning(
"Missing blob for command %s (expected at index %d), stopping property processing for this transaction.",
Expand All @@ -59,7 +60,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 BLOB_ADD_COMMANDS:
blob_index += 1

return x
4 changes: 2 additions & 2 deletions test/run_test_container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function teardown() {
docker network rm "${RUNNER_NAME}_non_http_default" || true
fi
echo "Cleaning up generated volumes..."
$(get_sudo) rm -rf "${SCRIPT_DIR}/aperturedb"
$(get_sudo) rm -rf "${SCRIPT_DIR}/aperturedb" || true
}
trap teardown EXIT

Expand Down Expand Up @@ -216,4 +216,4 @@ fi

echo "Tests completed"
echo " --- Runner name: ${RUNNER_NAME} ---"
check_containers_networks
check_containers_networks || true
16 changes: 16 additions & 0 deletions test/test_DataModels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pydantic import ValidationError
from aperturedb.DataModels import FrameDataModel
from aperturedb.Query import ObjectType
import pytest


def test_FrameDataModel():
# 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")
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


def test_load_entities_registry_frames():
registry = load_entities_registry()
assert ObjectType.FRAME.value in registry
assert registry[ObjectType.FRAME.value] is Frames
Loading
Loading