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/.stats.yml b/.stats.yml
index 333dfb4..4211093 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-606f0e2a6ecb6c36557e166764d39b8f619a74904db6bb5ed8bb348ed451b337.yml
+openapi_spec_hash: 1d5f5cdb3f7992a183c368ecd009316e
+config_hash: aeb6eb949d73382270bbd8bbf2e4cf2a
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e015abb..0648e14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
# 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
+
+* **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/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.
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/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/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'
diff --git a/src/imagekitio/_base_client.py b/src/imagekitio/_base_client.py
index 384e7c0..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,9 +1282,24 @@ def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: BinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", 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 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, content=content, files=to_httpx_files(files), **options
+ )
return self.request(cast_to, opts)
def put(
@@ -1258,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)
@@ -1272,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(
@@ -1714,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,
@@ -1726,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],
@@ -1739,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,
@@ -1751,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)
@@ -1767,9 +1854,29 @@ async def patch(
*,
cast_to: Type[ResponseT],
body: Body | None = None,
+ content: AsyncBinaryTypes | None = None,
+ files: RequestFiles | None = None,
options: RequestOptions = {},
) -> ResponseT:
- opts = FinalRequestOptions.construct(method="patch", 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 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,
+ content=content,
+ files=await async_to_httpx_files(files),
+ **options,
+ )
return await self.request(cast_to, opts)
async def put(
@@ -1778,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)
@@ -1792,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/_client.py b/src/imagekitio/_client.py
index 3b9f4ae..32ed4c0 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,30 @@
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,
+ saved_extensions,
+ 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.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
__all__ = [
"Timeout",
@@ -51,19 +69,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 +139,79 @@ 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 saved_extensions(self) -> SavedExtensionsResource:
+ from .resources.saved_extensions import SavedExtensionsResource
+
+ return SavedExtensionsResource(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
@@ -172,9 +238,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(
@@ -273,19 +337,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 +407,79 @@ 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 saved_extensions(self) -> AsyncSavedExtensionsResource:
+ from .resources.saved_extensions import AsyncSavedExtensionsResource
+
+ return AsyncSavedExtensionsResource(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
@@ -394,9 +506,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(
@@ -495,59 +605,247 @@ 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 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
+
+ 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 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
+
+ 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 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
+
+ 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 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
+
+ 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
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/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
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(
+ "",
+ )
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):