From 5389028a9ef5b7da67c7271b610e990acc702852 Mon Sep 17 00:00:00 2001 From: ad-claw000 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: {entity} ({matched:,}) ''' for prop, (matched, indexed, typ) in properties.items(): - table += f'{prop.strip()} {matched:,} {"Indexed" if indexed else "Unindexed"}, {typ}' + bg = colors["property_background"] + fg = colors["property_foreground"] + idx_str = "Indexed" if indexed else "Unindexed" + table += ( + f'' + f'{prop.strip()} ' + f'' + f'{matched:,} ' + f'' + f'{idx_str}, {typ}' + ) 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'{connection} ({matched:,})' + c_bg = colors["connection_background"] + c_fg = colors["connection_foreground"] + table += ( + '' + '{} ({:,})' + ).format(c_bg, connection, c_fg, connection, matched) if properties: for prop, (matched, indexed, typ) in properties.items(): - table += f'{prop.strip()} {matched:,} {"Indexed" if indexed else "Unindexed"}, {typ}' + cp_bg = colors["connection_property_background"] + cp_fg = colors["connection_property_foreground"] + idx_str = "Indexed" if indexed else "Unindexed" + table += ( + '' + '{} ' + '' + '{} ' + '' + '{}, {}' + ).format(cp_bg, cp_fg, prop.strip(), cp_bg, cp_fg, f"{matched:,}", cp_bg, cp_fg, idx_str, typ) 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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."""