Run the DatabaseSink tests against Postgres + pgvector as well as SQLite
Tests / test (==2.12.2) (push) Canceled after 0s
Tests / test (==2.14.3) (push) Canceled after 0s
Tests / test (>=3.3,<4) (push) Canceled after 0s
Tests / test (>=4,<5) (push) Canceled after 0s
Tests / test-postgres (==2.12.2) (push) Canceled after 0s
Tests / test-postgres (>=4,<5) (push) Canceled after 0s
Tests / canary (push) Canceled after 0s
Tests / test (==2.12.2) (push) Canceled after 0s
Tests / test (==2.14.3) (push) Canceled after 0s
Tests / test (>=3.3,<4) (push) Canceled after 0s
Tests / test (>=4,<5) (push) Canceled after 0s
Tests / test-postgres (==2.12.2) (push) Canceled after 0s
Tests / test-postgres (>=4,<5) (push) Canceled after 0s
Tests / canary (push) Canceled after 0s
The sink ships with a JSONB variant and is documented for asyncpg, but the suite only ever exercised SQLite. The embeddings work will need pgvector, so this adds a local Postgres stack and a way to run the database tests on it, here and in CI. - docker-compose.yml: ffb-postgres (pgvector/pgvector:pg16) on a private network, published on 127.0.0.1 only, with an init script that adds the vector extension to the test database and template1 - Makefile: pg-up/pg-down/pg-reset/pg-logs/pg-psql, test, test-pg, test-all, lint; .env is generated from .env.example with a random password, and FFB_TEST_DATABASE_URL from the environment wins over it - tests: a db_url fixture parametrized over sqlite and postgres. The postgres leg skips unless FFB_TEST_DATABASE_URL is set, and creates a uniquely named database per test (with vector) that is dropped after. The DatabaseSink factories in the events, retention, correlation, soft-error and instrumentation tests now use it, so their 38 database tests run on both. The missing-events-table test accepts either backend's error. New tests check isolation and that vector works. - asyncpg and pgvector added to the dev and test extras - CI: a test-postgres job on FastMCP 2.12.2 and 4.x with a pgvector service container, failing if the Postgres cases skip - CONTRIBUTING: how to run the suite against Postgres
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Copy to .env (make does this for you on first run) and adjust as needed.
|
||||
COMPOSE_PROJECT_NAME=fastmcp-feedback-pkg
|
||||
|
||||
FFB_PG_IMAGE=pgvector/pgvector:pg16
|
||||
FFB_PG_USER=ffb
|
||||
FFB_PG_PASSWORD=change-me
|
||||
FFB_PG_DB=ffb_test
|
||||
# Published on 127.0.0.1 only. 55432 is commonly taken by other local stacks.
|
||||
FFB_PG_PORT=55433
|
||||
|
||||
# Read by the test suite. Unset, the Postgres test cases skip.
|
||||
# Each test gets its own database created on this server and dropped after.
|
||||
FFB_TEST_DATABASE_URL=postgresql+asyncpg://ffb:change-me@127.0.0.1:55433/ffb_test
|
||||
@@ -36,11 +36,62 @@ jobs:
|
||||
uv pip show fastmcp | grep -i '^version'
|
||||
|
||||
- name: Lint
|
||||
run: uv run --no-sync ruff check src/ examples/ tests/_compat.py tests/unit/test_instrumentation.py
|
||||
run: uv run --no-sync ruff check src/ examples/ tests/_compat.py tests/_databases.py tests/unit/test_test_databases.py tests/unit/test_instrumentation.py
|
||||
|
||||
- name: Test
|
||||
run: uv run --no-sync pytest -q --no-cov
|
||||
|
||||
# The DatabaseSink tests again, on Postgres + pgvector instead of SQLite.
|
||||
# Oldest and newest supported FastMCP; the SQLite legs above cover the rest.
|
||||
# act_runner runs the job in a container, so the service is reached by name.
|
||||
test-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
fastmcp: ['==2.12.2', '>=4,<5']
|
||||
|
||||
services:
|
||||
ffb-postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
env:
|
||||
POSTGRES_USER: ffb
|
||||
POSTGRES_PASSWORD: ffb-ci
|
||||
POSTGRES_DB: ffb_test
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U ffb -d ffb_test"
|
||||
--health-interval 2s
|
||||
--health-timeout 3s
|
||||
--health-retries 30
|
||||
|
||||
env:
|
||||
FFB_TEST_DATABASE_URL: postgresql+asyncpg://ffb:ffb-ci@ffb-postgres:5432/ffb_test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install against fastmcp${{ matrix.fastmcp }}
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install -e ".[dev]" "fastmcp${{ matrix.fastmcp }}"
|
||||
uv pip show fastmcp | grep -i '^version'
|
||||
|
||||
- name: Test with the Postgres cases enabled
|
||||
run: make test-pg
|
||||
|
||||
- name: Postgres cases ran rather than skipped
|
||||
run: |
|
||||
out=$(uv run --no-sync pytest -q --no-cov -m postgres -rs)
|
||||
echo "$out" | tail -3
|
||||
if echo "$out" | grep -q 'SKIPPED'; then
|
||||
echo "Postgres cases skipped; is the service reachable?"; exit 1
|
||||
fi
|
||||
|
||||
# Newest FastMCP with no ceiling. Allowed to fail: it exists to say
|
||||
# "a new major version is out and here is what breaks" before users do.
|
||||
canary:
|
||||
|
||||
@@ -97,6 +97,36 @@ uv run pytest -m unit
|
||||
uv run pytest -m integration
|
||||
```
|
||||
|
||||
### Running the tests against Postgres
|
||||
|
||||
The `DatabaseSink` tests run on SQLite by default. Each one also has a
|
||||
`[postgres]` variant that runs against a real Postgres 16 with pgvector
|
||||
when `FFB_TEST_DATABASE_URL` is set, and skips otherwise. The repo ships a
|
||||
compose stack for it (Docker required):
|
||||
|
||||
```bash
|
||||
make pg-up # start ffb-postgres and wait until healthy (creates .env on first run)
|
||||
make test-pg # whole suite, Postgres cases enabled
|
||||
make test # whole suite, SQLite only
|
||||
make test-all # both
|
||||
make pg-down # stop it; the data volume is kept (pg-reset deletes it)
|
||||
```
|
||||
|
||||
The server listens on `127.0.0.1` only, on `FFB_PG_PORT` from `.env`
|
||||
(55433 by default; change it if that port is taken). `make pg-psql` opens a
|
||||
shell on it. Every Postgres test gets its own freshly created database with
|
||||
the `vector` extension, dropped when the test ends, so tests never see each
|
||||
other's rows. To write a test that runs on both backends, take the `db_url`
|
||||
fixture from `tests/conftest.py`: `db_url("name")` returns an async URL for
|
||||
an empty database, the same one for the same name within a test.
|
||||
|
||||
To point the suite at another server, set the variable yourself; the user
|
||||
needs permission to create and drop databases:
|
||||
|
||||
```bash
|
||||
FFB_TEST_DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/postgres uv run pytest -m postgres
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
- Place unit tests in `tests/unit/`
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Dev/test helpers. The Postgres stack exists only so the test suite can run
|
||||
# against a real server; see CONTRIBUTING.md.
|
||||
|
||||
COMPOSE ?= docker compose
|
||||
PYTEST ?= uv run --no-sync pytest
|
||||
PYTEST_ARGS ?= -q --no-cov
|
||||
|
||||
# FFB_TEST_DATABASE_URL and friends come from .env when it exists. A value
|
||||
# already in the environment (CI points it at a service container) wins.
|
||||
ENV_TEST_DATABASE_URL := $(FFB_TEST_DATABASE_URL)
|
||||
-include .env
|
||||
ifneq ($(ENV_TEST_DATABASE_URL),)
|
||||
FFB_TEST_DATABASE_URL := $(ENV_TEST_DATABASE_URL)
|
||||
endif
|
||||
|
||||
.PHONY: help pg-up pg-down pg-logs pg-psql pg-reset test test-pg test-all lint
|
||||
|
||||
help:
|
||||
@echo "pg-up start Postgres + pgvector and wait until healthy"
|
||||
@echo "pg-down stop it (the data volume is kept)"
|
||||
@echo "pg-reset stop it and delete the data volume"
|
||||
@echo "pg-logs follow its logs"
|
||||
@echo "pg-psql psql into the test database"
|
||||
@echo "test test suite on SQLite only (Postgres cases skip)"
|
||||
@echo "test-pg test suite with the Postgres cases enabled (needs pg-up)"
|
||||
@echo "test-all both"
|
||||
@echo "lint ruff"
|
||||
|
||||
# First run: copy the example and give it a random password.
|
||||
.env:
|
||||
@pw=$$(openssl rand -hex 16); sed "s/change-me/$$pw/g" .env.example > .env
|
||||
@echo "wrote .env from .env.example"
|
||||
|
||||
pg-up: .env
|
||||
$(COMPOSE) up -d --wait ffb-postgres
|
||||
$(COMPOSE) logs --tail=20 ffb-postgres
|
||||
|
||||
pg-down:
|
||||
$(COMPOSE) down
|
||||
|
||||
pg-reset:
|
||||
$(COMPOSE) down -v
|
||||
|
||||
pg-logs:
|
||||
$(COMPOSE) logs -f ffb-postgres
|
||||
|
||||
pg-psql:
|
||||
$(COMPOSE) exec ffb-postgres psql -U "$(FFB_PG_USER)" -d "$(FFB_PG_DB)"
|
||||
|
||||
test:
|
||||
env -u FFB_TEST_DATABASE_URL $(PYTEST) $(PYTEST_ARGS)
|
||||
|
||||
test-pg: .env
|
||||
@test -n "$(FFB_TEST_DATABASE_URL)" || { echo "FFB_TEST_DATABASE_URL is not set (environment or .env)"; exit 1; }
|
||||
@echo "$(PYTEST) $(PYTEST_ARGS) (Postgres cases enabled)"
|
||||
@FFB_TEST_DATABASE_URL="$(FFB_TEST_DATABASE_URL)" $(PYTEST) $(PYTEST_ARGS)
|
||||
|
||||
test-all: test test-pg
|
||||
|
||||
lint:
|
||||
uv run --no-sync ruff check src/ examples/ tests/_compat.py tests/_databases.py \
|
||||
tests/unit/test_test_databases.py tests/unit/test_instrumentation.py
|
||||
@@ -0,0 +1,31 @@
|
||||
# Postgres + pgvector for running the test suite against a real server.
|
||||
# Test-only: nothing here serves HTTP, so there is no caddy network.
|
||||
# The port is published on 127.0.0.1 only, for pytest running on the host.
|
||||
|
||||
services:
|
||||
ffb-postgres:
|
||||
image: ${FFB_PG_IMAGE:-pgvector/pgvector:pg16}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${FFB_PG_USER}
|
||||
POSTGRES_PASSWORD: ${FFB_PG_PASSWORD}
|
||||
POSTGRES_DB: ${FFB_PG_DB}
|
||||
ports:
|
||||
- "127.0.0.1:${FFB_PG_PORT:-55433}:5432"
|
||||
volumes:
|
||||
- ffb-postgres-data:/var/lib/postgresql/data
|
||||
- ./docker/postgres-init:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
start_period: 5s
|
||||
networks:
|
||||
- internal
|
||||
|
||||
volumes:
|
||||
ffb-postgres-data:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Runs once, on first start with an empty volume.
|
||||
-- The test fixtures create a fresh database per test from template1, so the
|
||||
-- extension goes there too; every database created afterwards inherits it.
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
\connect template1
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
@@ -56,6 +56,8 @@ mysql = ["pymysql>=1.1.0"]
|
||||
dev = [
|
||||
"sqlalchemy[asyncio]>=2.0.43",
|
||||
"aiosqlite>=0.22.1",
|
||||
"asyncpg>=0.31.0",
|
||||
"pgvector>=0.5.0",
|
||||
"pytest>=8.2.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"pytest-cov>=5.0.0",
|
||||
@@ -67,6 +69,8 @@ dev = [
|
||||
test = [
|
||||
"sqlalchemy[asyncio]>=2.0.43",
|
||||
"aiosqlite>=0.22.1",
|
||||
"asyncpg>=0.31.0",
|
||||
"pgvector>=0.5.0",
|
||||
"pytest>=8.2.0",
|
||||
"pytest-asyncio>=0.23.0",
|
||||
"pytest-cov>=5.0.0",
|
||||
@@ -119,6 +123,7 @@ markers = [
|
||||
"integration: Integration tests for component interaction",
|
||||
"slow: Slow running tests",
|
||||
"database: Tests requiring database setup",
|
||||
"postgres: Runs against the Postgres named by FFB_TEST_DATABASE_URL; skipped when unset",
|
||||
]
|
||||
filterwarnings = [
|
||||
"ignore::DeprecationWarning",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Fresh databases for DatabaseSink tests: SQLite always, Postgres when asked.
|
||||
|
||||
Set FFB_TEST_DATABASE_URL to a ``postgresql+asyncpg://`` URL (``make test-pg``
|
||||
does this from .env) and every test using the ``db_url`` fixture also runs
|
||||
against that server. Each test gets its own uniquely named databases, created
|
||||
on first use and dropped at teardown, so no two tests share tables.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import uuid
|
||||
from collections.abc import Coroutine
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.engine import URL, make_url
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
ENV_VAR = "FFB_TEST_DATABASE_URL"
|
||||
SKIP_REASON = f"{ENV_VAR} is not set; run `make pg-up` then `make test-pg`"
|
||||
|
||||
|
||||
def postgres_url() -> URL | None:
|
||||
raw = os.environ.get(ENV_VAR, "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
url = make_url(raw)
|
||||
if not url.drivername.startswith("postgresql"):
|
||||
raise ValueError(f"{ENV_VAR} must be a postgresql URL, got {url.drivername}")
|
||||
return url
|
||||
|
||||
|
||||
def run_blocking(coro: Coroutine[Any, Any, Any]) -> Any:
|
||||
"""Run ``coro`` to completion on its own loop in a worker thread.
|
||||
|
||||
The sink factories in the tests are plain functions called from inside a
|
||||
running event loop, so the admin work cannot await on that loop.
|
||||
"""
|
||||
box: dict[str, Any] = {}
|
||||
|
||||
def target() -> None:
|
||||
try:
|
||||
box["value"] = asyncio.run(coro)
|
||||
except BaseException as exc: # re-raised in the caller's thread
|
||||
box["error"] = exc
|
||||
|
||||
thread = threading.Thread(target=target)
|
||||
thread.start()
|
||||
thread.join()
|
||||
if "error" in box:
|
||||
raise box["error"]
|
||||
return box.get("value")
|
||||
|
||||
|
||||
class PostgresDatabases:
|
||||
"""Create databases on one server for a single test, then drop them all."""
|
||||
|
||||
def __init__(self, url: URL):
|
||||
self.url = url
|
||||
self.run_id = uuid.uuid4().hex[:10]
|
||||
self.by_label: dict[str, str] = {}
|
||||
|
||||
def _admin(self, database: str | None = None):
|
||||
url = self.url if database is None else self.url.set(database=database)
|
||||
return create_async_engine(
|
||||
url, isolation_level="AUTOCOMMIT", poolclass=NullPool
|
||||
)
|
||||
|
||||
def url_for(self, label: str) -> str:
|
||||
"""URL of this test's database for ``label``, created on first request."""
|
||||
if label not in self.by_label:
|
||||
safe = re.sub(r"[^a-z0-9_]", "_", label.lower())
|
||||
name = f"ffb_t_{self.run_id}_{safe}"[:63]
|
||||
run_blocking(self._create(name))
|
||||
self.by_label[label] = name
|
||||
return self.url.set(database=self.by_label[label]).render_as_string(
|
||||
hide_password=False
|
||||
)
|
||||
|
||||
async def _create(self, name: str) -> None:
|
||||
engine = self._admin()
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text(f'CREATE DATABASE "{name}"'))
|
||||
finally:
|
||||
await engine.dispose()
|
||||
# The compose init script puts vector in template1 already; CI's
|
||||
# service container has no init script, so make sure here.
|
||||
engine = self._admin(name)
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
def drop_all(self) -> None:
|
||||
if self.by_label:
|
||||
run_blocking(self._drop(list(self.by_label.values())))
|
||||
self.by_label.clear()
|
||||
|
||||
async def _drop(self, names: list[str]) -> None:
|
||||
engine = self._admin()
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
for name in names:
|
||||
# FORCE: a sink a test forgot to close must not keep it alive.
|
||||
await conn.execute(
|
||||
text(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
|
||||
)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
@@ -16,6 +16,8 @@ from fastmcp_feedback.database import FeedbackDatabase
|
||||
from fastmcp_feedback.insights import FeedbackInsights
|
||||
from fastmcp_feedback.models import Base, Feedback, FeedbackType, FeedbackStatus
|
||||
|
||||
from ._databases import SKIP_REASON, PostgresDatabases, postgres_url
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def event_loop():
|
||||
@@ -177,6 +179,37 @@ def mock_database(mock_session):
|
||||
return db
|
||||
|
||||
|
||||
# Database backends for DatabaseSink tests
|
||||
|
||||
@pytest.fixture(
|
||||
params=["sqlite", pytest.param("postgres", marks=pytest.mark.postgres)]
|
||||
)
|
||||
def db_backend(request) -> str:
|
||||
"""Which database a ``db_url`` test runs on. Postgres needs FFB_TEST_DATABASE_URL."""
|
||||
if request.param == "postgres" and postgres_url() is None:
|
||||
pytest.skip(SKIP_REASON)
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_url(db_backend, tmp_path):
|
||||
"""Factory: ``db_url(name)`` returns an async URL for an empty database.
|
||||
|
||||
The same name within one test returns the same database, so a test can
|
||||
reopen it; different names and different tests never share one. SQLite
|
||||
databases are files under tmp_path; Postgres ones are created on the
|
||||
server named by FFB_TEST_DATABASE_URL and dropped after the test.
|
||||
"""
|
||||
if db_backend == "sqlite":
|
||||
yield lambda name="calls": f"sqlite+aiosqlite:///{tmp_path}/{name}.db"
|
||||
return
|
||||
databases = PostgresDatabases(postgres_url())
|
||||
try:
|
||||
yield lambda name="calls": databases.url_for(name)
|
||||
finally:
|
||||
databases.drop_all()
|
||||
|
||||
|
||||
# Environment helpers
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
||||
@@ -37,7 +37,7 @@ SAME_CLIENT_RULE = RULE_USER_WINDOW if FASTMCP_4 else RULE_SESSION
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db(tmp_path):
|
||||
async def db(db_url):
|
||||
"""Make DatabaseSinks whose engines are always disposed, even on failure.
|
||||
|
||||
An undisposed aiosqlite engine keeps a worker thread alive and the test
|
||||
@@ -46,7 +46,7 @@ async def db(tmp_path):
|
||||
made = []
|
||||
|
||||
def factory(name="calls", cls=DatabaseSink):
|
||||
sink = cls(f"sqlite+aiosqlite:///{tmp_path}/{name}.db", create_tables=True)
|
||||
sink = cls(db_url(name), create_tables=True)
|
||||
made.append(sink)
|
||||
return sink
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import pytest
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.exceptions import ToolError
|
||||
from sqlalchemy import MetaData, func, select
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.exc import DBAPIError
|
||||
|
||||
from fastmcp_feedback.instrumentation import (
|
||||
REDACTED,
|
||||
@@ -30,13 +30,13 @@ from fastmcp_feedback.instrumentation import (
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db(tmp_path):
|
||||
async def db(db_url):
|
||||
"""DatabaseSinks whose engines are always disposed, or the process hangs."""
|
||||
made = []
|
||||
|
||||
def factory(name="calls", **kwargs):
|
||||
kwargs.setdefault("create_tables", True)
|
||||
sink = DatabaseSink(f"sqlite+aiosqlite:///{tmp_path}/{name}.db", **kwargs)
|
||||
sink = DatabaseSink(db_url(name), **kwargs)
|
||||
made.append(sink)
|
||||
return sink
|
||||
|
||||
@@ -437,7 +437,7 @@ class TestSinks:
|
||||
assert len(await table_rows(store, store.events)) == 1
|
||||
assert await table_rows(store, store.table) == []
|
||||
|
||||
async def test_calls_stored_when_events_table_missing(self, tmp_path, db):
|
||||
async def test_calls_stored_when_events_table_missing(self, db):
|
||||
store = db("old", create_tables=False)
|
||||
old = MetaData()
|
||||
build_metadata("ffb_", old)
|
||||
@@ -451,7 +451,8 @@ class TestSinks:
|
||||
ok=True,
|
||||
mode="meta",
|
||||
)
|
||||
with pytest.raises(OperationalError, match="no such table"):
|
||||
# SQLite raises OperationalError, Postgres ProgrammingError.
|
||||
with pytest.raises(DBAPIError, match="no such table|does not exist"):
|
||||
await store.write([rec, event()])
|
||||
assert len(await table_rows(store, store.table)) == 1
|
||||
|
||||
|
||||
@@ -373,9 +373,8 @@ class TestSinks:
|
||||
|
||||
|
||||
class TestDatabaseSink:
|
||||
async def test_end_to_end_with_created_tables(self, tmp_path):
|
||||
url = f"sqlite+aiosqlite:///{tmp_path}/calls.db"
|
||||
db = DatabaseSink(url, create_tables=True)
|
||||
async def test_end_to_end_with_created_tables(self, db_url):
|
||||
db = DatabaseSink(db_url(), create_tables=True)
|
||||
app, _, mw = make_app(sinks=[db], mode="full", server_version="9.9")
|
||||
await call(app, "create_token", {"label": "ci"})
|
||||
with pytest.raises(ToolError):
|
||||
@@ -397,8 +396,8 @@ class TestDatabaseSink:
|
||||
assert rows[0]["server_version"] == "9.9"
|
||||
assert "hunter2" not in json.dumps([dict(r) for r in rows], default=str)
|
||||
|
||||
async def test_prefix_and_host_owned_engine(self, tmp_path):
|
||||
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/host.db")
|
||||
async def test_prefix_and_host_owned_engine(self, db_url):
|
||||
engine = create_async_engine(db_url("host"))
|
||||
db = DatabaseSink(engine, prefix="qa_", create_tables=True)
|
||||
app, _, mw = make_app(sinks=[db])
|
||||
await call(app, "echo", {"text": "hi"})
|
||||
@@ -414,8 +413,8 @@ class TestDatabaseSink:
|
||||
"qa_tool_calls",
|
||||
]
|
||||
|
||||
async def test_no_tables_created_by_default(self, tmp_path):
|
||||
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/host.db")
|
||||
async def test_no_tables_created_by_default(self, db_url):
|
||||
engine = create_async_engine(db_url("host"))
|
||||
app, _, mw = make_app(sinks=[DatabaseSink(engine)])
|
||||
result = await call(app, "echo", {"text": "hi"})
|
||||
await mw.flush()
|
||||
@@ -429,9 +428,9 @@ class TestDatabaseSink:
|
||||
assert tables == []
|
||||
assert mw.dispatcher.sink_errors == 1
|
||||
|
||||
async def test_absent_payloads_are_sql_null(self, tmp_path):
|
||||
async def test_absent_payloads_are_sql_null(self, db_url):
|
||||
"""Meta mode stores no payloads; they must be SQL NULL, not JSON null."""
|
||||
db = DatabaseSink(f"sqlite+aiosqlite:///{tmp_path}/n.db", create_tables=True)
|
||||
db = DatabaseSink(db_url("n"), create_tables=True)
|
||||
app, _, mw = make_app(sinks=[db])
|
||||
await call(app, "echo", {"text": "hi"})
|
||||
await mw.flush()
|
||||
|
||||
@@ -21,14 +21,12 @@ from fastmcp_feedback.instrumentation import (
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db(tmp_path):
|
||||
async def db(db_url):
|
||||
"""DatabaseSinks whose engines are always disposed, or the process hangs."""
|
||||
made = []
|
||||
|
||||
def factory(name="calls", **kwargs):
|
||||
sink = DatabaseSink(
|
||||
f"sqlite+aiosqlite:///{tmp_path}/{name}.db", create_tables=True, **kwargs
|
||||
)
|
||||
sink = DatabaseSink(db_url(name), create_tables=True, **kwargs)
|
||||
made.append(sink)
|
||||
return sink
|
||||
|
||||
|
||||
@@ -39,14 +39,12 @@ except TypeError: # added in FastMCP 3.4
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db(tmp_path):
|
||||
async def db(db_url):
|
||||
"""DatabaseSinks whose engines are always disposed, or the process hangs."""
|
||||
made = []
|
||||
|
||||
def factory(name="calls"):
|
||||
sink = DatabaseSink(
|
||||
f"sqlite+aiosqlite:///{tmp_path}/{name}.db", create_tables=True
|
||||
)
|
||||
sink = DatabaseSink(db_url(name), create_tables=True)
|
||||
made.append(sink)
|
||||
return sink
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Checks on the db_url fixture itself: isolation between tests, and pgvector."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import func, select, text
|
||||
|
||||
from fastmcp_feedback.instrumentation import DatabaseSink, ToolCallRecord
|
||||
|
||||
SHARED_ID = "00000000-0000-0000-0000-00000000f00d"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def sink(db_url):
|
||||
made = DatabaseSink(db_url(), create_tables=True)
|
||||
yield made
|
||||
await made.engine.dispose()
|
||||
|
||||
|
||||
async def write_fixed_row(sink):
|
||||
"""Insert a row with a fixed primary key and return the table's row count.
|
||||
|
||||
Two tests that did this against one shared database would collide on the
|
||||
key, so each passing proves it had a database of its own.
|
||||
"""
|
||||
rec = ToolCallRecord(
|
||||
id=SHARED_ID,
|
||||
tool="t",
|
||||
started_at=datetime.now(UTC),
|
||||
duration_ms=1.0,
|
||||
ok=True,
|
||||
mode="meta",
|
||||
)
|
||||
await sink.write([rec])
|
||||
async with sink.engine.connect() as conn:
|
||||
return (
|
||||
await conn.execute(select(func.count()).select_from(sink.table))
|
||||
).scalar()
|
||||
|
||||
|
||||
class TestIsolation:
|
||||
async def test_first_writer(self, sink):
|
||||
assert await write_fixed_row(sink) == 1
|
||||
|
||||
async def test_second_writer(self, sink):
|
||||
assert await write_fixed_row(sink) == 1
|
||||
|
||||
def test_names_map_to_databases(self, db_url):
|
||||
assert db_url("a") == db_url("a")
|
||||
assert db_url("a") != db_url("b")
|
||||
|
||||
|
||||
async def test_vector_extension_available(db_url, db_backend):
|
||||
if db_backend != "postgres":
|
||||
pytest.skip("pgvector is Postgres only")
|
||||
sink = DatabaseSink(db_url())
|
||||
try:
|
||||
async with sink.engine.connect() as conn:
|
||||
version = (
|
||||
await conn.execute(
|
||||
text("SELECT extversion FROM pg_extension WHERE extname = 'vector'")
|
||||
)
|
||||
).scalar()
|
||||
distance = (
|
||||
await conn.execute(text("SELECT '[1,2,3]'::vector <-> '[1,2,4]'"))
|
||||
).scalar()
|
||||
finally:
|
||||
await sink.engine.dispose()
|
||||
assert version is not None
|
||||
assert distance == 1.0
|
||||
@@ -51,6 +51,54 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncpg"
|
||||
version = "0.31.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
@@ -564,7 +612,9 @@ dependencies = [
|
||||
[package.optional-dependencies]
|
||||
all = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pgvector" },
|
||||
{ name = "psycopg2-binary" },
|
||||
{ name = "pymysql" },
|
||||
{ name = "pytest" },
|
||||
@@ -577,7 +627,9 @@ all = [
|
||||
]
|
||||
dev = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "mypy" },
|
||||
{ name = "pgvector" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
@@ -598,6 +650,8 @@ postgresql = [
|
||||
]
|
||||
test = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "pgvector" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
@@ -611,9 +665,13 @@ requires-dist = [
|
||||
{ name = "aiosqlite", marker = "extra == 'dev'", specifier = ">=0.22.1" },
|
||||
{ name = "aiosqlite", marker = "extra == 'instrumentation'", specifier = ">=0.22.1" },
|
||||
{ name = "aiosqlite", marker = "extra == 'test'", specifier = ">=0.22.1" },
|
||||
{ name = "asyncpg", marker = "extra == 'dev'", specifier = ">=0.31.0" },
|
||||
{ name = "asyncpg", marker = "extra == 'test'", specifier = ">=0.31.0" },
|
||||
{ name = "fastmcp", specifier = ">=2.12.2,<5" },
|
||||
{ name = "fastmcp-feedback", extras = ["postgresql", "mysql", "dev"], marker = "extra == 'all'" },
|
||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" },
|
||||
{ name = "pgvector", marker = "extra == 'dev'", specifier = ">=0.5.0" },
|
||||
{ name = "pgvector", marker = "extra == 'test'", specifier = ">=0.5.0" },
|
||||
{ name = "psycopg2-binary", marker = "extra == 'postgresql'", specifier = ">=2.9.0" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
{ name = "pymysql", marker = "extra == 'mysql'", specifier = ">=1.1.0" },
|
||||
@@ -1264,6 +1322,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pgvector"
|
||||
version = "0.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/ec/6eb80aebc728200f95229219882994c1b0585b956ca47da5edb9d062627a/pgvector-0.5.0.tar.gz", hash = "sha256:07a9dcf735696879406983afc6eba9a787cef7c0cf6c367ca1a5779f036dee74", size = 35170, upload-time = "2026-07-06T18:27:27.767Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e4/a5573f2c579ca9ad133293bfb624148ba0893674ca4a6eeec85ced9a6a09/pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f", size = 30958, upload-time = "2026-07-06T18:27:26.797Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.5.1"
|
||||
|
||||
Reference in New Issue
Block a user