From c57fc0ea34d7faeeae48d3f40128d5dbc4c8d64f Mon Sep 17 00:00:00 2001 From: stainless-bot Date: Sun, 13 Oct 2024 23:23:19 +0000 Subject: [PATCH] feat(api): api update --- .stats.yml | 2 +- README.md | 68 +++- api.md | 24 +- src/arcadepy/_client.py | 45 +-- src/arcadepy/resources/auth.py | 50 +-- src/arcadepy/resources/chat.py | 27 +- src/arcadepy/resources/health.py | 20 +- src/arcadepy/resources/tools.py | 222 +++++++------ src/arcadepy/types/__init__.py | 13 +- src/arcadepy/types/auth_authorize_params.py | 26 ++ src/arcadepy/types/authorization_response.py | 25 ++ src/arcadepy/types/chat_completions_params.py | 44 +-- src/arcadepy/types/chat_message.py | 42 +++ src/arcadepy/types/chat_message_param.py | 42 +++ src/arcadepy/types/chat_response.py | 40 +-- src/arcadepy/types/shared/__init__.py | 2 +- src/arcadepy/types/shared/error.py | 13 + src/arcadepy/types/tool_definition.py | 87 ++++++ src/arcadepy/types/tool_response.py | 43 +++ .../types/tool_retrieve_definition_params.py | 15 + tests/api_resources/test_auth.py | 34 +- tests/api_resources/test_chat.py | 4 +- tests/api_resources/test_health.py | 24 +- tests/api_resources/test_tools.py | 117 +++---- tests/conftest.py | 6 +- tests/test_client.py | 295 +++++++++++++++--- 26 files changed, 924 insertions(+), 406 deletions(-) create mode 100644 src/arcadepy/types/auth_authorize_params.py create mode 100644 src/arcadepy/types/authorization_response.py create mode 100644 src/arcadepy/types/chat_message.py create mode 100644 src/arcadepy/types/chat_message_param.py create mode 100644 src/arcadepy/types/shared/error.py create mode 100644 src/arcadepy/types/tool_definition.py create mode 100644 src/arcadepy/types/tool_response.py create mode 100644 src/arcadepy/types/tool_retrieve_definition_params.py diff --git a/.stats.yml b/.stats.yml index 0d10bdc1..c9c0ed68 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,2 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-5d0034cce7bb0c697f3ec08ab4d585002898a26e9e0e03f42ae2515f1cc56087.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/arcade-ai%2Farcade-engine-2f4d672c34ee530fb7290e2fb799d907aba7d9e47030659422f4e7760625be90.yml diff --git a/README.md b/README.md index 1b7aecf1..92047a12 100644 --- a/README.md +++ b/README.md @@ -15,29 +15,35 @@ The REST API documentation can be found on [arcade-ai.com](https://arcade-ai.com ## Installation ```sh -# install from PyPI -pip install --pre arcadepy +# install from the production repo +pip install git+ssh://git@github.com/ArcadeAI/arcade-py.git ``` +> [!NOTE] +> Once this package is [published to PyPI](https://app.stainlessapi.com/docs/guides/publish), this will become: `pip install --pre arcadepy` + ## Usage The full API of this library can be found in [api.md](api.md). ```python +import os from arcadepy import ArcadeAI client = ArcadeAI( + # This is the default and can be omitted + api_key=os.environ.get("ARCADE_API_KEY"), # defaults to "production". environment="staging", ) -response = client.tools.execute( +tool_response = client.tools.execute( inputs="[object Object]", tool_name="Google.ListEmails", tool_version="0.1.0", - user_id="dev@arcade-ai.com", + user_id="user@example.com", ) -print(response.invocation_id) +print(tool_response.invocation_id) ``` While you can provide an `api_key` keyword argument, @@ -50,23 +56,26 @@ so that your API Key is not stored in source control. Simply import `AsyncArcadeAI` instead of `ArcadeAI` and use `await` with each API call: ```python +import os import asyncio from arcadepy import AsyncArcadeAI client = AsyncArcadeAI( + # This is the default and can be omitted + api_key=os.environ.get("ARCADE_API_KEY"), # defaults to "production". environment="staging", ) async def main() -> None: - response = await client.tools.execute( + tool_response = await client.tools.execute( inputs="[object Object]", tool_name="Google.ListEmails", tool_version="0.1.0", - user_id="dev@arcade-ai.com", + user_id="user@example.com", ) - print(response.invocation_id) + print(tool_response.invocation_id) asyncio.run(main()) @@ -99,7 +108,14 @@ from arcadepy import ArcadeAI client = ArcadeAI() try: - client.chat.completions() + client.chat.completions( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ], + ) except arcadepy.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. @@ -142,7 +158,14 @@ client = ArcadeAI( ) # Or, configure per-request: -client.with_options(max_retries=5).chat.completions() +client.with_options(max_retries=5).chat.completions( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ], +) ``` ### Timeouts @@ -165,7 +188,14 @@ client = ArcadeAI( ) # Override per-request: -client.with_options(timeout=5.0).chat.completions() +client.with_options(timeout=5.0).chat.completions( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ], +) ``` On timeout, an `APITimeoutError` is thrown. @@ -204,7 +234,12 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from arcadepy import ArcadeAI client = ArcadeAI() -response = client.chat.with_raw_response.completions() +response = client.chat.with_raw_response.completions( + messages=[{ + "role": "user", + "content": "Hello, how can I use Arcade AI?", + }], +) print(response.headers.get('X-My-Header')) chat = response.parse() # get the object that `chat.completions()` would have returned @@ -222,7 +257,14 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -with client.chat.with_streaming_response.completions() as response: +with client.chat.with_streaming_response.completions( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ], +) as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): diff --git a/api.md b/api.md index 332567af..35d431bc 100644 --- a/api.md +++ b/api.md @@ -1,22 +1,28 @@ # Shared Types ```python -from arcadepy.types import AuthorizationResponse +from arcadepy.types import Error ``` # Auth +Types: + +```python +from arcadepy.types import AuthorizationResponse +``` + Methods: -- client.auth.authorization(\*\*params) -> AuthorizationResponse -- client.auth.status(\*\*params) -> AuthorizationResponse +- client.auth.authorize(\*\*params) -> AuthorizationResponse +- client.auth.status(\*\*params) -> AuthorizationResponse # Chat Types: ```python -from arcadepy.types import ChatResponse +from arcadepy.types import ChatMessage, ChatRequest, ChatResponse ``` Methods: @@ -33,18 +39,18 @@ from arcadepy.types import HealthSchema Methods: -- client.health.list() -> HealthSchema +- client.health.check() -> HealthSchema # Tools Types: ```python -from arcadepy.types import Definition, Response +from arcadepy.types import AuthorizeToolRequest, ExecuteToolRequest, ToolDefinition, ToolResponse ``` Methods: -- client.tools.retrieve(\*\*params) -> Definition -- client.tools.authorize(\*\*params) -> AuthorizationResponse -- client.tools.execute(\*\*params) -> Response +- client.tools.authorize(\*\*params) -> AuthorizationResponse +- client.tools.execute(\*\*params) -> ToolResponse +- client.tools.retrieve_definition(\*\*params) -> ToolDefinition diff --git a/src/arcadepy/_client.py b/src/arcadepy/_client.py index aa263571..41b0073b 100644 --- a/src/arcadepy/_client.py +++ b/src/arcadepy/_client.py @@ -13,7 +13,6 @@ from ._types import ( NOT_GIVEN, Omit, - Headers, Timeout, NotGiven, Transport, @@ -26,7 +25,7 @@ ) from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream -from ._exceptions import APIStatusError +from ._exceptions import ArcadeAIError, APIStatusError from ._base_client import ( DEFAULT_MAX_RETRIES, SyncAPIClient, @@ -61,7 +60,7 @@ class ArcadeAI(SyncAPIClient): with_streaming_response: ArcadeAIWithStreamedResponse # client options - api_key: str | None + api_key: str _environment: Literal["production", "staging"] | NotGiven @@ -95,6 +94,10 @@ def __init__( """ if api_key is None: api_key = os.environ.get("ARCADE_API_KEY") + if api_key is None: + raise ArcadeAIError( + "The api_key client option must be set either by passing api_key to the client or by setting the ARCADE_API_KEY environment variable" + ) self.api_key = api_key self._environment = environment @@ -134,6 +137,8 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self._idempotency_header = "Idempotency-Key" + self.auth = resources.AuthResource(self) self.chat = resources.ChatResource(self) self.health = resources.HealthResource(self) @@ -150,8 +155,6 @@ def qs(self) -> Querystring: @override def auth_headers(self) -> dict[str, str]: api_key = self.api_key - if api_key is None: - return {} return {"Authorization": api_key} @property @@ -163,17 +166,6 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } - @override - def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): - return - - raise TypeError( - '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' - ) - def copy( self, *, @@ -270,7 +262,7 @@ class AsyncArcadeAI(AsyncAPIClient): with_streaming_response: AsyncArcadeAIWithStreamedResponse # client options - api_key: str | None + api_key: str _environment: Literal["production", "staging"] | NotGiven @@ -304,6 +296,10 @@ def __init__( """ if api_key is None: api_key = os.environ.get("ARCADE_API_KEY") + if api_key is None: + raise ArcadeAIError( + "The api_key client option must be set either by passing api_key to the client or by setting the ARCADE_API_KEY environment variable" + ) self.api_key = api_key self._environment = environment @@ -343,6 +339,8 @@ def __init__( _strict_response_validation=_strict_response_validation, ) + self._idempotency_header = "Idempotency-Key" + self.auth = resources.AsyncAuthResource(self) self.chat = resources.AsyncChatResource(self) self.health = resources.AsyncHealthResource(self) @@ -359,8 +357,6 @@ def qs(self) -> Querystring: @override def auth_headers(self) -> dict[str, str]: api_key = self.api_key - if api_key is None: - return {} return {"Authorization": api_key} @property @@ -372,17 +368,6 @@ def default_headers(self) -> dict[str, str | Omit]: **self._custom_headers, } - @override - def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.api_key and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): - return - - raise TypeError( - '"Could not resolve authentication method. Expected the api_key to be set. Or for the `Authorization` headers to be explicitly omitted"' - ) - def copy( self, *, diff --git a/src/arcadepy/resources/auth.py b/src/arcadepy/resources/auth.py index 54783973..920786cc 100644 --- a/src/arcadepy/resources/auth.py +++ b/src/arcadepy/resources/auth.py @@ -4,7 +4,7 @@ import httpx -from ..types import auth_status_params, auth_authorization_params +from ..types import auth_status_params, auth_authorize_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( maybe_transform, @@ -19,7 +19,7 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.shared.authorization_response import AuthorizationResponse +from ..types.authorization_response import AuthorizationResponse __all__ = ["AuthResource", "AsyncAuthResource"] @@ -44,10 +44,10 @@ def with_streaming_response(self) -> AuthResourceWithStreamingResponse: """ return AuthResourceWithStreamingResponse(self) - def authorization( + def authorize( self, *, - auth_requirement: auth_authorization_params.AuthRequirement, + auth_requirement: auth_authorize_params.AuthRequirement, 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. @@ -55,6 +55,7 @@ def authorization( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + idempotency_key: str | None = None, ) -> AuthorizationResponse: """ Starts the authorization process for given authorization requirements @@ -67,6 +68,8 @@ def authorization( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return self._post( "/v1/auth/authorize", @@ -75,10 +78,14 @@ def authorization( "auth_requirement": auth_requirement, "user_id": user_id, }, - auth_authorization_params.AuthAuthorizationParams, + auth_authorize_params.AuthAuthorizeParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), cast_to=AuthorizationResponse, ) @@ -150,10 +157,10 @@ def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: """ return AsyncAuthResourceWithStreamingResponse(self) - async def authorization( + async def authorize( self, *, - auth_requirement: auth_authorization_params.AuthRequirement, + auth_requirement: auth_authorize_params.AuthRequirement, 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. @@ -161,6 +168,7 @@ async def authorization( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + idempotency_key: str | None = None, ) -> AuthorizationResponse: """ Starts the authorization process for given authorization requirements @@ -173,6 +181,8 @@ async def authorization( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return await self._post( "/v1/auth/authorize", @@ -181,10 +191,14 @@ async def authorization( "auth_requirement": auth_requirement, "user_id": user_id, }, - auth_authorization_params.AuthAuthorizationParams, + auth_authorize_params.AuthAuthorizeParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), cast_to=AuthorizationResponse, ) @@ -240,8 +254,8 @@ class AuthResourceWithRawResponse: def __init__(self, auth: AuthResource) -> None: self._auth = auth - self.authorization = to_raw_response_wrapper( - auth.authorization, + self.authorize = to_raw_response_wrapper( + auth.authorize, ) self.status = to_raw_response_wrapper( auth.status, @@ -252,8 +266,8 @@ class AsyncAuthResourceWithRawResponse: def __init__(self, auth: AsyncAuthResource) -> None: self._auth = auth - self.authorization = async_to_raw_response_wrapper( - auth.authorization, + self.authorize = async_to_raw_response_wrapper( + auth.authorize, ) self.status = async_to_raw_response_wrapper( auth.status, @@ -264,8 +278,8 @@ class AuthResourceWithStreamingResponse: def __init__(self, auth: AuthResource) -> None: self._auth = auth - self.authorization = to_streamed_response_wrapper( - auth.authorization, + self.authorize = to_streamed_response_wrapper( + auth.authorize, ) self.status = to_streamed_response_wrapper( auth.status, @@ -276,8 +290,8 @@ class AsyncAuthResourceWithStreamingResponse: def __init__(self, auth: AsyncAuthResource) -> None: self._auth = auth - self.authorization = async_to_streamed_response_wrapper( - auth.authorization, + self.authorize = async_to_streamed_response_wrapper( + auth.authorize, ) self.status = async_to_streamed_response_wrapper( auth.status, diff --git a/src/arcadepy/resources/chat.py b/src/arcadepy/resources/chat.py index fffa5c54..6901ff2a 100644 --- a/src/arcadepy/resources/chat.py +++ b/src/arcadepy/resources/chat.py @@ -23,6 +23,7 @@ ) from .._base_client import make_request_options from ..types.chat_response import ChatResponse +from ..types.chat_message_param import ChatMessageParam __all__ = ["ChatResource", "AsyncChatResource"] @@ -54,10 +55,10 @@ def completions( logit_bias: Dict[str, int] | NotGiven = NOT_GIVEN, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, - messages: Iterable[chat_completions_params.Message] | NotGiven = NOT_GIVEN, + messages: Iterable[ChatMessageParam] | NotGiven = NOT_GIVEN, model: str | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, - parallel_tool_calls: object | NotGiven = NOT_GIVEN, + parallel_tool_calls: bool | NotGiven = NOT_GIVEN, presence_penalty: int | NotGiven = NOT_GIVEN, response_format: Literal["json_object", "text"] | NotGiven = NOT_GIVEN, seed: int | NotGiven = NOT_GIVEN, @@ -76,6 +77,7 @@ def completions( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + idempotency_key: str | None = None, ) -> ChatResponse: """ Talk to different LLM Chat APIs via OpenAI's API @@ -108,6 +110,8 @@ def completions( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return self._post( "/v1/chat/completions", @@ -137,7 +141,11 @@ def completions( chat_completions_params.ChatCompletionsParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), cast_to=ChatResponse, ) @@ -170,10 +178,10 @@ async def completions( logit_bias: Dict[str, int] | NotGiven = NOT_GIVEN, logprobs: bool | NotGiven = NOT_GIVEN, max_tokens: int | NotGiven = NOT_GIVEN, - messages: Iterable[chat_completions_params.Message] | NotGiven = NOT_GIVEN, + messages: Iterable[ChatMessageParam] | NotGiven = NOT_GIVEN, model: str | NotGiven = NOT_GIVEN, n: int | NotGiven = NOT_GIVEN, - parallel_tool_calls: object | NotGiven = NOT_GIVEN, + parallel_tool_calls: bool | NotGiven = NOT_GIVEN, presence_penalty: int | NotGiven = NOT_GIVEN, response_format: Literal["json_object", "text"] | NotGiven = NOT_GIVEN, seed: int | NotGiven = NOT_GIVEN, @@ -192,6 +200,7 @@ async def completions( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + idempotency_key: str | None = None, ) -> ChatResponse: """ Talk to different LLM Chat APIs via OpenAI's API @@ -224,6 +233,8 @@ async def completions( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return await self._post( "/v1/chat/completions", @@ -253,7 +264,11 @@ async def completions( chat_completions_params.ChatCompletionsParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), cast_to=ChatResponse, ) diff --git a/src/arcadepy/resources/health.py b/src/arcadepy/resources/health.py index ec6d66ab..bc67cfb4 100644 --- a/src/arcadepy/resources/health.py +++ b/src/arcadepy/resources/health.py @@ -39,7 +39,7 @@ def with_streaming_response(self) -> HealthResourceWithStreamingResponse: """ return HealthResourceWithStreamingResponse(self) - def list( + def check( self, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -79,7 +79,7 @@ def with_streaming_response(self) -> AsyncHealthResourceWithStreamingResponse: """ return AsyncHealthResourceWithStreamingResponse(self) - async def list( + async def check( self, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -103,8 +103,8 @@ class HealthResourceWithRawResponse: def __init__(self, health: HealthResource) -> None: self._health = health - self.list = to_raw_response_wrapper( - health.list, + self.check = to_raw_response_wrapper( + health.check, ) @@ -112,8 +112,8 @@ class AsyncHealthResourceWithRawResponse: def __init__(self, health: AsyncHealthResource) -> None: self._health = health - self.list = async_to_raw_response_wrapper( - health.list, + self.check = async_to_raw_response_wrapper( + health.check, ) @@ -121,8 +121,8 @@ class HealthResourceWithStreamingResponse: def __init__(self, health: HealthResource) -> None: self._health = health - self.list = to_streamed_response_wrapper( - health.list, + self.check = to_streamed_response_wrapper( + health.check, ) @@ -130,6 +130,6 @@ class AsyncHealthResourceWithStreamingResponse: def __init__(self, health: AsyncHealthResource) -> None: self._health = health - self.list = async_to_streamed_response_wrapper( - health.list, + self.check = async_to_streamed_response_wrapper( + health.check, ) diff --git a/src/arcadepy/resources/tools.py b/src/arcadepy/resources/tools.py index 290f4976..2d904456 100644 --- a/src/arcadepy/resources/tools.py +++ b/src/arcadepy/resources/tools.py @@ -4,7 +4,7 @@ import httpx -from ..types import tool_execute_params, tool_retrieve_params, tool_authorize_params +from ..types import tool_execute_params, tool_authorize_params, tool_retrieve_definition_params from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven from .._utils import ( maybe_transform, @@ -19,9 +19,9 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.response import Response -from ..types.definition import Definition -from ..types.shared.authorization_response import AuthorizationResponse +from ..types.tool_response import ToolResponse +from ..types.tool_definition import ToolDefinition +from ..types.authorization_response import AuthorizationResponse __all__ = ["ToolsResource", "AsyncToolsResource"] @@ -46,52 +46,6 @@ def with_streaming_response(self) -> ToolsResourceWithStreamingResponse: """ return ToolsResourceWithStreamingResponse(self) - def retrieve( - self, - *, - director_id: str, - tool_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, - ) -> Definition: - """ - Returns the arcade tool specification for a specific tool - - Args: - director_id: Director ID - - tool_id: Tool ID - - 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._get( - "/v1/tools/definition", - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - { - "director_id": director_id, - "tool_id": tool_id, - }, - tool_retrieve_params.ToolRetrieveParams, - ), - ), - cast_to=Definition, - ) - def authorize( self, *, @@ -104,6 +58,7 @@ def authorize( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + idempotency_key: str | None = None, ) -> AuthorizationResponse: """ Authorizes a user for a specific tool by name @@ -118,6 +73,8 @@ def authorize( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return self._post( "/v1/tools/authorize", @@ -130,7 +87,11 @@ def authorize( tool_authorize_params.ToolAuthorizeParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), cast_to=AuthorizationResponse, ) @@ -148,7 +109,8 @@ def execute( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Response: + idempotency_key: str | None = None, + ) -> ToolResponse: """ Executes a tool by name and arguments @@ -162,6 +124,8 @@ def execute( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return self._post( "/v1/tools/execute", @@ -175,33 +139,16 @@ def execute( tool_execute_params.ToolExecuteParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), - cast_to=Response, + cast_to=ToolResponse, ) - -class AsyncToolsResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncToolsResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return the - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/ArcadeAI/arcade-py#accessing-raw-response-data-eg-headers - """ - return AsyncToolsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/ArcadeAI/arcade-py#with_streaming_response - """ - return AsyncToolsResourceWithStreamingResponse(self) - - async def retrieve( + def retrieve_definition( self, *, director_id: str, @@ -212,7 +159,7 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Definition: + ) -> ToolDefinition: """ Returns the arcade tool specification for a specific tool @@ -229,24 +176,45 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._get( + return self._get( "/v1/tools/definition", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout, - query=await async_maybe_transform( + query=maybe_transform( { "director_id": director_id, "tool_id": tool_id, }, - tool_retrieve_params.ToolRetrieveParams, + tool_retrieve_definition_params.ToolRetrieveDefinitionParams, ), ), - cast_to=Definition, + cast_to=ToolDefinition, ) + +class AsyncToolsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncToolsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return the + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/ArcadeAI/arcade-py#accessing-raw-response-data-eg-headers + """ + return AsyncToolsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncToolsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/ArcadeAI/arcade-py#with_streaming_response + """ + return AsyncToolsResourceWithStreamingResponse(self) + async def authorize( self, *, @@ -259,6 +227,7 @@ async def authorize( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + idempotency_key: str | None = None, ) -> AuthorizationResponse: """ Authorizes a user for a specific tool by name @@ -273,6 +242,8 @@ async def authorize( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return await self._post( "/v1/tools/authorize", @@ -285,7 +256,11 @@ async def authorize( tool_authorize_params.ToolAuthorizeParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), cast_to=AuthorizationResponse, ) @@ -303,7 +278,8 @@ async def execute( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, - ) -> Response: + idempotency_key: str | None = None, + ) -> ToolResponse: """ Executes a tool by name and arguments @@ -317,6 +293,8 @@ async def execute( extra_body: Add additional JSON properties to the request timeout: Override the client-level default timeout for this request, in seconds + + idempotency_key: Specify a custom idempotency key for this request """ return await self._post( "/v1/tools/execute", @@ -330,9 +308,59 @@ async def execute( tool_execute_params.ToolExecuteParams, ), options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + idempotency_key=idempotency_key, ), - cast_to=Response, + cast_to=ToolResponse, + ) + + async def retrieve_definition( + self, + *, + director_id: str, + tool_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, + ) -> ToolDefinition: + """ + Returns the arcade tool specification for a specific tool + + Args: + director_id: Director ID + + tool_id: Tool ID + + 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._get( + "/v1/tools/definition", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "director_id": director_id, + "tool_id": tool_id, + }, + tool_retrieve_definition_params.ToolRetrieveDefinitionParams, + ), + ), + cast_to=ToolDefinition, ) @@ -340,57 +368,57 @@ class ToolsResourceWithRawResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools - self.retrieve = to_raw_response_wrapper( - tools.retrieve, - ) self.authorize = to_raw_response_wrapper( tools.authorize, ) self.execute = to_raw_response_wrapper( tools.execute, ) + self.retrieve_definition = to_raw_response_wrapper( + tools.retrieve_definition, + ) class AsyncToolsResourceWithRawResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools - self.retrieve = async_to_raw_response_wrapper( - tools.retrieve, - ) self.authorize = async_to_raw_response_wrapper( tools.authorize, ) self.execute = async_to_raw_response_wrapper( tools.execute, ) + self.retrieve_definition = async_to_raw_response_wrapper( + tools.retrieve_definition, + ) class ToolsResourceWithStreamingResponse: def __init__(self, tools: ToolsResource) -> None: self._tools = tools - self.retrieve = to_streamed_response_wrapper( - tools.retrieve, - ) self.authorize = to_streamed_response_wrapper( tools.authorize, ) self.execute = to_streamed_response_wrapper( tools.execute, ) + self.retrieve_definition = to_streamed_response_wrapper( + tools.retrieve_definition, + ) class AsyncToolsResourceWithStreamingResponse: def __init__(self, tools: AsyncToolsResource) -> None: self._tools = tools - self.retrieve = async_to_streamed_response_wrapper( - tools.retrieve, - ) self.authorize = async_to_streamed_response_wrapper( tools.authorize, ) self.execute = async_to_streamed_response_wrapper( tools.execute, ) + self.retrieve_definition = async_to_streamed_response_wrapper( + tools.retrieve_definition, + ) diff --git a/src/arcadepy/types/__init__.py b/src/arcadepy/types/__init__.py index 708af8d8..3ba9dbf0 100644 --- a/src/arcadepy/types/__init__.py +++ b/src/arcadepy/types/__init__.py @@ -2,14 +2,17 @@ from __future__ import annotations -from .shared import AuthorizationResponse as AuthorizationResponse -from .response import Response as Response -from .definition import Definition as Definition +from .shared import Error as Error +from .chat_message import ChatMessage as ChatMessage from .chat_response import ChatResponse as ChatResponse from .health_schema import HealthSchema as HealthSchema +from .tool_response import ToolResponse as ToolResponse +from .tool_definition import ToolDefinition as ToolDefinition from .auth_status_params import AuthStatusParams as AuthStatusParams +from .chat_message_param import ChatMessageParam as ChatMessageParam from .tool_execute_params import ToolExecuteParams as ToolExecuteParams -from .tool_retrieve_params import ToolRetrieveParams as ToolRetrieveParams +from .auth_authorize_params import AuthAuthorizeParams as AuthAuthorizeParams from .tool_authorize_params import ToolAuthorizeParams as ToolAuthorizeParams +from .authorization_response import AuthorizationResponse as AuthorizationResponse from .chat_completions_params import ChatCompletionsParams as ChatCompletionsParams -from .auth_authorization_params import AuthAuthorizationParams as AuthAuthorizationParams +from .tool_retrieve_definition_params import ToolRetrieveDefinitionParams as ToolRetrieveDefinitionParams diff --git a/src/arcadepy/types/auth_authorize_params.py b/src/arcadepy/types/auth_authorize_params.py new file mode 100644 index 00000000..d8da3d82 --- /dev/null +++ b/src/arcadepy/types/auth_authorize_params.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Required, TypedDict + +__all__ = ["AuthAuthorizeParams", "AuthRequirement", "AuthRequirementOauth2"] + + +class AuthAuthorizeParams(TypedDict, total=False): + auth_requirement: Required[AuthRequirement] + + user_id: Required[str] + + +class AuthRequirementOauth2(TypedDict, total=False): + authority: str + + scopes: List[str] + + +class AuthRequirement(TypedDict, total=False): + provider: Required[str] + + oauth2: AuthRequirementOauth2 diff --git a/src/arcadepy/types/authorization_response.py b/src/arcadepy/types/authorization_response.py new file mode 100644 index 00000000..43cb1c3e --- /dev/null +++ b/src/arcadepy/types/authorization_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["AuthorizationResponse", "Context"] + + +class Context(BaseModel): + token: Optional[str] = None + + +class AuthorizationResponse(BaseModel): + authorization_id: Optional[str] = FieldInfo(alias="authorizationID", default=None) + + authorization_url: Optional[str] = FieldInfo(alias="authorizationURL", default=None) + + context: Optional[Context] = None + + scopes: Optional[List[str]] = None + + status: Optional[str] = None diff --git a/src/arcadepy/types/chat_completions_params.py b/src/arcadepy/types/chat_completions_params.py index 6b388e59..c0893582 100644 --- a/src/arcadepy/types/chat_completions_params.py +++ b/src/arcadepy/types/chat_completions_params.py @@ -3,9 +3,11 @@ from __future__ import annotations from typing import Dict, List, Iterable -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Literal, TypedDict -__all__ = ["ChatCompletionsParams", "Message", "MessageToolCall", "MessageToolCallFunction", "StreamOptions"] +from .chat_message_param import ChatMessageParam + +__all__ = ["ChatCompletionsParams", "StreamOptions"] class ChatCompletionsParams(TypedDict, total=False): @@ -29,13 +31,13 @@ class ChatCompletionsParams(TypedDict, total=False): max_tokens: int - messages: Iterable[Message] + messages: Iterable[ChatMessageParam] model: str n: int - parallel_tool_calls: object + parallel_tool_calls: bool """Disable the default behavior of parallel tool calls by setting it: false.""" presence_penalty: int @@ -70,40 +72,6 @@ class ChatCompletionsParams(TypedDict, total=False): user: str -class MessageToolCallFunction(TypedDict, total=False): - arguments: str - - name: str - - -class MessageToolCall(TypedDict, total=False): - id: str - - function: MessageToolCallFunction - - type: Literal["function"] - - -class Message(TypedDict, total=False): - content: Required[str] - """The content of the message.""" - - role: Required[str] - """The role of the author of this message. - - One of system, user, tool, or assistant. - """ - - name: str - """tool Name""" - - tool_call_id: str - """tool_call_id""" - - tool_calls: Iterable[MessageToolCall] - """tool calls if any""" - - class StreamOptions(TypedDict, total=False): include_usage: bool """ diff --git a/src/arcadepy/types/chat_message.py b/src/arcadepy/types/chat_message.py new file mode 100644 index 00000000..2dcd1f31 --- /dev/null +++ b/src/arcadepy/types/chat_message.py @@ -0,0 +1,42 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ChatMessage", "ToolCall", "ToolCallFunction"] + + +class ToolCallFunction(BaseModel): + arguments: Optional[str] = None + + name: Optional[str] = None + + +class ToolCall(BaseModel): + id: Optional[str] = None + + function: Optional[ToolCallFunction] = None + + type: Optional[Literal["function"]] = None + + +class ChatMessage(BaseModel): + content: str + """The content of the message.""" + + role: str + """The role of the author of this message. + + One of system, user, tool, or assistant. + """ + + name: Optional[str] = None + """tool Name""" + + tool_call_id: Optional[str] = None + """tool_call_id""" + + tool_calls: Optional[List[ToolCall]] = None + """tool calls if any""" diff --git a/src/arcadepy/types/chat_message_param.py b/src/arcadepy/types/chat_message_param.py new file mode 100644 index 00000000..cefb5a6a --- /dev/null +++ b/src/arcadepy/types/chat_message_param.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 Iterable +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["ChatMessageParam", "ToolCall", "ToolCallFunction"] + + +class ToolCallFunction(TypedDict, total=False): + arguments: str + + name: str + + +class ToolCall(TypedDict, total=False): + id: str + + function: ToolCallFunction + + type: Literal["function"] + + +class ChatMessageParam(TypedDict, total=False): + content: Required[str] + """The content of the message.""" + + role: Required[str] + """The role of the author of this message. + + One of system, user, tool, or assistant. + """ + + name: str + """tool Name""" + + tool_call_id: str + """tool_call_id""" + + tool_calls: Iterable[ToolCall] + """tool calls if any""" diff --git a/src/arcadepy/types/chat_response.py b/src/arcadepy/types/chat_response.py index 4e3f63fb..04ce655e 100644 --- a/src/arcadepy/types/chat_response.py +++ b/src/arcadepy/types/chat_response.py @@ -1,45 +1,11 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional -from typing_extensions import Literal from .._models import BaseModel +from .chat_message import ChatMessage -__all__ = ["ChatResponse", "Choice", "ChoiceMessage", "ChoiceMessageToolCall", "ChoiceMessageToolCallFunction", "Usage"] - - -class ChoiceMessageToolCallFunction(BaseModel): - arguments: Optional[str] = None - - name: Optional[str] = None - - -class ChoiceMessageToolCall(BaseModel): - id: Optional[str] = None - - function: Optional[ChoiceMessageToolCallFunction] = None - - type: Optional[Literal["function"]] = None - - -class ChoiceMessage(BaseModel): - content: str - """The content of the message.""" - - role: str - """The role of the author of this message. - - One of system, user, tool, or assistant. - """ - - name: Optional[str] = None - """tool Name""" - - tool_call_id: Optional[str] = None - """tool_call_id""" - - tool_calls: Optional[List[ChoiceMessageToolCall]] = None - """tool calls if any""" +__all__ = ["ChatResponse", "Choice", "Usage"] class Choice(BaseModel): @@ -49,7 +15,7 @@ class Choice(BaseModel): logprobs: Optional[object] = None - message: Optional[ChoiceMessage] = None + message: Optional[ChatMessage] = None class Usage(BaseModel): diff --git a/src/arcadepy/types/shared/__init__.py b/src/arcadepy/types/shared/__init__.py index fd7448f5..867de170 100644 --- a/src/arcadepy/types/shared/__init__.py +++ b/src/arcadepy/types/shared/__init__.py @@ -1,3 +1,3 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from .authorization_response import AuthorizationResponse as AuthorizationResponse +from .error import Error as Error diff --git a/src/arcadepy/types/shared/error.py b/src/arcadepy/types/shared/error.py new file mode 100644 index 00000000..17e12a3d --- /dev/null +++ b/src/arcadepy/types/shared/error.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["Error"] + + +class Error(BaseModel): + message: Optional[str] = None + + name: Optional[str] = None diff --git a/src/arcadepy/types/tool_definition.py b/src/arcadepy/types/tool_definition.py new file mode 100644 index 00000000..c8df3bef --- /dev/null +++ b/src/arcadepy/types/tool_definition.py @@ -0,0 +1,87 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = [ + "ToolDefinition", + "Inputs", + "InputsParameter", + "InputsParameterValueSchema", + "Output", + "OutputValueSchema", + "Requirements", + "RequirementsAuthorization", + "RequirementsAuthorizationOauth2", +] + + +class InputsParameterValueSchema(BaseModel): + val_type: str + + enum: Optional[List[str]] = None + + inner_val_type: Optional[str] = None + + +class InputsParameter(BaseModel): + name: str + + value_schema: InputsParameterValueSchema + + description: Optional[str] = None + + inferrable: Optional[bool] = None + + required: Optional[bool] = None + + +class Inputs(BaseModel): + parameters: Optional[List[InputsParameter]] = None + + +class OutputValueSchema(BaseModel): + val_type: str + + enum: Optional[List[str]] = None + + inner_val_type: Optional[str] = None + + +class Output(BaseModel): + available_modes: Optional[List[str]] = None + + description: Optional[str] = None + + value_schema: Optional[OutputValueSchema] = None + + +class RequirementsAuthorizationOauth2(BaseModel): + authority: Optional[str] = None + + scopes: Optional[List[str]] = None + + +class RequirementsAuthorization(BaseModel): + provider: str + + oauth2: Optional[RequirementsAuthorizationOauth2] = None + + +class Requirements(BaseModel): + authorization: Optional[RequirementsAuthorization] = None + + +class ToolDefinition(BaseModel): + inputs: Inputs + + name: str + + version: str + + description: Optional[str] = None + + output: Optional[Output] = None + + requirements: Optional[Requirements] = None diff --git a/src/arcadepy/types/tool_response.py b/src/arcadepy/types/tool_response.py new file mode 100644 index 00000000..d003e6e6 --- /dev/null +++ b/src/arcadepy/types/tool_response.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["ToolResponse", "FinishedAt", "Output", "OutputError"] + + +class FinishedAt(BaseModel): + time_time: Optional[str] = FieldInfo(alias="time.Time", default=None) + + +class OutputError(BaseModel): + message: str + + additional_prompt_content: Optional[str] = None + + can_retry: Optional[bool] = None + + developer_message: Optional[str] = None + + retry_after_ms: Optional[int] = None + + +class Output(BaseModel): + value: object + + error: Optional[OutputError] = None + + +class ToolResponse(BaseModel): + invocation_id: str + + duration: Optional[float] = None + + finished_at: Optional[FinishedAt] = None + + output: Optional[Output] = None + + success: Optional[bool] = None diff --git a/src/arcadepy/types/tool_retrieve_definition_params.py b/src/arcadepy/types/tool_retrieve_definition_params.py new file mode 100644 index 00000000..8823e0de --- /dev/null +++ b/src/arcadepy/types/tool_retrieve_definition_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ToolRetrieveDefinitionParams"] + + +class ToolRetrieveDefinitionParams(TypedDict, total=False): + director_id: Required[str] + """Director ID""" + + tool_id: Required[str] + """Tool ID""" diff --git a/tests/api_resources/test_auth.py b/tests/api_resources/test_auth.py index f2d2eeeb..d4171b49 100644 --- a/tests/api_resources/test_auth.py +++ b/tests/api_resources/test_auth.py @@ -9,7 +9,7 @@ from arcadepy import ArcadeAI, AsyncArcadeAI from tests.utils import assert_matches_type -from arcadepy.types.shared import AuthorizationResponse +from arcadepy.types import AuthorizationResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,16 +18,16 @@ class TestAuth: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_authorization(self, client: ArcadeAI) -> None: - auth = client.auth.authorization( + def test_method_authorize(self, client: ArcadeAI) -> None: + auth = client.auth.authorize( auth_requirement={"provider": "provider"}, user_id="user_id", ) assert_matches_type(AuthorizationResponse, auth, path=["response"]) @parametrize - def test_method_authorization_with_all_params(self, client: ArcadeAI) -> None: - auth = client.auth.authorization( + def test_method_authorize_with_all_params(self, client: ArcadeAI) -> None: + auth = client.auth.authorize( auth_requirement={ "provider": "provider", "oauth2": { @@ -40,8 +40,8 @@ def test_method_authorization_with_all_params(self, client: ArcadeAI) -> None: assert_matches_type(AuthorizationResponse, auth, path=["response"]) @parametrize - def test_raw_response_authorization(self, client: ArcadeAI) -> None: - response = client.auth.with_raw_response.authorization( + def test_raw_response_authorize(self, client: ArcadeAI) -> None: + response = client.auth.with_raw_response.authorize( auth_requirement={"provider": "provider"}, user_id="user_id", ) @@ -52,8 +52,8 @@ def test_raw_response_authorization(self, client: ArcadeAI) -> None: assert_matches_type(AuthorizationResponse, auth, path=["response"]) @parametrize - def test_streaming_response_authorization(self, client: ArcadeAI) -> None: - with client.auth.with_streaming_response.authorization( + def test_streaming_response_authorize(self, client: ArcadeAI) -> None: + with client.auth.with_streaming_response.authorize( auth_requirement={"provider": "provider"}, user_id="user_id", ) as response: @@ -109,16 +109,16 @@ class TestAsyncAuth: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_authorization(self, async_client: AsyncArcadeAI) -> None: - auth = await async_client.auth.authorization( + async def test_method_authorize(self, async_client: AsyncArcadeAI) -> None: + auth = await async_client.auth.authorize( auth_requirement={"provider": "provider"}, user_id="user_id", ) assert_matches_type(AuthorizationResponse, auth, path=["response"]) @parametrize - async def test_method_authorization_with_all_params(self, async_client: AsyncArcadeAI) -> None: - auth = await async_client.auth.authorization( + async def test_method_authorize_with_all_params(self, async_client: AsyncArcadeAI) -> None: + auth = await async_client.auth.authorize( auth_requirement={ "provider": "provider", "oauth2": { @@ -131,8 +131,8 @@ async def test_method_authorization_with_all_params(self, async_client: AsyncArc assert_matches_type(AuthorizationResponse, auth, path=["response"]) @parametrize - async def test_raw_response_authorization(self, async_client: AsyncArcadeAI) -> None: - response = await async_client.auth.with_raw_response.authorization( + async def test_raw_response_authorize(self, async_client: AsyncArcadeAI) -> None: + response = await async_client.auth.with_raw_response.authorize( auth_requirement={"provider": "provider"}, user_id="user_id", ) @@ -143,8 +143,8 @@ async def test_raw_response_authorization(self, async_client: AsyncArcadeAI) -> assert_matches_type(AuthorizationResponse, auth, path=["response"]) @parametrize - async def test_streaming_response_authorization(self, async_client: AsyncArcadeAI) -> None: - async with async_client.auth.with_streaming_response.authorization( + async def test_streaming_response_authorize(self, async_client: AsyncArcadeAI) -> None: + async with async_client.auth.with_streaming_response.authorize( auth_requirement={"provider": "provider"}, user_id="user_id", ) as response: diff --git a/tests/api_resources/test_chat.py b/tests/api_resources/test_chat.py index 23417062..455b858a 100644 --- a/tests/api_resources/test_chat.py +++ b/tests/api_resources/test_chat.py @@ -129,7 +129,7 @@ def test_method_completions_with_all_params(self, client: ArcadeAI) -> None: ], model="model", n=0, - parallel_tool_calls={}, + parallel_tool_calls=True, presence_penalty=0, response_format="json_object", seed=0, @@ -281,7 +281,7 @@ async def test_method_completions_with_all_params(self, async_client: AsyncArcad ], model="model", n=0, - parallel_tool_calls={}, + parallel_tool_calls=True, presence_penalty=0, response_format="json_object", seed=0, diff --git a/tests/api_resources/test_health.py b/tests/api_resources/test_health.py index 4134f6a3..ca3ffc14 100644 --- a/tests/api_resources/test_health.py +++ b/tests/api_resources/test_health.py @@ -18,13 +18,13 @@ class TestHealth: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - def test_method_list(self, client: ArcadeAI) -> None: - health = client.health.list() + def test_method_check(self, client: ArcadeAI) -> None: + health = client.health.check() assert_matches_type(HealthSchema, health, path=["response"]) @parametrize - def test_raw_response_list(self, client: ArcadeAI) -> None: - response = client.health.with_raw_response.list() + def test_raw_response_check(self, client: ArcadeAI) -> None: + response = client.health.with_raw_response.check() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -32,8 +32,8 @@ def test_raw_response_list(self, client: ArcadeAI) -> None: assert_matches_type(HealthSchema, health, path=["response"]) @parametrize - def test_streaming_response_list(self, client: ArcadeAI) -> None: - with client.health.with_streaming_response.list() as response: + def test_streaming_response_check(self, client: ArcadeAI) -> None: + with client.health.with_streaming_response.check() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -47,13 +47,13 @@ class TestAsyncHealth: parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) @parametrize - async def test_method_list(self, async_client: AsyncArcadeAI) -> None: - health = await async_client.health.list() + async def test_method_check(self, async_client: AsyncArcadeAI) -> None: + health = await async_client.health.check() assert_matches_type(HealthSchema, health, path=["response"]) @parametrize - async def test_raw_response_list(self, async_client: AsyncArcadeAI) -> None: - response = await async_client.health.with_raw_response.list() + async def test_raw_response_check(self, async_client: AsyncArcadeAI) -> None: + response = await async_client.health.with_raw_response.check() assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -61,8 +61,8 @@ async def test_raw_response_list(self, async_client: AsyncArcadeAI) -> None: assert_matches_type(HealthSchema, health, path=["response"]) @parametrize - async def test_streaming_response_list(self, async_client: AsyncArcadeAI) -> None: - async with async_client.health.with_streaming_response.list() as response: + async def test_streaming_response_check(self, async_client: AsyncArcadeAI) -> None: + async with async_client.health.with_streaming_response.check() as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/test_tools.py b/tests/api_resources/test_tools.py index e0ad08ab..6a0d501a 100644 --- a/tests/api_resources/test_tools.py +++ b/tests/api_resources/test_tools.py @@ -9,8 +9,11 @@ from arcadepy import ArcadeAI, AsyncArcadeAI from tests.utils import assert_matches_type -from arcadepy.types import Response, Definition -from arcadepy.types.shared import AuthorizationResponse +from arcadepy.types import ( + ToolResponse, + ToolDefinition, + AuthorizationResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -18,40 +21,6 @@ class TestTools: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @parametrize - def test_method_retrieve(self, client: ArcadeAI) -> None: - tool = client.tools.retrieve( - director_id="director_id", - tool_id="tool_id", - ) - assert_matches_type(Definition, tool, path=["response"]) - - @parametrize - def test_raw_response_retrieve(self, client: ArcadeAI) -> None: - response = client.tools.with_raw_response.retrieve( - director_id="director_id", - tool_id="tool_id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = response.parse() - assert_matches_type(Definition, tool, path=["response"]) - - @parametrize - def test_streaming_response_retrieve(self, client: ArcadeAI) -> None: - with client.tools.with_streaming_response.retrieve( - director_id="director_id", - tool_id="tool_id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - tool = response.parse() - assert_matches_type(Definition, tool, path=["response"]) - - assert cast(Any, response.is_closed) is True - @parametrize def test_method_authorize(self, client: ArcadeAI) -> None: tool = client.tools.authorize( @@ -103,7 +72,7 @@ def test_method_execute(self, client: ArcadeAI) -> None: tool_version="tool_version", user_id="user_id", ) - assert_matches_type(Response, tool, path=["response"]) + assert_matches_type(ToolResponse, tool, path=["response"]) @parametrize def test_raw_response_execute(self, client: ArcadeAI) -> None: @@ -117,7 +86,7 @@ def test_raw_response_execute(self, client: ArcadeAI) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" tool = response.parse() - assert_matches_type(Response, tool, path=["response"]) + assert_matches_type(ToolResponse, tool, path=["response"]) @parametrize def test_streaming_response_execute(self, client: ArcadeAI) -> None: @@ -131,48 +100,48 @@ def test_streaming_response_execute(self, client: ArcadeAI) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" tool = response.parse() - assert_matches_type(Response, tool, path=["response"]) + assert_matches_type(ToolResponse, tool, path=["response"]) assert cast(Any, response.is_closed) is True - -class TestAsyncTools: - parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) - @parametrize - async def test_method_retrieve(self, async_client: AsyncArcadeAI) -> None: - tool = await async_client.tools.retrieve( + def test_method_retrieve_definition(self, client: ArcadeAI) -> None: + tool = client.tools.retrieve_definition( director_id="director_id", tool_id="tool_id", ) - assert_matches_type(Definition, tool, path=["response"]) + assert_matches_type(ToolDefinition, tool, path=["response"]) @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncArcadeAI) -> None: - response = await async_client.tools.with_raw_response.retrieve( + def test_raw_response_retrieve_definition(self, client: ArcadeAI) -> None: + response = client.tools.with_raw_response.retrieve_definition( director_id="director_id", tool_id="tool_id", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(Definition, tool, path=["response"]) + tool = response.parse() + assert_matches_type(ToolDefinition, tool, path=["response"]) @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncArcadeAI) -> None: - async with async_client.tools.with_streaming_response.retrieve( + def test_streaming_response_retrieve_definition(self, client: ArcadeAI) -> None: + with client.tools.with_streaming_response.retrieve_definition( director_id="director_id", tool_id="tool_id", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" - tool = await response.parse() - assert_matches_type(Definition, tool, path=["response"]) + tool = response.parse() + assert_matches_type(ToolDefinition, tool, path=["response"]) assert cast(Any, response.is_closed) is True + +class TestAsyncTools: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + @parametrize async def test_method_authorize(self, async_client: AsyncArcadeAI) -> None: tool = await async_client.tools.authorize( @@ -224,7 +193,7 @@ async def test_method_execute(self, async_client: AsyncArcadeAI) -> None: tool_version="tool_version", user_id="user_id", ) - assert_matches_type(Response, tool, path=["response"]) + assert_matches_type(ToolResponse, tool, path=["response"]) @parametrize async def test_raw_response_execute(self, async_client: AsyncArcadeAI) -> None: @@ -238,7 +207,7 @@ async def test_raw_response_execute(self, async_client: AsyncArcadeAI) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" tool = await response.parse() - assert_matches_type(Response, tool, path=["response"]) + assert_matches_type(ToolResponse, tool, path=["response"]) @parametrize async def test_streaming_response_execute(self, async_client: AsyncArcadeAI) -> None: @@ -252,6 +221,40 @@ async def test_streaming_response_execute(self, async_client: AsyncArcadeAI) -> assert response.http_request.headers.get("X-Stainless-Lang") == "python" tool = await response.parse() - assert_matches_type(Response, tool, path=["response"]) + assert_matches_type(ToolResponse, tool, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve_definition(self, async_client: AsyncArcadeAI) -> None: + tool = await async_client.tools.retrieve_definition( + director_id="director_id", + tool_id="tool_id", + ) + assert_matches_type(ToolDefinition, tool, path=["response"]) + + @parametrize + async def test_raw_response_retrieve_definition(self, async_client: AsyncArcadeAI) -> None: + response = await async_client.tools.with_raw_response.retrieve_definition( + director_id="director_id", + tool_id="tool_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tool = await response.parse() + assert_matches_type(ToolDefinition, tool, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve_definition(self, async_client: AsyncArcadeAI) -> None: + async with async_client.tools.with_streaming_response.retrieve_definition( + director_id="director_id", + tool_id="tool_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tool = await response.parse() + assert_matches_type(ToolDefinition, tool, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/conftest.py b/tests/conftest.py index 95bb7213..ba66b4aa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,6 +26,8 @@ def event_loop() -> Iterator[asyncio.AbstractEventLoop]: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +api_key = "My API Key" + @pytest.fixture(scope="session") def client(request: FixtureRequest) -> Iterator[ArcadeAI]: @@ -33,7 +35,7 @@ def client(request: FixtureRequest) -> Iterator[ArcadeAI]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with ArcadeAI(base_url=base_url, _strict_response_validation=strict) as client: + with ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @@ -43,5 +45,5 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncArcadeAI]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - async with AsyncArcadeAI(base_url=base_url, _strict_response_validation=strict) as client: + async with AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index da40a7de..534824d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -20,7 +20,7 @@ from arcadepy._types import Omit from arcadepy._models import BaseModel, FinalRequestOptions from arcadepy._constants import RAW_RESPONSE_HEADER -from arcadepy._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError +from arcadepy._exceptions import ArcadeAIError, APIStatusError, APITimeoutError, APIResponseValidationError from arcadepy._base_client import ( DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, @@ -31,6 +31,7 @@ from .utils import update_env base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +api_key = "My API Key" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -52,7 +53,7 @@ def _get_open_connections(client: ArcadeAI | AsyncArcadeAI) -> int: class TestArcadeAI: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True) + client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) def test_raw_response(self, respx_mock: MockRouter) -> None: @@ -78,6 +79,10 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + def test_copy_default_options(self) -> None: # options that have a default are overridden correctly copied = self.client.copy(max_retries=7) @@ -95,7 +100,9 @@ def test_copy_default_options(self) -> None: assert isinstance(self.client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) assert client.default_headers["X-Foo"] == "bar" # does not override the already given value when not specified @@ -127,7 +134,9 @@ def test_copy_default_headers(self) -> None: client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) def test_copy_default_query(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"}) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) assert _get_params(client)["foo"] == "bar" # does not override the already given value when not specified @@ -250,7 +259,9 @@ def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) def test_client_timeout_option(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, timeout=httpx.Timeout(0)) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -259,7 +270,9 @@ def test_client_timeout_option(self) -> None: def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=http_client) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -267,7 +280,9 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=http_client) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -275,7 +290,9 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=http_client) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -284,16 +301,24 @@ def test_http_client_timeout_option(self) -> None: async def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): async with httpx.AsyncClient() as http_client: - ArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client)) + ArcadeAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) def test_default_headers_option(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" client2 = ArcadeAI( base_url=base_url, + api_key=api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -304,8 +329,20 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + def test_validate_headers(self) -> None: + client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == api_key + + with pytest.raises(ArcadeAIError): + with update_env(**{"ARCADE_API_KEY": Omit()}): + client2 = ArcadeAI(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + def test_default_query_option(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"}) + client = ArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) assert dict(url.params) == {"query_param": "bar"} @@ -503,8 +540,39 @@ class Model(BaseModel): assert isinstance(response, Model) assert response.foo == 2 + @pytest.mark.respx(base_url=base_url) + def test_idempotency_header_options(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={})) + + response = self.client.post("/foo", cast_to=httpx.Response) + + header = response.request.headers.get("Idempotency-Key") + assert header is not None + assert header.startswith("stainless-python-retry") + + # explicit header + response = self.client.post( + "/foo", + cast_to=httpx.Response, + options=make_request_options(extra_headers={"Idempotency-Key": "custom-key"}), + ) + assert response.request.headers.get("Idempotency-Key") == "custom-key" + + response = self.client.post( + "/foo", + cast_to=httpx.Response, + options=make_request_options(extra_headers={"idempotency-key": "custom-key"}), + ) + assert response.request.headers.get("Idempotency-Key") == "custom-key" + + # custom argument + response = self.client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(idempotency_key="custom-key") + ) + assert response.request.headers.get("Idempotency-Key") == "custom-key" + def test_base_url_setter(self) -> None: - client = ArcadeAI(base_url="https://example.com/from_init", _strict_response_validation=True) + client = ArcadeAI(base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -513,23 +581,26 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(ARCADE_AI_BASE_URL="http://localhost:5000/from/env"): - client = ArcadeAI(_strict_response_validation=True) + client = ArcadeAI(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" # explicit environment arg requires explicitness with update_env(ARCADE_AI_BASE_URL="http://localhost:5000/from/env"): with pytest.raises(ValueError, match=r"you must pass base_url=None"): - ArcadeAI(_strict_response_validation=True, environment="production") + ArcadeAI(api_key=api_key, _strict_response_validation=True, environment="production") - client = ArcadeAI(base_url=None, _strict_response_validation=True, environment="production") + client = ArcadeAI( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) assert str(client.base_url).startswith("https://api.arcade-ai.com") @pytest.mark.parametrize( "client", [ - ArcadeAI(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), + ArcadeAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), ArcadeAI( base_url="http://localhost:5000/custom/path/", + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -549,9 +620,10 @@ def test_base_url_trailing_slash(self, client: ArcadeAI) -> None: @pytest.mark.parametrize( "client", [ - ArcadeAI(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), + ArcadeAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), ArcadeAI( base_url="http://localhost:5000/custom/path/", + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -571,9 +643,10 @@ def test_base_url_no_trailing_slash(self, client: ArcadeAI) -> None: @pytest.mark.parametrize( "client", [ - ArcadeAI(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), + ArcadeAI(base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True), ArcadeAI( base_url="http://localhost:5000/custom/path/", + api_key=api_key, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -591,7 +664,7 @@ def test_absolute_request_url(self, client: ArcadeAI) -> None: assert request.url == "https://myapi.com/foo" def test_copied_client_does_not_close_http(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True) + client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -602,7 +675,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() def test_client_context_manager(self) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True) + client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) with client as c2: assert c2 is client assert not c2.is_closed() @@ -623,7 +696,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - ArcadeAI(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None)) + ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None)) @pytest.mark.respx(base_url=base_url) def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: @@ -632,12 +705,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = ArcadeAI(base_url=base_url, _strict_response_validation=True) + strict_client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - client = ArcadeAI(base_url=base_url, _strict_response_validation=False) + client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -665,7 +738,7 @@ class Model(BaseModel): ) @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = ArcadeAI(base_url=base_url, _strict_response_validation=True) + client = ArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -680,7 +753,17 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No with pytest.raises(APITimeoutError): self.client.post( "/v1/chat/completions", - body=cast(object, dict()), + body=cast( + object, + dict( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ] + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -695,7 +778,17 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non with pytest.raises(APIStatusError): self.client.post( "/v1/chat/completions", - body=cast(object, dict()), + body=cast( + object, + dict( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ] + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -772,7 +865,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: class TestAsyncArcadeAI: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True) + client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -800,6 +893,10 @@ def test_copy(self) -> None: copied = self.client.copy() assert id(copied) != id(self.client) + copied = self.client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert self.client.api_key == "My API Key" + def test_copy_default_options(self) -> None: # options that have a default are overridden correctly copied = self.client.copy(max_retries=7) @@ -817,7 +914,9 @@ def test_copy_default_options(self) -> None: assert isinstance(self.client.timeout, httpx.Timeout) def test_copy_default_headers(self) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) assert client.default_headers["X-Foo"] == "bar" # does not override the already given value when not specified @@ -849,7 +948,9 @@ def test_copy_default_headers(self) -> None: client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) def test_copy_default_query(self) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, default_query={"foo": "bar"}) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) assert _get_params(client)["foo"] == "bar" # does not override the already given value when not specified @@ -972,7 +1073,9 @@ async def test_request_timeout(self) -> None: assert timeout == httpx.Timeout(100.0) async def test_client_timeout_option(self) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, timeout=httpx.Timeout(0)) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -981,7 +1084,9 @@ async def test_client_timeout_option(self) -> None: async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=http_client) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -989,7 +1094,9 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=http_client) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -997,7 +1104,9 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=http_client) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore @@ -1006,16 +1115,24 @@ async def test_http_client_timeout_option(self) -> None: def test_invalid_http_client(self) -> None: with pytest.raises(TypeError, match="Invalid `http_client` arg"): with httpx.Client() as http_client: - AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, http_client=cast(Any, http_client)) + AsyncArcadeAI( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) def test_default_headers_option(self) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, default_headers={"X-Foo": "bar"}) + client = AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" assert request.headers.get("x-stainless-lang") == "python" client2 = AsyncArcadeAI( base_url=base_url, + api_key=api_key, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1026,9 +1143,19 @@ def test_default_headers_option(self) -> None: assert request.headers.get("x-foo") == "stainless" assert request.headers.get("x-stainless-lang") == "my-overriding-header" + def test_validate_headers(self) -> None: + client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == api_key + + with pytest.raises(ArcadeAIError): + with update_env(**{"ARCADE_API_KEY": Omit()}): + client2 = AsyncArcadeAI(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + def test_default_query_option(self) -> None: client = AsyncArcadeAI( - base_url=base_url, _strict_response_validation=True, default_query={"query_param": "bar"} + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -1227,8 +1354,41 @@ class Model(BaseModel): assert isinstance(response, Model) assert response.foo == 2 + @pytest.mark.respx(base_url=base_url) + async def test_idempotency_header_options(self, respx_mock: MockRouter) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={})) + + response = await self.client.post("/foo", cast_to=httpx.Response) + + header = response.request.headers.get("Idempotency-Key") + assert header is not None + assert header.startswith("stainless-python-retry") + + # explicit header + response = await self.client.post( + "/foo", + cast_to=httpx.Response, + options=make_request_options(extra_headers={"Idempotency-Key": "custom-key"}), + ) + assert response.request.headers.get("Idempotency-Key") == "custom-key" + + response = await self.client.post( + "/foo", + cast_to=httpx.Response, + options=make_request_options(extra_headers={"idempotency-key": "custom-key"}), + ) + assert response.request.headers.get("Idempotency-Key") == "custom-key" + + # custom argument + response = await self.client.post( + "/foo", cast_to=httpx.Response, options=make_request_options(idempotency_key="custom-key") + ) + assert response.request.headers.get("Idempotency-Key") == "custom-key" + def test_base_url_setter(self) -> None: - client = AsyncArcadeAI(base_url="https://example.com/from_init", _strict_response_validation=True) + client = AsyncArcadeAI( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) assert client.base_url == "https://example.com/from_init/" client.base_url = "https://example.com/from_setter" # type: ignore[assignment] @@ -1237,23 +1397,28 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(ARCADE_AI_BASE_URL="http://localhost:5000/from/env"): - client = AsyncArcadeAI(_strict_response_validation=True) + client = AsyncArcadeAI(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" # explicit environment arg requires explicitness with update_env(ARCADE_AI_BASE_URL="http://localhost:5000/from/env"): with pytest.raises(ValueError, match=r"you must pass base_url=None"): - AsyncArcadeAI(_strict_response_validation=True, environment="production") + AsyncArcadeAI(api_key=api_key, _strict_response_validation=True, environment="production") - client = AsyncArcadeAI(base_url=None, _strict_response_validation=True, environment="production") + client = AsyncArcadeAI( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) assert str(client.base_url).startswith("https://api.arcade-ai.com") @pytest.mark.parametrize( "client", [ - AsyncArcadeAI(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), + AsyncArcadeAI( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), AsyncArcadeAI( base_url="http://localhost:5000/custom/path/", + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1273,9 +1438,12 @@ def test_base_url_trailing_slash(self, client: AsyncArcadeAI) -> None: @pytest.mark.parametrize( "client", [ - AsyncArcadeAI(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), + AsyncArcadeAI( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), AsyncArcadeAI( base_url="http://localhost:5000/custom/path/", + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1295,9 +1463,12 @@ def test_base_url_no_trailing_slash(self, client: AsyncArcadeAI) -> None: @pytest.mark.parametrize( "client", [ - AsyncArcadeAI(base_url="http://localhost:5000/custom/path/", _strict_response_validation=True), + AsyncArcadeAI( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), AsyncArcadeAI( base_url="http://localhost:5000/custom/path/", + api_key=api_key, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1315,7 +1486,7 @@ def test_absolute_request_url(self, client: AsyncArcadeAI) -> None: assert request.url == "https://myapi.com/foo" async def test_copied_client_does_not_close_http(self) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True) + client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not client.is_closed() copied = client.copy() @@ -1327,7 +1498,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not client.is_closed() async def test_client_context_manager(self) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True) + client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) async with client as c2: assert c2 is client assert not c2.is_closed() @@ -1349,7 +1520,9 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): - AsyncArcadeAI(base_url=base_url, _strict_response_validation=True, max_retries=cast(Any, None)) + AsyncArcadeAI( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) @pytest.mark.respx(base_url=base_url) @pytest.mark.asyncio @@ -1359,12 +1532,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True) + strict_client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=False) + client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = await client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1393,7 +1566,7 @@ class Model(BaseModel): @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) @pytest.mark.asyncio async def test_parse_retry_after_header(self, remaining_retries: int, retry_after: str, timeout: float) -> None: - client = AsyncArcadeAI(base_url=base_url, _strict_response_validation=True) + client = AsyncArcadeAI(base_url=base_url, api_key=api_key, _strict_response_validation=True) headers = httpx.Headers({"retry-after": retry_after}) options = FinalRequestOptions(method="get", url="/foo", max_retries=3) @@ -1408,7 +1581,17 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APITimeoutError): await self.client.post( "/v1/chat/completions", - body=cast(object, dict()), + body=cast( + object, + dict( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ] + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, ) @@ -1423,7 +1606,17 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) with pytest.raises(APIStatusError): await self.client.post( "/v1/chat/completions", - body=cast(object, dict()), + body=cast( + object, + dict( + messages=[ + { + "role": "user", + "content": "Hello, how can I use Arcade AI?", + } + ] + ), + ), cast_to=httpx.Response, options={"headers": {RAW_RESPONSE_HEADER: "stream"}}, )