Skip to content

Commit 02df728

Browse files
authored
feat: add IndexResource for listing and retrieving indexes (#39)
* feat: create indexes package * feat: add Index models * feat: add IndexResource * feat: import index models in protocols * feat: add indexes method to AsyncClientProtocol * feat: add IndexResourceProtocol * feat: import IndexResource in client * feat: add indexes method to AsyncDeepsetClient * test: add IndexResource tests * test: add IndexResource integration tests * fix: tests, linting, types
1 parent 40a2b52 commit 02df728

11 files changed

Lines changed: 281 additions & 10 deletions

File tree

src/deepset_mcp/api/client.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Self, TypeVar, overload
44

55
from deepset_mcp.api.haystack_service.resource import HaystackServiceResource
6+
from deepset_mcp.api.indexes.resource import IndexResource
67
from deepset_mcp.api.pipeline.resource import PipelineResource
78
from deepset_mcp.api.pipeline_template.resource import PipelineTemplateResource
89
from deepset_mcp.api.protocols import AsyncClientProtocol
@@ -134,3 +135,7 @@ def haystack_service(self) -> HaystackServiceResource:
134135
def pipeline_templates(self, workspace: str) -> PipelineTemplateResource:
135136
"""Resource to interact with pipeline templates in the specified workspace."""
136137
return PipelineTemplateResource(client=self, workspace=workspace)
138+
139+
def indexes(self, workspace: str) -> IndexResource:
140+
"""Resource to interact with indexes in the specified workspace."""
141+
return IndexResource(client=self, workspace=workspace)

src/deepset_mcp/api/indexes/__init__.py

Whitespace-only changes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from datetime import datetime
2+
from typing import Any
3+
4+
from pydantic import BaseModel
5+
6+
from deepset_mcp.api.shared_models import DeepsetUser
7+
8+
9+
class IndexStatus(BaseModel):
10+
"""Status information about documents in an index."""
11+
12+
pending_file_count: int
13+
failed_file_count: int
14+
indexed_no_documents_file_count: int
15+
indexed_file_count: int
16+
total_file_count: int
17+
18+
19+
class Index(BaseModel):
20+
"""A deepset index."""
21+
22+
pipeline_index_id: str
23+
name: str
24+
description: str | None = None
25+
config_yaml: str
26+
workspace_id: str
27+
settings: dict[str, Any]
28+
desired_status: str
29+
deployed_at: datetime | None = None
30+
last_edited_at: datetime | None = None
31+
max_index_replica_count: int
32+
created_at: datetime
33+
updated_at: datetime | None = None
34+
created_by: DeepsetUser
35+
last_edited_by: DeepsetUser | None = None
36+
status: IndexStatus
37+
38+
39+
class IndexList(BaseModel):
40+
"""Response model for listing indexes."""
41+
42+
data: list[Index]
43+
has_more: bool
44+
total: int
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from deepset_mcp.api.indexes.models import Index, IndexList
2+
from deepset_mcp.api.protocols import AsyncClientProtocol
3+
from deepset_mcp.api.transport import raise_for_status
4+
5+
6+
class IndexResource:
7+
"""Resource for interacting with deepset indexes."""
8+
9+
def __init__(self, client: AsyncClientProtocol, workspace: str) -> None:
10+
"""Initialize the index resource.
11+
12+
:param client: The async REST client.
13+
:param workspace: The workspace to use.
14+
"""
15+
self._client = client
16+
self._workspace = workspace
17+
18+
async def list(self, limit: int = 10, page_number: int = 1) -> IndexList:
19+
"""List all indexes.
20+
21+
:param limit: Maximum number of indexes to return.
22+
:param page_number: Page number for pagination.
23+
24+
:returns: List of indexes.
25+
"""
26+
params = {
27+
"limit": limit,
28+
"page_number": page_number,
29+
}
30+
31+
response = await self._client.request(f"/api/v1/workspaces/{self._workspace}/indexes", params=params)
32+
33+
raise_for_status(response)
34+
35+
return IndexList.model_validate(response.json)
36+
37+
async def get(self, index_name: str) -> Index:
38+
"""Get a specific index.
39+
40+
:param index_name: Name of the index.
41+
42+
:returns: Index details.
43+
"""
44+
response = await self._client.request(f"/api/v1/workspaces/{self._workspace}/indexes/{index_name}")
45+
46+
raise_for_status(response)
47+
48+
return Index.model_validate(response.json)

src/deepset_mcp/api/pipeline/models.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from pydantic import BaseModel, Field
55

6+
from deepset_mcp.api.shared_models import DeepsetUser
7+
68

79
class PipelineServiceLevel(StrEnum):
810
"""Describes the service level of a pipeline."""
@@ -12,15 +14,6 @@ class PipelineServiceLevel(StrEnum):
1214
DRAFT = "DRAFT"
1315

1416

15-
class DeepsetUser(BaseModel):
16-
"""Model representing a user on the deepset platform."""
17-
18-
id: str = Field(alias="user_id")
19-
given_name: str | None = None
20-
family_name: str | None = None
21-
email: str | None = None
22-
23-
2417
class DeepsetPipeline(BaseModel):
2518
"""Model representing a pipeline on the deepset platform."""
2619

src/deepset_mcp/api/protocols.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from types import TracebackType
22
from typing import Any, Protocol, Self, TypeVar, overload
33

4+
from deepset_mcp.api.indexes.models import Index, IndexList
45
from deepset_mcp.api.pipeline.models import DeepsetPipeline, NoContentResponse, PipelineValidationResult
56
from deepset_mcp.api.pipeline_template.models import PipelineTemplate
67
from deepset_mcp.api.transport import TransportResponse
@@ -87,6 +88,22 @@ def pipeline_templates(self, workspace: str) -> "PipelineTemplateResourceProtoco
8788
"""Access pipeline templates in the specified workspace."""
8889
...
8990

91+
def indexes(self, workspace: str) -> "IndexResourceProtocol":
92+
"""Access indexes in the specified workspace."""
93+
...
94+
95+
96+
class IndexResourceProtocol(Protocol):
97+
"""Protocol defining the implementation for IndexResource."""
98+
99+
async def list(self, limit: int = 10, page_number: int = 1) -> IndexList:
100+
"""List indexes in the configured workspace."""
101+
...
102+
103+
async def get(self, index_name: str) -> Index:
104+
"""Fetch a single index by its name."""
105+
...
106+
90107

91108
class PipelineTemplateResourceProtocol(Protocol):
92109
"""Protocol defining the implementation for PipelineTemplateResource."""
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from pydantic import BaseModel, Field
2+
3+
4+
class DeepsetUser(BaseModel):
5+
"""Model representing a user on the deepset platform."""
6+
7+
id: str = Field(alias="user_id")
8+
given_name: str | None = None
9+
family_name: str | None = None
10+
email: str | None = None

test/unit/api/indexes/__init__.py

Whitespace-only changes.
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import json
2+
from typing import Any
3+
4+
import pytest
5+
6+
from deepset_mcp.api.exceptions import ResourceNotFoundError, UnexpectedAPIError
7+
from deepset_mcp.api.indexes.models import Index, IndexList
8+
from deepset_mcp.api.indexes.resource import IndexResource
9+
from deepset_mcp.api.transport import TransportResponse
10+
from test.unit.conftest import BaseFakeClient
11+
12+
13+
@pytest.fixture()
14+
def fake_client() -> BaseFakeClient:
15+
return BaseFakeClient()
16+
17+
18+
@pytest.fixture
19+
def index_response() -> dict[str, Any]:
20+
"""Sample response for an index."""
21+
return {
22+
"pipeline_index_id": "my-id",
23+
"name": "test-index",
24+
"description": None,
25+
"config_yaml": "yaml: content",
26+
"workspace_id": "my-workspace",
27+
"settings": {},
28+
"desired_status": "DEPLOYED",
29+
"deployed_at": "2025-01-01T00:00:00Z",
30+
"last_edited_at": "2025-01-01T00:00:00Z",
31+
"max_index_replica_count": 10,
32+
"created_at": "2025-01-01T00:00:00Z",
33+
"updated_at": "2025-01-01T00:00:00Z",
34+
"created_by": {"given_name": "Test", "family_name": "User", "user_id": "test-id"},
35+
"last_edited_by": {"given_name": "Test", "family_name": "User", "user_id": "test-id"},
36+
"status": {
37+
"pending_file_count": 0,
38+
"failed_file_count": 0,
39+
"indexed_no_documents_file_count": 0,
40+
"indexed_file_count": 0,
41+
"total_file_count": 0,
42+
},
43+
}
44+
45+
46+
@pytest.fixture
47+
def index_list_response(index_response: dict[str, Any]) -> dict[str, Any]:
48+
"""Sample response for listing indexes."""
49+
return {"data": [index_response], "has_more": False, "total": 1}
50+
51+
52+
@pytest.fixture
53+
def workspace() -> str:
54+
"""Sample workspace ID."""
55+
return "test-workspace"
56+
57+
58+
@pytest.fixture()
59+
def fake_list_successful_response(
60+
fake_client: BaseFakeClient, index_list_response: dict[str, Any], workspace: str
61+
) -> None:
62+
"""Configure the fake client to return a successful response."""
63+
fake_client.responses[f"/api/v1/workspaces/{workspace}/indexes"] = TransportResponse(
64+
status_code=200,
65+
json=index_list_response,
66+
text=json.dumps(index_list_response),
67+
)
68+
69+
70+
@pytest.fixture()
71+
def fake_get_successful_response(fake_client: BaseFakeClient, index_response: dict[str, Any], workspace: str) -> None:
72+
"""Configure the fake client to return a successful response."""
73+
fake_client.responses[f"/api/v1/workspaces/{workspace}/indexes/test-index"] = TransportResponse(
74+
status_code=200,
75+
json=index_response,
76+
text=json.dumps(index_response),
77+
)
78+
79+
80+
@pytest.fixture()
81+
def fake_get_404_response(fake_client: BaseFakeClient, workspace: str) -> None:
82+
"""Configure fake client to return a 404 response."""
83+
fake_client.responses[f"/api/v1/workspaces/{workspace}/indexes/nonexistent-index"] = TransportResponse(
84+
status_code=404,
85+
json={"detail": "Resource not found"},
86+
text=json.dumps({"detail": "Resource not found"}),
87+
)
88+
89+
90+
@pytest.fixture()
91+
def fake_get_500_response(fake_client: BaseFakeClient, workspace: str) -> None:
92+
"""Configure fake client to return a 500 response."""
93+
fake_client.responses[f"/api/v1/workspaces/{workspace}/indexes/server-error-index"] = TransportResponse(
94+
status_code=500,
95+
json={"detail": "Internal server error"},
96+
text=json.dumps({"detail": "Internal server error"}),
97+
)
98+
99+
100+
class TestIndexResource:
101+
"""Test the IndexResource."""
102+
103+
async def test_get_index_returns_index(
104+
self, fake_client: BaseFakeClient, workspace: str, fake_get_successful_response: None
105+
) -> None:
106+
"""Test that getting an index returns an Index instance."""
107+
resource = IndexResource(fake_client, workspace)
108+
result = await resource.get("test-index")
109+
assert isinstance(result, Index)
110+
assert result.name == "test-index"
111+
112+
async def test_list_indexes_returns_list(
113+
self, fake_client: BaseFakeClient, workspace: str, fake_list_successful_response: None
114+
) -> None:
115+
"""Test that listing indexes returns an IndexList instance."""
116+
resource = IndexResource(fake_client, workspace)
117+
result = await resource.list()
118+
assert isinstance(result, IndexList)
119+
assert len(result.data) == 1
120+
assert isinstance(result.data[0], Index)
121+
assert result.total == 1
122+
assert result.has_more is False
123+
124+
async def test_get_nonexistent_index_raises_404(
125+
self, fake_client: BaseFakeClient, workspace: str, fake_get_404_response: None
126+
) -> None:
127+
"""Test that getting a nonexistent index raises ResourceNotFoundError."""
128+
resource = IndexResource(fake_client, workspace)
129+
with pytest.raises(ResourceNotFoundError):
130+
await resource.get("nonexistent-index")
131+
132+
async def test_get_server_error_raises_500(
133+
self, fake_client: BaseFakeClient, workspace: str, fake_get_500_response: None
134+
) -> None:
135+
"""Test that server error raises UnexpectedAPIError."""
136+
resource = IndexResource(fake_client, workspace)
137+
with pytest.raises(UnexpectedAPIError):
138+
await resource.get("server-error-index")
139+
140+
async def test_list_indexes_passes_params(
141+
self, fake_client: BaseFakeClient, workspace: str, fake_list_successful_response: None
142+
) -> None:
143+
"""Test that parameters are passed to the client in list method."""
144+
resource = IndexResource(fake_client, workspace)
145+
await resource.list(limit=20, page_number=2)
146+
147+
# Check the last request's parameters
148+
last_request = fake_client.requests[-1]
149+
assert last_request["params"] == {"limit": 20, "page_number": 2}

test/unit/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from deepset_mcp.api.protocols import (
66
AsyncClientProtocol,
77
HaystackServiceProtocol,
8+
IndexResourceProtocol,
89
PipelineResourceProtocol,
910
PipelineTemplateResourceProtocol,
1011
)
@@ -140,3 +141,7 @@ def haystack_service(self) -> HaystackServiceProtocol:
140141
def pipeline_templates(self, workspace: str) -> PipelineTemplateResourceProtocol:
141142
"""Overwrite this method when testing PipelineTemplateResource."""
142143
raise NotImplementedError
144+
145+
def indexes(self, workspace: str) -> IndexResourceProtocol:
146+
"""Overwrite this method when testing IndexResource."""
147+
raise NotImplementedError

0 commit comments

Comments
 (0)