diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 27d2fbb..078b9e2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.7.0" + ".": "3.8.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ae3bc64..19b887f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard%2Fscorecard-01e01e74f4fc1b108f0446c197c6d187bb79560234a33671ea2b3f33eb52954a.yml -openapi_spec_hash: f34c136d551da1fcce69b811557c48a2 -config_hash: 0b3b3dc41414f1a27e10168e44aeccd9 +configured_endpoints: 32 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard%2Fscorecard-fa56fcaa5a33929c01761841ded012553aee4881c47b78329c5f2007f64a3817.yml +openapi_spec_hash: 5c630f7b73df78a0fe9c0cd984f73c53 +config_hash: 68b2970b9abfab636bc4fe68812940b5 diff --git a/CHANGELOG.md b/CHANGELOG.md index f9220f1..b257a64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.8.0 (2026-03-10) + +Full Changelog: [v3.7.0...v3.8.0](https://github.com/scorecard-ai/scorecard-python/compare/v3.7.0...v3.8.0) + +### Features + +* **api:** api update ([4d66da2](https://github.com/scorecard-ai/scorecard-python/commit/4d66da28832c634c8dc3fb0c78a1bb101db0f62b)) + ## 3.7.0 (2026-03-10) Full Changelog: [v3.6.0...v3.7.0](https://github.com/scorecard-ai/scorecard-python/compare/v3.6.0...v3.7.0) diff --git a/api.md b/api.md index f61ff3d..e4eca22 100644 --- a/api.md +++ b/api.md @@ -89,9 +89,21 @@ from scorecard_ai.types import Record, RecordListResponse, RecordDeleteResponse Methods: -- client.records.create(run_id, \*\*params) -> Record -- client.records.list(run_id, \*\*params) -> SyncPaginatedResponse[RecordListResponse] -- client.records.delete(record_id) -> RecordDeleteResponse +- client.records.create(run_id, \*\*params) -> Record +- client.records.list(run_id, \*\*params) -> SyncPaginatedResponse[RecordListResponse] +- client.records.delete(record_id) -> RecordDeleteResponse + +## Annotations + +Types: + +```python +from scorecard_ai.types.records import Annotation, AnnotationListResponse +``` + +Methods: + +- client.records.annotations.list(record_id) -> AnnotationListResponse # Scores diff --git a/pyproject.toml b/pyproject.toml index 56606b0..cfc1ba3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scorecard-ai" -version = "3.7.0" +version = "3.8.0" description = "The official Python library for the Scorecard API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/scorecard_ai/_client.py b/src/scorecard_ai/_client.py index b31f51b..f6ae5bc 100644 --- a/src/scorecard_ai/_client.py +++ b/src/scorecard_ai/_client.py @@ -35,10 +35,10 @@ from .resources.runs import RunsResource, AsyncRunsResource from .resources.scores import ScoresResource, AsyncScoresResource from .resources.metrics import MetricsResource, AsyncMetricsResource - from .resources.records import RecordsResource, AsyncRecordsResource from .resources.projects import ProjectsResource, AsyncProjectsResource from .resources.testsets import TestsetsResource, AsyncTestsetsResource from .resources.testcases import TestcasesResource, AsyncTestcasesResource + from .resources.records.records import RecordsResource, AsyncRecordsResource from .resources.systems.systems import SystemsResource, AsyncSystemsResource __all__ = [ diff --git a/src/scorecard_ai/_version.py b/src/scorecard_ai/_version.py index 5a079b1..79a90de 100644 --- a/src/scorecard_ai/_version.py +++ b/src/scorecard_ai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "scorecard_ai" -__version__ = "3.7.0" # x-release-please-version +__version__ = "3.8.0" # x-release-please-version diff --git a/src/scorecard_ai/resources/records/__init__.py b/src/scorecard_ai/resources/records/__init__.py new file mode 100644 index 0000000..039de4b --- /dev/null +++ b/src/scorecard_ai/resources/records/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .records import ( + RecordsResource, + AsyncRecordsResource, + RecordsResourceWithRawResponse, + AsyncRecordsResourceWithRawResponse, + RecordsResourceWithStreamingResponse, + AsyncRecordsResourceWithStreamingResponse, +) +from .annotations import ( + AnnotationsResource, + AsyncAnnotationsResource, + AnnotationsResourceWithRawResponse, + AsyncAnnotationsResourceWithRawResponse, + AnnotationsResourceWithStreamingResponse, + AsyncAnnotationsResourceWithStreamingResponse, +) + +__all__ = [ + "AnnotationsResource", + "AsyncAnnotationsResource", + "AnnotationsResourceWithRawResponse", + "AsyncAnnotationsResourceWithRawResponse", + "AnnotationsResourceWithStreamingResponse", + "AsyncAnnotationsResourceWithStreamingResponse", + "RecordsResource", + "AsyncRecordsResource", + "RecordsResourceWithRawResponse", + "AsyncRecordsResourceWithRawResponse", + "RecordsResourceWithStreamingResponse", + "AsyncRecordsResourceWithStreamingResponse", +] diff --git a/src/scorecard_ai/resources/records/annotations.py b/src/scorecard_ai/resources/records/annotations.py new file mode 100644 index 0000000..5d9d735 --- /dev/null +++ b/src/scorecard_ai/resources/records/annotations.py @@ -0,0 +1,163 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.records.annotation_list_response import AnnotationListResponse + +__all__ = ["AnnotationsResource", "AsyncAnnotationsResource"] + + +class AnnotationsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AnnotationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/scorecard-ai/scorecard-python#accessing-raw-response-data-eg-headers + """ + return AnnotationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AnnotationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/scorecard-ai/scorecard-python#with_streaming_response + """ + return AnnotationsResourceWithStreamingResponse(self) + + def list( + self, + record_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AnnotationListResponse: + """ + List all annotations (ratings and comments) for a specific Record. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not record_id: + raise ValueError(f"Expected a non-empty value for `record_id` but received {record_id!r}") + return self._get( + f"/records/{record_id}/annotations", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AnnotationListResponse, + ) + + +class AsyncAnnotationsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAnnotationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/scorecard-ai/scorecard-python#accessing-raw-response-data-eg-headers + """ + return AsyncAnnotationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAnnotationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/scorecard-ai/scorecard-python#with_streaming_response + """ + return AsyncAnnotationsResourceWithStreamingResponse(self) + + async def list( + self, + record_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AnnotationListResponse: + """ + List all annotations (ratings and comments) for a specific Record. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not record_id: + raise ValueError(f"Expected a non-empty value for `record_id` but received {record_id!r}") + return await self._get( + f"/records/{record_id}/annotations", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AnnotationListResponse, + ) + + +class AnnotationsResourceWithRawResponse: + def __init__(self, annotations: AnnotationsResource) -> None: + self._annotations = annotations + + self.list = to_raw_response_wrapper( + annotations.list, + ) + + +class AsyncAnnotationsResourceWithRawResponse: + def __init__(self, annotations: AsyncAnnotationsResource) -> None: + self._annotations = annotations + + self.list = async_to_raw_response_wrapper( + annotations.list, + ) + + +class AnnotationsResourceWithStreamingResponse: + def __init__(self, annotations: AnnotationsResource) -> None: + self._annotations = annotations + + self.list = to_streamed_response_wrapper( + annotations.list, + ) + + +class AsyncAnnotationsResourceWithStreamingResponse: + def __init__(self, annotations: AsyncAnnotationsResource) -> None: + self._annotations = annotations + + self.list = async_to_streamed_response_wrapper( + annotations.list, + ) diff --git a/src/scorecard_ai/resources/records.py b/src/scorecard_ai/resources/records/records.py similarity index 89% rename from src/scorecard_ai/resources/records.py rename to src/scorecard_ai/resources/records/records.py index cae0760..b55ad72 100644 --- a/src/scorecard_ai/resources/records.py +++ b/src/scorecard_ai/resources/records/records.py @@ -6,27 +6,39 @@ import httpx -from ..types import record_list_params, record_create_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( +from ...types import record_list_params, record_create_params +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from ..pagination import SyncPaginatedResponse, AsyncPaginatedResponse -from .._base_client import AsyncPaginator, make_request_options -from ..types.record import Record -from ..types.record_list_response import RecordListResponse -from ..types.record_delete_response import RecordDeleteResponse +from .annotations import ( + AnnotationsResource, + AsyncAnnotationsResource, + AnnotationsResourceWithRawResponse, + AsyncAnnotationsResourceWithRawResponse, + AnnotationsResourceWithStreamingResponse, + AsyncAnnotationsResourceWithStreamingResponse, +) +from ...pagination import SyncPaginatedResponse, AsyncPaginatedResponse +from ..._base_client import AsyncPaginator, make_request_options +from ...types.record import Record +from ...types.record_list_response import RecordListResponse +from ...types.record_delete_response import RecordDeleteResponse __all__ = ["RecordsResource", "AsyncRecordsResource"] class RecordsResource(SyncAPIResource): + @cached_property + def annotations(self) -> AnnotationsResource: + return AnnotationsResource(self._client) + @cached_property def with_raw_response(self) -> RecordsResourceWithRawResponse: """ @@ -194,6 +206,10 @@ def delete( class AsyncRecordsResource(AsyncAPIResource): + @cached_property + def annotations(self) -> AsyncAnnotationsResource: + return AsyncAnnotationsResource(self._client) + @cached_property def with_raw_response(self) -> AsyncRecordsResourceWithRawResponse: """ @@ -374,6 +390,10 @@ def __init__(self, records: RecordsResource) -> None: records.delete, ) + @cached_property + def annotations(self) -> AnnotationsResourceWithRawResponse: + return AnnotationsResourceWithRawResponse(self._records.annotations) + class AsyncRecordsResourceWithRawResponse: def __init__(self, records: AsyncRecordsResource) -> None: @@ -389,6 +409,10 @@ def __init__(self, records: AsyncRecordsResource) -> None: records.delete, ) + @cached_property + def annotations(self) -> AsyncAnnotationsResourceWithRawResponse: + return AsyncAnnotationsResourceWithRawResponse(self._records.annotations) + class RecordsResourceWithStreamingResponse: def __init__(self, records: RecordsResource) -> None: @@ -404,6 +428,10 @@ def __init__(self, records: RecordsResource) -> None: records.delete, ) + @cached_property + def annotations(self) -> AnnotationsResourceWithStreamingResponse: + return AnnotationsResourceWithStreamingResponse(self._records.annotations) + class AsyncRecordsResourceWithStreamingResponse: def __init__(self, records: AsyncRecordsResource) -> None: @@ -418,3 +446,7 @@ def __init__(self, records: AsyncRecordsResource) -> None: self.delete = async_to_streamed_response_wrapper( records.delete, ) + + @cached_property + def annotations(self) -> AsyncAnnotationsResourceWithStreamingResponse: + return AsyncAnnotationsResourceWithStreamingResponse(self._records.annotations) diff --git a/src/scorecard_ai/types/records/__init__.py b/src/scorecard_ai/types/records/__init__.py new file mode 100644 index 0000000..1038f63 --- /dev/null +++ b/src/scorecard_ai/types/records/__init__.py @@ -0,0 +1,6 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .annotation import Annotation as Annotation +from .annotation_list_response import AnnotationListResponse as AnnotationListResponse diff --git a/src/scorecard_ai/types/records/annotation.py b/src/scorecard_ai/types/records/annotation.py new file mode 100644 index 0000000..1d1ad5e --- /dev/null +++ b/src/scorecard_ai/types/records/annotation.py @@ -0,0 +1,39 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["Annotation"] + + +class Annotation(BaseModel): + """ + An annotation on a Record, containing a rating (thumbs up/down) and/or a text comment. + """ + + id: str + """The ID of the Annotation.""" + + comment: Optional[str] = None + """An optional text comment for the annotation.""" + + created_at: str = FieldInfo(alias="createdAt") + """The ISO 8601 timestamp when the annotation was created.""" + + rating: Optional[bool] = None + """ + The rating of the annotation: true (positive), false (negative), or null (no + rating). + """ + + record_id: str = FieldInfo(alias="recordId") + """The ID of the Record this Annotation belongs to.""" + + span_id: Optional[str] = FieldInfo(alias="spanId", default=None) + """Optional span ID linking this annotation to a specific span.""" + + user_id: str = FieldInfo(alias="userId") + """The ID of the user who created the annotation.""" diff --git a/src/scorecard_ai/types/records/annotation_list_response.py b/src/scorecard_ai/types/records/annotation_list_response.py new file mode 100644 index 0000000..7f4ddb0 --- /dev/null +++ b/src/scorecard_ai/types/records/annotation_list_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from ..._models import BaseModel +from .annotation import Annotation + +__all__ = ["AnnotationListResponse"] + + +class AnnotationListResponse(BaseModel): + data: List[Annotation] diff --git a/tests/api_resources/records/__init__.py b/tests/api_resources/records/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/records/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/records/test_annotations.py b/tests/api_resources/records/test_annotations.py new file mode 100644 index 0000000..a726c66 --- /dev/null +++ b/tests/api_resources/records/test_annotations.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from scorecard_ai import Scorecard, AsyncScorecard +from scorecard_ai.types.records import AnnotationListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAnnotations: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: Scorecard) -> None: + annotation = client.records.annotations.list( + "777", + ) + assert_matches_type(AnnotationListResponse, annotation, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Scorecard) -> None: + response = client.records.annotations.with_raw_response.list( + "777", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + annotation = response.parse() + assert_matches_type(AnnotationListResponse, annotation, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Scorecard) -> None: + with client.records.annotations.with_streaming_response.list( + "777", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + annotation = response.parse() + assert_matches_type(AnnotationListResponse, annotation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_list(self, client: Scorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `record_id` but received ''"): + client.records.annotations.with_raw_response.list( + "", + ) + + +class TestAsyncAnnotations: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_list(self, async_client: AsyncScorecard) -> None: + annotation = await async_client.records.annotations.list( + "777", + ) + assert_matches_type(AnnotationListResponse, annotation, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncScorecard) -> None: + response = await async_client.records.annotations.with_raw_response.list( + "777", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + annotation = await response.parse() + assert_matches_type(AnnotationListResponse, annotation, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncScorecard) -> None: + async with async_client.records.annotations.with_streaming_response.list( + "777", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + annotation = await response.parse() + assert_matches_type(AnnotationListResponse, annotation, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_list(self, async_client: AsyncScorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `record_id` but received ''"): + await async_client.records.annotations.with_raw_response.list( + "", + )