diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 835e3ad..265fdb3 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -54,7 +54,7 @@ jobs: - name: Run test cases with coverage run: | source .venv/bin/activate - TAVILY_API_KEY=mock-key make coverage + TAVILY_API_KEY=mock-key DB_TESTS_ENABLED=true make coverage - name: Generate HTML Coverage Report run: | diff --git a/pyproject.toml b/pyproject.toml index 284d373..a64b41d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = [ "langchain-tavily<0.3", "langgraph-checkpoint-mongodb>=0.1.4", "langgraph-checkpoint-postgres==2.0.21", + "psycopg[binary]>=3.2.9", ] [project.optional-dependencies] @@ -48,6 +49,10 @@ test = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", "pytest-asyncio>=1.0.0", + "pytest-cov>=6.0.0", + "asyncpg-stubs>=0.30.2", + "mongomock>=4.3.0", + "pytest-postgresql>=7.0.2", ] [tool.uv] @@ -78,3 +83,4 @@ extend-include = ["*.pyi"] indent-style = "space" line-ending = "auto" exclude = ['^/build/'] + diff --git a/src/graph/checkpoint.py b/src/graph/checkpoint.py index a0d9597..d35ac38 100644 --- a/src/graph/checkpoint.py +++ b/src/graph/checkpoint.py @@ -368,5 +368,4 @@ def chat_stream_message(thread_id: str, message: str, finish_reason: str) -> boo thread_id, message, finish_reason ) else: - logging.warning("Checkpoint saver is disabled, message not processed") return False diff --git a/tests/unit/checkpoint/postgres_mock_utils.py b/tests/unit/checkpoint/postgres_mock_utils.py new file mode 100644 index 0000000..ab32a4b --- /dev/null +++ b/tests/unit/checkpoint/postgres_mock_utils.py @@ -0,0 +1,147 @@ +# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates +# SPDX-License-Identifier: MIT + +import pytest +import tempfile +import shutil +from pathlib import Path +from unittest.mock import MagicMock, patch +from typing import Dict, Any, Optional +import psycopg + +class PostgreSQLMockInstance: + """Utility class for managing PostgreSQL mock instances.""" + + def __init__(self, database_name: str = "test_db"): + self.database_name = database_name + self.temp_dir: Optional[Path] = None + self.mock_connection: Optional[MagicMock] = None + self.mock_data: Dict[str, Any] = {} + self._setup_mock_data() + + def _setup_mock_data(self): + """Initialize mock data storage.""" + self.mock_data = { + "chat_streams": {}, # thread_id -> record + "table_exists": False, + "connection_active": True + } + + def connect(self) -> MagicMock: + """Create a mock PostgreSQL connection.""" + self.mock_connection = MagicMock() + self._setup_mock_methods() + return self.mock_connection + + def _setup_mock_methods(self): + """Setup mock methods for PostgreSQL operations.""" + if not self.mock_connection: + return + + # Mock cursor context manager + mock_cursor = MagicMock() + mock_cursor.__enter__ = MagicMock(return_value=mock_cursor) + mock_cursor.__exit__ = MagicMock(return_value=False) + + # Setup cursor operations + mock_cursor.execute = MagicMock(side_effect=self._mock_execute) + mock_cursor.fetchone = MagicMock(side_effect=self._mock_fetchone) + mock_cursor.rowcount = 0 + + # Setup connection operations + self.mock_connection.cursor = MagicMock(return_value=mock_cursor) + self.mock_connection.commit = MagicMock() + self.mock_connection.rollback = MagicMock() + self.mock_connection.close = MagicMock() + + # Store cursor for external access + self._mock_cursor = mock_cursor + + def _mock_execute(self, sql: str, params=None): + """Mock SQL execution.""" + sql_upper = sql.upper().strip() + + if "CREATE TABLE" in sql_upper: + self.mock_data["table_exists"] = True + self._mock_cursor.rowcount = 0 + + elif "SELECT" in sql_upper and "chat_streams" in sql_upper: + # Mock SELECT query + if params and len(params) > 0: + thread_id = params[0] + if thread_id in self.mock_data["chat_streams"]: + self._mock_cursor._fetch_result = self.mock_data["chat_streams"][thread_id] + else: + self._mock_cursor._fetch_result = None + else: + self._mock_cursor._fetch_result = None + + elif "UPDATE" in sql_upper and "chat_streams" in sql_upper: + # Mock UPDATE query + if params and len(params) >= 2: + messages, thread_id = params[0], params[1] + if thread_id in self.mock_data["chat_streams"]: + self.mock_data["chat_streams"][thread_id] = { + "id": thread_id, + "thread_id": thread_id, + "messages": messages + } + self._mock_cursor.rowcount = 1 + else: + self._mock_cursor.rowcount = 0 + + elif "INSERT" in sql_upper and "chat_streams" in sql_upper: + # Mock INSERT query + if params and len(params) >= 2: + thread_id, messages = params[0], params[1] + self.mock_data["chat_streams"][thread_id] = { + "id": thread_id, + "thread_id": thread_id, + "messages": messages + } + self._mock_cursor.rowcount = 1 + + def _mock_fetchone(self): + """Mock fetchone operation.""" + return getattr(self._mock_cursor, '_fetch_result', None) + + def disconnect(self): + """Cleanup mock connection.""" + if self.mock_connection: + self.mock_connection.close() + self._setup_mock_data() # Reset data + + def reset_data(self): + """Reset all mock data.""" + self._setup_mock_data() + + def get_table_count(self, table_name: str) -> int: + """Get record count in a table.""" + if table_name == "chat_streams": + return len(self.mock_data["chat_streams"]) + return 0 + + def create_test_data(self, table_name: str, records: list): + """Insert test data into a table.""" + if table_name == "chat_streams": + for record in records: + thread_id = record.get("thread_id") + if thread_id: + self.mock_data["chat_streams"][thread_id] = record + +@pytest.fixture +def mock_postgresql(): + """Create a PostgreSQL mock instance.""" + instance = PostgreSQLMockInstance() + instance.connect() + yield instance + instance.disconnect() + +@pytest.fixture +def clean_mock_postgresql(): + """Create a clean PostgreSQL mock instance that resets between tests.""" + instance = PostgreSQLMockInstance() + instance.connect() + instance.reset_data() + yield instance + instance.disconnect() \ No newline at end of file diff --git a/tests/unit/checkpoint/test_checkpoint.py b/tests/unit/checkpoint/test_checkpoint.py index 2109a3c..1dbbc92 100644 --- a/tests/unit/checkpoint/test_checkpoint.py +++ b/tests/unit/checkpoint/test_checkpoint.py @@ -1,17 +1,32 @@ # Copyright (c) 2025 Bytedance Ltd. and/or its affiliates # SPDX-License-Identifier: MIT +import os +import pytest +import mongomock +from unittest.mock import patch, MagicMock import src.graph.checkpoint as checkpoint +from postgres_mock_utils import PostgreSQLMockInstance POSTGRES_URL = "postgresql://postgres:postgres@localhost:5432/checkpointing_db" MONGO_URL = "mongodb://admin:admin@localhost:27017/checkpointing_db?authSource=admin" +def has_real_db_connection(): + # Check the environment if the MongoDB server is available + enabled = os.getenv("DB_TESTS_ENABLED", "false") + if enabled.lower() == "true": + return True + return False def test_with_local_postgres_db(): """Ensure the ChatStreamManager can be initialized with a local PostgreSQL DB.""" - manager = checkpoint.ChatStreamManager( - checkpoint_saver=True, - db_uri=POSTGRES_URL, + with patch('psycopg.connect') as mock_connect: + # Setup mock PostgreSQL connection + pg_mock = PostgreSQLMockInstance() + mock_connect.return_value = pg_mock.connect() + manager = checkpoint.ChatStreamManager( + checkpoint_saver=True, + db_uri=POSTGRES_URL, ) assert manager.postgres_conn is not None assert manager.mongo_client is None @@ -19,12 +34,17 @@ def test_with_local_postgres_db(): def test_with_local_mongo_db(): """Ensure the ChatStreamManager can be initialized with a local MongoDB.""" - manager = checkpoint.ChatStreamManager( - checkpoint_saver=True, - db_uri=MONGO_URL, - ) - assert manager.mongo_db is not None - assert manager.postgres_conn is None + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + # Setup mongomock + mock_client = mongomock.MongoClient() + mock_mongo_client.return_value = mock_client + + manager = checkpoint.ChatStreamManager( + checkpoint_saver=True, + db_uri=MONGO_URL, + ) + assert manager.mongo_db is not None + assert manager.postgres_conn is None def test_init_without_checkpoint_saver(): @@ -58,30 +78,25 @@ def test_process_stream_partial_buffer_postgres(monkeypatch): assert "hello" in values -def test_process_stream_partial_buffer_mongo(monkeypatch): - """Partial chunks should be buffered; Mongo init is stubbed to no-op.""" - - # Patch Mongo init to no-op for speed - def _no_mongo(self): - self.mongo_client = None - self.mongo_db = None - - monkeypatch.setattr( - checkpoint.ChatStreamManager, "_init_mongodb", _no_mongo, raising=True - ) - - manager = checkpoint.ChatStreamManager( - checkpoint_saver=True, - db_uri=MONGO_URL, - ) - result = manager.process_stream_message("t2", "hello", finish_reason="partial") - assert result is True - # Verify the chunk was stored in the in-memory store - items = manager.store.search(("messages", "t2"), limit=10) - values = [it.dict()["value"] for it in items] - assert "hello" in values +def test_process_stream_partial_buffer_mongo(): + """Partial chunks should be buffered; Use mongomock instead of real MongoDB.""" + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + # Setup mongomock + mock_client = mongomock.MongoClient() + mock_mongo_client.return_value = mock_client + manager = checkpoint.ChatStreamManager( + checkpoint_saver=True, + db_uri=MONGO_URL, + ) + result = manager.process_stream_message("t2", "hello", finish_reason="partial") + assert result is True + # Verify the chunk was stored in the in-memory store + items = manager.store.search(("messages", "t2"), limit=10) + values = [it.dict()["value"] for it in items] + assert "hello" in values +@pytest.mark.skipif(not has_real_db_connection(), reason="PostgreSQL Server is not available") def test_persist_postgresql_local_db(): """Ensure that the ChatStreamManager can persist to a local PostgreSQL DB.""" manager = checkpoint.ChatStreamManager( @@ -101,7 +116,8 @@ def test_persist_postgresql_local_db(): assert result is True -def test_persist_postgresql_called_with_aggregated_chunks(monkeypatch): +@pytest.mark.skipif(not has_real_db_connection(), reason="PostgreSQL Server is not available") +def test_persist_postgresql_called_with_aggregated_chunks(): """On 'stop', aggregated chunks should be passed to PostgreSQL persist method.""" manager = checkpoint.ChatStreamManager( checkpoint_saver=True, @@ -134,24 +150,42 @@ def test_persist_not_attempted_when_saver_disabled(): def test_persist_mongodb_local_db(): - """Ensure that the ChatStreamManager can persist to a local MongoDB.""" - manager = checkpoint.ChatStreamManager( - checkpoint_saver=True, - db_uri=MONGO_URL, - ) - assert manager.mongo_db is not None - assert manager.postgres_conn is None - # Simulate a message to persist - thread_id = "test_thread" - messages = ["This is a test message."] - result = manager._persist_to_mongodb(thread_id, messages) - assert result is True - # Simulate a message with existing thread - result = manager._persist_to_mongodb(thread_id, ["Another message."]) - assert result is True + """Ensure that the ChatStreamManager can persist to a mocked MongoDB.""" + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + # Setup mongomock + mock_client = mongomock.MongoClient() + mock_mongo_client.return_value = mock_client + + manager = checkpoint.ChatStreamManager( + checkpoint_saver=True, + db_uri=MONGO_URL, + ) + assert manager.mongo_db is not None + assert manager.postgres_conn is None + + # Simulate a message to persist + thread_id = "test_thread" + messages = ["This is a test message."] + result = manager._persist_to_mongodb(thread_id, messages) + assert result is True + + # Verify data was persisted in mock + collection = manager.mongo_db.chat_streams + doc = collection.find_one({"thread_id": thread_id}) + assert doc is not None + assert doc["messages"] == messages + + # Simulate a message with existing thread + result = manager._persist_to_mongodb(thread_id, ["Another message."]) + assert result is True + + # Verify update worked + doc = collection.find_one({"thread_id": thread_id}) + assert doc["messages"] == ["Another message."] -def test_persist_mongodb_called_with_aggregated_chunks(monkeypatch): +@pytest.mark.skipif(not has_real_db_connection(), reason="MongoDB server is not available") +def test_persist_mongodb_called_with_aggregated_chunks(): """On 'stop', aggregated chunks should be passed to MongoDB persist method.""" manager = checkpoint.ChatStreamManager( @@ -205,25 +239,36 @@ def test_unsupported_db_uri_scheme(): def test_process_stream_with_interrupt_finish_reason(): """Test that 'interrupt' finish_reason triggers persistence like 'stop'.""" - manager = checkpoint.ChatStreamManager( - checkpoint_saver=True, - db_uri=MONGO_URL, - ) + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + # Setup mongomock + mock_client = mongomock.MongoClient() + mock_mongo_client.return_value = mock_client + + manager = checkpoint.ChatStreamManager( + checkpoint_saver=True, + db_uri=MONGO_URL, + ) - # Add partial message - assert ( - manager.process_stream_message( - "int_test", "Interrupted", finish_reason="partial" + # Add partial message + assert ( + manager.process_stream_message( + "int_test", "Interrupted", finish_reason="partial" + ) + is True ) - is True - ) - # Interrupt should trigger persistence - assert ( - manager.process_stream_message( - "int_test", " message", finish_reason="interrupt" + # Interrupt should trigger persistence + assert ( + manager.process_stream_message( + "int_test", " message", finish_reason="interrupt" + ) + is True ) - is True - ) + + # Verify persistence occurred + collection = manager.mongo_db.chat_streams + doc = collection.find_one({"thread_id": "int_test"}) + assert doc is not None + assert doc["messages"] == ["Interrupted", " message"] def test_postgresql_connection_failure(monkeypatch): @@ -348,64 +393,39 @@ def test_multiple_threads_isolation(): assert "msg2" not in thread1_values -def test_mongodb_insert_and_update_paths(monkeypatch): - """Exercise MongoDB insert, update, and exception branches.""" +def test_mongodb_insert_and_update_paths(): + """Exercise MongoDB insert, update, and exception branches using mongomock.""" + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + # Setup mongomock + mock_client = mongomock.MongoClient() + mock_mongo_client.return_value = mock_client - # Fake Mongo classes - class FakeUpdateResult: - def __init__(self, modified_count): - self.modified_count = modified_count + manager = checkpoint.ChatStreamManager(checkpoint_saver=True, db_uri=MONGO_URL) - class FakeInsertResult: - def __init__(self, inserted_id): - self.inserted_id = inserted_id + # Insert success (new thread) + assert manager._persist_to_mongodb("th1", ["message1"]) is True + + # Verify insert worked + collection = manager.mongo_db.chat_streams + doc = collection.find_one({"thread_id": "th1"}) + assert doc is not None + assert doc["messages"] == ["message1"] - class FakeCollection: - def __init__(self, mode="insert_success"): - self.mode = mode + # Update success (existing thread) + assert manager._persist_to_mongodb("th1", ["message2"]) is True + + # Verify update worked + doc = collection.find_one({"thread_id": "th1"}) + assert doc["messages"] == ["message2"] - def find_one(self, query): - if self.mode.startswith("insert"): - return None - return {"thread_id": query["thread_id"]} - - def update_one(self, q, s): - if self.mode == "update_success": - return FakeUpdateResult(1) - return FakeUpdateResult(0) - - def insert_one(self, doc): - if self.mode == "insert_success": - return FakeInsertResult("ok") - if self.mode == "insert_none": - return FakeInsertResult(None) - raise RuntimeError("boom") - - class FakeMongoDB: - def __init__(self, mode): - self.chat_streams = FakeCollection(mode) - - manager = checkpoint.ChatStreamManager(checkpoint_saver=True, db_uri=MONGO_URL) - - # Insert success - manager.mongo_db = FakeMongoDB("insert_success") - assert manager._persist_to_mongodb("th1", ["a"]) is True - - # Insert returns None id => False - manager.mongo_db = FakeMongoDB("insert_none") - assert manager._persist_to_mongodb("th2", ["a"]) is False - - # Insert raises => False - manager.mongo_db = FakeMongoDB("insert_raise") - assert manager._persist_to_mongodb("th3", ["a"]) is False - - # Update success - manager.mongo_db = FakeMongoDB("update_success") - assert manager._persist_to_mongodb("th4", ["a"]) is True - - # Update modifies 0 => False - manager.mongo_db = FakeMongoDB("update_zero") - assert manager._persist_to_mongodb("th5", ["a"]) is False + # Test error case by mocking collection methods + original_find_one = collection.find_one + collection.find_one = MagicMock(side_effect=RuntimeError("Database error")) + + assert manager._persist_to_mongodb("th2", ["message"]) is False + + # Restore original method + collection.find_one = original_find_one def test_postgresql_insert_update_and_error_paths(): @@ -570,38 +590,23 @@ def test_context_manager_calls_close(monkeypatch): def test_init_mongodb_success_and_failure(monkeypatch): - """MongoDB init should succeed with a valid client and fail gracefully otherwise.""" - - class FakeAdmin: - def command(self, name): - assert name == "ping" - - class DummyDB: - pass - - class FakeClient: - def __init__(self, uri): - self.uri = uri - self.admin = FakeAdmin() - self.checkpointing_db = DummyDB() - - def close(self): - pass - - # Success path - monkeypatch.setattr(checkpoint, "MongoClient", lambda uri: FakeClient(uri)) - manager = checkpoint.ChatStreamManager(checkpoint_saver=True, db_uri=MONGO_URL) - assert manager.mongo_db is not None + """MongoDB init should succeed with mongomock and fail gracefully with errors.""" + + # Success path with mongomock + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + mock_client = mongomock.MongoClient() + mock_mongo_client.return_value = mock_client + + manager = checkpoint.ChatStreamManager(checkpoint_saver=True, db_uri=MONGO_URL) + assert manager.mongo_db is not None # Failure path - class Boom: - def __init__(self, uri): - raise RuntimeError("fail connect") - - monkeypatch.setattr(checkpoint, "MongoClient", Boom) - manager = checkpoint.ChatStreamManager(checkpoint_saver=True, db_uri=MONGO_URL) - # Should have no mongo_db set on failure - assert getattr(manager, "mongo_db", None) is None + with patch('src.graph.checkpoint.MongoClient') as mock_mongo_client: + mock_mongo_client.side_effect = RuntimeError("Connection failed") + + manager = checkpoint.ChatStreamManager(checkpoint_saver=True, db_uri=MONGO_URL) + # Should have no mongo_db set on failure + assert getattr(manager, "mongo_db", None) is None def test_init_postgresql_calls_connect_and_create_table(monkeypatch): diff --git a/uv.lock b/uv.lock index 91f14d7..11e9675 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", @@ -116,6 +116,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/1e/e7f0393e836b5347605fc356c24d9f9ae9b26e0f7e52573b80e3d28335eb/arxiv-2.2.0-py3-none-any.whl", hash = "sha256:545b8af5ab301efff7697cd112b5189e631b80521ccbc33fbc1e1f9cff63ca4d", size = 11696, upload-time = "2025-04-08T06:16:08.844Z" }, ] +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162, upload-time = "2024-10-20T00:29:41.88Z" }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025, upload-time = "2024-10-20T00:29:43.352Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243, upload-time = "2024-10-20T00:29:44.922Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059, upload-time = "2024-10-20T00:29:46.891Z" }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596, upload-time = "2024-10-20T00:29:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632, upload-time = "2024-10-20T00:29:50.768Z" }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186, upload-time = "2024-10-20T00:29:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064, upload-time = "2024-10-20T00:29:53.757Z" }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload-time = "2024-10-20T00:30:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload-time = "2024-10-20T00:30:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload-time = "2024-10-20T00:30:04.501Z" }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload-time = "2024-10-20T00:30:06.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" }, +] + +[[package]] +name = "asyncpg-stubs" +version = "0.30.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asyncpg" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/e5/1a06ecec2a77a75974ba6b22d3bed697193177c0ed7595cce4dd2362735d/asyncpg_stubs-0.30.2.tar.gz", hash = "sha256:b8a1b7cb790a7b8a0e4e64e438a97c3fac77ea02441b563b1975748f18af33ab", size = 20250, upload-time = "2025-06-27T20:03:15.712Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/22/77a4a08cc9ef4f8bbb5e7ffbf4be008e596b535a3533a28c3465e9400d75/asyncpg_stubs-0.30.2-py3-none-any.whl", hash = "sha256:e57818bbaf10945a60ff3219da3c5ce97e1b424503b6a6f0a18db99797397cbb", size = 26929, upload-time = "2025-06-27T20:03:14.847Z" }, +] + [[package]] name = "attrs" version = "25.1.0" @@ -370,6 +407,7 @@ dependencies = [ { name = "mcp" }, { name = "numpy" }, { name = "pandas" }, + { name = "psycopg", extra = ["binary"] }, { name = "python-dotenv" }, { name = "readabilipy" }, { name = "socksio" }, @@ -385,14 +423,18 @@ dev = [ { name = "ruff" }, ] test = [ + { name = "asyncpg-stubs" }, + { name = "mongomock" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-postgresql" }, ] [package.metadata] requires-dist = [ { name = "arxiv", specifier = ">=2.2.0" }, + { name = "asyncpg-stubs", marker = "extra == 'test'", specifier = ">=0.30.2" }, { name = "duckduckgo-search", specifier = ">=8.0.0" }, { name = "fastapi", specifier = ">=0.110.0" }, { name = "httpx", specifier = ">=0.28.1" }, @@ -412,11 +454,15 @@ requires-dist = [ { name = "litellm", specifier = ">=1.63.11" }, { name = "markdownify", specifier = ">=1.1.0" }, { name = "mcp", specifier = ">=1.11.0" }, + { name = "mongomock", marker = "extra == 'test'", specifier = ">=4.3.0" }, { name = "numpy", specifier = ">=2.2.3" }, { name = "pandas", specifier = ">=2.2.3" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2.9" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.4.0" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.1.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.0.0" }, + { name = "pytest-postgresql", marker = "extra == 'test'", specifier = ">=7.0.2" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "readabilipy", specifier = ">=0.3.0" }, { name = "ruff", marker = "extra == 'dev'" }, @@ -1324,6 +1370,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/cf/3fd38cfe43962452e4bfadc6966b2ea0afaf8e0286cb3991c247c8c33ebd/mcp-1.12.2-py3-none-any.whl", hash = "sha256:b86d584bb60193a42bd78aef01882c5c42d614e416cbf0480149839377ab5a5f", size = 158473, upload-time = "2025-07-24T18:29:03.419Z" }, ] +[[package]] +name = "mirakuru" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "psutil", marker = "sys_platform != 'cygwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/57/bfa1e5b904b18f669e03b7c6981bb92fb473b7da9c3b082a875e25bfaa8c/mirakuru-2.6.1.tar.gz", hash = "sha256:95d4f5a5ad406a625e9ca418f20f8e09386a35dad1ea30fd9073e0ae93f712c7", size = 26889, upload-time = "2025-07-02T07:18:41.234Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/ce/139df7074328119869a1041ce91c082d78287541cf867f9c4c85097c5d8b/mirakuru-2.6.1-py3-none-any.whl", hash = "sha256:4be0bfd270744454fa0c0466b8127b66bd55f4decaf05bbee9b071f2acbd9473", size = 26202, upload-time = "2025-07-02T07:18:39.951Z" }, +] + +[[package]] +name = "mongomock" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytz" }, + { name = "sentinels" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/a4/4a560a9f2a0bec43d5f63104f55bc48666d619ca74825c8ae156b08547cf/mongomock-4.3.0.tar.gz", hash = "sha256:32667b79066fabc12d4f17f16a8fd7361b5f4435208b3ba32c226e52212a8c30", size = 135862, upload-time = "2024-11-16T11:23:25.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/4d/8bea712978e3aff017a2ab50f262c620e9239cc36f348aae45e48d6a4786/mongomock-4.3.0-py2.py3-none-any.whl", hash = "sha256:5ef86bd12fc8806c6e7af32f21266c61b6c4ba96096f85129852d1c4fec1327e", size = 64891, upload-time = "2024-11-16T11:23:24.748Z" }, +] + [[package]] name = "motor" version = "3.7.1" @@ -1584,6 +1656,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] +[[package]] +name = "port-for" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/84/ad5114c85217426d7a5170a74a6f9d6b724df117c2f3b75e41fc9d6c6811/port_for-0.7.4.tar.gz", hash = "sha256:fc7713e7b22f89442f335ce12536653656e8f35146739eccaeff43d28436028d", size = 25077, upload-time = "2024-10-09T12:28:38.875Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/a2/579dcefbb0285b31f8d65b537f8a9932ed51319e0a3694e01b5bbc271f92/port_for-0.7.4-py3-none-any.whl", hash = "sha256:08404aa072651a53dcefe8d7a598ee8a1dca320d9ac44ac464da16ccf2a02c4a", size = 21369, upload-time = "2024-10-09T12:28:37.853Z" }, +] + [[package]] name = "primp" version = "0.14.0" @@ -1669,6 +1750,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/35/6c4c6fc8774a9e3629cd750dc24a7a4fb090a25ccd5c3246d127b70f9e22/propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043", size = 12101, upload-time = "2025-02-20T19:03:27.202Z" }, ] +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + [[package]] name = "psycopg" version = "3.2.9" @@ -1682,6 +1778,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" }, ] +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.2.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205, upload-time = "2025-05-13T16:07:48.195Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795, upload-time = "2025-05-13T16:07:50.917Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043, upload-time = "2025-05-13T16:07:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972, upload-time = "2025-05-13T16:07:57.925Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516, upload-time = "2025-05-13T16:08:01.616Z" }, + { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160, upload-time = "2025-05-13T16:08:04.278Z" }, + { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518, upload-time = "2025-05-13T16:08:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598, upload-time = "2025-05-13T16:08:09.999Z" }, + { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289, upload-time = "2025-05-13T16:08:12.66Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493, upload-time = "2025-05-13T16:08:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400, upload-time = "2025-05-13T16:08:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127, upload-time = "2025-05-13T16:08:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322, upload-time = "2025-05-13T16:08:24.049Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097, upload-time = "2025-05-13T16:08:27.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114, upload-time = "2025-05-13T16:08:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693, upload-time = "2025-05-13T16:08:34.625Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423, upload-time = "2025-05-13T16:08:37.444Z" }, + { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667, upload-time = "2025-05-13T16:08:40.116Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576, upload-time = "2025-05-13T16:08:43.243Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439, upload-time = "2025-05-13T16:08:47.321Z" }, + { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477, upload-time = "2025-05-13T16:08:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009, upload-time = "2025-05-13T16:08:53.67Z" }, +] + [[package]] name = "psycopg-pool" version = "3.2.6" @@ -1856,6 +1986,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949, upload-time = "2024-10-29T20:13:33.215Z" }, ] +[[package]] +name = "pytest-postgresql" +version = "7.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mirakuru" }, + { name = "packaging" }, + { name = "port-for" }, + { name = "psycopg" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/15/b3c07d1537c7608c3f45d3ee6f778a56b1daa480221bb500abc9e44e01a0/pytest_postgresql-7.0.2.tar.gz", hash = "sha256:57c8d3f7d4e91d0ea8b2eac786d04f60080fa6ed6e66f1f94d747c71c9e5a4f4", size = 50691, upload-time = "2025-05-17T20:17:59.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/57/f2db5a80b10c3ac48ce41786cb9b14172f997509ee1b1055ab7db4238e5e/pytest_postgresql-7.0.2-py3-none-any.whl", hash = "sha256:0b0d31c51620a9c1d6be93286af354256bc58a47c379f56f4147b22da6e81fb5", size = 41447, upload-time = "2025-05-17T20:17:58.011Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2103,6 +2249,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278, upload-time = "2025-05-15T14:08:54.56Z" }, ] +[[package]] +name = "sentinels" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/9b/07195878aa25fe6ed209ec74bc55ae3e3d263b60a489c6e73fdca3c8fe05/sentinels-1.1.1.tar.gz", hash = "sha256:3c2f64f754187c19e0a1a029b148b74cf58dd12ec27b4e19c0e5d6e22b5a9a86", size = 4393, upload-time = "2025-08-12T07:57:50.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/65/dea992c6a97074f6d8ff9eab34741298cac2ce23e2b6c74fb7d08afdf85c/sentinels-1.1.1-py3-none-any.whl", hash = "sha256:835d3b28f3b47f5284afa4bf2db6e00f2dc5f80f9923d4b7e7aeeeccf6146a11", size = 3744, upload-time = "2025-08-12T07:57:48.858Z" }, +] + [[package]] name = "sgmllib3k" version = "1.0.0"