Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ All notable changes to this project will be documented in this file.
The format is based on the `Keep a Changelog <https://keepachangelog.com/en/1.1.0/>`__,
and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`__.

1.0.5 - 2026-10-01
------------------

Added
~~~~~

- Added configurable HTTP request timeouts through ``Configuration.timeout``,
supporting both total timeouts and separate connect/read timeouts.
- Added direct iteration support for query, paged, and vector collection
responses.
- Added configurable retry-delay limits for transient HTTP 429 responses,
including support for server-provided ``Retry-After`` values.

Fixed
~~~~~

- Improved upsert error messages for missing or oversized vector IDs, including guidance on enabling automatic ID generation.
- Removed REST endpoint values from being echoed in URL validation errors.

1.0.4 - 2026-09-21
------------------

Expand Down
71 changes: 27 additions & 44 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@ SHELL := /bin/bash
# ==============================================================================
# 1. CONFIGURATION
# ==============================================================================
# Prefer the platform's explicit Python 3 executable. Some environments do
# not provide an unversioned `python` command.
PYTHON = python
# Prefer the repository virtual environment when available, then fall back to
# the platform's Python 3 executable. The variable remains overridable with
# `make PYTHON=/path/to/python ...`.
PYTHON ?= $(shell if test -x .venv310/bin/python; then printf '%s' .venv310/bin/python; elif test -x .venv/bin/python; then printf '%s' .venv/bin/python; elif command -v python3 >/dev/null 2>&1; then command -v python3; else printf '%s' python; fi)
SOURCE_DIR = src
TARGET_DIRS = $(SOURCE_DIR) tests examples
DOCS_DIR = docs
DOCS_BUILD_DIR = $(DOCS_DIR)/build
DOC_ZIP_PREFIX = $(SDK_NAME)-api-ref

# Optional flag to enable report generation. Use 'gmake <target> REPORT=1'
REPORT ?=
INTEGRATION_TEST_WORKERS ?= 10
Expand All @@ -45,11 +42,11 @@ SECURITY_REPORT_DIR := $(REPORT_DIR)/security

# Tools
PYTEST = $(PYTHON) -m pytest
BLACK = black
FLAKE8 = flake8
MYPY = mypy
TOX = tox
BANDIT = bandit
BLACK = $(PYTHON) -m black
FLAKE8 = $(PYTHON) -m flake8
MYPY = $(PYTHON) -m mypy
TOX = $(PYTHON) -m tox
BANDIT = $(PYTHON) -m bandit

# Build command
BUILD = $(PYTHON) -m build
Expand All @@ -64,12 +61,7 @@ COVERAGE_XML_FLAG := $(if $(REPORT), --cov-report=xml:$(COVERAGE_REPORT_DIR)/cov
COVERAGE_HTML_FLAG := $(if $(REPORT), --cov-report=html:$(COVERAGE_REPORT_DIR)/html,)
BANDIT_REPORT_FLAG := $(if $(REPORT), -f json -o $(SECURITY_REPORT_DIR)/bandit_report.json,)

SDK_NAME := $(shell $(PYTHON) -c "import pathlib, re, sys; text = pathlib.Path('pyproject.toml').read_text(); match = re.search(r'^name\\s*=\\s*\"([^\"\\n]+)\"', text, re.MULTILINE); print(match.group(1)) if match else sys.exit('name not found in pyproject.toml')")
# Read the version source directly so `make install_dev` works in a freshly
# created environment, before runtime dependencies such as pydantic exist.
SDK_VERSION := $(shell $(PYTHON) -c "import ast, pathlib, sys; version = next((ast.literal_eval(line.split('=', 1)[1].strip()) for line in pathlib.Path('src/oracle_vecdb/version.py').read_text().splitlines() if line.lstrip().startswith('SDK_VERSION')), None); print(version) if version is not None else sys.exit('SDK_VERSION not found in src/oracle_vecdb/version.py')")

.PHONY: all check distribute format format_check lint type_check test integration_test build install_dev clean help reports_dirs security_check generate_docs
.PHONY: all check distribute format format_check lint type_check test integration_test build install_dev clean help reports_dirs security_check

# =============================================================================
# 2. CORE TARGETS
Expand Down Expand Up @@ -131,7 +123,7 @@ test: reports_dirs ## Run unit tests with coverage
@echo " -> All tests passed."

integration_test: reports_dirs ## Run live VecDB integration tests
@echo "Running VecDB integration tests (pytest-xdist)..."
@echo "Running VecDB integration tests..."
@if ! [[ "$(INTEGRATION_TEST_WORKERS)" =~ ^[0-9]+$$ ]] || [ "$(INTEGRATION_TEST_WORKERS)" -lt 1 ]; then \
echo "Error: INTEGRATION_TEST_WORKERS must be a positive integer."; \
exit 2; \
Expand All @@ -146,23 +138,32 @@ integration_test: reports_dirs ## Run live VecDB integration tests
export VECDB_REQUIRE_INTEGRATION_TEST_ENV=true; \
export PYTHONPATH="$(abspath $(SOURCE_DIR))"; \
export VECDB_TEST_RUN_ID="$${VECDB_TEST_RUN_ID:-$$(date +%Y%m%d%H%M%S)}"; \
EXCLUDED_TEST_FILE="dev-tools/tests/integration/test_tkvcvecdb_sdk_sample_sanity.py"; \
TEST_FILES=(); \
if [ -n "$(INTEGRATION_TEST_PARALLEL_FILES)" ]; then \
for test_file in $(INTEGRATION_TEST_PARALLEL_FILES); do \
TEST_FILES+=("$$test_file"); \
if [ "$$test_file" != "$$EXCLUDED_TEST_FILE" ] && [ "$$(basename "$$test_file")" != "test_tkvcvecdb_sdk_sample_sanity.py" ]; then \
TEST_FILES+=("$$test_file"); \
fi; \
done; \
else \
while IFS= read -r test_file; do TEST_FILES+=("$$test_file"); done < <(find dev-tools/tests/integration -type f -name 'test_*.py' -print | sort); \
while IFS= read -r test_file; do TEST_FILES+=("$$test_file"); done < <(find dev-tools/tests/integration -type f -name 'test_*.py' ! -name 'test_tkvcvecdb_sdk_sample_sanity.py' -print | sort); \
fi; \
for test_file in $(INTEGRATION_TEST_SERIAL_FILES); do \
if [[ ! " $${TEST_FILES[*]} " =~ " $$test_file " ]]; then TEST_FILES+=("$$test_file"); fi; \
if [ "$$test_file" != "$$EXCLUDED_TEST_FILE" ] && [ "$$(basename "$$test_file")" != "test_tkvcvecdb_sdk_sample_sanity.py" ] && [[ ! " $${TEST_FILES[*]} " =~ " $$test_file " ]]; then TEST_FILES+=("$$test_file"); fi; \
done; \
REPORT_ARG=(); \
if [ -n "$(REPORT)" ]; then \
REPORT_ARG=(--junitxml "$(INTEGRATION_TEST_REPORT_SHARDS_DIR)/integration.xml"); \
fi; \
PYTEST_XDIST_ARGS=(); \
if [ "$(INTEGRATION_TEST_WORKERS)" -gt 1 ]; then \
PYTEST_XDIST_ARGS=(-n "$(INTEGRATION_TEST_WORKERS)" --dist=$(WORKER_SCHEDULE)); \
fi; \
TEST_STATUS=0; \
if [ "$${#TEST_FILES[@]}" -gt 0 ] && [ "$(DEBUG)" -gt 0 ]; then \
if [ "$${#TEST_FILES[@]}" -eq 0 ]; then \
echo "No integration test files selected."; \
elif [ "$(DEBUG)" -gt 0 ]; then \
MASTER_PID=$$$$ $(PYTEST) \
-rfE \
--tb=short \
Expand All @@ -172,8 +173,7 @@ integration_test: reports_dirs ## Run live VecDB integration tests
--capture=tee-sys \
--color=yes \
--durations-min=1 \
-n "$(INTEGRATION_TEST_WORKERS)" \
--dist=$(WORKER_SCHEDULE) \
"$${PYTEST_XDIST_ARGS[@]}" \
"$${REPORT_ARG[@]}" \
"$${TEST_FILES[@]}"; \
TEST_STATUS=$$?; \
Expand All @@ -184,8 +184,7 @@ integration_test: reports_dirs ## Run live VecDB integration tests
--durations=5 \
--color=yes \
--durations-min=1 \
-n "$(INTEGRATION_TEST_WORKERS)" \
--dist=$(WORKER_SCHEDULE) \
"$${PYTEST_XDIST_ARGS[@]}" \
"$${REPORT_ARG[@]}" \
"$${TEST_FILES[@]}"; \
TEST_STATUS=$$?; \
Expand All @@ -205,22 +204,6 @@ integration_test: reports_dirs ## Run live VecDB integration tests
exit $$TEST_STATUS
@echo " -> Integration tests passed."

generate_docs: ## Build SDK docs and package archive
@echo "Building SDK documentation (Sphinx HTML)..."
@if [ ! -f "$(DOCS_DIR)/source/conf.py" ]; then \
echo "Error: Sphinx configuration not found at $(DOCS_DIR)/source/conf.py"; \
exit 2; \
fi
@rm -rf $(DOCS_BUILD_DIR)
@$(MAKE) -C $(DOCS_DIR) html
@echo "Packaging HTML documentation archive..."
@if [ ! -d "$(DOCS_BUILD_DIR)/html" ]; then \
echo "Error: Expected HTML build directory not found: $(DOCS_BUILD_DIR)/html"; \
exit 1; \
fi
@cd "$(DOCS_BUILD_DIR)/html" && zip -qr "../$(DOC_ZIP_PREFIX)-$(SDK_VERSION).zip" .
@echo " -> Documentation archived at $(DOCS_BUILD_DIR)/$(DOC_ZIP_PREFIX)-$(SDK_VERSION).zip"

# ==============================================================================
# 4. UTILITY TARGETS
# ==============================================================================
Expand All @@ -243,7 +226,7 @@ install_dev: ## Install development dependencies

clean: ## Remove build/test caches and reports
@echo "Cleaning up artifacts..."
@rm -rf .mypy_cache .pytest_cache .coverage htmlcov/ build dist __parfait__ $(REPORT_DIR) $(DOCS_BUILD_DIR)
@rm -rf .mypy_cache .pytest_cache .coverage htmlcov/ build dist __parfait__ $(REPORT_DIR)
@find . -name "__pycache__" -exec rm -rf {} +
@echo " -> Cleanup complete."

Expand Down
79 changes: 60 additions & 19 deletions src/oracle_vecdb/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import copy
import logging
import math
import os
import re
from logging import FileHandler
Expand Down Expand Up @@ -66,6 +67,15 @@

ServerVariablesT = Dict[str, str]


def _is_finite_timeout(value: Union[int, float]) -> bool:
"""Return whether a numeric timeout is finite without leaking overflow."""
try:
return math.isfinite(value)
except OverflowError:
return False


GenericAuthSetting = TypedDict(
"GenericAuthSetting",
{
Expand Down Expand Up @@ -222,6 +232,12 @@ class ConfigurationManualMixin:
:param retries: Number of retries for API requests.
:param ca_cert_data: verify the peer using concatenated CA certificate data
in PEM (str) or DER (bytes) format.
:param timeout: Optional request timeout in seconds. A number sets the
total timeout; a ``(connect, read)`` tuple sets separate connection
and read timeouts. If omitted, no SDK timeout is configured and the
underlying urllib3 behavior is preserved.
The setting applies to SDK HTTP requests, not asynchronous job
completion or polling loops. It can be changed after construction.

:Example:

Expand Down Expand Up @@ -279,6 +295,38 @@ def _validate_base_path(base_path: str) -> None:
def _raise_missing_service_configuration() -> None:
raise ValueError("Please provide an ORDS endpoint using 'rest_url'.")

@staticmethod
def _validate_timeout(
value: Optional[Union[float, tuple[float, float]]],
) -> Optional[Union[float, tuple[float, float]]]:
"""Validate and return a public HTTP request timeout value."""
if value is None:
return None

if isinstance(value, tuple):
if len(value) != 2 or any(
isinstance(item, bool)
or not isinstance(item, (int, float))
or not _is_finite_timeout(item)
or item <= 0
for item in value
):
raise ValueError(
"timeout tuple values must be positive numbers"
)
return value

if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or not _is_finite_timeout(value)
or value <= 0
):
raise ValueError(
"timeout must be a positive number or (connect, read) tuple"
)
return value

@staticmethod
def _build_auth_setting(
auth_type: str, value: Optional[str]
Expand Down Expand Up @@ -393,25 +441,7 @@ def __init__(
"""Default Base url
"""
if timeout is not None:
if isinstance(timeout, tuple):
if len(timeout) != 2 or any(
isinstance(value, bool)
or not isinstance(value, (int, float))
or value <= 0
for value in timeout
):
raise ValueError(
"timeout tuple values must be positive numbers"
)
elif (
isinstance(timeout, bool)
or not isinstance(timeout, (int, float))
or timeout <= 0
):
raise ValueError(
"timeout must be a positive number or (connect, read) tuple"
)
self.timeout = timeout
self.timeout = timeout
self.server_index = (
0
if server_index is None and resolved_base_path is None
Expand Down Expand Up @@ -552,6 +582,17 @@ def __deepcopy__(self, memo: Dict[int, Any]) -> Self:
result.debug = self.debug
return result

@property
def timeout(self) -> Optional[Union[float, tuple[float, float]]]:
"""Optional user-configured timeout for SDK HTTP requests."""
return getattr(self, "_timeout", None)

@timeout.setter
def timeout(
self, value: Optional[Union[float, tuple[float, float]]]
) -> None:
self._timeout = self._validate_timeout(value)

def __setattr__(self, name: str, value: Any) -> None:
object.__setattr__(self, name, value)

Expand Down
13 changes: 13 additions & 0 deletions src/oracle_vecdb/data_types/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Any,
Dict,
Iterable,
Iterator,
List,
Optional,
Sequence,
Expand Down Expand Up @@ -195,6 +196,10 @@ class QueryResponse(_VecDBModel):
def __len__(self) -> int:
return len(self.items)

def __iter__(self) -> Iterator[QueryResultItem]: # type: ignore[override]
"""Iterate over the query result items."""
return iter(self.items)

def __getitem__(self, index: int) -> QueryResultItem:
return self.items[index]

Expand Down Expand Up @@ -341,6 +346,10 @@ class _PagedResponse(_VecDBModel):
count: Optional[int] = None
links: Optional[List[Any]] = None

def __iter__(self) -> Iterator[Any]: # type: ignore[override]
"""Iterate over the response items."""
return iter(self.items)

@classmethod
def from_internal(cls, response: Any) -> Self:
values = {name: _value(response, name) for name in cls.model_fields}
Expand All @@ -366,6 +375,10 @@ class VectorCollectionResponse(_VecDBModel):
offset: Optional[int] = None
count: Optional[int] = None

def __iter__(self) -> Iterator[Any]: # type: ignore[override]
"""Iterate over the response items."""
return iter(self.items)

@classmethod
def from_internal(cls, response: Any) -> Self:
values = {name: _value(response, name) for name in cls.model_fields}
Expand Down
4 changes: 2 additions & 2 deletions src/oracle_vecdb/error_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

ERROR_MESSAGES: Mapping[str, Mapping[str, str]] = {
"VECDB-001": {
"message": "Insecure REST URL: {rest_url}. HTTPS is required.",
"message": "Insecure REST URL. HTTPS is required.",
"cause": "Plain-text HTTP can expose authentication details in transit.",
"action": "Set rest_url to an endpoint that starts with 'https://'.",
},
"VECDB-002": {
"message": "Invalid REST URL format: {rest_url}.",
"message": "Invalid REST URL format.",
"cause": "The REST URL does not match the required VecDB URL structure.",
"action": "Use https://<host>:<port>/ords/<schema>/_/db-api/(stable|<version>)/vecdb/.",
},
Expand Down
Loading
Loading