diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c40f3f2..f9f0b18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/scorecard-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -41,7 +41,7 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/scorecard-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -78,7 +78,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/scorecard-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 078b9e2..9127b1b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.8.0" + ".": "3.9.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 449029e..4193920 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 35 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-1ba77f87704707ae747bfd7ec95fdca5ea98bdab1c3c710353cb79eace286f78.yml -openapi_spec_hash: 7ce6bc5172f5678986b7c4d43ebd5b20 -config_hash: 31cceec463f09f560813ae649da927ab +configured_endpoints: 43 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-7c659d8a67917fbf3a8dc42fa925b55c27fcdddc1e9b51a5f807457152e0a581.yml +openapi_spec_hash: b86fa1ce6fa993b561d492aeec411e73 +config_hash: 5c97d607897cf4a982832a87d0cd12fb diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c49c33..170b0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 3.9.0 (2026-07-31) + +Full Changelog: [v3.8.0...v3.9.0](https://github.com/scorecard-ai/scorecard-python/compare/v3.8.0...v3.9.0) + +### Features + +* **api:** api update ([f7092fc](https://github.com/scorecard-ai/scorecard-python/commit/f7092fc7c61beae93e76b3b8f3136e691470c66e)) +* **api:** api update ([99eb116](https://github.com/scorecard-ai/scorecard-python/commit/99eb1167ba8c4c95666299e729334ae6ee478fb9)) +* **api:** api update ([ca4c4b5](https://github.com/scorecard-ai/scorecard-python/commit/ca4c4b526792ccbb10a9bd9a897dbcbddfc4a022)) +* **api:** api update ([b6d391b](https://github.com/scorecard-ai/scorecard-python/commit/b6d391be490a30e41617467dff18acfb3f337920)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([d0bba7c](https://github.com/scorecard-ai/scorecard-python/commit/d0bba7c44fa0bae6ee4cb576687ffcdbb7f02c46)) + + +### Bug Fixes + +* **internal:** resolve build failures ([86b5a52](https://github.com/scorecard-ai/scorecard-python/commit/86b5a5275d4e302a022c86548dcfb043748a81f0)) + ## 3.8.0 (2026-07-03) Full Changelog: [v3.7.0...v3.8.0](https://github.com/scorecard-ai/scorecard-python/compare/v3.7.0...v3.8.0) diff --git a/api.md b/api.md index 0761614..b9a6d85 100644 --- a/api.md +++ b/api.md @@ -119,6 +119,24 @@ Methods: - client.records.tags.list(record_id) -> TagListResponse - client.records.tags.delete(text, \*, record_id) -> TagDeleteResponse +## Assignees + +Types: + +```python +from scorecard_ai.types.records import ( + RecordAssignment, + AssigneeListResponse, + AssigneeDeleteResponse, +) +``` + +Methods: + +- client.records.assignees.create(record_id, \*\*params) -> RecordAssignment +- client.records.assignees.list(record_id) -> AssigneeListResponse +- client.records.assignees.delete(assignee_user_id, \*, record_id) -> AssigneeDeleteResponse + # Scores Types: @@ -131,6 +149,27 @@ Methods: - client.scores.upsert(metric_config_id, \*, record_id, \*\*params) -> Score +# Attachments + +Types: + +```python +from scorecard_ai.types import ( + Attachment, + AttachmentDeleteResponse, + AttachmentGetResponse, + AttachmentInitiateResponse, +) +``` + +Methods: + +- client.attachments.list(session_id, \*\*params) -> SyncPaginatedResponse[Attachment] +- client.attachments.delete(attachment_id) -> AttachmentDeleteResponse +- client.attachments.commit(attachment_id) -> Attachment +- client.attachments.get(attachment_id) -> AttachmentGetResponse +- client.attachments.initiate(\*\*params) -> AttachmentInitiateResponse + # Systems Types: diff --git a/pyproject.toml b/pyproject.toml index 7ee3031..c4fb133 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "scorecard-ai" -version = "3.8.0" +version = "3.9.0" description = "The official Python library for the Scorecard API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/scripts/lint b/scripts/lint index 2856f13..8e50ac5 100755 --- a/scripts/lint +++ b/scripts/lint @@ -13,7 +13,7 @@ else fi echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . diff --git a/src/scorecard_ai/_client.py b/src/scorecard_ai/_client.py index a3526b8..c9ff001 100644 --- a/src/scorecard_ai/_client.py +++ b/src/scorecard_ai/_client.py @@ -35,13 +35,14 @@ ) if TYPE_CHECKING: - from .resources import runs, scores, metrics, records, systems, projects, testsets, testcases + from .resources import runs, scores, metrics, records, systems, projects, testsets, testcases, attachments from .resources.runs import RunsResource, AsyncRunsResource from .resources.scores import ScoresResource, AsyncScoresResource from .resources.metrics import MetricsResource, AsyncMetricsResource from .resources.projects import ProjectsResource, AsyncProjectsResource from .resources.testsets import TestsetsResource, AsyncTestsetsResource from .resources.testcases import TestcasesResource, AsyncTestcasesResource + from .resources.attachments import AttachmentsResource, AsyncAttachmentsResource from .resources.records.records import RecordsResource, AsyncRecordsResource from .resources.systems.systems import SystemsResource, AsyncSystemsResource @@ -215,6 +216,12 @@ def scores(self) -> ScoresResource: return ScoresResource(self) + @cached_property + def attachments(self) -> AttachmentsResource: + from .resources.attachments import AttachmentsResource + + return AttachmentsResource(self) + @cached_property def systems(self) -> SystemsResource: from .resources.systems import SystemsResource @@ -466,6 +473,12 @@ def scores(self) -> AsyncScoresResource: return AsyncScoresResource(self) + @cached_property + def attachments(self) -> AsyncAttachmentsResource: + from .resources.attachments import AsyncAttachmentsResource + + return AsyncAttachmentsResource(self) + @cached_property def systems(self) -> AsyncSystemsResource: from .resources.systems import AsyncSystemsResource @@ -635,6 +648,12 @@ def scores(self) -> scores.ScoresResourceWithRawResponse: return ScoresResourceWithRawResponse(self._client.scores) + @cached_property + def attachments(self) -> attachments.AttachmentsResourceWithRawResponse: + from .resources.attachments import AttachmentsResourceWithRawResponse + + return AttachmentsResourceWithRawResponse(self._client.attachments) + @cached_property def systems(self) -> systems.SystemsResourceWithRawResponse: from .resources.systems import SystemsResourceWithRawResponse @@ -690,6 +709,12 @@ def scores(self) -> scores.AsyncScoresResourceWithRawResponse: return AsyncScoresResourceWithRawResponse(self._client.scores) + @cached_property + def attachments(self) -> attachments.AsyncAttachmentsResourceWithRawResponse: + from .resources.attachments import AsyncAttachmentsResourceWithRawResponse + + return AsyncAttachmentsResourceWithRawResponse(self._client.attachments) + @cached_property def systems(self) -> systems.AsyncSystemsResourceWithRawResponse: from .resources.systems import AsyncSystemsResourceWithRawResponse @@ -745,6 +770,12 @@ def scores(self) -> scores.ScoresResourceWithStreamingResponse: return ScoresResourceWithStreamingResponse(self._client.scores) + @cached_property + def attachments(self) -> attachments.AttachmentsResourceWithStreamingResponse: + from .resources.attachments import AttachmentsResourceWithStreamingResponse + + return AttachmentsResourceWithStreamingResponse(self._client.attachments) + @cached_property def systems(self) -> systems.SystemsResourceWithStreamingResponse: from .resources.systems import SystemsResourceWithStreamingResponse @@ -800,6 +831,12 @@ def scores(self) -> scores.AsyncScoresResourceWithStreamingResponse: return AsyncScoresResourceWithStreamingResponse(self._client.scores) + @cached_property + def attachments(self) -> attachments.AsyncAttachmentsResourceWithStreamingResponse: + from .resources.attachments import AsyncAttachmentsResourceWithStreamingResponse + + return AsyncAttachmentsResourceWithStreamingResponse(self._client.attachments) + @cached_property def systems(self) -> systems.AsyncSystemsResourceWithStreamingResponse: from .resources.systems import AsyncSystemsResourceWithStreamingResponse diff --git a/src/scorecard_ai/_version.py b/src/scorecard_ai/_version.py index 79a90de..85f2735 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.8.0" # x-release-please-version +__version__ = "3.9.0" # x-release-please-version diff --git a/src/scorecard_ai/resources/__init__.py b/src/scorecard_ai/resources/__init__.py index a766f01..9333272 100644 --- a/src/scorecard_ai/resources/__init__.py +++ b/src/scorecard_ai/resources/__init__.py @@ -64,6 +64,14 @@ TestcasesResourceWithStreamingResponse, AsyncTestcasesResourceWithStreamingResponse, ) +from .attachments import ( + AttachmentsResource, + AsyncAttachmentsResource, + AttachmentsResourceWithRawResponse, + AsyncAttachmentsResourceWithRawResponse, + AttachmentsResourceWithStreamingResponse, + AsyncAttachmentsResourceWithStreamingResponse, +) __all__ = [ "ProjectsResource", @@ -108,6 +116,12 @@ "AsyncScoresResourceWithRawResponse", "ScoresResourceWithStreamingResponse", "AsyncScoresResourceWithStreamingResponse", + "AttachmentsResource", + "AsyncAttachmentsResource", + "AttachmentsResourceWithRawResponse", + "AsyncAttachmentsResourceWithRawResponse", + "AttachmentsResourceWithStreamingResponse", + "AsyncAttachmentsResourceWithStreamingResponse", "SystemsResource", "AsyncSystemsResource", "SystemsResourceWithRawResponse", diff --git a/src/scorecard_ai/resources/attachments.py b/src/scorecard_ai/resources/attachments.py new file mode 100644 index 0000000..a1970f6 --- /dev/null +++ b/src/scorecard_ai/resources/attachments.py @@ -0,0 +1,609 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict + +import httpx + +from ..types import attachment_list_params, attachment_initiate_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, 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.attachment import Attachment +from ..types.attachment_get_response import AttachmentGetResponse +from ..types.attachment_delete_response import AttachmentDeleteResponse +from ..types.attachment_initiate_response import AttachmentInitiateResponse + +__all__ = ["AttachmentsResource", "AsyncAttachmentsResource"] + + +class AttachmentsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AttachmentsResourceWithRawResponse: + """ + 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 AttachmentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AttachmentsResourceWithStreamingResponse: + """ + 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 AttachmentsResourceWithStreamingResponse(self) + + def list( + self, + session_id: str, + *, + cursor: str | Omit = omit, + limit: int | Omit = omit, + # 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, + ) -> SyncPaginatedResponse[Attachment]: + """Lists the uploaded attachments for a session. + + Only committed attachments are + returned. + + Args: + cursor: Cursor for pagination. Pass the `nextCursor` from the previous response to get + the next page of results. + + limit: Maximum number of items to return (1-100). Use with `cursor` for pagination + through large sets. + + 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 session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return self._get_api_list( + path_template("/sessions/{session_id}/attachments", session_id=session_id), + page=SyncPaginatedResponse[Attachment], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "cursor": cursor, + "limit": limit, + }, + attachment_list_params.AttachmentListParams, + ), + ), + model=Attachment, + ) + + def delete( + self, + attachment_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, + ) -> AttachmentDeleteResponse: + """ + Deletes an attachment: both the stored file and its metadata. + + 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 attachment_id: + raise ValueError(f"Expected a non-empty value for `attachment_id` but received {attachment_id!r}") + return self._delete( + path_template("/attachments/{attachment_id}", attachment_id=attachment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AttachmentDeleteResponse, + ) + + def commit( + self, + attachment_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, + ) -> Attachment: + """ + Finalizes an upload after the file bytes have been PUT to the signed upload URL. + Verifies the object landed in storage before the attachment starts describing + the new content. Committing an already-committed attachment is a no-op. + + 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 attachment_id: + raise ValueError(f"Expected a non-empty value for `attachment_id` but received {attachment_id!r}") + return self._post( + path_template("/attachments/{attachment_id}/commit", attachment_id=attachment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Attachment, + ) + + def get( + self, + attachment_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, + ) -> AttachmentGetResponse: + """ + Retrieves an attachment's metadata and a short-lived signed download URL for its + content. + + 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 attachment_id: + raise ValueError(f"Expected a non-empty value for `attachment_id` but received {attachment_id!r}") + return self._get( + path_template("/attachments/{attachment_id}", attachment_id=attachment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AttachmentGetResponse, + ) + + def initiate( + self, + *, + content_type: str, + file_path: str, + session_id: str, + sha256: str, + size_bytes: int, + filename: str | Omit = omit, + metadata: Dict[str, object] | Omit = omit, + # 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, + ) -> AttachmentInitiateResponse: + """Initiates (or deduplicates) an upload of a file attached to a session. + + If the + exact content is already stored for this (session ID, file path), the response + has `alreadyExists: true` and no upload is needed. Otherwise, PUT the file bytes + to the returned `uploadUrl`, then call the commit endpoint. Re-initiating an + existing (session ID, file path) with new content updates the attachment in + place on commit. + + Args: + content_type: MIME type of the file. + + file_path: The logical file path of the attachment (e.g. the path the agent wrote on disk). + Together with the session ID it identifies the attachment: re-uploading the same + path in the same session updates the existing attachment in place. + + session_id: The session ID the attachment belongs to. Matches the `session.id` emitted on + OTel spans, which is how attachments are joined to traces and records. + + sha256: Lowercase hex SHA-256 of the file content. + + size_bytes: Size of the file in bytes. + + filename: Display filename. Defaults to none. + + metadata: Arbitrary metadata to store with the attachment. + + 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 + """ + return self._post( + "/attachments", + body=maybe_transform( + { + "content_type": content_type, + "file_path": file_path, + "session_id": session_id, + "sha256": sha256, + "size_bytes": size_bytes, + "filename": filename, + "metadata": metadata, + }, + attachment_initiate_params.AttachmentInitiateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AttachmentInitiateResponse, + ) + + +class AsyncAttachmentsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAttachmentsResourceWithRawResponse: + """ + 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 AsyncAttachmentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAttachmentsResourceWithStreamingResponse: + """ + 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 AsyncAttachmentsResourceWithStreamingResponse(self) + + def list( + self, + session_id: str, + *, + cursor: str | Omit = omit, + limit: int | Omit = omit, + # 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, + ) -> AsyncPaginator[Attachment, AsyncPaginatedResponse[Attachment]]: + """Lists the uploaded attachments for a session. + + Only committed attachments are + returned. + + Args: + cursor: Cursor for pagination. Pass the `nextCursor` from the previous response to get + the next page of results. + + limit: Maximum number of items to return (1-100). Use with `cursor` for pagination + through large sets. + + 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 session_id: + raise ValueError(f"Expected a non-empty value for `session_id` but received {session_id!r}") + return self._get_api_list( + path_template("/sessions/{session_id}/attachments", session_id=session_id), + page=AsyncPaginatedResponse[Attachment], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "cursor": cursor, + "limit": limit, + }, + attachment_list_params.AttachmentListParams, + ), + ), + model=Attachment, + ) + + async def delete( + self, + attachment_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, + ) -> AttachmentDeleteResponse: + """ + Deletes an attachment: both the stored file and its metadata. + + 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 attachment_id: + raise ValueError(f"Expected a non-empty value for `attachment_id` but received {attachment_id!r}") + return await self._delete( + path_template("/attachments/{attachment_id}", attachment_id=attachment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AttachmentDeleteResponse, + ) + + async def commit( + self, + attachment_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, + ) -> Attachment: + """ + Finalizes an upload after the file bytes have been PUT to the signed upload URL. + Verifies the object landed in storage before the attachment starts describing + the new content. Committing an already-committed attachment is a no-op. + + 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 attachment_id: + raise ValueError(f"Expected a non-empty value for `attachment_id` but received {attachment_id!r}") + return await self._post( + path_template("/attachments/{attachment_id}/commit", attachment_id=attachment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Attachment, + ) + + async def get( + self, + attachment_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, + ) -> AttachmentGetResponse: + """ + Retrieves an attachment's metadata and a short-lived signed download URL for its + content. + + 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 attachment_id: + raise ValueError(f"Expected a non-empty value for `attachment_id` but received {attachment_id!r}") + return await self._get( + path_template("/attachments/{attachment_id}", attachment_id=attachment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AttachmentGetResponse, + ) + + async def initiate( + self, + *, + content_type: str, + file_path: str, + session_id: str, + sha256: str, + size_bytes: int, + filename: str | Omit = omit, + metadata: Dict[str, object] | Omit = omit, + # 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, + ) -> AttachmentInitiateResponse: + """Initiates (or deduplicates) an upload of a file attached to a session. + + If the + exact content is already stored for this (session ID, file path), the response + has `alreadyExists: true` and no upload is needed. Otherwise, PUT the file bytes + to the returned `uploadUrl`, then call the commit endpoint. Re-initiating an + existing (session ID, file path) with new content updates the attachment in + place on commit. + + Args: + content_type: MIME type of the file. + + file_path: The logical file path of the attachment (e.g. the path the agent wrote on disk). + Together with the session ID it identifies the attachment: re-uploading the same + path in the same session updates the existing attachment in place. + + session_id: The session ID the attachment belongs to. Matches the `session.id` emitted on + OTel spans, which is how attachments are joined to traces and records. + + sha256: Lowercase hex SHA-256 of the file content. + + size_bytes: Size of the file in bytes. + + filename: Display filename. Defaults to none. + + metadata: Arbitrary metadata to store with the attachment. + + 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 + """ + return await self._post( + "/attachments", + body=await async_maybe_transform( + { + "content_type": content_type, + "file_path": file_path, + "session_id": session_id, + "sha256": sha256, + "size_bytes": size_bytes, + "filename": filename, + "metadata": metadata, + }, + attachment_initiate_params.AttachmentInitiateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AttachmentInitiateResponse, + ) + + +class AttachmentsResourceWithRawResponse: + def __init__(self, attachments: AttachmentsResource) -> None: + self._attachments = attachments + + self.list = to_raw_response_wrapper( + attachments.list, + ) + self.delete = to_raw_response_wrapper( + attachments.delete, + ) + self.commit = to_raw_response_wrapper( + attachments.commit, + ) + self.get = to_raw_response_wrapper( + attachments.get, + ) + self.initiate = to_raw_response_wrapper( + attachments.initiate, + ) + + +class AsyncAttachmentsResourceWithRawResponse: + def __init__(self, attachments: AsyncAttachmentsResource) -> None: + self._attachments = attachments + + self.list = async_to_raw_response_wrapper( + attachments.list, + ) + self.delete = async_to_raw_response_wrapper( + attachments.delete, + ) + self.commit = async_to_raw_response_wrapper( + attachments.commit, + ) + self.get = async_to_raw_response_wrapper( + attachments.get, + ) + self.initiate = async_to_raw_response_wrapper( + attachments.initiate, + ) + + +class AttachmentsResourceWithStreamingResponse: + def __init__(self, attachments: AttachmentsResource) -> None: + self._attachments = attachments + + self.list = to_streamed_response_wrapper( + attachments.list, + ) + self.delete = to_streamed_response_wrapper( + attachments.delete, + ) + self.commit = to_streamed_response_wrapper( + attachments.commit, + ) + self.get = to_streamed_response_wrapper( + attachments.get, + ) + self.initiate = to_streamed_response_wrapper( + attachments.initiate, + ) + + +class AsyncAttachmentsResourceWithStreamingResponse: + def __init__(self, attachments: AsyncAttachmentsResource) -> None: + self._attachments = attachments + + self.list = async_to_streamed_response_wrapper( + attachments.list, + ) + self.delete = async_to_streamed_response_wrapper( + attachments.delete, + ) + self.commit = async_to_streamed_response_wrapper( + attachments.commit, + ) + self.get = async_to_streamed_response_wrapper( + attachments.get, + ) + self.initiate = async_to_streamed_response_wrapper( + attachments.initiate, + ) diff --git a/src/scorecard_ai/resources/metrics.py b/src/scorecard_ai/resources/metrics.py index 8dc21da..f4b34c5 100644 --- a/src/scorecard_ai/resources/metrics.py +++ b/src/scorecard_ai/resources/metrics.py @@ -90,7 +90,8 @@ def create( passing_threshold: The threshold for determining pass/fail from integer scores (1-5). - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -238,7 +239,8 @@ def create( passing_threshold: Threshold for determining pass/fail from float scores (0.0-1.0). - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -383,7 +385,8 @@ def create( guidelines: Guidelines for AI evaluation on how to score the metric. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -574,7 +577,8 @@ def update( prompt_template: The complete prompt template for AI evaluation. Should include placeholders for dynamic content. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -725,7 +729,8 @@ def update( prompt_template: The complete prompt template for AI evaluation. Should include placeholders for dynamic content. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -873,7 +878,8 @@ def update( prompt_template: The complete prompt template for AI evaluation. Should include placeholders for dynamic content. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -1211,7 +1217,8 @@ async def create( passing_threshold: The threshold for determining pass/fail from integer scores (1-5). - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -1359,7 +1366,8 @@ async def create( passing_threshold: Threshold for determining pass/fail from float scores (0.0-1.0). - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -1504,7 +1512,8 @@ async def create( guidelines: Guidelines for AI evaluation on how to score the metric. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -1695,7 +1704,8 @@ async def update( prompt_template: The complete prompt template for AI evaluation. Should include placeholders for dynamic content. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -1846,7 +1856,8 @@ async def update( prompt_template: The complete prompt template for AI evaluation. Should include placeholders for dynamic content. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers @@ -1994,7 +2005,8 @@ async def update( prompt_template: The complete prompt template for AI evaluation. Should include placeholders for dynamic content. - temperature: The temperature for AI evaluation (0-2). + temperature: The temperature for AI evaluation (0-2). Omit to use the model provider's + default. extra_headers: Send extra headers diff --git a/src/scorecard_ai/resources/records/__init__.py b/src/scorecard_ai/resources/records/__init__.py index 26c8c12..8bed81d 100644 --- a/src/scorecard_ai/resources/records/__init__.py +++ b/src/scorecard_ai/resources/records/__init__.py @@ -16,6 +16,14 @@ RecordsResourceWithStreamingResponse, AsyncRecordsResourceWithStreamingResponse, ) +from .assignees import ( + AssigneesResource, + AsyncAssigneesResource, + AssigneesResourceWithRawResponse, + AsyncAssigneesResourceWithRawResponse, + AssigneesResourceWithStreamingResponse, + AsyncAssigneesResourceWithStreamingResponse, +) from .annotations import ( AnnotationsResource, AsyncAnnotationsResource, @@ -38,6 +46,12 @@ "AsyncTagsResourceWithRawResponse", "TagsResourceWithStreamingResponse", "AsyncTagsResourceWithStreamingResponse", + "AssigneesResource", + "AsyncAssigneesResource", + "AssigneesResourceWithRawResponse", + "AsyncAssigneesResourceWithRawResponse", + "AssigneesResourceWithStreamingResponse", + "AsyncAssigneesResourceWithStreamingResponse", "RecordsResource", "AsyncRecordsResource", "RecordsResourceWithRawResponse", diff --git a/src/scorecard_ai/resources/records/assignees.py b/src/scorecard_ai/resources/records/assignees.py new file mode 100644 index 0000000..728bc1d --- /dev/null +++ b/src/scorecard_ai/resources/records/assignees.py @@ -0,0 +1,353 @@ +# 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 ..._utils import path_template, 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 ..._base_client import make_request_options +from ...types.records import assignee_create_params +from ...types.records.record_assignment import RecordAssignment +from ...types.records.assignee_list_response import AssigneeListResponse +from ...types.records.assignee_delete_response import AssigneeDeleteResponse + +__all__ = ["AssigneesResource", "AsyncAssigneesResource"] + + +class AssigneesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> AssigneesResourceWithRawResponse: + """ + 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 AssigneesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AssigneesResourceWithStreamingResponse: + """ + 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 AssigneesResourceWithStreamingResponse(self) + + def create( + self, + record_id: str, + *, + assignee_user_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, + ) -> RecordAssignment: + """Assign an organization member to a Record. + + Idempotent: re-assigning an existing + member returns the existing assignment. + + Args: + assignee_user_id: The ID of the organization member to assign. Idempotent: re-assigning an + existing member returns the existing assignment. + + 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._post( + path_template("/records/{record_id}/assignees", record_id=record_id), + body=maybe_transform({"assignee_user_id": assignee_user_id}, assignee_create_params.AssigneeCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RecordAssignment, + ) + + 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, + ) -> AssigneeListResponse: + """ + List the organization members assigned to a 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( + path_template("/records/{record_id}/assignees", record_id=record_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssigneeListResponse, + ) + + def delete( + self, + assignee_user_id: str, + *, + 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, + ) -> AssigneeDeleteResponse: + """ + Remove an assignee from a 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}") + if not assignee_user_id: + raise ValueError(f"Expected a non-empty value for `assignee_user_id` but received {assignee_user_id!r}") + return self._delete( + path_template( + "/records/{record_id}/assignees/{assignee_user_id}", + record_id=record_id, + assignee_user_id=assignee_user_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssigneeDeleteResponse, + ) + + +class AsyncAssigneesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncAssigneesResourceWithRawResponse: + """ + 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 AsyncAssigneesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAssigneesResourceWithStreamingResponse: + """ + 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 AsyncAssigneesResourceWithStreamingResponse(self) + + async def create( + self, + record_id: str, + *, + assignee_user_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, + ) -> RecordAssignment: + """Assign an organization member to a Record. + + Idempotent: re-assigning an existing + member returns the existing assignment. + + Args: + assignee_user_id: The ID of the organization member to assign. Idempotent: re-assigning an + existing member returns the existing assignment. + + 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._post( + path_template("/records/{record_id}/assignees", record_id=record_id), + body=await async_maybe_transform( + {"assignee_user_id": assignee_user_id}, assignee_create_params.AssigneeCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=RecordAssignment, + ) + + 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, + ) -> AssigneeListResponse: + """ + List the organization members assigned to a 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( + path_template("/records/{record_id}/assignees", record_id=record_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssigneeListResponse, + ) + + async def delete( + self, + assignee_user_id: str, + *, + 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, + ) -> AssigneeDeleteResponse: + """ + Remove an assignee from a 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}") + if not assignee_user_id: + raise ValueError(f"Expected a non-empty value for `assignee_user_id` but received {assignee_user_id!r}") + return await self._delete( + path_template( + "/records/{record_id}/assignees/{assignee_user_id}", + record_id=record_id, + assignee_user_id=assignee_user_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AssigneeDeleteResponse, + ) + + +class AssigneesResourceWithRawResponse: + def __init__(self, assignees: AssigneesResource) -> None: + self._assignees = assignees + + self.create = to_raw_response_wrapper( + assignees.create, + ) + self.list = to_raw_response_wrapper( + assignees.list, + ) + self.delete = to_raw_response_wrapper( + assignees.delete, + ) + + +class AsyncAssigneesResourceWithRawResponse: + def __init__(self, assignees: AsyncAssigneesResource) -> None: + self._assignees = assignees + + self.create = async_to_raw_response_wrapper( + assignees.create, + ) + self.list = async_to_raw_response_wrapper( + assignees.list, + ) + self.delete = async_to_raw_response_wrapper( + assignees.delete, + ) + + +class AssigneesResourceWithStreamingResponse: + def __init__(self, assignees: AssigneesResource) -> None: + self._assignees = assignees + + self.create = to_streamed_response_wrapper( + assignees.create, + ) + self.list = to_streamed_response_wrapper( + assignees.list, + ) + self.delete = to_streamed_response_wrapper( + assignees.delete, + ) + + +class AsyncAssigneesResourceWithStreamingResponse: + def __init__(self, assignees: AsyncAssigneesResource) -> None: + self._assignees = assignees + + self.create = async_to_streamed_response_wrapper( + assignees.create, + ) + self.list = async_to_streamed_response_wrapper( + assignees.list, + ) + self.delete = async_to_streamed_response_wrapper( + assignees.delete, + ) diff --git a/src/scorecard_ai/resources/records/records.py b/src/scorecard_ai/resources/records/records.py index 2afb2a1..8e08eaf 100644 --- a/src/scorecard_ai/resources/records/records.py +++ b/src/scorecard_ai/resources/records/records.py @@ -18,6 +18,14 @@ from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property +from .assignees import ( + AssigneesResource, + AsyncAssigneesResource, + AssigneesResourceWithRawResponse, + AsyncAssigneesResourceWithRawResponse, + AssigneesResourceWithStreamingResponse, + AsyncAssigneesResourceWithStreamingResponse, +) from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( to_raw_response_wrapper, @@ -51,6 +59,10 @@ def annotations(self) -> AnnotationsResource: def tags(self) -> TagsResource: return TagsResource(self._client) + @cached_property + def assignees(self) -> AssigneesResource: + return AssigneesResource(self._client) + @cached_property def with_raw_response(self) -> RecordsResourceWithRawResponse: """ @@ -78,6 +90,7 @@ def create( inputs: Dict[str, object], outputs: Dict[str, object], otel_link_id: str | Omit = omit, + session_id: str | Omit = omit, testcase_id: str | Omit = omit, # 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. @@ -100,6 +113,9 @@ def create( otel_link_id: Optional ID for linking this record with an OpenTelemetry trace. Used for deduplication. + session_id: Optional session ID for this record. Matches the `session.id` emitted on OTel + spans, joining the record to its session's traces and attachments. + testcase_id: The ID of the Testcase. extra_headers: Send extra headers @@ -120,6 +136,7 @@ def create( "inputs": inputs, "outputs": outputs, "otel_link_id": otel_link_id, + "session_id": session_id, "testcase_id": testcase_id, }, record_create_params.RecordCreateParams, @@ -231,6 +248,10 @@ def annotations(self) -> AsyncAnnotationsResource: def tags(self) -> AsyncTagsResource: return AsyncTagsResource(self._client) + @cached_property + def assignees(self) -> AsyncAssigneesResource: + return AsyncAssigneesResource(self._client) + @cached_property def with_raw_response(self) -> AsyncRecordsResourceWithRawResponse: """ @@ -258,6 +279,7 @@ async def create( inputs: Dict[str, object], outputs: Dict[str, object], otel_link_id: str | Omit = omit, + session_id: str | Omit = omit, testcase_id: str | Omit = omit, # 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. @@ -280,6 +302,9 @@ async def create( otel_link_id: Optional ID for linking this record with an OpenTelemetry trace. Used for deduplication. + session_id: Optional session ID for this record. Matches the `session.id` emitted on OTel + spans, joining the record to its session's traces and attachments. + testcase_id: The ID of the Testcase. extra_headers: Send extra headers @@ -300,6 +325,7 @@ async def create( "inputs": inputs, "outputs": outputs, "otel_link_id": otel_link_id, + "session_id": session_id, "testcase_id": testcase_id, }, record_create_params.RecordCreateParams, @@ -424,6 +450,10 @@ def annotations(self) -> AnnotationsResourceWithRawResponse: def tags(self) -> TagsResourceWithRawResponse: return TagsResourceWithRawResponse(self._records.tags) + @cached_property + def assignees(self) -> AssigneesResourceWithRawResponse: + return AssigneesResourceWithRawResponse(self._records.assignees) + class AsyncRecordsResourceWithRawResponse: def __init__(self, records: AsyncRecordsResource) -> None: @@ -447,6 +477,10 @@ def annotations(self) -> AsyncAnnotationsResourceWithRawResponse: def tags(self) -> AsyncTagsResourceWithRawResponse: return AsyncTagsResourceWithRawResponse(self._records.tags) + @cached_property + def assignees(self) -> AsyncAssigneesResourceWithRawResponse: + return AsyncAssigneesResourceWithRawResponse(self._records.assignees) + class RecordsResourceWithStreamingResponse: def __init__(self, records: RecordsResource) -> None: @@ -470,6 +504,10 @@ def annotations(self) -> AnnotationsResourceWithStreamingResponse: def tags(self) -> TagsResourceWithStreamingResponse: return TagsResourceWithStreamingResponse(self._records.tags) + @cached_property + def assignees(self) -> AssigneesResourceWithStreamingResponse: + return AssigneesResourceWithStreamingResponse(self._records.assignees) + class AsyncRecordsResourceWithStreamingResponse: def __init__(self, records: AsyncRecordsResource) -> None: @@ -492,3 +530,7 @@ def annotations(self) -> AsyncAnnotationsResourceWithStreamingResponse: @cached_property def tags(self) -> AsyncTagsResourceWithStreamingResponse: return AsyncTagsResourceWithStreamingResponse(self._records.tags) + + @cached_property + def assignees(self) -> AsyncAssigneesResourceWithStreamingResponse: + return AsyncAssigneesResourceWithStreamingResponse(self._records.assignees) diff --git a/src/scorecard_ai/types/__init__.py b/src/scorecard_ai/types/__init__.py index e7949cc..b155b35 100644 --- a/src/scorecard_ai/types/__init__.py +++ b/src/scorecard_ai/types/__init__.py @@ -11,6 +11,7 @@ from .project import Project as Project from .testset import Testset as Testset from .testcase import Testcase as Testcase +from .attachment import Attachment as Attachment from .run_list_params import RunListParams as RunListParams from .run_create_params import RunCreateParams as RunCreateParams from .metric_list_params import MetricListParams as MetricListParams @@ -29,12 +30,17 @@ from .project_create_params import ProjectCreateParams as ProjectCreateParams from .testset_create_params import TestsetCreateParams as TestsetCreateParams from .testset_update_params import TestsetUpdateParams as TestsetUpdateParams +from .attachment_list_params import AttachmentListParams as AttachmentListParams from .metric_delete_response import MetricDeleteResponse as MetricDeleteResponse from .record_delete_response import RecordDeleteResponse as RecordDeleteResponse from .system_delete_response import SystemDeleteResponse as SystemDeleteResponse from .testcase_create_params import TestcaseCreateParams as TestcaseCreateParams from .testcase_delete_params import TestcaseDeleteParams as TestcaseDeleteParams from .testcase_update_params import TestcaseUpdateParams as TestcaseUpdateParams +from .attachment_get_response import AttachmentGetResponse as AttachmentGetResponse from .testset_delete_response import TestsetDeleteResponse as TestsetDeleteResponse from .testcase_create_response import TestcaseCreateResponse as TestcaseCreateResponse from .testcase_delete_response import TestcaseDeleteResponse as TestcaseDeleteResponse +from .attachment_delete_response import AttachmentDeleteResponse as AttachmentDeleteResponse +from .attachment_initiate_params import AttachmentInitiateParams as AttachmentInitiateParams +from .attachment_initiate_response import AttachmentInitiateResponse as AttachmentInitiateResponse diff --git a/src/scorecard_ai/types/attachment.py b/src/scorecard_ai/types/attachment.py new file mode 100644 index 0000000..5bc2d8f --- /dev/null +++ b/src/scorecard_ai/types/attachment.py @@ -0,0 +1,59 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Dict, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["Attachment"] + + +class Attachment(BaseModel): + """A file attached to a session. + + Bytes live in object storage; this describes the last committed content. + """ + + id: str + """The ID of the Attachment.""" + + content_type: Optional[str] = FieldInfo(alias="contentType", default=None) + """MIME type of the last committed content. Null until the first commit.""" + + filename: Optional[str] = None + """Display filename, if provided.""" + + file_path: str = FieldInfo(alias="filePath") + """The logical file path of the attachment (e.g. + + the path the agent wrote on disk). Together with the session ID it identifies + the attachment: re-uploading the same path in the same session updates the + existing attachment in place. + """ + + metadata: Optional[Dict[str, object]] = None + """Arbitrary caller-supplied metadata.""" + + session_id: str = FieldInfo(alias="sessionId") + """The session ID the attachment belongs to. + + Matches the `session.id` emitted on OTel spans, which is how attachments are + joined to traces and records. + """ + + sha256: Optional[str] = None + """SHA-256 of the last committed content. Null until the first commit.""" + + size_bytes: Optional[int] = FieldInfo(alias="sizeBytes", default=None) + """Size in bytes of the last committed content. Null until the first commit.""" + + status: Literal["pending", "uploaded"] + """ + `uploaded` once a commit has succeeded; `pending` while an initiated upload has + not been committed yet. + """ + + uploaded_at: Optional[str] = FieldInfo(alias="uploadedAt", default=None) + """ISO 8601 timestamp of the last successful commit. Null until the first commit.""" diff --git a/src/scorecard_ai/types/attachment_delete_response.py b/src/scorecard_ai/types/attachment_delete_response.py new file mode 100644 index 0000000..d405374 --- /dev/null +++ b/src/scorecard_ai/types/attachment_delete_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["AttachmentDeleteResponse"] + + +class AttachmentDeleteResponse(BaseModel): + success: bool + """Whether the deletion was successful.""" diff --git a/src/scorecard_ai/types/attachment_get_response.py b/src/scorecard_ai/types/attachment_get_response.py new file mode 100644 index 0000000..893ffab --- /dev/null +++ b/src/scorecard_ai/types/attachment_get_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from .attachment import Attachment + +__all__ = ["AttachmentGetResponse"] + + +class AttachmentGetResponse(Attachment): + """A file attached to a session. + + Bytes live in object storage; this describes the last committed content. + """ + + download_expires_at: Optional[str] = FieldInfo(alias="downloadExpiresAt", default=None) + """ISO 8601 expiry of `downloadUrl`.""" + + download_url: Optional[str] = FieldInfo(alias="downloadUrl", default=None) + """Short-lived signed URL to download the file. + + Null while the attachment has no committed content. + """ diff --git a/src/scorecard_ai/types/attachment_initiate_params.py b/src/scorecard_ai/types/attachment_initiate_params.py new file mode 100644 index 0000000..0fcddce --- /dev/null +++ b/src/scorecard_ai/types/attachment_initiate_params.py @@ -0,0 +1,42 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Dict +from typing_extensions import Required, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["AttachmentInitiateParams"] + + +class AttachmentInitiateParams(TypedDict, total=False): + content_type: Required[Annotated[str, PropertyInfo(alias="contentType")]] + """MIME type of the file.""" + + file_path: Required[Annotated[str, PropertyInfo(alias="filePath")]] + """The logical file path of the attachment (e.g. + + the path the agent wrote on disk). Together with the session ID it identifies + the attachment: re-uploading the same path in the same session updates the + existing attachment in place. + """ + + session_id: Required[Annotated[str, PropertyInfo(alias="sessionId")]] + """The session ID the attachment belongs to. + + Matches the `session.id` emitted on OTel spans, which is how attachments are + joined to traces and records. + """ + + sha256: Required[str] + """Lowercase hex SHA-256 of the file content.""" + + size_bytes: Required[Annotated[int, PropertyInfo(alias="sizeBytes")]] + """Size of the file in bytes.""" + + filename: str + """Display filename. Defaults to none.""" + + metadata: Dict[str, object] + """Arbitrary metadata to store with the attachment.""" diff --git a/src/scorecard_ai/types/attachment_initiate_response.py b/src/scorecard_ai/types/attachment_initiate_response.py new file mode 100644 index 0000000..ee7d682 --- /dev/null +++ b/src/scorecard_ai/types/attachment_initiate_response.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["AttachmentInitiateResponse"] + + +class AttachmentInitiateResponse(BaseModel): + id: str + """The ID of the Attachment.""" + + already_exists: bool = FieldInfo(alias="alreadyExists") + """ + True if this exact content is already stored for this (session, file path) — no + upload is needed and no upload URL is returned. + """ + + expires_at: Optional[str] = FieldInfo(alias="expiresAt", default=None) + """ISO 8601 expiry of `uploadUrl`.""" + + upload_method: Optional[Literal["PUT"]] = FieldInfo(alias="uploadMethod", default=None) + """HTTP method to use with `uploadUrl`.""" + + upload_url: Optional[str] = FieldInfo(alias="uploadUrl", default=None) + """Signed URL to PUT the file bytes to. Null when `alreadyExists` is true.""" diff --git a/src/scorecard_ai/types/attachment_list_params.py b/src/scorecard_ai/types/attachment_list_params.py new file mode 100644 index 0000000..244e913 --- /dev/null +++ b/src/scorecard_ai/types/attachment_list_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["AttachmentListParams"] + + +class AttachmentListParams(TypedDict, total=False): + cursor: str + """Cursor for pagination. + + Pass the `nextCursor` from the previous response to get the next page of + results. + """ + + limit: int + """Maximum number of items to return (1-100). + + Use with `cursor` for pagination through large sets. + """ diff --git a/src/scorecard_ai/types/metric.py b/src/scorecard_ai/types/metric.py index 5bde069..2aed793 100644 --- a/src/scorecard_ai/types/metric.py +++ b/src/scorecard_ai/types/metric.py @@ -54,8 +54,11 @@ class AIIntMetric(BaseModel): Should include placeholders for dynamic content. """ - temperature: float - """The temperature for AI evaluation (0-2).""" + temperature: Optional[float] = None + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanIntMetric(BaseModel): @@ -141,8 +144,11 @@ class AIFloatMetric(BaseModel): Should include placeholders for dynamic content. """ - temperature: float - """The temperature for AI evaluation (0-2).""" + temperature: Optional[float] = None + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanFloatMetric(BaseModel): @@ -225,8 +231,11 @@ class AIBooleanMetric(BaseModel): Should include placeholders for dynamic content. """ - temperature: float - """The temperature for AI evaluation (0-2).""" + temperature: Optional[float] = None + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanBooleanMetric(BaseModel): diff --git a/src/scorecard_ai/types/metric_create_params.py b/src/scorecard_ai/types/metric_create_params.py index e8eaaa7..1cc3cf3 100644 --- a/src/scorecard_ai/types/metric_create_params.py +++ b/src/scorecard_ai/types/metric_create_params.py @@ -50,7 +50,10 @@ class AIIntMetric(TypedDict, total=False): """The threshold for determining pass/fail from integer scores (1-5).""" temperature: float - """The temperature for AI evaluation (0-2).""" + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanIntMetric(TypedDict, total=False): @@ -122,7 +125,10 @@ class AIFloatMetric(TypedDict, total=False): """Threshold for determining pass/fail from float scores (0.0-1.0).""" temperature: float - """The temperature for AI evaluation (0-2).""" + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanFloatMetric(TypedDict, total=False): @@ -191,7 +197,10 @@ class AIBooleanMetric(TypedDict, total=False): """Guidelines for AI evaluation on how to score the metric.""" temperature: float - """The temperature for AI evaluation (0-2).""" + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanBooleanMetric(TypedDict, total=False): diff --git a/src/scorecard_ai/types/metric_update_params.py b/src/scorecard_ai/types/metric_update_params.py index 9296662..15384ac 100644 --- a/src/scorecard_ai/types/metric_update_params.py +++ b/src/scorecard_ai/types/metric_update_params.py @@ -50,7 +50,10 @@ class AIIntMetric(TypedDict, total=False): """ temperature: float - """The temperature for AI evaluation (0-2).""" + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanIntMetric(TypedDict, total=False): @@ -122,7 +125,10 @@ class AIFloatMetric(TypedDict, total=False): """ temperature: float - """The temperature for AI evaluation (0-2).""" + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanFloatMetric(TypedDict, total=False): @@ -191,7 +197,10 @@ class AIBooleanMetric(TypedDict, total=False): """ temperature: float - """The temperature for AI evaluation (0-2).""" + """The temperature for AI evaluation (0-2). + + Omit to use the model provider's default. + """ class HumanBooleanMetric(TypedDict, total=False): diff --git a/src/scorecard_ai/types/record_create_params.py b/src/scorecard_ai/types/record_create_params.py index 5bc4a7b..2f251d9 100644 --- a/src/scorecard_ai/types/record_create_params.py +++ b/src/scorecard_ai/types/record_create_params.py @@ -29,5 +29,12 @@ class RecordCreateParams(TypedDict, total=False): Used for deduplication. """ + session_id: Annotated[str, PropertyInfo(alias="sessionId")] + """Optional session ID for this record. + + Matches the `session.id` emitted on OTel spans, joining the record to its + session's traces and attachments. + """ + testcase_id: Annotated[str, PropertyInfo(alias="testcaseId")] """The ID of the Testcase.""" diff --git a/src/scorecard_ai/types/records/__init__.py b/src/scorecard_ai/types/records/__init__.py index b241b2c..cedc513 100644 --- a/src/scorecard_ai/types/records/__init__.py +++ b/src/scorecard_ai/types/records/__init__.py @@ -4,7 +4,11 @@ from .annotation import Annotation as Annotation from .record_tag import RecordTag as RecordTag +from .record_assignment import RecordAssignment as RecordAssignment from .tag_create_params import TagCreateParams as TagCreateParams from .tag_list_response import TagListResponse as TagListResponse from .tag_delete_response import TagDeleteResponse as TagDeleteResponse +from .assignee_create_params import AssigneeCreateParams as AssigneeCreateParams +from .assignee_list_response import AssigneeListResponse as AssigneeListResponse from .annotation_list_response import AnnotationListResponse as AnnotationListResponse +from .assignee_delete_response import AssigneeDeleteResponse as AssigneeDeleteResponse diff --git a/src/scorecard_ai/types/records/assignee_create_params.py b/src/scorecard_ai/types/records/assignee_create_params.py new file mode 100644 index 0000000..533cee6 --- /dev/null +++ b/src/scorecard_ai/types/records/assignee_create_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["AssigneeCreateParams"] + + +class AssigneeCreateParams(TypedDict, total=False): + assignee_user_id: Required[Annotated[str, PropertyInfo(alias="assigneeUserId")]] + """The ID of the organization member to assign. + + Idempotent: re-assigning an existing member returns the existing assignment. + """ diff --git a/src/scorecard_ai/types/records/assignee_delete_response.py b/src/scorecard_ai/types/records/assignee_delete_response.py new file mode 100644 index 0000000..6325164 --- /dev/null +++ b/src/scorecard_ai/types/records/assignee_delete_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from ..._models import BaseModel + +__all__ = ["AssigneeDeleteResponse"] + + +class AssigneeDeleteResponse(BaseModel): + deleted: float + """The number of assignment rows removed (0 if the member was not assigned).""" diff --git a/src/scorecard_ai/types/records/assignee_list_response.py b/src/scorecard_ai/types/records/assignee_list_response.py new file mode 100644 index 0000000..30f9967 --- /dev/null +++ b/src/scorecard_ai/types/records/assignee_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 .record_assignment import RecordAssignment + +__all__ = ["AssigneeListResponse"] + + +class AssigneeListResponse(BaseModel): + data: List[RecordAssignment] diff --git a/src/scorecard_ai/types/records/record_assignment.py b/src/scorecard_ai/types/records/record_assignment.py new file mode 100644 index 0000000..38fccb6 --- /dev/null +++ b/src/scorecard_ai/types/records/record_assignment.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["RecordAssignment"] + + +class RecordAssignment(BaseModel): + """An assignment of an organization member to a Record.""" + + id: str + """The ID of the record assignment.""" + + assigned_by_user_id: str = FieldInfo(alias="assignedByUserId") + """The ID of the user who created the assignment.""" + + assignee_user_id: str = FieldInfo(alias="assigneeUserId") + """The ID of the organization member assigned to the Record.""" + + created_at: str = FieldInfo(alias="createdAt") + """The ISO 8601 timestamp when the assignment was created.""" + + record_id: str = FieldInfo(alias="recordId") + """The ID of the Record this assignment belongs to.""" diff --git a/tests/api_resources/records/test_assignees.py b/tests/api_resources/records/test_assignees.py new file mode 100644 index 0000000..8875a2f --- /dev/null +++ b/tests/api_resources/records/test_assignees.py @@ -0,0 +1,284 @@ +# 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 ( + RecordAssignment, + AssigneeListResponse, + AssigneeDeleteResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAssignees: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Scorecard) -> None: + assignee = client.records.assignees.create( + record_id="777", + assignee_user_id="user_2abc123", + ) + assert_matches_type(RecordAssignment, assignee, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Scorecard) -> None: + response = client.records.assignees.with_raw_response.create( + record_id="777", + assignee_user_id="user_2abc123", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assignee = response.parse() + assert_matches_type(RecordAssignment, assignee, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Scorecard) -> None: + with client.records.assignees.with_streaming_response.create( + record_id="777", + assignee_user_id="user_2abc123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assignee = response.parse() + assert_matches_type(RecordAssignment, assignee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_create(self, client: Scorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `record_id` but received ''"): + client.records.assignees.with_raw_response.create( + record_id="", + assignee_user_id="user_2abc123", + ) + + @parametrize + def test_method_list(self, client: Scorecard) -> None: + assignee = client.records.assignees.list( + "777", + ) + assert_matches_type(AssigneeListResponse, assignee, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Scorecard) -> None: + response = client.records.assignees.with_raw_response.list( + "777", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assignee = response.parse() + assert_matches_type(AssigneeListResponse, assignee, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Scorecard) -> None: + with client.records.assignees.with_streaming_response.list( + "777", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assignee = response.parse() + assert_matches_type(AssigneeListResponse, assignee, 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.assignees.with_raw_response.list( + "", + ) + + @parametrize + def test_method_delete(self, client: Scorecard) -> None: + assignee = client.records.assignees.delete( + assignee_user_id="user_2abc123", + record_id="777", + ) + assert_matches_type(AssigneeDeleteResponse, assignee, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Scorecard) -> None: + response = client.records.assignees.with_raw_response.delete( + assignee_user_id="user_2abc123", + record_id="777", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assignee = response.parse() + assert_matches_type(AssigneeDeleteResponse, assignee, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Scorecard) -> None: + with client.records.assignees.with_streaming_response.delete( + assignee_user_id="user_2abc123", + record_id="777", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assignee = response.parse() + assert_matches_type(AssigneeDeleteResponse, assignee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Scorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `record_id` but received ''"): + client.records.assignees.with_raw_response.delete( + assignee_user_id="user_2abc123", + record_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assignee_user_id` but received ''"): + client.records.assignees.with_raw_response.delete( + assignee_user_id="", + record_id="777", + ) + + +class TestAsyncAssignees: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @parametrize + async def test_method_create(self, async_client: AsyncScorecard) -> None: + assignee = await async_client.records.assignees.create( + record_id="777", + assignee_user_id="user_2abc123", + ) + assert_matches_type(RecordAssignment, assignee, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncScorecard) -> None: + response = await async_client.records.assignees.with_raw_response.create( + record_id="777", + assignee_user_id="user_2abc123", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assignee = await response.parse() + assert_matches_type(RecordAssignment, assignee, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncScorecard) -> None: + async with async_client.records.assignees.with_streaming_response.create( + record_id="777", + assignee_user_id="user_2abc123", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assignee = await response.parse() + assert_matches_type(RecordAssignment, assignee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_create(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.assignees.with_raw_response.create( + record_id="", + assignee_user_id="user_2abc123", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncScorecard) -> None: + assignee = await async_client.records.assignees.list( + "777", + ) + assert_matches_type(AssigneeListResponse, assignee, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncScorecard) -> None: + response = await async_client.records.assignees.with_raw_response.list( + "777", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assignee = await response.parse() + assert_matches_type(AssigneeListResponse, assignee, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncScorecard) -> None: + async with async_client.records.assignees.with_streaming_response.list( + "777", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assignee = await response.parse() + assert_matches_type(AssigneeListResponse, assignee, 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.assignees.with_raw_response.list( + "", + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncScorecard) -> None: + assignee = await async_client.records.assignees.delete( + assignee_user_id="user_2abc123", + record_id="777", + ) + assert_matches_type(AssigneeDeleteResponse, assignee, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncScorecard) -> None: + response = await async_client.records.assignees.with_raw_response.delete( + assignee_user_id="user_2abc123", + record_id="777", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + assignee = await response.parse() + assert_matches_type(AssigneeDeleteResponse, assignee, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncScorecard) -> None: + async with async_client.records.assignees.with_streaming_response.delete( + assignee_user_id="user_2abc123", + record_id="777", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + assignee = await response.parse() + assert_matches_type(AssigneeDeleteResponse, assignee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(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.assignees.with_raw_response.delete( + assignee_user_id="user_2abc123", + record_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `assignee_user_id` but received ''"): + await async_client.records.assignees.with_raw_response.delete( + assignee_user_id="", + record_id="777", + ) diff --git a/tests/api_resources/test_attachments.py b/tests/api_resources/test_attachments.py new file mode 100644 index 0000000..eded6a7 --- /dev/null +++ b/tests/api_resources/test_attachments.py @@ -0,0 +1,464 @@ +# 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 import ( + Attachment, + AttachmentGetResponse, + AttachmentDeleteResponse, + AttachmentInitiateResponse, +) +from scorecard_ai.pagination import SyncPaginatedResponse, AsyncPaginatedResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAttachments: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_list(self, client: Scorecard) -> None: + attachment = client.attachments.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + ) + assert_matches_type(SyncPaginatedResponse[Attachment], attachment, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Scorecard) -> None: + attachment = client.attachments.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + cursor="eyJvZmZzZXQiOjAsInBhZ2VJZCI6ImNvZGUifQ", + limit=20, + ) + assert_matches_type(SyncPaginatedResponse[Attachment], attachment, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Scorecard) -> None: + response = client.attachments.with_raw_response.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = response.parse() + assert_matches_type(SyncPaginatedResponse[Attachment], attachment, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Scorecard) -> None: + with client.attachments.with_streaming_response.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = response.parse() + assert_matches_type(SyncPaginatedResponse[Attachment], attachment, 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 `session_id` but received ''"): + client.attachments.with_raw_response.list( + session_id="", + ) + + @parametrize + def test_method_delete(self, client: Scorecard) -> None: + attachment = client.attachments.delete( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert_matches_type(AttachmentDeleteResponse, attachment, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Scorecard) -> None: + response = client.attachments.with_raw_response.delete( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = response.parse() + assert_matches_type(AttachmentDeleteResponse, attachment, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Scorecard) -> None: + with client.attachments.with_streaming_response.delete( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = response.parse() + assert_matches_type(AttachmentDeleteResponse, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Scorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `attachment_id` but received ''"): + client.attachments.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_commit(self, client: Scorecard) -> None: + attachment = client.attachments.commit( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert_matches_type(Attachment, attachment, path=["response"]) + + @parametrize + def test_raw_response_commit(self, client: Scorecard) -> None: + response = client.attachments.with_raw_response.commit( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = response.parse() + assert_matches_type(Attachment, attachment, path=["response"]) + + @parametrize + def test_streaming_response_commit(self, client: Scorecard) -> None: + with client.attachments.with_streaming_response.commit( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = response.parse() + assert_matches_type(Attachment, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_commit(self, client: Scorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `attachment_id` but received ''"): + client.attachments.with_raw_response.commit( + "", + ) + + @parametrize + def test_method_get(self, client: Scorecard) -> None: + attachment = client.attachments.get( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert_matches_type(AttachmentGetResponse, attachment, path=["response"]) + + @parametrize + def test_raw_response_get(self, client: Scorecard) -> None: + response = client.attachments.with_raw_response.get( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = response.parse() + assert_matches_type(AttachmentGetResponse, attachment, path=["response"]) + + @parametrize + def test_streaming_response_get(self, client: Scorecard) -> None: + with client.attachments.with_streaming_response.get( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = response.parse() + assert_matches_type(AttachmentGetResponse, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_get(self, client: Scorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `attachment_id` but received ''"): + client.attachments.with_raw_response.get( + "", + ) + + @parametrize + def test_method_initiate(self, client: Scorecard) -> None: + attachment = client.attachments.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + ) + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + @parametrize + def test_method_initiate_with_all_params(self, client: Scorecard) -> None: + attachment = client.attachments.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + filename="report.pdf", + metadata={"foo": "bar"}, + ) + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + @parametrize + def test_raw_response_initiate(self, client: Scorecard) -> None: + response = client.attachments.with_raw_response.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = response.parse() + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + @parametrize + def test_streaming_response_initiate(self, client: Scorecard) -> None: + with client.attachments.with_streaming_response.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = response.parse() + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAttachments: + 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: + attachment = await async_client.attachments.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + ) + assert_matches_type(AsyncPaginatedResponse[Attachment], attachment, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncScorecard) -> None: + attachment = await async_client.attachments.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + cursor="eyJvZmZzZXQiOjAsInBhZ2VJZCI6ImNvZGUifQ", + limit=20, + ) + assert_matches_type(AsyncPaginatedResponse[Attachment], attachment, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncScorecard) -> None: + response = await async_client.attachments.with_raw_response.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = await response.parse() + assert_matches_type(AsyncPaginatedResponse[Attachment], attachment, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncScorecard) -> None: + async with async_client.attachments.with_streaming_response.list( + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = await response.parse() + assert_matches_type(AsyncPaginatedResponse[Attachment], attachment, 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 `session_id` but received ''"): + await async_client.attachments.with_raw_response.list( + session_id="", + ) + + @parametrize + async def test_method_delete(self, async_client: AsyncScorecard) -> None: + attachment = await async_client.attachments.delete( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert_matches_type(AttachmentDeleteResponse, attachment, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncScorecard) -> None: + response = await async_client.attachments.with_raw_response.delete( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = await response.parse() + assert_matches_type(AttachmentDeleteResponse, attachment, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncScorecard) -> None: + async with async_client.attachments.with_streaming_response.delete( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = await response.parse() + assert_matches_type(AttachmentDeleteResponse, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncScorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `attachment_id` but received ''"): + await async_client.attachments.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_commit(self, async_client: AsyncScorecard) -> None: + attachment = await async_client.attachments.commit( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert_matches_type(Attachment, attachment, path=["response"]) + + @parametrize + async def test_raw_response_commit(self, async_client: AsyncScorecard) -> None: + response = await async_client.attachments.with_raw_response.commit( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = await response.parse() + assert_matches_type(Attachment, attachment, path=["response"]) + + @parametrize + async def test_streaming_response_commit(self, async_client: AsyncScorecard) -> None: + async with async_client.attachments.with_streaming_response.commit( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = await response.parse() + assert_matches_type(Attachment, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_commit(self, async_client: AsyncScorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `attachment_id` but received ''"): + await async_client.attachments.with_raw_response.commit( + "", + ) + + @parametrize + async def test_method_get(self, async_client: AsyncScorecard) -> None: + attachment = await async_client.attachments.get( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + assert_matches_type(AttachmentGetResponse, attachment, path=["response"]) + + @parametrize + async def test_raw_response_get(self, async_client: AsyncScorecard) -> None: + response = await async_client.attachments.with_raw_response.get( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = await response.parse() + assert_matches_type(AttachmentGetResponse, attachment, path=["response"]) + + @parametrize + async def test_streaming_response_get(self, async_client: AsyncScorecard) -> None: + async with async_client.attachments.with_streaming_response.get( + "3fa85f64-5717-4562-b3fc-2c963f66afa6", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = await response.parse() + assert_matches_type(AttachmentGetResponse, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_get(self, async_client: AsyncScorecard) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `attachment_id` but received ''"): + await async_client.attachments.with_raw_response.get( + "", + ) + + @parametrize + async def test_method_initiate(self, async_client: AsyncScorecard) -> None: + attachment = await async_client.attachments.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + ) + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + @parametrize + async def test_method_initiate_with_all_params(self, async_client: AsyncScorecard) -> None: + attachment = await async_client.attachments.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + filename="report.pdf", + metadata={"foo": "bar"}, + ) + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + @parametrize + async def test_raw_response_initiate(self, async_client: AsyncScorecard) -> None: + response = await async_client.attachments.with_raw_response.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + attachment = await response.parse() + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + @parametrize + async def test_streaming_response_initiate(self, async_client: AsyncScorecard) -> None: + async with async_client.attachments.with_streaming_response.initiate( + content_type="application/pdf", + file_path="/tmp/report.pdf", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", + sha256="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + size_bytes=482133, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + attachment = await response.parse() + assert_matches_type(AttachmentInitiateResponse, attachment, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_records.py b/tests/api_resources/test_records.py index 30822d9..010a8ff 100644 --- a/tests/api_resources/test_records.py +++ b/tests/api_resources/test_records.py @@ -40,6 +40,7 @@ def test_method_create_with_all_params(self, client: Scorecard) -> None: inputs={"question": "bar"}, outputs={"response": "bar"}, otel_link_id="otelLinkId", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", testcase_id="248", ) assert_matches_type(Record, record, path=["response"]) @@ -194,6 +195,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncScorecard) inputs={"question": "bar"}, outputs={"response": "bar"}, otel_link_id="otelLinkId", + session_id="c59e5bd0-e5eb-4bf0-a08a-01f7e8f712c7", testcase_id="248", ) assert_matches_type(Record, record, path=["response"])