feat: improving single conclusion retrieval
This commit is contained in:
parent
45d56758d4
commit
94ace5e3f5
|
|
@ -2423,45 +2423,6 @@
|
|||
}
|
||||
},
|
||||
"/v3/workspaces/{workspace_id}/conclusions/{conclusion_id}": {
|
||||
"get": {
|
||||
"tags": ["conclusions"],
|
||||
"summary": "Get Conclusion",
|
||||
"description": "Get a single Conclusion by ID.",
|
||||
"operationId": "get_conclusion_v3_workspaces__workspace_id__conclusions__conclusion_id__get",
|
||||
"security": [{ "HTTPBearer": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Workspace Id" }
|
||||
},
|
||||
{
|
||||
"name": "conclusion_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Conclusion Id" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/Conclusion" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["conclusions"],
|
||||
"summary": "Delete Conclusion",
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ from .conclusions import (
|
|||
Conclusion,
|
||||
_reject_reserved_filter_keys,
|
||||
)
|
||||
from .http import routes
|
||||
from .http import NotFoundError, routes
|
||||
from .message import Message
|
||||
from .mixins import AsyncMetadataConfigMixin
|
||||
from .pagination import AsyncPage
|
||||
|
|
@ -1603,15 +1603,25 @@ class ConclusionScopeAio:
|
|||
async def get(self, conclusion_id: str) -> Conclusion:
|
||||
"""Get a single conclusion by ID asynchronously.
|
||||
|
||||
Equivalent to ``list`` with an ``id`` filter.
|
||||
|
||||
Returns:
|
||||
The Conclusion object, including its attribution fields
|
||||
(`source_ids`, `times_derived`)
|
||||
|
||||
Raises:
|
||||
NotFoundError: If no conclusion with the given ID exists
|
||||
"""
|
||||
await self._scope._honcho._ensure_workspace_async()
|
||||
data = await self._scope._honcho._async_http_client.get(
|
||||
routes.conclusion(self._scope.workspace_id, conclusion_id)
|
||||
data = await self._scope._honcho._async_http_client.post(
|
||||
routes.conclusions_list(self._scope.workspace_id),
|
||||
body={"filters": {"id": conclusion_id}},
|
||||
query={"page": 1, "size": 1},
|
||||
)
|
||||
return Conclusion.from_api_response(ConclusionResponse.model_validate(data))
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
raise NotFoundError("Conclusion not found")
|
||||
return Conclusion.from_api_response(ConclusionResponse.model_validate(items[0]))
|
||||
|
||||
async def get_many(self, conclusion_ids: list[str]) -> list[Conclusion]:
|
||||
"""Get multiple conclusions by ID in a single call asynchronously.
|
||||
|
|
@ -1662,7 +1672,7 @@ class ConclusionScopeAio:
|
|||
size: Number of results per page. Default: 50.
|
||||
reverse: If True, reverses the default newest-first ordering.
|
||||
|
||||
Sugar for ``list`` with a ``parent_id`` filter; an unknown
|
||||
Equivalent to ``list`` with a ``parent_id`` filter; an unknown
|
||||
``conclusion_id`` yields an empty page rather than an error.
|
||||
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from pydantic import BaseModel
|
|||
|
||||
from .api_types import ConclusionLevel, ConclusionResponse, RepresentationResponse
|
||||
from .base import SessionBase
|
||||
from .http import routes
|
||||
from .http import NotFoundError, routes
|
||||
from .pagination import SyncPage
|
||||
from .utils import resolve_id
|
||||
|
||||
|
|
@ -332,18 +332,28 @@ class ConclusionScope:
|
|||
"""
|
||||
Get a single conclusion by ID.
|
||||
|
||||
Equivalent to ``list`` with an ``id`` filter.
|
||||
|
||||
Args:
|
||||
conclusion_id: The ID of the conclusion to retrieve
|
||||
|
||||
Returns:
|
||||
The Conclusion object, including its attribution fields
|
||||
(`source_ids`, `times_derived`)
|
||||
|
||||
Raises:
|
||||
NotFoundError: If no conclusion with the given ID exists
|
||||
"""
|
||||
self._honcho._ensure_workspace()
|
||||
data = self._honcho._http.get(
|
||||
routes.conclusion(self.workspace_id, conclusion_id)
|
||||
data = self._honcho._http.post(
|
||||
routes.conclusions_list(self.workspace_id),
|
||||
body={"filters": {"id": conclusion_id}},
|
||||
query={"page": 1, "size": 1},
|
||||
)
|
||||
return Conclusion.from_api_response(ConclusionResponse.model_validate(data))
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
raise NotFoundError("Conclusion not found")
|
||||
return Conclusion.from_api_response(ConclusionResponse.model_validate(items[0]))
|
||||
|
||||
def get_many(self, conclusion_ids: list[str]) -> list[Conclusion]:
|
||||
"""
|
||||
|
|
@ -397,7 +407,7 @@ class ConclusionScope:
|
|||
size: Number of results per page. Default: 50.
|
||||
reverse: If True, reverses the default newest-first ordering.
|
||||
|
||||
Sugar for ``list`` with a ``parent_id`` filter; an unknown
|
||||
Equivalent to ``list`` with a ``parent_id`` filter; an unknown
|
||||
``conclusion_id`` yields an empty page rather than an error.
|
||||
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -334,10 +334,10 @@ describe('Conclusions', () => {
|
|||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Single Conclusion Retrieval (GET /conclusions/:id)
|
||||
// Single Conclusion Retrieval (list with an id filter)
|
||||
// ===========================================================================
|
||||
|
||||
describe('GET /conclusions/:id (get)', () => {
|
||||
describe('get (list with id filter)', () => {
|
||||
test('get returns conclusion with attribution fields', async () => {
|
||||
const peer = await client.peer('get-conclusion-peer', { metadata: {} })
|
||||
const session = await client.session('get-conclusion-session', { metadata: {} })
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { API_VERSION } from './api-version'
|
||||
import type { HonchoHTTPClient } from './http/client'
|
||||
import { NotFoundError } from './http/errors'
|
||||
import { Page } from './pagination'
|
||||
import type { Session } from './session'
|
||||
import type {
|
||||
|
|
@ -208,10 +209,17 @@ export class ConclusionScope {
|
|||
}
|
||||
|
||||
private async _get(conclusionId: string): Promise<ConclusionResponse> {
|
||||
await this._ensureWorkspace()
|
||||
return this._http.get<ConclusionResponse>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/conclusions/${conclusionId}`
|
||||
)
|
||||
// Equivalent to list with an `id` filter; there is no single-GET endpoint.
|
||||
const response = await this._list({
|
||||
filters: { id: conclusionId },
|
||||
page: 1,
|
||||
size: 1,
|
||||
})
|
||||
const item = response.items?.[0]
|
||||
if (!item) {
|
||||
throw new NotFoundError('Conclusion not found')
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
private async _derived(
|
||||
|
|
@ -222,7 +230,7 @@ export class ConclusionScope {
|
|||
reverse?: boolean
|
||||
}
|
||||
): Promise<PageResponse<ConclusionResponse>> {
|
||||
// Sugar for list with a parent_id filter; an unknown conclusionId
|
||||
// Equivalent to list with a parent_id filter; an unknown conclusionId
|
||||
// yields an empty page rather than an error.
|
||||
return this._list({
|
||||
filters: { parent_id: conclusionId },
|
||||
|
|
|
|||
|
|
@ -134,28 +134,6 @@ async def query_conclusions(
|
|||
return [schemas.Conclusion.model_validate(doc) for doc in documents]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{conclusion_id}",
|
||||
response_model=schemas.Conclusion,
|
||||
)
|
||||
async def get_conclusion(
|
||||
workspace_id: str = Path(...),
|
||||
conclusion_id: str = Path(...),
|
||||
db: AsyncSession = read_db,
|
||||
) -> schemas.Conclusion:
|
||||
"""
|
||||
Get a single Conclusion by ID.
|
||||
"""
|
||||
documents = await crud.get_documents_by_ids(
|
||||
db,
|
||||
workspace_name=workspace_id,
|
||||
document_ids=[conclusion_id],
|
||||
)
|
||||
if not documents:
|
||||
raise ResourceNotFoundException("Conclusion not found")
|
||||
return schemas.Conclusion.model_validate(documents[0])
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{conclusion_id}",
|
||||
status_code=204,
|
||||
|
|
|
|||
|
|
@ -667,13 +667,14 @@ class TestConclusionRoutes:
|
|||
assert "not found" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conclusion_success(
|
||||
async def test_list_conclusion_by_id_success(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test getting a single conclusion by ID with attribution fields"""
|
||||
"""Test getting a single conclusion via list with an id filter,
|
||||
including attribution fields"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
|
|
@ -719,13 +720,16 @@ class TestConclusionRoutes:
|
|||
db_session.add(derived)
|
||||
await db_session.commit()
|
||||
|
||||
# Get conclusion by ID
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/{derived.id}"
|
||||
# Get conclusion via list with an id filter
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": derived.id}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
conclusion = response.json()
|
||||
items = response.json()["items"]
|
||||
assert len(items) == 1
|
||||
conclusion = items[0]
|
||||
assert conclusion["id"] == derived.id
|
||||
assert conclusion["content"] == "User is likely a night owl"
|
||||
assert conclusion["observer_id"] == test_peer.name
|
||||
|
|
@ -739,7 +743,7 @@ class TestConclusionRoutes:
|
|||
assert "internal_metadata" not in conclusion
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conclusion_legacy_source_ids(
|
||||
async def test_list_conclusion_by_id_legacy_source_ids(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
|
|
@ -775,39 +779,41 @@ class TestConclusionRoutes:
|
|||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/{doc.id}"
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": doc.id}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
conclusion = response.json()
|
||||
assert conclusion["source_ids"] is None
|
||||
items = response.json()["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["source_ids"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conclusion_not_found(
|
||||
async def test_list_conclusion_by_id_not_found(
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test getting a non-existent conclusion"""
|
||||
"""Listing with a non-existent id filter yields an empty page"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/nonexistent_id"
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": "nonexistent_id"}},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
assert response.status_code == 200
|
||||
assert response.json()["items"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_conclusion_soft_deleted(
|
||||
async def test_list_conclusion_by_id_soft_deleted(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that a soft-deleted conclusion returns 404"""
|
||||
"""Test that a soft-deleted conclusion is excluded from list results"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
|
|
@ -837,10 +843,12 @@ class TestConclusionRoutes:
|
|||
)
|
||||
assert delete_response.status_code == 204
|
||||
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/{doc.id}"
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": doc.id}},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert response.status_code == 200
|
||||
assert response.json()["items"] == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conclusions_includes_attribution_fields(
|
||||
|
|
|
|||
Loading…
Reference in New Issue