feat: restore GET /conclusions/{conclusion_id}
This commit is contained in:
parent
b5a5219a09
commit
7aea397f3e
|
|
@ -224,6 +224,7 @@
|
|||
"v3/api-reference/endpoint/conclusions/create-conclusions",
|
||||
"v3/api-reference/endpoint/conclusions/list-conclusions",
|
||||
"v3/api-reference/endpoint/conclusions/query-conclusions",
|
||||
"v3/api-reference/endpoint/conclusions/get-conclusion",
|
||||
"v3/api-reference/endpoint/conclusions/delete-conclusion"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: get /v3/workspaces/{workspace_id}/conclusions/{conclusion_id}
|
||||
---
|
||||
|
|
@ -2423,6 +2423,45 @@
|
|||
}
|
||||
},
|
||||
"/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,8 +52,9 @@ from .conclusions import (
|
|||
_SCOPE_RESERVED,
|
||||
Conclusion,
|
||||
_reject_reserved_filter_keys,
|
||||
_require_scope,
|
||||
)
|
||||
from .http import NotFoundError, routes
|
||||
from .http import routes
|
||||
from .message import Message
|
||||
from .mixins import AsyncMetadataConfigMixin
|
||||
from .pagination import AsyncPage
|
||||
|
|
@ -1495,21 +1496,12 @@ class SessionAio(AsyncMetadataConfigMixin):
|
|||
return Message.from_api_response(MessageResponse.model_validate(data))
|
||||
|
||||
|
||||
async def _aget_conclusion(
|
||||
honcho: "Honcho",
|
||||
*,
|
||||
filters: dict[str, Any],
|
||||
) -> Conclusion:
|
||||
async def _aget_conclusion(honcho: "Honcho", conclusion_id: str) -> Conclusion:
|
||||
await honcho._ensure_workspace_async()
|
||||
data = await honcho._async_http_client.post(
|
||||
routes.conclusions_list(honcho.workspace_id),
|
||||
body={"filters": filters},
|
||||
query={"page": 1, "size": 1},
|
||||
data = await honcho._async_http_client.get(
|
||||
routes.conclusion(honcho.workspace_id, conclusion_id)
|
||||
)
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
raise NotFoundError("Conclusion not found")
|
||||
return Conclusion.from_api_response(ConclusionResponse.model_validate(items[0]))
|
||||
return Conclusion.from_api_response(ConclusionResponse.model_validate(data))
|
||||
|
||||
|
||||
async def _aget_many_conclusions(
|
||||
|
|
@ -1598,9 +1590,7 @@ class WorkspaceConclusionsAio:
|
|||
|
||||
async def get(self, conclusion_id: str) -> Conclusion:
|
||||
"""Get a single conclusion by ID, anywhere in the workspace."""
|
||||
return await _aget_conclusion(
|
||||
self._workspace._honcho, filters={"id": conclusion_id}
|
||||
)
|
||||
return await _aget_conclusion(self._workspace._honcho, conclusion_id)
|
||||
|
||||
async def get_many(self, conclusion_ids: list[str]) -> list[Conclusion]:
|
||||
"""Get multiple conclusions by ID. Missing IDs are omitted."""
|
||||
|
|
@ -1697,8 +1687,6 @@ 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`)
|
||||
|
|
@ -1708,13 +1696,10 @@ class ConclusionScopeAio:
|
|||
observer/observed pair. Use ``honcho.aio.conclusions.get`` for a
|
||||
workspace-wide lookup.
|
||||
"""
|
||||
return await _aget_conclusion(
|
||||
self._scope._honcho,
|
||||
filters={
|
||||
"id": conclusion_id,
|
||||
"observer_id": self._scope.observer,
|
||||
"observed_id": self._scope.observed,
|
||||
},
|
||||
return _require_scope(
|
||||
await _aget_conclusion(self._scope._honcho, conclusion_id),
|
||||
self._scope.observer,
|
||||
self._scope.observed,
|
||||
)
|
||||
|
||||
async def get_many(self, conclusion_ids: list[str]) -> list[Conclusion]:
|
||||
|
|
|
|||
|
|
@ -67,21 +67,18 @@ def _conclusion_from_item(item: Any) -> Conclusion:
|
|||
return Conclusion.from_api_response(ConclusionResponse.model_validate(item))
|
||||
|
||||
|
||||
def _get_conclusion(
|
||||
honcho: "Honcho",
|
||||
*,
|
||||
filters: dict[str, Any],
|
||||
) -> Conclusion:
|
||||
def _get_conclusion(honcho: "Honcho", conclusion_id: str) -> Conclusion:
|
||||
honcho._ensure_workspace()
|
||||
data = honcho._http.post(
|
||||
routes.conclusions_list(honcho.workspace_id),
|
||||
body={"filters": filters},
|
||||
query={"page": 1, "size": 1},
|
||||
)
|
||||
items = data.get("items", [])
|
||||
if not items:
|
||||
data = honcho._http.get(routes.conclusion(honcho.workspace_id, conclusion_id))
|
||||
return _conclusion_from_item(data)
|
||||
|
||||
|
||||
def _require_scope(
|
||||
conclusion: Conclusion, observer_id: str, observed_id: str
|
||||
) -> Conclusion:
|
||||
if conclusion.observer_id != observer_id or conclusion.observed_id != observed_id:
|
||||
raise NotFoundError("Conclusion not found")
|
||||
return _conclusion_from_item(items[0])
|
||||
return conclusion
|
||||
|
||||
|
||||
def _get_many_conclusions(
|
||||
|
|
@ -275,7 +272,7 @@ class WorkspaceConclusions:
|
|||
|
||||
def get(self, conclusion_id: str) -> Conclusion:
|
||||
"""Get a single conclusion by ID, anywhere in the workspace."""
|
||||
return _get_conclusion(self._honcho, filters={"id": conclusion_id})
|
||||
return _get_conclusion(self._honcho, conclusion_id)
|
||||
|
||||
def get_many(self, conclusion_ids: list[str]) -> list[Conclusion]:
|
||||
"""Get multiple conclusions by ID. Missing IDs are omitted."""
|
||||
|
|
@ -453,8 +450,6 @@ 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
|
||||
|
||||
|
|
@ -467,13 +462,10 @@ class ConclusionScope:
|
|||
observer/observed pair. Use ``honcho.conclusions.get`` for a
|
||||
workspace-wide lookup.
|
||||
"""
|
||||
return _get_conclusion(
|
||||
self._honcho,
|
||||
filters={
|
||||
"id": conclusion_id,
|
||||
"observer_id": self.observer,
|
||||
"observed_id": self.observed,
|
||||
},
|
||||
return _require_scope(
|
||||
_get_conclusion(self._honcho, conclusion_id),
|
||||
self.observer,
|
||||
self.observed,
|
||||
)
|
||||
|
||||
def get_many(self, conclusion_ids: list[str]) -> list[Conclusion]:
|
||||
|
|
|
|||
|
|
@ -335,10 +335,10 @@ describe('Conclusions', () => {
|
|||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Single Conclusion Retrieval (list with an id filter)
|
||||
// Single Conclusion Retrieval
|
||||
// ===========================================================================
|
||||
|
||||
describe('get (list with id filter)', () => {
|
||||
describe('get', () => {
|
||||
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: {} })
|
||||
|
|
|
|||
|
|
@ -211,18 +211,14 @@ export class ConclusionScope {
|
|||
}
|
||||
|
||||
private async _get(conclusionId: string): Promise<ConclusionResponse> {
|
||||
// Equivalent to list with an `id` filter, restricted to this pair.
|
||||
const response = await this._list({
|
||||
filters: {
|
||||
id: conclusionId,
|
||||
observer_id: this.observer,
|
||||
observed_id: this.observed,
|
||||
},
|
||||
page: 1,
|
||||
size: 1,
|
||||
})
|
||||
const item = response.items?.[0]
|
||||
if (!item) {
|
||||
await this._ensureWorkspace()
|
||||
const item = await this._http.get<ConclusionResponse>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/conclusions/${conclusionId}`
|
||||
)
|
||||
if (
|
||||
item.observer_id !== this.observer ||
|
||||
item.observed_id !== this.observed
|
||||
) {
|
||||
throw new NotFoundError('Conclusion not found')
|
||||
}
|
||||
return item
|
||||
|
|
@ -607,15 +603,10 @@ export class WorkspaceConclusions {
|
|||
* Get a single conclusion by ID, anywhere in the workspace.
|
||||
*/
|
||||
async get(conclusionId: string): Promise<Conclusion> {
|
||||
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')
|
||||
}
|
||||
await this._ensureWorkspace()
|
||||
const item = await this._http.get<ConclusionResponse>(
|
||||
`/${API_VERSION}/workspaces/${this.workspaceId}/conclusions/${conclusionId}`
|
||||
)
|
||||
return Conclusion.fromApiResponse(item)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,26 @@ 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,14 +667,13 @@ class TestConclusionRoutes:
|
|||
assert "not found" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conclusion_by_id_success(
|
||||
async def test_get_conclusion_success(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test getting a single conclusion via list with an id filter,
|
||||
including attribution fields"""
|
||||
"""Test getting a single conclusion by ID with attribution fields"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
|
|
@ -720,16 +719,13 @@ class TestConclusionRoutes:
|
|||
db_session.add(derived)
|
||||
await db_session.commit()
|
||||
|
||||
# Get conclusion via list with an id filter
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": derived.id}},
|
||||
# Get conclusion by ID
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/{derived.id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
items = response.json()["items"]
|
||||
assert len(items) == 1
|
||||
conclusion = items[0]
|
||||
conclusion = response.json()
|
||||
assert conclusion["id"] == derived.id
|
||||
assert conclusion["content"] == "User is likely a night owl"
|
||||
assert conclusion["observer_id"] == test_peer.name
|
||||
|
|
@ -743,7 +739,7 @@ class TestConclusionRoutes:
|
|||
assert "internal_metadata" not in conclusion
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conclusion_by_id_legacy_source_ids(
|
||||
async def test_get_conclusion_legacy_source_ids(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
|
|
@ -779,41 +775,39 @@ class TestConclusionRoutes:
|
|||
db_session.add(doc)
|
||||
await db_session.commit()
|
||||
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": doc.id}},
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/{doc.id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
items = response.json()["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["source_ids"] is None
|
||||
conclusion = response.json()
|
||||
assert conclusion["source_ids"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conclusion_by_id_not_found(
|
||||
async def test_get_conclusion_not_found(
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Listing with a non-existent id filter yields an empty page"""
|
||||
"""Test getting a non-existent conclusion"""
|
||||
test_workspace, _test_peer = sample_data
|
||||
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": "nonexistent_id"}},
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/nonexistent_id"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["items"] == []
|
||||
assert response.status_code == 404
|
||||
data = response.json()
|
||||
assert "not found" in data["detail"].lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conclusion_by_id_soft_deleted(
|
||||
async def test_get_conclusion_soft_deleted(
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that a soft-deleted conclusion is excluded from list results"""
|
||||
"""Test that a soft-deleted conclusion returns 404"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create another peer
|
||||
|
|
@ -843,12 +837,10 @@ class TestConclusionRoutes:
|
|||
)
|
||||
assert delete_response.status_code == 204
|
||||
|
||||
response = client.post(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/list",
|
||||
json={"filters": {"id": doc.id}},
|
||||
response = client.get(
|
||||
f"/v3/workspaces/{test_workspace.name}/conclusions/{doc.id}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["items"] == []
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_conclusions_includes_attribution_fields(
|
||||
|
|
|
|||
Loading…
Reference in New Issue