From 18b0f787b33a6de61988af06c6fb3398167eebec Mon Sep 17 00:00:00 2001 From: Abhijitam01 Date: Thu, 9 Apr 2026 01:10:46 +0530 Subject: [PATCH 1/3] feat: add Semantic Kernel plugin for Moss semantic search (#82) Adds a Python Semantic Kernel plugin that exposes Moss retrieval as a @kernel_function, letting any SK agent query a Moss index with 3 lines of code. Mirrors the strands-agents-moss integration pattern. Includes plugin implementation, 14 unit/integration tests, example, README with Quick Start and configuration docs, and TODOS.md for follow-up work (CI/CD pipeline, .NET design doc). --- TODOS.md | 21 ++ packages/semantic-kernel-moss/LICENSE | 25 ++ packages/semantic-kernel-moss/README.md | 98 +++++++ .../examples/moss_sk_simple.py | 28 ++ packages/semantic-kernel-moss/pyproject.toml | 55 ++++ .../src/semantic_kernel_moss/__init__.py | 22 ++ .../src/semantic_kernel_moss/moss_plugin.py | 147 ++++++++++ .../tests/test_moss_plugin.py | 252 ++++++++++++++++++ 8 files changed, 648 insertions(+) create mode 100644 TODOS.md create mode 100644 packages/semantic-kernel-moss/LICENSE create mode 100644 packages/semantic-kernel-moss/README.md create mode 100644 packages/semantic-kernel-moss/examples/moss_sk_simple.py create mode 100644 packages/semantic-kernel-moss/pyproject.toml create mode 100644 packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py create mode 100644 packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py create mode 100644 packages/semantic-kernel-moss/tests/test_moss_plugin.py diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 00000000..9d6175b6 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,21 @@ +# TODOs + +## CI/CD pipeline for integration packages + +**What:** Add GitHub Actions workflows to build, test, and publish `strands-agents-moss` and `semantic-kernel-moss` to PyPI. + +**Why:** Neither integration package has a CI/CD pipeline. Code without distribution is code nobody can use. Users currently have no way to `pip install` these packages from PyPI. + +**Context:** Both packages use setuptools with `pyproject.toml`. Tests use pytest + pytest-asyncio. Linting uses ruff. A single reusable workflow could cover both packages since they share the same build system and test tooling. Consider matrix strategy for Python 3.10-3.13. + +**Depends on:** Nothing. Can be done independently. + +## .NET Semantic Kernel plugin design doc + +**What:** Write a design document for a .NET version of the Moss Semantic Kernel plugin. + +**Why:** The GitHub issue (#82) lists .NET as a stretch goal. Semantic Kernel has strong .NET adoption in enterprise shops. A design doc captures the approach (NuGet package, IKernelPlugin interface, C# async patterns) without committing to implementation. + +**Context:** The Python plugin (`semantic-kernel-moss`) is the reference implementation. The .NET version would follow the same pattern: single `Search` kernel function, constructor-configured `MossClient`, pre-loaded index. Key decisions: whether to use the .NET Moss SDK (if it exists) or wrap the Python SDK, and how to handle the async index loading lifecycle in C#. + +**Depends on:** Python plugin shipped and validated by users. diff --git a/packages/semantic-kernel-moss/LICENSE b/packages/semantic-kernel-moss/LICENSE new file mode 100644 index 00000000..8e7b3214 --- /dev/null +++ b/packages/semantic-kernel-moss/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2025, InferEdge Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/semantic-kernel-moss/README.md b/packages/semantic-kernel-moss/README.md new file mode 100644 index 00000000..ad533054 --- /dev/null +++ b/packages/semantic-kernel-moss/README.md @@ -0,0 +1,98 @@ +# Semantic Kernel Moss Plugin + +Moss delivers sub-10ms semantic retrieval, giving your [Semantic Kernel](https://learn.microsoft.com/en-us/semantic-kernel/) agents instant access to a knowledge base during conversations. + +## Installation + +```bash +pip install semantic-kernel-moss +``` + +## Prerequisites + +- Moss project ID and project key (get them from [Moss Portal](https://portal.usemoss.dev)) +- Python 3.10+ +- A Semantic Kernel [ChatCompletion service](https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/) configured in your kernel + +## Quick Start + +```python +import asyncio +import os + +import semantic_kernel as sk +from semantic_kernel_moss import MossPlugin + + +async def main(): + # Create and pre-load the Moss plugin + moss = MossPlugin( + project_id=os.getenv("MOSS_PROJECT_ID"), + project_key=os.getenv("MOSS_PROJECT_KEY"), + index_name="my-index", + ) + await moss.load_index() + + # Register with a Semantic Kernel + kernel = sk.Kernel() + kernel.add_plugin(moss, plugin_name="moss") + + # Invoke the search function directly + result = await kernel.invoke(function_name="search", plugin_name="moss", query="What is your refund policy?") + print(result) + + +asyncio.run(main()) +``` + +## Configuration Options + +### MossPlugin + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `project_id` | `MOSS_PROJECT_ID` env var | Moss project ID | +| `project_key` | `MOSS_PROJECT_KEY` env var | Moss project key | +| `index_name` | (required) | Name of the Moss index to query | +| `top_k` | `5` | Number of results to retrieve per query | +| `alpha` | `0.8` | Blend: 1.0 = semantic only, 0.0 = keyword only | +| `result_prefix` | `Relevant knowledge base results:\n\n` | Prefix for formatted results | + +### Methods + +| Method | Description | +|--------|-------------| +| `load_index()` | Async. Pre-load the Moss index for fast first queries | +| `search(query)` | Async. Query Moss and return formatted results as a string | + +## Using with Chat Completion + +Moss works with any Semantic Kernel chat completion service. The kernel can automatically invoke the search function when the LLM decides it needs knowledge base information: + +```python +import semantic_kernel as sk +from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion +from semantic_kernel_moss import MossPlugin + +kernel = sk.Kernel() +kernel.add_service(OpenAIChatCompletion(service_id="chat")) + +moss = MossPlugin(index_name="product-docs") +await moss.load_index() +kernel.add_plugin(moss, plugin_name="moss") + +result = await kernel.invoke_prompt( + "Use the moss-search function to answer: {{$input}}", + input_vars={"input": "What are your shipping options?"}, +) +``` + +## License + +This plugin is provided under the [BSD 2-Clause License](LICENSE). + +## Support + +- [Moss Docs](https://docs.moss.dev) +- [Moss Discord](https://discord.com/invite/eMXExuafBR) +- [Semantic Kernel Docs](https://learn.microsoft.com/en-us/semantic-kernel/) diff --git a/packages/semantic-kernel-moss/examples/moss_sk_simple.py b/packages/semantic-kernel-moss/examples/moss_sk_simple.py new file mode 100644 index 00000000..4f491469 --- /dev/null +++ b/packages/semantic-kernel-moss/examples/moss_sk_simple.py @@ -0,0 +1,28 @@ +"""Minimal demo: Moss semantic search with a Semantic Kernel agent.""" + +import asyncio +import os + +import semantic_kernel as sk + +from semantic_kernel_moss import MossPlugin + + +async def main(): + """Run a Semantic Kernel agent with Moss semantic search.""" + moss = MossPlugin( + project_id=os.getenv("MOSS_PROJECT_ID"), + project_key=os.getenv("MOSS_PROJECT_KEY"), + index_name=os.getenv("MOSS_INDEX_NAME", "my-index"), + ) + await moss.load_index() + + kernel = sk.Kernel() + kernel.add_plugin(moss, plugin_name="moss") + + result = await kernel.invoke(function_name="search", plugin_name="moss", query="What are your shipping options?") + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/packages/semantic-kernel-moss/pyproject.toml b/packages/semantic-kernel-moss/pyproject.toml new file mode 100644 index 00000000..84add974 --- /dev/null +++ b/packages/semantic-kernel-moss/pyproject.toml @@ -0,0 +1,55 @@ +[project] +name = "semantic-kernel-moss" +version = "0.0.1" +description = "Moss semantic search plugin for Semantic Kernel" +readme = "README.md" +requires-python = ">=3.10,<3.14" +dependencies = [ + "inferedge-moss>=1.0.0b18", + "semantic-kernel>=1.0.0", +] + +[dependency-groups] +dev = [ + "pytest>=7.0", + "pytest-asyncio>=0.21", + "python-dotenv>=1.2.1", + "ruff>=0.1.0", +] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "UP", # pyupgrade + "D", # pydocstyle +] +ignore = [ + "D100", # Missing docstring in public module + "D104", # Missing docstring in public package +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + + +[tool.setuptools.packages.find] +where = ["src"] +namespaces = false diff --git a/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py b/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py new file mode 100644 index 00000000..f82b23f3 --- /dev/null +++ b/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py @@ -0,0 +1,22 @@ +"""Moss semantic search plugin for Semantic Kernel.""" + +from __future__ import annotations + +from inferedge_moss import ( + DocumentInfo, + GetDocumentsOptions, + IndexInfo, + MossClient, + SearchResult, +) + +from .moss_plugin import MossPlugin + +__all__ = [ + "DocumentInfo", + "GetDocumentsOptions", + "IndexInfo", + "MossClient", + "MossPlugin", + "SearchResult", +] diff --git a/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py b/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py new file mode 100644 index 00000000..bc099010 --- /dev/null +++ b/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py @@ -0,0 +1,147 @@ +# +# Copyright (c) 2025, InferEdge Inc. +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Semantic Kernel plugin for Moss semantic search.""" + +from __future__ import annotations + +import asyncio +import logging +from collections.abc import Sequence +from typing import Annotated, Any + +from inferedge_moss import MossClient, QueryOptions +from semantic_kernel.functions import kernel_function + +__all__ = ["MossPlugin"] + +logger = logging.getLogger("semantic_kernel_moss") + + +class MossPlugin: + """Provides Moss semantic search as a Semantic Kernel plugin. + + This class manages the MossClient lifecycle and exposes a + ``@kernel_function``-decorated ``search`` method that Semantic Kernel + discovers automatically when the plugin is registered. + + Usage:: + + import semantic_kernel as sk + from semantic_kernel_moss import MossPlugin + + moss = MossPlugin( + project_id="...", + project_key="...", + index_name="my-faq-index", + ) + await moss.load_index() + + kernel = sk.Kernel() + kernel.add_plugin(moss, plugin_name="moss") + + result = await kernel.invoke( + function_name="search", plugin_name="moss", query="What is your refund policy?" + ) + """ + + def __init__( + self, + *, + project_id: str | None = None, + project_key: str | None = None, + index_name: str, + top_k: int = 5, + alpha: float = 0.8, + result_prefix: str = "Relevant knowledge base results:\n\n", + ): + """Initialize with Moss credentials and retrieval settings. + + Args: + project_id: Moss project ID. Falls back to ``MOSS_PROJECT_ID`` env var. + project_key: Moss project key. Falls back to ``MOSS_PROJECT_KEY`` env var. + index_name: Name of the Moss index to query. + top_k: Number of results to retrieve per query. + alpha: Blend between semantic (1.0) and keyword (0.0) scoring. + result_prefix: Prefix prepended to formatted search results. + """ + self._client = MossClient(project_id=project_id, project_key=project_key) + self._index_name = index_name + self._top_k = top_k + self._alpha = alpha + self._result_prefix = result_prefix + self._index_loaded = False + self._load_lock = asyncio.Lock() + + async def load_index(self) -> None: + """Pre-load the Moss index for fast queries. + + Call this before registering the plugin with a Kernel so that + the first search invocation does not incur index-loading latency. + + This method is safe to call concurrently; only the first call + will actually load the index. + """ + async with self._load_lock: + if self._index_loaded: + return + logger.info("Loading Moss index '%s'", self._index_name) + await self._client.load_index(self._index_name) + self._index_loaded = True + logger.info("Moss index '%s' ready", self._index_name) + + @kernel_function( + name="search", + description="Search the knowledge base for documents relevant to a query", + ) + async def search( + self, + query: Annotated[str, "The search query to find relevant documents"], + ) -> str: + """Query the Moss index and return formatted results. + + Args: + query: The search query text. + + Returns: + Formatted string of search results suitable for LLM context, + or a short message if no results are found. + """ + if not self._index_loaded: + raise RuntimeError( + f"Index '{self._index_name}' not loaded. Call await load_index() first." + ) + + result = await self._client.query( + self._index_name, + query, + options=QueryOptions(top_k=self._top_k, alpha=self._alpha), + ) + logger.info( + "Moss query returned %d docs in %sms", + len(result.docs), + result.time_taken_ms, + ) + + return self._format_results(result.docs) + + def _format_results(self, documents: Sequence[Any]) -> str: + """Format Moss search results into a numbered string.""" + if not documents: + return "No relevant results found." + + lines = [self._result_prefix.rstrip(), ""] + for idx, doc in enumerate(documents, start=1): + meta = doc.metadata or {} + extras = [] + if source := meta.get("source"): + extras.append(f"source={source}") + if (score := getattr(doc, "score", None)) is not None: + extras.append(f"score={score:.3f}") + suffix = f" ({', '.join(extras)})" if extras else "" + text = getattr(doc, "text", "") or "" + lines.append(f"{idx}. {text}{suffix}") + return "\n".join(lines).strip() diff --git a/packages/semantic-kernel-moss/tests/test_moss_plugin.py b/packages/semantic-kernel-moss/tests/test_moss_plugin.py new file mode 100644 index 00000000..8236558f --- /dev/null +++ b/packages/semantic-kernel-moss/tests/test_moss_plugin.py @@ -0,0 +1,252 @@ +"""Tests for the Semantic Kernel Moss plugin.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from semantic_kernel_moss import MossPlugin + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_mock_search_result(docs=None): + """Create a mock SearchResult.""" + result = MagicMock() + result.docs = docs or [] + result.time_taken_ms = 2.5 + return result + + +def _make_mock_doc(text="sample text", score=0.95, doc_id="doc-1", metadata=None): + """Create a mock DocumentInfo.""" + doc = MagicMock() + doc.text = text + doc.score = score + doc.id = doc_id + doc.metadata = metadata or {} + return doc + + +# --------------------------------------------------------------------------- +# MossPlugin – init tests +# --------------------------------------------------------------------------- + + +class TestMossPluginInit: + """Tests for MossPlugin initialization.""" + + def test_default_values(self): + """Verify default parameter values on construction.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + assert plugin._index_name == "idx" + assert plugin._top_k == 5 + assert plugin._alpha == 0.8 + assert plugin._index_loaded is False + + def test_custom_values(self): + """Verify custom parameter values are stored.""" + plugin = MossPlugin( + project_id="pid", + project_key="pkey", + index_name="idx", + top_k=10, + alpha=0.5, + ) + assert plugin._top_k == 10 + assert plugin._alpha == 0.5 + + +# --------------------------------------------------------------------------- +# MossPlugin – load_index tests +# --------------------------------------------------------------------------- + + +class TestMossPluginLoadIndex: + """Tests for load_index.""" + + @pytest.mark.asyncio + async def test_load_index_sets_flag(self): + """Verify load_index delegates to client and sets the loaded flag.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + with patch.object(plugin._client, "load_index", new_callable=AsyncMock) as mock_load: + await plugin.load_index() + mock_load.assert_called_once_with("idx") + assert plugin._index_loaded is True + + @pytest.mark.asyncio + async def test_load_index_only_loads_once(self): + """Verify concurrent load_index calls only trigger one actual load.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + with patch.object(plugin._client, "load_index", new_callable=AsyncMock) as mock_load: + await plugin.load_index() + await plugin.load_index() + mock_load.assert_called_once() + + +# --------------------------------------------------------------------------- +# MossPlugin – search tests +# --------------------------------------------------------------------------- + + +class TestMossPluginSearch: + """Tests for the search method.""" + + @pytest.mark.asyncio + async def test_search_returns_formatted_results(self): + """Verify search formats multiple docs with scores.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + plugin._index_loaded = True + + docs = [ + _make_mock_doc("First result", 0.95, "d1"), + _make_mock_doc("Second result", 0.80, "d2"), + ] + mock_result = _make_mock_search_result(docs) + + with patch.object( + plugin._client, "query", new_callable=AsyncMock, return_value=mock_result + ): + output = await plugin.search("test query") + assert "First result" in output + assert "Second result" in output + assert "score=0.950" in output + assert "score=0.800" in output + + @pytest.mark.asyncio + async def test_search_empty_results(self): + """Verify empty results produce a 'no results' message.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + plugin._index_loaded = True + + mock_result = _make_mock_search_result([]) + with patch.object( + plugin._client, "query", new_callable=AsyncMock, return_value=mock_result + ): + output = await plugin.search("test query") + assert "No relevant results found" in output + + @pytest.mark.asyncio + async def test_search_raises_if_index_not_loaded(self): + """Verify search raises RuntimeError before load_index is called.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + with pytest.raises(RuntimeError, match="not loaded"): + await plugin.search("test query") + + @pytest.mark.asyncio + async def test_search_with_metadata_source(self): + """Verify source metadata is included in formatted output.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + plugin._index_loaded = True + + docs = [_make_mock_doc("doc text", 0.9, "d1", {"source": "faq.md"})] + mock_result = _make_mock_search_result(docs) + + with patch.object( + plugin._client, "query", new_callable=AsyncMock, return_value=mock_result + ): + output = await plugin.search("q") + assert "source=faq.md" in output + + @pytest.mark.asyncio + async def test_search_propagates_client_exceptions(self): + """Verify client exceptions bubble up without being caught.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + plugin._index_loaded = True + + with patch.object( + plugin._client, + "query", + new_callable=AsyncMock, + side_effect=ConnectionError("network failure"), + ): + with pytest.raises(ConnectionError, match="network failure"): + await plugin.search("test query") + + +# --------------------------------------------------------------------------- +# Kernel integration tests +# --------------------------------------------------------------------------- + + +class TestKernelIntegration: + """Tests that verify the plugin works with a real SK Kernel.""" + + @pytest.mark.asyncio + async def test_plugin_discoverable_by_kernel(self): + """Verify kernel discovers the search function after add_plugin.""" + import semantic_kernel as sk + + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + kernel = sk.Kernel() + kernel.add_plugin(plugin, plugin_name="moss") + + moss_plugin = kernel.get_plugin("moss") + assert moss_plugin is not None + assert "search" in moss_plugin + + @pytest.mark.asyncio + async def test_kernel_invoke_returns_string(self): + """Verify full round-trip: kernel.invoke returns formatted results.""" + import semantic_kernel as sk + + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + plugin._index_loaded = True + + docs = [_make_mock_doc("kernel result", 0.92, "d1")] + mock_result = _make_mock_search_result(docs) + + kernel = sk.Kernel() + kernel.add_plugin(plugin, plugin_name="moss") + + with patch.object( + plugin._client, "query", new_callable=AsyncMock, return_value=mock_result + ): + result = await kernel.invoke(function_name="search", plugin_name="moss", query="test") + output = str(result) + assert "kernel result" in output + assert "score=0.920" in output + + +# --------------------------------------------------------------------------- +# Format results – edge cases +# --------------------------------------------------------------------------- + + +class TestFormatResults: + """Tests for _format_results edge cases.""" + + def test_format_with_no_documents(self): + """Verify empty doc list returns fallback message.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + result = plugin._format_results([]) + assert "No relevant results found" in result + + def test_format_with_custom_prefix(self): + """Verify custom result_prefix is used in output.""" + plugin = MossPlugin( + project_id="pid", + project_key="pkey", + index_name="idx", + result_prefix="Search results:\n\n", + ) + docs = [_make_mock_doc("hello", 0.99)] + result = plugin._format_results(docs) + assert result.startswith("Search results:") + assert "hello" in result + + def test_format_numbers_results(self): + """Verify docs are numbered sequentially starting at 1.""" + plugin = MossPlugin(project_id="pid", project_key="pkey", index_name="idx") + docs = [ + _make_mock_doc("first", 0.9), + _make_mock_doc("second", 0.8), + _make_mock_doc("third", 0.7), + ] + result = plugin._format_results(docs) + assert "1. first" in result + assert "2. second" in result + assert "3. third" in result From 872de475599153895d96b672c5a8be6a6660049a Mon Sep 17 00:00:00 2001 From: Abhijitam01 Date: Thu, 9 Apr 2026 01:41:02 +0530 Subject: [PATCH 2/3] fix: use moss package instead of inferedge-moss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with the rest of the repo — all other integration packages (strands-agents-moss, elevenlabs-moss, pipecat-moss, moss-cli) depend on moss and import from moss, not inferedge-moss. --- packages/semantic-kernel-moss/pyproject.toml | 2 +- .../semantic-kernel-moss/src/semantic_kernel_moss/__init__.py | 2 +- .../src/semantic_kernel_moss/moss_plugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/semantic-kernel-moss/pyproject.toml b/packages/semantic-kernel-moss/pyproject.toml index 84add974..a68a154d 100644 --- a/packages/semantic-kernel-moss/pyproject.toml +++ b/packages/semantic-kernel-moss/pyproject.toml @@ -5,7 +5,7 @@ description = "Moss semantic search plugin for Semantic Kernel" readme = "README.md" requires-python = ">=3.10,<3.14" dependencies = [ - "inferedge-moss>=1.0.0b18", + "moss>=1.0.0b18", "semantic-kernel>=1.0.0", ] diff --git a/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py b/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py index f82b23f3..69622e76 100644 --- a/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py +++ b/packages/semantic-kernel-moss/src/semantic_kernel_moss/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from inferedge_moss import ( +from moss import ( DocumentInfo, GetDocumentsOptions, IndexInfo, diff --git a/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py b/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py index bc099010..13e2436e 100644 --- a/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py +++ b/packages/semantic-kernel-moss/src/semantic_kernel_moss/moss_plugin.py @@ -13,7 +13,7 @@ from collections.abc import Sequence from typing import Annotated, Any -from inferedge_moss import MossClient, QueryOptions +from moss import MossClient, QueryOptions from semantic_kernel.functions import kernel_function __all__ = ["MossPlugin"] From 64daf7bc1fa59cda49e385dbc434a24ee50b258a Mon Sep 17 00:00:00 2001 From: Abhijitam01 Date: Sun, 26 Apr 2026 13:19:21 +0530 Subject: [PATCH 3/3] feat: add moss-connector-dynamodb; DynamoDB source connector Add a new DynamoDB source connector package that reads every item from a DynamoDB table via boto3 scan with automatic pagination, maps each item to a DocumentInfo via a user-supplied mapper, and ingests them into a Moss search index. Key design decisions: - scan_kwargs dict for all DynamoDB scan options (FilterExpression, ProjectionExpression, ExpressionAttributeNames, etc.) - endpoint_url param for DynamoDB Local / localstack dev/testing - Standard boto3 credential chain (no AWS credential params) - Immutable scan_kwargs copy in __iter__ to prevent mutation Also pulls in the existing moss-data-connector packages (template, SQLite, MongoDB) from upstream/main, updates the parent README with the DynamoDB row, and adds a streaming/batched ingest TODO. Closes #171 Co-Authored-By: Claude Opus 4.6 --- TODOS.md | 10 + packages/moss-data-connector/README.md | 43 ++ .../moss-data-connector/_template/.gitignore | 10 + .../moss-data-connector/_template/README.md | 49 +++ .../_template/pyproject.toml | 60 +++ .../_template/src/__init__.py | 10 + .../_template/src/connector.py | 33 ++ .../_template/src/ingest.py | 22 + .../_template/tests/test_template.py | 35 ++ .../moss-connector-dynamodb/.gitignore | 10 + .../moss-connector-dynamodb/README.md | 80 ++++ .../moss-connector-dynamodb/pyproject.toml | 56 +++ .../moss-connector-dynamodb/src/__init__.py | 6 + .../moss-connector-dynamodb/src/connector.py | 57 +++ .../moss-connector-dynamodb/src/ingest.py | 22 + .../tests/test_dynamodb.py | 174 ++++++++ .../tests/test_integration_dynamodb_moss.py | 89 ++++ .../moss-connector-mongodb/.gitignore | 10 + .../moss-connector-mongodb/README.md | 64 +++ .../moss-connector-mongodb/pyproject.toml | 56 +++ .../moss-connector-mongodb/src/__init__.py | 9 + .../moss-connector-mongodb/src/connector.py | 54 +++ .../moss-connector-mongodb/src/ingest.py | 22 + .../tests/test_integration_mongodb_moss.py | 173 ++++++++ .../tests/test_mongodb.py | 107 +++++ .../moss-connector-mongodb/uv.lock | 416 ++++++++++++++++++ .../moss-connector-sqlite/.gitignore | 10 + .../moss-connector-sqlite/README.md | 57 +++ .../moss-connector-sqlite/pyproject.toml | 55 +++ .../moss-connector-sqlite/src/__init__.py | 9 + .../moss-connector-sqlite/src/connector.py | 38 ++ .../moss-connector-sqlite/src/ingest.py | 22 + .../tests/test_integration_sqlite_moss.py | 116 +++++ .../tests/test_sqlite.py | 104 +++++ .../moss-connector-sqlite/uv.lock | 334 ++++++++++++++ 35 files changed, 2422 insertions(+) create mode 100644 packages/moss-data-connector/README.md create mode 100644 packages/moss-data-connector/_template/.gitignore create mode 100644 packages/moss-data-connector/_template/README.md create mode 100644 packages/moss-data-connector/_template/pyproject.toml create mode 100644 packages/moss-data-connector/_template/src/__init__.py create mode 100644 packages/moss-data-connector/_template/src/connector.py create mode 100644 packages/moss-data-connector/_template/src/ingest.py create mode 100644 packages/moss-data-connector/_template/tests/test_template.py create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/.gitignore create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/README.md create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/pyproject.toml create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/src/__init__.py create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/src/connector.py create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/src/ingest.py create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/tests/test_dynamodb.py create mode 100644 packages/moss-data-connector/moss-connector-dynamodb/tests/test_integration_dynamodb_moss.py create mode 100644 packages/moss-data-connector/moss-connector-mongodb/.gitignore create mode 100644 packages/moss-data-connector/moss-connector-mongodb/README.md create mode 100644 packages/moss-data-connector/moss-connector-mongodb/pyproject.toml create mode 100644 packages/moss-data-connector/moss-connector-mongodb/src/__init__.py create mode 100644 packages/moss-data-connector/moss-connector-mongodb/src/connector.py create mode 100644 packages/moss-data-connector/moss-connector-mongodb/src/ingest.py create mode 100644 packages/moss-data-connector/moss-connector-mongodb/tests/test_integration_mongodb_moss.py create mode 100644 packages/moss-data-connector/moss-connector-mongodb/tests/test_mongodb.py create mode 100644 packages/moss-data-connector/moss-connector-mongodb/uv.lock create mode 100644 packages/moss-data-connector/moss-connector-sqlite/.gitignore create mode 100644 packages/moss-data-connector/moss-connector-sqlite/README.md create mode 100644 packages/moss-data-connector/moss-connector-sqlite/pyproject.toml create mode 100644 packages/moss-data-connector/moss-connector-sqlite/src/__init__.py create mode 100644 packages/moss-data-connector/moss-connector-sqlite/src/connector.py create mode 100644 packages/moss-data-connector/moss-connector-sqlite/src/ingest.py create mode 100644 packages/moss-data-connector/moss-connector-sqlite/tests/test_integration_sqlite_moss.py create mode 100644 packages/moss-data-connector/moss-connector-sqlite/tests/test_sqlite.py create mode 100644 packages/moss-data-connector/moss-connector-sqlite/uv.lock diff --git a/TODOS.md b/TODOS.md index 9d6175b6..e9987d83 100644 --- a/TODOS.md +++ b/TODOS.md @@ -19,3 +19,13 @@ **Context:** The Python plugin (`semantic-kernel-moss`) is the reference implementation. The .NET version would follow the same pattern: single `Search` kernel function, constructor-configured `MossClient`, pre-loaded index. Key decisions: whether to use the .NET Moss SDK (if it exists) or wrap the Python SDK, and how to handle the async index loading lifecycle in C#. **Depends on:** Python plugin shipped and validated by users. + +## Streaming/batched ingest for large tables + +**What:** Change shared `ingest.py` to pass iterables directly or batch in chunks instead of `list(source)`. + +**Why:** Current `list()` call loads entire dataset into memory. For 1M+ row tables, that's 1GB+ RAM. + +**Context:** The connector `__iter__` already streams correctly. The bottleneck is the shared `ingest.py` doing eager collection. Fix would be ~5 lines but affects all connectors (SQLite, MongoDB, DynamoDB). + +**Depends on:** Checking if `MossClient.create_index()` accepts iterables. diff --git a/packages/moss-data-connector/README.md b/packages/moss-data-connector/README.md new file mode 100644 index 00000000..de999971 --- /dev/null +++ b/packages/moss-data-connector/README.md @@ -0,0 +1,43 @@ +# moss-data-connector + +Folder holding the database-connector packages. Each subfolder is its own pip-installable package + +## Layout + +``` +moss-data-connector/ +├── _template/ # copy-me starting point for a new connector +├── moss-connector-sqlite/ # SQLite source (stdlib, no driver) +├── moss-connector-mongodb/ # MongoDB source (requires pymongo) +└── moss-connector-dynamodb/ # DynamoDB source (requires boto3) +``` + + +## Caller shape + +```python +from moss import DocumentInfo +from moss_connector_sqlite import SQLiteConnector, ingest + +source = SQLiteConnector( + database="my.db", + query="SELECT id, title, body FROM articles", + mapper=lambda r: DocumentInfo(id=str(r["id"]), text=r["body"], metadata={"title": r["title"]}), +) + +await ingest(source, project_id="...", project_key="...", index_name="articles") +``` + + +## Available connectors + +| Package | Source | Extra driver | +| ---------------------------------------------------- | -------- | ------------ | +| [`moss-connector-sqlite`](moss-connector-sqlite) | SQLite | — | +| [`moss-connector-mongodb`](moss-connector-mongodb) | MongoDB | `pymongo` | +| [`moss-connector-dynamodb`](moss-connector-dynamodb) | DynamoDB | `boto3` | + +## Adding a new connector + +See [`_template/README.md`](_template/README.md). + diff --git a/packages/moss-data-connector/_template/.gitignore b/packages/moss-data-connector/_template/.gitignore new file mode 100644 index 00000000..b86b0644 --- /dev/null +++ b/packages/moss-data-connector/_template/.gitignore @@ -0,0 +1,10 @@ +build/ +dist/ +*.egg-info/ +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.env diff --git a/packages/moss-data-connector/_template/README.md b/packages/moss-data-connector/_template/README.md new file mode 100644 index 00000000..a0935fb5 --- /dev/null +++ b/packages/moss-data-connector/_template/README.md @@ -0,0 +1,49 @@ +# moss-connector-template + +Starting point for a new connector. Not a real package, don't install it. + +## To create a new connector + +```bash +cd packages/moss-data-connector +cp -r _template moss-connector- +cd moss-connector- +``` + +Then: + +1. Open `pyproject.toml` and replace every `TODO` (name, description, keywords, Source URL, driver deps). The package name is `moss-connector-`, the Python module is `moss_connector_`. +2. Open `src/connector.py` and: + - Rename `TemplateConnector` → `Connector`. + - Add your source-specific config to `__init__`. + - Implement `__iter__` (connect, pull rows, `yield self.mapper(row)`). +3. Update `src/__init__.py` to re-export your renamed class. +4. Rename `tests/test_template.py` → `tests/test_.py` and fill in. +5. Add a live integration test in `tests/test_integration__moss.py` if you can (see sqlite/mongodb for patterns). +6. Update this package's README with install + usage snippets (see `moss-connector-sqlite/README.md` for shape). +7. Add a row to `packages/moss-data-connector/README.md`. +8. Open a PR. + +## Rules + +- **One source per package.** Don't combine. +- **Declare your driver as a main dependency** in `pyproject.toml` and import it at the top of the module. +- **No retries or rate-limit logic in `ingest.py`.** If a connector needs it, put it in the connector's own code. + +## Caller shape (what users write against your connector) + +```python +from moss import DocumentInfo +from moss_connector_ import Connector, ingest + +source = Connector( + # your config here + mapper=lambda r: DocumentInfo( + id=str(r["id"]), + text=r["body"], + metadata={"title": r["title"]}, + ), +) + +await ingest(source, project_id="...", project_key="...", index_name="articles") +``` diff --git a/packages/moss-data-connector/_template/pyproject.toml b/packages/moss-data-connector/_template/pyproject.toml new file mode 100644 index 00000000..2adee086 --- /dev/null +++ b/packages/moss-data-connector/_template/pyproject.toml @@ -0,0 +1,60 @@ +[project] +# TODO: rename to "moss-connector-" +name = "moss-connector-template" +version = "0.0.1" +description = "TODO: short description of the source this connector reads from." +readme = "README.md" +requires-python = ">=3.10,<3.15" +license = { text = "BSD-2-Clause" } +authors = [{ name = "InferEdge Inc.", email = "contact@moss.dev" }] +# TODO: update keywords +keywords = ["moss", "connectors", "ingest"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", +] +dependencies = [ + "moss>=1.0.0", + # TODO: add your source's driver, e.g. "psycopg[binary]>=3.1" +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "python-dotenv>=1.0.0", + "ruff>=0.5.0", +] + +[project.urls] +Homepage = "https://github.com/usemoss/moss" +Repository = "https://github.com/usemoss/moss" +# TODO: update the Source path +Source = "https://github.com/usemoss/moss/tree/main/packages/moss-data-connector/moss-connector-template" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +# Flat layout: src/ itself IS the package. +# TODO: rename to "moss_connector_" to match your package name. +[tool.setuptools] +packages = ["moss_connector_template"] +package-dir = { "moss_connector_template" = "src" } + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "UP"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/packages/moss-data-connector/_template/src/__init__.py b/packages/moss-data-connector/_template/src/__init__.py new file mode 100644 index 00000000..dffc327a --- /dev/null +++ b/packages/moss-data-connector/_template/src/__init__.py @@ -0,0 +1,10 @@ +"""Template connector package. + +Copy this directory to `packages/moss-data-connector/moss-connector-/`, +then rename `TemplateConnector` in `connector.py` to `Connector`. +""" + +from .connector import TemplateConnector +from .ingest import ingest + +__all__ = ["TemplateConnector", "ingest"] diff --git a/packages/moss-data-connector/_template/src/connector.py b/packages/moss-data-connector/_template/src/connector.py new file mode 100644 index 00000000..a8147c0e --- /dev/null +++ b/packages/moss-data-connector/_template/src/connector.py @@ -0,0 +1,33 @@ +"""Connector class goes here. Rename both the file's class and the module's +host package (`moss_connector_template` → `moss_connector_`). +""" + +from __future__ import annotations + +from typing import Any, Callable, Iterator + +from moss import DocumentInfo + + +class TemplateConnector: + """Yield one `DocumentInfo` per row from your source. + + `mapper` turns one row dict into a `DocumentInfo`, the caller decides + which keys become id / text / metadata / embedding. + """ + + def __init__( + self, + # TODO: add your source-specific config here (connection string, query, etc.) + mapper: Callable[[dict[str, Any]], DocumentInfo], + ) -> None: + self.mapper = mapper + + def __iter__(self) -> Iterator[DocumentInfo]: + # TODO: connect to your source, pull rows, and for each one: + # yield self.mapper(row_as_dict) + # Don't pre-filter columns - the caller's mapper decides what to use. + # Import your driver *inside* this method, not at module top, so + # importing the package never fails just because the driver isn't + # installed. + raise NotImplementedError diff --git a/packages/moss-data-connector/_template/src/ingest.py b/packages/moss-data-connector/_template/src/ingest.py new file mode 100644 index 00000000..68c44940 --- /dev/null +++ b/packages/moss-data-connector/_template/src/ingest.py @@ -0,0 +1,22 @@ +"""Copy rows into a Moss index.""" + +from __future__ import annotations + +from collections.abc import Iterable + +from moss import DocumentInfo, MossClient, MutationResult + + +async def ingest( + source: Iterable[DocumentInfo], + project_id: str, + project_key: str, + index_name: str, + model_id: str | None = None, +) -> MutationResult | None: + """Copy every `DocumentInfo` from `source` into a fresh Moss index.""" + docs = list(source) + if not docs: + return None + client = MossClient(project_id, project_key) + return await client.create_index(index_name, docs, model_id=model_id) diff --git a/packages/moss-data-connector/_template/tests/test_template.py b/packages/moss-data-connector/_template/tests/test_template.py new file mode 100644 index 00000000..5f8701d1 --- /dev/null +++ b/packages/moss-data-connector/_template/tests/test_template.py @@ -0,0 +1,35 @@ +"""Template unit test. Rename to test_.py and adapt.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any +from unittest.mock import patch + +import pytest # noqa: F401 + +from moss import DocumentInfo # noqa: F401 + +# TODO: update these imports to match your renamed package. +# from moss_connector_ import Connector, ingest + + +@dataclass +class FakeMossClient: + """Records create_index calls without hitting the network.""" + + calls: list[dict[str, Any]] = field(default_factory=list) + + async def create_index(self, name, docs, model_id=None): + self.calls.append({"name": name, "docs": list(docs), "model_id": model_id}) + + +# Example test, adapt to your source. See moss-connector-sqlite/tests/test_sqlite.py +# for a worked example that uses a real stdlib driver + fake MossClient. +# +# async def test__ingest(): +# fake_moss = FakeMossClient() +# with patch("moss_connector_.ingest.MossClient", return_value=fake_moss): +# source = Connector(..., mapper=lambda r: DocumentInfo(...)) +# count = await ingest(source, "fake_id", "fake_key", "idx") +# assert count == ... diff --git a/packages/moss-data-connector/moss-connector-dynamodb/.gitignore b/packages/moss-data-connector/moss-connector-dynamodb/.gitignore new file mode 100644 index 00000000..b86b0644 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/.gitignore @@ -0,0 +1,10 @@ +build/ +dist/ +*.egg-info/ +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.env diff --git a/packages/moss-data-connector/moss-connector-dynamodb/README.md b/packages/moss-data-connector/moss-connector-dynamodb/README.md new file mode 100644 index 00000000..b12329c6 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/README.md @@ -0,0 +1,80 @@ +# moss-connector-dynamodb + +DynamoDB source connector for Moss. Scans an entire table (with optional filters) and ingests items into a Moss search index. + +## Install + +```bash +pip install moss-connector-dynamodb +``` + +Pulls `boto3` as a dependency. Uses the standard boto3 credential chain (env vars, shared credentials file, IAM role, etc.). + +## Usage + +```python +import asyncio +from moss import DocumentInfo +from moss_connector_dynamodb import DynamoDBConnector, ingest + +async def main(): + source = DynamoDBConnector( + table_name="articles", + mapper=lambda item: DocumentInfo( + id=str(item["id"]), + text=item["body"], + metadata={"title": item["title"]}, + ), + region_name="us-east-1", + scan_kwargs={ # optional + "FilterExpression": "#s = :val", + "ExpressionAttributeNames": {"#s": "status"}, + "ExpressionAttributeValues": {":val": "published"}, + }, + ) + + result = await ingest( + source, + project_id="your_project_id", + project_key="your_project_key", + index_name="articles", + ) + print(f"copied {result.doc_count} rows") + +asyncio.run(main()) +``` + +DynamoDB items come back as dicts with Python types (Decimal for numbers, etc.). Handle type coercion in your mapper. + +For large tables, `ingest()` loads all items into memory before indexing. Consider batching for tables with 100K+ rows. + +### Local development + +Pass `endpoint_url` to target DynamoDB Local or localstack: + +```python +source = DynamoDBConnector( + table_name="articles", + mapper=my_mapper, + endpoint_url="http://localhost:8000", +) +``` + +## Layout + +``` +src/ +├── __init__.py # re-exports DynamoDBConnector and ingest +├── connector.py # DynamoDBConnector class +└── ingest.py # ingest() - keep in sync with the other connector packages +``` + +## Tests + +```bash +pip install -e ".[dev]" +pytest tests/test_dynamodb.py -v # mocked Moss + mocked boto3 +pytest tests/test_integration_dynamodb_moss.py -v -s # live Moss + real DynamoDB +``` + +The live integration test requires `DYNAMODB_TABLE`, `AWS_REGION`, `MOSS_PROJECT_ID`, and `MOSS_PROJECT_KEY` env vars. Optionally set `DYNAMODB_ENDPOINT_URL` for DynamoDB Local. diff --git a/packages/moss-data-connector/moss-connector-dynamodb/pyproject.toml b/packages/moss-data-connector/moss-connector-dynamodb/pyproject.toml new file mode 100644 index 00000000..1a1cf98d --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/pyproject.toml @@ -0,0 +1,56 @@ +[project] +name = "moss-connector-dynamodb" +version = "0.0.1" +description = "DynamoDB source connector for moss-connectors." +readme = "README.md" +requires-python = ">=3.10,<3.15" +license = { text = "BSD-2-Clause" } +authors = [{ name = "InferEdge Inc.", email = "contact@moss.dev" }] +keywords = ["moss", "connectors", "dynamodb", "aws", "ingest", "etl"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", +] +dependencies = [ + "moss>=1.0.0", + "boto3>=1.28", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "python-dotenv>=1.0.0", + "ruff>=0.5.0", +] + +[project.urls] +Homepage = "https://github.com/usemoss/moss" +Repository = "https://github.com/usemoss/moss" +Source = "https://github.com/usemoss/moss/tree/main/packages/moss-data-connector/moss-connector-dynamodb" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +# Flat layout: src/ itself IS the package `moss_connector_dynamodb`. +[tool.setuptools] +packages = ["moss_connector_dynamodb"] +package-dir = { "moss_connector_dynamodb" = "src" } + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "UP"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/packages/moss-data-connector/moss-connector-dynamodb/src/__init__.py b/packages/moss-data-connector/moss-connector-dynamodb/src/__init__.py new file mode 100644 index 00000000..9a18657a --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/src/__init__.py @@ -0,0 +1,6 @@ +"""DynamoDB source connector for Moss.""" + +from .connector import DynamoDBConnector +from .ingest import ingest + +__all__ = ["DynamoDBConnector", "ingest"] diff --git a/packages/moss-data-connector/moss-connector-dynamodb/src/connector.py b/packages/moss-data-connector/moss-connector-dynamodb/src/connector.py new file mode 100644 index 00000000..b08f3542 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/src/connector.py @@ -0,0 +1,57 @@ +"""DynamoDB connector. + +Reads items from a DynamoDB table via ``boto3``'s ``Table.scan()``. One yielded +``DocumentInfo`` per item. + +Uses the standard boto3 credential chain (env vars, shared credentials file, +IAM role, etc.). Pass ``endpoint_url`` to target DynamoDB Local or localstack +for development/testing. +""" + +from __future__ import annotations + +from typing import Any, Callable, Iterator + +import boto3 +from moss import DocumentInfo + + +class DynamoDBConnector: + """Read items from a DynamoDB table and yield one ``DocumentInfo`` per item. + + By default scans the entire table. Pass ``scan_kwargs`` to restrict results + or control which attributes come back (e.g. ``FilterExpression``, + ``ProjectionExpression``, ``ExpressionAttributeNames``). + + ``mapper`` turns a DynamoDB item (dict) into a ``DocumentInfo``. + """ + + def __init__( + self, + table_name: str, + mapper: Callable[[dict[str, Any]], DocumentInfo], + region_name: str | None = None, + endpoint_url: str | None = None, + scan_kwargs: dict[str, Any] | None = None, + ) -> None: + self.table_name = table_name + self.mapper = mapper + self.region_name = region_name + self.endpoint_url = endpoint_url + self.scan_kwargs = dict(scan_kwargs) if scan_kwargs else {} + + def __iter__(self) -> Iterator[DocumentInfo]: + resource = boto3.resource( + "dynamodb", + region_name=self.region_name, + endpoint_url=self.endpoint_url, + ) + table = resource.Table(self.table_name) + kwargs = dict(self.scan_kwargs) # don't mutate original + while True: + response = table.scan(**kwargs) + for item in response.get("Items", []): + yield self.mapper(item) + if "LastEvaluatedKey" not in response: + break + kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"] diff --git a/packages/moss-data-connector/moss-connector-dynamodb/src/ingest.py b/packages/moss-data-connector/moss-connector-dynamodb/src/ingest.py new file mode 100644 index 00000000..68c44940 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/src/ingest.py @@ -0,0 +1,22 @@ +"""Copy rows into a Moss index.""" + +from __future__ import annotations + +from collections.abc import Iterable + +from moss import DocumentInfo, MossClient, MutationResult + + +async def ingest( + source: Iterable[DocumentInfo], + project_id: str, + project_key: str, + index_name: str, + model_id: str | None = None, +) -> MutationResult | None: + """Copy every `DocumentInfo` from `source` into a fresh Moss index.""" + docs = list(source) + if not docs: + return None + client = MossClient(project_id, project_key) + return await client.create_index(index_name, docs, model_id=model_id) diff --git a/packages/moss-data-connector/moss-connector-dynamodb/tests/test_dynamodb.py b/packages/moss-data-connector/moss-connector-dynamodb/tests/test_dynamodb.py new file mode 100644 index 00000000..8f818114 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/tests/test_dynamodb.py @@ -0,0 +1,174 @@ +"""Unit tests for the DynamoDB connector. No live AWS needed, we mock +``boto3.resource`` so the test runs anywhere boto3 is importable, and +we patch ``moss.MossClient`` inside ingest so no Moss network call is made. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +pytest.importorskip("boto3") + +from moss import DocumentInfo # noqa: E402 + +from moss_connector_dynamodb import DynamoDBConnector, ingest # noqa: E402 + + +@dataclass +class FakeMutationResult: + doc_count: int + job_id: str = "fake-job-id" + index_name: str = "" + + +@dataclass +class FakeMossClient: + calls: list[dict[str, Any]] = field(default_factory=list) + + async def create_index(self, name, docs, model_id=None): + docs = list(docs) + self.calls.append({"name": name, "docs": docs, "model_id": model_id}) + return FakeMutationResult(doc_count=len(docs), index_name=name) + + +def _dynamo_mock_returning(pages: list[dict[str, Any]]) -> tuple[MagicMock, MagicMock]: + """Build a mock ``boto3.resource("dynamodb")`` that returns paginated scan results. + + ``pages`` is a list of scan response dicts. Each must have ``"Items"`` and + optionally ``"LastEvaluatedKey"``. + + Returns (resource, table) so the test can assert on either one. + """ + table = MagicMock() + table.scan.side_effect = pages + resource = MagicMock() + resource.Table.return_value = table + return resource, table + + +def _simple_mapper(item: dict[str, Any]) -> DocumentInfo: + return DocumentInfo( + id=str(item["id"]), + text=item["body"], + metadata={"title": item["title"]}, + ) + + +async def test_dynamodb_ingest_single_page(): + pages = [ + { + "Items": [ + {"id": "a1", "title": "Refund policy", "body": "Refunds take 3-5 days."}, + {"id": "a2", "title": "Shipping", "body": "We ship within 24 hours."}, + ], + }, + ] + fake_resource, fake_table = _dynamo_mock_returning(pages) + fake_moss = FakeMossClient() + + with patch( + "moss_connector_dynamodb.connector.boto3.resource", return_value=fake_resource + ), patch("moss_connector_dynamodb.ingest.MossClient", return_value=fake_moss): + source = DynamoDBConnector( + table_name="articles", + mapper=_simple_mapper, + region_name="us-east-1", + ) + result = await ingest(source, "fake_id", "fake_key", index_name="articles") + + assert result is not None + assert result.doc_count == 2 + fake_table.scan.assert_called_once_with() + + moss_docs = fake_moss.calls[0]["docs"] + assert moss_docs[0].id == "a1" + assert moss_docs[0].text == "Refunds take 3-5 days." + assert moss_docs[0].metadata == {"title": "Refund policy"} + + +async def test_dynamodb_ingest_multi_page(): + pages = [ + { + "Items": [ + {"id": "a1", "title": "Page one", "body": "First page content."}, + ], + "LastEvaluatedKey": {"id": "a1"}, + }, + { + "Items": [ + {"id": "a2", "title": "Page two", "body": "Second page content."}, + ], + }, + ] + fake_resource, fake_table = _dynamo_mock_returning(pages) + fake_moss = FakeMossClient() + + with patch( + "moss_connector_dynamodb.connector.boto3.resource", return_value=fake_resource + ), patch("moss_connector_dynamodb.ingest.MossClient", return_value=fake_moss): + source = DynamoDBConnector( + table_name="articles", + mapper=_simple_mapper, + region_name="us-east-1", + ) + result = await ingest(source, "fake_id", "fake_key", index_name="articles") + + assert result is not None + assert result.doc_count == 2 + + # Second call should include ExclusiveStartKey from first page + calls = fake_table.scan.call_args_list + assert len(calls) == 2 + assert calls[0].kwargs == {} + assert calls[1].kwargs == {"ExclusiveStartKey": {"id": "a1"}} + + moss_docs = fake_moss.calls[0]["docs"] + assert moss_docs[0].id == "a1" + assert moss_docs[1].id == "a2" + + +async def test_dynamodb_forwards_scan_kwargs(): + pages = [{"Items": []}] + fake_resource, fake_table = _dynamo_mock_returning(pages) + fake_moss = FakeMossClient() + + my_scan_kwargs = { + "FilterExpression": "category = :cat", + "ProjectionExpression": "id, title, body", + "ExpressionAttributeValues": {":cat": "billing"}, + } + + with patch( + "moss_connector_dynamodb.connector.boto3.resource", return_value=fake_resource + ), patch("moss_connector_dynamodb.ingest.MossClient", return_value=fake_moss): + source = DynamoDBConnector( + table_name="articles", + mapper=_simple_mapper, + region_name="us-east-1", + scan_kwargs=my_scan_kwargs, + ) + await ingest(source, "fake_id", "fake_key", index_name="x") + + fake_table.scan.assert_called_once_with(**my_scan_kwargs) + + +async def test_dynamodb_empty_table(): + pages = [{"Items": []}] + fake_resource, _ = _dynamo_mock_returning(pages) + fake_moss = FakeMossClient() + + with patch( + "moss_connector_dynamodb.connector.boto3.resource", return_value=fake_resource + ), patch("moss_connector_dynamodb.ingest.MossClient", return_value=fake_moss): + source = DynamoDBConnector( + table_name="empty", + mapper=_simple_mapper, + ) + result = await ingest(source, "fake_id", "fake_key", index_name="empty") + + assert result is None + assert len(fake_moss.calls) == 0 diff --git a/packages/moss-data-connector/moss-connector-dynamodb/tests/test_integration_dynamodb_moss.py b/packages/moss-data-connector/moss-connector-dynamodb/tests/test_integration_dynamodb_moss.py new file mode 100644 index 00000000..9265d375 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-dynamodb/tests/test_integration_dynamodb_moss.py @@ -0,0 +1,89 @@ +"""End-to-end integration test: DynamoDB -> Moss. + +Reads from a pre-existing DynamoDB table (create it yourself or use +DynamoDB Local), ingests into a live Moss project via ``ingest()``, +runs a real semantic query, and cleans up the Moss index on exit. + +SKIPPED unless DYNAMODB_TABLE, AWS_REGION, MOSS_PROJECT_ID, and +MOSS_PROJECT_KEY are all set. + +Run with: + pytest tests/test_integration_dynamodb_moss.py -v -s +""" + +from __future__ import annotations + +import os +import uuid +from pathlib import Path + +import pytest + +pytest.importorskip("boto3") + +try: + from dotenv import load_dotenv + + _here = Path(__file__).resolve() + for candidate in ( + _here.parents[1] / ".env", # this package's own .env + _here.parents[2] / ".env", # shared creds at moss-data-connector/.env + _here.parents[4] / ".env", # /.env + ): + if candidate.exists(): + load_dotenv(candidate, override=False) +except ImportError: + pass + +from moss import DocumentInfo, MossClient, QueryOptions # noqa: E402 + +from moss_connector_dynamodb import DynamoDBConnector, ingest # noqa: E402 + +DYNAMODB_TABLE = os.getenv("DYNAMODB_TABLE") +AWS_REGION = os.getenv("AWS_REGION") +DYNAMODB_ENDPOINT_URL = os.getenv("DYNAMODB_ENDPOINT_URL") # optional, for DynamoDB Local + +PROJECT_ID = os.getenv("MOSS_PROJECT_ID") +PROJECT_KEY = os.getenv("MOSS_PROJECT_KEY") + +pytestmark = pytest.mark.skipif( + not (DYNAMODB_TABLE and AWS_REGION and PROJECT_ID and PROJECT_KEY), + reason="Set DYNAMODB_TABLE, AWS_REGION, MOSS_PROJECT_ID, and MOSS_PROJECT_KEY to run.", +) + + +async def test_dynamodb_live_ingest_to_moss(): + """Full round trip: DynamoDB items -> ingest() -> Moss index -> query -> delete.""" + client = MossClient(PROJECT_ID, PROJECT_KEY) + index_name = f"moss-connectors-dynamodb-e2e-{uuid.uuid4().hex[:8]}" + + try: + source = DynamoDBConnector( + table_name=DYNAMODB_TABLE, + mapper=lambda item: DocumentInfo( + id=str(item.get("id", item.get("pk", "unknown"))), + text=str(item.get("text", item.get("body", item.get("content", "")))), + metadata={ + k: str(v) for k, v in item.items() + if k not in ("id", "pk", "text", "body", "content") + }, + ), + region_name=AWS_REGION, + endpoint_url=DYNAMODB_ENDPOINT_URL, + ) + + result = await ingest(source, PROJECT_ID, PROJECT_KEY, index_name=index_name) + assert result is not None, "expected at least one item in the DynamoDB table" + assert result.doc_count > 0 + + await client.load_index(index_name) + query_result = await client.query( + index_name, "test query", QueryOptions(top_k=3) + ) + assert query_result.docs, "expected at least one document in the search result" + + finally: + try: + await client.delete_index(index_name) + except Exception as exc: # pragma: no cover, best-effort cleanup + print(f"warning: failed to delete test index {index_name}: {exc}") diff --git a/packages/moss-data-connector/moss-connector-mongodb/.gitignore b/packages/moss-data-connector/moss-connector-mongodb/.gitignore new file mode 100644 index 00000000..b86b0644 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/.gitignore @@ -0,0 +1,10 @@ +build/ +dist/ +*.egg-info/ +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.env diff --git a/packages/moss-data-connector/moss-connector-mongodb/README.md b/packages/moss-data-connector/moss-connector-mongodb/README.md new file mode 100644 index 00000000..9e294f7d --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/README.md @@ -0,0 +1,64 @@ +# moss-connector-mongodb + +MongoDB source connector for Moss. Self-contained, no separate core package to install. + +## Install + +```bash +pip install moss-connector-mongodb +``` + +Pulls `pymongo` as a dependency. + +## Usage + +```python +import asyncio +from moss import DocumentInfo +from moss_connector_mongodb import MongoDBConnector, ingest + +async def main(): + source = MongoDBConnector( + uri="mongodb://localhost:27017", + database="shop", + collection="articles", + mapper=lambda r: DocumentInfo( + id=str(r["_id"]), + text=r["body"], + metadata={"title": r["title"]}, + ), + filter={"status": "published"}, # optional + projection={"_id": 1, "title": 1, "body": 1}, # optional + ) + + result = await ingest( + source, + project_id="your_project_id", + project_key="your_project_key", + index_name="articles", + ) + print(f"copied {result.doc_count} rows") + +asyncio.run(main()) +``` + +MongoDB's `_id` is a `bson.ObjectId`. Wrap it with `str()` in your mapper to render the hex string. + +## Layout + +``` +src/ +├── __init__.py # re-exports MongoDBConnector and ingest +├── connector.py # MongoDBConnector class +└── ingest.py # ingest() - keep in sync with the other connector packages +``` + +## Tests + +```bash +pip install -e ".[dev]" +pytest tests/test_mongodb.py -v # mocked Moss + mocked Mongo +pytest tests/test_integration_mongodb_moss.py -v -s # live Moss + local Mongo +``` + +The live integration test expects a Mongo at `mongodb://localhost:27017` - edit `MONGODB_URI` at the top of the test file to change. diff --git a/packages/moss-data-connector/moss-connector-mongodb/pyproject.toml b/packages/moss-data-connector/moss-connector-mongodb/pyproject.toml new file mode 100644 index 00000000..28a12020 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/pyproject.toml @@ -0,0 +1,56 @@ +[project] +name = "moss-connector-mongodb" +version = "0.0.1" +description = "MongoDB source connector for moss-connectors." +readme = "README.md" +requires-python = ">=3.10,<3.15" +license = { text = "BSD-2-Clause" } +authors = [{ name = "InferEdge Inc.", email = "contact@moss.dev" }] +keywords = ["moss", "connectors", "mongodb", "mongo", "ingest", "etl"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", +] +dependencies = [ + "moss>=1.0.0", + "pymongo>=4.6", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "python-dotenv>=1.0.0", + "ruff>=0.5.0", +] + +[project.urls] +Homepage = "https://github.com/usemoss/moss" +Repository = "https://github.com/usemoss/moss" +Source = "https://github.com/usemoss/moss/tree/main/packages/moss-data-connector/moss-connector-mongodb" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +# Flat layout: src/ itself IS the package `moss_connector_mongodb`. +[tool.setuptools] +packages = ["moss_connector_mongodb"] +package-dir = { "moss_connector_mongodb" = "src" } + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "UP"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/packages/moss-data-connector/moss-connector-mongodb/src/__init__.py b/packages/moss-data-connector/moss-connector-mongodb/src/__init__.py new file mode 100644 index 00000000..081547b9 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/src/__init__.py @@ -0,0 +1,9 @@ +"""MongoDB source connector for Moss. + + from moss_connector_mongodb import MongoDBConnector, ingest +""" + +from .connector import MongoDBConnector +from .ingest import ingest + +__all__ = ["MongoDBConnector", "ingest"] diff --git a/packages/moss-data-connector/moss-connector-mongodb/src/connector.py b/packages/moss-data-connector/moss-connector-mongodb/src/connector.py new file mode 100644 index 00000000..78a76a34 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/src/connector.py @@ -0,0 +1,54 @@ +"""MongoDB connector. + +Reads documents from a MongoDB collection via pymongo's `find()`. One yielded +`DocumentInfo` per document. + +Note on ids: MongoDB's `_id` comes back as a `bson.ObjectId`. In your `mapper`, +wrap it with `str()` to render the hex string, e.g. +`DocumentInfo(id=str(r["_id"]), ...)`. +""" + +from __future__ import annotations + +from typing import Any, Callable, Iterator, Optional + +from moss import DocumentInfo +from pymongo import MongoClient + + +class MongoDBConnector: + """Read documents from a MongoDB collection and yield one `DocumentInfo` per document. + + By default yields every document in the collection. Pass `filter` to + restrict results (any standard Mongo query dict). Pass `projection` to + limit which fields come back. + + `mapper` turns a Mongo document (dict) into a `DocumentInfo`. + """ + + def __init__( + self, + uri: str, + database: str, + collection: str, + mapper: Callable[[dict[str, Any]], DocumentInfo], + filter: Optional[dict[str, Any]] = None, + projection: Optional[dict[str, Any]] = None, + ) -> None: + self.uri = uri + self.database = database + self.collection = collection + self.mapper = mapper + self.filter = filter or {} + self.projection = projection + + def __iter__(self) -> Iterator[DocumentInfo]: + client = MongoClient(self.uri) + try: + cursor = client[self.database][self.collection].find( + self.filter, self.projection + ) + for doc in cursor: + yield self.mapper(doc) + finally: + client.close() diff --git a/packages/moss-data-connector/moss-connector-mongodb/src/ingest.py b/packages/moss-data-connector/moss-connector-mongodb/src/ingest.py new file mode 100644 index 00000000..68c44940 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/src/ingest.py @@ -0,0 +1,22 @@ +"""Copy rows into a Moss index.""" + +from __future__ import annotations + +from collections.abc import Iterable + +from moss import DocumentInfo, MossClient, MutationResult + + +async def ingest( + source: Iterable[DocumentInfo], + project_id: str, + project_key: str, + index_name: str, + model_id: str | None = None, +) -> MutationResult | None: + """Copy every `DocumentInfo` from `source` into a fresh Moss index.""" + docs = list(source) + if not docs: + return None + client = MossClient(project_id, project_key) + return await client.create_index(index_name, docs, model_id=model_id) diff --git a/packages/moss-data-connector/moss-connector-mongodb/tests/test_integration_mongodb_moss.py b/packages/moss-data-connector/moss-connector-mongodb/tests/test_integration_mongodb_moss.py new file mode 100644 index 00000000..e18219af --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/tests/test_integration_mongodb_moss.py @@ -0,0 +1,173 @@ +"""End-to-end integration test: MongoDB -> Moss. + +Populates a temporary database on a local MongoDB (URI hardcoded below), +ingests it into a live Moss project via `ingest()`, runs a real semantic +query, and cleans everything up on exit. + +SKIPPED unless both MOSS_PROJECT_ID and MOSS_PROJECT_KEY are set. + +Run with: + pytest tests/test_integration_mongodb_moss.py -v -s +""" + +from __future__ import annotations + +import os +import uuid +from pathlib import Path + +import pytest + +pytest.importorskip("pymongo") + +try: + from dotenv import load_dotenv + + _here = Path(__file__).resolve() + for candidate in ( + _here.parents[1] / ".env", # this package's own .env + _here.parents[2] / ".env", # shared creds at moss-data-connector/.env + _here.parents[4] / ".env", # /.env + ): + if candidate.exists(): + load_dotenv(candidate, override=False) +except ImportError: + pass + +from moss import DocumentInfo, MossClient, QueryOptions # noqa: E402 + +from moss_connector_mongodb import MongoDBConnector, ingest # noqa: E402 + +# Point this at whatever Mongo you're running locally. +MONGODB_URI = "mongodb://localhost:27017" + +PROJECT_ID = os.getenv("MOSS_PROJECT_ID") +PROJECT_KEY = os.getenv("MOSS_PROJECT_KEY") + +pytestmark = pytest.mark.skipif( + not (PROJECT_ID and PROJECT_KEY), + reason="Set MOSS_PROJECT_ID and MOSS_PROJECT_KEY to run this live test.", +) + + +@pytest.fixture() +def mongo_database(): + """Populate a unique DB with richly-typed articles; drop on exit. + + Field names deliberately avoid `id`, `text`, and `metadata` so the mapping + is clearly translating source fields into Moss concepts. + """ + from pymongo import MongoClient + + db_name = f"e2e_{uuid.uuid4().hex[:8]}" + mongo = MongoClient(MONGODB_URI) + try: + mongo[db_name]["articles"].insert_many( + [ + { + "sku": "ART-001", + "headline": "Refund policy", + "full_text": "Refunds are processed within 3 to 5 business days.", + "category": "billing", + "author": "ada", + "word_count": 12, + "published": True, + }, + { + "sku": "ART-002", + "headline": "Shipping time", + "full_text": "Most orders ship within 24 hours of being placed.", + "category": "shipping", + "author": "bob", + "word_count": 10, + "published": True, + }, + { + "sku": "ART-003", + "headline": "Contact support", + "full_text": "You can reach our support team 24/7 via live chat.", + "category": "support", + "author": "cal", + "word_count": 11, + "published": True, + }, + { + "sku": "ART-004", + "headline": "Password reset", + "full_text": "To reset your password, click the link on the login page.", + "category": "account", + "author": "dee", + "word_count": 12, + "published": True, + }, + { + "sku": "ART-005", + "headline": "Order tracking", + "full_text": "Every shipped order includes a tracking number by email.", + "category": "shipping", + "author": "eli", + "word_count": 10, + "published": True, + }, + ] + ) + yield db_name + finally: + mongo.drop_database(db_name) + mongo.close() + + +async def test_mongodb_live_ingest_to_moss(mongo_database): + """Full round trip: MongoDB docs -> ingest() -> Moss index -> query -> delete.""" + db_name = mongo_database + # ingest() builds its own MossClient from the creds; we need one here too + # for the query + cleanup assertions below. + client = MossClient(PROJECT_ID, PROJECT_KEY) + + index_name = f"moss-connectors-mongo-e2e-{uuid.uuid4().hex[:8]}" + + try: + source = MongoDBConnector( + uri=MONGODB_URI, + database=db_name, + collection="articles", + mapper=lambda r: DocumentInfo( + id=str(r["sku"]), + text=r["full_text"], + metadata={ + "headline": r["headline"], + "category": r["category"], + "author": r["author"], + "word_count": str(r["word_count"]), + "published": str(r["published"]), + }, + ), + ) + + result = await ingest(source, PROJECT_ID, PROJECT_KEY, index_name=index_name) + assert result is not None + assert result.doc_count == 5 + + await client.load_index(index_name) + result = await client.query( + index_name, "how long do refunds take", QueryOptions(top_k=3) + ) + + assert result.docs, "expected at least one document in the search result" + top_ids = [d.id for d in result.docs] + assert "ART-001" in top_ids, f"refund-policy doc not in top 3: {top_ids}" + + refund_doc = next(d for d in result.docs if d.id == "ART-001") + assert refund_doc.metadata is not None + # String fields survive as-is; int/bool are coerced to str for Moss. + assert refund_doc.metadata.get("headline") == "Refund policy" + assert refund_doc.metadata.get("category") == "billing" + assert refund_doc.metadata.get("author") == "ada" + assert refund_doc.metadata.get("word_count") == "12" + assert refund_doc.metadata.get("published") == "True" + + finally: + try: + await client.delete_index(index_name) + except Exception as exc: # pragma: no cover, best-effort cleanup + print(f"warning: failed to delete test index {index_name}: {exc}") diff --git a/packages/moss-data-connector/moss-connector-mongodb/tests/test_mongodb.py b/packages/moss-data-connector/moss-connector-mongodb/tests/test_mongodb.py new file mode 100644 index 00000000..530aacd7 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/tests/test_mongodb.py @@ -0,0 +1,107 @@ +"""Unit tests for the MongoDB connector. No live MongoDB needed, we mock +`pymongo.MongoClient` so the test runs anywhere pymongo is importable, and +we patch `moss.MossClient` inside ingest so no Moss network call is made. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +pytest.importorskip("pymongo") + +from moss import DocumentInfo # noqa: E402 + +from moss_connector_mongodb import MongoDBConnector, ingest # noqa: E402 + + +@dataclass +class FakeMutationResult: + doc_count: int + job_id: str = "fake-job-id" + index_name: str = "" + + +@dataclass +class FakeMossClient: + calls: list[dict[str, Any]] = field(default_factory=list) + + async def create_index(self, name, docs, model_id=None): + docs = list(docs) + self.calls.append({"name": name, "docs": docs, "model_id": model_id}) + return FakeMutationResult(doc_count=len(docs), index_name=name) + + +def _mongo_mock_returning(docs: list[dict[str, Any]]) -> tuple[MagicMock, MagicMock]: + """Build a mock `MongoClient(...)` that returns `docs` from its find() call. + + Returns (client, collection) so the test can assert on either one - + `client` for passing to `patch("pymongo.MongoClient", return_value=...)`, + `collection` for inspecting how `find()` was called. + """ + collection = MagicMock() + collection.find.return_value = iter(docs) + db = MagicMock() + db.__getitem__.return_value = collection + client = MagicMock() + client.__getitem__.return_value = db + return client, collection + + +async def test_mongodb_ingest_end_to_end(): + docs_from_mongo = [ + {"_id": "a1", "title": "Refund policy", "body": "Refunds take 3–5 days."}, + {"_id": "a2", "title": "Shipping", "body": "We ship within 24 hours."}, + ] + fake_mongo, fake_collection = _mongo_mock_returning(docs_from_mongo) + fake_moss = FakeMossClient() + + with patch("moss_connector_mongodb.connector.MongoClient", return_value=fake_mongo), patch( + "moss_connector_mongodb.ingest.MossClient", return_value=fake_moss + ): + source = MongoDBConnector( + uri="mongodb://localhost", + database="shop", + collection="articles", + mapper=lambda r: DocumentInfo( + id=str(r["_id"]), + text=r["body"], + metadata={"title": r["title"]}, + ), + ) + result = await ingest(source, "fake_id", "fake_key", index_name="articles") + + assert result is not None + assert result.doc_count == 2 + fake_collection.find.assert_called_once_with({}, None) + + moss_docs = fake_moss.calls[0]["docs"] + assert moss_docs[0].id == "a1" + assert moss_docs[0].text == "Refunds take 3–5 days." + assert moss_docs[0].metadata == {"title": "Refund policy"} + + +async def test_mongodb_forwards_filter_and_projection(): + fake_mongo, fake_collection = _mongo_mock_returning([]) + fake_moss = FakeMossClient() + + my_filter = {"status": "published"} + my_projection = {"_id": 1, "body": 1, "title": 1} + + with patch("moss_connector_mongodb.connector.MongoClient", return_value=fake_mongo), patch( + "moss_connector_mongodb.ingest.MossClient", return_value=fake_moss + ): + source = MongoDBConnector( + uri="mongodb://localhost", + database="shop", + collection="articles", + mapper=lambda r: DocumentInfo(id=str(r["_id"]), text=r["body"]), + filter=my_filter, + projection=my_projection, + ) + await ingest(source, "fake_id", "fake_key", index_name="x") + + fake_collection.find.assert_called_once_with(my_filter, my_projection) diff --git a/packages/moss-data-connector/moss-connector-mongodb/uv.lock b/packages/moss-data-connector/moss-connector-mongodb/uv.lock new file mode 100644 index 00000000..11b8a121 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-mongodb/uv.lock @@ -0,0 +1,416 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.15" + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" }, +] + +[[package]] +name = "inferedge-moss-core" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/60/e07fef3d462e5a6c26e86f2cf4425e7a4b75bec04c7b7638b947a9dcd855/inferedge_moss_core-0.8.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a21d8b4eace2146053864f484327b55e5267af16ffc19ee6c4aaecf7c5ff3beb", size = 12011165, upload-time = "2026-03-24T17:40:49.314Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/5abe406d875903e0a15b8c96879187fe995fde659c6481c50804977a9e45/inferedge_moss_core-0.8.7-cp310-cp310-manylinux_2_38_x86_64.whl", hash = "sha256:aac7e5762d8811ce3fee40a3156fb061464c60bf202653f30b190d339f0cabdc", size = 13141973, upload-time = "2026-03-24T17:41:24.085Z" }, + { url = "https://files.pythonhosted.org/packages/81/38/d7d6e4d81ea74b803eaead416c13a3305f01c7ef40b6fca96b9788b4bfc6/inferedge_moss_core-0.8.7-cp310-cp310-manylinux_2_39_aarch64.whl", hash = "sha256:d2d014280f0ea9c5d34d23c3eefb7a009646bd4137b46eaf5b990f5cb46b89f6", size = 14618007, upload-time = "2026-03-24T17:40:46.605Z" }, + { url = "https://files.pythonhosted.org/packages/d2/81/f7abaebba1ce27ad5ec6d451bd224ae17c10614bc36a33a888a74af35d4b/inferedge_moss_core-0.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:c05c6b1ca3228505397adfe64e60ae70d85e2a9a05699e04b4e7445fd75fcb97", size = 11010409, upload-time = "2026-03-24T17:46:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/db/90/675d540ba6724c7f48b9efaada616d08ab78b9c80900e8237253c139738f/inferedge_moss_core-0.8.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d3182449a4471a9ca2c4bec2bf36c6aa9360b2730d67f2ea1de565d7b2e5e93", size = 12010920, upload-time = "2026-03-24T17:40:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9b/85afd0c9c021718af9a6a285f0ecd5af549ad2dce59bbdb4b8ff7d89a481/inferedge_moss_core-0.8.7-cp311-cp311-manylinux_2_38_x86_64.whl", hash = "sha256:d68872a0020a0fa7825cc80d7cd3add35012462ce51565871838c1d43bd2ec59", size = 13142067, upload-time = "2026-03-24T17:41:53.912Z" }, + { url = "https://files.pythonhosted.org/packages/74/e9/b51a3de89afb8884a5728c675cf6984f94ecb1e8f9532f21d837d2e28544/inferedge_moss_core-0.8.7-cp311-cp311-manylinux_2_39_aarch64.whl", hash = "sha256:0a68b303ce5853c37dfbb80805bc6de1cba917289227d2cc35239081fc860251", size = 14617302, upload-time = "2026-03-24T17:40:57.978Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/c78e61c04ac78806a2f04d13997e8e19579f7d0a2cb398a8264e8e13fa34/inferedge_moss_core-0.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:970777df88701085c713b9ead9e6fd1aefca9a4460cd10c6f1adcd88636b967b", size = 11009652, upload-time = "2026-03-24T17:46:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/33/912419b8520350ec40d5678f88e683b82c1829f01738ca2e10c332f898d1/inferedge_moss_core-0.8.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c4c5903aefa71a2f37bf639f31076cc998d03aa56305a0df54cf1c47549e268", size = 12008052, upload-time = "2026-03-24T17:40:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/00/c3/d0c5a4aee67804ec9a22609b70e46fb446f3e5729b24f4752a43b7663a6b/inferedge_moss_core-0.8.7-cp312-cp312-manylinux_2_38_x86_64.whl", hash = "sha256:5a8e946da18a4700903040b59e047344a9e4d4b46a01b78e359c6b7ccc619ee0", size = 13141260, upload-time = "2026-03-24T17:41:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cdf2669d301786b86a3bcac8625e198b6ec77805ecf5775b0f8ee600e490/inferedge_moss_core-0.8.7-cp312-cp312-manylinux_2_39_aarch64.whl", hash = "sha256:72260b77311fda63528cf17eef0afbf22cdc8ee6123a2e75273b44917ecfc38e", size = 14619836, upload-time = "2026-03-24T17:40:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8a/77d7c05dca68b409a662115d5e0a7f4a8a5932fe6e54dc6c3c35debb4cef/inferedge_moss_core-0.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:956fd52f557d99aa216015b71e01e2211ee4b19da298687a584864d80d00394c", size = 11009060, upload-time = "2026-03-24T17:45:47.58Z" }, + { url = "https://files.pythonhosted.org/packages/67/48/41db4ec8482efe2a0d2021e02411a11dbc3004c8172925ee2ff0c61dfb54/inferedge_moss_core-0.8.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:060bfa1dce0998a5d0095a502742e62c29664b57a6a426b649f188d657217e2d", size = 12007900, upload-time = "2026-03-24T17:40:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f6/55e27007f46234082a3847446435f00a1815eebf7fcebe526183f1b8cc45/inferedge_moss_core-0.8.7-cp313-cp313-manylinux_2_38_x86_64.whl", hash = "sha256:628d5808a05a6cbd0aa6fa7b01fe6b4642b5df8aa48d379a66e608e16f300225", size = 13141311, upload-time = "2026-03-24T17:41:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3b/aff5bbce1e45d41961c103c5d83773206cb9300c87f9bc0d386df8dcb167/inferedge_moss_core-0.8.7-cp313-cp313-manylinux_2_39_aarch64.whl", hash = "sha256:a2ae3ae2bff6b0419c8040bd705cce3739e2d5b6a8afb1640dca5415cf50f683", size = 14622134, upload-time = "2026-03-24T17:40:48.58Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d8/8cfe68fc8f66bbdadb358b5bd50a97491d2dbd359c737b0dd7579b3c905e/inferedge_moss_core-0.8.7-cp313-cp313-win_amd64.whl", hash = "sha256:506a533e11379e86d227c44d10caf8e89a3302fae01131bb7bafa4d3d28313be", size = 11008608, upload-time = "2026-03-24T17:46:21.654Z" }, + { url = "https://files.pythonhosted.org/packages/0a/90/755c63a4806ba8307a2f0c9e9c595891696fcf44e126d60738449eb9aae9/inferedge_moss_core-0.8.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4e66c21910af6c692c936ab1c72e01748d65e420113d4510a6e5097e578fc298", size = 12008127, upload-time = "2026-03-24T17:39:51.89Z" }, + { url = "https://files.pythonhosted.org/packages/2f/17/83d9b56d7fe71dbe5ac57080550459761c66a6d90cb3e039dc8d6c2b2adc/inferedge_moss_core-0.8.7-cp314-cp314-manylinux_2_38_x86_64.whl", hash = "sha256:54aa26a391eb8ebd9dcebaeab72ae81065462da2b33d3bb580d2b0cc6e2bcabb", size = 13138063, upload-time = "2026-03-24T17:41:22.992Z" }, + { url = "https://files.pythonhosted.org/packages/30/16/9cb36e013ec6feccbde28181f853764babe94a450b432893ec60f7469874/inferedge_moss_core-0.8.7-cp314-cp314-manylinux_2_39_aarch64.whl", hash = "sha256:6dec35c0cd22ab77f68c8e94b7b6f550dd70e662ba85dc60823ff80445cf9e32", size = 14618319, upload-time = "2026-03-24T17:41:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e0/93dbfbfb358065c0ac9df5d6be6e6ddaaea53260ebd654900f0992abf9ce/inferedge_moss_core-0.8.7-cp314-cp314-win_amd64.whl", hash = "sha256:e14a8959a3a47b6ed8e783d51b5ea9cd901b803bfb0c02ce30e1213eb25fb3f3", size = 11005063, upload-time = "2026-03-24T17:45:14.825Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "moss" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "inferedge-moss-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/a7/971c660de82707de114a8f342e0b2a9f3c95f9b88823eb7a5f9872258cef/moss-1.0.0.tar.gz", hash = "sha256:20225f8604c9b1126a023fe38b48debd1911501828acba95a9e61ccb5d9844eb", size = 32591, upload-time = "2026-03-30T02:38:07.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/46/7e61b04ce3d4a029cd228ad0e268089e4b4c12d400a0f8ca06466218201e/moss-1.0.0-py3-none-any.whl", hash = "sha256:94c7b6021b49370a6b70cacdd8025633d00229848ec52f7d566efedbabede812", size = 12112, upload-time = "2026-03-30T02:38:06.673Z" }, +] + +[[package]] +name = "moss-connector-mongodb" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "moss" }, + { name = "pymongo" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "moss", specifier = ">=1.0.0" }, + { name = "pymongo", specifier = ">=4.6" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymongo" +version = "4.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/64/50be6fbac9c79fe2e4c17401a467da2d8764d82833d83cec325afe5cab32/pymongo-4.17.0.tar.gz", hash = "sha256:70ffa08ba641468cc068cf46c06b34f01a8ce3489f6411309fcb5ceabe6b2fc0", size = 2523370, upload-time = "2026-04-20T16:39:53.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/77/28ebbf69772a4341d530831c7a006cdb06877ac23075cb53b0a227df4fe1/pymongo-4.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:47b021363cd923ace5edc7a1d63c0ff8a6d9d43859b8a1ba23645f5afae63221", size = 819234, upload-time = "2026-04-20T16:37:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/88/cf/5a70cee503ff9a2fea20607607f14d189f4d975960ac0945ec306ee7b695/pymongo-4.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:422fa50d7d7f5c22ea0953554396c9ef95684a2d775f860bd75a7b510538dfca", size = 819969, upload-time = "2026-04-20T16:37:24.187Z" }, + { url = "https://files.pythonhosted.org/packages/23/d5/07b7e27e662c58d872efd104a0e8055eb6569aa1b6d4da436f3fdee7f897/pymongo-4.17.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:addd0498ebbdc6354227f6ed457ed9fce442d48a3bb30d5b5bad33e104996561", size = 1244510, upload-time = "2026-04-20T16:37:26.069Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/7cac5b1e89bd5a8e395067648241390321593a7c29243e36f91343c02a90/pymongo-4.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5c8e180cb2cabe37300e1e36c60aa4f2ff956cc579f0142135a5d2cba252243", size = 1263245, upload-time = "2026-04-20T16:37:28.003Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/40e8e99824c1fda18261411e65ce3b0cd3d9a6ed3c056cdd0a569adc870b/pymongo-4.17.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd835cdb37a1adec359dd072c24f8bb14809e2644fde86fab4ee2fc9719b9483", size = 1304113, upload-time = "2026-04-20T16:37:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/3a/94/fb7e25441dd66f2069a9b172380849b0eaa5881c18b3db217bf64a6d393c/pymongo-4.17.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4979e7e8887862bbb44d203f00cc8263a3f27237876fa691b6beba23e40e6d8", size = 1297046, upload-time = "2026-04-20T16:37:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c9/7352e0c20fe772541556e4d283c05e07ec48f8b0d2737ad930ac4a1b6655/pymongo-4.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77aa4bc164b4de60d5db193b322f0f5b6ead716e831031bfdef8e8bd92205556", size = 1265708, upload-time = "2026-04-20T16:37:33.934Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e4/3df15494c2015ed297958517f0e4f6493e21b00990748068a973e66d45e0/pymongo-4.17.0-cp310-cp310-win32.whl", hash = "sha256:48bbc576677b50af043df870d84ded67cc3a9b4aa7553201beef4da5dc050a0a", size = 805533, upload-time = "2026-04-20T16:37:35.744Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/b4e71bb8cb82ad7d21bb4e8c476f2d573ba68b20368aac36ef06e4a196b4/pymongo-4.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46767f28dea610e02edf6c5d956ce615c3c7790ea396660b9b1efd5c5ead2e0", size = 815677, upload-time = "2026-04-20T16:37:37.808Z" }, + { url = "https://files.pythonhosted.org/packages/22/e2/0a4bba644f1cda3970ea1012149eeae3594ebfeed3f81fdaf32b61d90c95/pymongo-4.17.0-cp310-cp310-win_arm64.whl", hash = "sha256:757f2a4c0c2c46cab87df0333681ce69e86c9d5b45bc5203ceba5410b3489e59", size = 807293, upload-time = "2026-04-20T16:37:39.707Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e2/336d86f221cf1b56b2ed9330d4a3b98f9f38f0b37829ae9a9184617d5419/pymongo-4.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4141e6c6a339789b2974efa00ecd9409101672d77a0e3ee2cc3839eedf8ec4df", size = 874668, upload-time = "2026-04-20T16:37:41.39Z" }, + { url = "https://files.pythonhosted.org/packages/34/8e/75d3c6c935d187ab59c61e9c15d9aab3f274b563eaf1706e8cae5f508dec/pymongo-4.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e68c76b84e0c132d9dbf9307f12ff8185702328187a87b9aca8c941303873433", size = 875294, upload-time = "2026-04-20T16:37:43.432Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ec/62e855744489dbcd54fd778aae4d80fa4c4819e8fb228ca0cf6f21a03997/pymongo-4.17.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba2195d4f386f839a52a23ea1cfd60ffaaba78a3d7841db51b7e433001139918", size = 1496233, upload-time = "2026-04-20T16:37:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/82/e8/93e4e5e5ce8fdf8929dabeefe24aafa5ce046028eed0dfa8eeb936e72c49/pymongo-4.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446ff4bfcb6ec2a2e50998c860986a1e992136f998b7f53e7a717fb8aa5a0b9", size = 1522927, upload-time = "2026-04-20T16:37:47.492Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/425dc1d21e0f17bdea0072fc463f662f7fa06d2852af52975c9eced3c07c/pymongo-4.17.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a0d5ac205728c86e0a02192f1aa5f865b0d7d51f8df6101c01a69a7fc620d72", size = 1583468, upload-time = "2026-04-20T16:37:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9d/f08b07eeffda1a43c1759f0fa625e88ae12360996eb56d42aad832fa7dff/pymongo-4.17.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:485c8a8eaa4c739f00a331fc73757898ee7c092c214a79e63866ff76aaf282ff", size = 1572787, upload-time = "2026-04-20T16:37:51.061Z" }, + { url = "https://files.pythonhosted.org/packages/e9/c2/6855a07aafa7b894929af23675b6fb9634800ce43122b76a62f6eeb8da2a/pymongo-4.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2dfcc795f5b9fedbe179a11fdf6051581479d196582a3fe819a92a00e9b9969", size = 1526184, upload-time = "2026-04-20T16:37:53.358Z" }, + { url = "https://files.pythonhosted.org/packages/4e/05/c952bac7db71c1942ea3559fcd308b49754cc5004b455935fb4000d1f37b/pymongo-4.17.0-cp311-cp311-win32.whl", hash = "sha256:c2292144505fb12156b981bd440f3dc994a883da06ac726c0c8692ccdbc1c510", size = 852621, upload-time = "2026-04-20T16:37:55.28Z" }, + { url = "https://files.pythonhosted.org/packages/11/c0/c04da9f4c0c6252404598f4e394b862a58a9e866822a70ae261c8a018fdf/pymongo-4.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:2e190827834fce70ecdf9d46796c6dbc0ce08ea87dc2ff5bc6f3f5579b605cb9", size = 867852, upload-time = "2026-04-20T16:37:57.233Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b2/c7b4870fbeef471e947d3e014676f5910d02e0197074d692ebcf24ec049a/pymongo-4.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:a8f9c40a09bb7d4b9fc8b1da65ecf6efa79bda5cb2756f39d9b6940fac1d19ae", size = 855019, upload-time = "2026-04-20T16:37:58.983Z" }, + { url = "https://files.pythonhosted.org/packages/98/90/60bcb508840135d5ee46b51b1a950f548338aa8145a8366dbe6639ae51ac/pymongo-4.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53ffa94b2340dbf6b055e09a0090618c60482c158ecfc9565642fc996bf0944", size = 930529, upload-time = "2026-04-20T16:38:00.936Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/313840f1e52c6dfac47f704428cbfbce59956ebe7633bffc92b03f74f0ad/pymongo-4.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe0de9d0f6791abce3471230b32b4817bf89d27b1182b6a550e1ec0fa72aa9a", size = 930665, upload-time = "2026-04-20T16:38:02.915Z" }, + { url = "https://files.pythonhosted.org/packages/78/35/9d3565ea45b1606f635c1e2cd2563c28d66caafdc50f7ad7d979fcd1b363/pymongo-4.17.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e537e95514dae1aaa718f481ec03151a0f0394bcd05f1322896d8fc1330cb729", size = 1762369, upload-time = "2026-04-20T16:38:05.375Z" }, + { url = "https://files.pythonhosted.org/packages/95/ee/149b0d4b1a11c38bff6f14c23d5814c9b0843fd6dc38ad40596bdb1a62d2/pymongo-4.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37a8385c29881b43eab31f584100fa0eaddedd5607adf010147ba1810118be90", size = 1798044, upload-time = "2026-04-20T16:38:07.195Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d4/4cee4a7b8d8f6f0550ef6cd2fea42455c5ed619a220cb6ba4fb40d6a5bc8/pymongo-4.17.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f3ee3d241ed77a4fc99ce3cff3b289c3ebce37f61fdd7349d3592c23b82c8784", size = 1878567, upload-time = "2026-04-20T16:38:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/45/ef/7fe366c84952619ee2f69973566c214775e083dd4df465751912153e4b72/pymongo-4.17.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9eb5d63a3c518cb0804ed678f5e2b875af032d89a7cf57a57360322cf6a4d222", size = 1864881, upload-time = "2026-04-20T16:38:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/2f/35/b577d82c6d1be7aee7ac7e249bc86f7847998345042e5f8360de238e177b/pymongo-4.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e97e03fa13327c87e3fdc5656acd01e71817f0c1dc3221cd8f30de136bf4ec3", size = 1800349, upload-time = "2026-04-20T16:38:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/b8/69/dafcf04f66e130ddd91aeb92e7a692480eda46dcd04ec1dbe82c06619e10/pymongo-4.17.0-cp312-cp312-win32.whl", hash = "sha256:6877214bff5f06f6884a9fc8d9016a4a7a5f51f537f5c51ac3a576f93e7dfb32", size = 900518, upload-time = "2026-04-20T16:38:15.541Z" }, + { url = "https://files.pythonhosted.org/packages/11/35/5c9262a459f988b4eb2605f70815240b77a0d4131136c4326d18f1822b89/pymongo-4.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9828485f72f63c7d802e0ec41f71906f633c2692621ab3af55ca990186b091b1", size = 920335, upload-time = "2026-04-20T16:38:17.665Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/e9c7265ee176faccf4e52c4797837e794d93569a1046f6b19a4acc36e5ad/pymongo-4.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:1195370a77baf003b59b10e91ecc4706297197f0dd9d29c840cc556dc08f7cee", size = 903289, upload-time = "2026-04-20T16:38:19.33Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6b/c1206879708b94e82fcd8b9653440ec271f79a3674d122192df383047f5a/pymongo-4.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:809ec74de3b9148ae43fa8df9faf53470f511c8d384f13b99d6f671f2a379f15", size = 985829, upload-time = "2026-04-20T16:38:21.031Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cf/bb044ed85160e5c40f568c7c4f4e8ea16f40764ff5d302e5befbe8f6f814/pymongo-4.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a431b737816bf4cddd4fa0fcef04e424ad36b7692734a64150f872fb8f3208be", size = 985899, upload-time = "2026-04-20T16:38:23.409Z" }, + { url = "https://files.pythonhosted.org/packages/74/0a/f6dfd5ea3901e5d6888da8de8ba728971a1d447debab681cfc56f90d1208/pymongo-4.17.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4fab10f8403169ce92f3cea921609d9ee81107306caae06c08f592d4b8ad2b5", size = 2028569, upload-time = "2026-04-20T16:38:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c5/081f59a1c02ae8c0dc73ae58e563838c44eec81aeafa7d0b93a637841c9b/pymongo-4.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20323b0b1c1d33770ad1fc68d429c757734ce9ad3594421c3d6618f10572b1b9", size = 2072916, upload-time = "2026-04-20T16:38:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/31/42/6e41d434297ffe8b30d9c3717916591a4a7be9075a0dcc2fafdfaaaa62ed/pymongo-4.17.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5a5de048e6da5c18e27cc2437e8c15b3b0cdc8385c15b41178b0caa3322a09c2", size = 2173234, upload-time = "2026-04-20T16:38:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cf/1e4a7db352ef9485831c7268dfe8402f0117b32a9ad54b16e810699e3617/pymongo-4.17.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dff3de1294fbbc1db0ba6b511f77b8e540601d092538a31312e99c8a91a78b1e", size = 2156784, upload-time = "2026-04-20T16:38:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/12/10/6195be29962a61ebb5f4bd9e4c7519890b172f7968a0a0d880398c6ddb02/pymongo-4.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faf03e4c2aafd6de626dbd30ba246d369ae33f47f10629d1bbe40f72115027a6", size = 2074446, upload-time = "2026-04-20T16:38:34.004Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/33410b8819837ed370c738587306bdf060b59cef11823be212f4a07703c5/pymongo-4.17.0-cp313-cp313-win32.whl", hash = "sha256:c9786665926a09630c5d420c79762cfadbff35a9438bcbc4c81a9fb5ab9228b7", size = 948435, upload-time = "2026-04-20T16:38:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/77/c0ed522f798a286b99acaa7914ed8d9c80ab091f97f57c59ffed72906e5e/pymongo-4.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:5960519b4d7168f1ecdd3ea10c81b2aedeb9423651aca953cfbc8e76705d3b38", size = 972847, upload-time = "2026-04-20T16:38:37.888Z" }, + { url = "https://files.pythonhosted.org/packages/97/f0/c39480a2db385fde23861d0c8acda41cdaf1d43e46579db72c5c013a2e81/pymongo-4.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:0ff6bd2f735ab5356541e3e57d5b7dbfbc3f2ee1ccb10b6b0f82d58af69d1d8e", size = 951575, upload-time = "2026-04-20T16:38:40.544Z" }, + { url = "https://files.pythonhosted.org/packages/da/49/2b0250762a89737ed6f9cea238331baca061b89a8ddd10dd17fee52c3970/pymongo-4.17.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff5aa3f1c7e3f08eb0e7a016c91ba468b1850ccfd63d9b1f12f56350f4974cef", size = 1040945, upload-time = "2026-04-20T16:38:42.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/1c/7a9b5447a08be20e84b6e5b17330917e8d6d9507daa3cd099a9309f11ad7/pymongo-4.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e816db649ba5d7de0568cf3a9f287a9dc9aad21cf0ca667ab156a7ef47fca0b0", size = 1041187, upload-time = "2026-04-20T16:38:45.358Z" }, + { url = "https://files.pythonhosted.org/packages/78/a1/71704f61632dfc90407a5834fe5f6132854937c4a3648f6c05c351d85a45/pymongo-4.17.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c4fded3a9f1d6a687e36ebd384ac6d00b9b00de1969aa74048e7051ec2a713", size = 2294806, upload-time = "2026-04-20T16:38:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b9/aff42be75108b96c2469b1d9329b912c15108f3e7ef32fdc86da8423c330/pymongo-4.17.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2db66aa8dd253a0fc1fad3b0d23d5b3993f7ebde02fbbd7727128debf2853675", size = 2348231, upload-time = "2026-04-20T16:38:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/f2/30/44c115b8ba1479942c15fd9480eb29a7da0ba68acd56983423ba0deb4a94/pymongo-4.17.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3987e96e7c7be4083d42e8ac2cc6c0d5b78db9973c90fce42ae800b616ca6b20", size = 2467614, upload-time = "2026-04-20T16:38:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/d2/84/21ee95c8bf0ca7acae7ec7eb365d740bf8fc0156c194baf2c3bdfcb85ec0/pymongo-4.17.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cee36b3c0d0354f880fa7a7fdcdaf2bb5e542c2281e25c1bfadf8cfe21eba7d2", size = 2445970, upload-time = "2026-04-20T16:38:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/06/89/081d7f1809d5ca09d1e47e49f2111b245f5694de3a7af32cd3a353a6f43f/pymongo-4.17.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:320b34457b20bbcc79997801f95d25ce00472915ca5241167242b42c4359e027", size = 2348605, upload-time = "2026-04-20T16:38:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/ea/c3/0d949f9d3f2a341c1f635c398c16615e96f89f51ff424ed81e914cf1a4de/pymongo-4.17.0-cp314-cp314-win32.whl", hash = "sha256:df4a644af9ae132d4bfdb2e9516ea51a615fd881caddfbfbd071cf1354844479", size = 1004119, upload-time = "2026-04-20T16:39:00.309Z" }, + { url = "https://files.pythonhosted.org/packages/f7/55/5c3a3db1048054c695c75c5964cc8bedc2247fdb5a75ef6fab4ec8bb013e/pymongo-4.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:c797f8a80957134f6dd9690367a0f8f5906d672119af2c6aa55f0c527b656bed", size = 1032314, upload-time = "2026-04-20T16:39:02.665Z" }, + { url = "https://files.pythonhosted.org/packages/e0/19/e235f39906134cb0ffd5574c5a59c355ef5380f0499644ab94994afbb109/pymongo-4.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:68fca71e05ee5da23a8d73cee8379dfb3d26e609a377cae731d742771ed96946", size = 1007627, upload-time = "2026-04-20T16:39:04.678Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/c4c1a86791415b14c684fa0908f9da96de91594a3fd1fa1b8dc689fbb800/pymongo-4.17.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b4384700cffc3f1dd98e088bc0072dedf6d7d68a230bb4b972665cf69c071c1e", size = 1099151, upload-time = "2026-04-20T16:39:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/81/4b/69c67f3e23fd9b23b9bedc7ebd23754881cc9d5c5d5b2a9811e96b07f475/pymongo-4.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:93641192644fa1ee0f34030e774fd31022a27ad11ba22cb1716142231524f8bd", size = 1099346, upload-time = "2026-04-20T16:39:08.996Z" }, + { url = "https://files.pythonhosted.org/packages/a2/19/a5208f62f9508a26d73acc69bd3821b8c8adae253679a3c26d2f9652f0d5/pymongo-4.17.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75bc3aa5b94fdb7138d357ec6ca61cd97e0c79f4f7f0bd3efe9639b15cc50942", size = 2619034, upload-time = "2026-04-20T16:39:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/77/27/426cba1ec5973082a56d4150798529bfdf4151c31391ed1fbbecb23ef2ac/pymongo-4.17.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e8f8e23c6df7c6d6929f5e734980b227706e73ee847517c9ba5af90f7fc466", size = 2689939, upload-time = "2026-04-20T16:39:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2e/f70993d1255e33f6ee59a4ec4371cc65bff7a7e3fda7d55c3386f25287e8/pymongo-4.17.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:15d3f3d732aecac1f8d481bde4029755615639bd3076f258a2147210aec8515a", size = 2824994, upload-time = "2026-04-20T16:39:16.057Z" }, + { url = "https://files.pythonhosted.org/packages/b3/eb/87b0e988ba889e1fcc3430c2cfc166b251872c813e92b43174298bee17ff/pymongo-4.17.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5f62862d0f87be481fa1fe8cb811994486773c94a2b61e509285e3f2890763", size = 2801745, upload-time = "2026-04-20T16:39:18.476Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/3f83412d086f682d4d468761d66ddc49cf161e786ea74073045eb4491c60/pymongo-4.17.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64837adbbd72073301af51bb0fc80e3d7707fe5527cea1033ba0320f0b2f881b", size = 2684636, upload-time = "2026-04-20T16:39:20.878Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/b75f6f4ab6c8beb50b0270a4f1e2530b5774f5e116563440e1677ca1820f/pymongo-4.17.0-cp314-cp314t-win32.whl", hash = "sha256:b93b22eedc62598cf5ee9d8c8007a8e9121c50fd88137012d8985500e9dc3151", size = 1056356, upload-time = "2026-04-20T16:39:22.996Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5e/648c8a238eef18a25ed8a169ea6542d4a860bbec3e95b3d9badac2935c71/pymongo-4.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3689ea34f6b647c7d1e7bdc60fcfb214b2789ed1359a7fb96569c69f50e5f18f", size = 1090964, upload-time = "2026-04-20T16:39:24.989Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cb/d9780b66939c4fc1f024bcc7be23a2abcfe06a9745ca8fa76dc73395482e/pymongo-4.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9543d8f84c2e5608565c08ac679774811e6730770d8a645439b073422a4276fb", size = 1058526, upload-time = "2026-04-20T16:39:27.924Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/packages/moss-data-connector/moss-connector-sqlite/.gitignore b/packages/moss-data-connector/moss-connector-sqlite/.gitignore new file mode 100644 index 00000000..b86b0644 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/.gitignore @@ -0,0 +1,10 @@ +build/ +dist/ +*.egg-info/ +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.env diff --git a/packages/moss-data-connector/moss-connector-sqlite/README.md b/packages/moss-data-connector/moss-connector-sqlite/README.md new file mode 100644 index 00000000..3c527f5d --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/README.md @@ -0,0 +1,57 @@ +# moss-connector-sqlite + +SQLite source connector for Moss. Self-contained - no separate core package to install. + +## Install + +```bash +pip install moss-connector-sqlite +``` + +No driver needed, uses Python's stdlib `sqlite3`. + +## Usage + +```python +import asyncio +from moss import DocumentInfo +from moss_connector_sqlite import SQLiteConnector, ingest + +async def main(): + source = SQLiteConnector( + database="./my.db", + query="SELECT id, title, body FROM articles", + mapper=lambda r: DocumentInfo( + id=str(r["id"]), + text=r["body"], + metadata={"title": r["title"]}, + ), + ) + + result = await ingest( + source, + project_id="your_project_id", + project_key="your_project_key", + index_name="articles", + ) + print(f"copied {result.doc_count} rows") + +asyncio.run(main()) +``` + +## Layout + +``` +src/ +├── __init__.py # re-exports SQLiteConnector and ingest +├── connector.py # SQLiteConnector class +└── ingest.py # ingest() - keep in sync with the other connector packages +``` + +## Tests + +```bash +pip install -e ".[dev]" +pytest tests/test_sqlite.py -v # mocked Moss, no network +pytest tests/test_integration_moss.py -v -s # live Moss (requires MOSS_PROJECT_ID, MOSS_PROJECT_KEY) +``` diff --git a/packages/moss-data-connector/moss-connector-sqlite/pyproject.toml b/packages/moss-data-connector/moss-connector-sqlite/pyproject.toml new file mode 100644 index 00000000..21816adc --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/pyproject.toml @@ -0,0 +1,55 @@ +[project] +name = "moss-connector-sqlite" +version = "0.0.1" +description = "SQLite source connector for moss-connectors." +readme = "README.md" +requires-python = ">=3.10,<3.15" +license = { text = "BSD-2-Clause" } +authors = [{ name = "InferEdge Inc.", email = "contact@moss.dev" }] +keywords = ["moss", "connectors", "sqlite", "ingest", "etl"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Database", +] +dependencies = [ + "moss>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "python-dotenv>=1.0.0", + "ruff>=0.5.0", +] + +[project.urls] +Homepage = "https://github.com/usemoss/moss" +Repository = "https://github.com/usemoss/moss" +Source = "https://github.com/usemoss/moss/tree/main/packages/moss-data-connector/moss-connector-sqlite" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +# Flat layout: src/ itself IS the package `moss_connector_sqlite`. +[tool.setuptools] +packages = ["moss_connector_sqlite"] +package-dir = { "moss_connector_sqlite" = "src" } + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "W", "F", "I", "B", "UP"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/packages/moss-data-connector/moss-connector-sqlite/src/__init__.py b/packages/moss-data-connector/moss-connector-sqlite/src/__init__.py new file mode 100644 index 00000000..03d1f81e --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/src/__init__.py @@ -0,0 +1,9 @@ +"""SQLite source connector for Moss. + + from moss_connector_sqlite import SQLiteConnector, ingest +""" + +from .connector import SQLiteConnector +from .ingest import ingest + +__all__ = ["SQLiteConnector", "ingest"] diff --git a/packages/moss-data-connector/moss-connector-sqlite/src/connector.py b/packages/moss-data-connector/moss-connector-sqlite/src/connector.py new file mode 100644 index 00000000..85db0d92 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/src/connector.py @@ -0,0 +1,38 @@ +"""SQLite connector - the reference implementation. + +Uses only the stdlib so it doubles as a zero-dep test fixture. +""" + +from __future__ import annotations + +import sqlite3 +from typing import Any, Callable, Iterator + +from moss import DocumentInfo + + +class SQLiteConnector: + """Run a SELECT against a SQLite database and yield one `DocumentInfo` per row. + + `mapper` turns a row (dict of column -> value) into a `DocumentInfo`, the + caller decides which columns become id / text / metadata / embedding. + """ + + def __init__( + self, + database: str, + query: str, + mapper: Callable[[dict[str, Any]], DocumentInfo], + ) -> None: + self.database = database + self.query = query + self.mapper = mapper + + def __iter__(self) -> Iterator[DocumentInfo]: + conn = sqlite3.connect(self.database) + conn.row_factory = sqlite3.Row + try: + for row in conn.execute(self.query): + yield self.mapper(dict(row)) + finally: + conn.close() diff --git a/packages/moss-data-connector/moss-connector-sqlite/src/ingest.py b/packages/moss-data-connector/moss-connector-sqlite/src/ingest.py new file mode 100644 index 00000000..68c44940 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/src/ingest.py @@ -0,0 +1,22 @@ +"""Copy rows into a Moss index.""" + +from __future__ import annotations + +from collections.abc import Iterable + +from moss import DocumentInfo, MossClient, MutationResult + + +async def ingest( + source: Iterable[DocumentInfo], + project_id: str, + project_key: str, + index_name: str, + model_id: str | None = None, +) -> MutationResult | None: + """Copy every `DocumentInfo` from `source` into a fresh Moss index.""" + docs = list(source) + if not docs: + return None + client = MossClient(project_id, project_key) + return await client.create_index(index_name, docs, model_id=model_id) diff --git a/packages/moss-data-connector/moss-connector-sqlite/tests/test_integration_sqlite_moss.py b/packages/moss-data-connector/moss-connector-sqlite/tests/test_integration_sqlite_moss.py new file mode 100644 index 00000000..62a6d3d5 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/tests/test_integration_sqlite_moss.py @@ -0,0 +1,116 @@ +"""End-to-end integration test against a real Moss project. + +This test actually creates an index on Moss, ingests from a local SQLite DB, +queries it, and deletes the index afterwards. It is SKIPPED unless both +MOSS_PROJECT_ID and MOSS_PROJECT_KEY are set in the environment (or in a +.env file at the repo root or package root). + +Run it with: + cd packages/moss-connectors + pytest tests/test_integration_moss.py -v -s + +Or filter to just this file: + pytest -k integration_moss -v +""" + +from __future__ import annotations + +import os +import sqlite3 +import uuid +from pathlib import Path + +import pytest + +# Load .env from the package dir, then the repo root, if present. +try: + from dotenv import load_dotenv + + _here = Path(__file__).resolve() + for candidate in ( + _here.parents[1] / ".env", # this package's own .env + _here.parents[2] / ".env", # shared creds at moss-data-connector/.env + _here.parents[4] / ".env", # /.env + ): + if candidate.exists(): + load_dotenv(candidate, override=False) +except ImportError: + pass # dotenv is optional; env vars can also be set directly. + +from moss import DocumentInfo, MossClient, QueryOptions # noqa: E402 + +from moss_connector_sqlite import SQLiteConnector, ingest # noqa: E402 + +PROJECT_ID = os.getenv("MOSS_PROJECT_ID") +PROJECT_KEY = os.getenv("MOSS_PROJECT_KEY") + +pytestmark = pytest.mark.skipif( + not (PROJECT_ID and PROJECT_KEY), + reason="Set MOSS_PROJECT_ID and MOSS_PROJECT_KEY to run the real integration test.", +) + + +@pytest.fixture() +def sqlite_source(tmp_path): + """A 5-row SQLite DB with recognisable, query-friendly content.""" + path = tmp_path / "articles.db" + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE articles (id INTEGER PRIMARY KEY, title TEXT, body TEXT)") + conn.executemany( + "INSERT INTO articles (id, title, body) VALUES (?, ?, ?)", + [ + (1, "Refund policy", "Refunds are processed within 3 to 5 business days."), + (2, "Shipping time", "Most orders ship within 24 hours of being placed."), + (3, "Contact support", "You can reach our support team 24/7 via live chat."), + (4, "Password reset", "To reset your password, click the link on the login page."), + (5, "Order tracking", "Every shipped order includes a tracking number by email."), + ], + ) + conn.commit() + conn.close() + return str(path) + + +async def test_sqlite_ingest_end_to_end(sqlite_source): + """Full round trip: SQLite -> Moss index -> query -> delete.""" + # ingest() builds its own MossClient from the creds; we need one here too + # for the query + cleanup assertions below. + client = MossClient(PROJECT_ID, PROJECT_KEY) + + # Unique index name per run so concurrent runs don't collide. + index_name = f"moss-connectors-e2e-{uuid.uuid4().hex[:8]}" + + try: + connector = SQLiteConnector( + database=sqlite_source, + query="SELECT id, title, body FROM articles", + mapper=lambda r: DocumentInfo( + id=str(r["id"]), + text=r["body"], + metadata={"title": r["title"]}, + ), + ) + + result = await ingest(connector, PROJECT_ID, PROJECT_KEY, index_name=index_name) + assert result is not None + assert result.doc_count == 5 + + # Query the live index. "refund" should pull back article 1. + await client.load_index(index_name) + result = await client.query(index_name, "how long do refunds take", QueryOptions(top_k=3)) + + assert result.docs, "expected at least one document in the search result" + top_ids = [d.id for d in result.docs] + assert "1" in top_ids, f"refund-policy doc not in top 3: {top_ids}" + + # Check the metadata survived the round trip. + refund_doc = next(d for d in result.docs if d.id == "1") + assert refund_doc.metadata is not None + assert refund_doc.metadata.get("title") == "Refund policy" + + finally: + # Always try to clean up, even if an assertion above failed. + try: + await client.delete_index(index_name) + except Exception as exc: # pragma: no cover, best-effort cleanup + print(f"warning: failed to delete test index {index_name}: {exc}") diff --git a/packages/moss-data-connector/moss-connector-sqlite/tests/test_sqlite.py b/packages/moss-data-connector/moss-connector-sqlite/tests/test_sqlite.py new file mode 100644 index 00000000..44853d8c --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/tests/test_sqlite.py @@ -0,0 +1,104 @@ +"""Unit tests for ingest() against SQLite and in-memory sources. No network. + +We patch `moss_connector_sqlite.ingest.MossClient` so ingest() builds a fake client +instead of a real one. +""" + +from __future__ import annotations + +import sqlite3 +from dataclasses import dataclass, field +from typing import Any +from unittest.mock import patch + +import pytest + +from moss import DocumentInfo + +from moss_connector_sqlite import SQLiteConnector, ingest + + +@dataclass +class FakeMutationResult: + doc_count: int + job_id: str = "fake-job-id" + index_name: str = "" + + +@dataclass +class FakeMossClient: + """Stand-in for moss.MossClient that records what would be uploaded.""" + + calls: list[dict[str, Any]] = field(default_factory=list) + + async def create_index(self, name, docs, model_id=None): + docs = list(docs) + self.calls.append({"name": name, "docs": docs, "model_id": model_id}) + return FakeMutationResult(doc_count=len(docs), index_name=name) + + +@pytest.fixture() +def fake_client(): + """Patch MossClient inside the ingest module so it returns our fake.""" + fake = FakeMossClient() + with patch("moss_connector_sqlite.ingest.MossClient", return_value=fake): + yield fake + + +@pytest.fixture() +def sqlite_db(tmp_path): + path = tmp_path / "test.db" + conn = sqlite3.connect(path) + conn.execute("CREATE TABLE articles (id INTEGER PRIMARY KEY, title TEXT, body TEXT)") + conn.executemany( + "INSERT INTO articles (id, title, body) VALUES (?, ?, ?)", + [(i, f"Title {i}", f"Body for article {i}") for i in range(1, 4)], + ) + conn.commit() + conn.close() + return str(path) + + +async def test_ingest_creates_index(sqlite_db, fake_client): + source = SQLiteConnector( + database=sqlite_db, + query="SELECT id, title, body FROM articles", + mapper=lambda r: DocumentInfo( + id=str(r["id"]), + text=r["body"], + metadata={"title": r["title"]}, + ), + ) + + result = await ingest(source, "fake_id", "fake_key", index_name="articles") + + assert result is not None + assert result.doc_count == 3 + assert len(fake_client.calls) == 1 + call = fake_client.calls[0] + assert call["name"] == "articles" + assert len(call["docs"]) == 3 + assert call["docs"][0].id == "1" + assert call["docs"][0].text == "Body for article 1" + assert call["docs"][0].metadata == {"title": "Title 1"} + + +async def test_empty_source_skips_network_call(fake_client): + result = await ingest([], "fake_id", "fake_key", "empty") + assert result is None + assert fake_client.calls == [] + + +async def test_embedding_passthrough(fake_client): + """A source of pre-built DocumentInfos (e.g. from a vector DB) works directly.""" + source = [ + DocumentInfo(id="a", text="hi", embedding=[0.1, 0.2, 0.3]), + DocumentInfo(id="b", text="bye", embedding=[0.4, 0.5, 0.6]), + ] + + await ingest(source, "fake_id", "fake_key", index_name="vecs") + + docs = fake_client.calls[0]["docs"] + # Moss stores embeddings as float32, so compare with tolerance. + assert docs[0].embedding == pytest.approx([0.1, 0.2, 0.3], rel=1e-6) + assert docs[1].embedding == pytest.approx([0.4, 0.5, 0.6], rel=1e-6) diff --git a/packages/moss-data-connector/moss-connector-sqlite/uv.lock b/packages/moss-data-connector/moss-connector-sqlite/uv.lock new file mode 100644 index 00000000..54e26f75 --- /dev/null +++ b/packages/moss-data-connector/moss-connector-sqlite/uv.lock @@ -0,0 +1,334 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <3.15" + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/12/2948fbe5513d062169bd91f7d7b1cd97bc8894f32946b71fa39f6e63ca0c/idna-3.12.tar.gz", hash = "sha256:724e9952cc9e2bd7550ea784adb098d837ab5267ef67a1ab9cf7846bdbdd8254", size = 194350, upload-time = "2026-04-21T13:32:48.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/b2/acc33950394b3becb2b664741a0c0889c7ef9f9ffbfa8d47eddb53a50abd/idna-3.12-py3-none-any.whl", hash = "sha256:60ffaa1858fac94c9c124728c24fcde8160f3fb4a7f79aa8cdd33a9d1af60a67", size = 68634, upload-time = "2026-04-21T13:32:47.403Z" }, +] + +[[package]] +name = "inferedge-moss-core" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/60/e07fef3d462e5a6c26e86f2cf4425e7a4b75bec04c7b7638b947a9dcd855/inferedge_moss_core-0.8.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a21d8b4eace2146053864f484327b55e5267af16ffc19ee6c4aaecf7c5ff3beb", size = 12011165, upload-time = "2026-03-24T17:40:49.314Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/5abe406d875903e0a15b8c96879187fe995fde659c6481c50804977a9e45/inferedge_moss_core-0.8.7-cp310-cp310-manylinux_2_38_x86_64.whl", hash = "sha256:aac7e5762d8811ce3fee40a3156fb061464c60bf202653f30b190d339f0cabdc", size = 13141973, upload-time = "2026-03-24T17:41:24.085Z" }, + { url = "https://files.pythonhosted.org/packages/81/38/d7d6e4d81ea74b803eaead416c13a3305f01c7ef40b6fca96b9788b4bfc6/inferedge_moss_core-0.8.7-cp310-cp310-manylinux_2_39_aarch64.whl", hash = "sha256:d2d014280f0ea9c5d34d23c3eefb7a009646bd4137b46eaf5b990f5cb46b89f6", size = 14618007, upload-time = "2026-03-24T17:40:46.605Z" }, + { url = "https://files.pythonhosted.org/packages/d2/81/f7abaebba1ce27ad5ec6d451bd224ae17c10614bc36a33a888a74af35d4b/inferedge_moss_core-0.8.7-cp310-cp310-win_amd64.whl", hash = "sha256:c05c6b1ca3228505397adfe64e60ae70d85e2a9a05699e04b4e7445fd75fcb97", size = 11010409, upload-time = "2026-03-24T17:46:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/db/90/675d540ba6724c7f48b9efaada616d08ab78b9c80900e8237253c139738f/inferedge_moss_core-0.8.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d3182449a4471a9ca2c4bec2bf36c6aa9360b2730d67f2ea1de565d7b2e5e93", size = 12010920, upload-time = "2026-03-24T17:40:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9b/85afd0c9c021718af9a6a285f0ecd5af549ad2dce59bbdb4b8ff7d89a481/inferedge_moss_core-0.8.7-cp311-cp311-manylinux_2_38_x86_64.whl", hash = "sha256:d68872a0020a0fa7825cc80d7cd3add35012462ce51565871838c1d43bd2ec59", size = 13142067, upload-time = "2026-03-24T17:41:53.912Z" }, + { url = "https://files.pythonhosted.org/packages/74/e9/b51a3de89afb8884a5728c675cf6984f94ecb1e8f9532f21d837d2e28544/inferedge_moss_core-0.8.7-cp311-cp311-manylinux_2_39_aarch64.whl", hash = "sha256:0a68b303ce5853c37dfbb80805bc6de1cba917289227d2cc35239081fc860251", size = 14617302, upload-time = "2026-03-24T17:40:57.978Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/c78e61c04ac78806a2f04d13997e8e19579f7d0a2cb398a8264e8e13fa34/inferedge_moss_core-0.8.7-cp311-cp311-win_amd64.whl", hash = "sha256:970777df88701085c713b9ead9e6fd1aefca9a4460cd10c6f1adcd88636b967b", size = 11009652, upload-time = "2026-03-24T17:46:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/33/912419b8520350ec40d5678f88e683b82c1829f01738ca2e10c332f898d1/inferedge_moss_core-0.8.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c4c5903aefa71a2f37bf639f31076cc998d03aa56305a0df54cf1c47549e268", size = 12008052, upload-time = "2026-03-24T17:40:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/00/c3/d0c5a4aee67804ec9a22609b70e46fb446f3e5729b24f4752a43b7663a6b/inferedge_moss_core-0.8.7-cp312-cp312-manylinux_2_38_x86_64.whl", hash = "sha256:5a8e946da18a4700903040b59e047344a9e4d4b46a01b78e359c6b7ccc619ee0", size = 13141260, upload-time = "2026-03-24T17:41:36.205Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cdf2669d301786b86a3bcac8625e198b6ec77805ecf5775b0f8ee600e490/inferedge_moss_core-0.8.7-cp312-cp312-manylinux_2_39_aarch64.whl", hash = "sha256:72260b77311fda63528cf17eef0afbf22cdc8ee6123a2e75273b44917ecfc38e", size = 14619836, upload-time = "2026-03-24T17:40:47.331Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8a/77d7c05dca68b409a662115d5e0a7f4a8a5932fe6e54dc6c3c35debb4cef/inferedge_moss_core-0.8.7-cp312-cp312-win_amd64.whl", hash = "sha256:956fd52f557d99aa216015b71e01e2211ee4b19da298687a584864d80d00394c", size = 11009060, upload-time = "2026-03-24T17:45:47.58Z" }, + { url = "https://files.pythonhosted.org/packages/67/48/41db4ec8482efe2a0d2021e02411a11dbc3004c8172925ee2ff0c61dfb54/inferedge_moss_core-0.8.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:060bfa1dce0998a5d0095a502742e62c29664b57a6a426b649f188d657217e2d", size = 12007900, upload-time = "2026-03-24T17:40:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f6/55e27007f46234082a3847446435f00a1815eebf7fcebe526183f1b8cc45/inferedge_moss_core-0.8.7-cp313-cp313-manylinux_2_38_x86_64.whl", hash = "sha256:628d5808a05a6cbd0aa6fa7b01fe6b4642b5df8aa48d379a66e608e16f300225", size = 13141311, upload-time = "2026-03-24T17:41:29.318Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3b/aff5bbce1e45d41961c103c5d83773206cb9300c87f9bc0d386df8dcb167/inferedge_moss_core-0.8.7-cp313-cp313-manylinux_2_39_aarch64.whl", hash = "sha256:a2ae3ae2bff6b0419c8040bd705cce3739e2d5b6a8afb1640dca5415cf50f683", size = 14622134, upload-time = "2026-03-24T17:40:48.58Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d8/8cfe68fc8f66bbdadb358b5bd50a97491d2dbd359c737b0dd7579b3c905e/inferedge_moss_core-0.8.7-cp313-cp313-win_amd64.whl", hash = "sha256:506a533e11379e86d227c44d10caf8e89a3302fae01131bb7bafa4d3d28313be", size = 11008608, upload-time = "2026-03-24T17:46:21.654Z" }, + { url = "https://files.pythonhosted.org/packages/0a/90/755c63a4806ba8307a2f0c9e9c595891696fcf44e126d60738449eb9aae9/inferedge_moss_core-0.8.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4e66c21910af6c692c936ab1c72e01748d65e420113d4510a6e5097e578fc298", size = 12008127, upload-time = "2026-03-24T17:39:51.89Z" }, + { url = "https://files.pythonhosted.org/packages/2f/17/83d9b56d7fe71dbe5ac57080550459761c66a6d90cb3e039dc8d6c2b2adc/inferedge_moss_core-0.8.7-cp314-cp314-manylinux_2_38_x86_64.whl", hash = "sha256:54aa26a391eb8ebd9dcebaeab72ae81065462da2b33d3bb580d2b0cc6e2bcabb", size = 13138063, upload-time = "2026-03-24T17:41:22.992Z" }, + { url = "https://files.pythonhosted.org/packages/30/16/9cb36e013ec6feccbde28181f853764babe94a450b432893ec60f7469874/inferedge_moss_core-0.8.7-cp314-cp314-manylinux_2_39_aarch64.whl", hash = "sha256:6dec35c0cd22ab77f68c8e94b7b6f550dd70e662ba85dc60823ff80445cf9e32", size = 14618319, upload-time = "2026-03-24T17:41:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e0/93dbfbfb358065c0ac9df5d6be6e6ddaaea53260ebd654900f0992abf9ce/inferedge_moss_core-0.8.7-cp314-cp314-win_amd64.whl", hash = "sha256:e14a8959a3a47b6ed8e783d51b5ea9cd901b803bfb0c02ce30e1213eb25fb3f3", size = 11005063, upload-time = "2026-03-24T17:45:14.825Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "moss" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "inferedge-moss-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/a7/971c660de82707de114a8f342e0b2a9f3c95f9b88823eb7a5f9872258cef/moss-1.0.0.tar.gz", hash = "sha256:20225f8604c9b1126a023fe38b48debd1911501828acba95a9e61ccb5d9844eb", size = 32591, upload-time = "2026-03-30T02:38:07.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/46/7e61b04ce3d4a029cd228ad0e268089e4b4c12d400a0f8ca06466218201e/moss-1.0.0-py3-none-any.whl", hash = "sha256:94c7b6021b49370a6b70cacdd8025633d00229848ec52f7d566efedbabede812", size = 12112, upload-time = "2026-03-30T02:38:06.673Z" }, +] + +[[package]] +name = "moss-connector-sqlite" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "moss" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "moss", specifier = ">=1.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]