From b6d391be490a30e41617467dff18acfb3f337920 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 7 Jul 2026 22:22:05 +0000
Subject: [PATCH 1/7] feat(api): api update
---
.stats.yml | 4 +--
src/scorecard_ai/resources/metrics.py | 36 ++++++++++++-------
src/scorecard_ai/types/metric.py | 21 +++++++----
.../types/metric_create_params.py | 15 ++++++--
.../types/metric_update_params.py | 15 ++++++--
5 files changed, 65 insertions(+), 26 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index 449029e..e0e961e 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
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-3dcb36bf0a81f7d59c6b8c0f1f2d523e826fbc18e48dcf1595ccae92859c7a98.yml
+openapi_spec_hash: 3295d1cc99a003f78e058586c600c87d
config_hash: 31cceec463f09f560813ae649da927ab
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/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):
From 86b5a5275d4e302a022c86548dcfb043748a81f0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 14 Jul 2026 02:02:51 +0000
Subject: [PATCH 2/7] fix(internal): resolve build failures
---
scripts/lint | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 .
From ca4c4b526792ccbb10a9bd9a897dbcbddfc4a022 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 17 Jul 2026 20:45:53 +0000
Subject: [PATCH 3/7] feat(api): api update
---
.stats.yml | 8 +-
api.md | 21 +
src/scorecard_ai/_client.py | 39 +-
src/scorecard_ai/resources/__init__.py | 14 +
src/scorecard_ai/resources/attachments.py | 609 ++++++++++++++++++
src/scorecard_ai/types/__init__.py | 6 +
src/scorecard_ai/types/attachment.py | 59 ++
.../types/attachment_delete_response.py | 10 +
.../types/attachment_get_response.py | 25 +
.../types/attachment_initiate_params.py | 42 ++
.../types/attachment_initiate_response.py | 30 +
.../types/attachment_list_params.py | 22 +
tests/api_resources/test_attachments.py | 464 +++++++++++++
13 files changed, 1344 insertions(+), 5 deletions(-)
create mode 100644 src/scorecard_ai/resources/attachments.py
create mode 100644 src/scorecard_ai/types/attachment.py
create mode 100644 src/scorecard_ai/types/attachment_delete_response.py
create mode 100644 src/scorecard_ai/types/attachment_get_response.py
create mode 100644 src/scorecard_ai/types/attachment_initiate_params.py
create mode 100644 src/scorecard_ai/types/attachment_initiate_response.py
create mode 100644 src/scorecard_ai/types/attachment_list_params.py
create mode 100644 tests/api_resources/test_attachments.py
diff --git a/.stats.yml b/.stats.yml
index e0e961e..ec636db 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-3dcb36bf0a81f7d59c6b8c0f1f2d523e826fbc18e48dcf1595ccae92859c7a98.yml
-openapi_spec_hash: 3295d1cc99a003f78e058586c600c87d
-config_hash: 31cceec463f09f560813ae649da927ab
+configured_endpoints: 40
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-f611b82a4bf9c430e8e3f653e92c5971c08051fc4bf5fe8f42bc733e2816fa1d.yml
+openapi_spec_hash: 3bae59e27cc14172457a789fb3d3d530
+config_hash: d074bb171a9f085a529a441922c2e35c
diff --git a/api.md b/api.md
index 0761614..7293646 100644
--- a/api.md
+++ b/api.md
@@ -131,6 +131,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/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/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/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/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
From d0bba7c44fa0bae6ee4cb576687ffcdbb7f02c46 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Jul 2026 02:03:03 +0000
Subject: [PATCH 4/7] feat(stlc): configurable CI runner and
private-production-repo support in workflow templates
---
.github/workflows/ci.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
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
From 99eb1167ba8c4c95666299e729334ae6ee478fb9 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 20 Jul 2026 21:24:09 +0000
Subject: [PATCH 5/7] feat(api): api update
---
.stats.yml | 4 ++--
src/scorecard_ai/resources/records/records.py | 10 ++++++++++
src/scorecard_ai/types/record_create_params.py | 7 +++++++
tests/api_resources/test_records.py | 2 ++
4 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index ec636db..c7646b6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 40
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-f611b82a4bf9c430e8e3f653e92c5971c08051fc4bf5fe8f42bc733e2816fa1d.yml
-openapi_spec_hash: 3bae59e27cc14172457a789fb3d3d530
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-a96bc24efc067805f80c6345c220a92254c0aabcc7db3748800d525d19425d35.yml
+openapi_spec_hash: a7a5085c8f199005de20f129993bb4c1
config_hash: d074bb171a9f085a529a441922c2e35c
diff --git a/src/scorecard_ai/resources/records/records.py b/src/scorecard_ai/resources/records/records.py
index 2afb2a1..411e6fb 100644
--- a/src/scorecard_ai/resources/records/records.py
+++ b/src/scorecard_ai/resources/records/records.py
@@ -78,6 +78,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 +101,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 +124,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,
@@ -258,6 +263,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 +286,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 +309,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,
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/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"])
From f7092fc7c61beae93e76b3b8f3136e691470c66e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Jul 2026 14:20:25 +0000
Subject: [PATCH 6/7] feat(api): api update
---
.stats.yml | 8 +-
api.md | 18 +
.../resources/records/__init__.py | 14 +
.../resources/records/assignees.py | 353 ++++++++++++++++++
src/scorecard_ai/resources/records/records.py | 32 ++
src/scorecard_ai/types/records/__init__.py | 4 +
.../types/records/assignee_create_params.py | 17 +
.../types/records/assignee_delete_response.py | 10 +
.../types/records/assignee_list_response.py | 12 +
.../types/records/record_assignment.py | 26 ++
tests/api_resources/records/test_assignees.py | 284 ++++++++++++++
11 files changed, 774 insertions(+), 4 deletions(-)
create mode 100644 src/scorecard_ai/resources/records/assignees.py
create mode 100644 src/scorecard_ai/types/records/assignee_create_params.py
create mode 100644 src/scorecard_ai/types/records/assignee_delete_response.py
create mode 100644 src/scorecard_ai/types/records/assignee_list_response.py
create mode 100644 src/scorecard_ai/types/records/record_assignment.py
create mode 100644 tests/api_resources/records/test_assignees.py
diff --git a/.stats.yml b/.stats.yml
index c7646b6..4193920 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 40
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/scorecard/scorecard-a96bc24efc067805f80c6345c220a92254c0aabcc7db3748800d525d19425d35.yml
-openapi_spec_hash: a7a5085c8f199005de20f129993bb4c1
-config_hash: d074bb171a9f085a529a441922c2e35c
+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/api.md b/api.md
index 7293646..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:
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 411e6fb..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:
"""
@@ -236,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:
"""
@@ -434,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:
@@ -457,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:
@@ -480,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:
@@ -502,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/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",
+ )
From 44b4f7dd0b17fc70bff3f91ea6cd634d625c6db0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 31 Jul 2026 14:20:53 +0000
Subject: [PATCH 7/7] release: 3.9.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 17 +++++++++++++++++
pyproject.toml | 2 +-
src/scorecard_ai/_version.py | 2 +-
4 files changed, 20 insertions(+), 3 deletions(-)
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/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/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/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