diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ac9a2e7..ff261ba 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.35.0" RYE_INSTALL_OPTION="--yes" bash +RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash ENV PATH=/home/vscode/.rye/shims:$PATH -RUN echo "[[ -d .venv ]] && source .venv/bin/activate" >> /home/vscode/.bashrc +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bbeb30b..c17fdc1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,6 +24,9 @@ } } } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} } // Features to add to the dev container. More info: https://containers.dev/features. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4029396..81f6dc2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,6 @@ jobs: lint: name: lint runs-on: ubuntu-latest - - steps: - uses: actions/checkout@v4 @@ -22,7 +20,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Install dependencies @@ -30,10 +28,10 @@ jobs: - name: Run lints run: ./scripts/lint + test: name: test runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -42,7 +40,7 @@ jobs: curl -sSf https://rye.astral.sh/get | bash echo "$HOME/.rye/shims" >> $GITHUB_PATH env: - RYE_VERSION: '0.35.0' + RYE_VERSION: '0.44.0' RYE_INSTALL_OPTION: '--yes' - name: Bootstrap @@ -50,4 +48,3 @@ jobs: - name: Run tests run: ./scripts/test - diff --git a/.stats.yml b/.stats.yml index 53e5b22..bb3959b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,2 +1,4 @@ configured_endpoints: 42 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-60c8c820f2cbc6b5b0b129ccf707ec246921b9e93cfb18f4ed47a96cc97c47f8.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/cleanlab%2Fagility-b890eea39144ced38d6196197401b71ac734bcd2343d046c04169bfab9d746f2.yml +openapi_spec_hash: 50035b732d759a3bc9a00402b5ccf5c4 +config_hash: 6d2156cfe279456cf3c35ba5c66be1c1 diff --git a/SECURITY.md b/SECURITY.md index c32c806..9c3e857 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,9 +2,9 @@ ## Reporting Security Issues -This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. -To report a security issue, please contact the Stainless team at security@stainlessapi.com. +To report a security issue, please contact the Stainless team at security@stainless.com. ## Responsible Disclosure diff --git a/bin/publish-pypi b/bin/publish-pypi index 05bfccb..826054e 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -3,7 +3,4 @@ set -eux mkdir -p dist rye build --clean -# Patching importlib-metadata version until upstream library version is updated -# https://github.com/pypa/twine/issues/977#issuecomment-2189800841 -"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1' rye publish --yes --token=$PYPI_TOKEN diff --git a/mypy.ini b/mypy.ini index f353443..7a8ca92 100644 --- a/mypy.ini +++ b/mypy.ini @@ -41,7 +41,7 @@ cache_fine_grained = True # ``` # Changing this codegen to make mypy happy would increase complexity # and would not be worth it. -disable_error_code = func-returns-value +disable_error_code = func-returns-value,overload-cannot-match # https://github.com/python/mypy/issues/12162 [mypy.overrides] diff --git a/pyproject.toml b/pyproject.toml index ebb43dd..7200bf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ Homepage = "https://github.com/stainless-sdks/agility-python" Repository = "https://github.com/stainless-sdks/agility-python" - [tool.rye] managed = true # version pins are in requirements-dev.lock @@ -87,7 +86,7 @@ typecheck = { chain = [ "typecheck:mypy" = "mypy ." [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme"] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] build-backend = "hatchling.build" [tool.hatch.build] @@ -129,6 +128,7 @@ testpaths = ["tests"] addopts = "--tb=short" xfail_strict = true asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" filterwarnings = [ "error" ] @@ -151,7 +151,6 @@ reportImplicitOverride = true reportImportCycles = false reportPrivateUsage = false - [tool.ruff] line-length = 120 output-format = "grouped" @@ -176,7 +175,7 @@ select = [ "T201", "T203", # misuse of typing.TYPE_CHECKING - "TCH004", + "TC004", # import rules "TID251", ] diff --git a/requirements-dev.lock b/requirements-dev.lock index 7cb01f1..2ecab23 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 @@ -48,7 +49,7 @@ markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -mypy==1.13.0 +mypy==1.14.1 mypy-extensions==1.0.0 # via mypy nest-asyncio==1.6.0 @@ -68,7 +69,7 @@ pydantic-core==2.27.1 # via pydantic pygments==2.18.0 # via rich -pyright==1.1.390 +pyright==1.1.392.post0 pytest==8.3.3 # via pytest-asyncio pytest-asyncio==0.24.0 @@ -78,7 +79,7 @@ pytz==2023.3.post1 # via dirty-equals respx==0.22.0 rich==13.7.1 -ruff==0.6.9 +ruff==0.9.4 setuptools==68.2.2 # via nodeenv six==1.16.0 diff --git a/requirements.lock b/requirements.lock index b2ab8b1..8f0aee0 100644 --- a/requirements.lock +++ b/requirements.lock @@ -7,6 +7,7 @@ # all-features: true # with-sources: false # generate-hashes: false +# universal: false -e file:. annotated-types==0.6.0 diff --git a/scripts/bootstrap b/scripts/bootstrap index 8c5c60e..e84fe62 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then +if ! command -v rye >/dev/null 2>&1 && [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ]; then brew bundle check >/dev/null 2>&1 || { echo "==> Installing Homebrew dependencies…" brew bundle diff --git a/scripts/lint b/scripts/lint index 97fd526..1fe1413 100755 --- a/scripts/lint +++ b/scripts/lint @@ -9,4 +9,3 @@ rye run lint echo "==> Making sure it imports" rye run python -c 'import agility' - diff --git a/scripts/test b/scripts/test index 4fa5698..2b87845 100755 --- a/scripts/test +++ b/scripts/test @@ -52,6 +52,8 @@ else echo fi +export DEFER_PYDANTIC_BUILD=false + echo "==> Running tests" rye run pytest "$@" diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py index 37b3d94..0cf2bd2 100644 --- a/scripts/utils/ruffen-docs.py +++ b/scripts/utils/ruffen-docs.py @@ -47,7 +47,7 @@ def _md_match(match: Match[str]) -> str: with _collect_error(match): code = format_code_block(code) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" def _pycon_match(match: Match[str]) -> str: code = "" @@ -97,7 +97,7 @@ def finish_fragment() -> None: def _md_pycon_match(match: Match[str]) -> str: code = _pycon_match(match) code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' + return f"{match['before']}{code}{match['after']}" src = MD_RE.sub(_md_match, src) src = MD_PYCON_RE.sub(_md_pycon_match, src) diff --git a/src/agility/_base_client.py b/src/agility/_base_client.py index b8bea80..caf5b3c 100644 --- a/src/agility/_base_client.py +++ b/src/agility/_base_client.py @@ -9,7 +9,6 @@ import inspect import logging import platform -import warnings import email.utils from types import TracebackType from random import random @@ -36,7 +35,7 @@ import httpx import distro import pydantic -from httpx import URL, Limits +from httpx import URL from pydantic import PrivateAttr from . import _exceptions @@ -51,19 +50,16 @@ Timeout, NotGiven, ResponseT, - Transport, AnyMapping, PostParser, - ProxiesTypes, RequestFiles, HttpxSendArgs, - AsyncTransport, RequestOptions, HttpxRequestFiles, ModelBuilderProtocol, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping -from ._compat import model_copy, model_dump +from ._compat import PYDANTIC_V2, model_copy, model_dump from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, @@ -207,6 +203,9 @@ def _set_private_attributes( model: Type[_T], options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options @@ -292,6 +291,9 @@ def _set_private_attributes( client: AsyncAPIClient, options: FinalRequestOptions, ) -> None: + if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + self._model = model self._client = client self._options = options @@ -331,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): _base_url: URL max_retries: int timeout: Union[float, Timeout, None] - _limits: httpx.Limits - _proxies: ProxiesTypes | None - _transport: Transport | AsyncTransport | None _strict_response_validation: bool _idempotency_header: str | None _default_stream_cls: type[_DefaultStreamT] | None = None @@ -346,9 +345,6 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None = DEFAULT_TIMEOUT, - limits: httpx.Limits, - transport: Transport | AsyncTransport | None, - proxies: ProxiesTypes | None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: @@ -356,9 +352,6 @@ def __init__( self._base_url = self._enforce_trailing_slash(URL(base_url)) self.max_retries = max_retries self.timeout = timeout - self._limits = limits - self._proxies = proxies - self._transport = transport self._custom_headers = custom_headers or {} self._custom_query = custom_query or {} self._strict_response_validation = _strict_response_validation @@ -418,10 +411,17 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers: headers[idempotency_header] = options.idempotency_key or self._idempotency_key() - # Don't set the retry count header if it was already set or removed by the caller. We check + # Don't set these headers if they were already set or removed by the caller. We check # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. - if "x-stainless-retry-count" not in (header.lower() for header in custom_headers): + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) return headers @@ -511,7 +511,7 @@ def _build_request( # so that passing a `TypedDict` doesn't cause an error. # https://github.com/microsoft/pyright/issues/3526#event-6715453066 params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, - json=json_data, + json=json_data if is_given(json_data) else None, files=files, **kwargs, ) @@ -787,46 +787,11 @@ def __init__( base_url: str | URL, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: Transport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.Client | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, _strict_response_validation: bool, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - if not is_given(timeout): # if the user passed in a custom http client with a non-default # timeout set then we use that timeout. @@ -847,12 +812,9 @@ def __init__( super().__init__( version=version, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, base_url=base_url, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -862,9 +824,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: @@ -1359,45 +1318,10 @@ def __init__( _strict_response_validation: bool, max_retries: int = DEFAULT_MAX_RETRIES, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, - transport: AsyncTransport | None = None, - proxies: ProxiesTypes | None = None, - limits: Limits | None = None, http_client: httpx.AsyncClient | None = None, custom_headers: Mapping[str, str] | None = None, custom_query: Mapping[str, object] | None = None, ) -> None: - kwargs: dict[str, Any] = {} - if limits is not None: - warnings.warn( - "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`") - else: - limits = DEFAULT_CONNECTION_LIMITS - - if transport is not None: - kwargs["transport"] = transport - warnings.warn( - "The `transport` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `transport`") - - if proxies is not None: - kwargs["proxies"] = proxies - warnings.warn( - "The `proxies` argument is deprecated. The `http_client` argument should be passed instead", - category=DeprecationWarning, - stacklevel=3, - ) - if http_client is not None: - raise ValueError("The `http_client` argument is mutually exclusive with `proxies`") - if not is_given(timeout): # if the user passed in a custom http client with a non-default # timeout set then we use that timeout. @@ -1419,11 +1343,8 @@ def __init__( super().__init__( version=version, base_url=base_url, - limits=limits, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - proxies=proxies, - transport=transport, max_retries=max_retries, custom_query=custom_query, custom_headers=custom_headers, @@ -1433,9 +1354,6 @@ def __init__( base_url=base_url, # cast to a valid type because mypy doesn't understand our type narrowing timeout=cast(Timeout, timeout), - limits=limits, - follow_redirects=True, - **kwargs, # type: ignore ) def is_closed(self) -> bool: diff --git a/src/agility/_client.py b/src/agility/_client.py index 9fe8b29..818130a 100644 --- a/src/agility/_client.py +++ b/src/agility/_client.py @@ -99,7 +99,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new synchronous agility client instance. + """Construct a new synchronous Agility client instance. This automatically infers the following arguments from their corresponding environment variables if they are not provided: - `bearer_token` from `BEARER_TOKEN` @@ -175,13 +175,7 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._http_bearer: - return self._http_bearer - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} + return {**self._http_bearer, **self._authenticated_api_key, **self._public_access_key} @property def _http_bearer(self) -> dict[str, str]: @@ -344,7 +338,7 @@ def __init__( # part of our public interface in the future. _strict_response_validation: bool = False, ) -> None: - """Construct a new async agility client instance. + """Construct a new async AsyncAgility client instance. This automatically infers the following arguments from their corresponding environment variables if they are not provided: - `bearer_token` from `BEARER_TOKEN` @@ -420,13 +414,7 @@ def qs(self) -> Querystring: @property @override def auth_headers(self) -> dict[str, str]: - if self._http_bearer: - return self._http_bearer - if self._authenticated_api_key: - return self._authenticated_api_key - if self._public_access_key: - return self._public_access_key - return {} + return {**self._http_bearer, **self._authenticated_api_key, **self._public_access_key} @property def _http_bearer(self) -> dict[str, str]: diff --git a/src/agility/_constants.py b/src/agility/_constants.py index a2ac3b6..6ddf2c7 100644 --- a/src/agility/_constants.py +++ b/src/agility/_constants.py @@ -6,7 +6,7 @@ OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" # default timeout is 1 minute -DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0) +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) DEFAULT_MAX_RETRIES = 2 DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) diff --git a/src/agility/_models.py b/src/agility/_models.py index d56ea1d..3493571 100644 --- a/src/agility/_models.py +++ b/src/agility/_models.py @@ -65,7 +65,7 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: - from pydantic_core.core_schema import ModelField, LiteralSchema, ModelFieldsSchema + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema __all__ = ["BaseModel", "GenericModel"] @@ -172,21 +172,21 @@ def to_json( @override def __str__(self) -> str: # mypy complains about an invalid self arg - return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc] + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] # Override the 'construct' method in a way that supports recursive parsing without validation. # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. @classmethod @override def construct( # pyright: ignore[reportIncompatibleMethodOverride] - cls: Type[ModelT], + __cls: Type[ModelT], _fields_set: set[str] | None = None, **values: object, ) -> ModelT: - m = cls.__new__(cls) + m = __cls.__new__(__cls) fields_values: dict[str, object] = {} - config = get_model_config(cls) + config = get_model_config(__cls) populate_by_name = ( config.allow_population_by_field_name if isinstance(config, _ConfigProtocol) @@ -196,7 +196,7 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride] if _fields_set is None: _fields_set = set() - model_fields = get_model_fields(cls) + model_fields = get_model_fields(__cls) for name, field in model_fields.items(): key = field.alias if key is None or (key not in values and populate_by_name): @@ -426,10 +426,16 @@ def construct_type(*, value: object, type_: object) -> object: If the given value does not match the expected type then it is returned as-is. """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + # we allow `object` as the input type because otherwise, passing things like # `Literal['value']` will be reported as a type error by type checkers type_ = cast("type[object]", type_) if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] type_ = type_.__value__ # type: ignore[unreachable] # unwrap `Annotated[T, ...]` -> `T` @@ -446,7 +452,7 @@ def construct_type(*, value: object, type_: object) -> object: if is_union(origin): try: - return validate_type(type_=cast("type[object]", type_), value=value) + return validate_type(type_=cast("type[object]", original_type or type_), value=value) except Exception: pass @@ -640,15 +646,18 @@ def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + if schema["type"] != "model": return None + schema = cast("ModelSchema", schema) fields_schema = schema["schema"] if fields_schema["type"] != "model-fields": return None fields_schema = cast("ModelFieldsSchema", fields_schema) - field = fields_schema["fields"].get(field_name) if not field: return None @@ -672,7 +681,7 @@ def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: setattr(typ, "__pydantic_config__", config) # noqa: B010 -# our use of subclasssing here causes weirdness for type checkers, +# our use of subclassing here causes weirdness for type checkers, # so we just pretend that we don't subclass if TYPE_CHECKING: GenericModel = BaseModel diff --git a/src/agility/_response.py b/src/agility/_response.py index 00f5b74..9c02d4e 100644 --- a/src/agility/_response.py +++ b/src/agility/_response.py @@ -136,6 +136,8 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to and is_annotated_type(cast_to): cast_to = extract_type_arg(cast_to, 0) + origin = get_origin(cast_to) or cast_to + if self._is_sse_stream: if to: if not is_stream_class_type(to): @@ -195,8 +197,6 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: if cast_to == bool: return cast(R, response.text.lower() == "true") - origin = get_origin(cast_to) or cast_to - if origin == APIResponse: raise RuntimeError("Unexpected state - cast_to is `APIResponse`") @@ -210,7 +210,13 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") return cast(R, response) - if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel): + if ( + inspect.isclass( + origin # pyright: ignore[reportUnknownArgumentType] + ) + and not issubclass(origin, BaseModel) + and issubclass(origin, pydantic.BaseModel) + ): raise TypeError("Pydantic models must subclass our base model type, e.g. `from agility import BaseModel`") if ( diff --git a/src/agility/_utils/_sync.py b/src/agility/_utils/_sync.py index 8b3aaf2..ad7ec71 100644 --- a/src/agility/_utils/_sync.py +++ b/src/agility/_utils/_sync.py @@ -7,16 +7,20 @@ from typing import Any, TypeVar, Callable, Awaitable from typing_extensions import ParamSpec +import anyio +import sniffio +import anyio.to_thread + T_Retval = TypeVar("T_Retval") T_ParamSpec = ParamSpec("T_ParamSpec") if sys.version_info >= (3, 9): - to_thread = asyncio.to_thread + _asyncio_to_thread = asyncio.to_thread else: # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread # for Python 3.8 support - async def to_thread( + async def _asyncio_to_thread( func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs ) -> Any: """Asynchronously run function *func* in a separate thread. @@ -34,6 +38,17 @@ async def to_thread( return await loop.run_in_executor(None, func_call) +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await _asyncio_to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + # inspired by `asyncer`, https://github.com/tiangolo/asyncer def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: """ diff --git a/src/agility/_utils/_transform.py b/src/agility/_utils/_transform.py index a6b62ca..b0cc20a 100644 --- a/src/agility/_utils/_transform.py +++ b/src/agility/_utils/_transform.py @@ -5,13 +5,15 @@ import pathlib from typing import Any, Mapping, TypeVar, cast from datetime import date, datetime -from typing_extensions import Literal, get_args, override, get_type_hints +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints import anyio import pydantic from ._utils import ( is_list, + is_given, + lru_cache, is_mapping, is_iterable, ) @@ -25,7 +27,7 @@ is_annotated_type, strip_annotated_type, ) -from .._compat import model_dump, is_typeddict +from .._compat import get_origin, model_dump, is_typeddict _T = TypeVar("_T") @@ -108,6 +110,7 @@ class Params(TypedDict, total=False): return cast(_T, transformed) +@lru_cache(maxsize=8096) def _get_annotated_type(type_: type) -> type | None: """If the given type is an `Annotated` type then it is returned, if not `None` is returned. @@ -126,7 +129,7 @@ def _get_annotated_type(type_: type) -> type | None: def _maybe_transform_key(key: str, type_: type) -> str: """Transform the given `data` based on the annotations provided in `type_`. - Note: this function only looks at `Annotated` types that contain `PropertInfo` metadata. + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. """ annotated_type = _get_annotated_type(type_) if annotated_type is None: @@ -142,6 +145,10 @@ def _maybe_transform_key(key: str, type_: type) -> str: return key +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + def _transform_recursive( data: object, *, @@ -164,9 +171,14 @@ def _transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return _transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) @@ -179,6 +191,15 @@ def _transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): @@ -240,6 +261,11 @@ def _transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is @@ -307,9 +333,14 @@ async def _async_transform_recursive( inner_type = annotation stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type if is_typeddict(stripped_type) and is_mapping(data): return await _async_transform_typeddict(data, stripped_type) + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + if ( # List[T] (is_list_type(stripped_type) and is_list(data)) @@ -322,6 +353,15 @@ async def _async_transform_recursive( return cast(object, data) inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] if is_union_type(stripped_type): @@ -383,6 +423,11 @@ async def _async_transform_typeddict( result: dict[str, object] = {} annotations = get_type_hints(expected_type, include_extras=True) for key, value in data.items(): + if not is_given(value): + # we don't need to include `NotGiven` values here as they'll + # be stripped out before the request is sent anyway + continue + type_ = annotations.get(key) if type_ is None: # we do not have a type annotation for this field, leave it as is @@ -390,3 +435,13 @@ async def _async_transform_typeddict( else: result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/agility/_utils/_typing.py b/src/agility/_utils/_typing.py index 278749b..1958820 100644 --- a/src/agility/_utils/_typing.py +++ b/src/agility/_utils/_typing.py @@ -13,6 +13,7 @@ get_origin, ) +from ._utils import lru_cache from .._types import InheritsGeneric from .._compat import is_union as _is_union @@ -66,6 +67,7 @@ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) def strip_annotated_type(typ: type) -> type: if is_required_type(typ) or is_annotated_type(typ): return strip_annotated_type(cast(type, get_args(typ)[0])) diff --git a/src/agility/resources/assistants/access_keys.py b/src/agility/resources/assistants/access_keys.py index 80d11d9..2b154fe 100644 --- a/src/agility/resources/assistants/access_keys.py +++ b/src/agility/resources/assistants/access_keys.py @@ -32,7 +32,7 @@ class AccessKeysResource(SyncAPIResource): @cached_property def with_raw_response(self) -> AccessKeysResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -143,7 +143,7 @@ class AsyncAccessKeysResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncAccessKeysResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/assistants/assistants.py b/src/agility/resources/assistants/assistants.py index 69d40fe..232caa8 100644 --- a/src/agility/resources/assistants/assistants.py +++ b/src/agility/resources/assistants/assistants.py @@ -46,7 +46,7 @@ def access_keys(self) -> AccessKeysResource: @cached_property def with_raw_response(self) -> AssistantsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -68,8 +68,11 @@ def create( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, @@ -91,6 +94,10 @@ def create( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -110,8 +117,11 @@ def create( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, @@ -166,8 +176,11 @@ def update( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, @@ -189,6 +202,10 @@ def update( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -211,8 +228,11 @@ def update( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, @@ -312,7 +332,7 @@ def access_keys(self) -> AsyncAccessKeysResource: @cached_property def with_raw_response(self) -> AsyncAssistantsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -334,8 +354,11 @@ async def create( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_create_params.Tool]] | NotGiven = NOT_GIVEN, @@ -357,6 +380,10 @@ async def create( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -376,8 +403,11 @@ async def create( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, @@ -432,8 +462,11 @@ async def update( description: str, knowledge_base_id: Optional[str], name: str, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, + logo_s3_key: Optional[str] | NotGiven = NOT_GIVEN, + logo_text: Optional[str] | NotGiven = NOT_GIVEN, model: Optional[Literal["gpt-4o"]] | NotGiven = NOT_GIVEN, suggested_questions: List[str] | NotGiven = NOT_GIVEN, tools: Optional[Iterable[assistant_update_params.Tool]] | NotGiven = NOT_GIVEN, @@ -455,6 +488,10 @@ async def update( context_limit: The maximum number of context chunks to include in a run. + logo_s3_key: S3 object key to the assistant's logo image + + logo_text: Text to display alongside the assistant's logo + suggested_questions: A list of suggested questions that can be asked to the assistant url_slug: Optional URL suffix - unique identifier for the assistant's endpoint @@ -477,8 +514,11 @@ async def update( "description": description, "knowledge_base_id": knowledge_base_id, "name": name, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, + "logo_s3_key": logo_s3_key, + "logo_text": logo_text, "model": model, "suggested_questions": suggested_questions, "tools": tools, diff --git a/src/agility/resources/integrations/available.py b/src/agility/resources/integrations/available.py index 0f77dea..380c3d3 100644 --- a/src/agility/resources/integrations/available.py +++ b/src/agility/resources/integrations/available.py @@ -23,7 +23,7 @@ class AvailableResource(SyncAPIResource): @cached_property def with_raw_response(self) -> AvailableResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -63,7 +63,7 @@ class AsyncAvailableResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncAvailableResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/integrations/integrations.py b/src/agility/resources/integrations/integrations.py index 29d0539..536398a 100644 --- a/src/agility/resources/integrations/integrations.py +++ b/src/agility/resources/integrations/integrations.py @@ -57,7 +57,7 @@ def rbac(self) -> RbacResource: @cached_property def with_raw_response(self) -> IntegrationsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -242,7 +242,7 @@ def rbac(self) -> AsyncRbacResource: @cached_property def with_raw_response(self) -> AsyncIntegrationsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/integrations/rbac.py b/src/agility/resources/integrations/rbac.py index a758cde..40e690a 100644 --- a/src/agility/resources/integrations/rbac.py +++ b/src/agility/resources/integrations/rbac.py @@ -23,7 +23,7 @@ class RbacResource(SyncAPIResource): @cached_property def with_raw_response(self) -> RbacResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -77,7 +77,7 @@ class AsyncRbacResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRbacResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/knowledge_bases/knowledge_bases.py b/src/agility/resources/knowledge_bases/knowledge_bases.py index 02681c0..3d6cf25 100644 --- a/src/agility/resources/knowledge_bases/knowledge_bases.py +++ b/src/agility/resources/knowledge_bases/knowledge_bases.py @@ -46,7 +46,7 @@ def sources(self) -> SourcesResource: @cached_property def with_raw_response(self) -> KnowledgeBasesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -79,9 +79,7 @@ def create( Create a new knowledge base. Args: - ingestion_pipeline_params: Knowledge base pipeline params. - - Parameters defined on the knowledge-base level for a pipeline. + ingestion_pipeline_params: Knowledge base pipeline params input. extra_headers: Send extra headers @@ -274,7 +272,7 @@ def sources(self) -> AsyncSourcesResource: @cached_property def with_raw_response(self) -> AsyncKnowledgeBasesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -307,9 +305,7 @@ async def create( Create a new knowledge base. Args: - ingestion_pipeline_params: Knowledge base pipeline params. - - Parameters defined on the knowledge-base level for a pipeline. + ingestion_pipeline_params: Knowledge base pipeline params input. extra_headers: Send extra headers diff --git a/src/agility/resources/knowledge_bases/sources/documents.py b/src/agility/resources/knowledge_bases/sources/documents.py index e788eeb..05a3833 100644 --- a/src/agility/resources/knowledge_bases/sources/documents.py +++ b/src/agility/resources/knowledge_bases/sources/documents.py @@ -26,7 +26,7 @@ class DocumentsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> DocumentsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -135,7 +135,7 @@ class AsyncDocumentsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncDocumentsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/knowledge_bases/sources/sources.py b/src/agility/resources/knowledge_bases/sources/sources.py index 93f8987..06bc2a6 100644 --- a/src/agility/resources/knowledge_bases/sources/sources.py +++ b/src/agility/resources/knowledge_bases/sources/sources.py @@ -42,7 +42,7 @@ def documents(self) -> DocumentsResource: @cached_property def with_raw_response(self) -> SourcesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -371,7 +371,7 @@ def documents(self) -> AsyncDocumentsResource: @cached_property def with_raw_response(self) -> AsyncSourcesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/threads/messages.py b/src/agility/resources/threads/messages.py index 2b7d7f3..c1e7fd9 100644 --- a/src/agility/resources/threads/messages.py +++ b/src/agility/resources/threads/messages.py @@ -32,7 +32,7 @@ class MessagesResource(SyncAPIResource): @cached_property def with_raw_response(self) -> MessagesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -216,7 +216,7 @@ class AsyncMessagesResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/threads/runs.py b/src/agility/resources/threads/runs.py index 0c706ba..a55809a 100644 --- a/src/agility/resources/threads/runs.py +++ b/src/agility/resources/threads/runs.py @@ -31,7 +31,7 @@ class RunsResource(SyncAPIResource): @cached_property def with_raw_response(self) -> RunsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -54,6 +54,7 @@ def create( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -89,6 +90,7 @@ def create( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, @@ -183,6 +185,7 @@ def stream( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -218,6 +221,7 @@ def stream( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, @@ -237,7 +241,7 @@ class AsyncRunsResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncRunsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -260,6 +264,7 @@ async def create( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_create_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -295,6 +300,7 @@ async def create( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, @@ -389,6 +395,7 @@ async def stream( assistant_id: str, additional_instructions: Optional[str] | NotGiven = NOT_GIVEN, additional_messages: Iterable[run_stream_params.AdditionalMessage] | NotGiven = NOT_GIVEN, + codex_access_key: Optional[str] | NotGiven = NOT_GIVEN, context_limit: Optional[int] | NotGiven = NOT_GIVEN, instructions: Optional[str] | NotGiven = NOT_GIVEN, knowledge_base_id: Optional[str] | NotGiven = NOT_GIVEN, @@ -424,6 +431,7 @@ async def stream( "assistant_id": assistant_id, "additional_instructions": additional_instructions, "additional_messages": additional_messages, + "codex_access_key": codex_access_key, "context_limit": context_limit, "instructions": instructions, "knowledge_base_id": knowledge_base_id, diff --git a/src/agility/resources/threads/threads.py b/src/agility/resources/threads/threads.py index 6d57caa..de35eef 100644 --- a/src/agility/resources/threads/threads.py +++ b/src/agility/resources/threads/threads.py @@ -50,7 +50,7 @@ def runs(self) -> RunsResource: @cached_property def with_raw_response(self) -> ThreadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -208,7 +208,7 @@ def runs(self) -> AsyncRunsResource: @cached_property def with_raw_response(self) -> AsyncThreadsResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/users/api_key.py b/src/agility/resources/users/api_key.py index c21e5ff..fa6cac6 100644 --- a/src/agility/resources/users/api_key.py +++ b/src/agility/resources/users/api_key.py @@ -23,7 +23,7 @@ class APIKeyResource(SyncAPIResource): @cached_property def with_raw_response(self) -> APIKeyResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -110,7 +110,7 @@ class AsyncAPIKeyResource(AsyncAPIResource): @cached_property def with_raw_response(self) -> AsyncAPIKeyResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/resources/users/users.py b/src/agility/resources/users/users.py index 16a7b3d..ceea91c 100644 --- a/src/agility/resources/users/users.py +++ b/src/agility/resources/users/users.py @@ -35,7 +35,7 @@ def api_key(self) -> APIKeyResource: @cached_property def with_raw_response(self) -> UsersResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers @@ -93,7 +93,7 @@ def api_key(self) -> AsyncAPIKeyResource: @cached_property def with_raw_response(self) -> AsyncUsersResourceWithRawResponse: """ - This property can be used as a prefix for any HTTP method call to return the + This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/stainless-sdks/agility-python#accessing-raw-response-data-eg-headers diff --git a/src/agility/types/assistant.py b/src/agility/types/assistant.py index 52bf49d..118c9ef 100644 --- a/src/agility/types/assistant.py +++ b/src/agility/types/assistant.py @@ -23,6 +23,12 @@ class Assistant(BaseModel): updated_at: datetime + logo_s3_key: Optional[str] = None + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] = None + """Text to display alongside the assistant's logo""" + suggested_questions: Optional[List[str]] = None """A list of suggested questions that can be asked to the assistant""" diff --git a/src/agility/types/assistant_create_params.py b/src/agility/types/assistant_create_params.py index 53b5790..1bc8eec 100644 --- a/src/agility/types/assistant_create_params.py +++ b/src/agility/types/assistant_create_params.py @@ -5,7 +5,7 @@ from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["AssistantCreateParams", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantCreateParams", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] class AssistantCreateParams(TypedDict, total=False): @@ -17,11 +17,19 @@ class AssistantCreateParams(TypedDict, total=False): name: Required[str] """The name of the assistant""" + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include in a run.""" instructions: Optional[str] + logo_s3_key: Optional[str] + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] suggested_questions: List[str] @@ -33,16 +41,14 @@ class AssistantCreateParams(TypedDict, total=False): """Optional URL suffix - unique identifier for the assistant's endpoint""" -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/src/agility/types/assistant_list_response.py b/src/agility/types/assistant_list_response.py index ddd1f80..5ccc27a 100644 --- a/src/agility/types/assistant_list_response.py +++ b/src/agility/types/assistant_list_response.py @@ -6,22 +6,20 @@ from .._models import BaseModel -__all__ = ["AssistantListResponse", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantListResponse", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] -class ToolAlphaV0Tool(BaseModel): +class ToolCodexV0Tool(BaseModel): access_key: str - project_id: int - - name: Optional[Literal["alpha_v0"]] = None + type: Optional[Literal["codex_v0"]] = None class ToolNoOpTool(BaseModel): - name: Optional[Literal["noop"]] = None + type: Optional[Literal["noop"]] = None -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] class AssistantListResponse(BaseModel): @@ -36,18 +34,26 @@ class AssistantListResponse(BaseModel): knowledge_base_id: Optional[str] = None - knowledge_base_name: str + knowledge_base_name: Optional[str] = None name: str """The name of the assistant""" updated_at: datetime + codex_access_key: Optional[str] = None + context_limit: Optional[int] = None """The maximum number of context chunks to include in a run.""" instructions: Optional[str] = None + logo_s3_key: Optional[str] = None + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] = None + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] = None suggested_questions: Optional[List[str]] = None diff --git a/src/agility/types/assistant_update_params.py b/src/agility/types/assistant_update_params.py index d107d1d..a97c2db 100644 --- a/src/agility/types/assistant_update_params.py +++ b/src/agility/types/assistant_update_params.py @@ -5,7 +5,7 @@ from typing import List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict -__all__ = ["AssistantUpdateParams", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantUpdateParams", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] class AssistantUpdateParams(TypedDict, total=False): @@ -19,11 +19,19 @@ class AssistantUpdateParams(TypedDict, total=False): name: Required[str] """The name of the assistant""" + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include in a run.""" instructions: Optional[str] + logo_s3_key: Optional[str] + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] suggested_questions: List[str] @@ -35,16 +43,14 @@ class AssistantUpdateParams(TypedDict, total=False): """Optional URL suffix - unique identifier for the assistant's endpoint""" -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/src/agility/types/assistant_with_config.py b/src/agility/types/assistant_with_config.py index 23a651d..fd3a213 100644 --- a/src/agility/types/assistant_with_config.py +++ b/src/agility/types/assistant_with_config.py @@ -6,22 +6,20 @@ from .._models import BaseModel -__all__ = ["AssistantWithConfig", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool"] +__all__ = ["AssistantWithConfig", "Tool", "ToolCodexV0Tool", "ToolNoOpTool"] -class ToolAlphaV0Tool(BaseModel): +class ToolCodexV0Tool(BaseModel): access_key: str - project_id: int - - name: Optional[Literal["alpha_v0"]] = None + type: Optional[Literal["codex_v0"]] = None class ToolNoOpTool(BaseModel): - name: Optional[Literal["noop"]] = None + type: Optional[Literal["noop"]] = None -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] class AssistantWithConfig(BaseModel): @@ -41,11 +39,19 @@ class AssistantWithConfig(BaseModel): updated_at: datetime + codex_access_key: Optional[str] = None + context_limit: Optional[int] = None """The maximum number of context chunks to include in a run.""" instructions: Optional[str] = None + logo_s3_key: Optional[str] = None + """S3 object key to the assistant's logo image""" + + logo_text: Optional[str] = None + """Text to display alongside the assistant's logo""" + model: Optional[Literal["gpt-4o"]] = None suggested_questions: Optional[List[str]] = None diff --git a/src/agility/types/integration_create_response.py b/src/agility/types/integration_create_response.py index 91b0891..8a3054c 100644 --- a/src/agility/types/integration_create_response.py +++ b/src/agility/types/integration_create_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import Dict, Union, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -21,7 +21,7 @@ class NotionV0IntegrationTokenNotionAccessToken(BaseModel): bot_id: str - owner: object + owner: Dict[str, object] workspace_id: str diff --git a/src/agility/types/integration_list_response.py b/src/agility/types/integration_list_response.py index 96b8e15..704a2f4 100644 --- a/src/agility/types/integration_list_response.py +++ b/src/agility/types/integration_list_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import Dict, Union, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -21,7 +21,7 @@ class NotionV0IntegrationTokenNotionAccessToken(BaseModel): bot_id: str - owner: object + owner: Dict[str, object] workspace_id: str diff --git a/src/agility/types/integration_retrieve_response.py b/src/agility/types/integration_retrieve_response.py index aa3aef3..1524b3b 100644 --- a/src/agility/types/integration_retrieve_response.py +++ b/src/agility/types/integration_retrieve_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Union, Optional +from typing import Dict, Union, Optional from typing_extensions import Literal, TypeAlias from .._models import BaseModel @@ -21,7 +21,7 @@ class NotionV0IntegrationTokenNotionAccessToken(BaseModel): bot_id: str - owner: object + owner: Dict[str, object] workspace_id: str diff --git a/src/agility/types/knowledge_base_create_params.py b/src/agility/types/knowledge_base_create_params.py index 36e547e..8503149 100644 --- a/src/agility/types/knowledge_base_create_params.py +++ b/src/agility/types/knowledge_base_create_params.py @@ -31,10 +31,7 @@ class KnowledgeBaseCreateParams(TypedDict, total=False): description: Required[str] ingestion_pipeline_params: Required[IngestionPipelineParams] - """Knowledge base pipeline params. - - Parameters defined on the knowledge-base level for a pipeline. - """ + """Knowledge base pipeline params input.""" name: Required[str] @@ -152,12 +149,6 @@ class IngestionPipelineParamsTransform(TypedDict, total=False): class IngestionPipelineParamsVectorStore(TypedDict, total=False): - weaviate_collection_name: Required[str] - """The name of the Weaviate collection to use for storing documents. - - Must start with AgilityKB and be valid. - """ - node_tags: Dict[str, str] @@ -180,4 +171,4 @@ class IngestionPipelineParams(TypedDict, total=False): """ vector_store: Required[IngestionPipelineParamsVectorStore] - """Vector store params.""" + """Vector store params input.""" diff --git a/src/agility/types/s3_v0_integration.py b/src/agility/types/s3_v0_integration.py index e9b09f9..e129a8e 100644 --- a/src/agility/types/s3_v0_integration.py +++ b/src/agility/types/s3_v0_integration.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from typing_extensions import Literal from .._models import BaseModel @@ -17,7 +17,7 @@ class ResourceAccessDefinitionResource(BaseModel): class ResourceAccessDefinition(BaseModel): - policy: object + policy: Dict[str, object] resource: ResourceAccessDefinitionResource diff --git a/src/agility/types/threads/message.py b/src/agility/types/threads/message.py index c5d56cf..45aa481 100644 --- a/src/agility/types/threads/message.py +++ b/src/agility/types/threads/message.py @@ -1,17 +1,31 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Dict, List, Optional from datetime import datetime from typing_extensions import Literal from ..._models import BaseModel -__all__ = ["Message", "Metadata"] +__all__ = ["Message", "Metadata", "MetadataScores"] + + +class MetadataScores(BaseModel): + response_helpfulness: Optional[Dict[str, object]] = None + + trustworthiness: Optional[Dict[str, object]] = None class Metadata(BaseModel): citations: Optional[List[str]] = None + is_bad_response: Optional[bool] = None + + is_expert_answer: Optional[bool] = None + + scores: Optional[MetadataScores] = None + + trustworthiness_explanation: Optional[str] = None + trustworthiness_score: Optional[float] = None diff --git a/src/agility/types/threads/message_create_params.py b/src/agility/types/threads/message_create_params.py index cccbb06..a11ef3c 100644 --- a/src/agility/types/threads/message_create_params.py +++ b/src/agility/types/threads/message_create_params.py @@ -2,10 +2,10 @@ from __future__ import annotations -from typing import List, Optional +from typing import Dict, List, Optional from typing_extensions import Literal, Required, TypedDict -__all__ = ["MessageCreateParams", "Metadata"] +__all__ = ["MessageCreateParams", "Metadata", "MetadataScores"] class MessageCreateParams(TypedDict, total=False): @@ -16,7 +16,21 @@ class MessageCreateParams(TypedDict, total=False): role: Required[Literal["user", "assistant"]] +class MetadataScores(TypedDict, total=False): + response_helpfulness: Optional[Dict[str, object]] + + trustworthiness: Optional[Dict[str, object]] + + class Metadata(TypedDict, total=False): citations: Optional[List[str]] + is_bad_response: Optional[bool] + + is_expert_answer: Optional[bool] + + scores: Optional[MetadataScores] + + trustworthiness_explanation: Optional[str] + trustworthiness_score: Optional[float] diff --git a/src/agility/types/threads/run.py b/src/agility/types/threads/run.py index 627f62b..4d0e03d 100644 --- a/src/agility/types/threads/run.py +++ b/src/agility/types/threads/run.py @@ -6,22 +6,20 @@ from ..._models import BaseModel -__all__ = ["Run", "Tool", "ToolAlphaV0Tool", "ToolNoOpTool", "Usage"] +__all__ = ["Run", "Tool", "ToolCodexV0Tool", "ToolNoOpTool", "Usage"] -class ToolAlphaV0Tool(BaseModel): +class ToolCodexV0Tool(BaseModel): access_key: str - project_id: int - - name: Optional[Literal["alpha_v0"]] = None + type: Optional[Literal["codex_v0"]] = None class ToolNoOpTool(BaseModel): - name: Optional[Literal["noop"]] = None + type: Optional[Literal["noop"]] = None -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] class Usage(BaseModel): @@ -47,6 +45,8 @@ class Run(BaseModel): additional_instructions: Optional[str] = None + codex_access_key: Optional[str] = None + context_limit: Optional[int] = None """The maximum number of context chunks to include.""" diff --git a/src/agility/types/threads/run_create_params.py b/src/agility/types/threads/run_create_params.py index 6ab6ba2..13295cf 100644 --- a/src/agility/types/threads/run_create_params.py +++ b/src/agility/types/threads/run_create_params.py @@ -2,15 +2,16 @@ from __future__ import annotations -from typing import List, Union, Iterable, Optional +from typing import Dict, List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict __all__ = [ "RunCreateParams", "AdditionalMessage", "AdditionalMessageMetadata", + "AdditionalMessageMetadataScores", "Tool", - "ToolAlphaV0Tool", + "ToolCodexV0Tool", "ToolNoOpTool", ] @@ -22,6 +23,8 @@ class RunCreateParams(TypedDict, total=False): additional_messages: Iterable[AdditionalMessage] + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include.""" @@ -34,9 +37,23 @@ class RunCreateParams(TypedDict, total=False): tools: Optional[Iterable[Tool]] +class AdditionalMessageMetadataScores(TypedDict, total=False): + response_helpfulness: Optional[Dict[str, object]] + + trustworthiness: Optional[Dict[str, object]] + + class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] + is_bad_response: Optional[bool] + + is_expert_answer: Optional[bool] + + scores: Optional[AdditionalMessageMetadataScores] + + trustworthiness_explanation: Optional[str] + trustworthiness_score: Optional[float] @@ -50,16 +67,14 @@ class AdditionalMessage(TypedDict, total=False): thread_id: Required[str] -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/src/agility/types/threads/run_stream_params.py b/src/agility/types/threads/run_stream_params.py index eecc6a5..73171b0 100644 --- a/src/agility/types/threads/run_stream_params.py +++ b/src/agility/types/threads/run_stream_params.py @@ -2,15 +2,16 @@ from __future__ import annotations -from typing import List, Union, Iterable, Optional +from typing import Dict, List, Union, Iterable, Optional from typing_extensions import Literal, Required, TypeAlias, TypedDict __all__ = [ "RunStreamParams", "AdditionalMessage", "AdditionalMessageMetadata", + "AdditionalMessageMetadataScores", "Tool", - "ToolAlphaV0Tool", + "ToolCodexV0Tool", "ToolNoOpTool", ] @@ -22,6 +23,8 @@ class RunStreamParams(TypedDict, total=False): additional_messages: Iterable[AdditionalMessage] + codex_access_key: Optional[str] + context_limit: Optional[int] """The maximum number of context chunks to include.""" @@ -34,9 +37,23 @@ class RunStreamParams(TypedDict, total=False): tools: Optional[Iterable[Tool]] +class AdditionalMessageMetadataScores(TypedDict, total=False): + response_helpfulness: Optional[Dict[str, object]] + + trustworthiness: Optional[Dict[str, object]] + + class AdditionalMessageMetadata(TypedDict, total=False): citations: Optional[List[str]] + is_bad_response: Optional[bool] + + is_expert_answer: Optional[bool] + + scores: Optional[AdditionalMessageMetadataScores] + + trustworthiness_explanation: Optional[str] + trustworthiness_score: Optional[float] @@ -50,16 +67,14 @@ class AdditionalMessage(TypedDict, total=False): thread_id: Required[str] -class ToolAlphaV0Tool(TypedDict, total=False): +class ToolCodexV0Tool(TypedDict, total=False): access_key: Required[str] - project_id: Required[int] - - name: Literal["alpha_v0"] + type: Literal["codex_v0"] class ToolNoOpTool(TypedDict, total=False): - name: Literal["noop"] + type: Literal["noop"] -Tool: TypeAlias = Union[ToolAlphaV0Tool, ToolNoOpTool] +Tool: TypeAlias = Union[ToolCodexV0Tool, ToolNoOpTool] diff --git a/tests/api_resources/knowledge_bases/test_sources.py b/tests/api_resources/knowledge_bases/test_sources.py index f4bcffa..eb1ba6c 100644 --- a/tests/api_resources/knowledge_bases/test_sources.py +++ b/tests/api_resources/knowledge_bases/test_sources.py @@ -27,7 +27,10 @@ def test_method_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -71,7 +74,10 @@ def test_raw_response_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -89,7 +95,10 @@ def test_streaming_response_create(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -110,7 +119,10 @@ def test_path_params_create(self, client: Agility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -172,7 +184,10 @@ def test_method_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -218,7 +233,10 @@ def test_raw_response_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -237,7 +255,10 @@ def test_streaming_response_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -259,7 +280,10 @@ def test_path_params_update(self, client: Agility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -272,7 +296,10 @@ def test_path_params_update(self, client: Agility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -480,7 +507,10 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -524,7 +554,10 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -542,7 +575,10 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -563,7 +599,10 @@ async def test_path_params_create(self, async_client: AsyncAgility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -625,7 +664,10 @@ async def test_method_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -671,7 +713,10 @@ async def test_raw_response_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -690,7 +735,10 @@ async def test_streaming_response_update(self, async_client: AsyncAgility) -> No knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -712,7 +760,10 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, @@ -725,7 +776,10 @@ async def test_path_params_update(self, async_client: AsyncAgility) -> None: knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", description="description", name="name", - source_params={"urls": ["string"]}, + source_params={ + "urls": ["string"], + "name": "web_v0", + }, source_schedule={ "cron": "cron", "utc_offset": 0, diff --git a/tests/api_resources/test_assistants.py b/tests/api_resources/test_assistants.py index 8dcc938..c0cf764 100644 --- a/tests/api_resources/test_assistants.py +++ b/tests/api_resources/test_assistants.py @@ -37,15 +37,17 @@ def test_method_create_with_all_params(self, client: Agility) -> None: description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", @@ -137,15 +139,17 @@ def test_method_update_with_all_params(self, client: Agility) -> None: description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", @@ -285,15 +289,17 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", @@ -385,15 +391,17 @@ async def test_method_update_with_all_params(self, async_client: AsyncAgility) - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name", + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", + logo_s3_key="logo_s3_key", + logo_text="logo_text", model="gpt-4o", suggested_questions=["string"], tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], url_slug="url_slug", diff --git a/tests/api_resources/test_integrations.py b/tests/api_resources/test_integrations.py index 5b92346..82d4400 100644 --- a/tests/api_resources/test_integrations.py +++ b/tests/api_resources/test_integrations.py @@ -29,7 +29,8 @@ def test_method_create(self, client: Agility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) @@ -56,7 +57,8 @@ def test_raw_response_create(self, client: Agility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) @@ -72,7 +74,8 @@ def test_streaming_response_create(self, client: Agility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) as response: assert not response.is_closed @@ -203,7 +206,8 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) assert_matches_type(IntegrationCreateResponse, integration, path=["response"]) @@ -230,7 +234,8 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) @@ -246,7 +251,8 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No "resource": { "bucket_name": "bucket_name", "prefix": "prefix", - } + }, + "integration_type": "s3/v0", }, ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_knowledge_bases.py b/tests/api_resources/test_knowledge_bases.py index 2e915ba..1f7410f 100644 --- a/tests/api_resources/test_knowledge_bases.py +++ b/tests/api_resources/test_knowledge_bases.py @@ -29,7 +29,7 @@ def test_method_create(self, client: Agility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -43,7 +43,7 @@ def test_raw_response_create(self, client: Agility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -61,7 +61,7 @@ def test_streaming_response_create(self, client: Agility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) as response: @@ -264,7 +264,7 @@ async def test_method_create(self, async_client: AsyncAgility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -278,7 +278,7 @@ async def test_raw_response_create(self, async_client: AsyncAgility) -> None: "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) @@ -296,7 +296,7 @@ async def test_streaming_response_create(self, async_client: AsyncAgility) -> No "curate": {}, "curate_document_store": {}, "transform": {}, - "vector_store": {"weaviate_collection_name": "weaviate_collection_name"}, + "vector_store": {}, }, name="name", ) as response: diff --git a/tests/api_resources/threads/test_messages.py b/tests/api_resources/threads/test_messages.py index 6c7578b..f65ed3d 100644 --- a/tests/api_resources/threads/test_messages.py +++ b/tests/api_resources/threads/test_messages.py @@ -35,6 +35,13 @@ def test_method_create_with_all_params(self, client: Agility) -> None: content="content", metadata={ "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, role="user", @@ -245,6 +252,13 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - content="content", metadata={ "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, role="user", diff --git a/tests/api_resources/threads/test_runs.py b/tests/api_resources/threads/test_runs.py index 4de25cf..af3e153 100644 --- a/tests/api_resources/threads/test_runs.py +++ b/tests/api_resources/threads/test_runs.py @@ -36,12 +36,20 @@ def test_method_create_with_all_params(self, client: Agility) -> None: "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -49,8 +57,7 @@ def test_method_create_with_all_params(self, client: Agility) -> None: tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) @@ -205,12 +212,20 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -218,8 +233,7 @@ def test_method_stream_with_all_params(self, client: Agility) -> None: tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) @@ -282,12 +296,20 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -295,8 +317,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncAgility) - tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) @@ -451,12 +472,20 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - "content": "content", "metadata": { "citations": ["string"], + "is_bad_response": True, + "is_expert_answer": True, + "scores": { + "response_helpfulness": {"foo": "bar"}, + "trustworthiness": {"foo": "bar"}, + }, + "trustworthiness_explanation": "trustworthiness_explanation", "trustworthiness_score": 0, }, "role": "user", "thread_id": "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", } ], + codex_access_key="codex_access_key", context_limit=1, instructions="instructions", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", @@ -464,8 +493,7 @@ async def test_method_stream_with_all_params(self, async_client: AsyncAgility) - tools=[ { "access_key": "access_key", - "project_id": 0, - "name": "alpha_v0", + "type": "codex_v0", } ], ) diff --git a/tests/test_client.py b/tests/test_client.py index 92cb3a5..68bc2b7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -6,6 +6,7 @@ import os import sys import json +import time import asyncio import inspect import subprocess @@ -22,6 +23,7 @@ from agility import Agility, AsyncAgility, APIResponseValidationError from agility._types import Omit +from agility._utils import maybe_transform from agility._models import BaseModel, FinalRequestOptions from agility._constants import RAW_RESPONSE_HEADER from agility._exceptions import APIStatusError, APITimeoutError, APIResponseValidationError @@ -31,6 +33,7 @@ BaseClient, make_request_options, ) +from agility.types.assistant_create_params import AssistantCreateParams from .utils import update_env @@ -714,8 +717,13 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> No "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -734,8 +742,13 @@ def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> Non "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -1512,8 +1525,13 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -1532,8 +1550,13 @@ async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) "/api/assistants/", body=cast( object, - dict( - description="description", knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", name="name" + maybe_transform( + dict( + description="description", + knowledge_base_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="name", + ), + AssistantCreateParams, ), ), cast_to=httpx.Response, @@ -1646,7 +1669,7 @@ def test_get_platform(self) -> None: import threading from agility._utils import asyncify - from agility._base_client import get_platform + from agility._base_client import get_platform async def test_main() -> None: result = await asyncify(get_platform)() @@ -1661,10 +1684,20 @@ async def test_main() -> None: [sys.executable, "-c", test_code], text=True, ) as process: - try: - process.wait(2) - if process.returncode: - raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") - except subprocess.TimeoutExpired as e: - process.kill() - raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e + timeout = 10 # seconds + + start_time = time.monotonic() + while True: + return_code = process.poll() + if return_code is not None: + if return_code != 0: + raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code") + + # success + break + + if time.monotonic() - start_time > timeout: + process.kill() + raise AssertionError("calling get_platform using asyncify resulted in a hung process") + + time.sleep(0.1) diff --git a/tests/test_models.py b/tests/test_models.py index a224db3..51b9989 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -844,3 +844,45 @@ class Model(BaseModel): assert m.alias == "foo" assert isinstance(m.union, str) assert m.union == "bar" + + +@pytest.mark.skipif(not PYDANTIC_V2, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) diff --git a/tests/test_transform.py b/tests/test_transform.py index bdb468b..11784bf 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -2,13 +2,13 @@ import io import pathlib -from typing import Any, List, Union, TypeVar, Iterable, Optional, cast +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast from datetime import date, datetime from typing_extensions import Required, Annotated, TypedDict import pytest -from agility._types import Base64FileInput +from agility._types import NOT_GIVEN, Base64FileInput from agility._utils import ( PropertyInfo, transform as _transform, @@ -388,6 +388,15 @@ def my_iter() -> Iterable[Baz8]: } +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + class TypedDictIterableUnionStr(TypedDict): foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] @@ -423,3 +432,22 @@ async def test_base64_file_input(use_async: bool) -> None: assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { "foo": "SGVsbG8sIHdvcmxkIQ==" } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": NOT_GIVEN}, Foo1, use_async) == {}