From 5389028a9ef5b7da67c7271b610e990acc702852 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 18 May 2026 16:57:05 -0700
Subject: [PATCH 01/34] fix: update pre-commit CI to use python 3.12 (#676)

---
 .github/workflows/checks.yml                  |  2 +-
 .github/workflows/pr.yaml                     |  2 +-
 aperturedb/CSVWriter.py                       | 24 +++++++++----
 aperturedb/CommonLibrary.py                   | 16 ++++++---
 aperturedb/ParallelQuery.py                   |  3 +-
 aperturedb/ParallelQuerySet.py                |  6 ++--
 aperturedb/Query.py                           |  6 ++--
 aperturedb/Utils.py                           | 34 ++++++++++++++++---
 aperturedb/cli/configure.py                   |  3 +-
 examples/CelebADataKaggle.py                  |  8 ++++-
 .../loading_with_models/get_tl_embeddings.py  |  2 +-
 test/conftest.py                              |  5 ++-
 test/docker-compose.yml                       |  6 ++--
 test/run_test_container.sh                    |  7 +++-
 test/test_Datawizard.py                       |  2 +-
 test/test_Server.py                           |  8 +++--
 test/test_Stats.py                            |  6 ++--
 17 files changed, 106 insertions(+), 34 deletions(-)

diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index fcaf731b..70fe31c1 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -16,7 +16,7 @@ jobs:
 
     - uses: actions/setup-python@v3
       with:
-        python-version: '3.10'
+        python-version: '3.12'
 
     - uses: pre-commit/action@v3.0.1
 
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
index 284be9dc..6a0cc1e5 100644
--- a/.github/workflows/pr.yaml
+++ b/.github/workflows/pr.yaml
@@ -18,7 +18,7 @@ jobs:
     steps:
 
     - name: Cleanup previous run
-      run: docker run --rm -v ${{ github.workspace }}:/workspace alpine sh -c "rm -rf /workspace/test/aperturedb/db*"
+      run: docker run --rm -v ${{ github.workspace }}:/workspace alpine sh -c "rm -rf /workspace/test/aperturedb/db* /workspace/test/aperturedb/logs*"
       continue-on-error: true
 
     - uses: actions/checkout@v3
diff --git a/aperturedb/CSVWriter.py b/aperturedb/CSVWriter.py
index dd5721d4..38b6ed48 100644
--- a/aperturedb/CSVWriter.py
+++ b/aperturedb/CSVWriter.py
@@ -30,7 +30,9 @@ def convert_entity_data(input, entity_class: str, unique_key: Optional[str] = No
     df = pd.DataFrame(input)
     df.insert(0, 'EntityClass', entity_class)
     if unique_key:
-        assert unique_key in df.columns, f"unique_key {unique_key} not found in the input data"
+        assert unique_key in df.columns, (
+            f"unique_key {unique_key} not found in the input data"
+        )
         df[f"constraint_{unique_key}"] = df[unique_key]
     return df
 
@@ -66,7 +68,9 @@ def convert_image_data(input, source_column: str, source_type: Optional[str] = N
     """
     df = pd.DataFrame(input)
 
-    assert source_column in df.columns, f"source_column {source_column} not found in the input data"
+    assert source_column in df.columns, (
+        f"source_column {source_column} not found in the input data"
+    )
 
     if source_type is None:
         source_type = source_column
@@ -82,7 +86,9 @@ def convert_image_data(input, source_column: str, source_type: Optional[str] = N
         df.insert(0, source_type, df[source_column])
 
     if unique_key is not None:
-        assert unique_key in df.columns, f"unique_key {unique_key} not found in the input data"
+        assert unique_key in df.columns, (
+            f"unique_key {unique_key} not found in the input data"
+        )
         df[f"constraint_{unique_key}"] = df[unique_key]
 
     if format is not None:
@@ -140,11 +146,15 @@ def convert_connection_data(input,
 
     if source_column is None:
         source_column = source_property
-    assert source_column in df.columns, f"source_column {source_column} not found in the input data"
+    assert source_column in df.columns, (
+        f"source_column {source_column} not found in the input data"
+    )
 
     if destination_column is None:
         destination_column = destination_property
-    assert destination_column in df.columns, f"destination_column {destination_column} not found in the input data"
+    assert destination_column in df.columns, (
+        f"destination_column {destination_column} not found in the input data"
+    )
 
     df.insert(0, 'ConnectionClass', connection_class)
     df.insert(1, f"{source_class}@{source_property}", df[source_column])
@@ -152,7 +162,9 @@ def convert_connection_data(input,
               df[destination_column])
 
     if unique_key:
-        assert unique_key in df.columns, f"unique_key {unique_key} not found in the input data"
+        assert unique_key in df.columns, (
+            f"unique_key {unique_key} not found in the input data"
+        )
         df[f"constraint_{unique_key}"] = df[unique_key]
 
     return df
diff --git a/aperturedb/CommonLibrary.py b/aperturedb/CommonLibrary.py
index 2ffd665a..49b562a0 100644
--- a/aperturedb/CommonLibrary.py
+++ b/aperturedb/CommonLibrary.py
@@ -83,9 +83,15 @@ def _create_configuration_from_json(config: Union[Dict, str],
     clean_config = {k: v for k, v in config.items() if k != "password"}
 
     # These fields are required.
-    assert "host" in config, f"host is required in the configuration: {clean_config}"
-    assert "username" in config, f"username is required in the configuration: {clean_config}"
-    assert "password" in config, f"password is required in the configuration: {clean_config}"
+    assert "host" in config, (
+        f"host is required in the configuration: {clean_config}"
+    )
+    assert "username" in config, (
+        f"username is required in the configuration: {clean_config}"
+    )
+    assert "password" in config, (
+        f"password is required in the configuration: {clean_config}"
+    )
 
     # These fields have no default in the Configuration class.
     if 'port' not in config:
@@ -95,7 +101,9 @@ def _create_configuration_from_json(config: Union[Dict, str],
         config["name"] = name  # will overwrite the name in the config
 
     if name_required:
-        assert "name" in config, f"name is required in the configuration: {clean_config}"
+        assert "name" in config, (
+            f"name is required in the configuration: {clean_config}"
+        )
     elif 'name' not in config:
         config["name"] = "from_json"
 
diff --git a/aperturedb/ParallelQuery.py b/aperturedb/ParallelQuery.py
index 82cdfe68..31fb840e 100644
--- a/aperturedb/ParallelQuery.py
+++ b/aperturedb/ParallelQuery.py
@@ -308,7 +308,8 @@ def query(self, generator, batchsize: int = 1, numthreads: int = 4, stats: bool
                         f"Could not determine query structure from:\n{generator[0]}")
                     logger.error(type(generator[0]))
             logger.info(
-                f"Commands per query = {self.commands_per_query}, Blobs per query = {self.blobs_per_query}"
+                f"Commands per query = {self.commands_per_query}, "
+                f"Blobs per query = {self.blobs_per_query}"
             )
             self.batched_run(generator, batchsize, numthreads, stats)
 
diff --git a/aperturedb/ParallelQuerySet.py b/aperturedb/ParallelQuerySet.py
index 6f12e2c8..de1fb81b 100644
--- a/aperturedb/ParallelQuerySet.py
+++ b/aperturedb/ParallelQuerySet.py
@@ -152,11 +152,13 @@ def first_only_blobs(all_blobs, strike_list, set_nm):
             blobs_this_set = len(blob_filter(blob_set, [], i))
             expected_blobs = blobs_per_query[i] * batch_size
             logger.info(
-                f"Set {i}: Commands per query = {commands_per_query[i]}, Blobs per query = {blobs_per_query[i]}"
+                f"Set {i}: Commands per query = {commands_per_query[i]}, "
+                f"Blobs per query = {blobs_per_query[i]}"
             )
             if blobs_this_set != expected_blobs:
                 logger.error(
-                    f"Set {i}: Expected {expected_blobs} blobs, but filter is returning {blobs_this_set}"
+                    f"Set {i}: Expected {expected_blobs} blobs, "
+                    f"but filter is returning {blobs_this_set}"
                 )
 
             # now we determine if the executing set has a constraint
diff --git a/aperturedb/Query.py b/aperturedb/Query.py
index b067760e..60fe02de 100644
--- a/aperturedb/Query.py
+++ b/aperturedb/Query.py
@@ -87,8 +87,10 @@ def get_specific(obj: BaseModel) -> dict:
         start, stop  = obj.start, obj.stop
         if obj.range_type == RangeType.TIME:
             start, stop = int(start), int(stop)
-            start = f"{start//3600:0>2}:{start//60:0>2}:{start%60:0>2}"
-            stop = f"{stop//3600:0>2}:{stop//60:0>2}:{stop%60:0>2}"
+            start = "{:0>2}:{:0>2}:{:0>2}".format(
+                start // 3600, (start // 60) % 60, start % 60)
+            stop = "{:0>2}:{:0>2}:{:0>2}".format(
+                stop // 3600, (stop // 60) % 60, stop % 60)
         elif obj.range_type == RangeType.FRAME:
             start = int(obj.start)
             stop = int(obj.stop)
diff --git a/aperturedb/Utils.py b/aperturedb/Utils.py
index 8be817d0..c062e991 100644
--- a/aperturedb/Utils.py
+++ b/aperturedb/Utils.py
@@ -182,7 +182,17 @@ def visualize_schema(self, filename: str = None, format: str = "png") -> Source:
             <TR><TD BGCOLOR="{colors["entity_background"]}" COLSPAN="3"><FONT COLOR="{colors["entity_foreground"]}"><B>{entity}</B> ({matched:,})</FONT></TD></TR>
             '''
             for prop, (matched, indexed, typ) in properties.items():
-                table += f'<TR><TD BGCOLOR="{colors["property_background"]}"><FONT COLOR="{colors["property_foreground"]}"><B>{prop.strip()}</B></FONT></TD> <TD BGCOLOR="{colors["property_background"]}"><FONT COLOR="{colors["property_foreground"]}">{matched:,}</FONT></TD> <TD BGCOLOR="{colors["property_background"]}"><FONT COLOR="{colors["property_foreground"]}">{"Indexed" if indexed else "Unindexed"}, {typ}</FONT></TD></TR>'
+                bg = colors["property_background"]
+                fg = colors["property_foreground"]
+                idx_str = "Indexed" if indexed else "Unindexed"
+                table += (
+                    f'<TR><TD BGCOLOR="{bg}"><FONT COLOR="{fg}">'
+                    f'<B>{prop.strip()}</B></FONT></TD> '
+                    f'<TD BGCOLOR="{bg}"><FONT COLOR="{fg}">'
+                    f'{matched:,}</FONT></TD> '
+                    f'<TD BGCOLOR="{bg}"><FONT COLOR="{fg}">'
+                    f'{idx_str}, {typ}</FONT></TD></TR>'
+                )
             for connection, data in connections.items():
                 data_list = [data] if isinstance(data, dict) else data
                 for data in data_list:
@@ -190,10 +200,26 @@ def visualize_schema(self, filename: str = None, format: str = "png") -> Source:
                         matched = data["matched"]
                         # dictionary from name to (matched, indexed, type)
                         properties = data["properties"]
-                        table += f'<TR><TD BGCOLOR="{colors["connection_background"]}" COLSPAN="3" PORT="{connection}"><FONT COLOR="{colors["connection_foreground"]}"><B>{connection}</B> ({matched:,})</FONT></TD></TR>'
+                        c_bg = colors["connection_background"]
+                        c_fg = colors["connection_foreground"]
+                        table += (
+                            '<TR><TD BGCOLOR="{}" COLSPAN="3" '
+                            'PORT="{}"><FONT COLOR="{}">'
+                            '<B>{}</B> ({:,})</FONT></TD></TR>'
+                        ).format(c_bg, connection, c_fg, connection, matched)
                         if properties:
                             for prop, (matched, indexed, typ) in properties.items():
-                                table += f'<TR><TD BGCOLOR="{colors["connection_property_background"]}"><FONT COLOR="{colors["connection_property_foreground"]}"><B>{prop.strip()}</B></FONT></TD> <TD BGCOLOR="{colors["connection_property_background"]}"><FONT COLOR="{colors["connection_property_foreground"]}">{matched:,}</FONT></TD> <TD BGCOLOR="{colors["connection_property_background"]}"><FONT COLOR="{colors["connection_property_foreground"]}">{"Indexed" if indexed else "Unindexed"}, {typ}</FONT></TD></TR>'
+                                cp_bg = colors["connection_property_background"]
+                                cp_fg = colors["connection_property_foreground"]
+                                idx_str = "Indexed" if indexed else "Unindexed"
+                                table += (
+                                    '<TR><TD BGCOLOR="{}"><FONT COLOR="{}">'
+                                    '<B>{}</B></FONT></TD> '
+                                    '<TD BGCOLOR="{}"><FONT COLOR="{}">'
+                                    '{}</FONT></TD> '
+                                    '<TD BGCOLOR="{}"><FONT COLOR="{}">'
+                                    '{}, {}</FONT></TD></TR>'
+                                ).format(cp_bg, cp_fg, prop.strip(), cp_bg, cp_fg, f"{matched:,}", cp_bg, cp_fg, idx_str, typ)
 
             table += '</TABLE>>'
             dot.node(entity, label=table)
@@ -243,7 +269,7 @@ def _object_summary(self, name, object):
             w = "!" if "id" in k and not p[k][1] else w
             print(f"{i} {w} {p[k][2].ljust(8)} |"
                   f" {k.ljust(max)} | {str(p[k][0]).rjust(9)} "
-                  f"({int(p[k][0]/total_elements*100.0)}%)")
+                  f"({int(p[k][0] / total_elements * 100.0)}%)")
 
         return total_elements
 
diff --git a/aperturedb/cli/configure.py b/aperturedb/cli/configure.py
index c0ae58e4..5afd2ba6 100644
--- a/aperturedb/cli/configure.py
+++ b/aperturedb/cli/configure.py
@@ -193,7 +193,8 @@ def create(
     def check_for_overwrite(name):
         if name in configs and not overwrite:
             console.log(
-                f"Configuration named '{name}' already exists. Use --overwrite to overwrite.",
+                "Configuration named '{}' already exists. Use --overwrite to overwrite.".format(
+                    name),
                 style="bold yellow")
             raise typer.Exit(code=2)
 
diff --git a/examples/CelebADataKaggle.py b/examples/CelebADataKaggle.py
index a995e669..73195dd9 100644
--- a/examples/CelebADataKaggle.py
+++ b/examples/CelebADataKaggle.py
@@ -62,7 +62,13 @@ def generate_query(self, idx: int) -> Tuple[List[dict], List[bytes]]:
                 }
             }
         ]
-        q[0]["AddImage"]["properties"]["keypoints"] = f"10 {p['lefteye_x']} {p['lefteye_y']} {p['righteye_x']} {p['righteye_y']} {p['nose_x']} {p['nose_y']} {p['leftmouth_x']} {p['leftmouth_y']} {p['rightmouth_x']} {p['rightmouth_y']}"
+        q[0]["AddImage"]["properties"]["keypoints"] = (
+            f"10 {p['lefteye_x']} {p['lefteye_y']} "
+            f"{p['righteye_x']} {p['righteye_y']} "
+            f"{p['nose_x']} {p['nose_y']} "
+            f"{p['leftmouth_x']} {p['leftmouth_y']} "
+            f"{p['rightmouth_x']} {p['rightmouth_y']}"
+        )
 
         image_file_name = os.path.join(
             self.workdir,
diff --git a/examples/loading_with_models/get_tl_embeddings.py b/examples/loading_with_models/get_tl_embeddings.py
index 5ae5bd02..5b76149c 100644
--- a/examples/loading_with_models/get_tl_embeddings.py
+++ b/examples/loading_with_models/get_tl_embeddings.py
@@ -59,7 +59,7 @@ def generate_text_embeddings(text: str):
 
 print(f"Generated {len(embeddings)} embeddings for the video")
 for i, emb in enumerate(embeddings):
-    print(f"Embedding {i+1}:")
+    print(f"Embedding {i + 1}:")
     print(f"  Scope: {emb['embedding_scope']}")
     print(
         f"  Time range: {emb['start_offset_sec']} - {emb['end_offset_sec']} seconds")
diff --git a/test/conftest.py b/test/conftest.py
index f5144220..1a602d1a 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -145,7 +145,10 @@ def insert_data_from_csv(in_csv_file, rec_count=-1, expected_error_count=0, load
             for tp, classes in expected_indices.items():
                 for cls, props in classes.items():
                     for prop in props:
-                        err_msg = f"Index {prop} not found for {cls}, {expected_indices=}, {observed_indices=}"
+                        err_msg = (
+                            f"Index {prop} not found for {cls}, "
+                            f"{expected_indices=}, {observed_indices=}"
+                        )
                         assert prop in observed_indices[tp][cls], err_msg
         assert loader.error_counter == 0
         assert len(data) - \
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
index a1395ad1..2ae566a3 100644
--- a/test/docker-compose.yml
+++ b/test/docker-compose.yml
@@ -25,7 +25,7 @@ services:
         condition: service_started
     image: $LENZ_REPO:$LENZ_TAG
     ports:
-      - $GATEWAY:55556:55551
+      - "$GATEWAY:0:55551"
     restart: always
     environment:
       LNZ_HEALTH_PORT: 58085
@@ -65,8 +65,8 @@ services:
     image: nginx
     restart: always
     ports:
-      - $GATEWAY:8087:80
-      - $GATEWAY:8443:443
+      - "$GATEWAY:0:80"
+      - "$GATEWAY:0:443"
     configs:
       - source: nginx.conf
         target: /etc/nginx/conf.d/default.conf
diff --git a/test/run_test_container.sh b/test/run_test_container.sh
index 99e23725..eb2536c4 100755
--- a/test/run_test_container.sh
+++ b/test/run_test_container.sh
@@ -25,7 +25,12 @@ function run_aperturedb_instance(){
     docker network create ${TAG}_host_default
     GATEWAY=$(docker network inspect ${TAG}_host_default | jq -r .[0].IPAM.Config[0].Gateway)
     GATEWAY=$GATEWAY RUNNER_NAME=$TAG docker compose -f docker-compose.yml up -d
-    echo "$GATEWAY"
+    if [ "$TAG" == "${RUNNER_NAME}_http" ]; then
+        PORT=$(RUNNER_NAME=$TAG docker compose -f docker-compose.yml port nginx 80 | cut -d: -f2)
+    else
+        PORT=$(RUNNER_NAME=$TAG docker compose -f docker-compose.yml port lenz 55551 | cut -d: -f2)
+    fi
+    echo "$GATEWAY:$PORT"
 }
 
 IP_REGEX='[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}'
diff --git a/test/test_Datawizard.py b/test/test_Datawizard.py
index 932b60b9..f956e90c 100644
--- a/test/test_Datawizard.py
+++ b/test/test_Datawizard.py
@@ -123,7 +123,7 @@ def make_hand(side: Side) -> Hand:
 
     people = []
     for i in range(10):
-        person = Person(name=f"adam{i+1}")
+        person = Person(name=f"adam{i + 1}")
         left_hand = make_hand(Side.LEFT)
         right_hand = make_hand(Side.RIGHT)
         person.hands.extend([left_hand, right_hand])
diff --git a/test/test_Server.py b/test/test_Server.py
index 1ca18cda..38bcf3c7 100644
--- a/test/test_Server.py
+++ b/test/test_Server.py
@@ -62,8 +62,12 @@ def test_response_half_non_unique(a: Connector, query, blobs):
                             "entity": {"_Image": {"id"}}})
         input_data = pd.read_csv("./input/images.adb.csv")
         data, loader = insert_data_from_csv(
-            in_csv_file = "./input/images.adb.csv", expected_error_count = len(input_data))
-        assert loader.error_counter == 0, f"Error counter: {loader.error_counter=}"
+            in_csv_file="./input/images.adb.csv",
+            expected_error_count=len(input_data)
+        )
+        assert loader.error_counter == 0, (
+            f"Error counter: {loader.error_counter=}"
+        )
         assert loader.get_succeeded_queries(
         ) == 0, f"Queries: {loader.get_succeeded_queries()=}"
         assert loader.get_succeeded_commands(
diff --git a/test/test_Stats.py b/test/test_Stats.py
index 483c758a..e9785db8 100644
--- a/test/test_Stats.py
+++ b/test/test_Stats.py
@@ -31,8 +31,10 @@ def validate_stats(self, out, assertions):
                     first, second = line.split(":")
                     print(first, second)
                     if first in assertions:
-                        assert assertions[first.strip()](second.strip()) == True, \
-                            f"Assertion failed for '{first}' with value {second}"
+                        assert assertions[first.strip()](second.strip()) is True, (
+                            f"Assertion failed for '{first}' "
+                            f"with value {second}"
+                        )
 
     def test_stats_all_errors_non_equal_last_batch(self, db, utils):
         utils.remove_all_objects()

From 893a4ebfdb67a64b3880290d94af85252f2e5aad Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 18 May 2026 19:03:29 -0700
Subject: [PATCH 02/34] test: add initial suite of tests for Images.py (#674)

---
 test/test_Images.py | 116 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 116 insertions(+)
 create mode 100644 test/test_Images.py

diff --git a/test/test_Images.py b/test/test_Images.py
new file mode 100644
index 00000000..43e54dac
--- /dev/null
+++ b/test/test_Images.py
@@ -0,0 +1,116 @@
+import numpy as np
+from aperturedb.Images import Images, resolve, rotate
+from unittest.mock import patch
+
+
+def test_rotate():
+    points = np.array([(10, 10), (20, 20)])
+    rotated = rotate(points, 90, c_x=10, c_y=10)
+    assert len(rotated) == 2
+    assert rotated[0][0] == 10 and rotated[0][1] == 10
+    assert rotated[1][0] == 0 and rotated[1][1] == 20
+
+
+def test_resolve_resize():
+    points = np.array([[10, 10], [20, 20]], dtype=float)
+    meta = {"adb_image_width": 100, "adb_image_height": 100}
+    operations = [{"type": "resize", "width": 50, "height": 50}]
+    resolved = resolve(points, meta, operations)
+    assert resolved[0][0] == 5
+    assert resolved[0][1] == 5
+    assert resolved[1][0] == 10
+    assert resolved[1][1] == 10
+
+
+def test_resolve_rotate():
+    points = np.array([[10, 10]], dtype=float)
+    meta = {"adb_image_width": 100, "adb_image_height": 100}
+    operations = [{"type": "rotate", "angle": 90}]
+    resolved = resolve(points, meta, operations)
+    assert len(resolved) == 1
+    # Note: 9 instead of 10 due to float truncation in .astype(int)
+    assert resolved[0][0] == 90 and resolved[0][1] == 9
+
+
+class MockClient:
+    def __init__(self):
+        self.responses = []
+        self.queries = []
+
+    def query(self, q, blobs=None):
+        if blobs is None:
+            blobs = []
+        self.queries.append(q)
+        return self.responses.pop(0) if self.responses else ([{}], [])
+
+    def last_query_ok(self):
+        return True
+
+
+def test_Images_init():
+    client = MockClient()
+    img = Images(client)
+    assert img.client == client
+    assert img.db_object.value == "_Image"
+
+
+def test_Images_search():
+    client = MockClient()
+    with patch('aperturedb.Images.execute_query') as mock_execute:
+        mock_execute.return_value = (
+            0, [{"FindImage": {"entities": [{"_uniqueid": "123"}, {"_uniqueid": "456"}]}}], [])
+        img = Images(client)
+        img.search(limit=2)
+        assert "123" in img.images_ids
+        assert "456" in img.images_ids
+        mock_execute.assert_called_once()
+        query_passed = mock_execute.call_args[1][
+            "query"] if "query" in mock_execute.call_args[1] else mock_execute.call_args[0][1]
+        assert "FindImage" in query_passed[0]
+        assert query_passed[0]["FindImage"]["results"]["limit"] == 2
+
+
+def test_Images_search_by_property():
+    client = MockClient()
+    with patch('aperturedb.Images.execute_query') as mock_execute:
+        mock_execute.return_value = (
+            0, [{"FindImage": {"entities": [{"_uniqueid": "789"}]}}], [])
+        img = Images(client)
+        img.search_by_property("label", ["test_label"])
+        assert "789" in img.images_ids
+        query_passed = mock_execute.call_args[1][
+            "query"] if "query" in mock_execute.call_args[1] else mock_execute.call_args[0][1]
+        assert "constraints" in query_passed[0]["FindImage"]
+
+
+def test_Images_get_image_by_index():
+    client = MockClient()
+    img = Images(client)
+    img.images_ids = ["111"]
+
+    with patch('aperturedb.Images.execute_query') as mock_execute:
+        mock_execute.return_value = (0, [], [b'fakeimageblob'])
+        # Override last_query_ok since MockClient does that
+        client.last_query_ok = lambda: True
+
+        res = img.get_image_by_index(0)
+        assert res == b'fakeimageblob'
+        assert "111" in img.images
+
+
+def test_Images_get_np_image_by_index():
+    client = MockClient()
+    img = Images(client)
+    img.images_ids = ["111"]
+
+    with patch('aperturedb.Images.execute_query') as mock_execute:
+        # Create a small valid jpeg or png mock blob
+        import cv2
+        fake_np = np.zeros((10, 10, 3), dtype=np.uint8)
+        _, fake_blob = cv2.imencode('.jpg', fake_np)
+
+        mock_execute.return_value = (0, [], [fake_blob.tobytes()])
+        client.last_query_ok = lambda: True
+
+        res = img.get_np_image_by_index(0)
+        assert res.shape == (10, 10, 3)

From c1f511a4809575f03238428f6456266c258ba10e Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 09:03:07 +0000
Subject: [PATCH 03/34] feat: Add ConnectionPool class for thread-safe
 connections

    Closes #598.
    This implementation ports the ConnectionPool originally written for the workflows repository.
---
 aperturedb/ConnectionPool.py | 101 +++++++++++++++++++++++++++++++++++
 test/test_ConnectionPool.py  |  45 ++++++++++++++++
 2 files changed, 146 insertions(+)
 create mode 100644 aperturedb/ConnectionPool.py
 create mode 100644 test/test_ConnectionPool.py

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
new file mode 100644
index 00000000..0dd46264
--- /dev/null
+++ b/aperturedb/ConnectionPool.py
@@ -0,0 +1,101 @@
+import threading
+import queue
+from contextlib import contextmanager
+
+from aperturedb.CommonLibrary import create_connector
+
+
+class ConnectionPool:
+    """
+    A thread-safe connection pool for aperturedb.Connector.
+
+    This pool manages a fixed number of Connector instances, allowing multiple
+    threads to safely execute queries by borrowing and returning connections.
+    """
+
+    def __init__(self, pool_size: int = 10, connection_factory=create_connector):
+        """
+        Initializes the connection pool.
+
+        Args:
+            pool_size (int): The number of connections to keep in the pool.
+            connection_factory (callable): A factory function to create new connections.
+        """
+        if pool_size <= 0:
+            raise ValueError("Pool size must be greater than 0.")
+
+        self._pool_size = pool_size
+        self._connection_factory = connection_factory
+        # A thread-safe queue to hold the available connections
+        self._pool = queue.Queue(maxsize=pool_size)
+
+        # A lock to ensure the initial population is thread-safe, just in case.
+        self._lock = threading.Lock()
+
+        # Pre-populate the pool with connections
+        for _ in range(pool_size):
+            try:
+                connection = self._connection_factory()
+                if not connection:
+                    raise ConnectionError("Failed to create a connection.")
+                self._pool.put(connection)
+            except Exception as e:
+                print(f"Failed to create a connection for the pool: {e}")
+                # Depending on requirements, you might want to raise an error
+                # if the pool cannot be fully populated.
+
+        if self.available() == 0:
+            raise ConnectionError(
+                "Failed to initialize any connections for the pool. "
+                "Please check connection parameters and network."
+            )
+
+    def available(self) -> int:
+        """Returns the number of available connections in the pool."""
+        return self._pool.qsize()
+
+    def total(self) -> int:
+        """Returns the total number of connections in the pool."""
+        return self._pool_size
+
+    @contextmanager
+    def get_connection(self):
+        """
+        A context manager to get a connection from the pool.
+        This is the recommended way to use a connection.
+        It automatically gets a connection and releases it back to the pool.
+
+        Usage:
+            with pool.get_connection() as conn:
+                conn.query(...)
+        """
+        # The get() call will block until a connection is available.
+        connection = self._pool.get()
+        try:
+            # Yield the connection for the user to use
+            yield connection
+        finally:
+            # This block is guaranteed to execute, ensuring the connection
+            # is always returned to the pool.
+            self._pool.put(connection)
+
+    def query(self, query, blobs: list = None, **kwargs):
+        """
+        A convenience method to execute a query directly from the pool.
+
+        This method handles getting a connection, executing the query,
+        and returning the connection to the pool.
+
+        Args:
+            query: The query string or list to execute.
+            blobs (list): A list of blobs to include with the query.
+            **kwargs: Other arguments for the Connector's query method.
+
+        Returns:
+            Response from the executed query.
+            Blobs
+        """
+        if blobs is None:
+            blobs = []
+        with self.get_connection() as connection:
+            return connection.query(query, blobs, **kwargs)
diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
new file mode 100644
index 00000000..c8b04829
--- /dev/null
+++ b/test/test_ConnectionPool.py
@@ -0,0 +1,45 @@
+import unittest
+import threading
+from aperturedb.ConnectionPool import ConnectionPool
+
+class TestConnectionPool(unittest.TestCase):
+    def test_pool_initialization(self):
+        pool = ConnectionPool(pool_size=3)
+        self.assertEqual(pool.total(), 3)
+        self.assertEqual(pool.available(), 3)
+
+    def test_pool_borrow_return(self):
+        pool = ConnectionPool(pool_size=2)
+        with pool.get_connection() as conn:
+            self.assertEqual(pool.available(), 1)
+            # Test a simple query to ensure the connection is real
+            response, _ = conn.query([{"GetStatus": {}}])
+            self.assertTrue(isinstance(response, list))
+        
+        # Should be returned to the pool
+        self.assertEqual(pool.available(), 2)
+
+    def test_pool_convenience_query(self):
+        pool = ConnectionPool(pool_size=1)
+        response, blobs = pool.query([{"GetStatus": {}}])
+        self.assertTrue(isinstance(response, list))
+        self.assertTrue(isinstance(blobs, list))
+
+    def test_pool_concurrency(self):
+        pool = ConnectionPool(pool_size=5)
+        results = []
+        
+        def worker():
+            res, _ = pool.query([{"GetStatus": {}}])
+            results.append(res)
+            
+        threads = [threading.Thread(target=worker) for _ in range(10)]
+        for t in threads:
+            t.start()
+        for t in threads:
+            t.join()
+            
+        self.assertEqual(len(results), 10)
+
+if __name__ == '__main__':
+    unittest.main()

From ada265c828a78502990187d6e27113b56bf28174 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 09:32:35 +0000
Subject: [PATCH 04/34] style: run pre-commit autopep8

---
 test/test_ConnectionPool.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index c8b04829..bd41d3e4 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -2,6 +2,7 @@
 import threading
 from aperturedb.ConnectionPool import ConnectionPool
 
+
 class TestConnectionPool(unittest.TestCase):
     def test_pool_initialization(self):
         pool = ConnectionPool(pool_size=3)
@@ -15,7 +16,7 @@ def test_pool_borrow_return(self):
             # Test a simple query to ensure the connection is real
             response, _ = conn.query([{"GetStatus": {}}])
             self.assertTrue(isinstance(response, list))
-        
+
         # Should be returned to the pool
         self.assertEqual(pool.available(), 2)
 
@@ -28,18 +29,19 @@ def test_pool_convenience_query(self):
     def test_pool_concurrency(self):
         pool = ConnectionPool(pool_size=5)
         results = []
-        
+
         def worker():
             res, _ = pool.query([{"GetStatus": {}}])
             results.append(res)
-            
+
         threads = [threading.Thread(target=worker) for _ in range(10)]
         for t in threads:
             t.start()
         for t in threads:
             t.join()
-            
+
         self.assertEqual(len(results), 10)
 
+
 if __name__ == '__main__':
     unittest.main()

From b57c59bc10e76a4b72bdbd8907b1979d99674d4b Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 09:39:52 +0000
Subject: [PATCH 05/34] fix(tests): use create_connection factory for
 ConnectionPool tests

---
 test/test_ConnectionPool.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index bd41d3e4..eb45a867 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -3,14 +3,15 @@
 from aperturedb.ConnectionPool import ConnectionPool
 
 
-class TestConnectionPool(unittest.TestCase):
+from test_Command import TestCommand
+class TestConnectionPool(TestCommand):
     def test_pool_initialization(self):
-        pool = ConnectionPool(pool_size=3)
+        pool = ConnectionPool(pool_size=3, connection_factory=lambda: self.create_connection())
         self.assertEqual(pool.total(), 3)
         self.assertEqual(pool.available(), 3)
 
     def test_pool_borrow_return(self):
-        pool = ConnectionPool(pool_size=2)
+        pool = ConnectionPool(pool_size=2, connection_factory=lambda: self.create_connection())
         with pool.get_connection() as conn:
             self.assertEqual(pool.available(), 1)
             # Test a simple query to ensure the connection is real
@@ -21,13 +22,13 @@ def test_pool_borrow_return(self):
         self.assertEqual(pool.available(), 2)
 
     def test_pool_convenience_query(self):
-        pool = ConnectionPool(pool_size=1)
+        pool = ConnectionPool(pool_size=1, connection_factory=lambda: self.create_connection())
         response, blobs = pool.query([{"GetStatus": {}}])
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
     def test_pool_concurrency(self):
-        pool = ConnectionPool(pool_size=5)
+        pool = ConnectionPool(pool_size=5, connection_factory=lambda: self.create_connection())
         results = []
 
         def worker():

From cac3fdb085639f1b929b19be8614911270bfdd73 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 09:41:09 +0000
Subject: [PATCH 06/34] fix(tests): resolve pre-commit autopep8

---
 test/test_ConnectionPool.py | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index eb45a867..c62df3a7 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -4,14 +4,18 @@
 
 
 from test_Command import TestCommand
+
+
 class TestConnectionPool(TestCommand):
     def test_pool_initialization(self):
-        pool = ConnectionPool(pool_size=3, connection_factory=lambda: self.create_connection())
+        pool = ConnectionPool(
+            pool_size=3, connection_factory=lambda: self.create_connection())
         self.assertEqual(pool.total(), 3)
         self.assertEqual(pool.available(), 3)
 
     def test_pool_borrow_return(self):
-        pool = ConnectionPool(pool_size=2, connection_factory=lambda: self.create_connection())
+        pool = ConnectionPool(
+            pool_size=2, connection_factory=lambda: self.create_connection())
         with pool.get_connection() as conn:
             self.assertEqual(pool.available(), 1)
             # Test a simple query to ensure the connection is real
@@ -22,13 +26,15 @@ def test_pool_borrow_return(self):
         self.assertEqual(pool.available(), 2)
 
     def test_pool_convenience_query(self):
-        pool = ConnectionPool(pool_size=1, connection_factory=lambda: self.create_connection())
+        pool = ConnectionPool(
+            pool_size=1, connection_factory=lambda: self.create_connection())
         response, blobs = pool.query([{"GetStatus": {}}])
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
     def test_pool_concurrency(self):
-        pool = ConnectionPool(pool_size=5, connection_factory=lambda: self.create_connection())
+        pool = ConnectionPool(
+            pool_size=5, connection_factory=lambda: self.create_connection())
         results = []
 
         def worker():

From 4c3c7bb0828ab5a6f5760b3ef41b5a7e0bd12582 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 09:42:09 +0000
Subject: [PATCH 07/34] fix(tests): resolve failing ConnectionPool test setup

---
 test/test_ConnectionPool.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index c62df3a7..96bda59f 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -2,10 +2,8 @@
 import threading
 from aperturedb.ConnectionPool import ConnectionPool
 
-
 from test_Command import TestCommand
 
-
 class TestConnectionPool(TestCommand):
     def test_pool_initialization(self):
         pool = ConnectionPool(
@@ -38,8 +36,11 @@ def test_pool_concurrency(self):
         results = []
 
         def worker():
-            res, _ = pool.query([{"GetStatus": {}}])
-            results.append(res)
+            try:
+                res, _ = pool.query([{"GetStatus": {}}])
+                results.append(res)
+            except Exception as e:
+                print("Error in worker thread", e)
 
         threads = [threading.Thread(target=worker) for _ in range(10)]
         for t in threads:

From b8717e8dd7b250b2bd4a5f8d24a222e8428b502b Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 09:44:09 +0000
Subject: [PATCH 08/34] style: run pre-commit autopep8

---
 test/test_ConnectionPool.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 96bda59f..86c25392 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -4,6 +4,7 @@
 
 from test_Command import TestCommand
 
+
 class TestConnectionPool(TestCommand):
     def test_pool_initialization(self):
         pool = ConnectionPool(

From 5028cc125c63a4f29d8f05d013593d0b396a786e Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 12:49:58 +0000
Subject: [PATCH 09/34] test(ConnectionPool): remove invalid import and use
 unittest.TestCase

---
 test/test_ConnectionPool.py | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 86c25392..c473d567 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -1,20 +1,18 @@
 import unittest
 import threading
 from aperturedb.ConnectionPool import ConnectionPool
+from aperturedb.CommonLibrary import create_connector
 
-from test_Command import TestCommand
-
-
-class TestConnectionPool(TestCommand):
+class TestConnectionPool(unittest.TestCase):
     def test_pool_initialization(self):
         pool = ConnectionPool(
-            pool_size=3, connection_factory=lambda: self.create_connection())
+            pool_size=3, connection_factory=lambda: create_connector())
         self.assertEqual(pool.total(), 3)
         self.assertEqual(pool.available(), 3)
 
     def test_pool_borrow_return(self):
         pool = ConnectionPool(
-            pool_size=2, connection_factory=lambda: self.create_connection())
+            pool_size=2, connection_factory=lambda: create_connector())
         with pool.get_connection() as conn:
             self.assertEqual(pool.available(), 1)
             # Test a simple query to ensure the connection is real
@@ -26,14 +24,14 @@ def test_pool_borrow_return(self):
 
     def test_pool_convenience_query(self):
         pool = ConnectionPool(
-            pool_size=1, connection_factory=lambda: self.create_connection())
+            pool_size=1, connection_factory=lambda: create_connector())
         response, blobs = pool.query([{"GetStatus": {}}])
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
     def test_pool_concurrency(self):
         pool = ConnectionPool(
-            pool_size=5, connection_factory=lambda: self.create_connection())
+            pool_size=5, connection_factory=lambda: create_connector())
         results = []
 
         def worker():
@@ -51,6 +49,5 @@ def worker():
 
         self.assertEqual(len(results), 10)
 
-
 if __name__ == '__main__':
     unittest.main()

From 710414b777220e829253dd1792b940e0ad61e208 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 13:15:37 +0000
Subject: [PATCH 10/34] style: autopep8 fixes

---
 test/test_ConnectionPool.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index c473d567..e4cf3ee5 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -3,6 +3,7 @@
 from aperturedb.ConnectionPool import ConnectionPool
 from aperturedb.CommonLibrary import create_connector
 
+
 class TestConnectionPool(unittest.TestCase):
     def test_pool_initialization(self):
         pool = ConnectionPool(
@@ -49,5 +50,6 @@ def worker():
 
         self.assertEqual(len(results), 10)
 
+
 if __name__ == '__main__':
     unittest.main()

From 156805e013a944671212b8c3766d50b6371d9098 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Mon, 4 May 2026 16:45:27 +0000
Subject: [PATCH 11/34] style: autopep8 remaining test and example files

---
 aperturedb/Query.py          | 10 +++++-----
 examples/CelebADataKaggle.py |  2 +-
 test/test_Datawizard.py      |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/aperturedb/Query.py b/aperturedb/Query.py
index 60fe02de..01366260 100644
--- a/aperturedb/Query.py
+++ b/aperturedb/Query.py
@@ -84,7 +84,7 @@ def get_specific(obj: BaseModel) -> dict:
             RangeType.FRAME: "frame_number_range",
             RangeType.FRACTION: "time_fraction_range"
         }
-        start, stop  = obj.start, obj.stop
+        start, stop = obj.start, obj.stop
         if obj.range_type == RangeType.TIME:
             start, stop = int(start), int(stop)
             start = "{:0>2}:{:0>2}:{:0>2}".format(
@@ -94,7 +94,7 @@ def get_specific(obj: BaseModel) -> dict:
         elif obj.range_type == RangeType.FRAME:
             start = int(obj.start)
             stop = int(obj.stop)
-        return{
+        return {
             range_types[obj.range_type]: {
                 "start": start,
                 "stop": stop,
@@ -377,10 +377,10 @@ def spec(cls,
             operations=operations,
             with_class=with_class,
             limit=limit,
-            sort = sort,
-            list = list,
+            sort=sort,
+            list=list,
             blobs=blobs,
-            group_by_src = group_by_src,
+            group_by_src=group_by_src,
             set=set,
             vector=vector,
             k_neighbors=k_neighbors
diff --git a/examples/CelebADataKaggle.py b/examples/CelebADataKaggle.py
index 73195dd9..041897fb 100644
--- a/examples/CelebADataKaggle.py
+++ b/examples/CelebADataKaggle.py
@@ -15,7 +15,7 @@ class CelebADataKaggle(KaggleData):
 
     def __init__(self, **kwargs) -> None:
         self.records_count = -1
-        super().__init__(dataset_ref = "jessicali9530/celeba-dataset",
+        super().__init__(dataset_ref="jessicali9530/celeba-dataset",
                          records_count=self.records_count)
 
     def generate_index(self, root: str, records_count=-1) -> pd.DataFrame:
diff --git a/test/test_Datawizard.py b/test/test_Datawizard.py
index f956e90c..204b4e28 100644
--- a/test/test_Datawizard.py
+++ b/test/test_Datawizard.py
@@ -115,7 +115,7 @@ class Person(IdentityDataModel):
         dominant_hand: Hand = None
 
     def make_hand(side: Side) -> Hand:
-        hand = Hand(side = side, url= "input/images/0079.jpg")
+        hand = Hand(side=side, url="input/images/0079.jpg")
         hand.fingers = [Finger(nail_clean=True) if random.randint(
             0, 1) == 1 else Finger(nail_clean=False) for i in range(5)]
         hand.thumb = hand.fingers[0]

From b721facf2b65b92d274c95c9cf02e9b916bb5900 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Wed, 6 May 2026 00:45:47 +0000
Subject: [PATCH 12/34] test: add sleep to connection pool tests to reduce
 flakiness

---
 test/test_ConnectionPool.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index e4cf3ee5..786278c6 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -5,6 +5,10 @@
 
 
 class TestConnectionPool(unittest.TestCase):
+    def setUp(self):
+        import time
+        time.sleep(1) # Give server time to breathe between tests
+
     def test_pool_initialization(self):
         pool = ConnectionPool(
             pool_size=3, connection_factory=lambda: create_connector())

From d0df03f5569e5a68261c1f26030dbf9ab2833f41 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Wed, 6 May 2026 01:15:47 +0000
Subject: [PATCH 13/34] style: autopep8 test_ConnectionPool.py

---
 test/test_ConnectionPool.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 786278c6..12dcc9c6 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -7,7 +7,7 @@
 class TestConnectionPool(unittest.TestCase):
     def setUp(self):
         import time
-        time.sleep(1) # Give server time to breathe between tests
+        time.sleep(1)  # Give server time to breathe between tests
 
     def test_pool_initialization(self):
         pool = ConnectionPool(

From 823ddc7a17043c8198e3c1625d23af89d40553db Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 00:15:53 +0000
Subject: [PATCH 14/34] test: increase connection pool sleep interval

---
 test/test_ConnectionPool.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 12dcc9c6..4c81e8f9 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -7,7 +7,8 @@
 class TestConnectionPool(unittest.TestCase):
     def setUp(self):
         import time
-        time.sleep(1)  # Give server time to breathe between tests
+        time.sleep(2)  # Give server time to breathe between tests
+
 
     def test_pool_initialization(self):
         pool = ConnectionPool(

From b6f741480ef391b35deda72d4801e001eacdebb1 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 02:15:45 +0000
Subject: [PATCH 15/34] style: fix autopep8 empty line violation in
 test_ConnectionPool.py

---
 test/test_ConnectionPool.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 4c81e8f9..b22aa9ae 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -9,7 +9,6 @@ def setUp(self):
         import time
         time.sleep(2)  # Give server time to breathe between tests
 
-
     def test_pool_initialization(self):
         pool = ConnectionPool(
             pool_size=3, connection_factory=lambda: create_connector())

From 2f8761021e2e219cb9fdb62ba7659dac5cdec2b6 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 04:47:18 +0000
Subject: [PATCH 16/34] test: increase connection pool sleep interval

---
 test/test_ConnectionPool.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index b22aa9ae..5925a27a 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -7,7 +7,8 @@
 class TestConnectionPool(unittest.TestCase):
     def setUp(self):
         import time
-        time.sleep(2)  # Give server time to breathe between tests
+        time.sleep(3)  # Give server time to breathe between tests
+
 
     def test_pool_initialization(self):
         pool = ConnectionPool(

From b01f44ebd51e4e31c26e41136013f06259ad923d Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 05:15:43 +0000
Subject: [PATCH 17/34] style: fix autopep8 empty line violation in
 test_ConnectionPool.py

---
 test/test_ConnectionPool.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 5925a27a..031f267b 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -9,7 +9,6 @@ def setUp(self):
         import time
         time.sleep(3)  # Give server time to breathe between tests
 
-
     def test_pool_initialization(self):
         pool = ConnectionPool(
             pool_size=3, connection_factory=lambda: create_connector())

From 16a6069fc934d49e0cc4dfef75c0619260534236 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 06:17:31 +0000
Subject: [PATCH 18/34] test: further increase connection pool sleep interval

---
 test/test_ConnectionPool.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 031f267b..c8be94dd 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -7,7 +7,8 @@
 class TestConnectionPool(unittest.TestCase):
     def setUp(self):
         import time
-        time.sleep(3)  # Give server time to breathe between tests
+        time.sleep(5)  # Give server time to breathe between tests
+
 
     def test_pool_initialization(self):
         pool = ConnectionPool(

From 0a2913b9f7ce5112c7490c400a84a4315a9c0abc Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 06:54:59 +0000
Subject: [PATCH 19/34] style: fix autopep8 empty line violation in
 test_ConnectionPool.py

---
 test/test_ConnectionPool.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index c8be94dd..eb819a30 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -9,7 +9,6 @@ def setUp(self):
         import time
         time.sleep(5)  # Give server time to breathe between tests
 
-
     def test_pool_initialization(self):
         pool = ConnectionPool(
             pool_size=3, connection_factory=lambda: create_connector())

From 5bda15495d99592dfa0b4d18dac7ada16977ab08 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Thu, 7 May 2026 09:50:31 +0000
Subject: [PATCH 20/34] test: instantiate Connector using dbinfo for
 test_ConnectionPool

---
 test/test_ConnectionPool.py | 28 +++++++++++++++++++++++-----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index eb819a30..a11d9dad 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -1,7 +1,25 @@
 import unittest
 import threading
 from aperturedb.ConnectionPool import ConnectionPool
-from aperturedb.CommonLibrary import create_connector
+from aperturedb.Connector import Connector
+try:
+    from . import dbinfo
+except ImportError:
+    import dbinfo
+
+
+def test_make_connector():
+    return Connector(
+        host=dbinfo.DB_TCP_HOST,
+        port=dbinfo.DB_TCP_PORT,
+        user=dbinfo.DB_USER,
+        password=dbinfo.DB_PASSWORD,
+        use_ssl=True,
+        ca_cert=dbinfo.CA_CERT,
+        verify_hostname=dbinfo.VERIFY_HOSTNAME,
+        retry_max_attempts=3,
+        retry_interval_seconds=0
+    )
 
 
 class TestConnectionPool(unittest.TestCase):
@@ -11,13 +29,13 @@ def setUp(self):
 
     def test_pool_initialization(self):
         pool = ConnectionPool(
-            pool_size=3, connection_factory=lambda: create_connector())
+            pool_size=3, connection_factory=test_make_connector)
         self.assertEqual(pool.total(), 3)
         self.assertEqual(pool.available(), 3)
 
     def test_pool_borrow_return(self):
         pool = ConnectionPool(
-            pool_size=2, connection_factory=lambda: create_connector())
+            pool_size=2, connection_factory=test_make_connector)
         with pool.get_connection() as conn:
             self.assertEqual(pool.available(), 1)
             # Test a simple query to ensure the connection is real
@@ -29,14 +47,14 @@ def test_pool_borrow_return(self):
 
     def test_pool_convenience_query(self):
         pool = ConnectionPool(
-            pool_size=1, connection_factory=lambda: create_connector())
+            pool_size=1, connection_factory=test_make_connector)
         response, blobs = pool.query([{"GetStatus": {}}])
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
     def test_pool_concurrency(self):
         pool = ConnectionPool(
-            pool_size=5, connection_factory=lambda: create_connector())
+            pool_size=5, connection_factory=test_make_connector)
         results = []
 
         def worker():

From 4d0d51fdbbc7bfecff6a0cf1d65199fd025b715a Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 01:23:40 +0000
Subject: [PATCH 21/34] trigger ci


From 3757d30bc7913b1f08a719301062b003e0217008 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 01:31:43 +0000
Subject: [PATCH 22/34] fix(ConnectionPool): address PR review comments

---
 aperturedb/ConnectionPool.py | 35 ++++++++++++++++++++++++++---------
 test/test_ConnectionPool.py  | 13 ++++++-------
 2 files changed, 32 insertions(+), 16 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index 0dd46264..555b6d6b 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -1,9 +1,12 @@
 import threading
 import queue
+import logging
 from contextlib import contextmanager
 
 from aperturedb.CommonLibrary import create_connector
 
+logger = logging.getLogger(__name__)
+
 
 class ConnectionPool:
     """
@@ -33,16 +36,18 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
         self._lock = threading.Lock()
 
         # Pre-populate the pool with connections
+        created_count = 0
         for _ in range(pool_size):
             try:
                 connection = self._connection_factory()
                 if not connection:
                     raise ConnectionError("Failed to create a connection.")
                 self._pool.put(connection)
+                created_count += 1
             except Exception as e:
-                print(f"Failed to create a connection for the pool: {e}")
-                # Depending on requirements, you might want to raise an error
-                # if the pool cannot be fully populated.
+                logger.error(f"Failed to create a connection for the pool: {e}")
+
+        self._pool_size = created_count
 
         if self.available() == 0:
             raise ConnectionError(
@@ -74,12 +79,25 @@ def get_connection(self):
         try:
             # Yield the connection for the user to use
             yield connection
+            return_to_pool = True
+        except Exception:
+            return_to_pool = False
+            raise
         finally:
             # This block is guaranteed to execute, ensuring the connection
-            # is always returned to the pool.
-            self._pool.put(connection)
-
-    def query(self, query, blobs: list = None, **kwargs):
+            # is always returned to the pool unless an exception occurred.
+            if return_to_pool:
+                self._pool.put(connection)
+            else:
+                try:
+                    new_connection = self._connection_factory()
+                    self._pool.put(new_connection)
+                except Exception as e:
+                    logger.error(f"Failed to recreate connection for pool: {e}")
+                    # Reduce total pool size since connection could not be recreated
+                    self._pool_size -= 1
+
+    def query(self, query, blobs: list = None):
         """
         A convenience method to execute a query directly from the pool.
 
@@ -89,7 +107,6 @@ def query(self, query, blobs: list = None, **kwargs):
         Args:
             query: The query string or list to execute.
             blobs (list): A list of blobs to include with the query.
-            **kwargs: Other arguments for the Connector's query method.
 
         Returns:
             Response from the executed query.
@@ -98,4 +115,4 @@ def query(self, query, blobs: list = None, **kwargs):
         if blobs is None:
             blobs = []
         with self.get_connection() as connection:
-            return connection.query(query, blobs, **kwargs)
+            return connection.query(query, blobs)
diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index a11d9dad..7880906b 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -8,7 +8,7 @@
     import dbinfo
 
 
-def test_make_connector():
+def _make_connector():
     return Connector(
         host=dbinfo.DB_TCP_HOST,
         port=dbinfo.DB_TCP_PORT,
@@ -24,18 +24,17 @@ def test_make_connector():
 
 class TestConnectionPool(unittest.TestCase):
     def setUp(self):
-        import time
-        time.sleep(5)  # Give server time to breathe between tests
+        pass
 
     def test_pool_initialization(self):
         pool = ConnectionPool(
-            pool_size=3, connection_factory=test_make_connector)
+            pool_size=3, connection_factory=_make_connector)
         self.assertEqual(pool.total(), 3)
         self.assertEqual(pool.available(), 3)
 
     def test_pool_borrow_return(self):
         pool = ConnectionPool(
-            pool_size=2, connection_factory=test_make_connector)
+            pool_size=2, connection_factory=_make_connector)
         with pool.get_connection() as conn:
             self.assertEqual(pool.available(), 1)
             # Test a simple query to ensure the connection is real
@@ -47,14 +46,14 @@ def test_pool_borrow_return(self):
 
     def test_pool_convenience_query(self):
         pool = ConnectionPool(
-            pool_size=1, connection_factory=test_make_connector)
+            pool_size=1, connection_factory=_make_connector)
         response, blobs = pool.query([{"GetStatus": {}}])
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
     def test_pool_concurrency(self):
         pool = ConnectionPool(
-            pool_size=5, connection_factory=test_make_connector)
+            pool_size=5, connection_factory=_make_connector)
         results = []
 
         def worker():

From 6a6f537a2455e5819d1c88d26f9e0b59b0276abd Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 01:35:27 +0000
Subject: [PATCH 23/34] style: fix autopep8 violations in ConnectionPool

---
 aperturedb/ConnectionPool.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index 555b6d6b..6bc00863 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -45,7 +45,8 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
                 self._pool.put(connection)
                 created_count += 1
             except Exception as e:
-                logger.error(f"Failed to create a connection for the pool: {e}")
+                logger.error(
+                    f"Failed to create a connection for the pool: {e}")
 
         self._pool_size = created_count
 
@@ -93,7 +94,8 @@ def get_connection(self):
                     new_connection = self._connection_factory()
                     self._pool.put(new_connection)
                 except Exception as e:
-                    logger.error(f"Failed to recreate connection for pool: {e}")
+                    logger.error(
+                        f"Failed to recreate connection for pool: {e}")
                     # Reduce total pool size since connection could not be recreated
                     self._pool_size -= 1
 

From 8b788b232373f86cb3cff8f31ded07368c8d75f1 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 01:40:23 +0000
Subject: [PATCH 24/34] trigger ci


From 37417bf338b1c9d35048e211031eab07e5890318 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 02:28:27 +0000
Subject: [PATCH 25/34] style: revert unrelated formatting changes per review

---
 aperturedb/Query.py          | 10 +++++-----
 examples/CelebADataKaggle.py |  2 +-
 test/test_Datawizard.py      |  2 +-
 3 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/aperturedb/Query.py b/aperturedb/Query.py
index 01366260..60fe02de 100644
--- a/aperturedb/Query.py
+++ b/aperturedb/Query.py
@@ -84,7 +84,7 @@ def get_specific(obj: BaseModel) -> dict:
             RangeType.FRAME: "frame_number_range",
             RangeType.FRACTION: "time_fraction_range"
         }
-        start, stop = obj.start, obj.stop
+        start, stop  = obj.start, obj.stop
         if obj.range_type == RangeType.TIME:
             start, stop = int(start), int(stop)
             start = "{:0>2}:{:0>2}:{:0>2}".format(
@@ -94,7 +94,7 @@ def get_specific(obj: BaseModel) -> dict:
         elif obj.range_type == RangeType.FRAME:
             start = int(obj.start)
             stop = int(obj.stop)
-        return {
+        return{
             range_types[obj.range_type]: {
                 "start": start,
                 "stop": stop,
@@ -377,10 +377,10 @@ def spec(cls,
             operations=operations,
             with_class=with_class,
             limit=limit,
-            sort=sort,
-            list=list,
+            sort = sort,
+            list = list,
             blobs=blobs,
-            group_by_src=group_by_src,
+            group_by_src = group_by_src,
             set=set,
             vector=vector,
             k_neighbors=k_neighbors
diff --git a/examples/CelebADataKaggle.py b/examples/CelebADataKaggle.py
index 041897fb..73195dd9 100644
--- a/examples/CelebADataKaggle.py
+++ b/examples/CelebADataKaggle.py
@@ -15,7 +15,7 @@ class CelebADataKaggle(KaggleData):
 
     def __init__(self, **kwargs) -> None:
         self.records_count = -1
-        super().__init__(dataset_ref="jessicali9530/celeba-dataset",
+        super().__init__(dataset_ref = "jessicali9530/celeba-dataset",
                          records_count=self.records_count)
 
     def generate_index(self, root: str, records_count=-1) -> pd.DataFrame:
diff --git a/test/test_Datawizard.py b/test/test_Datawizard.py
index 204b4e28..f956e90c 100644
--- a/test/test_Datawizard.py
+++ b/test/test_Datawizard.py
@@ -115,7 +115,7 @@ class Person(IdentityDataModel):
         dominant_hand: Hand = None
 
     def make_hand(side: Side) -> Hand:
-        hand = Hand(side=side, url="input/images/0079.jpg")
+        hand = Hand(side = side, url= "input/images/0079.jpg")
         hand.fingers = [Finger(nail_clean=True) if random.randint(
             0, 1) == 1 else Finger(nail_clean=False) for i in range(5)]
         hand.thumb = hand.fingers[0]

From ac22acecc1f5c20b2b70ae32c6788408fe0672af Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 02:30:51 +0000
Subject: [PATCH 26/34] refactor: remove unused lock

---
 aperturedb/ConnectionPool.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index 6bc00863..a72957ca 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -32,9 +32,6 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
         # A thread-safe queue to hold the available connections
         self._pool = queue.Queue(maxsize=pool_size)
 
-        # A lock to ensure the initial population is thread-safe, just in case.
-        self._lock = threading.Lock()
-
         # Pre-populate the pool with connections
         created_count = 0
         for _ in range(pool_size):

From 4bca587bc3c5debde0ce7727216db4fca5dcaaa9 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 11:40:18 -0700
Subject: [PATCH 27/34] fix: Combined logger exception/warning (#684)

---
 aperturedb/Connector.py | 19 +++++++++----------
 1 file changed, 9 insertions(+), 10 deletions(-)

diff --git a/aperturedb/Connector.py b/aperturedb/Connector.py
index f21a0d67..a044f6f7 100644
--- a/aperturedb/Connector.py
+++ b/aperturedb/Connector.py
@@ -422,8 +422,7 @@ def _connect(self):
 
         except FileNotFoundError as e:
             logger.exception(
-                f"The certificate file does not exist: {self.config.ca_cert}")
-            logger.exception(
+                f"The certificate file does not exist: {self.config.ca_cert}\n"
                 f"You can use the ca_cert parameter to specify a custom CA certificate")
             assert False, "Certificate verification failed" + os.linesep + \
                 f"The ca certificate file does not exist: {self.config.ca_cert} " + os.linesep + \
@@ -500,24 +499,24 @@ def _query(self, query, blob_array = [], try_resume=True):
                 if tries != 0 or (self.last_query_timestamp is not None and
                                   (now - self.last_query_timestamp) <
                                   self.query_connection_error_suppression_delta):
-                    logger.exception(ssle)
                     logger.warning(
-                        f"SSL connection error on process {os.getpid()}")
+                        f"SSL connection error on process {os.getpid()}",
+                        exc_info=True)
             except ssl.SSLError as ssle:
                 # This can happen in a scenario where multiple
                 # processes might be accessing a single connection.
                 # The copy does not make usable connections.
-                logger.exception(ssle)
-                logger.warning(f"SSL error on process {os.getpid()}")
+                logger.warning(
+                    f"SSL error on process {os.getpid()}", exc_info=True)
             except OSError as ose:
-                logger.exception(ose)
-                logger.warning(f"OS error on process {os.getpid()}")
+                logger.warning(
+                    f"OS error on process {os.getpid()}", exc_info=True)
             except AttributeError as ae:
                 if self.connected:
                     # Only log if we got this while connected.
                     # else it is expected after unification of query/connect
-                    logger.exception(ae)
-                    logger.warning(f"Attribute error on process {os.getpid()}")
+                    logger.warning(
+                        f"Attribute error on process {os.getpid()}", exc_info=True)
 
             tries += 1
             # Do not log when trying for the first time.

From 72b21fb53854ea9aee6ac3d5e5c9e4952d1513b1 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 12:28:17 -0700
Subject: [PATCH 28/34] docs: fixup reference links (#685)

---
 examples/README.md                              | 6 +++---
 examples/loading_with_models/add_video_model.py | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/examples/README.md b/examples/README.md
index ec330409..d1cb72d1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -26,7 +26,7 @@ The following files are under *image_classification*
 | imagenet_classes.txt | The class labels for the outputs from alexnet | used by pytorch_classification.py |
 | prepare_aperturedb.py | Helper to download images from coco dataset, and load them into aperturedb | ``python prepare_aperturedb.py -images_count 100`` |
 | pytorch_classification.py | Pulls all images from aperturedb with a certain property set by prepare_aperturedb.py script , and classifies them using alexnet | ``python pytorch_classification.py`` |
-| pytorch_classification.ipynb | It does the same operation as ``pytorch_classification.py``. Also displays the classified images | Also available to read at [Aperturedb python documentation](https://docs.aperturedata.io/HowToGuides/Basic/pytorch_classification) |
+| pytorch_classification.ipynb | It does the same operation as ``pytorch_classification.py``. Also displays the classified images | Also available to read at [Aperturedb python documentation](https://docs.aperturedata.io/HowToGuides/Advanced/pytorch_classification) |
 
 ## Example 3: Similarity search using apertureDB
 
@@ -42,7 +42,7 @@ The following files are under *similarity_search*
 
 | File | Description | instructions |
 | -----| ------------| -----|
-| similarity_search.ipynb | A notebook with some sample code for describing similarity search using aperturedb | Also available to read at [Aperturedb documentation](https://docs.aperturedata.io/HowToGuides/Advanced/similarity_search)|
+| similarity_search.ipynb | A notebook with some sample code for describing similarity search using aperturedb | Also available to read at [Aperturedb documentation](https://docs.aperturedata.io/HowToGuides/Applications/similarity_search)|
 | facenet.py | Face Recognition using facenet and pytorch | Is invoked indirectly |
 | add_faces.py | A Script to load celebA dataset into aperturedb | ``python add_faces.py``|
 
@@ -62,4 +62,4 @@ The following files are under *loading_with_models*
 
 | File | Description | instructions |
 | -----| ------------| -----|
-| models.ipynb | A notebook with some sample code to add data using models | Also available to read at [Aperturedb model example](https://docs.aperturedata.io/HowToGuides/Advanced/models)|
+| models.ipynb | A notebook with some sample code to add data using models | Also available to read at [Aperturedb model example](https://docs.aperturedata.io/HowToGuides/Ingestion/DataModels)|
diff --git a/examples/loading_with_models/add_video_model.py b/examples/loading_with_models/add_video_model.py
index 5fc877a3..21108778 100644
--- a/examples/loading_with_models/add_video_model.py
+++ b/examples/loading_with_models/add_video_model.py
@@ -54,7 +54,7 @@ def save_video_details_to_aperturedb(URL: str, clips, collection):
 
 # Create a descriptor set
 # DS is a search space for descriptors added to it (some times called collections)
-# https://docs.aperturedata.io/HowToGuides/Advanced/similarity_search#descriptorsets-and-descriptors
+# https://docs.aperturedata.io/HowToGuides/Applications/similarity_search#descriptorsets-and-descriptors
 collection = DescriptorSetDataModel(
     name="marengo26", dimensions=len(clips[0]['embedding']))
 q, blobs, c = generate_add_query(collection)

From 3f8767f711b063937a0e6c3534db069dd5736391 Mon Sep 17 00:00:00 2001
From: ad-claw000 <claw000@aperturedata.io>
Date: Tue, 19 May 2026 13:25:18 -0700
Subject: [PATCH 29/34] feat(connector): add boolean parameter to connect at
 construction (#681)

---
 aperturedb/Connector.py |  6 +++++-
 test/test_Session.py    | 25 +++++++++++++++++++++++++
 2 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/aperturedb/Connector.py b/aperturedb/Connector.py
index a044f6f7..149a360c 100644
--- a/aperturedb/Connector.py
+++ b/aperturedb/Connector.py
@@ -153,7 +153,8 @@ def __init__(self, host="localhost", port=DEFAULT_PORT,
                  retry_interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS,
                  retry_max_attempts=DEFAULT_RETRY_MAX_ATTEMPTS,
                  config: Optional[Configuration] = None,
-                 key: Optional[str] = None):
+                 key: Optional[str] = None,
+                 connect: bool = False):
         """
         Constructor for the Connector class.
         """
@@ -210,6 +211,9 @@ def __init__(self, host="localhost", port=DEFAULT_PORT,
         # to prevent logging of connection errors on first connect.
         self._ever_connected = False
 
+        if connect:
+            self.connect()
+
     def authenticate(self, shared_data, user, password, token):
         """
         Authenticate with the database. This will be called automatically from query.
diff --git a/test/test_Session.py b/test/test_Session.py
index 91ae6a44..d56aae54 100644
--- a/test/test_Session.py
+++ b/test/test_Session.py
@@ -265,3 +265,28 @@ def mock_refresher(query, blobs=[], try_resume=True):
         resp, blobs = db.query([{'GetSchema': {}}], [])
         print(f"{resp=}")
         assert enable_mock == False, "Authentication query was not invoked"
+
+    def test_connect_at_construction(self, monkeypatch):
+        # We want to test that connect() is called when connect=True.
+        # mock connect so it doesn't fail on missing server
+        def mock_connect(self):
+            self.connected = True
+        monkeypatch.setattr(Connector, "connect", mock_connect)
+
+        new_db = Connector(
+            dbinfo.DB_TCP_HOST,
+            dbinfo.DB_TCP_PORT,
+            dbinfo.DB_USER,
+            dbinfo.DB_PASSWORD,
+            ca_cert=dbinfo.CA_CERT,
+            connect=True)
+        assert getattr(new_db, "connected", False) == True
+
+        # also test default behavior
+        new_db_lazy = Connector(
+            dbinfo.DB_TCP_HOST,
+            dbinfo.DB_TCP_PORT,
+            dbinfo.DB_USER,
+            dbinfo.DB_PASSWORD,
+            ca_cert=dbinfo.CA_CERT)
+        assert getattr(new_db_lazy, "connected", False) == False

From 5f44696d77a400f9b0863e8ebf91c68f6bd6b26e Mon Sep 17 00:00:00 2001
From: claw <claw000@aperturedata.io>
Date: Tue, 19 May 2026 23:19:07 +0000
Subject: [PATCH 30/34] fix(ConnectionPool): Address PR review comments

---
 aperturedb/ConnectionPool.py | 54 +++++++++++++++++-------------------
 test/test_ConnectionPool.py  | 15 +++++++++-
 2 files changed, 40 insertions(+), 29 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index a72957ca..e084e9bf 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -33,26 +33,23 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
         self._pool = queue.Queue(maxsize=pool_size)
 
         # Pre-populate the pool with connections
-        created_count = 0
+        last_error = None
         for _ in range(pool_size):
             try:
                 connection = self._connection_factory()
                 if not connection:
                     raise ConnectionError("Failed to create a connection.")
                 self._pool.put(connection)
-                created_count += 1
             except Exception as e:
                 logger.error(
                     f"Failed to create a connection for the pool: {e}")
+                last_error = e
+                break
 
-        self._pool_size = created_count
-
-        if self.available() == 0:
+        if self._pool.qsize() < pool_size:
             raise ConnectionError(
-                "Failed to initialize any connections for the pool. "
-                "Please check connection parameters and network."
-            )
-
+                f"Failed to initialize pool: expected {pool_size} connections, got {self._pool.qsize()}."
+            ) from last_error
     def available(self) -> int:
         """Returns the number of available connections in the pool."""
         return self._pool.qsize()
@@ -62,40 +59,31 @@ def total(self) -> int:
         return self._pool_size
 
     @contextmanager
-    def get_connection(self):
+    def get_connection(self, timeout=None):
         """
         A context manager to get a connection from the pool.
         This is the recommended way to use a connection.
         It automatically gets a connection and releases it back to the pool.
 
+        Args:
+            timeout (float, optional): How long to wait for a connection to become available before raising TimeoutError.
+
         Usage:
             with pool.get_connection() as conn:
                 conn.query(...)
         """
-        # The get() call will block until a connection is available.
-        connection = self._pool.get()
+        try:
+            connection = self._pool.get(timeout=timeout)
+        except queue.Empty:
+            raise TimeoutError(f"No connection available in the pool within the specified timeout ({timeout}s).")
+            
         try:
             # Yield the connection for the user to use
             yield connection
-            return_to_pool = True
-        except Exception:
-            return_to_pool = False
-            raise
         finally:
             # This block is guaranteed to execute, ensuring the connection
             # is always returned to the pool unless an exception occurred.
-            if return_to_pool:
-                self._pool.put(connection)
-            else:
-                try:
-                    new_connection = self._connection_factory()
-                    self._pool.put(new_connection)
-                except Exception as e:
-                    logger.error(
-                        f"Failed to recreate connection for pool: {e}")
-                    # Reduce total pool size since connection could not be recreated
-                    self._pool_size -= 1
-
+            self._pool.put(connection)
     def query(self, query, blobs: list = None):
         """
         A convenience method to execute a query directly from the pool.
@@ -115,3 +103,13 @@ def query(self, query, blobs: list = None):
             blobs = []
         with self.get_connection() as connection:
             return connection.query(query, blobs)
+
+    def close(self):
+        """
+        Closes all connections in the pool.
+        """
+        while not self._pool.empty():
+            try:
+                self._pool.get_nowait()
+            except queue.Empty:
+                break
diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index 7880906b..a1c44a3c 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -51,6 +51,11 @@ def test_pool_convenience_query(self):
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
+    def test_pool_concurrency(self):
+        pool = ConnectionPool(
+            pool_size=5, connection_factory=_make_connector)
+        results = []
+
     def test_pool_concurrency(self):
         pool = ConnectionPool(
             pool_size=5, connection_factory=_make_connector)
@@ -61,7 +66,7 @@ def worker():
                 res, _ = pool.query([{"GetStatus": {}}])
                 results.append(res)
             except Exception as e:
-                print("Error in worker thread", e)
+                results.append(e)
 
         threads = [threading.Thread(target=worker) for _ in range(10)]
         for t in threads:
@@ -70,7 +75,15 @@ def worker():
             t.join()
 
         self.assertEqual(len(results), 10)
+        for r in results:
+            self.assertTrue(isinstance(r, list), f"Worker failed with exception: {r}")
 
+    def test_pool_timeout(self):
+        pool = ConnectionPool(pool_size=1, connection_factory=_make_connector)
+        with pool.get_connection():
+            with self.assertRaises(TimeoutError):
+                with pool.get_connection(timeout=0.1):
+                    pass
 
 if __name__ == '__main__':
     unittest.main()

From 39c70f6faa08e4db8eba2c47ef7044de8aff40b5 Mon Sep 17 00:00:00 2001
From: claw <claw000@aperturedata.io>
Date: Tue, 19 May 2026 23:23:38 +0000
Subject: [PATCH 31/34] style: pre-commit autopep8 fixes

---
 aperturedb/ConnectionPool.py | 10 +++++++---
 test/test_ConnectionPool.py  |  4 +++-
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index e084e9bf..7121cc31 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -48,8 +48,10 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
 
         if self._pool.qsize() < pool_size:
             raise ConnectionError(
-                f"Failed to initialize pool: expected {pool_size} connections, got {self._pool.qsize()}."
+                f"Failed to initialize pool: expected {
+                    pool_size} connections, got {self._pool.qsize()}."
             ) from last_error
+
     def available(self) -> int:
         """Returns the number of available connections in the pool."""
         return self._pool.qsize()
@@ -75,8 +77,9 @@ def get_connection(self, timeout=None):
         try:
             connection = self._pool.get(timeout=timeout)
         except queue.Empty:
-            raise TimeoutError(f"No connection available in the pool within the specified timeout ({timeout}s).")
-            
+            raise TimeoutError(
+                f"No connection available in the pool within the specified timeout ({timeout}s).")
+
         try:
             # Yield the connection for the user to use
             yield connection
@@ -84,6 +87,7 @@ def get_connection(self, timeout=None):
             # This block is guaranteed to execute, ensuring the connection
             # is always returned to the pool unless an exception occurred.
             self._pool.put(connection)
+
     def query(self, query, blobs: list = None):
         """
         A convenience method to execute a query directly from the pool.
diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index a1c44a3c..f8584511 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -76,7 +76,8 @@ def worker():
 
         self.assertEqual(len(results), 10)
         for r in results:
-            self.assertTrue(isinstance(r, list), f"Worker failed with exception: {r}")
+            self.assertTrue(isinstance(r, list),
+                            f"Worker failed with exception: {r}")
 
     def test_pool_timeout(self):
         pool = ConnectionPool(pool_size=1, connection_factory=_make_connector)
@@ -85,5 +86,6 @@ def test_pool_timeout(self):
                 with pool.get_connection(timeout=0.1):
                     pass
 
+
 if __name__ == '__main__':
     unittest.main()

From 09b3b08261e825a6f106f4cc6ea1968c7e5fb82c Mon Sep 17 00:00:00 2001
From: claw <claw000@aperturedata.io>
Date: Wed, 20 May 2026 08:03:35 +0000
Subject: [PATCH 32/34] fix: address review comments on ConnectionPool - Fix
 f-string syntax error across lines - Drain and close created connections if
 initialization fails - Add note about connection health responsibility on
 exception - Invoke close() on Connector instances when draining pool - Remove
 duplicate test definition

---
 aperturedb/ConnectionPool.py | 19 ++++++++++++++++---
 test/test_ConnectionPool.py  |  5 -----
 2 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index 7121cc31..eff70a1f 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -47,9 +47,16 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
                 break
 
         if self._pool.qsize() < pool_size:
+            # Drain any successfully created connections to prevent leaks
+            while not self._pool.empty():
+                try:
+                    conn = self._pool.get_nowait()
+                    if hasattr(conn, 'close'):
+                        conn.close()
+                except queue.Empty:
+                    break
             raise ConnectionError(
-                f"Failed to initialize pool: expected {
-                    pool_size} connections, got {self._pool.qsize()}."
+                f"Failed to initialize pool: expected {pool_size} connections, got {self._pool.qsize()}."
             ) from last_error
 
     def available(self) -> int:
@@ -70,6 +77,10 @@ def get_connection(self, timeout=None):
         Args:
             timeout (float, optional): How long to wait for a connection to become available before raising TimeoutError.
 
+        Note:
+            Callers are responsible for the connection's health. If an exception occurs
+            within the context manager, the connection is still returned to the pool.
+
         Usage:
             with pool.get_connection() as conn:
                 conn.query(...)
@@ -114,6 +125,8 @@ def close(self):
         """
         while not self._pool.empty():
             try:
-                self._pool.get_nowait()
+                conn = self._pool.get_nowait()
+                if hasattr(conn, 'close'):
+                    conn.close()
             except queue.Empty:
                 break
diff --git a/test/test_ConnectionPool.py b/test/test_ConnectionPool.py
index f8584511..3f42094b 100644
--- a/test/test_ConnectionPool.py
+++ b/test/test_ConnectionPool.py
@@ -51,11 +51,6 @@ def test_pool_convenience_query(self):
         self.assertTrue(isinstance(response, list))
         self.assertTrue(isinstance(blobs, list))
 
-    def test_pool_concurrency(self):
-        pool = ConnectionPool(
-            pool_size=5, connection_factory=_make_connector)
-        results = []
-
     def test_pool_concurrency(self):
         pool = ConnectionPool(
             pool_size=5, connection_factory=_make_connector)

From f632f747e997cb67173145f1799d133603e31bbd Mon Sep 17 00:00:00 2001
From: claw <claw000@aperturedata.io>
Date: Wed, 20 May 2026 08:15:41 +0000
Subject: [PATCH 33/34] Fix pre-commit issues

---
 aperturedb/ConnectionPool.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index eff70a1f..a395a8e5 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -56,7 +56,8 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
                 except queue.Empty:
                     break
             raise ConnectionError(
-                f"Failed to initialize pool: expected {pool_size} connections, got {self._pool.qsize()}."
+                f"Failed to initialize pool: expected {
+                    pool_size} connections, got {self._pool.qsize()}."
             ) from last_error
 
     def available(self) -> int:

From a3bd5006e275e55e2028e3a3de85711e8216ac9d Mon Sep 17 00:00:00 2001
From: claw <claw000@aperturedata.io>
Date: Wed, 20 May 2026 08:35:41 +0000
Subject: [PATCH 34/34] fix: report correct pool size and fix string formatting
 when draining

---
 aperturedb/ConnectionPool.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/aperturedb/ConnectionPool.py b/aperturedb/ConnectionPool.py
index a395a8e5..3e87741b 100644
--- a/aperturedb/ConnectionPool.py
+++ b/aperturedb/ConnectionPool.py
@@ -47,6 +47,7 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
                 break
 
         if self._pool.qsize() < pool_size:
+            created = self._pool.qsize()
             # Drain any successfully created connections to prevent leaks
             while not self._pool.empty():
                 try:
@@ -55,10 +56,11 @@ def __init__(self, pool_size: int = 10, connection_factory=create_connector):
                         conn.close()
                 except queue.Empty:
                     break
-            raise ConnectionError(
-                f"Failed to initialize pool: expected {
-                    pool_size} connections, got {self._pool.qsize()}."
-            ) from last_error
+            msg = (
+                f"Failed to initialize pool: expected {pool_size} "
+                f"connections, got {created}."
+            )
+            raise ConnectionError(msg) from last_error
 
     def available(self) -> int:
         """Returns the number of available connections in the pool."""
