From aec7892b063c00b730afcdc440c0fa3ebe1cdae8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:18:43 +0000 Subject: [PATCH 01/12] chore(internal): add missing files argument to base client --- src/imagekitio/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/imagekitio/_base_client.py b/src/imagekitio/_base_client.py index 384e7c0..542a71b 100644 --- a/src/imagekitio/_base_client.py +++ b/src/imagekitio/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From ad1da84adad57d0a64a8f06a04c6ddb6b8f0e96b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:27:03 +0000 Subject: [PATCH 02/12] chore: speedup initial import --- src/imagekitio/_client.py | 451 +++++++++++++++++++++++++++++--------- 1 file changed, 353 insertions(+), 98 deletions(-) diff --git a/src/imagekitio/_client.py b/src/imagekitio/_client.py index 3b9f4ae..8a76fd5 100644 --- a/src/imagekitio/_client.py +++ b/src/imagekitio/_client.py @@ -4,14 +4,13 @@ import os import base64 -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx from . import _exceptions from ._qs import Querystring -from .lib import helper from ._types import ( Omit, Headers, @@ -23,8 +22,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import dummy, assets, webhooks, custom_metadata_fields from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import ImageKitError, APIStatusError from ._base_client import ( @@ -32,11 +31,19 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.beta import beta -from .resources.cache import cache -from .resources.files import files -from .resources.folders import folders -from .resources.accounts import accounts + +if TYPE_CHECKING: + from .resources import beta, cache, dummy, files, assets, folders, accounts, custom_metadata_fields + from .resources.dummy import DummyResource, AsyncDummyResource + from .resources.assets import AssetsResource, AsyncAssetsResource + from .resources.webhooks import WebhooksResource, AsyncWebhooksResource + from .resources.beta.beta import BetaResource, AsyncBetaResource + from .resources.cache.cache import CacheResource, AsyncCacheResource + from .resources.files.files import FilesResource, AsyncFilesResource + from .resources.folders.folders import FoldersResource, AsyncFoldersResource + from .resources.accounts.accounts import AccountsResource, AsyncAccountsResource + from .resources.custom_metadata_fields import CustomMetadataFieldsResource, AsyncCustomMetadataFieldsResource + from .lib.helper import HelperResource, AsyncHelperResource __all__ = [ "Timeout", @@ -51,19 +58,6 @@ class ImageKit(SyncAPIClient): - dummy: dummy.DummyResource - custom_metadata_fields: custom_metadata_fields.CustomMetadataFieldsResource - files: files.FilesResource - assets: assets.AssetsResource - cache: cache.CacheResource - folders: folders.FoldersResource - accounts: accounts.AccountsResource - beta: beta.BetaResource - webhooks: webhooks.WebhooksResource - helper: helper.HelperResource - with_raw_response: ImageKitWithRawResponse - with_streaming_response: ImageKitWithStreamedResponse - # client options private_key: str password: str | None @@ -134,18 +128,73 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.dummy = dummy.DummyResource(self) - self.custom_metadata_fields = custom_metadata_fields.CustomMetadataFieldsResource(self) - self.files = files.FilesResource(self) - self.assets = assets.AssetsResource(self) - self.cache = cache.CacheResource(self) - self.folders = folders.FoldersResource(self) - self.accounts = accounts.AccountsResource(self) - self.beta = beta.BetaResource(self) - self.webhooks = webhooks.WebhooksResource(self) - self.helper = helper.HelperResource(self) - self.with_raw_response = ImageKitWithRawResponse(self) - self.with_streaming_response = ImageKitWithStreamedResponse(self) + @cached_property + def dummy(self) -> DummyResource: + from .resources.dummy import DummyResource + + return DummyResource(self) + + @cached_property + def custom_metadata_fields(self) -> CustomMetadataFieldsResource: + from .resources.custom_metadata_fields import CustomMetadataFieldsResource + + return CustomMetadataFieldsResource(self) + + @cached_property + def files(self) -> FilesResource: + from .resources.files import FilesResource + + return FilesResource(self) + + @cached_property + def assets(self) -> AssetsResource: + from .resources.assets import AssetsResource + + return AssetsResource(self) + + @cached_property + def cache(self) -> CacheResource: + from .resources.cache import CacheResource + + return CacheResource(self) + + @cached_property + def folders(self) -> FoldersResource: + from .resources.folders import FoldersResource + + return FoldersResource(self) + + @cached_property + def accounts(self) -> AccountsResource: + from .resources.accounts import AccountsResource + + return AccountsResource(self) + + @cached_property + def beta(self) -> BetaResource: + from .resources.beta import BetaResource + + return BetaResource(self) + + @cached_property + def webhooks(self) -> WebhooksResource: + from .resources.webhooks import WebhooksResource + + return WebhooksResource(self) + + @cached_property + def helper(self) -> HelperResource: + from .lib.helper import HelperResource + + return HelperResource(self) + + @cached_property + def with_raw_response(self) -> ImageKitWithRawResponse: + return ImageKitWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ImageKitWithStreamedResponse: + return ImageKitWithStreamedResponse(self) @property @override @@ -273,19 +322,6 @@ def _make_status_error( class AsyncImageKit(AsyncAPIClient): - dummy: dummy.AsyncDummyResource - custom_metadata_fields: custom_metadata_fields.AsyncCustomMetadataFieldsResource - files: files.AsyncFilesResource - assets: assets.AsyncAssetsResource - cache: cache.AsyncCacheResource - folders: folders.AsyncFoldersResource - accounts: accounts.AsyncAccountsResource - beta: beta.AsyncBetaResource - webhooks: webhooks.AsyncWebhooksResource - helper: helper.AsyncHelperResource - with_raw_response: AsyncImageKitWithRawResponse - with_streaming_response: AsyncImageKitWithStreamedResponse - # client options private_key: str password: str | None @@ -356,18 +392,73 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.dummy = dummy.AsyncDummyResource(self) - self.custom_metadata_fields = custom_metadata_fields.AsyncCustomMetadataFieldsResource(self) - self.files = files.AsyncFilesResource(self) - self.assets = assets.AsyncAssetsResource(self) - self.cache = cache.AsyncCacheResource(self) - self.folders = folders.AsyncFoldersResource(self) - self.accounts = accounts.AsyncAccountsResource(self) - self.beta = beta.AsyncBetaResource(self) - self.webhooks = webhooks.AsyncWebhooksResource(self) - self.helper = helper.AsyncHelperResource(self) - self.with_raw_response = AsyncImageKitWithRawResponse(self) - self.with_streaming_response = AsyncImageKitWithStreamedResponse(self) + @cached_property + def dummy(self) -> AsyncDummyResource: + from .resources.dummy import AsyncDummyResource + + return AsyncDummyResource(self) + + @cached_property + def custom_metadata_fields(self) -> AsyncCustomMetadataFieldsResource: + from .resources.custom_metadata_fields import AsyncCustomMetadataFieldsResource + + return AsyncCustomMetadataFieldsResource(self) + + @cached_property + def files(self) -> AsyncFilesResource: + from .resources.files import AsyncFilesResource + + return AsyncFilesResource(self) + + @cached_property + def assets(self) -> AsyncAssetsResource: + from .resources.assets import AsyncAssetsResource + + return AsyncAssetsResource(self) + + @cached_property + def cache(self) -> AsyncCacheResource: + from .resources.cache import AsyncCacheResource + + return AsyncCacheResource(self) + + @cached_property + def folders(self) -> AsyncFoldersResource: + from .resources.folders import AsyncFoldersResource + + return AsyncFoldersResource(self) + + @cached_property + def accounts(self) -> AsyncAccountsResource: + from .resources.accounts import AsyncAccountsResource + + return AsyncAccountsResource(self) + + @cached_property + def beta(self) -> AsyncBetaResource: + from .resources.beta import AsyncBetaResource + + return AsyncBetaResource(self) + + @cached_property + def webhooks(self) -> AsyncWebhooksResource: + from .resources.webhooks import AsyncWebhooksResource + + return AsyncWebhooksResource(self) + + @cached_property + def helper(self) -> AsyncHelperResource: + from .lib.helper import AsyncHelperResource + + return AsyncHelperResource(self) + + @cached_property + def with_raw_response(self) -> AsyncImageKitWithRawResponse: + return AsyncImageKitWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncImageKitWithStreamedResponse: + return AsyncImageKitWithStreamedResponse(self) @property @override @@ -495,59 +586,223 @@ def _make_status_error( class ImageKitWithRawResponse: + _client: ImageKit + def __init__(self, client: ImageKit) -> None: - self.dummy = dummy.DummyResourceWithRawResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.CustomMetadataFieldsResourceWithRawResponse( - client.custom_metadata_fields - ) - self.files = files.FilesResourceWithRawResponse(client.files) - self.assets = assets.AssetsResourceWithRawResponse(client.assets) - self.cache = cache.CacheResourceWithRawResponse(client.cache) - self.folders = folders.FoldersResourceWithRawResponse(client.folders) - self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts) - self.beta = beta.BetaResourceWithRawResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.DummyResourceWithRawResponse: + from .resources.dummy import DummyResourceWithRawResponse + + return DummyResourceWithRawResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.CustomMetadataFieldsResourceWithRawResponse: + from .resources.custom_metadata_fields import CustomMetadataFieldsResourceWithRawResponse + + return CustomMetadataFieldsResourceWithRawResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.FilesResourceWithRawResponse: + from .resources.files import FilesResourceWithRawResponse + + return FilesResourceWithRawResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AssetsResourceWithRawResponse: + from .resources.assets import AssetsResourceWithRawResponse + + return AssetsResourceWithRawResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.CacheResourceWithRawResponse: + from .resources.cache import CacheResourceWithRawResponse + + return CacheResourceWithRawResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.FoldersResourceWithRawResponse: + from .resources.folders import FoldersResourceWithRawResponse + + return FoldersResourceWithRawResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AccountsResourceWithRawResponse: + from .resources.accounts import AccountsResourceWithRawResponse + + return AccountsResourceWithRawResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.BetaResourceWithRawResponse: + from .resources.beta import BetaResourceWithRawResponse + + return BetaResourceWithRawResponse(self._client.beta) class AsyncImageKitWithRawResponse: + _client: AsyncImageKit + def __init__(self, client: AsyncImageKit) -> None: - self.dummy = dummy.AsyncDummyResourceWithRawResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithRawResponse( - client.custom_metadata_fields - ) - self.files = files.AsyncFilesResourceWithRawResponse(client.files) - self.assets = assets.AsyncAssetsResourceWithRawResponse(client.assets) - self.cache = cache.AsyncCacheResourceWithRawResponse(client.cache) - self.folders = folders.AsyncFoldersResourceWithRawResponse(client.folders) - self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts) - self.beta = beta.AsyncBetaResourceWithRawResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.AsyncDummyResourceWithRawResponse: + from .resources.dummy import AsyncDummyResourceWithRawResponse + + return AsyncDummyResourceWithRawResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithRawResponse: + from .resources.custom_metadata_fields import AsyncCustomMetadataFieldsResourceWithRawResponse + + return AsyncCustomMetadataFieldsResourceWithRawResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithRawResponse: + from .resources.files import AsyncFilesResourceWithRawResponse + + return AsyncFilesResourceWithRawResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AsyncAssetsResourceWithRawResponse: + from .resources.assets import AsyncAssetsResourceWithRawResponse + + return AsyncAssetsResourceWithRawResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.AsyncCacheResourceWithRawResponse: + from .resources.cache import AsyncCacheResourceWithRawResponse + + return AsyncCacheResourceWithRawResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.AsyncFoldersResourceWithRawResponse: + from .resources.folders import AsyncFoldersResourceWithRawResponse + + return AsyncFoldersResourceWithRawResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AsyncAccountsResourceWithRawResponse: + from .resources.accounts import AsyncAccountsResourceWithRawResponse + + return AsyncAccountsResourceWithRawResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.AsyncBetaResourceWithRawResponse: + from .resources.beta import AsyncBetaResourceWithRawResponse + + return AsyncBetaResourceWithRawResponse(self._client.beta) class ImageKitWithStreamedResponse: + _client: ImageKit + def __init__(self, client: ImageKit) -> None: - self.dummy = dummy.DummyResourceWithStreamingResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.CustomMetadataFieldsResourceWithStreamingResponse( - client.custom_metadata_fields - ) - self.files = files.FilesResourceWithStreamingResponse(client.files) - self.assets = assets.AssetsResourceWithStreamingResponse(client.assets) - self.cache = cache.CacheResourceWithStreamingResponse(client.cache) - self.folders = folders.FoldersResourceWithStreamingResponse(client.folders) - self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts) - self.beta = beta.BetaResourceWithStreamingResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.DummyResourceWithStreamingResponse: + from .resources.dummy import DummyResourceWithStreamingResponse + + return DummyResourceWithStreamingResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.CustomMetadataFieldsResourceWithStreamingResponse: + from .resources.custom_metadata_fields import CustomMetadataFieldsResourceWithStreamingResponse + + return CustomMetadataFieldsResourceWithStreamingResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.FilesResourceWithStreamingResponse: + from .resources.files import FilesResourceWithStreamingResponse + + return FilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AssetsResourceWithStreamingResponse: + from .resources.assets import AssetsResourceWithStreamingResponse + + return AssetsResourceWithStreamingResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.CacheResourceWithStreamingResponse: + from .resources.cache import CacheResourceWithStreamingResponse + + return CacheResourceWithStreamingResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.FoldersResourceWithStreamingResponse: + from .resources.folders import FoldersResourceWithStreamingResponse + + return FoldersResourceWithStreamingResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AccountsResourceWithStreamingResponse: + from .resources.accounts import AccountsResourceWithStreamingResponse + + return AccountsResourceWithStreamingResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.BetaResourceWithStreamingResponse: + from .resources.beta import BetaResourceWithStreamingResponse + + return BetaResourceWithStreamingResponse(self._client.beta) class AsyncImageKitWithStreamedResponse: + _client: AsyncImageKit + def __init__(self, client: AsyncImageKit) -> None: - self.dummy = dummy.AsyncDummyResourceWithStreamingResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithStreamingResponse( - client.custom_metadata_fields - ) - self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) - self.assets = assets.AsyncAssetsResourceWithStreamingResponse(client.assets) - self.cache = cache.AsyncCacheResourceWithStreamingResponse(client.cache) - self.folders = folders.AsyncFoldersResourceWithStreamingResponse(client.folders) - self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts) - self.beta = beta.AsyncBetaResourceWithStreamingResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.AsyncDummyResourceWithStreamingResponse: + from .resources.dummy import AsyncDummyResourceWithStreamingResponse + + return AsyncDummyResourceWithStreamingResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithStreamingResponse: + from .resources.custom_metadata_fields import AsyncCustomMetadataFieldsResourceWithStreamingResponse + + return AsyncCustomMetadataFieldsResourceWithStreamingResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithStreamingResponse: + from .resources.files import AsyncFilesResourceWithStreamingResponse + + return AsyncFilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AsyncAssetsResourceWithStreamingResponse: + from .resources.assets import AsyncAssetsResourceWithStreamingResponse + + return AsyncAssetsResourceWithStreamingResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.AsyncCacheResourceWithStreamingResponse: + from .resources.cache import AsyncCacheResourceWithStreamingResponse + + return AsyncCacheResourceWithStreamingResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.AsyncFoldersResourceWithStreamingResponse: + from .resources.folders import AsyncFoldersResourceWithStreamingResponse + + return AsyncFoldersResourceWithStreamingResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AsyncAccountsResourceWithStreamingResponse: + from .resources.accounts import AsyncAccountsResourceWithStreamingResponse + + return AsyncAccountsResourceWithStreamingResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.AsyncBetaResourceWithStreamingResponse: + from .resources.beta import AsyncBetaResourceWithStreamingResponse + + return AsyncBetaResourceWithStreamingResponse(self._client.beta) Client = ImageKit From 0014808307e55091a943d2f6b087fefbaee8ed0a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:47:31 +0000 Subject: [PATCH 03/12] fix: use async_to_httpx_files in patch method --- src/imagekitio/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/imagekitio/_base_client.py b/src/imagekitio/_base_client.py index 542a71b..f8b7757 100644 --- a/src/imagekitio/_base_client.py +++ b/src/imagekitio/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From e6bf0196fe985302e11fb440cd3d215114a8e4c3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:53:36 +0000 Subject: [PATCH 04/12] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index eb9a4dd..d4778c6 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import imagekitio' From 49635b4dc6bd4268fc6a62f9df2a2e15c56afcee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:06:08 +0000 Subject: [PATCH 05/12] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index e7a4d16..2027861 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Image Kit + Copyright 2026 Image Kit Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 51c1a9ae1545a25b574195ec73b83dab64d9becb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 08:31:37 +0000 Subject: [PATCH 06/12] docs: prominently feature MCP server setup in root SDK readmes From 40ef10e6e81ff3727a095aead127d296486a3c09 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:25:29 +0000 Subject: [PATCH 07/12] fix(client): loosen auth header validation --- src/imagekitio/_client.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/imagekitio/_client.py b/src/imagekitio/_client.py index 8a76fd5..f41d306 100644 --- a/src/imagekitio/_client.py +++ b/src/imagekitio/_client.py @@ -221,9 +221,7 @@ def default_headers(self) -> dict[str, str | Omit]: @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.private_key and self.password and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): return raise TypeError( @@ -485,9 +483,7 @@ def default_headers(self) -> dict[str, str | Omit]: @override def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: - if self.private_key and self.password and headers.get("Authorization"): - return - if isinstance(custom_headers.get("Authorization"), Omit): + if headers.get("Authorization") or isinstance(custom_headers.get("Authorization"), Omit): return raise TypeError( From f8580d644e31312e439a54704ca2e3858407ea0b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:34:59 +0000 Subject: [PATCH 08/12] feat(client): add support for binary request streaming --- src/imagekitio/_base_client.py | 145 +++++++++++++++++++++++-- src/imagekitio/_models.py | 17 ++- src/imagekitio/_types.py | 9 ++ tests/test_client.py | 189 ++++++++++++++++++++++++++++++++- 4 files changed, 346 insertions(+), 14 deletions(-) diff --git a/src/imagekitio/_base_client.py b/src/imagekitio/_base_client.py index f8b7757..a2f5b04 100644 --- a/src/imagekitio/_base_client.py +++ b/src/imagekitio/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -532,7 +546,13 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data else: kwargs["json"] = json_data if is_given(json_data) else None @@ -1194,6 +1214,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1227,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1241,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1254,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1282,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1308,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1334,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1786,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1799,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1813,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1826,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1854,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1885,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1911,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( diff --git a/src/imagekitio/_models.py b/src/imagekitio/_models.py index ca9500b..29070e0 100644 --- a/src/imagekitio/_models.py +++ b/src/imagekitio/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -787,6 +800,7 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/imagekitio/_types.py b/src/imagekitio/_types.py index 714fee2..eb6e4cf 100644 --- a/src/imagekitio/_types.py +++ b/src/imagekitio/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, diff --git a/tests/test_client.py b/tests/test_client.py index 73532a8..8fef2fd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") private_key = "My Private Key" password = "My Password" @@ -51,6 +53,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: ImageKit | AsyncImageKit) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -548,6 +601,71 @@ def test_multipart_repeating_array(self, client: ImageKit) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: ImageKit) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with ImageKit( + base_url=base_url, + private_key=private_key, + password=password, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: ImageKit) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: ImageKit) -> None: class Model1(BaseModel): @@ -1455,6 +1573,73 @@ def test_multipart_repeating_array(self, async_client: AsyncImageKit) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncImageKit) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncImageKit( + base_url=base_url, + private_key=private_key, + password=password, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncImageKit + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncImageKit) -> None: class Model1(BaseModel): From a0781edc19f2cbd78a87e973e0cc2277079fb02a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 06:52:18 +0000 Subject: [PATCH 09/12] feat(api): Add saved extensions API and enhance transformation options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added complete CRUD API endpoints for saved extensions, allowing users to save complex extension configurations and reuse them across multiple assets Fixed metadata endpoint path: /v1/files/metadata → /v1/metadata Added and improved transformation options e.g. color replace, layer modes, distort effect, gradient background etc. --- .stats.yml | 8 +- api.md | 20 +- src/imagekitio/_client.py | 49 +- src/imagekitio/resources/__init__.py | 14 + src/imagekitio/resources/dummy.py | 20 + src/imagekitio/resources/files/metadata.py | 4 +- src/imagekitio/resources/saved_extensions.py | 545 ++++++++++++++++++ src/imagekitio/types/__init__.py | 5 + src/imagekitio/types/dummy_create_params.py | 11 + .../types/saved_extension_create_params.py | 23 + .../types/saved_extension_list_response.py | 10 + .../types/saved_extension_update_params.py | 23 + src/imagekitio/types/shared/__init__.py | 2 + src/imagekitio/types/shared/base_overlay.py | 38 ++ .../types/shared/extension_config.py | 258 +++++++++ src/imagekitio/types/shared/extensions.py | 202 ++++++- src/imagekitio/types/shared/image_overlay.py | 6 + .../types/shared/saved_extension.py | 36 ++ .../solid_color_overlay_transformation.py | 14 +- .../types/shared/subtitle_overlay.py | 6 + .../shared/subtitle_overlay_transformation.py | 8 +- src/imagekitio/types/shared/text_overlay.py | 3 + .../shared/text_overlay_transformation.py | 26 +- src/imagekitio/types/shared/transformation.py | 48 +- src/imagekitio/types/shared/video_overlay.py | 6 + .../types/shared_params/__init__.py | 2 + .../types/shared_params/base_overlay.py | 36 +- .../types/shared_params/extension_config.py | 254 ++++++++ .../types/shared_params/extensions.py | 202 ++++++- .../types/shared_params/image_overlay.py | 6 + .../types/shared_params/saved_extension.py | 37 ++ .../solid_color_overlay_transformation.py | 14 +- .../types/shared_params/subtitle_overlay.py | 6 + .../subtitle_overlay_transformation.py | 8 +- .../types/shared_params/text_overlay.py | 3 + .../text_overlay_transformation.py | 26 +- .../types/shared_params/transformation.py | 48 +- .../types/shared_params/video_overlay.py | 6 + tests/api_resources/beta/v2/test_files.py | 106 ++++ tests/api_resources/test_dummy.py | 201 +++++++ tests/api_resources/test_files.py | 212 +++++++ tests/api_resources/test_saved_extensions.py | 489 ++++++++++++++++ 42 files changed, 2979 insertions(+), 62 deletions(-) create mode 100644 src/imagekitio/resources/saved_extensions.py create mode 100644 src/imagekitio/types/saved_extension_create_params.py create mode 100644 src/imagekitio/types/saved_extension_list_response.py create mode 100644 src/imagekitio/types/saved_extension_update_params.py create mode 100644 src/imagekitio/types/shared/extension_config.py create mode 100644 src/imagekitio/types/shared/saved_extension.py create mode 100644 src/imagekitio/types/shared_params/extension_config.py create mode 100644 src/imagekitio/types/shared_params/saved_extension.py create mode 100644 tests/api_resources/test_saved_extensions.py diff --git a/.stats.yml b/.stats.yml index 333dfb4..0bf4f71 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 43 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-9d184cb502ab32a85db2889c796cdfebe812f2a55a604df79c85dd4b5e7e2add.yml -openapi_spec_hash: a9aa620376fce66532c84f9364209b0b -config_hash: 71cab8223bb5610c6c7ca6e9c4cc1f89 +configured_endpoints: 48 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-7a3257eb171467b637c8d72877f201c2e6038c71ed447a9453230b7309ce7416.yml +openapi_spec_hash: 87b000a9989ad5c9526f28d91b8a1749 +config_hash: aeb6eb949d73382270bbd8bbf2e4cf2a diff --git a/api.md b/api.md index b617936..2a17f3a 100644 --- a/api.md +++ b/api.md @@ -3,6 +3,7 @@ ```python from imagekitio.types import ( BaseOverlay, + ExtensionConfig, Extensions, GetImageAttributesOptions, ImageOverlay, @@ -10,6 +11,7 @@ from imagekitio.types import ( OverlayPosition, OverlayTiming, ResponsiveImageAttributes, + SavedExtension, SolidColorOverlay, SolidColorOverlayTransformation, SrcOptions, @@ -111,7 +113,23 @@ Methods: Methods: - client.files.metadata.get(file_id) -> Metadata -- client.files.metadata.get_from_url(\*\*params) -> Metadata +- client.files.metadata.get_from_url(\*\*params) -> Metadata + +# SavedExtensions + +Types: + +```python +from imagekitio.types import SavedExtensionListResponse +``` + +Methods: + +- client.saved_extensions.create(\*\*params) -> SavedExtension +- client.saved_extensions.update(id, \*\*params) -> SavedExtension +- client.saved_extensions.list() -> SavedExtensionListResponse +- client.saved_extensions.delete(id) -> None +- client.saved_extensions.get(id) -> SavedExtension # Assets diff --git a/src/imagekitio/_client.py b/src/imagekitio/_client.py index f41d306..32ed4c0 100644 --- a/src/imagekitio/_client.py +++ b/src/imagekitio/_client.py @@ -33,7 +33,17 @@ ) if TYPE_CHECKING: - from .resources import beta, cache, dummy, files, assets, folders, accounts, custom_metadata_fields + from .resources import ( + beta, + cache, + dummy, + files, + assets, + folders, + accounts, + saved_extensions, + custom_metadata_fields, + ) from .resources.dummy import DummyResource, AsyncDummyResource from .resources.assets import AssetsResource, AsyncAssetsResource from .resources.webhooks import WebhooksResource, AsyncWebhooksResource @@ -41,6 +51,7 @@ from .resources.cache.cache import CacheResource, AsyncCacheResource from .resources.files.files import FilesResource, AsyncFilesResource from .resources.folders.folders import FoldersResource, AsyncFoldersResource + from .resources.saved_extensions import SavedExtensionsResource, AsyncSavedExtensionsResource from .resources.accounts.accounts import AccountsResource, AsyncAccountsResource from .resources.custom_metadata_fields import CustomMetadataFieldsResource, AsyncCustomMetadataFieldsResource from .lib.helper import HelperResource, AsyncHelperResource @@ -146,6 +157,12 @@ def files(self) -> FilesResource: return FilesResource(self) + @cached_property + def saved_extensions(self) -> SavedExtensionsResource: + from .resources.saved_extensions import SavedExtensionsResource + + return SavedExtensionsResource(self) + @cached_property def assets(self) -> AssetsResource: from .resources.assets import AssetsResource @@ -408,6 +425,12 @@ def files(self) -> AsyncFilesResource: return AsyncFilesResource(self) + @cached_property + def saved_extensions(self) -> AsyncSavedExtensionsResource: + from .resources.saved_extensions import AsyncSavedExtensionsResource + + return AsyncSavedExtensionsResource(self) + @cached_property def assets(self) -> AsyncAssetsResource: from .resources.assets import AsyncAssetsResource @@ -605,6 +628,12 @@ def files(self) -> files.FilesResourceWithRawResponse: return FilesResourceWithRawResponse(self._client.files) + @cached_property + def saved_extensions(self) -> saved_extensions.SavedExtensionsResourceWithRawResponse: + from .resources.saved_extensions import SavedExtensionsResourceWithRawResponse + + return SavedExtensionsResourceWithRawResponse(self._client.saved_extensions) + @cached_property def assets(self) -> assets.AssetsResourceWithRawResponse: from .resources.assets import AssetsResourceWithRawResponse @@ -660,6 +689,12 @@ def files(self) -> files.AsyncFilesResourceWithRawResponse: return AsyncFilesResourceWithRawResponse(self._client.files) + @cached_property + def saved_extensions(self) -> saved_extensions.AsyncSavedExtensionsResourceWithRawResponse: + from .resources.saved_extensions import AsyncSavedExtensionsResourceWithRawResponse + + return AsyncSavedExtensionsResourceWithRawResponse(self._client.saved_extensions) + @cached_property def assets(self) -> assets.AsyncAssetsResourceWithRawResponse: from .resources.assets import AsyncAssetsResourceWithRawResponse @@ -715,6 +750,12 @@ def files(self) -> files.FilesResourceWithStreamingResponse: return FilesResourceWithStreamingResponse(self._client.files) + @cached_property + def saved_extensions(self) -> saved_extensions.SavedExtensionsResourceWithStreamingResponse: + from .resources.saved_extensions import SavedExtensionsResourceWithStreamingResponse + + return SavedExtensionsResourceWithStreamingResponse(self._client.saved_extensions) + @cached_property def assets(self) -> assets.AssetsResourceWithStreamingResponse: from .resources.assets import AssetsResourceWithStreamingResponse @@ -770,6 +811,12 @@ def files(self) -> files.AsyncFilesResourceWithStreamingResponse: return AsyncFilesResourceWithStreamingResponse(self._client.files) + @cached_property + def saved_extensions(self) -> saved_extensions.AsyncSavedExtensionsResourceWithStreamingResponse: + from .resources.saved_extensions import AsyncSavedExtensionsResourceWithStreamingResponse + + return AsyncSavedExtensionsResourceWithStreamingResponse(self._client.saved_extensions) + @cached_property def assets(self) -> assets.AsyncAssetsResourceWithStreamingResponse: from .resources.assets import AsyncAssetsResourceWithStreamingResponse diff --git a/src/imagekitio/resources/__init__.py b/src/imagekitio/resources/__init__.py index 81ba578..cbd36ee 100644 --- a/src/imagekitio/resources/__init__.py +++ b/src/imagekitio/resources/__init__.py @@ -61,6 +61,14 @@ HelperResource, AsyncHelperResource, ) +from .saved_extensions import ( + SavedExtensionsResource, + AsyncSavedExtensionsResource, + SavedExtensionsResourceWithRawResponse, + AsyncSavedExtensionsResourceWithRawResponse, + SavedExtensionsResourceWithStreamingResponse, + AsyncSavedExtensionsResourceWithStreamingResponse, +) from .custom_metadata_fields import ( CustomMetadataFieldsResource, AsyncCustomMetadataFieldsResource, @@ -89,6 +97,12 @@ "AsyncFilesResourceWithRawResponse", "FilesResourceWithStreamingResponse", "AsyncFilesResourceWithStreamingResponse", + "SavedExtensionsResource", + "AsyncSavedExtensionsResource", + "SavedExtensionsResourceWithRawResponse", + "AsyncSavedExtensionsResourceWithRawResponse", + "SavedExtensionsResourceWithStreamingResponse", + "AsyncSavedExtensionsResourceWithStreamingResponse", "AssetsResource", "AsyncAssetsResource", "AssetsResourceWithRawResponse", diff --git a/src/imagekitio/resources/dummy.py b/src/imagekitio/resources/dummy.py index 072340e..34ecebe 100644 --- a/src/imagekitio/resources/dummy.py +++ b/src/imagekitio/resources/dummy.py @@ -26,7 +26,9 @@ from ..types.shared_params.video_overlay import VideoOverlay from ..types.shared_params.overlay_timing import OverlayTiming from ..types.shared_params.transformation import Transformation +from ..types.shared_params.saved_extension import SavedExtension from ..types.shared.transformation_position import TransformationPosition +from ..types.shared_params.extension_config import ExtensionConfig from ..types.shared_params.overlay_position import OverlayPosition from ..types.shared_params.subtitle_overlay import SubtitleOverlay from ..types.shared_params.solid_color_overlay import SolidColorOverlay @@ -63,6 +65,7 @@ def create( self, *, base_overlay: BaseOverlay | Omit = omit, + extension_config: ExtensionConfig | Omit = omit, extensions: Extensions | Omit = omit, get_image_attributes_options: GetImageAttributesOptions | Omit = omit, image_overlay: ImageOverlay | Omit = omit, @@ -70,6 +73,7 @@ def create( overlay_position: OverlayPosition | Omit = omit, overlay_timing: OverlayTiming | Omit = omit, responsive_image_attributes: ResponsiveImageAttributes | Omit = omit, + saved_extensions: SavedExtension | Omit = omit, solid_color_overlay: SolidColorOverlay | Omit = omit, solid_color_overlay_transformation: SolidColorOverlayTransformation | Omit = omit, src_options: SrcOptions | Omit = omit, @@ -95,6 +99,9 @@ def create( and is not intended for public consumption. Args: + extension_config: Configuration object for an extension (base extensions only, not saved extension + references). + extensions: Array of extensions to be applied to the asset. Each extension can be configured with specific parameters based on the extension type. @@ -110,6 +117,8 @@ def create( responsive_image_attributes: Resulting set of attributes suitable for an HTML `` element. Useful for enabling responsive image loading with `srcSet` and `sizes`. + saved_extensions: Saved extension object containing extension configuration. + src_options: Options for generating ImageKit URLs with transformations. See the [Transformations guide](https://imagekit.io/docs/transformations). @@ -146,6 +155,7 @@ def create( body=maybe_transform( { "base_overlay": base_overlay, + "extension_config": extension_config, "extensions": extensions, "get_image_attributes_options": get_image_attributes_options, "image_overlay": image_overlay, @@ -153,6 +163,7 @@ def create( "overlay_position": overlay_position, "overlay_timing": overlay_timing, "responsive_image_attributes": responsive_image_attributes, + "saved_extensions": saved_extensions, "solid_color_overlay": solid_color_overlay, "solid_color_overlay_transformation": solid_color_overlay_transformation, "src_options": src_options, @@ -198,6 +209,7 @@ async def create( self, *, base_overlay: BaseOverlay | Omit = omit, + extension_config: ExtensionConfig | Omit = omit, extensions: Extensions | Omit = omit, get_image_attributes_options: GetImageAttributesOptions | Omit = omit, image_overlay: ImageOverlay | Omit = omit, @@ -205,6 +217,7 @@ async def create( overlay_position: OverlayPosition | Omit = omit, overlay_timing: OverlayTiming | Omit = omit, responsive_image_attributes: ResponsiveImageAttributes | Omit = omit, + saved_extensions: SavedExtension | Omit = omit, solid_color_overlay: SolidColorOverlay | Omit = omit, solid_color_overlay_transformation: SolidColorOverlayTransformation | Omit = omit, src_options: SrcOptions | Omit = omit, @@ -230,6 +243,9 @@ async def create( and is not intended for public consumption. Args: + extension_config: Configuration object for an extension (base extensions only, not saved extension + references). + extensions: Array of extensions to be applied to the asset. Each extension can be configured with specific parameters based on the extension type. @@ -245,6 +261,8 @@ async def create( responsive_image_attributes: Resulting set of attributes suitable for an HTML `` element. Useful for enabling responsive image loading with `srcSet` and `sizes`. + saved_extensions: Saved extension object containing extension configuration. + src_options: Options for generating ImageKit URLs with transformations. See the [Transformations guide](https://imagekit.io/docs/transformations). @@ -281,6 +299,7 @@ async def create( body=await async_maybe_transform( { "base_overlay": base_overlay, + "extension_config": extension_config, "extensions": extensions, "get_image_attributes_options": get_image_attributes_options, "image_overlay": image_overlay, @@ -288,6 +307,7 @@ async def create( "overlay_position": overlay_position, "overlay_timing": overlay_timing, "responsive_image_attributes": responsive_image_attributes, + "saved_extensions": saved_extensions, "solid_color_overlay": solid_color_overlay, "solid_color_overlay_transformation": solid_color_overlay_transformation, "src_options": src_options, diff --git a/src/imagekitio/resources/files/metadata.py b/src/imagekitio/resources/files/metadata.py index d9e0541..071a1e9 100644 --- a/src/imagekitio/resources/files/metadata.py +++ b/src/imagekitio/resources/files/metadata.py @@ -106,7 +106,7 @@ def get_from_url( timeout: Override the client-level default timeout for this request, in seconds """ return self._get( - "/v1/files/metadata", + "/v1/metadata", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, @@ -203,7 +203,7 @@ async def get_from_url( timeout: Override the client-level default timeout for this request, in seconds """ return await self._get( - "/v1/files/metadata", + "/v1/metadata", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, diff --git a/src/imagekitio/resources/saved_extensions.py b/src/imagekitio/resources/saved_extensions.py new file mode 100644 index 0000000..e8a6f10 --- /dev/null +++ b/src/imagekitio/resources/saved_extensions.py @@ -0,0 +1,545 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import saved_extension_create_params, saved_extension_update_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.shared.saved_extension import SavedExtension +from ..types.saved_extension_list_response import SavedExtensionListResponse +from ..types.shared_params.extension_config import ExtensionConfig + +__all__ = ["SavedExtensionsResource", "AsyncSavedExtensionsResource"] + + +class SavedExtensionsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SavedExtensionsResourceWithRawResponse: + """ + 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/imagekit-developer/imagekit-python#accessing-raw-response-data-eg-headers + """ + return SavedExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SavedExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/imagekit-developer/imagekit-python#with_streaming_response + """ + return SavedExtensionsResourceWithStreamingResponse(self) + + def create( + self, + *, + config: ExtensionConfig, + description: str, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtension: + """This API creates a new saved extension. + + Saved extensions allow you to save + complex extension configurations (like AI tasks) and reuse them by referencing + the ID in upload or update file APIs. + + **Saved extension limit** \\ + You can create a maximum of 100 saved extensions per account. + + Args: + config: Configuration object for an extension (base extensions only, not saved extension + references). + + description: Description of what the saved extension does. + + name: Name of the saved extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/saved-extensions", + body=maybe_transform( + { + "config": config, + "description": description, + "name": name, + }, + saved_extension_create_params.SavedExtensionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtension, + ) + + def update( + self, + id: str, + *, + config: ExtensionConfig | Omit = omit, + description: str | Omit = omit, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtension: + """This API updates an existing saved extension. + + You can update the name, + description, or config. + + Args: + config: Configuration object for an extension (base extensions only, not saved extension + references). + + description: Updated description of the saved extension. + + name: Updated name of the saved extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._patch( + f"/v1/saved-extensions/{id}", + body=maybe_transform( + { + "config": config, + "description": description, + "name": name, + }, + saved_extension_update_params.SavedExtensionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtension, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtensionListResponse: + """This API returns an array of all saved extensions for your account. + + Saved + extensions allow you to save complex extension configurations and reuse them by + referencing them by ID in upload or update file APIs. + """ + return self._get( + "/v1/saved-extensions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtensionListResponse, + ) + + def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + This API deletes a saved extension permanently. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + f"/v1/saved-extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + def get( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtension: + """ + This API returns details of a specific saved extension by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return self._get( + f"/v1/saved-extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtension, + ) + + +class AsyncSavedExtensionsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSavedExtensionsResourceWithRawResponse: + """ + 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/imagekit-developer/imagekit-python#accessing-raw-response-data-eg-headers + """ + return AsyncSavedExtensionsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSavedExtensionsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/imagekit-developer/imagekit-python#with_streaming_response + """ + return AsyncSavedExtensionsResourceWithStreamingResponse(self) + + async def create( + self, + *, + config: ExtensionConfig, + description: str, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtension: + """This API creates a new saved extension. + + Saved extensions allow you to save + complex extension configurations (like AI tasks) and reuse them by referencing + the ID in upload or update file APIs. + + **Saved extension limit** \\ + You can create a maximum of 100 saved extensions per account. + + Args: + config: Configuration object for an extension (base extensions only, not saved extension + references). + + description: Description of what the saved extension does. + + name: Name of the saved extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/saved-extensions", + body=await async_maybe_transform( + { + "config": config, + "description": description, + "name": name, + }, + saved_extension_create_params.SavedExtensionCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtension, + ) + + async def update( + self, + id: str, + *, + config: ExtensionConfig | Omit = omit, + description: str | Omit = omit, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtension: + """This API updates an existing saved extension. + + You can update the name, + description, or config. + + Args: + config: Configuration object for an extension (base extensions only, not saved extension + references). + + description: Updated description of the saved extension. + + name: Updated name of the saved extension. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._patch( + f"/v1/saved-extensions/{id}", + body=await async_maybe_transform( + { + "config": config, + "description": description, + "name": name, + }, + saved_extension_update_params.SavedExtensionUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtension, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtensionListResponse: + """This API returns an array of all saved extensions for your account. + + Saved + extensions allow you to save complex extension configurations and reuse them by + referencing them by ID in upload or update file APIs. + """ + return await self._get( + "/v1/saved-extensions", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtensionListResponse, + ) + + async def delete( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """ + This API deletes a saved extension permanently. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + f"/v1/saved-extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + async def get( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SavedExtension: + """ + This API returns details of a specific saved extension by ID. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return await self._get( + f"/v1/saved-extensions/{id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SavedExtension, + ) + + +class SavedExtensionsResourceWithRawResponse: + def __init__(self, saved_extensions: SavedExtensionsResource) -> None: + self._saved_extensions = saved_extensions + + self.create = to_raw_response_wrapper( + saved_extensions.create, + ) + self.update = to_raw_response_wrapper( + saved_extensions.update, + ) + self.list = to_raw_response_wrapper( + saved_extensions.list, + ) + self.delete = to_raw_response_wrapper( + saved_extensions.delete, + ) + self.get = to_raw_response_wrapper( + saved_extensions.get, + ) + + +class AsyncSavedExtensionsResourceWithRawResponse: + def __init__(self, saved_extensions: AsyncSavedExtensionsResource) -> None: + self._saved_extensions = saved_extensions + + self.create = async_to_raw_response_wrapper( + saved_extensions.create, + ) + self.update = async_to_raw_response_wrapper( + saved_extensions.update, + ) + self.list = async_to_raw_response_wrapper( + saved_extensions.list, + ) + self.delete = async_to_raw_response_wrapper( + saved_extensions.delete, + ) + self.get = async_to_raw_response_wrapper( + saved_extensions.get, + ) + + +class SavedExtensionsResourceWithStreamingResponse: + def __init__(self, saved_extensions: SavedExtensionsResource) -> None: + self._saved_extensions = saved_extensions + + self.create = to_streamed_response_wrapper( + saved_extensions.create, + ) + self.update = to_streamed_response_wrapper( + saved_extensions.update, + ) + self.list = to_streamed_response_wrapper( + saved_extensions.list, + ) + self.delete = to_streamed_response_wrapper( + saved_extensions.delete, + ) + self.get = to_streamed_response_wrapper( + saved_extensions.get, + ) + + +class AsyncSavedExtensionsResourceWithStreamingResponse: + def __init__(self, saved_extensions: AsyncSavedExtensionsResource) -> None: + self._saved_extensions = saved_extensions + + self.create = async_to_streamed_response_wrapper( + saved_extensions.create, + ) + self.update = async_to_streamed_response_wrapper( + saved_extensions.update, + ) + self.list = async_to_streamed_response_wrapper( + saved_extensions.list, + ) + self.delete = async_to_streamed_response_wrapper( + saved_extensions.delete, + ) + self.get = async_to_streamed_response_wrapper( + saved_extensions.get, + ) diff --git a/src/imagekitio/types/__init__.py b/src/imagekitio/types/__init__.py index dfbbb78..180fe49 100644 --- a/src/imagekitio/types/__init__.py +++ b/src/imagekitio/types/__init__.py @@ -15,7 +15,9 @@ ImageOverlay as ImageOverlay, VideoOverlay as VideoOverlay, OverlayTiming as OverlayTiming, + SavedExtension as SavedExtension, Transformation as Transformation, + ExtensionConfig as ExtensionConfig, OverlayPosition as OverlayPosition, SubtitleOverlay as SubtitleOverlay, SolidColorOverlay as SolidColorOverlay, @@ -56,6 +58,9 @@ from .folder_rename_response import FolderRenameResponse as FolderRenameResponse from .update_file_request_param import UpdateFileRequestParam as UpdateFileRequestParam from .unsafe_unwrap_webhook_event import UnsafeUnwrapWebhookEvent as UnsafeUnwrapWebhookEvent +from .saved_extension_create_params import SavedExtensionCreateParams as SavedExtensionCreateParams +from .saved_extension_list_response import SavedExtensionListResponse as SavedExtensionListResponse +from .saved_extension_update_params import SavedExtensionUpdateParams as SavedExtensionUpdateParams from .upload_pre_transform_error_event import UploadPreTransformErrorEvent as UploadPreTransformErrorEvent from .video_transformation_error_event import VideoTransformationErrorEvent as VideoTransformationErrorEvent from .video_transformation_ready_event import VideoTransformationReadyEvent as VideoTransformationReadyEvent diff --git a/src/imagekitio/types/dummy_create_params.py b/src/imagekitio/types/dummy_create_params.py index e21a396..7df5de9 100644 --- a/src/imagekitio/types/dummy_create_params.py +++ b/src/imagekitio/types/dummy_create_params.py @@ -10,7 +10,9 @@ from .shared_params.text_overlay import TextOverlay from .shared.streaming_resolution import StreamingResolution from .shared_params.overlay_timing import OverlayTiming +from .shared_params.saved_extension import SavedExtension from .shared.transformation_position import TransformationPosition +from .shared_params.extension_config import ExtensionConfig from .shared_params.overlay_position import OverlayPosition from .shared_params.subtitle_overlay import SubtitleOverlay from .shared_params.solid_color_overlay import SolidColorOverlay @@ -25,6 +27,12 @@ class DummyCreateParams(TypedDict, total=False): base_overlay: Annotated[BaseOverlay, PropertyInfo(alias="baseOverlay")] + extension_config: Annotated[ExtensionConfig, PropertyInfo(alias="extensionConfig")] + """ + Configuration object for an extension (base extensions only, not saved extension + references). + """ + extensions: Extensions """Array of extensions to be applied to the asset. @@ -61,6 +69,9 @@ class DummyCreateParams(TypedDict, total=False): enabling responsive image loading with `srcSet` and `sizes`. """ + saved_extensions: Annotated[SavedExtension, PropertyInfo(alias="savedExtensions")] + """Saved extension object containing extension configuration.""" + solid_color_overlay: Annotated[SolidColorOverlay, PropertyInfo(alias="solidColorOverlay")] solid_color_overlay_transformation: Annotated[ diff --git a/src/imagekitio/types/saved_extension_create_params.py b/src/imagekitio/types/saved_extension_create_params.py new file mode 100644 index 0000000..212eabd --- /dev/null +++ b/src/imagekitio/types/saved_extension_create_params.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .shared_params.extension_config import ExtensionConfig + +__all__ = ["SavedExtensionCreateParams"] + + +class SavedExtensionCreateParams(TypedDict, total=False): + config: Required[ExtensionConfig] + """ + Configuration object for an extension (base extensions only, not saved extension + references). + """ + + description: Required[str] + """Description of what the saved extension does.""" + + name: Required[str] + """Name of the saved extension.""" diff --git a/src/imagekitio/types/saved_extension_list_response.py b/src/imagekitio/types/saved_extension_list_response.py new file mode 100644 index 0000000..326c206 --- /dev/null +++ b/src/imagekitio/types/saved_extension_list_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from typing_extensions import TypeAlias + +from .shared.saved_extension import SavedExtension + +__all__ = ["SavedExtensionListResponse"] + +SavedExtensionListResponse: TypeAlias = List[SavedExtension] diff --git a/src/imagekitio/types/saved_extension_update_params.py b/src/imagekitio/types/saved_extension_update_params.py new file mode 100644 index 0000000..47dd10c --- /dev/null +++ b/src/imagekitio/types/saved_extension_update_params.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +from .shared_params.extension_config import ExtensionConfig + +__all__ = ["SavedExtensionUpdateParams"] + + +class SavedExtensionUpdateParams(TypedDict, total=False): + config: ExtensionConfig + """ + Configuration object for an extension (base extensions only, not saved extension + references). + """ + + description: str + """Updated description of the saved extension.""" + + name: str + """Updated name of the saved extension.""" diff --git a/src/imagekitio/types/shared/__init__.py b/src/imagekitio/types/shared/__init__.py index 49f3e91..cae1a71 100644 --- a/src/imagekitio/types/shared/__init__.py +++ b/src/imagekitio/types/shared/__init__.py @@ -9,6 +9,8 @@ from .video_overlay import VideoOverlay as VideoOverlay from .overlay_timing import OverlayTiming as OverlayTiming from .transformation import Transformation as Transformation +from .saved_extension import SavedExtension as SavedExtension +from .extension_config import ExtensionConfig as ExtensionConfig from .overlay_position import OverlayPosition as OverlayPosition from .subtitle_overlay import SubtitleOverlay as SubtitleOverlay from .solid_color_overlay import SolidColorOverlay as SolidColorOverlay diff --git a/src/imagekitio/types/shared/base_overlay.py b/src/imagekitio/types/shared/base_overlay.py index fa490a4..cac5e6d 100644 --- a/src/imagekitio/types/shared/base_overlay.py +++ b/src/imagekitio/types/shared/base_overlay.py @@ -1,6 +1,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo from ..._models import BaseModel from .overlay_timing import OverlayTiming @@ -10,6 +13,41 @@ class BaseOverlay(BaseModel): + layer_mode: Optional[Literal["multiply", "cutter", "cutout", "displace"]] = FieldInfo( + alias="layerMode", default=None + ) + """Controls how the layer blends with the base image or underlying content. + + Maps to `lm` in the URL. By default, layers completely cover the base image + beneath them. Layer modes change this behavior: + + - `multiply`: Multiplies the pixel values of the layer with the base image. The + result is always darker than the original images. This is ideal for applying + shadows or color tints. + - `displace`: Uses the layer as a displacement map to distort pixels in the base + image. The red channel controls horizontal displacement, and the green channel + controls vertical displacement. Requires `x` or `y` parameter to control + displacement magnitude. + - `cutout`: Acts as an inverse mask where opaque areas of the layer turn the + base image transparent, while transparent areas leave the base image + unchanged. This mode functions like a hole-punch, effectively cutting the + shape of the layer out of the underlying image. + - `cutter`: Acts as a shape mask where only the parts of the base image that + fall inside the opaque area of the layer are preserved. This mode functions + like a cookie-cutter, trimming the base image to match the specific dimensions + and shape of the layer. See + [Layer modes](https://imagekit.io/docs/add-overlays-on-images#layer-modes). + """ + position: Optional[OverlayPosition] = None + """ + Specifies the overlay's position relative to the parent asset. See + [Position of Layer](https://imagekit.io/docs/transformations#position-of-layer). + """ timing: Optional[OverlayTiming] = None + """ + Specifies timing information for the overlay (only applicable if the base asset + is a video). See + [Position of Layer](https://imagekit.io/docs/transformations#position-of-layer). + """ diff --git a/src/imagekitio/types/shared/extension_config.py b/src/imagekitio/types/shared/extension_config.py new file mode 100644 index 0000000..04ae44f --- /dev/null +++ b/src/imagekitio/types/shared/extension_config.py @@ -0,0 +1,258 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Union, Optional +from typing_extensions import Literal, Annotated, TypeAlias + +from pydantic import Field as FieldInfo + +from ..._utils import PropertyInfo +from ..._models import BaseModel + +__all__ = [ + "ExtensionConfig", + "RemoveBg", + "RemoveBgOptions", + "AutoTaggingExtension", + "AIAutoDescription", + "AITasks", + "AITasksTask", + "AITasksTaskSelectTags", + "AITasksTaskSelectMetadata", + "AITasksTaskYesNo", + "AITasksTaskYesNoOnNo", + "AITasksTaskYesNoOnNoSetMetadata", + "AITasksTaskYesNoOnNoUnsetMetadata", + "AITasksTaskYesNoOnUnknown", + "AITasksTaskYesNoOnUnknownSetMetadata", + "AITasksTaskYesNoOnUnknownUnsetMetadata", + "AITasksTaskYesNoOnYes", + "AITasksTaskYesNoOnYesSetMetadata", + "AITasksTaskYesNoOnYesUnsetMetadata", +] + + +class RemoveBgOptions(BaseModel): + add_shadow: Optional[bool] = None + """Whether to add an artificial shadow to the result. + + Default is false. Note: Adding shadows is currently only supported for car + photos. + """ + + bg_color: Optional[str] = None + """ + Specifies a solid color background using hex code (e.g., "81d4fa", "fff") or + color name (e.g., "green"). If this parameter is set, `bg_image_url` must be + empty. + """ + + bg_image_url: Optional[str] = None + """Sets a background image from a URL. + + If this parameter is set, `bg_color` must be empty. + """ + + semitransparency: Optional[bool] = None + """Allows semi-transparent regions in the result. + + Default is true. Note: Semitransparency is currently only supported for car + windows. + """ + + +class RemoveBg(BaseModel): + name: Literal["remove-bg"] + """Specifies the background removal extension.""" + + options: Optional[RemoveBgOptions] = None + + +class AutoTaggingExtension(BaseModel): + max_tags: int = FieldInfo(alias="maxTags") + """Maximum number of tags to attach to the asset.""" + + min_confidence: int = FieldInfo(alias="minConfidence") + """Minimum confidence level for tags to be considered valid.""" + + name: Literal["google-auto-tagging", "aws-auto-tagging"] + """Specifies the auto-tagging extension used.""" + + +class AIAutoDescription(BaseModel): + name: Literal["ai-auto-description"] + """Specifies the auto description extension.""" + + +class AITasksTaskSelectTags(BaseModel): + instruction: str + """The question or instruction for the AI to analyze the image.""" + + type: Literal["select_tags"] + """Task type that analyzes the image and adds matching tags from a vocabulary.""" + + vocabulary: List[str] + """Array of possible tag values. + + Combined length of all strings must not exceed 500 characters. Cannot contain + the `%` character. + """ + + max_selections: Optional[int] = None + """Maximum number of tags to select from the vocabulary.""" + + min_selections: Optional[int] = None + """Minimum number of tags to select from the vocabulary.""" + + +class AITasksTaskSelectMetadata(BaseModel): + field: str + """Name of the custom metadata field to set. The field must exist in your account.""" + + instruction: str + """The question or instruction for the AI to analyze the image.""" + + type: Literal["select_metadata"] + """ + Task type that analyzes the image and sets a custom metadata field value from a + vocabulary. + """ + + max_selections: Optional[int] = None + """Maximum number of values to select from the vocabulary.""" + + min_selections: Optional[int] = None + """Minimum number of values to select from the vocabulary.""" + + vocabulary: Optional[List[Union[str, float, bool]]] = None + """Array of possible values matching the custom metadata field type.""" + + +class AITasksTaskYesNoOnNoSetMetadata(BaseModel): + field: str + """Name of the custom metadata field to set.""" + + value: Union[str, float, bool, List[Union[str, float, bool]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class AITasksTaskYesNoOnNoUnsetMetadata(BaseModel): + field: str + """Name of the custom metadata field to remove.""" + + +class AITasksTaskYesNoOnNo(BaseModel): + """Actions to execute if the AI answers no.""" + + add_tags: Optional[List[str]] = None + """Array of tag strings to add to the asset.""" + + remove_tags: Optional[List[str]] = None + """Array of tag strings to remove from the asset.""" + + set_metadata: Optional[List[AITasksTaskYesNoOnNoSetMetadata]] = None + """Array of custom metadata field updates.""" + + unset_metadata: Optional[List[AITasksTaskYesNoOnNoUnsetMetadata]] = None + """Array of custom metadata fields to remove.""" + + +class AITasksTaskYesNoOnUnknownSetMetadata(BaseModel): + field: str + """Name of the custom metadata field to set.""" + + value: Union[str, float, bool, List[Union[str, float, bool]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class AITasksTaskYesNoOnUnknownUnsetMetadata(BaseModel): + field: str + """Name of the custom metadata field to remove.""" + + +class AITasksTaskYesNoOnUnknown(BaseModel): + """Actions to execute if the AI cannot determine the answer.""" + + add_tags: Optional[List[str]] = None + """Array of tag strings to add to the asset.""" + + remove_tags: Optional[List[str]] = None + """Array of tag strings to remove from the asset.""" + + set_metadata: Optional[List[AITasksTaskYesNoOnUnknownSetMetadata]] = None + """Array of custom metadata field updates.""" + + unset_metadata: Optional[List[AITasksTaskYesNoOnUnknownUnsetMetadata]] = None + """Array of custom metadata fields to remove.""" + + +class AITasksTaskYesNoOnYesSetMetadata(BaseModel): + field: str + """Name of the custom metadata field to set.""" + + value: Union[str, float, bool, List[Union[str, float, bool]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class AITasksTaskYesNoOnYesUnsetMetadata(BaseModel): + field: str + """Name of the custom metadata field to remove.""" + + +class AITasksTaskYesNoOnYes(BaseModel): + """Actions to execute if the AI answers yes.""" + + add_tags: Optional[List[str]] = None + """Array of tag strings to add to the asset.""" + + remove_tags: Optional[List[str]] = None + """Array of tag strings to remove from the asset.""" + + set_metadata: Optional[List[AITasksTaskYesNoOnYesSetMetadata]] = None + """Array of custom metadata field updates.""" + + unset_metadata: Optional[List[AITasksTaskYesNoOnYesUnsetMetadata]] = None + """Array of custom metadata fields to remove.""" + + +class AITasksTaskYesNo(BaseModel): + instruction: str + """The yes/no question for the AI to answer about the image.""" + + type: Literal["yes_no"] + """Task type that asks a yes/no question and executes actions based on the answer.""" + + on_no: Optional[AITasksTaskYesNoOnNo] = None + """Actions to execute if the AI answers no.""" + + on_unknown: Optional[AITasksTaskYesNoOnUnknown] = None + """Actions to execute if the AI cannot determine the answer.""" + + on_yes: Optional[AITasksTaskYesNoOnYes] = None + """Actions to execute if the AI answers yes.""" + + +AITasksTask: TypeAlias = Annotated[ + Union[AITasksTaskSelectTags, AITasksTaskSelectMetadata, AITasksTaskYesNo], PropertyInfo(discriminator="type") +] + + +class AITasks(BaseModel): + name: Literal["ai-tasks"] + """Specifies the AI tasks extension for automated image analysis using AI models.""" + + tasks: List[AITasksTask] + """Array of task objects defining AI operations to perform on the asset.""" + + +ExtensionConfig: TypeAlias = Annotated[ + Union[RemoveBg, AutoTaggingExtension, AIAutoDescription, AITasks], PropertyInfo(discriminator="name") +] diff --git a/src/imagekitio/types/shared/extensions.py b/src/imagekitio/types/shared/extensions.py index 36d0a05..f061a94 100644 --- a/src/imagekitio/types/shared/extensions.py +++ b/src/imagekitio/types/shared/extensions.py @@ -15,6 +15,21 @@ "ExtensionItemRemoveBgOptions", "ExtensionItemAutoTaggingExtension", "ExtensionItemAIAutoDescription", + "ExtensionItemAITasks", + "ExtensionItemAITasksTask", + "ExtensionItemAITasksTaskSelectTags", + "ExtensionItemAITasksTaskSelectMetadata", + "ExtensionItemAITasksTaskYesNo", + "ExtensionItemAITasksTaskYesNoOnNo", + "ExtensionItemAITasksTaskYesNoOnNoSetMetadata", + "ExtensionItemAITasksTaskYesNoOnNoUnsetMetadata", + "ExtensionItemAITasksTaskYesNoOnUnknown", + "ExtensionItemAITasksTaskYesNoOnUnknownSetMetadata", + "ExtensionItemAITasksTaskYesNoOnUnknownUnsetMetadata", + "ExtensionItemAITasksTaskYesNoOnYes", + "ExtensionItemAITasksTaskYesNoOnYesSetMetadata", + "ExtensionItemAITasksTaskYesNoOnYesUnsetMetadata", + "ExtensionItemSavedExtension", ] @@ -70,8 +85,193 @@ class ExtensionItemAIAutoDescription(BaseModel): """Specifies the auto description extension.""" +class ExtensionItemAITasksTaskSelectTags(BaseModel): + instruction: str + """The question or instruction for the AI to analyze the image.""" + + type: Literal["select_tags"] + """Task type that analyzes the image and adds matching tags from a vocabulary.""" + + vocabulary: List[str] + """Array of possible tag values. + + Combined length of all strings must not exceed 500 characters. Cannot contain + the `%` character. + """ + + max_selections: Optional[int] = None + """Maximum number of tags to select from the vocabulary.""" + + min_selections: Optional[int] = None + """Minimum number of tags to select from the vocabulary.""" + + +class ExtensionItemAITasksTaskSelectMetadata(BaseModel): + field: str + """Name of the custom metadata field to set. The field must exist in your account.""" + + instruction: str + """The question or instruction for the AI to analyze the image.""" + + type: Literal["select_metadata"] + """ + Task type that analyzes the image and sets a custom metadata field value from a + vocabulary. + """ + + max_selections: Optional[int] = None + """Maximum number of values to select from the vocabulary.""" + + min_selections: Optional[int] = None + """Minimum number of values to select from the vocabulary.""" + + vocabulary: Optional[List[Union[str, float, bool]]] = None + """Array of possible values matching the custom metadata field type.""" + + +class ExtensionItemAITasksTaskYesNoOnNoSetMetadata(BaseModel): + field: str + """Name of the custom metadata field to set.""" + + value: Union[str, float, bool, List[Union[str, float, bool]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class ExtensionItemAITasksTaskYesNoOnNoUnsetMetadata(BaseModel): + field: str + """Name of the custom metadata field to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnNo(BaseModel): + """Actions to execute if the AI answers no.""" + + add_tags: Optional[List[str]] = None + """Array of tag strings to add to the asset.""" + + remove_tags: Optional[List[str]] = None + """Array of tag strings to remove from the asset.""" + + set_metadata: Optional[List[ExtensionItemAITasksTaskYesNoOnNoSetMetadata]] = None + """Array of custom metadata field updates.""" + + unset_metadata: Optional[List[ExtensionItemAITasksTaskYesNoOnNoUnsetMetadata]] = None + """Array of custom metadata fields to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnUnknownSetMetadata(BaseModel): + field: str + """Name of the custom metadata field to set.""" + + value: Union[str, float, bool, List[Union[str, float, bool]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class ExtensionItemAITasksTaskYesNoOnUnknownUnsetMetadata(BaseModel): + field: str + """Name of the custom metadata field to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnUnknown(BaseModel): + """Actions to execute if the AI cannot determine the answer.""" + + add_tags: Optional[List[str]] = None + """Array of tag strings to add to the asset.""" + + remove_tags: Optional[List[str]] = None + """Array of tag strings to remove from the asset.""" + + set_metadata: Optional[List[ExtensionItemAITasksTaskYesNoOnUnknownSetMetadata]] = None + """Array of custom metadata field updates.""" + + unset_metadata: Optional[List[ExtensionItemAITasksTaskYesNoOnUnknownUnsetMetadata]] = None + """Array of custom metadata fields to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnYesSetMetadata(BaseModel): + field: str + """Name of the custom metadata field to set.""" + + value: Union[str, float, bool, List[Union[str, float, bool]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class ExtensionItemAITasksTaskYesNoOnYesUnsetMetadata(BaseModel): + field: str + """Name of the custom metadata field to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnYes(BaseModel): + """Actions to execute if the AI answers yes.""" + + add_tags: Optional[List[str]] = None + """Array of tag strings to add to the asset.""" + + remove_tags: Optional[List[str]] = None + """Array of tag strings to remove from the asset.""" + + set_metadata: Optional[List[ExtensionItemAITasksTaskYesNoOnYesSetMetadata]] = None + """Array of custom metadata field updates.""" + + unset_metadata: Optional[List[ExtensionItemAITasksTaskYesNoOnYesUnsetMetadata]] = None + """Array of custom metadata fields to remove.""" + + +class ExtensionItemAITasksTaskYesNo(BaseModel): + instruction: str + """The yes/no question for the AI to answer about the image.""" + + type: Literal["yes_no"] + """Task type that asks a yes/no question and executes actions based on the answer.""" + + on_no: Optional[ExtensionItemAITasksTaskYesNoOnNo] = None + """Actions to execute if the AI answers no.""" + + on_unknown: Optional[ExtensionItemAITasksTaskYesNoOnUnknown] = None + """Actions to execute if the AI cannot determine the answer.""" + + on_yes: Optional[ExtensionItemAITasksTaskYesNoOnYes] = None + """Actions to execute if the AI answers yes.""" + + +ExtensionItemAITasksTask: TypeAlias = Annotated[ + Union[ExtensionItemAITasksTaskSelectTags, ExtensionItemAITasksTaskSelectMetadata, ExtensionItemAITasksTaskYesNo], + PropertyInfo(discriminator="type"), +] + + +class ExtensionItemAITasks(BaseModel): + name: Literal["ai-tasks"] + """Specifies the AI tasks extension for automated image analysis using AI models.""" + + tasks: List[ExtensionItemAITasksTask] + """Array of task objects defining AI operations to perform on the asset.""" + + +class ExtensionItemSavedExtension(BaseModel): + id: str + """The unique ID of the saved extension to apply.""" + + name: Literal["saved-extension"] + """Indicates this is a reference to a saved extension.""" + + ExtensionItem: TypeAlias = Annotated[ - Union[ExtensionItemRemoveBg, ExtensionItemAutoTaggingExtension, ExtensionItemAIAutoDescription], + Union[ + ExtensionItemRemoveBg, + ExtensionItemAutoTaggingExtension, + ExtensionItemAIAutoDescription, + ExtensionItemAITasks, + ExtensionItemSavedExtension, + ], PropertyInfo(discriminator="name"), ] diff --git a/src/imagekitio/types/shared/image_overlay.py b/src/imagekitio/types/shared/image_overlay.py index 178864c..f22479b 100644 --- a/src/imagekitio/types/shared/image_overlay.py +++ b/src/imagekitio/types/shared/image_overlay.py @@ -23,6 +23,12 @@ class ImageOverlay(BaseOverlay): format automatically. To always use base64 encoding (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method: + + - Leading and trailing slashes are removed. + - Remaining slashes within the path are replaced with `@@` when using plain + text. """ transformation: Optional[List["Transformation"]] = None diff --git a/src/imagekitio/types/shared/saved_extension.py b/src/imagekitio/types/shared/saved_extension.py new file mode 100644 index 0000000..a6732c2 --- /dev/null +++ b/src/imagekitio/types/shared/saved_extension.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel +from .extension_config import ExtensionConfig + +__all__ = ["SavedExtension"] + + +class SavedExtension(BaseModel): + """Saved extension object containing extension configuration.""" + + id: Optional[str] = None + """Unique identifier of the saved extension.""" + + config: Optional[ExtensionConfig] = None + """ + Configuration object for an extension (base extensions only, not saved extension + references). + """ + + created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) + """Timestamp when the saved extension was created.""" + + description: Optional[str] = None + """Description of the saved extension.""" + + name: Optional[str] = None + """Name of the saved extension.""" + + updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) + """Timestamp when the saved extension was last updated.""" diff --git a/src/imagekitio/types/shared/solid_color_overlay_transformation.py b/src/imagekitio/types/shared/solid_color_overlay_transformation.py index 4e0f173..53131d6 100644 --- a/src/imagekitio/types/shared/solid_color_overlay_transformation.py +++ b/src/imagekitio/types/shared/solid_color_overlay_transformation.py @@ -10,9 +10,9 @@ class SolidColorOverlayTransformation(BaseModel): alpha: Optional[float] = None - """Specifies the transparency level of the solid color overlay. + """Specifies the transparency level of the overlaid solid color layer. - Accepts integers from `1` to `9`. + Supports integers from `1` to `9`. """ background: Optional[str] = None @@ -37,11 +37,15 @@ class SolidColorOverlayTransformation(BaseModel): [arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations). """ - radius: Union[float, Literal["max"], None] = None + radius: Union[float, Literal["max"], str, None] = None """Specifies the corner radius of the solid color overlay. - Set to `max` for circular or oval shape. See - [radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). + - Single value (positive integer): Applied to all corners (e.g., `20`). + - `max`: Creates a circular or oval shape. + - Per-corner array: Provide four underscore-separated values representing + top-left, top-right, bottom-right, and bottom-left corners respectively (e.g., + `10_20_30_40`). See + [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). """ width: Union[float, str, None] = None diff --git a/src/imagekitio/types/shared/subtitle_overlay.py b/src/imagekitio/types/shared/subtitle_overlay.py index f44f3c4..b589672 100644 --- a/src/imagekitio/types/shared/subtitle_overlay.py +++ b/src/imagekitio/types/shared/subtitle_overlay.py @@ -22,6 +22,12 @@ class SubtitleOverlay(BaseOverlay): format automatically. To always use base64 encoding (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method: + + - Leading and trailing slashes are removed. + - Remaining slashes within the path are replaced with `@@` when using plain + text. """ transformation: Optional[List[SubtitleOverlayTransformation]] = None diff --git a/src/imagekitio/types/shared/subtitle_overlay_transformation.py b/src/imagekitio/types/shared/subtitle_overlay_transformation.py index 2f7c739..8669721 100644 --- a/src/imagekitio/types/shared/subtitle_overlay_transformation.py +++ b/src/imagekitio/types/shared/subtitle_overlay_transformation.py @@ -33,10 +33,10 @@ class SubtitleOverlayTransformation(BaseModel): """ font_family: Optional[str] = FieldInfo(alias="fontFamily", default=None) - """Font family for subtitles. - - Refer to the - [supported fonts](https://imagekit.io/docs/add-overlays-on-images#supported-text-font-list). + """ + Sets the font family of subtitle text. Refer to the + [supported fonts documented](https://imagekit.io/docs/add-overlays-on-images#supported-text-font-list) + in the ImageKit transformations guide. """ font_outline: Optional[str] = FieldInfo(alias="fontOutline", default=None) diff --git a/src/imagekitio/types/shared/text_overlay.py b/src/imagekitio/types/shared/text_overlay.py index b156834..1d64ae2 100644 --- a/src/imagekitio/types/shared/text_overlay.py +++ b/src/imagekitio/types/shared/text_overlay.py @@ -25,6 +25,9 @@ class TextOverlay(BaseOverlay): appropriate format based on the input text. To always use base64 (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method, the input text is always percent-encoded to + ensure it is URL-safe. """ transformation: Optional[List[TextOverlayTransformation]] = None diff --git a/src/imagekitio/types/shared/text_overlay_transformation.py b/src/imagekitio/types/shared/text_overlay_transformation.py index 8aa0711..47ff17f 100644 --- a/src/imagekitio/types/shared/text_overlay_transformation.py +++ b/src/imagekitio/types/shared/text_overlay_transformation.py @@ -24,7 +24,11 @@ class TextOverlayTransformation(BaseModel): """ flip: Optional[Literal["h", "v", "h_v", "v_h"]] = None - """Flip the text overlay horizontally, vertically, or both.""" + """ + Flip/mirror the text horizontally, vertically, or in both directions. Acceptable + values: `h` (horizontal), `v` (vertical), `h_v` (horizontal and vertical), or + `v_h`. + """ font_color: Optional[str] = FieldInfo(alias="fontColor", default=None) """Specifies the font color of the overlaid text. @@ -55,11 +59,10 @@ class TextOverlayTransformation(BaseModel): """ line_height: Union[float, str, None] = FieldInfo(alias="lineHeight", default=None) - """Specifies the line height of the text overlay. + """Specifies the line height for multi-line text overlays. - Accepts integer values representing line height in points. It can also accept - [arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations) - such as `bw_mul_0.2`, or `bh_div_20`. + It will come into effect only if the text wraps over multiple lines. Accepts + either an integer value or an arithmetic expression. """ padding: Union[float, str, None] = None @@ -69,10 +72,15 @@ class TextOverlayTransformation(BaseModel): shorthand order). Arithmetic expressions are also accepted. """ - radius: Union[float, Literal["max"], None] = None - """ - Specifies the corner radius of the text overlay. Set to `max` to achieve a - circular or oval shape. + radius: Union[float, Literal["max"], str, None] = None + """Specifies the corner radius: + + - Single value (positive integer): Applied to all corners (e.g., `20`). + - `max`: Creates a circular or oval shape. + - Per-corner array: Provide four underscore-separated values representing + top-left, top-right, bottom-right, and bottom-left corners respectively (e.g., + `10_20_30_40`). See + [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). """ rotation: Union[float, str, None] = None diff --git a/src/imagekitio/types/shared/transformation.py b/src/imagekitio/types/shared/transformation.py index c0f42d1..89b5841 100644 --- a/src/imagekitio/types/shared/transformation.py +++ b/src/imagekitio/types/shared/transformation.py @@ -107,6 +107,12 @@ class Transformation(BaseModel): - A solid color: e.g., `red`, `F3F3F3`, `AAFF0010`. See [Solid color background](https://imagekit.io/docs/effects-and-enhancements#solid-color-background). + - Dominant color: `dominant` extracts the dominant color from the image. See + [Dominant color background](https://imagekit.io/docs/effects-and-enhancements#dominant-color-background). + - Gradient: `gradient_dominant` or `gradient_dominant_2` creates a gradient + using the dominant colors. Optionally specify palette size (2 or 4), e.g., + `gradient_dominant_4`. See + [Gradient background](https://imagekit.io/docs/effects-and-enhancements#gradient-background). - A blurred background: e.g., `blurred`, `blurred_25_N15`, etc. See [Blurred background](https://imagekit.io/docs/effects-and-enhancements#blurred-background). - Expand the image boundaries using generative fill: `genfill`. Not supported @@ -137,6 +143,17 @@ class Transformation(BaseModel): [Color profile](https://imagekit.io/docs/image-optimization#color-profile---cp). """ + color_replace: Optional[str] = FieldInfo(alias="colorReplace", default=None) + """Replaces colors in the image. Supports three formats: + + - `toColor` - Replace dominant color with the specified color. + - `toColor_tolerance` - Replace dominant color with specified tolerance (0-100). + - `toColor_tolerance_fromColor` - Replace a specific color with another within + tolerance range. Colors can be hex codes (e.g., `FF0022`) or names (e.g., + `red`, `blue`). See + [Color replacement](https://imagekit.io/docs/effects-and-enhancements#color-replace---cr). + """ + contrast_stretch: Optional[Literal[True]] = FieldInfo(alias="contrastStretch", default=None) """ Automatically enhances the contrast of an image (contrast stretch). See @@ -164,11 +181,24 @@ class Transformation(BaseModel): [Default image](https://imagekit.io/docs/image-transformation#default-image---di). """ + distort: Optional[str] = None + """Distorts the shape of an image. Supports two modes: + + - Perspective distortion: `p-x1_y1_x2_y2_x3_y3_x4_y4` changes the position of + the four corners starting clockwise from top-left. + - Arc distortion: `a-degrees` curves the image upwards (positive values) or + downwards (negative values). See + [Distort effect](https://imagekit.io/docs/effects-and-enhancements#distort---e-distort). + """ + dpr: Optional[float] = None """ Accepts values between 0.1 and 5, or `auto` for automatic device pixel ratio - (DPR) calculation. See - [DPR](https://imagekit.io/docs/image-resize-and-crop#dpr---dpr). + (DPR) calculation. Also accepts arithmetic expressions. + + - Learn about + [Arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations). + - See [DPR](https://imagekit.io/docs/image-resize-and-crop#dpr---dpr). """ duration: Union[float, str, None] = None @@ -309,11 +339,15 @@ class Transformation(BaseModel): See [Quality](https://imagekit.io/docs/image-optimization#quality---q). """ - radius: Union[float, Literal["max"], None] = None - """ - Specifies the corner radius for rounded corners (e.g., 20) or `max` for circular - or oval shape. See - [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). + radius: Union[float, Literal["max"], str, None] = None + """Specifies the corner radius for rounded corners. + + - Single value (positive integer): Applied to all corners (e.g., `20`). + - `max`: Creates a circular or oval shape. + - Per-corner array: Provide four underscore-separated values representing + top-left, top-right, bottom-right, and bottom-left corners respectively (e.g., + `10_20_30_40`). See + [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). """ raw: Optional[str] = None diff --git a/src/imagekitio/types/shared/video_overlay.py b/src/imagekitio/types/shared/video_overlay.py index 3cc64c6..171657a 100644 --- a/src/imagekitio/types/shared/video_overlay.py +++ b/src/imagekitio/types/shared/video_overlay.py @@ -23,6 +23,12 @@ class VideoOverlay(BaseOverlay): format automatically. To always use base64 encoding (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method: + + - Leading and trailing slashes are removed. + - Remaining slashes within the path are replaced with `@@` when using plain + text. """ transformation: Optional[List["Transformation"]] = None diff --git a/src/imagekitio/types/shared_params/__init__.py b/src/imagekitio/types/shared_params/__init__.py index 49f3e91..cae1a71 100644 --- a/src/imagekitio/types/shared_params/__init__.py +++ b/src/imagekitio/types/shared_params/__init__.py @@ -9,6 +9,8 @@ from .video_overlay import VideoOverlay as VideoOverlay from .overlay_timing import OverlayTiming as OverlayTiming from .transformation import Transformation as Transformation +from .saved_extension import SavedExtension as SavedExtension +from .extension_config import ExtensionConfig as ExtensionConfig from .overlay_position import OverlayPosition as OverlayPosition from .subtitle_overlay import SubtitleOverlay as SubtitleOverlay from .solid_color_overlay import SolidColorOverlay as SolidColorOverlay diff --git a/src/imagekitio/types/shared_params/base_overlay.py b/src/imagekitio/types/shared_params/base_overlay.py index bf3bf1e..2ad2352 100644 --- a/src/imagekitio/types/shared_params/base_overlay.py +++ b/src/imagekitio/types/shared_params/base_overlay.py @@ -2,8 +2,9 @@ from __future__ import annotations -from typing_extensions import TypedDict +from typing_extensions import Literal, Annotated, TypedDict +from ..._utils import PropertyInfo from .overlay_timing import OverlayTiming from .overlay_position import OverlayPosition @@ -11,6 +12,39 @@ class BaseOverlay(TypedDict, total=False): + layer_mode: Annotated[Literal["multiply", "cutter", "cutout", "displace"], PropertyInfo(alias="layerMode")] + """Controls how the layer blends with the base image or underlying content. + + Maps to `lm` in the URL. By default, layers completely cover the base image + beneath them. Layer modes change this behavior: + + - `multiply`: Multiplies the pixel values of the layer with the base image. The + result is always darker than the original images. This is ideal for applying + shadows or color tints. + - `displace`: Uses the layer as a displacement map to distort pixels in the base + image. The red channel controls horizontal displacement, and the green channel + controls vertical displacement. Requires `x` or `y` parameter to control + displacement magnitude. + - `cutout`: Acts as an inverse mask where opaque areas of the layer turn the + base image transparent, while transparent areas leave the base image + unchanged. This mode functions like a hole-punch, effectively cutting the + shape of the layer out of the underlying image. + - `cutter`: Acts as a shape mask where only the parts of the base image that + fall inside the opaque area of the layer are preserved. This mode functions + like a cookie-cutter, trimming the base image to match the specific dimensions + and shape of the layer. See + [Layer modes](https://imagekit.io/docs/add-overlays-on-images#layer-modes). + """ + position: OverlayPosition + """ + Specifies the overlay's position relative to the parent asset. See + [Position of Layer](https://imagekit.io/docs/transformations#position-of-layer). + """ timing: OverlayTiming + """ + Specifies timing information for the overlay (only applicable if the base asset + is a video). See + [Position of Layer](https://imagekit.io/docs/transformations#position-of-layer). + """ diff --git a/src/imagekitio/types/shared_params/extension_config.py b/src/imagekitio/types/shared_params/extension_config.py new file mode 100644 index 0000000..b582a6c --- /dev/null +++ b/src/imagekitio/types/shared_params/extension_config.py @@ -0,0 +1,254 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict + +from ..._types import SequenceNotStr +from ..._utils import PropertyInfo + +__all__ = [ + "ExtensionConfig", + "RemoveBg", + "RemoveBgOptions", + "AutoTaggingExtension", + "AIAutoDescription", + "AITasks", + "AITasksTask", + "AITasksTaskSelectTags", + "AITasksTaskSelectMetadata", + "AITasksTaskYesNo", + "AITasksTaskYesNoOnNo", + "AITasksTaskYesNoOnNoSetMetadata", + "AITasksTaskYesNoOnNoUnsetMetadata", + "AITasksTaskYesNoOnUnknown", + "AITasksTaskYesNoOnUnknownSetMetadata", + "AITasksTaskYesNoOnUnknownUnsetMetadata", + "AITasksTaskYesNoOnYes", + "AITasksTaskYesNoOnYesSetMetadata", + "AITasksTaskYesNoOnYesUnsetMetadata", +] + + +class RemoveBgOptions(TypedDict, total=False): + add_shadow: bool + """Whether to add an artificial shadow to the result. + + Default is false. Note: Adding shadows is currently only supported for car + photos. + """ + + bg_color: str + """ + Specifies a solid color background using hex code (e.g., "81d4fa", "fff") or + color name (e.g., "green"). If this parameter is set, `bg_image_url` must be + empty. + """ + + bg_image_url: str + """Sets a background image from a URL. + + If this parameter is set, `bg_color` must be empty. + """ + + semitransparency: bool + """Allows semi-transparent regions in the result. + + Default is true. Note: Semitransparency is currently only supported for car + windows. + """ + + +class RemoveBg(TypedDict, total=False): + name: Required[Literal["remove-bg"]] + """Specifies the background removal extension.""" + + options: RemoveBgOptions + + +class AutoTaggingExtension(TypedDict, total=False): + max_tags: Required[Annotated[int, PropertyInfo(alias="maxTags")]] + """Maximum number of tags to attach to the asset.""" + + min_confidence: Required[Annotated[int, PropertyInfo(alias="minConfidence")]] + """Minimum confidence level for tags to be considered valid.""" + + name: Required[Literal["google-auto-tagging", "aws-auto-tagging"]] + """Specifies the auto-tagging extension used.""" + + +class AIAutoDescription(TypedDict, total=False): + name: Required[Literal["ai-auto-description"]] + """Specifies the auto description extension.""" + + +class AITasksTaskSelectTags(TypedDict, total=False): + instruction: Required[str] + """The question or instruction for the AI to analyze the image.""" + + type: Required[Literal["select_tags"]] + """Task type that analyzes the image and adds matching tags from a vocabulary.""" + + vocabulary: Required[SequenceNotStr[str]] + """Array of possible tag values. + + Combined length of all strings must not exceed 500 characters. Cannot contain + the `%` character. + """ + + max_selections: int + """Maximum number of tags to select from the vocabulary.""" + + min_selections: int + """Minimum number of tags to select from the vocabulary.""" + + +class AITasksTaskSelectMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set. The field must exist in your account.""" + + instruction: Required[str] + """The question or instruction for the AI to analyze the image.""" + + type: Required[Literal["select_metadata"]] + """ + Task type that analyzes the image and sets a custom metadata field value from a + vocabulary. + """ + + max_selections: int + """Maximum number of values to select from the vocabulary.""" + + min_selections: int + """Minimum number of values to select from the vocabulary.""" + + vocabulary: SequenceNotStr[Union[str, float, bool]] + """Array of possible values matching the custom metadata field type.""" + + +class AITasksTaskYesNoOnNoSetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set.""" + + value: Required[Union[str, float, bool, SequenceNotStr[Union[str, float, bool]]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class AITasksTaskYesNoOnNoUnsetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to remove.""" + + +class AITasksTaskYesNoOnNo(TypedDict, total=False): + """Actions to execute if the AI answers no.""" + + add_tags: SequenceNotStr[str] + """Array of tag strings to add to the asset.""" + + remove_tags: SequenceNotStr[str] + """Array of tag strings to remove from the asset.""" + + set_metadata: Iterable[AITasksTaskYesNoOnNoSetMetadata] + """Array of custom metadata field updates.""" + + unset_metadata: Iterable[AITasksTaskYesNoOnNoUnsetMetadata] + """Array of custom metadata fields to remove.""" + + +class AITasksTaskYesNoOnUnknownSetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set.""" + + value: Required[Union[str, float, bool, SequenceNotStr[Union[str, float, bool]]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class AITasksTaskYesNoOnUnknownUnsetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to remove.""" + + +class AITasksTaskYesNoOnUnknown(TypedDict, total=False): + """Actions to execute if the AI cannot determine the answer.""" + + add_tags: SequenceNotStr[str] + """Array of tag strings to add to the asset.""" + + remove_tags: SequenceNotStr[str] + """Array of tag strings to remove from the asset.""" + + set_metadata: Iterable[AITasksTaskYesNoOnUnknownSetMetadata] + """Array of custom metadata field updates.""" + + unset_metadata: Iterable[AITasksTaskYesNoOnUnknownUnsetMetadata] + """Array of custom metadata fields to remove.""" + + +class AITasksTaskYesNoOnYesSetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set.""" + + value: Required[Union[str, float, bool, SequenceNotStr[Union[str, float, bool]]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class AITasksTaskYesNoOnYesUnsetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to remove.""" + + +class AITasksTaskYesNoOnYes(TypedDict, total=False): + """Actions to execute if the AI answers yes.""" + + add_tags: SequenceNotStr[str] + """Array of tag strings to add to the asset.""" + + remove_tags: SequenceNotStr[str] + """Array of tag strings to remove from the asset.""" + + set_metadata: Iterable[AITasksTaskYesNoOnYesSetMetadata] + """Array of custom metadata field updates.""" + + unset_metadata: Iterable[AITasksTaskYesNoOnYesUnsetMetadata] + """Array of custom metadata fields to remove.""" + + +class AITasksTaskYesNo(TypedDict, total=False): + instruction: Required[str] + """The yes/no question for the AI to answer about the image.""" + + type: Required[Literal["yes_no"]] + """Task type that asks a yes/no question and executes actions based on the answer.""" + + on_no: AITasksTaskYesNoOnNo + """Actions to execute if the AI answers no.""" + + on_unknown: AITasksTaskYesNoOnUnknown + """Actions to execute if the AI cannot determine the answer.""" + + on_yes: AITasksTaskYesNoOnYes + """Actions to execute if the AI answers yes.""" + + +AITasksTask: TypeAlias = Union[AITasksTaskSelectTags, AITasksTaskSelectMetadata, AITasksTaskYesNo] + + +class AITasks(TypedDict, total=False): + name: Required[Literal["ai-tasks"]] + """Specifies the AI tasks extension for automated image analysis using AI models.""" + + tasks: Required[Iterable[AITasksTask]] + """Array of task objects defining AI operations to perform on the asset.""" + + +ExtensionConfig: TypeAlias = Union[RemoveBg, AutoTaggingExtension, AIAutoDescription, AITasks] diff --git a/src/imagekitio/types/shared_params/extensions.py b/src/imagekitio/types/shared_params/extensions.py index f2ab9d1..0285854 100644 --- a/src/imagekitio/types/shared_params/extensions.py +++ b/src/imagekitio/types/shared_params/extensions.py @@ -2,9 +2,10 @@ from __future__ import annotations -from typing import List, Union +from typing import List, Union, Iterable from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict +from ..._types import SequenceNotStr from ..._utils import PropertyInfo __all__ = [ @@ -14,6 +15,21 @@ "ExtensionItemRemoveBgOptions", "ExtensionItemAutoTaggingExtension", "ExtensionItemAIAutoDescription", + "ExtensionItemAITasks", + "ExtensionItemAITasksTask", + "ExtensionItemAITasksTaskSelectTags", + "ExtensionItemAITasksTaskSelectMetadata", + "ExtensionItemAITasksTaskYesNo", + "ExtensionItemAITasksTaskYesNoOnNo", + "ExtensionItemAITasksTaskYesNoOnNoSetMetadata", + "ExtensionItemAITasksTaskYesNoOnNoUnsetMetadata", + "ExtensionItemAITasksTaskYesNoOnUnknown", + "ExtensionItemAITasksTaskYesNoOnUnknownSetMetadata", + "ExtensionItemAITasksTaskYesNoOnUnknownUnsetMetadata", + "ExtensionItemAITasksTaskYesNoOnYes", + "ExtensionItemAITasksTaskYesNoOnYesSetMetadata", + "ExtensionItemAITasksTaskYesNoOnYesUnsetMetadata", + "ExtensionItemSavedExtension", ] @@ -69,8 +85,190 @@ class ExtensionItemAIAutoDescription(TypedDict, total=False): """Specifies the auto description extension.""" +class ExtensionItemAITasksTaskSelectTags(TypedDict, total=False): + instruction: Required[str] + """The question or instruction for the AI to analyze the image.""" + + type: Required[Literal["select_tags"]] + """Task type that analyzes the image and adds matching tags from a vocabulary.""" + + vocabulary: Required[SequenceNotStr[str]] + """Array of possible tag values. + + Combined length of all strings must not exceed 500 characters. Cannot contain + the `%` character. + """ + + max_selections: int + """Maximum number of tags to select from the vocabulary.""" + + min_selections: int + """Minimum number of tags to select from the vocabulary.""" + + +class ExtensionItemAITasksTaskSelectMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set. The field must exist in your account.""" + + instruction: Required[str] + """The question or instruction for the AI to analyze the image.""" + + type: Required[Literal["select_metadata"]] + """ + Task type that analyzes the image and sets a custom metadata field value from a + vocabulary. + """ + + max_selections: int + """Maximum number of values to select from the vocabulary.""" + + min_selections: int + """Minimum number of values to select from the vocabulary.""" + + vocabulary: SequenceNotStr[Union[str, float, bool]] + """Array of possible values matching the custom metadata field type.""" + + +class ExtensionItemAITasksTaskYesNoOnNoSetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set.""" + + value: Required[Union[str, float, bool, SequenceNotStr[Union[str, float, bool]]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class ExtensionItemAITasksTaskYesNoOnNoUnsetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnNo(TypedDict, total=False): + """Actions to execute if the AI answers no.""" + + add_tags: SequenceNotStr[str] + """Array of tag strings to add to the asset.""" + + remove_tags: SequenceNotStr[str] + """Array of tag strings to remove from the asset.""" + + set_metadata: Iterable[ExtensionItemAITasksTaskYesNoOnNoSetMetadata] + """Array of custom metadata field updates.""" + + unset_metadata: Iterable[ExtensionItemAITasksTaskYesNoOnNoUnsetMetadata] + """Array of custom metadata fields to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnUnknownSetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set.""" + + value: Required[Union[str, float, bool, SequenceNotStr[Union[str, float, bool]]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class ExtensionItemAITasksTaskYesNoOnUnknownUnsetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnUnknown(TypedDict, total=False): + """Actions to execute if the AI cannot determine the answer.""" + + add_tags: SequenceNotStr[str] + """Array of tag strings to add to the asset.""" + + remove_tags: SequenceNotStr[str] + """Array of tag strings to remove from the asset.""" + + set_metadata: Iterable[ExtensionItemAITasksTaskYesNoOnUnknownSetMetadata] + """Array of custom metadata field updates.""" + + unset_metadata: Iterable[ExtensionItemAITasksTaskYesNoOnUnknownUnsetMetadata] + """Array of custom metadata fields to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnYesSetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to set.""" + + value: Required[Union[str, float, bool, SequenceNotStr[Union[str, float, bool]]]] + """Value to set for the custom metadata field. + + The value type should match the custom metadata field type. + """ + + +class ExtensionItemAITasksTaskYesNoOnYesUnsetMetadata(TypedDict, total=False): + field: Required[str] + """Name of the custom metadata field to remove.""" + + +class ExtensionItemAITasksTaskYesNoOnYes(TypedDict, total=False): + """Actions to execute if the AI answers yes.""" + + add_tags: SequenceNotStr[str] + """Array of tag strings to add to the asset.""" + + remove_tags: SequenceNotStr[str] + """Array of tag strings to remove from the asset.""" + + set_metadata: Iterable[ExtensionItemAITasksTaskYesNoOnYesSetMetadata] + """Array of custom metadata field updates.""" + + unset_metadata: Iterable[ExtensionItemAITasksTaskYesNoOnYesUnsetMetadata] + """Array of custom metadata fields to remove.""" + + +class ExtensionItemAITasksTaskYesNo(TypedDict, total=False): + instruction: Required[str] + """The yes/no question for the AI to answer about the image.""" + + type: Required[Literal["yes_no"]] + """Task type that asks a yes/no question and executes actions based on the answer.""" + + on_no: ExtensionItemAITasksTaskYesNoOnNo + """Actions to execute if the AI answers no.""" + + on_unknown: ExtensionItemAITasksTaskYesNoOnUnknown + """Actions to execute if the AI cannot determine the answer.""" + + on_yes: ExtensionItemAITasksTaskYesNoOnYes + """Actions to execute if the AI answers yes.""" + + +ExtensionItemAITasksTask: TypeAlias = Union[ + ExtensionItemAITasksTaskSelectTags, ExtensionItemAITasksTaskSelectMetadata, ExtensionItemAITasksTaskYesNo +] + + +class ExtensionItemAITasks(TypedDict, total=False): + name: Required[Literal["ai-tasks"]] + """Specifies the AI tasks extension for automated image analysis using AI models.""" + + tasks: Required[Iterable[ExtensionItemAITasksTask]] + """Array of task objects defining AI operations to perform on the asset.""" + + +class ExtensionItemSavedExtension(TypedDict, total=False): + id: Required[str] + """The unique ID of the saved extension to apply.""" + + name: Required[Literal["saved-extension"]] + """Indicates this is a reference to a saved extension.""" + + ExtensionItem: TypeAlias = Union[ - ExtensionItemRemoveBg, ExtensionItemAutoTaggingExtension, ExtensionItemAIAutoDescription + ExtensionItemRemoveBg, + ExtensionItemAutoTaggingExtension, + ExtensionItemAIAutoDescription, + ExtensionItemAITasks, + ExtensionItemSavedExtension, ] Extensions: TypeAlias = List[ExtensionItem] diff --git a/src/imagekitio/types/shared_params/image_overlay.py b/src/imagekitio/types/shared_params/image_overlay.py index 3b7d74e..3c1238c 100644 --- a/src/imagekitio/types/shared_params/image_overlay.py +++ b/src/imagekitio/types/shared_params/image_overlay.py @@ -23,6 +23,12 @@ class ImageOverlay(BaseOverlay, total=False): format automatically. To always use base64 encoding (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method: + + - Leading and trailing slashes are removed. + - Remaining slashes within the path are replaced with `@@` when using plain + text. """ transformation: Iterable["Transformation"] diff --git a/src/imagekitio/types/shared_params/saved_extension.py b/src/imagekitio/types/shared_params/saved_extension.py new file mode 100644 index 0000000..de5b3bc --- /dev/null +++ b/src/imagekitio/types/shared_params/saved_extension.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Annotated, TypedDict + +from ..._utils import PropertyInfo +from .extension_config import ExtensionConfig + +__all__ = ["SavedExtension"] + + +class SavedExtension(TypedDict, total=False): + """Saved extension object containing extension configuration.""" + + id: str + """Unique identifier of the saved extension.""" + + config: ExtensionConfig + """ + Configuration object for an extension (base extensions only, not saved extension + references). + """ + + created_at: Annotated[Union[str, datetime], PropertyInfo(alias="createdAt", format="iso8601")] + """Timestamp when the saved extension was created.""" + + description: str + """Description of the saved extension.""" + + name: str + """Name of the saved extension.""" + + updated_at: Annotated[Union[str, datetime], PropertyInfo(alias="updatedAt", format="iso8601")] + """Timestamp when the saved extension was last updated.""" diff --git a/src/imagekitio/types/shared_params/solid_color_overlay_transformation.py b/src/imagekitio/types/shared_params/solid_color_overlay_transformation.py index 8bfcca7..4079c34 100644 --- a/src/imagekitio/types/shared_params/solid_color_overlay_transformation.py +++ b/src/imagekitio/types/shared_params/solid_color_overlay_transformation.py @@ -10,9 +10,9 @@ class SolidColorOverlayTransformation(TypedDict, total=False): alpha: float - """Specifies the transparency level of the solid color overlay. + """Specifies the transparency level of the overlaid solid color layer. - Accepts integers from `1` to `9`. + Supports integers from `1` to `9`. """ background: str @@ -37,11 +37,15 @@ class SolidColorOverlayTransformation(TypedDict, total=False): [arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations). """ - radius: Union[float, Literal["max"]] + radius: Union[float, Literal["max"], str] """Specifies the corner radius of the solid color overlay. - Set to `max` for circular or oval shape. See - [radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). + - Single value (positive integer): Applied to all corners (e.g., `20`). + - `max`: Creates a circular or oval shape. + - Per-corner array: Provide four underscore-separated values representing + top-left, top-right, bottom-right, and bottom-left corners respectively (e.g., + `10_20_30_40`). See + [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). """ width: Union[float, str] diff --git a/src/imagekitio/types/shared_params/subtitle_overlay.py b/src/imagekitio/types/shared_params/subtitle_overlay.py index 71e885e..20ad47d 100644 --- a/src/imagekitio/types/shared_params/subtitle_overlay.py +++ b/src/imagekitio/types/shared_params/subtitle_overlay.py @@ -24,6 +24,12 @@ class SubtitleOverlay(BaseOverlay, total=False): format automatically. To always use base64 encoding (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method: + + - Leading and trailing slashes are removed. + - Remaining slashes within the path are replaced with `@@` when using plain + text. """ transformation: Iterable[SubtitleOverlayTransformation] diff --git a/src/imagekitio/types/shared_params/subtitle_overlay_transformation.py b/src/imagekitio/types/shared_params/subtitle_overlay_transformation.py index 08b8de5..8e6572c 100644 --- a/src/imagekitio/types/shared_params/subtitle_overlay_transformation.py +++ b/src/imagekitio/types/shared_params/subtitle_overlay_transformation.py @@ -32,10 +32,10 @@ class SubtitleOverlayTransformation(TypedDict, total=False): """ font_family: Annotated[str, PropertyInfo(alias="fontFamily")] - """Font family for subtitles. - - Refer to the - [supported fonts](https://imagekit.io/docs/add-overlays-on-images#supported-text-font-list). + """ + Sets the font family of subtitle text. Refer to the + [supported fonts documented](https://imagekit.io/docs/add-overlays-on-images#supported-text-font-list) + in the ImageKit transformations guide. """ font_outline: Annotated[str, PropertyInfo(alias="fontOutline")] diff --git a/src/imagekitio/types/shared_params/text_overlay.py b/src/imagekitio/types/shared_params/text_overlay.py index 62ebe4c..eb4dd21 100644 --- a/src/imagekitio/types/shared_params/text_overlay.py +++ b/src/imagekitio/types/shared_params/text_overlay.py @@ -27,6 +27,9 @@ class TextOverlay(BaseOverlay, total=False): appropriate format based on the input text. To always use base64 (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method, the input text is always percent-encoded to + ensure it is URL-safe. """ transformation: Iterable[TextOverlayTransformation] diff --git a/src/imagekitio/types/shared_params/text_overlay_transformation.py b/src/imagekitio/types/shared_params/text_overlay_transformation.py index 5f05fbd..06127bc 100644 --- a/src/imagekitio/types/shared_params/text_overlay_transformation.py +++ b/src/imagekitio/types/shared_params/text_overlay_transformation.py @@ -24,7 +24,11 @@ class TextOverlayTransformation(TypedDict, total=False): """ flip: Literal["h", "v", "h_v", "v_h"] - """Flip the text overlay horizontally, vertically, or both.""" + """ + Flip/mirror the text horizontally, vertically, or in both directions. Acceptable + values: `h` (horizontal), `v` (vertical), `h_v` (horizontal and vertical), or + `v_h`. + """ font_color: Annotated[str, PropertyInfo(alias="fontColor")] """Specifies the font color of the overlaid text. @@ -55,11 +59,10 @@ class TextOverlayTransformation(TypedDict, total=False): """ line_height: Annotated[Union[float, str], PropertyInfo(alias="lineHeight")] - """Specifies the line height of the text overlay. + """Specifies the line height for multi-line text overlays. - Accepts integer values representing line height in points. It can also accept - [arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations) - such as `bw_mul_0.2`, or `bh_div_20`. + It will come into effect only if the text wraps over multiple lines. Accepts + either an integer value or an arithmetic expression. """ padding: Union[float, str] @@ -69,10 +72,15 @@ class TextOverlayTransformation(TypedDict, total=False): shorthand order). Arithmetic expressions are also accepted. """ - radius: Union[float, Literal["max"]] - """ - Specifies the corner radius of the text overlay. Set to `max` to achieve a - circular or oval shape. + radius: Union[float, Literal["max"], str] + """Specifies the corner radius: + + - Single value (positive integer): Applied to all corners (e.g., `20`). + - `max`: Creates a circular or oval shape. + - Per-corner array: Provide four underscore-separated values representing + top-left, top-right, bottom-right, and bottom-left corners respectively (e.g., + `10_20_30_40`). See + [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). """ rotation: Union[float, str] diff --git a/src/imagekitio/types/shared_params/transformation.py b/src/imagekitio/types/shared_params/transformation.py index a48ddf8..77c317a 100644 --- a/src/imagekitio/types/shared_params/transformation.py +++ b/src/imagekitio/types/shared_params/transformation.py @@ -105,6 +105,12 @@ class Transformation(TypedDict, total=False): - A solid color: e.g., `red`, `F3F3F3`, `AAFF0010`. See [Solid color background](https://imagekit.io/docs/effects-and-enhancements#solid-color-background). + - Dominant color: `dominant` extracts the dominant color from the image. See + [Dominant color background](https://imagekit.io/docs/effects-and-enhancements#dominant-color-background). + - Gradient: `gradient_dominant` or `gradient_dominant_2` creates a gradient + using the dominant colors. Optionally specify palette size (2 or 4), e.g., + `gradient_dominant_4`. See + [Gradient background](https://imagekit.io/docs/effects-and-enhancements#gradient-background). - A blurred background: e.g., `blurred`, `blurred_25_N15`, etc. See [Blurred background](https://imagekit.io/docs/effects-and-enhancements#blurred-background). - Expand the image boundaries using generative fill: `genfill`. Not supported @@ -135,6 +141,17 @@ class Transformation(TypedDict, total=False): [Color profile](https://imagekit.io/docs/image-optimization#color-profile---cp). """ + color_replace: Annotated[str, PropertyInfo(alias="colorReplace")] + """Replaces colors in the image. Supports three formats: + + - `toColor` - Replace dominant color with the specified color. + - `toColor_tolerance` - Replace dominant color with specified tolerance (0-100). + - `toColor_tolerance_fromColor` - Replace a specific color with another within + tolerance range. Colors can be hex codes (e.g., `FF0022`) or names (e.g., + `red`, `blue`). See + [Color replacement](https://imagekit.io/docs/effects-and-enhancements#color-replace---cr). + """ + contrast_stretch: Annotated[Literal[True], PropertyInfo(alias="contrastStretch")] """ Automatically enhances the contrast of an image (contrast stretch). See @@ -162,11 +179,24 @@ class Transformation(TypedDict, total=False): [Default image](https://imagekit.io/docs/image-transformation#default-image---di). """ + distort: str + """Distorts the shape of an image. Supports two modes: + + - Perspective distortion: `p-x1_y1_x2_y2_x3_y3_x4_y4` changes the position of + the four corners starting clockwise from top-left. + - Arc distortion: `a-degrees` curves the image upwards (positive values) or + downwards (negative values). See + [Distort effect](https://imagekit.io/docs/effects-and-enhancements#distort---e-distort). + """ + dpr: float """ Accepts values between 0.1 and 5, or `auto` for automatic device pixel ratio - (DPR) calculation. See - [DPR](https://imagekit.io/docs/image-resize-and-crop#dpr---dpr). + (DPR) calculation. Also accepts arithmetic expressions. + + - Learn about + [Arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations). + - See [DPR](https://imagekit.io/docs/image-resize-and-crop#dpr---dpr). """ duration: Union[float, str] @@ -307,11 +337,15 @@ class Transformation(TypedDict, total=False): See [Quality](https://imagekit.io/docs/image-optimization#quality---q). """ - radius: Union[float, Literal["max"]] - """ - Specifies the corner radius for rounded corners (e.g., 20) or `max` for circular - or oval shape. See - [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). + radius: Union[float, Literal["max"], str] + """Specifies the corner radius for rounded corners. + + - Single value (positive integer): Applied to all corners (e.g., `20`). + - `max`: Creates a circular or oval shape. + - Per-corner array: Provide four underscore-separated values representing + top-left, top-right, bottom-right, and bottom-left corners respectively (e.g., + `10_20_30_40`). See + [Radius](https://imagekit.io/docs/effects-and-enhancements#radius---r). """ raw: str diff --git a/src/imagekitio/types/shared_params/video_overlay.py b/src/imagekitio/types/shared_params/video_overlay.py index 6c020fa..ecb088b 100644 --- a/src/imagekitio/types/shared_params/video_overlay.py +++ b/src/imagekitio/types/shared_params/video_overlay.py @@ -23,6 +23,12 @@ class VideoOverlay(BaseOverlay, total=False): format automatically. To always use base64 encoding (`ie-{base64}`), set this parameter to `base64`. To always use plain text (`i-{input}`), set it to `plain`. + + Regardless of the encoding method: + + - Leading and trailing slashes are removed. + - Remaining slashes within the path are replaced with `@@` when using plain + text. """ transformation: Iterable["Transformation"] diff --git a/tests/api_resources/beta/v2/test_files.py b/tests/api_resources/beta/v2/test_files.py index d5f6bbd..391e369 100644 --- a/tests/api_resources/beta/v2/test_files.py +++ b/tests/api_resources/beta/v2/test_files.py @@ -56,6 +56,59 @@ def test_method_upload_with_all_params(self, client: ImageKit) -> None: "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], folder="folder", is_private_file=True, @@ -158,6 +211,59 @@ async def test_method_upload_with_all_params(self, async_client: AsyncImageKit) "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], folder="folder", is_private_file=True, diff --git a/tests/api_resources/test_dummy.py b/tests/api_resources/test_dummy.py index bf19fc3..74f515a 100644 --- a/tests/api_resources/test_dummy.py +++ b/tests/api_resources/test_dummy.py @@ -8,6 +8,7 @@ import pytest from imagekitio import ImageKit, AsyncImageKit +from imagekitio._utils import parse_datetime base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -26,6 +27,7 @@ def test_method_create(self, client: ImageKit) -> None: def test_method_create_with_all_params(self, client: ImageKit) -> None: dummy = client.dummy.create( base_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -37,6 +39,15 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "start": 0, }, }, + extension_config={ + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, extensions=[ { "name": "remove-bg", @@ -53,6 +64,59 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], get_image_attributes_options={ "src": "/my-image.jpg", @@ -76,10 +140,12 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -95,6 +161,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -154,6 +221,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "width": 400, }, image_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -183,10 +251,12 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -202,6 +272,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -256,6 +327,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: ], }, overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -303,7 +375,24 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "src_set": "https://ik.imagekit.io/demo/image.jpg?tr=w-640 640w, https://ik.imagekit.io/demo/image.jpg?tr=w-1080 1080w, https://ik.imagekit.io/demo/image.jpg?tr=w-1920 1920w", "width": 400, }, + saved_extensions={ + "id": "ext_abc123", + "config": { + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, + "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), + "description": "Analyzes vehicle images for type, condition, and quality assessment", + "name": "Car Quality Analysis", + "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), + }, solid_color_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -357,10 +446,12 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -376,6 +467,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -432,6 +524,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: }, streaming_resolution="240", subtitle_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -467,6 +560,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "typography": "b", }, text_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -528,10 +622,12 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -547,6 +643,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -600,6 +697,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: }, transformation_position="path", video_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -629,10 +727,12 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -648,6 +748,7 @@ def test_method_create_with_all_params(self, client: ImageKit) -> None: "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -743,6 +844,7 @@ async def test_method_create(self, async_client: AsyncImageKit) -> None: async def test_method_create_with_all_params(self, async_client: AsyncImageKit) -> None: dummy = await async_client.dummy.create( base_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -754,6 +856,15 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "start": 0, }, }, + extension_config={ + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, extensions=[ { "name": "remove-bg", @@ -770,6 +881,59 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], get_image_attributes_options={ "src": "/my-image.jpg", @@ -793,10 +957,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -812,6 +978,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -871,6 +1038,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "width": 400, }, image_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -900,10 +1068,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -919,6 +1089,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -973,6 +1144,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) ], }, overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1020,7 +1192,24 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "src_set": "https://ik.imagekit.io/demo/image.jpg?tr=w-640 640w, https://ik.imagekit.io/demo/image.jpg?tr=w-1080 1080w, https://ik.imagekit.io/demo/image.jpg?tr=w-1920 1920w", "width": 400, }, + saved_extensions={ + "id": "ext_abc123", + "config": { + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, + "created_at": parse_datetime("2019-12-27T18:11:19.117Z"), + "description": "Analyzes vehicle images for type, condition, and quality assessment", + "name": "Car Quality Analysis", + "updated_at": parse_datetime("2019-12-27T18:11:19.117Z"), + }, solid_color_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1074,10 +1263,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -1093,6 +1284,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1149,6 +1341,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) }, streaming_resolution="240", subtitle_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1184,6 +1377,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "typography": "b", }, text_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1245,10 +1439,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -1264,6 +1460,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1317,6 +1514,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) }, transformation_position="path", video_overlay={ + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, @@ -1346,10 +1544,12 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "blur": 10, "border": "5_FF0000", "color_profile": True, + "color_replace": "colorReplace", "contrast_stretch": True, "crop": "force", "crop_mode": "pad_resize", "default_image": "defaultImage", + "distort": "distort", "dpr": 2, "duration": 0, "end_offset": 0, @@ -1365,6 +1565,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncImageKit) "opacity": 0, "original": True, "overlay": { + "layer_mode": "multiply", "position": { "focus": "center", "x": 0, diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py index 4d3d4aa..c9aa9cd 100644 --- a/tests/api_resources/test_files.py +++ b/tests/api_resources/test_files.py @@ -56,6 +56,59 @@ def test_method_update_with_all_params_overload_1(self, client: ImageKit) -> Non "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], remove_ai_tags=["string"], tags=["tag1", "tag2"], @@ -406,6 +459,59 @@ def test_method_upload_with_all_params(self, client: ImageKit) -> None: "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], folder="folder", is_private_file=True, @@ -503,6 +609,59 @@ async def test_method_update_with_all_params_overload_1(self, async_client: Asyn "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], remove_ai_tags=["string"], tags=["tag1", "tag2"], @@ -853,6 +1012,59 @@ async def test_method_upload_with_all_params(self, async_client: AsyncImageKit) "name": "google-auto-tagging", }, {"name": "ai-auto-description"}, + { + "name": "ai-tasks", + "tasks": [ + { + "instruction": "What types of clothing items are visible in this image?", + "type": "select_tags", + "vocabulary": ["shirt", "tshirt", "dress", "trousers", "jacket"], + "max_selections": 1, + "min_selections": 0, + }, + { + "instruction": "Is this a luxury or high-end fashion item?", + "type": "yes_no", + "on_no": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_unknown": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + "on_yes": { + "add_tags": ["luxury", "premium"], + "remove_tags": ["budget", "affordable"], + "set_metadata": [ + { + "field": "price_range", + "value": "premium", + } + ], + "unset_metadata": [{"field": "price_range"}], + }, + }, + ], + }, + { + "id": "ext_abc123", + "name": "saved-extension", + }, ], folder="folder", is_private_file=True, diff --git a/tests/api_resources/test_saved_extensions.py b/tests/api_resources/test_saved_extensions.py new file mode 100644 index 0000000..4a12279 --- /dev/null +++ b/tests/api_resources/test_saved_extensions.py @@ -0,0 +1,489 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from imagekitio import ImageKit, AsyncImageKit +from tests.utils import assert_matches_type +from imagekitio.types import ( + SavedExtensionListResponse, +) +from imagekitio.types.shared import SavedExtension + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSavedExtensions: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.create( + config={"name": "remove-bg"}, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.create( + config={ + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_create(self, client: ImageKit) -> None: + response = client.saved_extensions.with_raw_response.create( + config={"name": "remove-bg"}, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_create(self, client: ImageKit) -> None: + with client.saved_extensions.with_streaming_response.create( + config={"name": "remove-bg"}, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.update( + id="id", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.update( + id="id", + config={ + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, + description="x", + name="x", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_update(self, client: ImageKit) -> None: + response = client.saved_extensions.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_update(self, client: ImageKit) -> None: + with client.saved_extensions.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_update(self, client: ImageKit) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.saved_extensions.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_list(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.list() + assert_matches_type(SavedExtensionListResponse, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_list(self, client: ImageKit) -> None: + response = client.saved_extensions.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = response.parse() + assert_matches_type(SavedExtensionListResponse, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_list(self, client: ImageKit) -> None: + with client.saved_extensions.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = response.parse() + assert_matches_type(SavedExtensionListResponse, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_delete(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.delete( + "id", + ) + assert saved_extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_delete(self, client: ImageKit) -> None: + response = client.saved_extensions.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = response.parse() + assert saved_extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: ImageKit) -> None: + with client.saved_extensions.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = response.parse() + assert saved_extension is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_delete(self, client: ImageKit) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.saved_extensions.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_method_get(self, client: ImageKit) -> None: + saved_extension = client.saved_extensions.get( + "id", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_raw_response_get(self, client: ImageKit) -> None: + response = client.saved_extensions.with_raw_response.get( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_streaming_response_get(self, client: ImageKit) -> None: + with client.saved_extensions.with_streaming_response.get( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + def test_path_params_get(self, client: ImageKit) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.saved_extensions.with_raw_response.get( + "", + ) + + +class TestAsyncSavedExtensions: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.create( + config={"name": "remove-bg"}, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.create( + config={ + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncImageKit) -> None: + response = await async_client.saved_extensions.with_raw_response.create( + config={"name": "remove-bg"}, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = await response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncImageKit) -> None: + async with async_client.saved_extensions.with_streaming_response.create( + config={"name": "remove-bg"}, + description="Analyzes vehicle images for type, condition, and quality assessment", + name="Car Quality Analysis", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = await response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.update( + id="id", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.update( + id="id", + config={ + "name": "remove-bg", + "options": { + "add_shadow": True, + "bg_color": "bg_color", + "bg_image_url": "bg_image_url", + "semitransparency": True, + }, + }, + description="x", + name="x", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncImageKit) -> None: + response = await async_client.saved_extensions.with_raw_response.update( + id="id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = await response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncImageKit) -> None: + async with async_client.saved_extensions.with_streaming_response.update( + id="id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = await response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncImageKit) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.saved_extensions.with_raw_response.update( + id="", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.list() + assert_matches_type(SavedExtensionListResponse, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncImageKit) -> None: + response = await async_client.saved_extensions.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = await response.parse() + assert_matches_type(SavedExtensionListResponse, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncImageKit) -> None: + async with async_client.saved_extensions.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = await response.parse() + assert_matches_type(SavedExtensionListResponse, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.delete( + "id", + ) + assert saved_extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncImageKit) -> None: + response = await async_client.saved_extensions.with_raw_response.delete( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = await response.parse() + assert saved_extension is None + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncImageKit) -> None: + async with async_client.saved_extensions.with_streaming_response.delete( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = await response.parse() + assert saved_extension is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncImageKit) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.saved_extensions.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_method_get(self, async_client: AsyncImageKit) -> None: + saved_extension = await async_client.saved_extensions.get( + "id", + ) + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_raw_response_get(self, async_client: AsyncImageKit) -> None: + response = await async_client.saved_extensions.with_raw_response.get( + "id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + saved_extension = await response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_streaming_response_get(self, async_client: AsyncImageKit) -> None: + async with async_client.saved_extensions.with_streaming_response.get( + "id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + saved_extension = await response.parse() + assert_matches_type(SavedExtension, saved_extension, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism tests are disabled") + @parametrize + async def test_path_params_get(self, async_client: AsyncImageKit) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.saved_extensions.with_raw_response.get( + "", + ) From 04fde194de1d741c80f34f4d734949fab81ef8a5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:09:51 +0000 Subject: [PATCH 10/12] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 0bf4f71..4211093 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 48 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-7a3257eb171467b637c8d72877f201c2e6038c71ed447a9453230b7309ce7416.yml -openapi_spec_hash: 87b000a9989ad5c9526f28d91b8a1749 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-606f0e2a6ecb6c36557e166764d39b8f619a74904db6bb5ed8bb348ed451b337.yml +openapi_spec_hash: 1d5f5cdb3f7992a183c368ecd009316e config_hash: aeb6eb949d73382270bbd8bbf2e4cf2a From 06de9ebc34e6fbf21f3863cd86d75556c429ff8f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:21:38 +0000 Subject: [PATCH 11/12] fix: add ai-tasks property to response schemas with enum values --- .stats.yml | 4 ++-- src/imagekitio/types/beta/v2/file_upload_response.py | 2 ++ src/imagekitio/types/file_update_response.py | 2 ++ src/imagekitio/types/file_upload_response.py | 2 ++ src/imagekitio/types/upload_pre_transform_success_event.py | 2 ++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4211093..265ca1f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 48 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-606f0e2a6ecb6c36557e166764d39b8f619a74904db6bb5ed8bb348ed451b337.yml -openapi_spec_hash: 1d5f5cdb3f7992a183c368ecd009316e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-c028a7584d3508f268ce5c5b824b50af88eaa140620dd03a1b35f409f510603c.yml +openapi_spec_hash: f9b780b2398a87678a13355e48cd515f config_hash: aeb6eb949d73382270bbd8bbf2e4cf2a diff --git a/src/imagekitio/types/beta/v2/file_upload_response.py b/src/imagekitio/types/beta/v2/file_upload_response.py index 46cb755..4696c05 100644 --- a/src/imagekitio/types/beta/v2/file_upload_response.py +++ b/src/imagekitio/types/beta/v2/file_upload_response.py @@ -41,6 +41,8 @@ class ExtensionStatus(BaseModel): alias="ai-auto-description", default=None ) + ai_tasks: Optional[Literal["success", "pending", "failed"]] = FieldInfo(alias="ai-tasks", default=None) + aws_auto_tagging: Optional[Literal["success", "pending", "failed"]] = FieldInfo( alias="aws-auto-tagging", default=None ) diff --git a/src/imagekitio/types/file_update_response.py b/src/imagekitio/types/file_update_response.py index 936e336..3ff72c0 100644 --- a/src/imagekitio/types/file_update_response.py +++ b/src/imagekitio/types/file_update_response.py @@ -16,6 +16,8 @@ class FileUpdateResponseExtensionStatus(BaseModel): alias="ai-auto-description", default=None ) + ai_tasks: Optional[Literal["success", "pending", "failed"]] = FieldInfo(alias="ai-tasks", default=None) + aws_auto_tagging: Optional[Literal["success", "pending", "failed"]] = FieldInfo( alias="aws-auto-tagging", default=None ) diff --git a/src/imagekitio/types/file_upload_response.py b/src/imagekitio/types/file_upload_response.py index e99dc77..a33b47e 100644 --- a/src/imagekitio/types/file_upload_response.py +++ b/src/imagekitio/types/file_upload_response.py @@ -41,6 +41,8 @@ class ExtensionStatus(BaseModel): alias="ai-auto-description", default=None ) + ai_tasks: Optional[Literal["success", "pending", "failed"]] = FieldInfo(alias="ai-tasks", default=None) + aws_auto_tagging: Optional[Literal["success", "pending", "failed"]] = FieldInfo( alias="aws-auto-tagging", default=None ) diff --git a/src/imagekitio/types/upload_pre_transform_success_event.py b/src/imagekitio/types/upload_pre_transform_success_event.py index 8584f86..991baa1 100644 --- a/src/imagekitio/types/upload_pre_transform_success_event.py +++ b/src/imagekitio/types/upload_pre_transform_success_event.py @@ -51,6 +51,8 @@ class UploadPreTransformSuccessEventDataExtensionStatus(BaseModel): alias="ai-auto-description", default=None ) + ai_tasks: Optional[Literal["success", "pending", "failed"]] = FieldInfo(alias="ai-tasks", default=None) + aws_auto_tagging: Optional[Literal["success", "pending", "failed"]] = FieldInfo( alias="aws-auto-tagging", default=None ) From 2202b5af9e9eacd68b798b1f1377bde6490812b1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:22:01 +0000 Subject: [PATCH 12/12] release: 5.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/imagekitio/_version.py | 2 +- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8e76abb..4808d97 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "5.0.0" + ".": "5.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e015abb..931d524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## 5.1.0 (2026-01-15) + +Full Changelog: [v5.0.0...v5.1.0](https://github.com/imagekit-developer/imagekit-python/compare/v5.0.0...v5.1.0) + +### Features + +* **api:** Add saved extensions API and enhance transformation options ([a0781ed](https://github.com/imagekit-developer/imagekit-python/commit/a0781edc19f2cbd78a87e973e0cc2277079fb02a)) +* **client:** add support for binary request streaming ([f8580d6](https://github.com/imagekit-developer/imagekit-python/commit/f8580d644e31312e439a54704ca2e3858407ea0b)) + + +### Bug Fixes + +* add ai-tasks property to response schemas with enum values ([06de9eb](https://github.com/imagekit-developer/imagekit-python/commit/06de9ebc34e6fbf21f3863cd86d75556c429ff8f)) +* **client:** loosen auth header validation ([40ef10e](https://github.com/imagekit-developer/imagekit-python/commit/40ef10e6e81ff3727a095aead127d296486a3c09)) +* use async_to_httpx_files in patch method ([0014808](https://github.com/imagekit-developer/imagekit-python/commit/0014808307e55091a943d2f6b087fefbaee8ed0a)) + + +### Chores + +* **internal:** add `--fix` argument to lint script ([e6bf019](https://github.com/imagekit-developer/imagekit-python/commit/e6bf0196fe985302e11fb440cd3d215114a8e4c3)) +* **internal:** add missing files argument to base client ([aec7892](https://github.com/imagekit-developer/imagekit-python/commit/aec7892b063c00b730afcdc440c0fa3ebe1cdae8)) +* **internal:** codegen related update ([49635b4](https://github.com/imagekit-developer/imagekit-python/commit/49635b4dc6bd4268fc6a62f9df2a2e15c56afcee)) +* speedup initial import ([ad1da84](https://github.com/imagekit-developer/imagekit-python/commit/ad1da84adad57d0a64a8f06a04c6ddb6b8f0e96b)) + + +### Documentation + +* prominently feature MCP server setup in root SDK readmes ([51c1a9a](https://github.com/imagekit-developer/imagekit-python/commit/51c1a9ae1545a25b574195ec73b83dab64d9becb)) + ## 5.0.0 (2025-12-13) Full Changelog: [v0.0.1...v5.0.0](https://github.com/imagekit-developer/imagekit-python/compare/v0.0.1...v5.0.0) diff --git a/pyproject.toml b/pyproject.toml index 9902514..39ea40b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "imagekitio" -version = "5.0.0" +version = "5.1.0" description = "The official Python library for the ImageKit API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/imagekitio/_version.py b/src/imagekitio/_version.py index 32a263a..ed8df8a 100644 --- a/src/imagekitio/_version.py +++ b/src/imagekitio/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "imagekitio" -__version__ = "5.0.0" # x-release-please-version +__version__ = "5.1.0" # x-release-please-version