From 95a29b7bd6bb5e476017675ec6132cd503c651b5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:41:50 +0000 Subject: [PATCH 01/16] chore(deps): mypy 1.18.1 has a regression, pin to 1.17 --- pyproject.toml | 2 +- requirements-dev.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 47e208a..f5ab80a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index 48c3c54..9aae991 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -291,7 +291,7 @@ ml-dtypes==0.2.0 multidict==6.7.0 # via aiohttp # via yarl -mypy==1.14.1 +mypy==1.17.0 mypy-extensions==1.0.0 # via mypy nbclient==0.10.2 From aeaae87e004d4971c0b03a7504bac9a1803b679e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 04:27:29 +0000 Subject: [PATCH 02/16] chore: update lockfile --- pyproject.toml | 14 ++++++++------ requirements-dev.lock | 17 ++++++++++------- requirements.lock | 4 ++-- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f5ab80a..d91bc70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,14 @@ license = "Apache-2.0" authors = [ { name = "Aymara AI", email = "support@aymara.ai" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", "boto3>=1.37.37", "matplotlib>=3.9.4", "pandas>=2.2.3", @@ -24,6 +25,7 @@ dependencies = [ "colorama>=0.4.6", "tqdm>=4.67.1", ] + requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", diff --git a/requirements-dev.lock b/requirements-dev.lock index 9aae991..f6b8252 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -47,7 +47,7 @@ async-lru==2.0.5 # via jupyterlab async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp # via jsonschema # via referencing @@ -101,7 +101,7 @@ defusedxml==0.7.1 dirty-equals==0.6.0 distlib==0.3.7 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via aymara-ai-sdk # via openai eliot==1.17.5 @@ -113,7 +113,7 @@ exceptiongroup==1.2.2 # via anyio # via ipython # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist executing==2.2.0 # via stack-data @@ -164,7 +164,9 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via aymara-ai-sdk -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via jsonschema @@ -292,7 +294,7 @@ multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy nbclient==0.10.2 # via nbconvert @@ -357,7 +359,7 @@ pillow==11.2.1 platformdirs==3.11.0 # via jupyter-core # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest prometheus-client==0.21.1 # via jupyter-server @@ -508,6 +510,7 @@ tinycss2==1.4.0 tomli==2.0.2 # via jupyterlab # via mypy + # via nox # via pytest toolz==1.1.0 # via eliot-tree @@ -594,7 +597,7 @@ wrapt==1.14.2 # via tensorflow yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata # via importlib-resources zope-interface==8.0.1 diff --git a/requirements.lock b/requirements.lock index 7b716a7..da867ee 100644 --- a/requirements.lock +++ b/requirements.lock @@ -45,7 +45,7 @@ async-lru==2.0.5 # via jupyterlab async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp # via jsonschema # via referencing @@ -152,7 +152,7 @@ httpx==0.28.1 # via openai httpx-aiohttp==0.1.9 # via aymara-ai-sdk -idna==3.4 +idna==3.11 # via anyio # via httpx # via jsonschema From 1781507d6f5da5943777d86a75ddfa2e5260a99a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 04:40:59 +0000 Subject: [PATCH 03/16] chore(docs): use environment variables for authentication in code snippets --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 29efbe7..c606e0a 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ pip install aymara-ai-sdk[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from aymara_ai import DefaultAioHttpClient from aymara_ai import AsyncAymaraAI @@ -106,7 +107,7 @@ from aymara_ai import AsyncAymaraAI async def main() -> None: async with AsyncAymaraAI( - api_key="My API Key", + api_key=os.environ.get("AYMARA_AI_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: eval_run_result = await client.evals.runs.create( From 35c60e580328fc794dd60d16dd3a8fc2b376b39d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:32:28 +0000 Subject: [PATCH 04/16] feat(api): api update --- .stats.yml | 4 ++-- src/aymara_ai/resources/evals/evals.py | 10 ++++++++ src/aymara_ai/resources/evals/runs.py | 16 ++++++------- src/aymara_ai/types/eval.py | 6 +++++ src/aymara_ai/types/eval_create_params.py | 6 +++++ .../types/evals/run_update_response_params.py | 10 ++++---- tests/api_resources/evals/test_runs.py | 24 +++++++++++++++++-- tests/api_resources/test_evals.py | 2 ++ 8 files changed, 61 insertions(+), 17 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1387f90..f739618 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/aymara%2Faymara-sdk-0cb64ef11adfe528cc90a67e64197eab629bf73fb581e7afd7b884e114219217.yml -openapi_spec_hash: 0717100a8b054007347209657b0e9a59 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/aymara%2Faymara-sdk-ad96d8d628e05902f87a1643da9fbf2d9ca10b7154c4a54598729afc32ee9b23.yml +openapi_spec_hash: e246e620f37ff63091957c9dfd2264eb config_hash: 890fa7ef8d5b8a0287741a83ed69ca36 diff --git a/src/aymara_ai/resources/evals/evals.py b/src/aymara_ai/resources/evals/evals.py index f5d3db4..b7a4e0c 100644 --- a/src/aymara_ai/resources/evals/evals.py +++ b/src/aymara_ai/resources/evals/evals.py @@ -90,6 +90,7 @@ def create( num_prompts: Optional[int] | Omit = omit, prompt_examples: Optional[Iterable[PromptExampleParam]] | Omit = omit, status: Optional[Status] | Omit = omit, + task_timeout: Optional[int] | Omit = omit, updated_at: Union[str, datetime, None] | Omit = omit, workspace_uuid: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -145,6 +146,9 @@ def create( status: Resource status. + task_timeout: Custom timeout in seconds for task execution warning threshold. If not set, + defaults to 180 seconds. + updated_at: Timestamp when the eval was last updated. workspace_uuid: UUID of the associated workspace, if any. @@ -177,6 +181,7 @@ def create( "num_prompts": num_prompts, "prompt_examples": prompt_examples, "status": status, + "task_timeout": task_timeout, "updated_at": updated_at, "workspace_uuid": workspace_uuid, }, @@ -665,6 +670,7 @@ async def create( num_prompts: Optional[int] | Omit = omit, prompt_examples: Optional[Iterable[PromptExampleParam]] | Omit = omit, status: Optional[Status] | Omit = omit, + task_timeout: Optional[int] | Omit = omit, updated_at: Union[str, datetime, None] | Omit = omit, workspace_uuid: Optional[str] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -720,6 +726,9 @@ async def create( status: Resource status. + task_timeout: Custom timeout in seconds for task execution warning threshold. If not set, + defaults to 180 seconds. + updated_at: Timestamp when the eval was last updated. workspace_uuid: UUID of the associated workspace, if any. @@ -752,6 +761,7 @@ async def create( "num_prompts": num_prompts, "prompt_examples": prompt_examples, "status": status, + "task_timeout": task_timeout, "updated_at": updated_at, "workspace_uuid": workspace_uuid, }, diff --git a/src/aymara_ai/resources/evals/runs.py b/src/aymara_ai/resources/evals/runs.py index 539f030..878f5ed 100644 --- a/src/aymara_ai/resources/evals/runs.py +++ b/src/aymara_ai/resources/evals/runs.py @@ -502,9 +502,9 @@ def update_response( response_uuid: str, *, eval_run_uuid: str, + confidence: float, + explanation: str, workspace_uuid: str | Omit = omit, - confidence: Optional[float] | Omit = omit, - explanation: Optional[str] | Omit = omit, is_passed: Optional[bool] | 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. @@ -514,7 +514,7 @@ def update_response( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ScoredResponse: """ - Update the score, confidence, or explanation for a specific response. + Update the score, confidence, and explanation for this response. Args: eval_run_uuid (str): UUID of the eval run. response_uuid (str): UUID of the response to update. update_request (AnswerUpdateRequest): The update data. @@ -528,7 +528,7 @@ def update_response( "is_passed": true, "confidence": 0.95, "explanation": "Updated explanation" } Args: - confidence: Confidence score for the response. + confidence: Confidence score between 0 and 1 explanation: Explanation for the response. @@ -1036,9 +1036,9 @@ async def update_response( response_uuid: str, *, eval_run_uuid: str, + confidence: float, + explanation: str, workspace_uuid: str | Omit = omit, - confidence: Optional[float] | Omit = omit, - explanation: Optional[str] | Omit = omit, is_passed: Optional[bool] | 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. @@ -1048,7 +1048,7 @@ async def update_response( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> ScoredResponse: """ - Update the score, confidence, or explanation for a specific response. + Update the score, confidence, and explanation for this response. Args: eval_run_uuid (str): UUID of the eval run. response_uuid (str): UUID of the response to update. update_request (AnswerUpdateRequest): The update data. @@ -1062,7 +1062,7 @@ async def update_response( "is_passed": true, "confidence": 0.95, "explanation": "Updated explanation" } Args: - confidence: Confidence score for the response. + confidence: Confidence score between 0 and 1 explanation: Explanation for the response. diff --git a/src/aymara_ai/types/eval.py b/src/aymara_ai/types/eval.py index edee5cd..e77b25c 100644 --- a/src/aymara_ai/types/eval.py +++ b/src/aymara_ai/types/eval.py @@ -72,6 +72,12 @@ class Eval(BaseModel): status: Optional[Status] = None """Resource status.""" + task_timeout: Optional[int] = None + """Custom timeout in seconds for task execution warning threshold. + + If not set, defaults to 180 seconds. + """ + updated_at: Optional[datetime] = None """Timestamp when the eval was last updated.""" diff --git a/src/aymara_ai/types/eval_create_params.py b/src/aymara_ai/types/eval_create_params.py index 2ba3f38..a993de4 100644 --- a/src/aymara_ai/types/eval_create_params.py +++ b/src/aymara_ai/types/eval_create_params.py @@ -70,6 +70,12 @@ class EvalCreateParams(TypedDict, total=False): status: Optional[Status] """Resource status.""" + task_timeout: Optional[int] + """Custom timeout in seconds for task execution warning threshold. + + If not set, defaults to 180 seconds. + """ + updated_at: Annotated[Union[str, datetime, None], PropertyInfo(format="iso8601")] """Timestamp when the eval was last updated.""" diff --git a/src/aymara_ai/types/evals/run_update_response_params.py b/src/aymara_ai/types/evals/run_update_response_params.py index a38d2ad..f11dfae 100644 --- a/src/aymara_ai/types/evals/run_update_response_params.py +++ b/src/aymara_ai/types/evals/run_update_response_params.py @@ -11,13 +11,13 @@ class RunUpdateResponseParams(TypedDict, total=False): eval_run_uuid: Required[str] - workspace_uuid: str - - confidence: Optional[float] - """Confidence score for the response.""" + confidence: Required[float] + """Confidence score between 0 and 1""" - explanation: Optional[str] + explanation: Required[str] """Explanation for the response.""" + workspace_uuid: str + is_passed: Optional[bool] """Whether the response passed the evaluation.""" diff --git a/tests/api_resources/evals/test_runs.py b/tests/api_resources/evals/test_runs.py index c787eb3..aaed608 100644 --- a/tests/api_resources/evals/test_runs.py +++ b/tests/api_resources/evals/test_runs.py @@ -428,6 +428,8 @@ def test_method_update_response(self, client: AymaraAI) -> None: run = client.evals.runs.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) assert_matches_type(ScoredResponse, run, path=["response"]) @@ -437,9 +439,9 @@ def test_method_update_response_with_all_params(self, client: AymaraAI) -> None: run = client.evals.runs.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", - workspace_uuid="workspace_uuid", confidence=0, explanation="explanation", + workspace_uuid="workspace_uuid", is_passed=True, ) assert_matches_type(ScoredResponse, run, path=["response"]) @@ -450,6 +452,8 @@ def test_raw_response_update_response(self, client: AymaraAI) -> None: response = client.evals.runs.with_raw_response.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) assert response.is_closed is True @@ -463,6 +467,8 @@ def test_streaming_response_update_response(self, client: AymaraAI) -> None: with client.evals.runs.with_streaming_response.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -479,12 +485,16 @@ def test_path_params_update_response(self, client: AymaraAI) -> None: client.evals.runs.with_raw_response.update_response( response_uuid="response_uuid", eval_run_uuid="", + confidence=0, + explanation="explanation", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `response_uuid` but received ''"): client.evals.runs.with_raw_response.update_response( response_uuid="", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) @@ -899,6 +909,8 @@ async def test_method_update_response(self, async_client: AsyncAymaraAI) -> None run = await async_client.evals.runs.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) assert_matches_type(ScoredResponse, run, path=["response"]) @@ -908,9 +920,9 @@ async def test_method_update_response_with_all_params(self, async_client: AsyncA run = await async_client.evals.runs.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", - workspace_uuid="workspace_uuid", confidence=0, explanation="explanation", + workspace_uuid="workspace_uuid", is_passed=True, ) assert_matches_type(ScoredResponse, run, path=["response"]) @@ -921,6 +933,8 @@ async def test_raw_response_update_response(self, async_client: AsyncAymaraAI) - response = await async_client.evals.runs.with_raw_response.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) assert response.is_closed is True @@ -934,6 +948,8 @@ async def test_streaming_response_update_response(self, async_client: AsyncAymar async with async_client.evals.runs.with_streaming_response.update_response( response_uuid="response_uuid", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -950,10 +966,14 @@ async def test_path_params_update_response(self, async_client: AsyncAymaraAI) -> await async_client.evals.runs.with_raw_response.update_response( response_uuid="response_uuid", eval_run_uuid="", + confidence=0, + explanation="explanation", ) with pytest.raises(ValueError, match=r"Expected a non-empty value for `response_uuid` but received ''"): await async_client.evals.runs.with_raw_response.update_response( response_uuid="", eval_run_uuid="eval_run_uuid", + confidence=0, + explanation="explanation", ) diff --git a/tests/api_resources/test_evals.py b/tests/api_resources/test_evals.py index d812062..5307859 100644 --- a/tests/api_resources/test_evals.py +++ b/tests/api_resources/test_evals.py @@ -59,6 +59,7 @@ def test_method_create_with_all_params(self, client: AymaraAI) -> None: } ], status="created", + task_timeout=0, updated_at=parse_datetime("2019-12-27T18:11:19.117Z"), workspace_uuid="workspace_uuid", ) @@ -453,6 +454,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAymaraAI) } ], status="created", + task_timeout=0, updated_at=parse_datetime("2019-12-27T18:11:19.117Z"), workspace_uuid="workspace_uuid", ) From 3c302b2a665b0b3afdad9b6a7ef1efd59ee5c03a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 03:58:03 +0000 Subject: [PATCH 05/16] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/aymara_ai/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/aymara_ai/_types.py b/src/aymara_ai/_types.py index 07b3fde..c48b0a3 100644 --- a/src/aymara_ai/_types.py +++ b/src/aymara_ai/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From 69dedfa3958e39146b28a7277eec90a29c1e22fb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 03:59:32 +0000 Subject: [PATCH 06/16] chore: add missing docstrings --- src/aymara_ai/types/ai_instruction.py | 2 ++ src/aymara_ai/types/eval.py | 2 ++ src/aymara_ai/types/eval_analyze_response.py | 6 ++++++ src/aymara_ai/types/eval_response_param.py | 2 ++ src/aymara_ai/types/eval_suite_report.py | 2 ++ src/aymara_ai/types/eval_type.py | 2 ++ src/aymara_ai/types/eval_update_params.py | 4 ++++ src/aymara_ai/types/evals/answer_history.py | 4 ++++ src/aymara_ai/types/evals/eval_run_example.py | 2 ++ src/aymara_ai/types/evals/eval_run_example_param.py | 2 ++ src/aymara_ai/types/evals/eval_run_result.py | 2 ++ src/aymara_ai/types/evals/scored_response.py | 4 ++++ src/aymara_ai/types/file_detail.py | 2 ++ src/aymara_ai/types/file_frames.py | 2 ++ src/aymara_ai/types/file_status.py | 2 ++ src/aymara_ai/types/shared/file_reference.py | 6 ++++++ src/aymara_ai/types/shared/tool.py | 2 ++ src/aymara_ai/types/shared/tool_array.py | 2 ++ src/aymara_ai/types/shared/tool_dict.py | 2 ++ src/aymara_ai/types/shared/tool_string.py | 2 ++ src/aymara_ai/types/shared/workflow_instructions.py | 2 ++ src/aymara_ai/types/shared_params/file_reference.py | 6 ++++++ src/aymara_ai/types/shared_params/tool.py | 2 ++ src/aymara_ai/types/shared_params/tool_array.py | 2 ++ src/aymara_ai/types/shared_params/tool_dict.py | 2 ++ src/aymara_ai/types/shared_params/tool_string.py | 2 ++ src/aymara_ai/types/shared_params/workflow_instructions.py | 2 ++ 27 files changed, 72 insertions(+) diff --git a/src/aymara_ai/types/ai_instruction.py b/src/aymara_ai/types/ai_instruction.py index ea75b38..d873fe9 100644 --- a/src/aymara_ai/types/ai_instruction.py +++ b/src/aymara_ai/types/ai_instruction.py @@ -8,6 +8,8 @@ class AIInstruction(BaseModel): + """Schema for AI instructions.""" + content: str eval_type: str diff --git a/src/aymara_ai/types/eval.py b/src/aymara_ai/types/eval.py index e77b25c..46ff685 100644 --- a/src/aymara_ai/types/eval.py +++ b/src/aymara_ai/types/eval.py @@ -20,6 +20,8 @@ class Eval(BaseModel): + """Schema for configuring an Eval based on a eval_type.""" + ai_description: str """Description of the AI under evaluation.""" diff --git a/src/aymara_ai/types/eval_analyze_response.py b/src/aymara_ai/types/eval_analyze_response.py index b3883a2..d214dc4 100644 --- a/src/aymara_ai/types/eval_analyze_response.py +++ b/src/aymara_ai/types/eval_analyze_response.py @@ -10,6 +10,8 @@ class ResultStats(BaseModel): + """Aggregated statistics for this eval""" + total_responses_scored: int """Total number of responses scored across all runs""" @@ -30,6 +32,8 @@ class ResultStats(BaseModel): class Result(BaseModel): + """Schema for a single eval result in analysis response.""" + eval: Eval """The eval data""" @@ -38,6 +42,8 @@ class Result(BaseModel): class EvalAnalyzeResponse(BaseModel): + """Schema for eval analysis response.""" + has_more: bool """Whether there are more results available""" diff --git a/src/aymara_ai/types/eval_response_param.py b/src/aymara_ai/types/eval_response_param.py index ff0f597..f01fb26 100644 --- a/src/aymara_ai/types/eval_response_param.py +++ b/src/aymara_ai/types/eval_response_param.py @@ -14,6 +14,8 @@ class EvalResponseParam(TypedDict, total=False): + """Schema for submitting AI responses to eval prompts.""" + prompt_uuid: Required[str] """Unique identifier for the prompt.""" diff --git a/src/aymara_ai/types/eval_suite_report.py b/src/aymara_ai/types/eval_suite_report.py index b05200a..52de78d 100644 --- a/src/aymara_ai/types/eval_suite_report.py +++ b/src/aymara_ai/types/eval_suite_report.py @@ -31,6 +31,8 @@ class EvalRunReport(BaseModel): class EvalSuiteReport(BaseModel): + """Schema for returning eval run suite summary data.""" + created_at: datetime """Timestamp when the eval suite report was created.""" diff --git a/src/aymara_ai/types/eval_type.py b/src/aymara_ai/types/eval_type.py index 624528b..5b14a65 100644 --- a/src/aymara_ai/types/eval_type.py +++ b/src/aymara_ai/types/eval_type.py @@ -8,6 +8,8 @@ class EvalType(BaseModel): + """Schema for eval eval_types.""" + description: str """Description of the eval type.""" diff --git a/src/aymara_ai/types/eval_update_params.py b/src/aymara_ai/types/eval_update_params.py index 06db8b5..c812e1e 100644 --- a/src/aymara_ai/types/eval_update_params.py +++ b/src/aymara_ai/types/eval_update_params.py @@ -47,6 +47,8 @@ class EvalUpdateParams(TypedDict, total=False): class PromptCreate(TypedDict, total=False): + """Schema for creating new prompts.""" + content: Required[str] """Content of the new prompt.""" @@ -55,6 +57,8 @@ class PromptCreate(TypedDict, total=False): class PromptUpdate(TypedDict, total=False): + """Schema for updating existing prompts.""" + prompt_uuid: Required[str] """UUID of the prompt to update or delete.""" diff --git a/src/aymara_ai/types/evals/answer_history.py b/src/aymara_ai/types/evals/answer_history.py index 1287be4..ab63aad 100644 --- a/src/aymara_ai/types/evals/answer_history.py +++ b/src/aymara_ai/types/evals/answer_history.py @@ -29,6 +29,8 @@ class UpdatedByWorkspace(BaseModel): class UpdatedBy(BaseModel): + """User who made the update.""" + email: str feature_flags: UpdatedByFeatureFlags @@ -45,6 +47,8 @@ class UpdatedBy(BaseModel): class AnswerHistory(BaseModel): + """Schema for answer history entries.""" + answer_history_uuid: str """Unique identifier for the history entry.""" diff --git a/src/aymara_ai/types/evals/eval_run_example.py b/src/aymara_ai/types/evals/eval_run_example.py index a87b96e..4c1108a 100644 --- a/src/aymara_ai/types/evals/eval_run_example.py +++ b/src/aymara_ai/types/evals/eval_run_example.py @@ -9,6 +9,8 @@ class EvalRunExample(BaseModel): + """Schema for examples to include with an eval run.""" + prompt: str """Prompt text for the example.""" diff --git a/src/aymara_ai/types/evals/eval_run_example_param.py b/src/aymara_ai/types/evals/eval_run_example_param.py index f5c6370..5313e70 100644 --- a/src/aymara_ai/types/evals/eval_run_example_param.py +++ b/src/aymara_ai/types/evals/eval_run_example_param.py @@ -9,6 +9,8 @@ class EvalRunExampleParam(TypedDict, total=False): + """Schema for examples to include with an eval run.""" + prompt: Required[str] """Prompt text for the example.""" diff --git a/src/aymara_ai/types/evals/eval_run_result.py b/src/aymara_ai/types/evals/eval_run_result.py index 5511d4a..7fd0a80 100644 --- a/src/aymara_ai/types/evals/eval_run_result.py +++ b/src/aymara_ai/types/evals/eval_run_result.py @@ -13,6 +13,8 @@ class EvalRunResult(BaseModel): + """Schema for returning eval run data.""" + created_at: datetime """Timestamp when the eval run was created.""" diff --git a/src/aymara_ai/types/evals/scored_response.py b/src/aymara_ai/types/evals/scored_response.py index 0440fd2..ed5af41 100644 --- a/src/aymara_ai/types/evals/scored_response.py +++ b/src/aymara_ai/types/evals/scored_response.py @@ -43,6 +43,8 @@ class ManuallyUpdatedByWorkspace(BaseModel): class ManuallyUpdatedBy(BaseModel): + """User who last manually updated this response.""" + email: str feature_flags: ManuallyUpdatedByFeatureFlags @@ -59,6 +61,8 @@ class ManuallyUpdatedBy(BaseModel): class ScoredResponse(BaseModel): + """Schema for returning AI response data.""" + prompt_uuid: str """Unique identifier for the prompt.""" diff --git a/src/aymara_ai/types/file_detail.py b/src/aymara_ai/types/file_detail.py index 93b8887..db1e2b4 100644 --- a/src/aymara_ai/types/file_detail.py +++ b/src/aymara_ai/types/file_detail.py @@ -9,6 +9,8 @@ class FileDetail(BaseModel): + """Schema for returning detailed file information.""" + content_type: str """MIME type of the file.""" diff --git a/src/aymara_ai/types/file_frames.py b/src/aymara_ai/types/file_frames.py index 48a201b..3463021 100644 --- a/src/aymara_ai/types/file_frames.py +++ b/src/aymara_ai/types/file_frames.py @@ -8,6 +8,8 @@ class FileFrames(BaseModel): + """Schema for video file frames endpoint.""" + file_uuid: str """Unique identifier for the file.""" diff --git a/src/aymara_ai/types/file_status.py b/src/aymara_ai/types/file_status.py index 316f95c..158c271 100644 --- a/src/aymara_ai/types/file_status.py +++ b/src/aymara_ai/types/file_status.py @@ -9,6 +9,8 @@ class FileStatus(BaseModel): + """Schema for file processing status endpoint.""" + created_at: datetime """Timestamp when the file was created.""" diff --git a/src/aymara_ai/types/shared/file_reference.py b/src/aymara_ai/types/shared/file_reference.py index 0094e5a..5a479f4 100644 --- a/src/aymara_ai/types/shared/file_reference.py +++ b/src/aymara_ai/types/shared/file_reference.py @@ -8,6 +8,12 @@ class FileReference(BaseModel): + """Reference to a file, either by file_uuid (preferred) or legacy remote_file_path. + + When file_uuid is provided, the system will look up the File record and use its + remote_file_path. The remote_file_path field is maintained for backwards compatibility. + """ + file_uuid: Optional[str] = None remote_file_path: Optional[str] = None diff --git a/src/aymara_ai/types/shared/tool.py b/src/aymara_ai/types/shared/tool.py index f909e5f..1fa68bd 100644 --- a/src/aymara_ai/types/shared/tool.py +++ b/src/aymara_ai/types/shared/tool.py @@ -8,6 +8,8 @@ class Tool(BaseModel): + """Tools the agent has access to.""" + content: Union[str, object, None] = None name: str diff --git a/src/aymara_ai/types/shared/tool_array.py b/src/aymara_ai/types/shared/tool_array.py index d199a3d..34d6897 100644 --- a/src/aymara_ai/types/shared/tool_array.py +++ b/src/aymara_ai/types/shared/tool_array.py @@ -10,6 +10,8 @@ class ToolArray(BaseModel): + """Container for an array of tools""" + value: List[Tool] type: Optional[Literal["array"]] = None diff --git a/src/aymara_ai/types/shared/tool_dict.py b/src/aymara_ai/types/shared/tool_dict.py index a6d63ce..385cf3a 100644 --- a/src/aymara_ai/types/shared/tool_dict.py +++ b/src/aymara_ai/types/shared/tool_dict.py @@ -11,6 +11,8 @@ class ToolDict(BaseModel): + """Container for a tool dictionary""" + value: object type: Optional[Literal["dict"]] = None diff --git a/src/aymara_ai/types/shared/tool_string.py b/src/aymara_ai/types/shared/tool_string.py index a8d5fa4..6a5eead 100644 --- a/src/aymara_ai/types/shared/tool_string.py +++ b/src/aymara_ai/types/shared/tool_string.py @@ -9,6 +9,8 @@ class ToolString(BaseModel): + """Container for string-based tool instructions""" + value: str type: Optional[Literal["string"]] = None diff --git a/src/aymara_ai/types/shared/workflow_instructions.py b/src/aymara_ai/types/shared/workflow_instructions.py index 2e9ace1..827f2ae 100644 --- a/src/aymara_ai/types/shared/workflow_instructions.py +++ b/src/aymara_ai/types/shared/workflow_instructions.py @@ -9,5 +9,7 @@ class WorkflowInstructions(BaseModel): + """Workflow instructions for multi-agent workflows.""" + instructions: List[AgentInstructions] """List of agent instructions for the workflow. Must contain at least one agent.""" diff --git a/src/aymara_ai/types/shared_params/file_reference.py b/src/aymara_ai/types/shared_params/file_reference.py index b2e1ad6..10459f3 100644 --- a/src/aymara_ai/types/shared_params/file_reference.py +++ b/src/aymara_ai/types/shared_params/file_reference.py @@ -9,6 +9,12 @@ class FileReference(TypedDict, total=False): + """Reference to a file, either by file_uuid (preferred) or legacy remote_file_path. + + When file_uuid is provided, the system will look up the File record and use its + remote_file_path. The remote_file_path field is maintained for backwards compatibility. + """ + file_uuid: Optional[str] remote_file_path: Optional[str] diff --git a/src/aymara_ai/types/shared_params/tool.py b/src/aymara_ai/types/shared_params/tool.py index a24e18a..4841db8 100644 --- a/src/aymara_ai/types/shared_params/tool.py +++ b/src/aymara_ai/types/shared_params/tool.py @@ -9,6 +9,8 @@ class Tool(TypedDict, total=False): + """Tools the agent has access to.""" + content: Required[Union[str, object, None]] name: Required[str] diff --git a/src/aymara_ai/types/shared_params/tool_array.py b/src/aymara_ai/types/shared_params/tool_array.py index 2ba8f8f..75bbb78 100644 --- a/src/aymara_ai/types/shared_params/tool_array.py +++ b/src/aymara_ai/types/shared_params/tool_array.py @@ -11,6 +11,8 @@ class ToolArray(TypedDict, total=False): + """Container for an array of tools""" + value: Required[Iterable[Tool]] type: Literal["array"] diff --git a/src/aymara_ai/types/shared_params/tool_dict.py b/src/aymara_ai/types/shared_params/tool_dict.py index d7c26ea..bd1ef04 100644 --- a/src/aymara_ai/types/shared_params/tool_dict.py +++ b/src/aymara_ai/types/shared_params/tool_dict.py @@ -9,6 +9,8 @@ class ToolDictTyped(TypedDict, total=False): + """Container for a tool dictionary""" + value: Required[object] type: Literal["dict"] diff --git a/src/aymara_ai/types/shared_params/tool_string.py b/src/aymara_ai/types/shared_params/tool_string.py index 802f722..419e902 100644 --- a/src/aymara_ai/types/shared_params/tool_string.py +++ b/src/aymara_ai/types/shared_params/tool_string.py @@ -8,6 +8,8 @@ class ToolString(TypedDict, total=False): + """Container for string-based tool instructions""" + value: Required[str] type: Literal["string"] diff --git a/src/aymara_ai/types/shared_params/workflow_instructions.py b/src/aymara_ai/types/shared_params/workflow_instructions.py index 8363e6b..00c6f36 100644 --- a/src/aymara_ai/types/shared_params/workflow_instructions.py +++ b/src/aymara_ai/types/shared_params/workflow_instructions.py @@ -11,5 +11,7 @@ class WorkflowInstructions(TypedDict, total=False): + """Workflow instructions for multi-agent workflows.""" + instructions: Required[Iterable[AgentInstructions]] """List of agent instructions for the workflow. Must contain at least one agent.""" From 52bd065b84fdcfc2b95f05d0efbca9defc96e6e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:47:15 +0000 Subject: [PATCH 07/16] chore(internal): add missing files argument to base client --- src/aymara_ai/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/aymara_ai/_base_client.py b/src/aymara_ai/_base_client.py index a51ecc6..842159a 100644 --- a/src/aymara_ai/_base_client.py +++ b/src/aymara_ai/_base_client.py @@ -1249,9 +1249,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1769,9 +1772,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From 5f355404ac5548d014145b16aa05ac990815f83c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 04:37:17 +0000 Subject: [PATCH 08/16] chore: speedup initial import --- src/aymara_ai/_client.py | 269 +++++++++++++++++++++++++++++++-------- 1 file changed, 216 insertions(+), 53 deletions(-) diff --git a/src/aymara_ai/_client.py b/src/aymara_ai/_client.py index db959b5..df07216 100644 --- a/src/aymara_ai/_client.py +++ b/src/aymara_ai/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Dict, Mapping, cast +from typing import TYPE_CHECKING, Any, Dict, Mapping, cast from typing_extensions import Self, Literal, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library, maybe_coerce_boolean +from ._compat import cached_property from ._version import __version__ -from .resources import files, health, reports, eval_types from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import AymaraAIError, APIStatusError from ._base_client import ( @@ -29,7 +29,14 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.evals import evals + +if TYPE_CHECKING: + from .resources import evals, files, health, reports, eval_types + from .resources.files import FilesResource, AsyncFilesResource + from .resources.health import HealthResource, AsyncHealthResource + from .resources.reports import ReportsResource, AsyncReportsResource + from .resources.eval_types import EvalTypesResource, AsyncEvalTypesResource + from .resources.evals.evals import EvalsResource, AsyncEvalsResource __all__ = [ "ENVIRONMENTS", @@ -51,14 +58,6 @@ class AymaraAI(SyncAPIClient): - health: health.HealthResource - evals: evals.EvalsResource - eval_types: eval_types.EvalTypesResource - reports: reports.ReportsResource - files: files.FilesResource - with_raw_response: AymaraAIWithRawResponse - with_streaming_response: AymaraAIWithStreamedResponse - # client options api_key: str bearer_token: str | None @@ -152,13 +151,43 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.health = health.HealthResource(self) - self.evals = evals.EvalsResource(self) - self.eval_types = eval_types.EvalTypesResource(self) - self.reports = reports.ReportsResource(self) - self.files = files.FilesResource(self) - self.with_raw_response = AymaraAIWithRawResponse(self) - self.with_streaming_response = AymaraAIWithStreamedResponse(self) + @cached_property + def health(self) -> HealthResource: + from .resources.health import HealthResource + + return HealthResource(self) + + @cached_property + def evals(self) -> EvalsResource: + from .resources.evals import EvalsResource + + return EvalsResource(self) + + @cached_property + def eval_types(self) -> EvalTypesResource: + from .resources.eval_types import EvalTypesResource + + return EvalTypesResource(self) + + @cached_property + def reports(self) -> ReportsResource: + from .resources.reports import ReportsResource + + return ReportsResource(self) + + @cached_property + def files(self) -> FilesResource: + from .resources.files import FilesResource + + return FilesResource(self) + + @cached_property + def with_raw_response(self) -> AymaraAIWithRawResponse: + return AymaraAIWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AymaraAIWithStreamedResponse: + return AymaraAIWithStreamedResponse(self) @property @override @@ -284,14 +313,6 @@ def _make_status_error( class AsyncAymaraAI(AsyncAPIClient): - health: health.AsyncHealthResource - evals: evals.AsyncEvalsResource - eval_types: eval_types.AsyncEvalTypesResource - reports: reports.AsyncReportsResource - files: files.AsyncFilesResource - with_raw_response: AsyncAymaraAIWithRawResponse - with_streaming_response: AsyncAymaraAIWithStreamedResponse - # client options api_key: str bearer_token: str | None @@ -385,13 +406,43 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.health = health.AsyncHealthResource(self) - self.evals = evals.AsyncEvalsResource(self) - self.eval_types = eval_types.AsyncEvalTypesResource(self) - self.reports = reports.AsyncReportsResource(self) - self.files = files.AsyncFilesResource(self) - self.with_raw_response = AsyncAymaraAIWithRawResponse(self) - self.with_streaming_response = AsyncAymaraAIWithStreamedResponse(self) + @cached_property + def health(self) -> AsyncHealthResource: + from .resources.health import AsyncHealthResource + + return AsyncHealthResource(self) + + @cached_property + def evals(self) -> AsyncEvalsResource: + from .resources.evals import AsyncEvalsResource + + return AsyncEvalsResource(self) + + @cached_property + def eval_types(self) -> AsyncEvalTypesResource: + from .resources.eval_types import AsyncEvalTypesResource + + return AsyncEvalTypesResource(self) + + @cached_property + def reports(self) -> AsyncReportsResource: + from .resources.reports import AsyncReportsResource + + return AsyncReportsResource(self) + + @cached_property + def files(self) -> AsyncFilesResource: + from .resources.files import AsyncFilesResource + + return AsyncFilesResource(self) + + @cached_property + def with_raw_response(self) -> AsyncAymaraAIWithRawResponse: + return AsyncAymaraAIWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAymaraAIWithStreamedResponse: + return AsyncAymaraAIWithStreamedResponse(self) @property @override @@ -517,39 +568,151 @@ def _make_status_error( class AymaraAIWithRawResponse: + _client: AymaraAI + def __init__(self, client: AymaraAI) -> None: - self.health = health.HealthResourceWithRawResponse(client.health) - self.evals = evals.EvalsResourceWithRawResponse(client.evals) - self.eval_types = eval_types.EvalTypesResourceWithRawResponse(client.eval_types) - self.reports = reports.ReportsResourceWithRawResponse(client.reports) - self.files = files.FilesResourceWithRawResponse(client.files) + self._client = client + + @cached_property + def health(self) -> health.HealthResourceWithRawResponse: + from .resources.health import HealthResourceWithRawResponse + + return HealthResourceWithRawResponse(self._client.health) + + @cached_property + def evals(self) -> evals.EvalsResourceWithRawResponse: + from .resources.evals import EvalsResourceWithRawResponse + + return EvalsResourceWithRawResponse(self._client.evals) + + @cached_property + def eval_types(self) -> eval_types.EvalTypesResourceWithRawResponse: + from .resources.eval_types import EvalTypesResourceWithRawResponse + + return EvalTypesResourceWithRawResponse(self._client.eval_types) + + @cached_property + def reports(self) -> reports.ReportsResourceWithRawResponse: + from .resources.reports import ReportsResourceWithRawResponse + + return ReportsResourceWithRawResponse(self._client.reports) + + @cached_property + def files(self) -> files.FilesResourceWithRawResponse: + from .resources.files import FilesResourceWithRawResponse + + return FilesResourceWithRawResponse(self._client.files) class AsyncAymaraAIWithRawResponse: + _client: AsyncAymaraAI + def __init__(self, client: AsyncAymaraAI) -> None: - self.health = health.AsyncHealthResourceWithRawResponse(client.health) - self.evals = evals.AsyncEvalsResourceWithRawResponse(client.evals) - self.eval_types = eval_types.AsyncEvalTypesResourceWithRawResponse(client.eval_types) - self.reports = reports.AsyncReportsResourceWithRawResponse(client.reports) - self.files = files.AsyncFilesResourceWithRawResponse(client.files) + self._client = client + + @cached_property + def health(self) -> health.AsyncHealthResourceWithRawResponse: + from .resources.health import AsyncHealthResourceWithRawResponse + + return AsyncHealthResourceWithRawResponse(self._client.health) + + @cached_property + def evals(self) -> evals.AsyncEvalsResourceWithRawResponse: + from .resources.evals import AsyncEvalsResourceWithRawResponse + + return AsyncEvalsResourceWithRawResponse(self._client.evals) + + @cached_property + def eval_types(self) -> eval_types.AsyncEvalTypesResourceWithRawResponse: + from .resources.eval_types import AsyncEvalTypesResourceWithRawResponse + + return AsyncEvalTypesResourceWithRawResponse(self._client.eval_types) + + @cached_property + def reports(self) -> reports.AsyncReportsResourceWithRawResponse: + from .resources.reports import AsyncReportsResourceWithRawResponse + + return AsyncReportsResourceWithRawResponse(self._client.reports) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithRawResponse: + from .resources.files import AsyncFilesResourceWithRawResponse + + return AsyncFilesResourceWithRawResponse(self._client.files) class AymaraAIWithStreamedResponse: + _client: AymaraAI + def __init__(self, client: AymaraAI) -> None: - self.health = health.HealthResourceWithStreamingResponse(client.health) - self.evals = evals.EvalsResourceWithStreamingResponse(client.evals) - self.eval_types = eval_types.EvalTypesResourceWithStreamingResponse(client.eval_types) - self.reports = reports.ReportsResourceWithStreamingResponse(client.reports) - self.files = files.FilesResourceWithStreamingResponse(client.files) + self._client = client + + @cached_property + def health(self) -> health.HealthResourceWithStreamingResponse: + from .resources.health import HealthResourceWithStreamingResponse + + return HealthResourceWithStreamingResponse(self._client.health) + + @cached_property + def evals(self) -> evals.EvalsResourceWithStreamingResponse: + from .resources.evals import EvalsResourceWithStreamingResponse + + return EvalsResourceWithStreamingResponse(self._client.evals) + + @cached_property + def eval_types(self) -> eval_types.EvalTypesResourceWithStreamingResponse: + from .resources.eval_types import EvalTypesResourceWithStreamingResponse + + return EvalTypesResourceWithStreamingResponse(self._client.eval_types) + + @cached_property + def reports(self) -> reports.ReportsResourceWithStreamingResponse: + from .resources.reports import ReportsResourceWithStreamingResponse + + return ReportsResourceWithStreamingResponse(self._client.reports) + + @cached_property + def files(self) -> files.FilesResourceWithStreamingResponse: + from .resources.files import FilesResourceWithStreamingResponse + + return FilesResourceWithStreamingResponse(self._client.files) class AsyncAymaraAIWithStreamedResponse: + _client: AsyncAymaraAI + def __init__(self, client: AsyncAymaraAI) -> None: - self.health = health.AsyncHealthResourceWithStreamingResponse(client.health) - self.evals = evals.AsyncEvalsResourceWithStreamingResponse(client.evals) - self.eval_types = eval_types.AsyncEvalTypesResourceWithStreamingResponse(client.eval_types) - self.reports = reports.AsyncReportsResourceWithStreamingResponse(client.reports) - self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) + self._client = client + + @cached_property + def health(self) -> health.AsyncHealthResourceWithStreamingResponse: + from .resources.health import AsyncHealthResourceWithStreamingResponse + + return AsyncHealthResourceWithStreamingResponse(self._client.health) + + @cached_property + def evals(self) -> evals.AsyncEvalsResourceWithStreamingResponse: + from .resources.evals import AsyncEvalsResourceWithStreamingResponse + + return AsyncEvalsResourceWithStreamingResponse(self._client.evals) + + @cached_property + def eval_types(self) -> eval_types.AsyncEvalTypesResourceWithStreamingResponse: + from .resources.eval_types import AsyncEvalTypesResourceWithStreamingResponse + + return AsyncEvalTypesResourceWithStreamingResponse(self._client.eval_types) + + @cached_property + def reports(self) -> reports.AsyncReportsResourceWithStreamingResponse: + from .resources.reports import AsyncReportsResourceWithStreamingResponse + + return AsyncReportsResourceWithStreamingResponse(self._client.reports) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithStreamingResponse: + from .resources.files import AsyncFilesResourceWithStreamingResponse + + return AsyncFilesResourceWithStreamingResponse(self._client.files) Client = AymaraAI From 5a0914b737b8d86c6ca93e0a1212f5e0be49d557 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 05:05:47 +0000 Subject: [PATCH 09/16] fix: use async_to_httpx_files in patch method --- src/aymara_ai/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aymara_ai/_base_client.py b/src/aymara_ai/_base_client.py index 842159a..f625d98 100644 --- a/src/aymara_ai/_base_client.py +++ b/src/aymara_ai/_base_client.py @@ -1776,7 +1776,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From cff7b50849ac38e0fe46b0cb29bdb81363f88448 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 04:37:19 +0000 Subject: [PATCH 10/16] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index 3a25549..0519158 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import aymara_ai' From 0b87735ae80dd068a502b5f1b76ad2d6a540b2dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 05:34:53 +0000 Subject: [PATCH 11/16] chore(internal): codegen related update --- .github/workflows/ci.yml | 8 +- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- src/aymara_ai/_base_client.py | 145 +++++++++++++++++++-- src/aymara_ai/_models.py | 17 ++- src/aymara_ai/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++++- 7 files changed, 350 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39346dd..bea4c7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/aymara-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/aymara-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/aymara-sdk-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/aymara-sdk-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index e2e7714..1c3df9b 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 24512be..a5413e4 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'aymara-ai/aymara-sdk-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/src/aymara_ai/_base_client.py b/src/aymara_ai/_base_client.py index f625d98..87a74ac 100644 --- a/src/aymara_ai/_base_client.py +++ b/src/aymara_ai/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -479,8 +482,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -534,7 +548,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1196,6 +1216,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1208,6 +1229,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1221,6 +1243,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1233,13 +1256,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1249,11 +1284,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1263,11 +1310,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1277,9 +1336,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1719,6 +1788,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1731,6 +1801,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1744,6 +1815,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1756,13 +1828,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1772,11 +1856,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1786,11 +1887,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1800,9 +1913,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/aymara_ai/_models.py b/src/aymara_ai/_models.py index 0069014..61e7534 100644 --- a/src/aymara_ai/_models.py +++ b/src/aymara_ai/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -797,6 +810,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -815,6 +829,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/aymara_ai/_types.py b/src/aymara_ai/_types.py index c48b0a3..a81692d 100644 --- a/src/aymara_ai/_types.py +++ b/src/aymara_ai/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index 5df7521..7f1883b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: AymaraAI | AsyncAymaraAI) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -502,6 +555,70 @@ def test_multipart_repeating_array(self, client: AymaraAI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: AymaraAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with AymaraAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: AymaraAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: AymaraAI) -> None: class Model1(BaseModel): @@ -1341,6 +1458,72 @@ def test_multipart_repeating_array(self, async_client: AsyncAymaraAI) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncAymaraAI) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncAymaraAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncAymaraAI + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncAymaraAI) -> None: class Model1(BaseModel): From c6f487114cafde5c733e0896e85de7fe7a3be841 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:48:12 +0000 Subject: [PATCH 12/16] feat(client): add custom JSON encoder for extended type support --- src/aymara_ai/_base_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/aymara_ai/_base_client.py b/src/aymara_ai/_base_client.py index 87a74ac..b830a6c 100644 --- a/src/aymara_ai/_base_client.py +++ b/src/aymara_ai/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -556,8 +557,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) From 2d32cae6bb544bb055eb34f36debbc994d774fb3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:12:23 +0000 Subject: [PATCH 13/16] feat(api): api update --- .stats.yml | 4 ++-- src/aymara_ai/resources/reports.py | 22 +++++++++++++++++++-- src/aymara_ai/types/report_create_params.py | 6 ++++++ tests/api_resources/test_reports.py | 2 ++ 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index f739618..ce4d424 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/aymara%2Faymara-sdk-ad96d8d628e05902f87a1643da9fbf2d9ca10b7154c4a54598729afc32ee9b23.yml -openapi_spec_hash: e246e620f37ff63091957c9dfd2264eb +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/aymara%2Faymara-sdk-8fd97e27ffbd76766993860c0dc0fdca78306e9d8fdf5ba01c6542721cf35577.yml +openapi_spec_hash: 2ad7f387fe68851eaca42d23f37cf7dd config_hash: 890fa7ef8d5b8a0287741a83ed69ca36 diff --git a/src/aymara_ai/resources/reports.py b/src/aymara_ai/resources/reports.py index 431ac50..08a1af3 100644 --- a/src/aymara_ai/resources/reports.py +++ b/src/aymara_ai/resources/reports.py @@ -47,6 +47,7 @@ def create( *, eval_run_uuids: SequenceNotStr[str], workspace_uuid: str | Omit = omit, + proceed_with_missing_turns: bool | 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, @@ -60,6 +61,9 @@ def create( Args: eval_run_uuids: List of eval run UUIDs to include in the suite summary. + proceed_with_missing_turns: If True, proceed with report creation even if some turn score runs are missing + for multi-turn evals. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -70,7 +74,13 @@ def create( """ return self._post( "/v2/eval-reports", - body=maybe_transform({"eval_run_uuids": eval_run_uuids}, report_create_params.ReportCreateParams), + body=maybe_transform( + { + "eval_run_uuids": eval_run_uuids, + "proceed_with_missing_turns": proceed_with_missing_turns, + }, + report_create_params.ReportCreateParams, + ), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -229,6 +239,7 @@ async def create( *, eval_run_uuids: SequenceNotStr[str], workspace_uuid: str | Omit = omit, + proceed_with_missing_turns: bool | 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, @@ -242,6 +253,9 @@ async def create( Args: eval_run_uuids: List of eval run UUIDs to include in the suite summary. + proceed_with_missing_turns: If True, proceed with report creation even if some turn score runs are missing + for multi-turn evals. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -253,7 +267,11 @@ async def create( return await self._post( "/v2/eval-reports", body=await async_maybe_transform( - {"eval_run_uuids": eval_run_uuids}, report_create_params.ReportCreateParams + { + "eval_run_uuids": eval_run_uuids, + "proceed_with_missing_turns": proceed_with_missing_turns, + }, + report_create_params.ReportCreateParams, ), options=make_request_options( extra_headers=extra_headers, diff --git a/src/aymara_ai/types/report_create_params.py b/src/aymara_ai/types/report_create_params.py index c2e6e56..2adfeb6 100644 --- a/src/aymara_ai/types/report_create_params.py +++ b/src/aymara_ai/types/report_create_params.py @@ -14,3 +14,9 @@ class ReportCreateParams(TypedDict, total=False): """List of eval run UUIDs to include in the suite summary.""" workspace_uuid: str + + proceed_with_missing_turns: bool + """ + If True, proceed with report creation even if some turn score runs are missing + for multi-turn evals. + """ diff --git a/tests/api_resources/test_reports.py b/tests/api_resources/test_reports.py index d1eca41..1a1fde1 100644 --- a/tests/api_resources/test_reports.py +++ b/tests/api_resources/test_reports.py @@ -34,6 +34,7 @@ def test_method_create_with_all_params(self, client: AymaraAI) -> None: report = client.reports.create( eval_run_uuids=["string"], workspace_uuid="workspace_uuid", + proceed_with_missing_turns=True, ) assert_matches_type(EvalSuiteReport, report, path=["response"]) @@ -223,6 +224,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAymaraAI) report = await async_client.reports.create( eval_run_uuids=["string"], workspace_uuid="workspace_uuid", + proceed_with_missing_turns=True, ) assert_matches_type(EvalSuiteReport, report, path=["response"]) From e8b8e6b8d7d3b126e742fa58123135d7ec16c993 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:39:57 +0000 Subject: [PATCH 14/16] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ce4d424..ea8cce9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/aymara%2Faymara-sdk-8fd97e27ffbd76766993860c0dc0fdca78306e9d8fdf5ba01c6542721cf35577.yml openapi_spec_hash: 2ad7f387fe68851eaca42d23f37cf7dd -config_hash: 890fa7ef8d5b8a0287741a83ed69ca36 +config_hash: 70b44eeeb96fe35337f04be42a838d95 From 0aaee71be6fc7478f60818842f521f468f768a12 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:29:22 +0000 Subject: [PATCH 15/16] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index ea8cce9..ce4d424 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 31 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/aymara%2Faymara-sdk-8fd97e27ffbd76766993860c0dc0fdca78306e9d8fdf5ba01c6542721cf35577.yml openapi_spec_hash: 2ad7f387fe68851eaca42d23f37cf7dd -config_hash: 70b44eeeb96fe35337f04be42a838d95 +config_hash: 890fa7ef8d5b8a0287741a83ed69ca36 From 3ae88e36c3bd6ccb6ace42aebc5dc349f0e1c5f1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:48:53 +0000 Subject: [PATCH 16/16] release: 1.3.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/aymara_ai/_version.py | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d43a621..2a8f4ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.1" + ".": "1.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 124c11f..009ac39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 1.3.0 (2026-02-09) + +Full Changelog: [v1.2.1...v1.3.0](https://github.com/aymara-ai/aymara-sdk-python/compare/v1.2.1...v1.3.0) + +### Features + +* **api:** api update ([2d32cae](https://github.com/aymara-ai/aymara-sdk-python/commit/2d32cae6bb544bb055eb34f36debbc994d774fb3)) +* **api:** api update ([35c60e5](https://github.com/aymara-ai/aymara-sdk-python/commit/35c60e580328fc794dd60d16dd3a8fc2b376b39d)) +* **client:** add custom JSON encoder for extended type support ([c6f4871](https://github.com/aymara-ai/aymara-sdk-python/commit/c6f487114cafde5c733e0896e85de7fe7a3be841)) + + +### Bug Fixes + +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([3c302b2](https://github.com/aymara-ai/aymara-sdk-python/commit/3c302b2a665b0b3afdad9b6a7ef1efd59ee5c03a)) +* use async_to_httpx_files in patch method ([5a0914b](https://github.com/aymara-ai/aymara-sdk-python/commit/5a0914b737b8d86c6ca93e0a1212f5e0be49d557)) + + +### Chores + +* add missing docstrings ([69dedfa](https://github.com/aymara-ai/aymara-sdk-python/commit/69dedfa3958e39146b28a7277eec90a29c1e22fb)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([95a29b7](https://github.com/aymara-ai/aymara-sdk-python/commit/95a29b7bd6bb5e476017675ec6132cd503c651b5)) +* **docs:** use environment variables for authentication in code snippets ([1781507](https://github.com/aymara-ai/aymara-sdk-python/commit/1781507d6f5da5943777d86a75ddfa2e5260a99a)) +* **internal:** add `--fix` argument to lint script ([cff7b50](https://github.com/aymara-ai/aymara-sdk-python/commit/cff7b50849ac38e0fe46b0cb29bdb81363f88448)) +* **internal:** add missing files argument to base client ([52bd065](https://github.com/aymara-ai/aymara-sdk-python/commit/52bd065b84fdcfc2b95f05d0efbca9defc96e6e5)) +* **internal:** codegen related update ([0b87735](https://github.com/aymara-ai/aymara-sdk-python/commit/0b87735ae80dd068a502b5f1b76ad2d6a540b2dd)) +* speedup initial import ([5f35540](https://github.com/aymara-ai/aymara-sdk-python/commit/5f355404ac5548d014145b16aa05ac990815f83c)) +* update lockfile ([aeaae87](https://github.com/aymara-ai/aymara-sdk-python/commit/aeaae87e004d4971c0b03a7504bac9a1803b679e)) + ## 1.2.0 (2025-12-04) Full Changelog: [v1.1.1...v1.2.0](https://github.com/aymara-ai/aymara-sdk-python/compare/v1.1.1...v1.2.0) diff --git a/pyproject.toml b/pyproject.toml index d91bc70..b2da303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "aymara-ai-sdk" -version = "1.2.1" +version = "1.3.0" description = "The official Python library for the aymara-ai API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/aymara_ai/_version.py b/src/aymara_ai/_version.py index 12fd5d5..c094d79 100644 --- a/src/aymara_ai/_version.py +++ b/src/aymara_ai/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "aymara_ai" -__version__ = "1.2.1" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version