Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ concurrency:

env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
UV_VERSION: "0.13.1"
AUTO_VERSION: "11.3.6"
RELEASE_BOT_NAME: "applied-ai-releases[bot]"
RELEASE_BOT_EMAIL: "applied-ai-releases[bot]@users.noreply.github.com"
Expand Down
15 changes: 13 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
- main

env:
UV_VERSION: "0.12.3"
UV_VERSION: "0.13.1"

jobs:
lock:
Expand Down Expand Up @@ -43,6 +43,7 @@ jobs:
- "3.12"
- "3.13"
- "3.14"
- "3.15"

steps:
- name: Check out repository
Expand All @@ -64,8 +65,18 @@ jobs:
uv.lock

- name: Install dependencies
env:
MATRIX_PYTHON: ${{ matrix.python-version }}
run: |
uv sync --all-extras --frozen
set -euo pipefail

# torch has no cp315 wheels on PyPI yet, so sentence-transformers
# (and `all`, which includes it) cannot install on 3.15.
extras=(--all-extras)
if [ "$MATRIX_PYTHON" = "3.15" ]; then
extras+=(--no-extra sentence-transformers --no-extra all)
fi
uv sync "${extras[@]}" --frozen

- name: check-sort-import
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-fork-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:

env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
UV_VERSION: "0.13.1"

jobs:
setup:
Expand Down
15 changes: 12 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:

env:
PYTHON_VERSION: "3.11"
UV_VERSION: "0.12.3"
UV_VERSION: "0.13.1"

jobs:
service-tests:
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
redis-py-version: ["6.x", "7.x", "8.x"]
# `redis:latest` floats, so a failure on it is not reproducible from a
# SHA; it is advisory below. Pinned tags carry the blocking coverage.
Expand Down Expand Up @@ -117,10 +117,19 @@ jobs:
uv.lock

- name: Install dependencies
env:
MATRIX_PYTHON: ${{ matrix.python-version }}
run: |
set -euo pipefail

uv sync --all-extras --frozen
# torch has no cp315 wheels on PyPI yet, so sentence-transformers
# (and `all`, which includes it) cannot install on 3.15. Tests marked
# requires_hf skip themselves when it is missing (tests/conftest.py).
extras=(--all-extras)
if [ "$MATRIX_PYTHON" = "3.15" ]; then
extras+=(--no-extra sentence-transformers --no-extra all)
fi
uv sync "${extras[@]}" --frozen

# Must cover every redis-py-version matrix value; unhandled values fail.
case "$REDIS_PY_VERSION" in
Expand Down
8 changes: 7 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Here's how to get started with your code contribution:

### Prerequisites

- **Python**: RedisVL supports Python 3.9 and above
- **Python**: RedisVL supports Python 3.10 through 3.15
- **Docker**: Required for running Redis and integration tests
- **UV**: Modern Python package manager for fast dependency management

Expand Down Expand Up @@ -81,6 +81,12 @@ uv sync --all-extras

This will create a virtual environment and install all necessary dependencies for development.

On Python 3.15, PyTorch has no wheels on PyPI yet, so leave out the extras that need it. Tests marked `requires_hf` skip when sentence-transformers is not installed.

```bash
uv sync --all-extras --no-extra sentence-transformers --no-extra all
```

### Changing Dependencies

`uv.lock` is checked in, and CI verifies it matches `pyproject.toml`. If you add, remove, or re-bound a dependency, re-lock and commit the result:
Expand Down
4 changes: 4 additions & 0 deletions docs/user_guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ To install **all** optional dependencies at once:
$ pip install redisvl[all]
```

```{note}
On Python 3.15, the `sentence-transformers` and `all` extras can't be installed yet, because PyTorch doesn't publish Python 3.15 wheels on PyPI. Install the individual extras you need instead. The `voyageai` and `sql-redis` packages also declare a Python upper bound below 3.15, so `pip` refuses them on 3.15 until their maintainers lift it.
```

## Install RedisVL from Source

To install RedisVL from source, clone the repository and install the package using `pip`:
Expand Down
19 changes: 16 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name = "redisvl"
version = "0.28.0"
description = "Python client library and CLI for using Redis as a vector database"
authors = [{ name = "Redis Inc.", email = "applied.ai@redis.com" }]
requires-python = ">=3.10,<3.15"
requires-python = ">=3.10,<3.16"
readme = "README.md"
license = "MIT"
keywords = [
Expand All @@ -20,6 +20,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"License :: OSI Approved :: MIT License",
]
dependencies = [
Expand All @@ -45,6 +46,10 @@ mcp = [
# Supporting that revision is its own piece of work.
"fastmcp>=2.0.0,<4",
"pydantic-settings>=2.0,<3",
# fastmcp pulls in beartype transitively. Releases before 0.23 import
# typing.no_type_check_decorator, which Python 3.15 removed, so fastmcp
# fails to import and the MCP server falls back to its stub.
"beartype>=0.23.1",
]
mistralai = ["mistralai>=1.0.0,<2"]
openai = ["openai>=1.1.0"]
Expand Down Expand Up @@ -115,7 +120,7 @@ build-backend = "hatchling.build"

[dependency-groups]
dev = [
"black>=25.1.0,<27",
"black>=26.5.0,<27",
"isort>=5.6.4,<6",
"pylint>=3.1.0,<4",
"pytest>=8.1.1,<9",
Expand Down Expand Up @@ -151,7 +156,7 @@ default-groups = [
]

[tool.black]
target-version = ['py310', 'py311', 'py312', 'py313', 'py314']
target-version = ['py310', 'py311', 'py312', 'py313', 'py314', 'py315']
exclude = '''
(
| \.egg
Expand All @@ -171,6 +176,14 @@ exclude = '''
[tool.pytest.ini_options]
log_cli = true
asyncio_mode = "auto"
# pytest-asyncio 0.23 calls asyncio APIs that Python 3.14+ deprecates, once
# per collected object. On 3.15 that is millions of warnings per run, which
# slows the suite roughly tenfold. Scoped to pytest_asyncio so the same
# deprecations raised from redisvl still surface. Drop this once the
# pytest-asyncio<0.24 pin is lifted.
filterwarnings = [
"ignore:'asyncio\\.(iscoroutinefunction|get_event_loop_policy|set_event_loop_policy)' is deprecated:DeprecationWarning:pytest_asyncio",
]

[tool.mypy]
warn_unused_configs = true
Expand Down
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import hashlib
import importlib.util
import logging
import os
import re
Expand Down Expand Up @@ -533,13 +534,20 @@ def pytest_collection_modifyitems(
skip_cluster = pytest.mark.skip(
reason="Skipping test because Redis cluster is not available. Use --run-cluster-tests to run these tests."
)
# torch, and so sentence-transformers, can lag a new CPython release.
has_hf = importlib.util.find_spec("sentence_transformers") is not None
skip_hf = pytest.mark.skip(
reason="Skipping test because sentence-transformers is not installed."
)

# Apply skip markers independently based on flags
for item in items:
if item.get_closest_marker("requires_api_keys") and not run_api_tests:
item.add_marker(skip_api)
if item.get_closest_marker("requires_cluster") and not run_cluster_tests:
item.add_marker(skip_cluster)
if item.get_closest_marker("requires_hf") and not has_hf:
item.add_marker(skip_hf)


@pytest.fixture
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/test_key_separator_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def test_empty_prefix_handled_correctly(self):
# Should return just the ID without prefix or separator
assert key == "789", f"Expected '789' but got '{key}'"

@pytest.mark.requires_hf
def test_semantic_router_uses_index_separator(self, redis_url):
"""Test that SemanticRouter uses the index's key_separator."""
# Create a route
Expand Down Expand Up @@ -188,6 +189,7 @@ def test_trailing_separator_normalization(self):
), f"For prefix='{prefix}', sep='{separator}', id='{id_val}': expected '{expected}' but got '{key}'"


@pytest.mark.requires_hf
class TestSemanticRouterKeyConstruction:
"""Test SemanticRouter's key construction with separators."""

Expand Down
4 changes: 4 additions & 0 deletions tests/integration/test_message_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ def test_semantic_count(semantic_history):
assert semantic_history.count() == 0


@pytest.mark.requires_hf
def test_different_vector_dtypes(client, redis_url, redis_test_name):
skip_if_no_redis_search(client)
sessions = []
Expand Down Expand Up @@ -749,6 +750,7 @@ def test_different_vector_dtypes(client, redis_url, redis_test_name):
sess.delete()


@pytest.mark.requires_hf
def test_bad_dtype_connecting_to_exiting_history(client, redis_url, redis_test_name):
skip_if_no_redis_search(client)
# Skip this test for Redis 6.2.x as FT.INFO doesn't return dims properly
Expand Down Expand Up @@ -814,6 +816,7 @@ def test_invalid_vectorizer(client, redis_url, redis_test_name):
)


@pytest.mark.requires_hf
def test_passes_through_dtype_to_default_vectorizer(client, redis_url, redis_test_name):
skip_if_no_redis_search(client)
# The default is float32, so we should see float64 if we pass it in.
Expand All @@ -834,6 +837,7 @@ def test_passes_through_dtype_to_default_vectorizer(client, redis_url, redis_tes
cache.delete()


@pytest.mark.requires_hf
def test_deprecated_dtype_argument(client, redis_url, redis_test_name):
skip_if_no_redis_search(client)
history = None
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_role_filter_get_recent.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def test_get_recent_role_with_raw_output(self, redis_url):
history.delete()


@pytest.mark.requires_hf
class TestSemanticMessageHistoryRoleFilter:
"""Test role filtering functionality in SemanticMessageHistory with real Redis."""

Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ async def test_connection_kwargs_type_error(self):
await cache._get_async_redis_client()

error_msg = str(exc_info.value)
assert "argument after ** must be a mapping" in error_msg
# CPython 3.15 reworded this from "argument after **" to "Value after **".
assert "after ** must be a mapping" in error_msg

@pytest.mark.asyncio
async def test_connection_kwargs_valid_dict(self):
Expand Down
19 changes: 15 additions & 4 deletions tests/unit/test_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,34 @@ def test_semantic_cache_repr_with_ttl(patched_semantic_cache):
def test_semantic_router_repr():
from redisvl.extensions.router.schema import Route
from redisvl.extensions.router.semantic import SemanticRouter
from redisvl.utils.vectorize import CustomVectorizer

routes = [
Route(name="greeting", references=["hello", "hi"]),
Route(name="farewell", references=["bye", "goodbye"]),
]
# model_construct bypasses __init__ (and therefore Redis/vectorizer setup)
# while still setting the Pydantic fields that __repr__ reads.
router = SemanticRouter.model_construct(name="my-router", routes=routes)
# model_construct bypasses __init__ (and therefore Redis setup) while still
# setting the Pydantic fields that __repr__ reads. The stub vectorizer stops
# the field's HFTextVectorizer default from needing sentence-transformers.
router = SemanticRouter.model_construct(
name="my-router",
routes=routes,
vectorizer=CustomVectorizer(lambda text: [0.1, 0.2, 0.3]),
)
assert repr(router) == "SemanticRouter(name='my-router', routes=2)"


def test_semantic_router_repr_single_route():
from redisvl.extensions.router.schema import Route
from redisvl.extensions.router.semantic import SemanticRouter
from redisvl.utils.vectorize import CustomVectorizer

routes = [Route(name="support", references=["help", "issue"])]
router = SemanticRouter.model_construct(name="support-router", routes=routes)
router = SemanticRouter.model_construct(
name="support-router",
routes=routes,
vectorizer=CustomVectorizer(lambda text: [0.1, 0.2, 0.3]),
)
assert repr(router) == "SemanticRouter(name='support-router', routes=1)"


Expand Down
Loading
Loading