diff --git a/.gitignore b/.gitignore index f382096f..95ceb189 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,3 @@ dist .envrc codegen.log Brewfile.lock.json - -examples/temp \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c3a26d68..5b010307 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,3 @@ { "python.analysis.importFormat": "relative", - "python.analysis.typeCheckingMode": "basic" } diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e015abb4..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,57 +0,0 @@ -# Changelog - -## 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) - -### Features - -* add bulk delete options ([c1c4d32](https://github.com/imagekit-developer/imagekit-python/commit/c1c4d3206b06594ba77a8a1c4dab7d0c5b74de9a)) -* add file related functionalities ([681677b](https://github.com/imagekit-developer/imagekit-python/commit/681677bc60a207f433b4bc242c41e37f2d4c05a1)) -* add sdk version to url ([9c3e67d](https://github.com/imagekit-developer/imagekit-python/commit/9c3e67d20f78b799e974889420ead23f457b5cfa)) -* add url class for url genration ([5e615ed](https://github.com/imagekit-developer/imagekit-python/commit/5e615ed34386e3231c5c7963ff37ceb28ab7d2f1)) -* **api:** python publish true ([8072dfd](https://github.com/imagekit-developer/imagekit-python/commit/8072dfd2eee562f98ac79fb5b11afe700e0dd6a3)) -* implement client with all func. ([67dd4b2](https://github.com/imagekit-developer/imagekit-python/commit/67dd4b28822086009278e4ab3f85d52690e6e9b7)) -* implement get_remote_url_metadata ([1272740](https://github.com/imagekit-developer/imagekit-python/commit/12727400dc5bc6678f6769c5143c11962f58eea4)) -* **webhooks:** allow key parameter to accept bytes in unwrap method ([09ae375](https://github.com/imagekit-developer/imagekit-python/commit/09ae37575b6b1eba57f67c6b1dea3d59e10d270d)) - - -### Bug Fixes - -* binary file upload ([23c9c46](https://github.com/imagekit-developer/imagekit-python/commit/23c9c46f37a5b32144f86700227254e6f05bf491)) -* change ubuntu latest to ubuntu-20.04 in test.yml ([1e4b551](https://github.com/imagekit-developer/imagekit-python/commit/1e4b55192d08ebf1aa436fa56832322477605942)) -* Changes for CI/CD ([0bd2ac3](https://github.com/imagekit-developer/imagekit-python/commit/0bd2ac3e9b11e8269a2eacb2424d49ef58e37c5f)) -* fix issue [#35](https://github.com/imagekit-developer/imagekit-python/issues/35),[#37](https://github.com/imagekit-developer/imagekit-python/issues/37),[#41](https://github.com/imagekit-developer/imagekit-python/issues/41),[#44](https://github.com/imagekit-developer/imagekit-python/issues/44) ([1f913c8](https://github.com/imagekit-developer/imagekit-python/commit/1f913c8e34a06afbffa93adbbc79e8a174a02dac)) -* fix query params implementation ([2b7e6d4](https://github.com/imagekit-developer/imagekit-python/commit/2b7e6d4a148b6d94b52532846bd950d4eeeefac4)) -* make ik-attachment option handle True boolean value ([6eb9cd0](https://github.com/imagekit-developer/imagekit-python/commit/6eb9cd099021a1fd9bcc9dfeb080ec610d4bcfbd)) -* move the workflow to correct folder ([d9f933a](https://github.com/imagekit-developer/imagekit-python/commit/d9f933a8e78c61b8a61df1d74a28859f9e889378)) -* request toolbelt to 0.10.1 in requirements/test/txt ([c22ed89](https://github.com/imagekit-developer/imagekit-python/commit/c22ed89208f69f7d8fb21cc777049d72dad40093)) -* **serialization:** adjust custom_metadata type check for serialization ([6e3f209](https://github.com/imagekit-developer/imagekit-python/commit/6e3f2092cad4b2c3ed7d1f3086c7bfb2a9a51b08)) - - -### Chores - -* add func alias ([d7ce593](https://github.com/imagekit-developer/imagekit-python/commit/d7ce593318b24f33ba828b65042e16e892690b80)) -* add init file ([0cbbd27](https://github.com/imagekit-developer/imagekit-python/commit/0cbbd27f00ac3fe36d3fbc0bf6fa2b015308576c)) -* add publish github workflow script ([a275172](https://github.com/imagekit-developer/imagekit-python/commit/a275172c3e7096b7390665102bae4d95c718db9d)) -* add required constants ([48de1c0](https://github.com/imagekit-developer/imagekit-python/commit/48de1c02295fb42d522f8ee930c16ee763d7b93d)) -* add requirements files ([e8d3d9d](https://github.com/imagekit-developer/imagekit-python/commit/e8d3d9d60e946b036b3f8e37a9dbf1e68be5482d)) -* add sample file for devs ([65d1a3f](https://github.com/imagekit-developer/imagekit-python/commit/65d1a3f77eaa5a5c9dba5202a75dee3c70aa64a0)) -* add sample of get file metadata ([6d11584](https://github.com/imagekit-developer/imagekit-python/commit/6d115841c341df0f7a9d4d9bd0c33c1cf386d9c7)) -* change pacakge name & fix import ([2c1734a](https://github.com/imagekit-developer/imagekit-python/commit/2c1734a6e12c935bc80f72ec6b8cdd5a971e5a47)) -* fix package name ([c0c939d](https://github.com/imagekit-developer/imagekit-python/commit/c0c939d86fa5738855a0d6b606e33249ecd5a47a)) -* fix package name ([4bc8041](https://github.com/imagekit-developer/imagekit-python/commit/4bc8041e22c6333710645ddc95446c9c348eea5b)) -* fix sample ([2188038](https://github.com/imagekit-developer/imagekit-python/commit/2188038436aabfce68a3c1d7bb198ffda203dc72)) -* init ([febccef](https://github.com/imagekit-developer/imagekit-python/commit/febccef19d6ca6ae2b6c4272d44ae1625c9f3391)) -* remove unecessary workflow file ([97f19eb](https://github.com/imagekit-developer/imagekit-python/commit/97f19eb8284c5edfe164f98ad296ea1e69b21bf8)) -* remove unused dummy methods from API documentation ([4727908](https://github.com/imagekit-developer/imagekit-python/commit/472790845ef7009aa3695fc084ef8c5d1d63f2ab)) -* sync repo ([c6afd44](https://github.com/imagekit-developer/imagekit-python/commit/c6afd449e74ebb20ebc8d3390355219fccaf2178)) -* unused import removed ([22774ff](https://github.com/imagekit-developer/imagekit-python/commit/22774fff1ac08c0573efc06ab10f3fe31e6d3f69)) -* update SDK settings ([81f0de9](https://github.com/imagekit-developer/imagekit-python/commit/81f0de954a0d531c6b98354386462f4186a58aba)) - - -### Build System - -* add url and requirements ([211228e](https://github.com/imagekit-developer/imagekit-python/commit/211228ef91fe29b83507c89f3bf22cfb6b1c8184)) -* add url and requirements ([683ad01](https://github.com/imagekit-developer/imagekit-python/commit/683ad016099d4e4614b6f369bff69d9a7422029e)) -* add url and requirements ([#2](https://github.com/imagekit-developer/imagekit-python/issues/2)) ([211228e](https://github.com/imagekit-developer/imagekit-python/commit/211228ef91fe29b83507c89f3bf22cfb6b1c8184)) diff --git a/README.md b/README.md index 6ea03a48..29a8c93e 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,15 @@ -# ImageKit.io Python SDK +# Image Kit Python API library [![PyPI version](https://img.shields.io/pypi/v/imagekitio.svg?label=pypi%20(stable))](https://pypi.org/project/imagekitio/) -The ImageKit Python SDK provides convenient access to the ImageKit REST API from any Python 3.9+ application. It offers powerful tools for URL generation and transformation, signed URLs for secure content delivery, webhook verification, file uploads, and more. The library includes type definitions for all request params and response fields, and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). +The Image Kit Python library provides convenient access to the Image Kit REST API from any Python 3.9+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). -The REST API documentation can be found on [imagekit.io](https://imagekit.io/docs/api-reference). The full API of this library can be found in [api.md](api.md). +## Documentation -## Table of Contents - -- [Installation](#installation) -- [Requirements](#requirements) -- [Usage](#usage) - - [Using types](#using-types) - - [Nested params](#nested-params) - - [Async usage](#async-usage) -- [URL generation](#url-generation) - - [Basic URL generation](#basic-url-generation) - - [URL generation with transformations](#url-generation-with-transformations) - - [URL generation with image overlay](#url-generation-with-image-overlay) - - [URL generation with text overlay](#url-generation-with-text-overlay) - - [URL generation with multiple overlays](#url-generation-with-multiple-overlays) - - [Signed URLs for secure delivery](#signed-urls-for-secure-delivery) - - [Using Raw transformations for undocumented features](#using-raw-transformations-for-undocumented-features) -- [Authentication parameters for client-side uploads](#authentication-parameters-for-client-side-uploads) -- [Webhook verification](#webhook-verification) -- [Advanced Usage](#advanced-usage) - - [File uploads](#file-uploads) - - [Handling errors](#handling-errors) - - [Retries](#retries) - - [Timeouts](#timeouts) - - [Logging](#logging) - - [Accessing raw response data](#accessing-raw-response-data-eg-headers) - - [Making custom/undocumented requests](#making-customundocumented-requests) - - [Configuring the HTTP client](#configuring-the-http-client) - - [Managing HTTP resources](#managing-http-resources) -- [Versioning](#versioning) -- [Contributing](#contributing) +The REST API documentation can be found on [imagekit.io](https://imagekit.io/docs/api-reference). The full API of this library can be found in [api.md](api.md). ## Installation @@ -55,18 +28,16 @@ from imagekitio import ImageKit client = ImageKit( private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), # This is the default and can be omitted + password=os.environ.get( + "OPTIONAL_IMAGEKIT_IGNORES_THIS" + ), # This is the default and can be omitted ) -# Upload a file -with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - response = client.files.upload( - file=file_data, - file_name="uploaded-image.jpg", + file=b"https://www.example.com/public-url.jpg", + file_name="file-name.jpg", ) -print(response.file_id) -print(response.url) +print(response.video_codec) ``` While you can provide a `private_key` keyword argument, @@ -74,50 +45,7 @@ we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) to add `IMAGEKIT_PRIVATE_KEY="My Private Key"` to your `.env` file so that your Private Key is not stored in source control. - -### Using types - -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: - -- Serializing back into JSON, `model.to_json()` -- Converting to a dictionary, `model.to_dict()` - -Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. - -### Nested params - -Nested parameters are dictionaries, typed using `TypedDict`, for example: - -```python -from imagekitio import ImageKit - -client = ImageKit() - -# Read file into memory and upload -with open("/path/to/file.jpg", "rb") as f: - file_data = f.read() - -response = client.files.upload( - file=file_data, - file_name="fileName", - transformation={ - "post": [ - { - "type": "thumbnail", - "value": "w-150,h-150", - }, - { - "protocol": "dash", - "type": "abs", - "value": "sr-240_360_480_720_1080", - }, - ] - }, -) -print(response.file_id) -``` - -### Async usage +## Async usage Simply import `AsyncImageKit` instead of `ImageKit` and use `await` with each API call: @@ -128,20 +56,18 @@ from imagekitio import AsyncImageKit client = AsyncImageKit( private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), # This is the default and can be omitted + password=os.environ.get( + "OPTIONAL_IMAGEKIT_IGNORES_THIS" + ), # This is the default and can be omitted ) async def main() -> None: - # Read file into memory and upload - with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - response = await client.files.upload( - file=file_data, + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) - print(response.file_id) - print(response.url) + print(response.video_codec) asyncio.run(main()) @@ -149,7 +75,7 @@ asyncio.run(main()) Functionality between the synchronous and asynchronous clients is otherwise identical. -#### With aiohttp +### With aiohttp By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. @@ -174,400 +100,78 @@ async def main() -> None: private_key=os.environ.get( "IMAGEKIT_PRIVATE_KEY" ), # This is the default and can be omitted + password=os.environ.get( + "OPTIONAL_IMAGEKIT_IGNORES_THIS" + ), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: - # Read file into memory and upload - with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - response = await client.files.upload( - file=file_data, + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) - print(response.file_id) - print(response.url) + print(response.video_codec) asyncio.run(main()) ``` -## URL generation +## Using types -The ImageKit SDK provides a powerful `helper.build_url()` method for generating optimized image and video URLs with transformations. Here are examples ranging from simple URLs to complex transformations with overlays and signed URLs. - -### Basic URL generation - -Generate a simple URL without any transformations: - -```python -import os -from imagekitio import ImageKit - -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) - -# Basic URL without transformations -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/path/to/image.jpg", -) -print(url) -# Result: https://ik.imagekit.io/your_imagekit_id/path/to/image.jpg -``` - -### URL generation with transformations - -Apply common transformations like resizing, cropping, and format conversion: - -```python -import os -from imagekitio import ImageKit - -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) - -# URL with basic transformations -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/path/to/image.jpg", - transformation=[ - { - "width": 400, - "height": 300, - "crop": "maintain_ratio", - "quality": 80, - "format": "webp", - } - ], -) -print(url) -# Result: https://ik.imagekit.io/your_imagekit_id/path/to/image.jpg?tr=w-400,h-300,c-maintain_ratio,q-80,f-webp -``` - -### URL generation with image overlay - -Add image overlays to your base image: - -```python -import os -from imagekitio import ImageKit - -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) - -# URL with image overlay -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/path/to/base-image.jpg", - transformation=[ - { - "width": 500, - "height": 400, - "overlay": { - "type": "image", - "input": "/path/to/overlay-logo.png", - "position": { - "x": 10, - "y": 10, - }, - "transformation": [ - { - "width": 100, - "height": 50, - } - ], - }, - } - ], -) -print(url) -# Result: URL with image overlay positioned at x:10, y:10 -``` - -### URL generation with text overlay - -Add customized text overlays: - -```python -import os -from imagekitio import ImageKit +Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) +- Serializing back into JSON, `model.to_json()` +- Converting to a dictionary, `model.to_dict()` -# URL with text overlay -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/path/to/base-image.jpg", - transformation=[ - { - "width": 600, - "height": 400, - "overlay": { - "type": "text", - "text": "Sample Text Overlay", - "position": { - "x": 50, - "y": 50, - "focus": "center", - }, - "transformation": [ - { - "font_size": 40, - "font_family": "Arial", - "font_color": "FFFFFF", - "typography": "b", # bold - } - ], - }, - } - ], -) -print(url) -# Result: URL with bold white Arial text overlay at center position -``` +Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. -### URL generation with multiple overlays +## Nested params -Combine multiple overlays for complex compositions: +Nested parameters are dictionaries, typed using `TypedDict`, for example: ```python -import os from imagekitio import ImageKit -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) +client = ImageKit() -# URL with multiple overlays (text + image) -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/path/to/base-image.jpg", - transformation=[ - { - "width": 800, - "height": 600, - "overlay": { - "type": "text", - "text": "Header Text", - "position": { - "x": 20, - "y": 20, - }, - "transformation": [ - { - "font_size": 30, - "font_color": "000000", - } - ], +response = client.files.upload( + file=b"raw file contents", + file_name="fileName", + transformation={ + "post": [ + { + "type": "thumbnail", + "value": "w-150,h-150", }, - }, - { - "overlay": { - "type": "image", - "input": "/watermark.png", - "position": { - "focus": "bottom_right", - }, - "transformation": [ - { - "width": 100, - "opacity": 70, - } - ], + { + "protocol": "dash", + "type": "abs", + "value": "sr-240_360_480_720_1080", }, - }, - ], -) -print(url) -# Result: URL with text overlay at top-left and semi-transparent watermark at bottom-right -``` - -### Signed URLs for secure delivery - -Generate signed URLs that expire after a specified time for secure content delivery: - -```python -import os -from imagekitio import ImageKit - -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) - -# Generate a signed URL that expires in 1 hour (3600 seconds) -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/private/secure-image.jpg", - transformation=[ - { - "width": 400, - "height": 300, - "quality": 90, - } - ], - signed=True, - expires_in=3600, # URL expires in 1 hour -) -print(url) -# Result: URL with signature parameters (?ik-t=timestamp&ik-s=signature) - -# Generate a signed URL that doesn't expire -permanent_signed_url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/private/secure-image.jpg", - signed=True, - # No expires_in means the URL won't expire -) -print(permanent_signed_url) -# Result: URL with signature parameter (?ik-s=signature) -``` - -### Using Raw transformations for undocumented features - -ImageKit frequently adds new transformation parameters that might not yet be documented in the SDK. You can use the `raw` parameter to access these features or create custom transformation strings: - -```python -import os -from imagekitio import ImageKit - -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) - -# Using Raw transformation for undocumented or new parameters -url = client.helper.build_url( - url_endpoint="https://ik.imagekit.io/your_imagekit_id", - src="/https/github.com/path/to/image.jpg", - transformation=[ - { - # Combine documented transformations with raw parameters - "width": 400, - "height": 300, - }, - { - # Use raw for undocumented transformations or complex parameters - "raw": "something-new", - }, - ], -) -print(url) -# Result: https://ik.imagekit.io/your_imagekit_id/path/to/image.jpg?tr=w-400,h-300:something-new -``` - -## Authentication parameters for client-side uploads - -Generate authentication parameters for secure client-side file uploads: - -```python -import os -from imagekitio import ImageKit - -client = ImageKit( - private_key=os.environ.get("IMAGEKIT_PRIVATE_KEY"), -) - -# Generate authentication parameters for client-side uploads -auth_params = client.helper.get_authentication_parameters() -print(auth_params) -# Result: {'expire': , 'signature': '', 'token': ''} - -# Generate with custom token and expiry -custom_auth_params = client.helper.get_authentication_parameters( - token="my-custom-token", - expire=1800 + ] + }, ) -print(custom_auth_params) -# Result: {'expire': 1800, 'signature': '', 'token': 'my-custom-token'} +print(response.transformation) ``` -These authentication parameters can be used in client-side upload forms to securely upload files without exposing your private API key. +## File uploads -## Webhook verification - -The ImageKit SDK provides utilities to verify webhook signatures for secure event handling. This ensures that webhook requests are actually coming from ImageKit and haven't been tampered with. - -For detailed information about webhook setup, signature verification, and handling different webhook events, refer to the [ImageKit webhook documentation](https://imagekit.io/docs/webhooks#verify-webhook-signature). - -## Advanced Usage - -### File uploads - -Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, an `IO[bytes]` file object, or a tuple of `(filename, contents, media type)`. - -Here are common file upload patterns: +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. ```python from pathlib import Path from imagekitio import ImageKit -import io client = ImageKit() -# Method 1: Upload from bytes -# Read file into memory first, then upload -with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - -response = client.files.upload( - file=file_data, - file_name="uploaded-image.jpg", -) - -# Method 2: Upload from file stream (for large files) -# Pass file object directly - SDK reads it -with open("/path/to/your/image.jpg", "rb") as file_stream: - response = client.files.upload( - file=file_stream, - file_name="uploaded-image.jpg", - ) - -# Method 3: Upload using Path object (SDK reads automatically) -response = client.files.upload( - file=Path("/path/to/file.jpg"), - file_name="fileName.jpg", -) - -# Method 4: Upload from BytesIO (for programmatically generated content) -content = b"your binary data" -bytes_io = io.BytesIO(content) -response = client.files.upload( - file=bytes_io, - file_name="binary-upload.jpg", -) - -# Method 5: Upload with custom content type using tuple format -image_data = b"your binary data" -response = client.files.upload( - file=("custom.jpg", image_data, "image/jpeg"), - file_name="custom-upload.jpg", +client.files.upload( + file=Path("/path/to/file"), + file_name="fileName", ) ``` The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. -**Note:** URL strings (e.g., `"https://example.com/image.jpg"`) are not supported by the Python SDK. To upload from a URL, download the content first: - -```python -import urllib.request - -# Download from URL and upload to ImageKit -url = "https://example.com/image.jpg" -with urllib.request.urlopen(url) as response: - url_content = response.read() - -# Upload the downloaded content -upload_response = client.files.upload( - file=url_content, - file_name="downloaded-image.jpg", -) -``` - -### Handling errors +## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `imagekitio.APIConnectionError` is raised. @@ -583,12 +187,8 @@ from imagekitio import ImageKit client = ImageKit() try: - # Read file into memory and upload - with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - - response = client.files.upload( - file=file_data, + client.files.upload( + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) except imagekitio.APIConnectionError as e: @@ -633,11 +233,8 @@ client = ImageKit( ) # Or, configure per-request: -with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - client.with_options(max_retries=5).files.upload( - file=file_data, + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) ``` @@ -662,11 +259,8 @@ client = ImageKit( ) # Override per-request: -with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - client.with_options(timeout=5.0).files.upload( - file=file_data, + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) ``` @@ -675,6 +269,8 @@ On timeout, an `APITimeoutError` is thrown. Note that requests that time out are [retried twice by default](#retries). +## Advanced + ### Logging We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. @@ -707,19 +303,14 @@ The "raw" Response object can be accessed by prefixing `.with_raw_response.` to from imagekitio import ImageKit client = ImageKit() - -# Read file into memory and upload -with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - response = client.files.with_raw_response.upload( - file=file_data, + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) print(response.headers.get('X-My-Header')) file = response.parse() # get the object that `files.upload()` would have returned -print(file.file_id) +print(file.video_codec) ``` These methods return an [`APIResponse`](https://github.com/imagekit-developer/imagekit-python/tree/master/src/imagekitio/_response.py) object. @@ -733,12 +324,8 @@ The above interface eagerly reads the full response body when you make the reque To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -# Read file into memory and upload -with open("/path/to/your/image.jpg", "rb") as f: - file_data = f.read() - with client.files.with_streaming_response.upload( - file=file_data, + file=b"https://www.example.com/public-url.jpg", file_name="file-name.jpg", ) as response: print(response.headers.get("X-My-Header")) diff --git a/api.md b/api.md index b617936d..c88d631a 100644 --- a/api.md +++ b/api.md @@ -24,6 +24,12 @@ from imagekitio.types import ( ) ``` +# Dummy + +Methods: + +- client.dummy.create(\*\*params) -> None + # CustomMetadataFields Types: diff --git a/scripts/lint b/scripts/lint index eb9a4dda..d4778c62 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 384e7c0a..f8b77578 100644 --- a/src/imagekitio/_base_client.py +++ b/src/imagekitio/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( diff --git a/src/imagekitio/_client.py b/src/imagekitio/_client.py index 3b9f4aec..efcf3b52 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,18 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.beta import beta -from .resources.cache import cache -from .resources.files import files -from .resources.folders import folders -from .resources.accounts import accounts + +if TYPE_CHECKING: + from .resources import beta, cache, dummy, files, assets, folders, accounts, custom_metadata_fields + from .resources.dummy import DummyResource, AsyncDummyResource + from .resources.assets import AssetsResource, AsyncAssetsResource + from .resources.webhooks import WebhooksResource, AsyncWebhooksResource + from .resources.beta.beta import BetaResource, AsyncBetaResource + from .resources.cache.cache import CacheResource, AsyncCacheResource + from .resources.files.files import FilesResource, AsyncFilesResource + from .resources.folders.folders import FoldersResource, AsyncFoldersResource + from .resources.accounts.accounts import AccountsResource, AsyncAccountsResource + from .resources.custom_metadata_fields import CustomMetadataFieldsResource, AsyncCustomMetadataFieldsResource __all__ = [ "Timeout", @@ -51,19 +57,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 +127,67 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.dummy = dummy.DummyResource(self) - self.custom_metadata_fields = custom_metadata_fields.CustomMetadataFieldsResource(self) - self.files = files.FilesResource(self) - self.assets = assets.AssetsResource(self) - self.cache = cache.CacheResource(self) - self.folders = folders.FoldersResource(self) - self.accounts = accounts.AccountsResource(self) - self.beta = beta.BetaResource(self) - self.webhooks = webhooks.WebhooksResource(self) - self.helper = helper.HelperResource(self) - self.with_raw_response = ImageKitWithRawResponse(self) - self.with_streaming_response = ImageKitWithStreamedResponse(self) + @cached_property + def dummy(self) -> DummyResource: + from .resources.dummy import DummyResource + + return DummyResource(self) + + @cached_property + def custom_metadata_fields(self) -> CustomMetadataFieldsResource: + from .resources.custom_metadata_fields import CustomMetadataFieldsResource + + return CustomMetadataFieldsResource(self) + + @cached_property + def files(self) -> FilesResource: + from .resources.files import FilesResource + + return FilesResource(self) + + @cached_property + def assets(self) -> AssetsResource: + from .resources.assets import AssetsResource + + return AssetsResource(self) + + @cached_property + def cache(self) -> CacheResource: + from .resources.cache import CacheResource + + return CacheResource(self) + + @cached_property + def folders(self) -> FoldersResource: + from .resources.folders import FoldersResource + + return FoldersResource(self) + + @cached_property + def accounts(self) -> AccountsResource: + from .resources.accounts import AccountsResource + + return AccountsResource(self) + + @cached_property + def beta(self) -> BetaResource: + from .resources.beta import BetaResource + + return BetaResource(self) + + @cached_property + def webhooks(self) -> WebhooksResource: + from .resources.webhooks import WebhooksResource + + return WebhooksResource(self) + + @cached_property + def with_raw_response(self) -> ImageKitWithRawResponse: + return ImageKitWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ImageKitWithStreamedResponse: + return ImageKitWithStreamedResponse(self) @property @override @@ -273,19 +315,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 +385,67 @@ def __init__( _strict_response_validation=_strict_response_validation, ) - self.dummy = dummy.AsyncDummyResource(self) - self.custom_metadata_fields = custom_metadata_fields.AsyncCustomMetadataFieldsResource(self) - self.files = files.AsyncFilesResource(self) - self.assets = assets.AsyncAssetsResource(self) - self.cache = cache.AsyncCacheResource(self) - self.folders = folders.AsyncFoldersResource(self) - self.accounts = accounts.AsyncAccountsResource(self) - self.beta = beta.AsyncBetaResource(self) - self.webhooks = webhooks.AsyncWebhooksResource(self) - self.helper = helper.AsyncHelperResource(self) - self.with_raw_response = AsyncImageKitWithRawResponse(self) - self.with_streaming_response = AsyncImageKitWithStreamedResponse(self) + @cached_property + def dummy(self) -> AsyncDummyResource: + from .resources.dummy import AsyncDummyResource + + return AsyncDummyResource(self) + + @cached_property + def custom_metadata_fields(self) -> AsyncCustomMetadataFieldsResource: + from .resources.custom_metadata_fields import AsyncCustomMetadataFieldsResource + + return AsyncCustomMetadataFieldsResource(self) + + @cached_property + def files(self) -> AsyncFilesResource: + from .resources.files import AsyncFilesResource + + return AsyncFilesResource(self) + + @cached_property + def assets(self) -> AsyncAssetsResource: + from .resources.assets import AsyncAssetsResource + + return AsyncAssetsResource(self) + + @cached_property + def cache(self) -> AsyncCacheResource: + from .resources.cache import AsyncCacheResource + + return AsyncCacheResource(self) + + @cached_property + def folders(self) -> AsyncFoldersResource: + from .resources.folders import AsyncFoldersResource + + return AsyncFoldersResource(self) + + @cached_property + def accounts(self) -> AsyncAccountsResource: + from .resources.accounts import AsyncAccountsResource + + return AsyncAccountsResource(self) + + @cached_property + def beta(self) -> AsyncBetaResource: + from .resources.beta import AsyncBetaResource + + return AsyncBetaResource(self) + + @cached_property + def webhooks(self) -> AsyncWebhooksResource: + from .resources.webhooks import AsyncWebhooksResource + + return AsyncWebhooksResource(self) + + @cached_property + def with_raw_response(self) -> AsyncImageKitWithRawResponse: + return AsyncImageKitWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncImageKitWithStreamedResponse: + return AsyncImageKitWithStreamedResponse(self) @property @override @@ -495,59 +573,223 @@ def _make_status_error( class ImageKitWithRawResponse: + _client: ImageKit + def __init__(self, client: ImageKit) -> None: - self.dummy = dummy.DummyResourceWithRawResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.CustomMetadataFieldsResourceWithRawResponse( - client.custom_metadata_fields - ) - self.files = files.FilesResourceWithRawResponse(client.files) - self.assets = assets.AssetsResourceWithRawResponse(client.assets) - self.cache = cache.CacheResourceWithRawResponse(client.cache) - self.folders = folders.FoldersResourceWithRawResponse(client.folders) - self.accounts = accounts.AccountsResourceWithRawResponse(client.accounts) - self.beta = beta.BetaResourceWithRawResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.DummyResourceWithRawResponse: + from .resources.dummy import DummyResourceWithRawResponse + + return DummyResourceWithRawResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.CustomMetadataFieldsResourceWithRawResponse: + from .resources.custom_metadata_fields import CustomMetadataFieldsResourceWithRawResponse + + return CustomMetadataFieldsResourceWithRawResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.FilesResourceWithRawResponse: + from .resources.files import FilesResourceWithRawResponse + + return FilesResourceWithRawResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AssetsResourceWithRawResponse: + from .resources.assets import AssetsResourceWithRawResponse + + return AssetsResourceWithRawResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.CacheResourceWithRawResponse: + from .resources.cache import CacheResourceWithRawResponse + + return CacheResourceWithRawResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.FoldersResourceWithRawResponse: + from .resources.folders import FoldersResourceWithRawResponse + + return FoldersResourceWithRawResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AccountsResourceWithRawResponse: + from .resources.accounts import AccountsResourceWithRawResponse + + return AccountsResourceWithRawResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.BetaResourceWithRawResponse: + from .resources.beta import BetaResourceWithRawResponse + + return BetaResourceWithRawResponse(self._client.beta) class AsyncImageKitWithRawResponse: + _client: AsyncImageKit + def __init__(self, client: AsyncImageKit) -> None: - self.dummy = dummy.AsyncDummyResourceWithRawResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithRawResponse( - client.custom_metadata_fields - ) - self.files = files.AsyncFilesResourceWithRawResponse(client.files) - self.assets = assets.AsyncAssetsResourceWithRawResponse(client.assets) - self.cache = cache.AsyncCacheResourceWithRawResponse(client.cache) - self.folders = folders.AsyncFoldersResourceWithRawResponse(client.folders) - self.accounts = accounts.AsyncAccountsResourceWithRawResponse(client.accounts) - self.beta = beta.AsyncBetaResourceWithRawResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.AsyncDummyResourceWithRawResponse: + from .resources.dummy import AsyncDummyResourceWithRawResponse + + return AsyncDummyResourceWithRawResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithRawResponse: + from .resources.custom_metadata_fields import AsyncCustomMetadataFieldsResourceWithRawResponse + + return AsyncCustomMetadataFieldsResourceWithRawResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithRawResponse: + from .resources.files import AsyncFilesResourceWithRawResponse + + return AsyncFilesResourceWithRawResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AsyncAssetsResourceWithRawResponse: + from .resources.assets import AsyncAssetsResourceWithRawResponse + + return AsyncAssetsResourceWithRawResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.AsyncCacheResourceWithRawResponse: + from .resources.cache import AsyncCacheResourceWithRawResponse + + return AsyncCacheResourceWithRawResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.AsyncFoldersResourceWithRawResponse: + from .resources.folders import AsyncFoldersResourceWithRawResponse + + return AsyncFoldersResourceWithRawResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AsyncAccountsResourceWithRawResponse: + from .resources.accounts import AsyncAccountsResourceWithRawResponse + + return AsyncAccountsResourceWithRawResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.AsyncBetaResourceWithRawResponse: + from .resources.beta import AsyncBetaResourceWithRawResponse + + return AsyncBetaResourceWithRawResponse(self._client.beta) class ImageKitWithStreamedResponse: + _client: ImageKit + def __init__(self, client: ImageKit) -> None: - self.dummy = dummy.DummyResourceWithStreamingResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.CustomMetadataFieldsResourceWithStreamingResponse( - client.custom_metadata_fields - ) - self.files = files.FilesResourceWithStreamingResponse(client.files) - self.assets = assets.AssetsResourceWithStreamingResponse(client.assets) - self.cache = cache.CacheResourceWithStreamingResponse(client.cache) - self.folders = folders.FoldersResourceWithStreamingResponse(client.folders) - self.accounts = accounts.AccountsResourceWithStreamingResponse(client.accounts) - self.beta = beta.BetaResourceWithStreamingResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.DummyResourceWithStreamingResponse: + from .resources.dummy import DummyResourceWithStreamingResponse + + return DummyResourceWithStreamingResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.CustomMetadataFieldsResourceWithStreamingResponse: + from .resources.custom_metadata_fields import CustomMetadataFieldsResourceWithStreamingResponse + + return CustomMetadataFieldsResourceWithStreamingResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.FilesResourceWithStreamingResponse: + from .resources.files import FilesResourceWithStreamingResponse + + return FilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AssetsResourceWithStreamingResponse: + from .resources.assets import AssetsResourceWithStreamingResponse + + return AssetsResourceWithStreamingResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.CacheResourceWithStreamingResponse: + from .resources.cache import CacheResourceWithStreamingResponse + + return CacheResourceWithStreamingResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.FoldersResourceWithStreamingResponse: + from .resources.folders import FoldersResourceWithStreamingResponse + + return FoldersResourceWithStreamingResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AccountsResourceWithStreamingResponse: + from .resources.accounts import AccountsResourceWithStreamingResponse + + return AccountsResourceWithStreamingResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.BetaResourceWithStreamingResponse: + from .resources.beta import BetaResourceWithStreamingResponse + + return BetaResourceWithStreamingResponse(self._client.beta) class AsyncImageKitWithStreamedResponse: + _client: AsyncImageKit + def __init__(self, client: AsyncImageKit) -> None: - self.dummy = dummy.AsyncDummyResourceWithStreamingResponse(client.dummy) - self.custom_metadata_fields = custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithStreamingResponse( - client.custom_metadata_fields - ) - self.files = files.AsyncFilesResourceWithStreamingResponse(client.files) - self.assets = assets.AsyncAssetsResourceWithStreamingResponse(client.assets) - self.cache = cache.AsyncCacheResourceWithStreamingResponse(client.cache) - self.folders = folders.AsyncFoldersResourceWithStreamingResponse(client.folders) - self.accounts = accounts.AsyncAccountsResourceWithStreamingResponse(client.accounts) - self.beta = beta.AsyncBetaResourceWithStreamingResponse(client.beta) + self._client = client + + @cached_property + def dummy(self) -> dummy.AsyncDummyResourceWithStreamingResponse: + from .resources.dummy import AsyncDummyResourceWithStreamingResponse + + return AsyncDummyResourceWithStreamingResponse(self._client.dummy) + + @cached_property + def custom_metadata_fields(self) -> custom_metadata_fields.AsyncCustomMetadataFieldsResourceWithStreamingResponse: + from .resources.custom_metadata_fields import AsyncCustomMetadataFieldsResourceWithStreamingResponse + + return AsyncCustomMetadataFieldsResourceWithStreamingResponse(self._client.custom_metadata_fields) + + @cached_property + def files(self) -> files.AsyncFilesResourceWithStreamingResponse: + from .resources.files import AsyncFilesResourceWithStreamingResponse + + return AsyncFilesResourceWithStreamingResponse(self._client.files) + + @cached_property + def assets(self) -> assets.AsyncAssetsResourceWithStreamingResponse: + from .resources.assets import AsyncAssetsResourceWithStreamingResponse + + return AsyncAssetsResourceWithStreamingResponse(self._client.assets) + + @cached_property + def cache(self) -> cache.AsyncCacheResourceWithStreamingResponse: + from .resources.cache import AsyncCacheResourceWithStreamingResponse + + return AsyncCacheResourceWithStreamingResponse(self._client.cache) + + @cached_property + def folders(self) -> folders.AsyncFoldersResourceWithStreamingResponse: + from .resources.folders import AsyncFoldersResourceWithStreamingResponse + + return AsyncFoldersResourceWithStreamingResponse(self._client.folders) + + @cached_property + def accounts(self) -> accounts.AsyncAccountsResourceWithStreamingResponse: + from .resources.accounts import AsyncAccountsResourceWithStreamingResponse + + return AsyncAccountsResourceWithStreamingResponse(self._client.accounts) + + @cached_property + def beta(self) -> beta.AsyncBetaResourceWithStreamingResponse: + from .resources.beta import AsyncBetaResourceWithStreamingResponse + + return AsyncBetaResourceWithStreamingResponse(self._client.beta) Client = ImageKit diff --git a/src/imagekitio/lib/__init__.py b/src/imagekitio/lib/__init__.py deleted file mode 100644 index 5ba9d0db..00000000 --- a/src/imagekitio/lib/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# Custom helper functions - not generated from OpenAPI spec - -from .helper import ( - HelperResource, - AsyncHelperResource, -) - -__all__ = [ - "HelperResource", - "AsyncHelperResource", -] diff --git a/src/imagekitio/lib/helper.py b/src/imagekitio/lib/helper.py deleted file mode 100644 index ed57436a..00000000 --- a/src/imagekitio/lib/helper.py +++ /dev/null @@ -1,808 +0,0 @@ -# File manually created for helper functions - not generated from OpenAPI spec - -from __future__ import annotations - -import re -import hmac -import time -import uuid -import base64 -import hashlib -from typing import Any, Dict, List, Union, Iterable, Optional, Sequence, cast -from urllib.parse import quote, parse_qs, urlparse, urlunparse -from typing_extensions import Unpack - -from .._resource import SyncAPIResource, AsyncAPIResource -from ..types.shared_params.overlay import Overlay -from ..types.shared_params.src_options import SrcOptions -from ..types.shared_params.transformation import Transformation -from ..types.shared_params.text_overlay_transformation import TextOverlayTransformation -from ..types.shared_params.subtitle_overlay_transformation import SubtitleOverlayTransformation -from ..types.shared_params.solid_color_overlay_transformation import SolidColorOverlayTransformation - -# Type alias for any transformation type (main or overlay-specific) -AnyTransformation = Union[ - Transformation, TextOverlayTransformation, SubtitleOverlayTransformation, SolidColorOverlayTransformation -] - -__all__ = ["HelperResource", "AsyncHelperResource"] - -# Constants -TRANSFORMATION_PARAMETER = "tr" -SIGNATURE_PARAMETER = "ik-s" -TIMESTAMP_PARAMETER = "ik-t" -DEFAULT_TIMESTAMP = 9999999999 -SIMPLE_OVERLAY_PATH_REGEX = re.compile(r"^[a-zA-Z0-9-._/ ]*$") -SIMPLE_OVERLAY_TEXT_REGEX = re.compile(r"^[a-zA-Z0-9-._ ]*$") - -# Transformation key mapping -SUPPORTED_TRANSFORMS = { - # Basic sizing & layout - "width": "w", - "height": "h", - "aspect_ratio": "ar", - "background": "bg", - "border": "b", - "crop": "c", - "crop_mode": "cm", - "dpr": "dpr", - "focus": "fo", - "quality": "q", - "x": "x", - "x_center": "xc", - "y": "y", - "y_center": "yc", - "format": "f", - "video_codec": "vc", - "audio_codec": "ac", - "radius": "r", - "rotation": "rt", - "blur": "bl", - "named": "n", - "default_image": "di", - "flip": "fl", - "original": "orig", - "start_offset": "so", - "end_offset": "eo", - "duration": "du", - "streaming_resolutions": "sr", - # AI & advanced effects - "grayscale": "e-grayscale", - "ai_upscale": "e-upscale", - "ai_retouch": "e-retouch", - "ai_variation": "e-genvar", - "ai_drop_shadow": "e-dropshadow", - "ai_change_background": "e-changebg", - "ai_remove_background": "e-bgremove", - "ai_remove_background_external": "e-removedotbg", - "ai_edit": "e-edit", - "contrast_stretch": "e-contrast", - "shadow": "e-shadow", - "sharpen": "e-sharpen", - "unsharp_mask": "e-usm", - "gradient": "e-gradient", - # Other flags & finishing - "progressive": "pr", - "lossless": "lo", - "color_profile": "cp", - "metadata": "md", - "opacity": "o", - "trim": "t", - "zoom": "z", - "page": "pg", - # Text overlay transformations - "font_size": "fs", - "font_family": "ff", - "font_color": "co", - "inner_alignment": "ia", - "padding": "pa", - "alpha": "al", - "typography": "tg", - "line_height": "lh", - # Subtitles transformations - "font_outline": "fol", - "font_shadow": "fsh", - "color": "co", - # Raw pass-through - "raw": "raw", -} - -CHAIN_TRANSFORM_DELIMITER = ":" -TRANSFORM_DELIMITER = "," -TRANSFORM_KEY_VALUE_DELIMITER = "-" - -# RFC 3986 section 3.3 defines 'pchar' (path characters) that are safe to use unencoded: -# pchar = unreserved / pct-encoded / sub-delims / ":" / "@" -# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" -# This matches what Node.js URL.pathname uses and ensures compatibility across SDKs -RFC3986_PATH_SAFE_CHARS = "/:@!$&'()*+,;=-._~" - - -def _get_transform_key(transform: str) -> str: - """Get the short transformation key from the long form.""" - if not transform: - return "" - return SUPPORTED_TRANSFORMS.get(transform, transform) - - -def _add_trailing_slash(s: str) -> str: - """Add trailing slash if not present.""" - if s and not s.endswith("/"): - return s + "/" - return s - - -def _remove_trailing_slash(s: str) -> str: - """Remove trailing slash if present.""" - if s and s.endswith("/"): - return s[:-1] - return s - - -def _remove_leading_slash(s: str) -> str: - """Remove leading slash if present.""" - if s and s.startswith("/"): - return s[1:] - return s - - -def _format_number(value: Any) -> str: - """ - Format a numeric value as a string, removing unnecessary decimal points. - - Examples: - 5.0 -> "5" - 5.5 -> "5.5" - 5 -> "5" - "5" -> "5" - """ - if isinstance(value, (int, float)): - # Check if it's a whole number - if isinstance(value, float) and value.is_integer(): - return str(int(value)) - return str(value) - return str(value) - - -def _path_join(parts: List[str], sep: str = "/") -> str: - """Join path parts, handling slashes correctly.""" - cleaned_parts: List[str] = [] - for part in parts: - if part: - # Remove leading and trailing slashes from parts - cleaned_part = part.strip("/") - if cleaned_part: - cleaned_parts.append(cleaned_part) - return sep + sep.join(cleaned_parts) if cleaned_parts else "" - - -def _safe_btoa(s: str) -> str: - """ - Base64 encode a string and then URL-encode it. - This matches Node.js behavior: safeBtoa() + encodeURIComponent(). - - In Node.js: - - encodeURIComponent() encodes: / as %2F, + as %2B, = as %3D - - Python's quote() with default safe='/' doesn't encode / - - So we need to explicitly set safe='' to encode everything - """ - encoded = base64.b64encode(s.encode("utf-8")).decode("utf-8") - # URL encode the entire base64 string (/, +, =, etc.) - # quote() with safe='' will encode all special characters to match encodeURIComponent - return quote(encoded, safe="") - - -def _process_input_path(s: str, encoding: str) -> str: - """ - Process input path for overlays. - Returns the full parameter string including the i- or ie- prefix. - """ - if not s: - return "" - - # Remove leading and trailing slashes - s = _remove_trailing_slash(_remove_leading_slash(s)) - - if encoding == "plain": - return f"i-{s.replace('/', '@@')}" - - if encoding == "base64": - # safeBtoa already encodes = as %3D, no need for further encoding - return f"ie-{_safe_btoa(s)}" - - # Auto encoding: use plain for simple paths, base64 for special characters - if SIMPLE_OVERLAY_PATH_REGEX.match(s): - return f"i-{s.replace('/', '@@')}" - else: - # safeBtoa already encodes = as %3D, no need for further encoding - return f"ie-{_safe_btoa(s)}" - - -def _process_text(s: str, encoding: str) -> str: - """ - Process text for overlays. - Returns the full parameter string including the i- or ie- prefix. - """ - if not s: - return "" - - if encoding == "plain": - return f"i-{quote(s, safe='')}" - - if encoding == "base64": - # safeBtoa already encodes = as %3D, no need for further encoding - return f"ie-{_safe_btoa(s)}" - - # Auto encoding: use plain for simple text, base64 for special characters - if SIMPLE_OVERLAY_TEXT_REGEX.match(s): - return f"i-{quote(s, safe='')}" - - # safeBtoa already encodes = as %3D, no need for further encoding - return f"ie-{_safe_btoa(s)}" - - -def _process_overlay(overlay: Overlay) -> str: - """Process overlay transformations.""" - if not overlay: - return "" - - # Extract type, position, timing, and transformation from overlay - overlay_type: str = cast(str, overlay.get("type", "")) - position: Dict[str, Any] = cast(Dict[str, Any], overlay.get("position", {})) - timing: Dict[str, Any] = cast(Dict[str, Any], overlay.get("timing", {})) - transformation: List[Any] = cast(List[Any], overlay.get("transformation", [])) - - if not overlay_type: - return "" - - parsed_overlay: List[str] = [] - - if overlay_type == "text": - text: str = cast(str, overlay.get("text", "")) - if not text: - return "" - - encoding: str = cast(str, overlay.get("encoding", "auto")) - parsed_overlay.append("l-text") - - # Process the text - returns full string with i- or ie- prefix - parsed_overlay.append(_process_text(text, encoding)) - - elif overlay_type == "image": - parsed_overlay.append("l-image") - - input_val: str = cast(str, overlay.get("input", "")) - if not input_val: - return "" - - img_encoding = cast(str, overlay.get("encoding", "auto")) - - # Process the input path - returns full string with i- or ie- prefix - parsed_overlay.append(_process_input_path(input_val, img_encoding)) - - elif overlay_type == "video": - parsed_overlay.append("l-video") - - video_input = cast(str, overlay.get("input", "")) - if not video_input: - return "" - - video_encoding = cast(str, overlay.get("encoding", "auto")) - - # Process the input path - returns full string with i- or ie- prefix - parsed_overlay.append(_process_input_path(video_input, video_encoding)) - - elif overlay_type == "subtitle": - parsed_overlay.append("l-subtitle") - - subtitle_input = cast(str, overlay.get("input", "")) - if not subtitle_input: - return "" - - subtitle_encoding = cast(str, overlay.get("encoding", "auto")) - - # Process the input path - returns full string with i- or ie- prefix - parsed_overlay.append(_process_input_path(subtitle_input, subtitle_encoding)) - - elif overlay_type == "solidColor": - parsed_overlay.append("l-image") - parsed_overlay.append("i-ik_canvas") - - color: str = cast(str, overlay.get("color", "")) - if not color: - return "" - - parsed_overlay.append(f"bg-{color}") - - # Handle position properties (x, y, focus) - # Node.js uses if (x) which skips falsy values like 0, '', false, null, undefined - x = position.get("x") - if x: - parsed_overlay.append(f"lx-{x}") - - y = position.get("y") - if y: - parsed_overlay.append(f"ly-{y}") - - focus = position.get("focus") - if focus: - parsed_overlay.append(f"lfo-{focus}") - - # Handle timing properties (start, end, duration) - # Node.js uses if (start) which skips falsy values - start = timing.get("start") - if start: - parsed_overlay.append(f"lso-{_format_number(start)}") - - end = timing.get("end") - if end: - parsed_overlay.append(f"leo-{_format_number(end)}") - - duration = timing.get("duration") - if duration: - parsed_overlay.append(f"ldu-{duration}") - - # Handle nested transformations for image/video overlays - if transformation: - transformation_string: str = _build_transformation_string(transformation) - if transformation_string and transformation_string.strip(): - parsed_overlay.append(transformation_string) - - # Close overlay - parsed_overlay.append("l-end") - - return TRANSFORM_DELIMITER.join(parsed_overlay) - - -def _build_transformation_string(transformation: Optional[Sequence[AnyTransformation]]) -> str: - """Build transformation string from transformation objects.""" - if not transformation: - return "" - - parsed_transforms: List[str] = [] - - for current_transform in transformation: - if not current_transform: - continue - - parsed_transform_step: List[str] = [] - - for key, value in current_transform.items(): - if value is None: - continue - - # Handle overlay separately - if key == "overlay" and isinstance(value, dict): - raw_string: str = _process_overlay(cast(Overlay, value)) - if raw_string and raw_string.strip(): - parsed_transform_step.append(raw_string) - continue - - # Get the transformation key - transform_key: str = _get_transform_key(key) - if not transform_key: - transform_key = key - - if not transform_key: - continue - - # Handle boolean transformations that should only output key - if transform_key in [ - "e-grayscale", - "e-contrast", - "e-removedotbg", - "e-bgremove", - "e-upscale", - "e-retouch", - "e-genvar", - ]: - if value is True or value == "-" or value == "true": - parsed_transform_step.append(transform_key) - # Any other value means that the effect should not be applied - continue - - # Handle transformations that can be true or have values - if transform_key in ["e-sharpen", "e-shadow", "e-gradient", "e-usm", "e-dropshadow"] and ( - str(value).strip() == "" or value is True or value == "true" - ): - parsed_transform_step.append(transform_key) - continue - - # Handle raw transformation - if key == "raw": - if isinstance(value, str) and value.strip(): - parsed_transform_step.append(value) - continue - - # Handle default_image and font_family - replace slashes - if transform_key in ["di", "ff"]: - value = _remove_trailing_slash(_remove_leading_slash(str(value) if value else "")) - value = value.replace("/", "@@") - - # Handle streaming_resolutions array - if transform_key == "sr" and isinstance(value, list): - value = "_".join(str(v) for v in cast(List[Any], value)) - - # Special case for trim with empty string - if transform_key == "t" and str(value).strip() == "": - value = "true" - - # Skip false values - if value is False: - continue - - # Skip empty strings (except for special keys that allow empty values) - if isinstance(value, str) and value.strip() == "": - continue - - # Convert boolean True to lowercase "true" - if value is True: - value = "true" - - # Format numeric values to avoid unnecessary .0 for integers - if isinstance(value, (int, float)): - value = _format_number(value) - - # Add the transformation - parsed_transform_step.append(f"{transform_key}{TRANSFORM_KEY_VALUE_DELIMITER}{value}") - - if parsed_transform_step: - parsed_transforms.append(TRANSFORM_DELIMITER.join(parsed_transform_step)) - - return CHAIN_TRANSFORM_DELIMITER.join(parsed_transforms) - - -def _get_signature_timestamp(seconds: Optional[float]) -> int: - """Calculate expiry timestamp for URL signing.""" - if not seconds or seconds <= 0: - return DEFAULT_TIMESTAMP - - # Try to parse as int, return DEFAULT_TIMESTAMP if invalid - try: - sec = int(seconds) - if sec <= 0: - return DEFAULT_TIMESTAMP - except (ValueError, TypeError): - return DEFAULT_TIMESTAMP - - return int(time.time()) + sec - - -def _get_signature(private_key: str, url: str, url_endpoint: str, expiry_timestamp: int) -> str: - """Generate HMAC-SHA1 signature for URL signing.""" - if not private_key or not url or not url_endpoint: - return "" - - # Create the string to sign: relative path + expiry timestamp - # This matches Node.js: url.replace(addTrailingSlash(urlEndpoint), '') + String(expiryTimestamp) - url_endpoint_with_slash = _add_trailing_slash(url_endpoint) - string_to_sign = url.replace(url_endpoint_with_slash, "") + str(expiry_timestamp) - - # Generate HMAC-SHA1 signature - signature = hmac.new(private_key.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha1).hexdigest() - - return signature - - -def _get_authentication_parameters(token: str, expire: int, private_key: str) -> Dict[str, Any]: - """Generate authentication parameters for uploads.""" - auth_parameters = { - "token": token, - "expire": expire, - "signature": "", - } - - signature = hmac.new(private_key.encode("utf-8"), f"{token}{expire}".encode("utf-8"), hashlib.sha1).hexdigest() - - auth_parameters["signature"] = signature - return auth_parameters - - -def _build_url( - src: str, - url_endpoint: str, - transformation_position: str, - transformation: Any, - query_parameters: Dict[str, Any], - signed: bool, - expires_in: Optional[float], - private_key: str, -) -> str: - """ - Internal implementation of build_url. - - Args: - src: Accepts a relative or absolute path of the resource. - url_endpoint: Get your urlEndpoint from the ImageKit dashboard. - transformation_position: By default, the transformation string is added as a query parameter. - transformation: An array of objects specifying the transformations to be applied in the URL. - query_parameters: Additional query parameters to add to the final URL. - signed: Whether to sign the URL or not. - expires_in: When you want the signed URL to expire, specified in seconds. - private_key: Private key for signing URLs. - - Returns: - The constructed source URL. - """ - if not src: - return "" - - # Check if src is absolute URL - is_absolute_url = src.startswith("https://") or src.startswith("https://") - - # Track if src parameter is used for URL (matches Node.js isSrcParameterUsedForURL) - is_src_parameter_used_for_url = False - - # Parse URL - try: - if not is_absolute_url: - parsed_url = urlparse(url_endpoint) - else: - parsed_url = urlparse(src) - is_src_parameter_used_for_url = True - except Exception: - return "" - - # Build query parameters - query_dict_raw = dict(parse_qs(parsed_url.query)) - # Flatten lists from parse_qs - query_dict: Dict[str, str] = {k: v[0] if len(v) == 1 else ",".join(v) for k, v in query_dict_raw.items()} - - # Add additional query parameters - convert values to strings like Node.js does - if query_parameters: - for k, v in query_parameters.items(): - query_dict[k] = str(v) - - # Build transformation string - transformation_string = _build_transformation_string(transformation) - - # Determine if transformation should be in query or path - # Matches Node.js: addAsQuery = transformationUtils.addAsQueryParameter(opts) || isSrcParameterUsedForURL - add_as_query = transformation_position == "query" or is_src_parameter_used_for_url - - # Placeholder for transformation to avoid URL encoding issues - TRANSFORMATION_PLACEHOLDER = "PLEASEREPLACEJUSTBEFORESIGN" - - # Build the path - if not is_absolute_url: - # For relative URLs - endpoint_path = urlparse(url_endpoint).path - path_parts = [endpoint_path] if endpoint_path else [] - - # Add transformation in path if needed - if transformation_string and not add_as_query: - path_parts.append(f"{TRANSFORMATION_PARAMETER}{CHAIN_TRANSFORM_DELIMITER}{TRANSFORMATION_PLACEHOLDER}") - - # Add src path with RFC 3986 compliant encoding - # Python's urlunparse() doesn't auto-encode Unicode like Node.js URL does, - # so we must manually encode the path while preserving RFC 3986 safe chars - encoded_src = quote(src, safe=RFC3986_PATH_SAFE_CHARS) - path_parts.append(encoded_src) - - path = _path_join(path_parts) - else: - path = parsed_url.path - - # Add transformation to query if needed - if transformation_string and add_as_query: - query_dict[TRANSFORMATION_PARAMETER] = TRANSFORMATION_PLACEHOLDER - - # Build the URL - scheme = parsed_url.scheme or "https" - netloc = parsed_url.netloc if is_absolute_url else urlparse(url_endpoint).netloc - - # Build query string manually to avoid encoding transformation string - query_string = "" - if query_dict: - query_parts: List[str] = [] - for k, v in query_dict.items(): - query_parts.append(f"{k}={v}") - query_string = "&".join(query_parts) - - final_url = urlunparse((scheme, netloc, path, "", query_string, "")) - - # Replace placeholder with actual transformation string - if transformation_string: - final_url = final_url.replace(TRANSFORMATION_PLACEHOLDER, transformation_string) - - # Sign URL if needed - if signed or (expires_in and expires_in > 0): - expiry_timestamp = _get_signature_timestamp(expires_in) - - url_signature = _get_signature( - private_key=private_key, url=final_url, url_endpoint=url_endpoint, expiry_timestamp=expiry_timestamp - ) - - # Add signature parameters - parsed_final = urlparse(final_url) - has_existing_params = bool(parsed_final.query) - separator = "&" if has_existing_params else "?" - - if expiry_timestamp and expiry_timestamp != DEFAULT_TIMESTAMP: - final_url += f"{separator}{TIMESTAMP_PARAMETER}={expiry_timestamp}" - final_url += f"&{SIGNATURE_PARAMETER}={url_signature}" - else: - final_url += f"{separator}{SIGNATURE_PARAMETER}={url_signature}" - - return final_url - - -def _get_authentication_parameters_with_defaults( - token: Optional[str], expire: Optional[int], private_key: str -) -> Dict[str, Any]: - """ - Internal implementation of get_authentication_parameters with default value handling. - - Args: - token: Custom token for the upload session. If not provided, a UUID v4 will be generated automatically. - expire: Expiration time in seconds from now. If not provided, defaults to 1800 seconds (30 minutes). - private_key: Private key for generating authentication parameters. - - Returns: - Authentication parameters object containing token, expire, and signature. - """ - if not private_key: - raise ValueError("Private key is required for generating authentication parameters") - - # Generate token if not provided - if not token: - token = str(uuid.uuid4()) - - # Set default expiry if not provided - if expire is None: - expire = int(time.time()) + 1800 # 30 minutes default - - return _get_authentication_parameters(token, expire, private_key) - - -class HelperResource(SyncAPIResource): - """ - Helper resource for additional utility functions like URL building and authentication. - """ - - def build_url(self, **options: Unpack[SrcOptions]) -> str: - """ - Builds a source URL with the given options. - - Args: - src: Accepts a relative or absolute path of the resource. If a relative path is provided, - it is appended to the `url_endpoint`. If an absolute path is provided, `url_endpoint` is ignored. - url_endpoint: Get your urlEndpoint from the ImageKit dashboard. - transformation: An array of objects specifying the transformations to be applied in the URL. - transformation_position: By default, the transformation string is added as a query parameter. - Set to `path` to add it in the URL path instead. - signed: Whether to sign the URL or not. Set to `true` to generate a signed URL. - expires_in: When you want the signed URL to expire, specified in seconds. - query_parameters: Additional query parameters to add to the final URL. - - Returns: - The constructed source URL. - """ - return _build_url( - src=options.get("src", ""), - url_endpoint=options.get("url_endpoint", ""), - transformation_position=options.get("transformation_position", "query"), - transformation=options.get("transformation"), - query_parameters=options.get("query_parameters", {}), - signed=options.get("signed", False), - expires_in=options.get("expires_in"), - private_key=self._client.private_key, - ) - - def get_authentication_parameters( - self, - token: Optional[str] = None, - expire: Optional[int] = None, - ) -> Dict[str, Any]: - """ - Generates authentication parameters for client-side file uploads using ImageKit's Upload API. - - Args: - token: Custom token for the upload session. If not provided, a UUID v4 will be generated automatically. - expire: Expiration time in seconds from now. If not provided, defaults to 1800 seconds (30 minutes). - - Returns: - Authentication parameters object containing: - - token: Unique identifier for this upload session - - expire: Unix timestamp when these parameters expire - - signature: HMAC-SHA1 signature for authenticating the upload - """ - return _get_authentication_parameters_with_defaults( - token=token, expire=expire, private_key=self._client.private_key - ) - - def build_transformation_string(self, transformation: Optional[Iterable[Transformation]] = None) -> str: - """ - Builds a transformation string from an array of transformation objects. - - Args: - transformation: List of transformation dictionaries. - - Returns: - The transformation string in ImageKit format. - """ - if transformation is None: - return "" - - # Convert to list if it's an iterable - if not isinstance(transformation, list): - transformation = list(transformation) - - return _build_transformation_string(transformation) - - -class AsyncHelperResource(AsyncAPIResource): - """ - Async version of helper resource for additional utility functions. - """ - - async def build_url(self, **options: Unpack[SrcOptions]) -> str: - """ - Async version of build_url. - - Args: - src: Accepts a relative or absolute path of the resource. If a relative path is provided, - it is appended to the `url_endpoint`. If an absolute path is provided, `url_endpoint` is ignored. - url_endpoint: Get your urlEndpoint from the ImageKit dashboard. - transformation: An array of objects specifying the transformations to be applied in the URL. - transformation_position: By default, the transformation string is added as a query parameter. - Set to `path` to add it in the URL path instead. - signed: Whether to sign the URL or not. Set to `true` to generate a signed URL. - expires_in: When you want the signed URL to expire, specified in seconds. - query_parameters: Additional query parameters to add to the final URL. - - Returns: - The constructed source URL. - """ - return _build_url( - src=options.get("src", ""), - url_endpoint=options.get("url_endpoint", ""), - transformation_position=options.get("transformation_position", "query"), - transformation=options.get("transformation"), - query_parameters=options.get("query_parameters", {}), - signed=options.get("signed", False), - expires_in=options.get("expires_in"), - private_key=self._client.private_key, - ) - - async def get_authentication_parameters( - self, - token: Optional[str] = None, - expire: Optional[int] = None, - ) -> Dict[str, Any]: - """ - Async version of get_authentication_parameters. - - Args: - token: Custom token for the upload session. If not provided, a UUID v4 will be generated automatically. - expire: Expiration time in seconds from now. If not provided, defaults to 1800 seconds (30 minutes). - - Returns: - Authentication parameters object containing: - - token: Unique identifier for this upload session - - expire: Unix timestamp when these parameters expire - - signature: HMAC-SHA1 signature for authenticating the upload - """ - return _get_authentication_parameters_with_defaults( - token=token, expire=expire, private_key=self._client.private_key - ) - - async def build_transformation_string(self, transformation: Optional[Iterable[Transformation]] = None) -> str: - """ - Async version of build_transformation_string. - - Args: - transformation: List of transformation dictionaries. - - Returns: - The transformation string in ImageKit format. - """ - if transformation is None: - return "" - - # Convert to list if it's an iterable - if not isinstance(transformation, list): - transformation = list(transformation) - - return _build_transformation_string(transformation) diff --git a/src/imagekitio/lib/serialization_utils.py b/src/imagekitio/lib/serialization_utils.py deleted file mode 100644 index 4fe5a473..00000000 --- a/src/imagekitio/lib/serialization_utils.py +++ /dev/null @@ -1,47 +0,0 @@ -# Serialization utilities for upload options -# This file handles serialization of upload parameters before sending to ImageKit API - -import json -from typing import Any, Dict, Sequence, cast - - -def serialize_upload_options(upload_options: Dict[str, Any]) -> Dict[str, Any]: - """ - Serialize upload options to handle proper formatting for ImageKit backend API. - - Special cases handled: - - tags: converted to comma-separated string - - response_fields: converted to comma-separated string - - extensions: JSON stringified - - custom_metadata: JSON stringified - - transformation: JSON stringified - - Args: - upload_options: Dictionary containing upload parameters - - Returns: - Dictionary with serialized values - """ - serialized: Dict[str, Any] = {**upload_options} - - for key in list(serialized.keys()): - if key and serialized[key] is not None: - value = serialized[key] - - if key == "tags" and isinstance(value, (list, tuple)): - # Tags should be comma-separated string - serialized[key] = ",".join(cast(Sequence[str], value)) - elif key == "response_fields" and isinstance(value, (list, tuple)): - # Response fields should be comma-separated string - serialized[key] = ",".join(cast(Sequence[str], value)) - elif key == "extensions" and isinstance(value, list): - # Extensions should be JSON stringified - serialized[key] = json.dumps(value) - elif key == "custom_metadata" and isinstance(value, dict): - # Custom metadata should be JSON stringified - serialized[key] = json.dumps(value) - elif key == "transformation" and isinstance(value, dict): - # Transformation should be JSON stringified - serialized[key] = json.dumps(value) - - return serialized diff --git a/src/imagekitio/resources/__init__.py b/src/imagekitio/resources/__init__.py index 81ba578e..e9d0986d 100644 --- a/src/imagekitio/resources/__init__.py +++ b/src/imagekitio/resources/__init__.py @@ -57,10 +57,6 @@ AsyncAccountsResourceWithStreamingResponse, ) from .webhooks import WebhooksResource, AsyncWebhooksResource -from ..lib.helper import ( - HelperResource, - AsyncHelperResource, -) from .custom_metadata_fields import ( CustomMetadataFieldsResource, AsyncCustomMetadataFieldsResource, @@ -121,6 +117,4 @@ "AsyncBetaResourceWithStreamingResponse", "WebhooksResource", "AsyncWebhooksResource", - "HelperResource", - "AsyncHelperResource", ] diff --git a/src/imagekitio/resources/beta/v2/files.py b/src/imagekitio/resources/beta/v2/files.py index 03b198fd..eb9d3ed6 100644 --- a/src/imagekitio/resources/beta/v2/files.py +++ b/src/imagekitio/resources/beta/v2/files.py @@ -29,7 +29,6 @@ ) from ...._base_client import make_request_options from ....types.beta.v2 import file_upload_params -from ....lib.serialization_utils import serialize_upload_options from ....types.shared_params.extensions import Extensions from ....types.beta.v2.file_upload_response import FileUploadResponse @@ -271,7 +270,6 @@ def upload( "webhook_url": webhook_url, } ) - body = serialize_upload_options(body) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -525,7 +523,6 @@ async def upload( "webhook_url": webhook_url, } ) - body = serialize_upload_options(body) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. diff --git a/src/imagekitio/resources/files/files.py b/src/imagekitio/resources/files/files.py index cd9ff3e7..d99ba84e 100644 --- a/src/imagekitio/resources/files/files.py +++ b/src/imagekitio/resources/files/files.py @@ -61,7 +61,6 @@ ) from ...types.file import File from ..._base_client import make_request_options -from ...lib.serialization_utils import serialize_upload_options from ...types.file_copy_response import FileCopyResponse from ...types.file_move_response import FileMoveResponse from ...types.file_rename_response import FileRenameResponse @@ -726,7 +725,6 @@ def upload( "webhook_url": webhook_url, } ) - body = serialize_upload_options(body) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. @@ -1399,7 +1397,6 @@ async def upload( "webhook_url": webhook_url, } ) - body = serialize_upload_options(body) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be # sent to the server will contain a `boundary` parameter, e.g. diff --git a/src/imagekitio/resources/webhooks.py b/src/imagekitio/resources/webhooks.py index 0ca75b5c..a561adee 100644 --- a/src/imagekitio/resources/webhooks.py +++ b/src/imagekitio/resources/webhooks.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import base64 from typing import Mapping, cast from .._models import construct_type @@ -41,13 +40,7 @@ def unwrap(self, payload: str, *, headers: Mapping[str, str], key: str | bytes | if not isinstance(headers, dict): headers = dict(headers) - if isinstance(key, str): - key_bytes = key.encode("utf-8") - else: - key_bytes = key - encoded_key = base64.b64encode(key_bytes).decode("ascii") - - Webhook(encoded_key).verify(payload, headers) + Webhook(key).verify(payload, headers) return cast( UnwrapWebhookEvent, @@ -84,13 +77,7 @@ def unwrap(self, payload: str, *, headers: Mapping[str, str], key: str | bytes | if not isinstance(headers, dict): headers = dict(headers) - if isinstance(key, str): - key_bytes = key.encode("utf-8") - else: - key_bytes = key - encoded_key = base64.b64encode(key_bytes).decode("ascii") - - Webhook(encoded_key).verify(payload, headers) + Webhook(key).verify(payload, headers) return cast( UnwrapWebhookEvent, diff --git a/tests/custom/__init__.py b/tests/custom/__init__.py deleted file mode 100644 index dad8a0a3..00000000 --- a/tests/custom/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Custom tests for manually created helper functions -# These tests are separate from auto-generated API tests diff --git a/tests/custom/test_helper_authentication.py b/tests/custom/test_helper_authentication.py deleted file mode 100644 index a0a08efa..00000000 --- a/tests/custom/test_helper_authentication.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Helper authentication tests - converted from Ruby SDK.""" - -import re - -import pytest - -from imagekitio import ImageKit, ImageKitError - - -class TestHelperAuthentication: - """Test helper authentication parameter generation.""" - - def test_should_return_correct_authentication_parameters_with_provided_token_and_expire(self) -> None: - """Should return correct authentication parameters with provided token and expire.""" - private_key = "private_key_test" - client = ImageKit(private_key=private_key) - - token = "your_token" - expire = 1582269249 - - params = client.helper.get_authentication_parameters(token=token, expire=expire) - - # Expected exact match with Node.js output - expected_signature = "e71bcd6031016b060d349d212e23e85c791decdd" - - assert params["token"] == token - assert params["expire"] == expire - assert params["signature"] == expected_signature - - def test_should_return_authentication_parameters_with_required_properties_when_no_params_provided(self) -> None: - """Should return authentication parameters with required properties when no params provided.""" - private_key = "private_key_test" - client = ImageKit(private_key=private_key) - - params = client.helper.get_authentication_parameters() - - # Check that all required properties exist - assert "token" in params, "Expected token parameter" - assert "expire" in params, "Expected expire parameter" - assert "signature" in params, "Expected signature parameter" - - # Token should be a UUID v4 format (36 characters with dashes) - token = params["token"] - assert isinstance(token, str) - assert re.match( - r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", token, re.IGNORECASE - ), "Expected token to be UUID v4 format" - - # Expire should be a number greater than current time - expire = params["expire"] - assert isinstance(expire, int) - import time - - current_time = int(time.time()) - assert expire > current_time, f"Expected expire {expire} to be greater than current time {current_time}" - - # Signature should be a hex string (40 characters for HMAC-SHA1) - signature = params["signature"] - assert isinstance(signature, str) - assert re.match(r"^[a-f0-9]{40}$", signature), "Expected signature to be 40 character hex string" - - def test_should_handle_edge_case_with_expire_time_0(self) -> None: - """Should handle edge case with expire time 0.""" - private_key = "private_key_test" - client = ImageKit(private_key=private_key) - - token = "test_token" - expire = 0 - - params = client.helper.get_authentication_parameters(token=token, expire=expire) - - assert params["token"] == token - assert params["expire"] == expire - assert "signature" in params - # Signature should still be generated even with expire = 0 - assert isinstance(params["signature"], str) - assert len(params["signature"]) == 40 - - def test_should_handle_empty_string_token(self) -> None: - """Should handle empty string token.""" - private_key = "private_key_test" - client = ImageKit(private_key=private_key) - - token = "" # Empty string is falsy - expire = 1582269249 - - params = client.helper.get_authentication_parameters(token=token, expire=expire) - - # Since empty string is falsy, it should generate a token - token_result = params["token"] - assert isinstance(token_result, str) - assert len(token_result) > 0, "Expected token to be generated when empty string is provided" - assert re.match( - r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", token_result, re.IGNORECASE - ), "Expected generated token to be UUID v4 format" - - assert params["expire"] == expire - - # Signature should be a hex string (40 characters for HMAC-SHA1) - signature = params["signature"] - assert isinstance(signature, str) - assert re.match(r"^[a-f0-9]{40}$", signature), "Expected signature to be 40 character hex string" - - def test_should_raise_error_when_private_key_is_not_provided(self) -> None: - """Should raise error when private key is empty.""" - with pytest.raises(ValueError, match="Private key is required"): - client = ImageKit(private_key="") - client.helper.get_authentication_parameters(token="test", expire=123) - - def test_should_raise_error_when_private_key_is_nil(self) -> None: - """Should raise error when private key is None.""" - with pytest.raises(ImageKitError, match="private_key client option must be set"): - client = ImageKit(private_key=None) # type: ignore - client.helper.get_authentication_parameters(token="test", expire=123) diff --git a/tests/custom/test_serialization_utils.py b/tests/custom/test_serialization_utils.py deleted file mode 100644 index b523aeb4..00000000 --- a/tests/custom/test_serialization_utils.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Unit tests for serialization_utils module.""" - -import json -from typing import Any, Dict, List - -from imagekitio.lib.serialization_utils import serialize_upload_options - - -class TestSerializeUploadOptions: - """Test cases for serialize_upload_options function.""" - - def test_should_convert_tags_array_to_comma_separated_string(self): - """Test that tags array is converted to comma-separated string.""" - body = {"tags": ["tag1", "tag2", "tag3"]} - result = serialize_upload_options(body) - assert result["tags"] == "tag1,tag2,tag3" - - def test_should_convert_tags_tuple_to_comma_separated_string(self): - """Test that tags tuple is converted to comma-separated string.""" - body = {"tags": ("tag1", "tag2", "tag3")} - result = serialize_upload_options(body) - assert result["tags"] == "tag1,tag2,tag3" - - def test_should_convert_response_fields_array_to_comma_separated_string(self): - """Test that response_fields array is converted to comma-separated string.""" - body = {"response_fields": ["tags", "customCoordinates", "metadata"]} - result = serialize_upload_options(body) - assert result["response_fields"] == "tags,customCoordinates,metadata" - - def test_should_convert_response_fields_tuple_to_comma_separated_string(self): - """Test that response_fields tuple is converted to comma-separated string.""" - body = {"response_fields": ("tags", "customCoordinates")} - result = serialize_upload_options(body) - assert result["response_fields"] == "tags,customCoordinates" - - def test_should_json_stringify_extensions_array(self): - """Test that extensions array is JSON stringified.""" - body = {"extensions": [{"name": "remove-bg"}, {"name": "google-auto-tagging", "minConfidence": 80}]} - result = serialize_upload_options(body) - expected = json.dumps(body["extensions"]) - assert result["extensions"] == expected - # Verify it's valid JSON - assert json.loads(result["extensions"]) == body["extensions"] - - def test_should_json_stringify_custom_metadata_object(self): - """Test that custom_metadata object is JSON stringified.""" - body = {"custom_metadata": {"key1": "value1", "key2": 123, "key3": True}} - result = serialize_upload_options(body) - expected = json.dumps(body["custom_metadata"]) - assert result["custom_metadata"] == expected - # Verify it's valid JSON - assert json.loads(result["custom_metadata"]) == body["custom_metadata"] - - def test_should_json_stringify_transformation_object(self): - """Test that transformation object is JSON stringified.""" - body = { - "transformation": { - "pre": "l-image,i-logo.png,w-100,h-100", - "post": [{"type": "thumbnail", "value": "h-300"}], - } - } - result = serialize_upload_options(body) - expected = json.dumps(body["transformation"]) - assert result["transformation"] == expected - # Verify it's valid JSON - assert json.loads(result["transformation"]) == body["transformation"] - - def test_should_handle_all_serializable_fields_together(self): - """Test that all serializable fields are processed correctly together.""" - body = { - "file": "test.jpg", - "file_name": "test.jpg", - "tags": ["tag1", "tag2"], - "response_fields": ["tags", "metadata"], - "extensions": [{"name": "remove-bg"}], - "custom_metadata": {"key": "value"}, - "transformation": {"pre": "w-100"}, - "folder": "/images", - } - result = serialize_upload_options(body) - - assert result["tags"] == "tag1,tag2" - assert result["response_fields"] == "tags,metadata" - assert result["extensions"] == json.dumps([{"name": "remove-bg"}]) - assert result["custom_metadata"] == json.dumps({"key": "value"}) - assert result["transformation"] == json.dumps({"pre": "w-100"}) - # Non-serializable fields should remain unchanged - assert result["file"] == "test.jpg" - assert result["file_name"] == "test.jpg" - assert result["folder"] == "/images" - - def test_should_not_modify_original_body(self): - """Test that the original body is not modified.""" - body = { - "tags": ["tag1", "tag2"], - "response_fields": ["tags"], - "extensions": [{"name": "ext1"}], - } - original_tags = body["tags"].copy() - original_response_fields = body["response_fields"].copy() - original_extensions = body["extensions"].copy() - - serialize_upload_options(body) - - # Original should remain unchanged - assert body["tags"] == original_tags - assert body["response_fields"] == original_response_fields - assert body["extensions"] == original_extensions - - def test_should_handle_empty_arrays(self): - """Test that empty arrays are converted to empty strings.""" - body: Dict[str, List[str]] = {"tags": [], "response_fields": []} - result = serialize_upload_options(body) - assert result["tags"] == "" - assert result["response_fields"] == "" - - def test_should_handle_empty_extensions_array(self): - """Test that empty extensions array is JSON stringified.""" - body: Dict[str, List[Any]] = {"extensions": []} - result = serialize_upload_options(body) - assert result["extensions"] == "[]" - - def test_should_handle_none_values(self): - """Test that None values are not processed.""" - body = { - "tags": None, - "response_fields": None, - "extensions": None, - "custom_metadata": None, - "transformation": None, - } - result = serialize_upload_options(body) - # None values should remain None - assert result["tags"] is None - assert result["response_fields"] is None - assert result["extensions"] is None - assert result["custom_metadata"] is None - assert result["transformation"] is None - - def test_should_handle_empty_object(self): - """Test that an empty object is returned as is.""" - body: Dict[str, Any] = {} - result = serialize_upload_options(body) - assert result == {} - - def test_should_skip_non_matching_fields(self): - """Test that fields not in the serialization list are left unchanged.""" - body = { - "file_name": "test.jpg", - "folder": "/images", - "is_private_file": True, - "use_unique_file_name": False, - } - result = serialize_upload_options(body) - assert result == body - - def test_should_handle_single_tag(self): - """Test that a single tag array is handled correctly.""" - body = {"tags": ["single-tag"]} - result = serialize_upload_options(body) - assert result["tags"] == "single-tag" - - def test_should_handle_tags_with_empty_strings(self): - """Test that tags with empty strings are still joined.""" - body = {"tags": ["tag1", "", "tag2"]} - result = serialize_upload_options(body) - assert result["tags"] == "tag1,,tag2" - - def test_should_handle_complex_nested_extensions(self): - """Test that complex nested extensions are properly JSON stringified.""" - body = { - "extensions": [ - { - "name": "aws-auto-tagging", - "options": {"maxTags": 10, "minConfidence": 75}, - }, - { - "name": "remove-bg", - "options": {"add_shadow": True, "bg_color": "white"}, - }, - ] - } - result = serialize_upload_options(body) - expected = json.dumps(body["extensions"]) - assert result["extensions"] == expected - assert json.loads(result["extensions"]) == body["extensions"] - - def test_should_handle_nested_custom_metadata(self): - """Test that nested custom metadata is properly JSON stringified.""" - body = { - "custom_metadata": { - "product": {"name": "Test Product", "price": 99.99, "inStock": True}, - "category": "electronics", - } - } - result = serialize_upload_options(body) - expected = json.dumps(body["custom_metadata"]) - assert result["custom_metadata"] == expected - assert json.loads(result["custom_metadata"]) == body["custom_metadata"] - - def test_should_handle_transformation_with_both_pre_and_post(self): - """Test that transformation with both pre and post is properly handled.""" - body = { - "transformation": { - "pre": "w-200,h-200", - "post": [{"type": "transformation", "value": "w-100,h-100"}], - } - } - result = serialize_upload_options(body) - expected = json.dumps(body["transformation"]) - assert result["transformation"] == expected - assert json.loads(result["transformation"]) == body["transformation"] - - def test_should_not_modify_non_dict_custom_metadata(self): - """Test that custom_metadata is only serialized when it's a dict.""" - # This shouldn't happen in practice but testing edge case - body = {"custom_metadata": "string_value"} - result = serialize_upload_options(body) - # String value should remain unchanged - assert result["custom_metadata"] == "string_value" - - def test_should_not_modify_non_list_extensions(self): - """Test that extensions is only serialized when it's a list.""" - # This shouldn't happen in practice but testing edge case - body = {"extensions": "string_value"} - result = serialize_upload_options(body) - # String value should remain unchanged - assert result["extensions"] == "string_value" diff --git a/tests/custom/url_generation/__init__.py b/tests/custom/url_generation/__init__.py deleted file mode 100644 index e0c071a8..00000000 --- a/tests/custom/url_generation/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# URL generation test module diff --git a/tests/custom/url_generation/test_advanced_url_generation.py b/tests/custom/url_generation/test_advanced_url_generation.py deleted file mode 100644 index 6111f07d..00000000 --- a/tests/custom/url_generation/test_advanced_url_generation.py +++ /dev/null @@ -1,281 +0,0 @@ -"""Advanced URL generation tests imported from Ruby SDK.""" - -import pytest - -from imagekitio import ImageKit - - -class TestAdvancedURLGeneration: - """Test advanced URL generation matching Ruby SDK advanced_url_generation_test.rb.""" - - @pytest.fixture(autouse=True) - def setup(self): - """Setup client for each test.""" - self.client = ImageKit(private_key="My Private API Key") - - # AI Transformation Tests - def test_should_generate_the_correct_url_for_ai_background_removal_when_set_to_true(self): - """Test AI background removal transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"ai_remove_background": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-bgremove" - assert url == expected - - def test_should_generate_the_correct_url_for_external_ai_background_removal_when_set_to_true(self): - """Test external AI background removal transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"ai_remove_background_external": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-removedotbg" - assert url == expected - - def test_should_generate_the_correct_url_when_ai_drop_shadow_transformation_is_set_to_true(self): - """Test AI drop shadow transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"ai_drop_shadow": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-dropshadow" - assert url == expected - - def test_should_generate_the_correct_url_when_gradient_transformation_is_set_to_true(self): - """Test gradient transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"gradient": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-gradient" - assert url == expected - - def test_should_not_apply_ai_background_removal_when_value_is_not_true(self): - """Test that AI background removal is not applied when not true.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg" - assert url == expected - - def test_should_not_apply_external_ai_background_removal_when_value_is_not_true(self): - """Test that external AI background removal is not applied when not true.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg" - assert url == expected - - def test_should_handle_ai_transformations_with_parameters(self): - """Test AI transformations with custom parameters.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"ai_drop_shadow": "custom-shadow-params"}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-dropshadow-custom-shadow-params" - assert url == expected - - def test_should_handle_gradient_with_parameters(self): - """Test gradient with custom parameters.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"gradient": "ld-top_from-green_to-00FF0010_sp-1"}], - ) - expected = ( - "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-gradient-ld-top_from-green_to-00FF0010_sp-1" - ) - assert url == expected - - def test_should_combine_ai_transformations_with_regular_transformations(self): - """Test combining AI and regular transformations.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"width": 300, "height": 200, "ai_remove_background": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=w-300,h-200,e-bgremove" - assert url == expected - - def test_should_handle_multiple_ai_transformations(self): - """Test multiple AI transformations.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"ai_remove_background": True, "ai_drop_shadow": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=e-bgremove,e-dropshadow" - assert url == expected - - # Parameter-specific tests - def test_should_generate_the_correct_url_for_width_transformation_when_provided_with_a_number_value(self): - """Test width transformation with number value.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"width": 400}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=w-400" - assert url == expected - - def test_should_generate_the_correct_url_for_height_transformation_when_provided_with_a_string_value(self): - """Test height transformation with string value.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": "300"}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=h-300" - assert url == expected - - def test_should_generate_the_correct_url_for_aspect_ratio_transformation_when_provided_with_colon_format(self): - """Test aspect ratio transformation with colon format.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"aspect_ratio": "4:3"}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=ar-4:3" - assert url == expected - - def test_should_generate_the_correct_url_for_quality_transformation_when_provided_with_a_number_value(self): - """Test quality transformation with number value.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"quality": 80}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=q-80" - assert url == expected - - # Additional parameter validation tests - def test_should_skip_transformation_parameters_that_are_undefined_or_empty(self): - """Test that undefined/empty parameters are skipped.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"width": 300}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=w-300" - assert url == expected - - def test_should_handle_boolean_transformation_values(self): - """Test boolean transformation values.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"trim": True}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=t-true" - assert url == expected - - def test_should_handle_transformation_parameter_with_empty_string_value(self): - """Test transformation with empty string value.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"default_image": ""}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg" - assert url == expected - - def test_should_handle_complex_transformation_combinations(self): - """Test complex transformation combinations.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path1.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"width": 300, "height": 200, "quality": 85, "border": "5_FF0000"}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path1.jpg?tr=w-300,h-200,q-85,b-5_FF0000" - assert url == expected - - def test_should_generate_the_correct_url_with_many_transformations_including_video_and_ai_transforms(self): - """Test many transformations including video and AI.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[ - { - "height": 300, - "width": 400, - "aspect_ratio": "4-3", - "quality": 40, - "crop": "force", - "crop_mode": "extract", - "focus": "left", - "format": "jpeg", - "radius": 50, - "background": "A94D34", - "border": "5-A94D34", - "rotation": 90, - "blur": 10, - "named": "some_name", - "progressive": True, - "lossless": True, - "trim": 5, - "metadata": True, - "color_profile": True, - "default_image": "/folder/file.jpg/", - "dpr": 3, - "x": 10, - "y": 20, - "x_center": 30, - "y_center": 40, - "flip": "h", - "opacity": 0.8, - "zoom": 2, - "video_codec": "h264", - "audio_codec": "aac", - "start_offset": 5, - "end_offset": 15, - "duration": 10, - "streaming_resolutions": ["1440", "1080"], - "grayscale": True, - "ai_upscale": True, - "ai_retouch": True, - "ai_variation": True, - "ai_drop_shadow": True, - "ai_change_background": "prompt-car", - "ai_edit": "prompt-make it vintage", - "ai_remove_background": True, - "contrast_stretch": True, - "shadow": "bl-15_st-40_x-10_y-N5", - "sharpen": 10, - "unsharp_mask": "2-2-0.8-0.024", - "gradient": "from-red_to-white", - "original": True, - "page": "2_4", - "raw": "h-200,w-300,l-image,i-logo.png,l-end", - } - ], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400,ar-4-3,q-40,c-force,cm-extract,fo-left,f-jpeg,r-50,bg-A94D34,b-5-A94D34,rt-90,bl-10,n-some_name,pr-true,lo-true,t-5,md-true,cp-true,di-folder@@file.jpg,dpr-3,x-10,y-20,xc-30,yc-40,fl-h,o-0.8,z-2,vc-h264,ac-aac,so-5,eo-15,du-10,sr-1440_1080,e-grayscale,e-upscale,e-retouch,e-genvar,e-dropshadow,e-changebg-prompt-car,e-edit-prompt-make it vintage,e-bgremove,e-contrast,e-shadow-bl-15_st-40_x-10_y-N5,e-sharpen-10,e-usm-2-2-0.8-0.024,e-gradient-from-red_to-white,orig-true,pg-2_4,h-200,w-300,l-image,i-logo.png,l-end" - assert url == expected diff --git a/tests/custom/url_generation/test_basic_url_generation.py b/tests/custom/url_generation/test_basic_url_generation.py deleted file mode 100644 index d91614b1..00000000 --- a/tests/custom/url_generation/test_basic_url_generation.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Basic URL generation tests - converted from Ruby SDK.""" - -from typing import TYPE_CHECKING - -import pytest - -from imagekitio import ImageKit - -if TYPE_CHECKING: - from imagekitio._client import ImageKit as ImageKitType - - -class TestBasicURLGeneration: - """Test basic URL generation functionality.""" - - client: "ImageKitType" - - @pytest.fixture(autouse=True) - def setup(self) -> None: - """Set up test client.""" - self.client = ImageKit(private_key="My Private API Key") - - def test_should_return_an_empty_string_when_src_is_not_provided(self) -> None: - """Should return an empty string when src is not provided.""" - url = self.client.helper.build_url( - src="", url_endpoint="https://ik.imagekit.io/test_url_endpoint", transformation_position="query" - ) - - assert url == "" - - def test_should_generate_a_valid_url_when_src_is_slash(self) -> None: - """Should generate a valid URL when src is slash.""" - url = self.client.helper.build_url( - src="/https/github.com/", url_endpoint="https://ik.imagekit.io/test_url_endpoint", transformation_position="query" - ) - - expected = "https://ik.imagekit.io/test_url_endpoint" - assert url == expected - - def test_should_generate_a_valid_url_when_src_is_provided_with_transformation(self) -> None: - """Should generate a valid URL when src is provided with transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg" - assert url == expected - - def test_should_generate_a_valid_url_when_a_src_is_provided_without_transformation(self) -> None: - """Should generate a valid URL when a src is provided without transformation.""" - url = self.client.helper.build_url( - src="https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg" - assert url == expected - - def test_should_generate_a_valid_url_when_undefined_transformation_parameters_are_provided_with_path(self) -> None: - """Should generate a valid URL when undefined transformation parameters are provided with path.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path_alt.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg" - assert url == expected - - def test_by_default_transformation_position_should_be_query(self) -> None: - """By default transformation position should be query.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation=[{"height": 300, "width": 400}, {"rotation": 90}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400:rt-90" - assert url == expected - - def test_should_generate_the_url_without_sdk_version(self) -> None: - """Should generate the URL without SDK version.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation=[{"height": 300, "width": 400}], - transformation_position="path", - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/tr:h-300,w-400/test_path.jpg" - assert url == expected - - def test_should_generate_the_correct_url_with_a_valid_src_and_transformation(self) -> None: - """Should generate the correct URL with a valid src and transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400" - assert url == expected - - def test_should_add_transformation_as_query_when_src_has_absolute_url_even_if_transformation_position_is_path( - self, - ) -> None: - """Should add transformation as query when src has absolute URL even if transformation position is path.""" - url = self.client.helper.build_url( - src="https://my.custom.domain.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://my.custom.domain.com/test_path.jpg?tr=h-300,w-400" - assert url == expected - - def test_should_generate_correct_url_when_src_has_query_params(self) -> None: - """Should generate correct URL when src has query params.""" - url = self.client.helper.build_url( - src="https://ik.imagekit.io/imagekit_id/new-endpoint/test_path.jpg?t1=v1", - url_endpoint="https://ik.imagekit.io/imagekit_id/new-endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/imagekit_id/new-endpoint/test_path.jpg?t1=v1&tr=h-300,w-400" - assert url == expected - - def test_should_generate_the_correct_url_when_the_provided_path_contains_multiple_leading_slashes(self) -> None: - """Should generate the correct URL when the provided path contains multiple leading slashes.""" - url = self.client.helper.build_url( - src="///test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400" - assert url == expected - - def test_should_generate_the_correct_url_when_the_url_endpoint_is_overridden(self) -> None: - """Should generate the correct URL when the URL endpoint is overridden.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint_alt", - transformation_position="query", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint_alt/test_path.jpg?tr=h-300,w-400" - assert url == expected - - def test_should_generate_the_correct_url_with_transformation_position_as_query_parameter_when_src_is_provided( - self, - ) -> None: - """Should generate the correct URL with transformation position as query parameter when src is provided.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400" - assert url == expected - - def test_should_generate_the_correct_url_with_a_valid_src_parameter_and_transformation(self) -> None: - """Should generate the correct URL with a valid src parameter and transformation.""" - url = self.client.helper.build_url( - src="https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg?tr=h-300,w-400" - assert url == expected - - def test_should_merge_query_parameters_correctly_in_the_generated_url(self) -> None: - """Should merge query parameters correctly in the generated URL.""" - url = self.client.helper.build_url( - src="https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg?t1=v1", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - query_parameters={"t2": "v2", "t3": "v3"}, - transformation=[{"height": 300, "width": 400}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path_alt.jpg?t1=v1&t2=v2&t3=v3&tr=h-300,w-400" - assert url == expected - - def test_should_generate_the_correct_url_with_chained_transformations(self) -> None: - """Should generate the correct URL with chained transformations.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}, {"rotation": 90}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400:rt-90" - assert url == expected - - def test_should_generate_the_correct_url_with_chained_transformations_including_raw_transformation(self) -> None: - """Should generate the correct URL with chained transformations including raw transformation.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400}, {"raw": "rndm_trnsf-abcd"}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400:rndm_trnsf-abcd" - assert url == expected - - def test_should_generate_the_correct_url_when_border_transformation_is_applied(self) -> None: - """Should generate the correct URL when border transformation is applied.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"height": 300, "width": 400, "border": "20_FF0000"}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg?tr=h-300,w-400,b-20_FF0000" - assert url == expected - - def test_should_generate_the_correct_url_when_transformation_has_empty_key_and_value(self) -> None: - """Should generate the correct URL when transformation has empty key and value.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="query", - transformation=[{"raw": ""}], - ) - - expected = "https://ik.imagekit.io/test_url_endpoint/test_path.jpg" - assert url == expected - - def test_should_generate_a_valid_url_when_cname_is_used(self) -> None: - """Should generate a valid URL when CNAME is used.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", url_endpoint="https://custom.domain.com", transformation_position="query" - ) - - expected = "https://custom.domain.com/test_path.jpg" - assert url == expected - - def test_should_generate_a_valid_url_when_cname_with_path_is_used(self) -> None: - """Should generate a valid URL when CNAME with path is used.""" - url = self.client.helper.build_url( - src="/https/github.com/test_path.jpg", url_endpoint="https://custom.domain.com/url-pattern", transformation_position="query" - ) - - expected = "https://custom.domain.com/url-pattern/test_path.jpg" - assert url == expected diff --git a/tests/custom/url_generation/test_build_transformation_string.py b/tests/custom/url_generation/test_build_transformation_string.py deleted file mode 100644 index 447bd94c..00000000 --- a/tests/custom/url_generation/test_build_transformation_string.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Build transformation string tests imported from Ruby SDK.""" - -import pytest - -from imagekitio import ImageKit - - -class TestBuildTransformationString: - """Test build_transformation_string matching Ruby SDK build_transformation_string_test.rb.""" - - @pytest.fixture(autouse=True) - def setup(self): - """Setup client for each test.""" - self.client = ImageKit(private_key="test-key") - - def test_should_return_empty_string_for_empty_transformation_array(self): - """Test empty transformation array returns empty string.""" - result = self.client.helper.build_transformation_string(None) - assert result == "" - - result = self.client.helper.build_transformation_string([]) - assert result == "" - - def test_should_generate_transformation_string_for_width_only(self): - """Test transformation string for width only.""" - result = self.client.helper.build_transformation_string([{"width": 300}]) - expected = "w-300" - assert result == expected - - def test_should_generate_transformation_string_for_multiple_parameters(self): - """Test transformation string for multiple parameters.""" - result = self.client.helper.build_transformation_string([{"width": 300, "height": 200}]) - expected = "w-300,h-200" - assert result == expected - - def test_should_generate_transformation_string_for_chained_transformations(self): - """Test transformation string for chained transformations.""" - result = self.client.helper.build_transformation_string([{"width": 300}, {"height": 200}]) - expected = "w-300:h-200" - assert result == expected - - def test_should_handle_empty_transformation_object(self): - """Test empty transformation object.""" - result = self.client.helper.build_transformation_string([{}]) - expected = "" - assert result == expected - - def test_should_handle_transformation_with_overlay(self): - """Test transformation with overlay.""" - result = self.client.helper.build_transformation_string([{"overlay": {"type": "text", "text": "Hello"}}]) - expected = "l-text,i-Hello,l-end" - assert result == expected - - def test_should_handle_raw_transformation_parameter(self): - """Test raw transformation parameter.""" - result = self.client.helper.build_transformation_string([{"raw": "custom-transform-123"}]) - expected = "custom-transform-123" - assert result == expected - - def test_should_handle_mixed_parameters_with_raw(self): - """Test mixed parameters with raw.""" - result = self.client.helper.build_transformation_string([{"width": 300, "raw": "custom-param-123"}]) - expected = "w-300,custom-param-123" - assert result == expected - - def test_should_handle_quality_parameter(self): - """Test quality parameter.""" - result = self.client.helper.build_transformation_string([{"quality": 80}]) - expected = "q-80" - assert result == expected - - def test_should_handle_aspect_ratio_parameter(self): - """Test aspect ratio parameter.""" - result = self.client.helper.build_transformation_string([{"aspect_ratio": "4:3"}]) - expected = "ar-4:3" - assert result == expected diff --git a/tests/custom/url_generation/test_overlay.py b/tests/custom/url_generation/test_overlay.py deleted file mode 100644 index 68d6200c..00000000 --- a/tests/custom/url_generation/test_overlay.py +++ /dev/null @@ -1,405 +0,0 @@ -"""Overlay transformation tests imported from Ruby SDK.""" - -import pytest - -from imagekitio import ImageKit - - -class TestOverlay: - """Test overlay functionality matching Ruby SDK overlay_test.rb.""" - - @pytest.fixture(autouse=True) - def setup(self): - """Setup client for each test.""" - self.client = ImageKit(private_key="My Private API Key") - - # Basic overlay tests - def test_should_ignore_overlay_when_type_property_is_missing(self): - """Test that overlay is ignored when type is missing.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"width": 300}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:w-300/base-image.jpg" - assert url == expected - - def test_should_ignore_text_overlay_when_text_property_is_missing(self): - """Test that text overlay is ignored when text is empty.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": ""}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/base-image.jpg" - assert url == expected - - def test_should_ignore_image_overlay_when_input_property_is_missing(self): - """Test that image overlay is ignored when input is empty.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "image", "input": ""}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/base-image.jpg" - assert url == expected - - def test_should_ignore_video_overlay_when_input_property_is_missing(self): - """Test that video overlay is ignored when input is empty.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "video", "input": ""}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/base-image.jpg" - assert url == expected - - def test_should_ignore_subtitle_overlay_when_input_property_is_missing(self): - """Test that subtitle overlay is ignored when input is empty.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "subtitle", "input": ""}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/base-image.jpg" - assert url == expected - - def test_should_ignore_solid_color_overlay_when_color_property_is_missing(self): - """Test that solid color overlay is ignored when color is empty.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "solidColor", "color": ""}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/base-image.jpg" - assert url == expected - - # Basic overlay functionality tests - def test_should_generate_url_with_text_overlay_using_url_encoding(self): - """Test text overlay with URL encoding.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": "Minimal Text"}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:l-text,i-Minimal%20Text,l-end/base-image.jpg" - assert url == expected - - def test_should_generate_url_with_image_overlay_from_input_file(self): - """Test image overlay from input file.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "image", "input": "logo.png"}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:l-image,i-logo.png,l-end/base-image.jpg" - assert url == expected - - def test_should_generate_url_with_video_overlay_from_input_file(self): - """Test video overlay from input file.""" - url = self.client.helper.build_url( - src="/https/github.com/base-video.mp4", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "video", "input": "play-pause-loop.mp4"}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:l-video,i-play-pause-loop.mp4,l-end/base-video.mp4" - assert url == expected - - def test_should_generate_url_with_subtitle_overlay_from_input_file(self): - """Test subtitle overlay from input file.""" - url = self.client.helper.build_url( - src="/https/github.com/base-video.mp4", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "subtitle", "input": "subtitle.srt"}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:l-subtitle,i-subtitle.srt,l-end/base-video.mp4" - assert url == expected - - def test_should_generate_url_with_solid_color_overlay_using_background_color(self): - """Test solid color overlay.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[{"overlay": {"type": "solidColor", "color": "FF0000"}}], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:l-image,i-ik_canvas,bg-FF0000,l-end/base-image.jpg" - assert url == expected - - def test_should_generate_url_with_multiple_complex_overlays_including_nested_transformations(self): - """Test complex overlays with nested transformations.""" - url = self.client.helper.build_url( - src="/https/github.com/base-image.jpg", - url_endpoint="https://ik.imagekit.io/test_url_endpoint", - transformation_position="path", - transformation=[ - # Text overlay - { - "overlay": { - "type": "text", - "text": "Every thing", - "position": {"x": "10", "y": "20", "focus": "center"}, - "timing": {"start": 5.0, "duration": "10", "end": 15.0}, - "transformation": [ - { - "width": "bw_mul_0.5", - "font_size": 20.0, - "font_family": "Arial", - "font_color": "0000ff", - "inner_alignment": "left", - "padding": 5.0, - "alpha": 7.0, - "typography": "b", - "background": "red", - "radius": 10.0, - "rotation": "N45", - "flip": "h", - "line_height": 20.0, - } - ], - } - }, - # Image overlay - { - "overlay": { - "type": "image", - "input": "logo.png", - "position": {"x": "10", "y": "20", "focus": "center"}, - "timing": {"start": 5.0, "duration": "10", "end": 15.0}, - "transformation": [ - { - "width": "bw_mul_0.5", - "height": "bh_mul_0.5", - "rotation": "N45", - "flip": "h", - "overlay": {"type": "text", "text": "Nested text overlay"}, - } - ], - } - }, - # Video overlay - { - "overlay": { - "type": "video", - "input": "play-pause-loop.mp4", - "position": {"x": "10", "y": "20", "focus": "center"}, - "timing": {"start": 5.0, "duration": "10", "end": 15.0}, - "transformation": [ - {"width": "bw_mul_0.5", "height": "bh_mul_0.5", "rotation": "N45", "flip": "h"} - ], - } - }, - # Subtitle overlay - { - "overlay": { - "type": "subtitle", - "input": "subtitle.srt", - "position": {"x": "10", "y": "20", "focus": "center"}, - "timing": {"start": 5.0, "duration": "10", "end": 15.0}, - "transformation": [ - { - "background": "red", - "color": "0000ff", - "font_family": "Arial", - "font_outline": "2_A1CCDD50", - "font_shadow": "A1CCDD_3", - } - ], - } - }, - # Solid color overlay - { - "overlay": { - "type": "solidColor", - "color": "FF0000", - "position": {"x": "10", "y": "20", "focus": "center"}, - "timing": {"start": 5.0, "duration": "10", "end": 15.0}, - "transformation": [ - { - "width": "bw_mul_0.5", - "height": "bh_mul_0.5", - "alpha": 0.5, - "background": "red", - "gradient": True, - "radius": "max", - } - ], - } - }, - ], - ) - expected = "https://ik.imagekit.io/test_url_endpoint/tr:l-text,i-Every%20thing,lx-10,ly-20,lfo-center,lso-5,leo-15,ldu-10,w-bw_mul_0.5,fs-20,ff-Arial,co-0000ff,ia-left,pa-5,al-7,tg-b,bg-red,r-10,rt-N45,fl-h,lh-20,l-end:l-image,i-logo.png,lx-10,ly-20,lfo-center,lso-5,leo-15,ldu-10,w-bw_mul_0.5,h-bh_mul_0.5,rt-N45,fl-h,l-text,i-Nested%20text%20overlay,l-end,l-end:l-video,i-play-pause-loop.mp4,lx-10,ly-20,lfo-center,lso-5,leo-15,ldu-10,w-bw_mul_0.5,h-bh_mul_0.5,rt-N45,fl-h,l-end:l-subtitle,i-subtitle.srt,lx-10,ly-20,lfo-center,lso-5,leo-15,ldu-10,bg-red,co-0000ff,ff-Arial,fol-2_A1CCDD50,fsh-A1CCDD_3,l-end:l-image,i-ik_canvas,bg-FF0000,lx-10,ly-20,lfo-center,lso-5,leo-15,ldu-10,w-bw_mul_0.5,h-bh_mul_0.5,al-0.5,bg-red,e-gradient,r-max,l-end/base-image.jpg" - assert url == expected - - # Overlay encoding tests - def test_should_use_plain_encoding_for_simple_image_paths_with_slashes_converted_to_double_at(self): - """Test plain encoding for simple image paths.""" - url = self.client.helper.build_url( - src="/https/github.com/medium_cafe_B1iTdD0C.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "image", "input": "/customer_logo/nykaa.png"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-image,i-customer_logo@@nykaa.png,l-end/medium_cafe_B1iTdD0C.jpg" - assert url == expected - - def test_should_use_base64_encoding_for_image_paths_containing_special_characters(self): - """Test base64 encoding for image paths with special characters.""" - url = self.client.helper.build_url( - src="/https/github.com/medium_cafe_B1iTdD0C.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "image", "input": "/customer_logo/Ñykaa.png"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-image,ie-Y3VzdG9tZXJfbG9nby%2FDkXlrYWEucG5n,l-end/medium_cafe_B1iTdD0C.jpg" - assert url == expected - - def test_should_use_plain_encoding_for_simple_text_overlays(self): - """Test plain encoding for simple text.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": "HelloWorld"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-text,i-HelloWorld,l-end/sample.jpg" - assert url == expected - - def test_should_convert_slashes_to_double_at_in_font_family_paths_for_custom_fonts(self): - """Test font family path conversion.""" - url = self.client.helper.build_url( - src="/https/github.com/medium_cafe_B1iTdD0C.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[ - { - "overlay": { - "type": "text", - "text": "Manu", - "transformation": [{"font_family": "nested-path/Poppins-Regular_Q15GrYWmL.ttf"}], - } - } - ], - ) - expected = "https://ik.imagekit.io/demo/tr:l-text,i-Manu,ff-nested-path@@Poppins-Regular_Q15GrYWmL.ttf,l-end/medium_cafe_B1iTdD0C.jpg" - assert url == expected - - def test_should_use_url_encoding_for_text_overlays_with_spaces_and_safe_characters(self): - """Test URL encoding for text with spaces.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": "Hello World"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-text,i-Hello%20World,l-end/sample.jpg" - assert url == expected - - def test_should_use_base64_encoding_for_text_overlays_with_special_unicode_characters(self): - """Test base64 encoding for Unicode text.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": "हिन्दी"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-text,ie-4KS54KS%2F4KSo4KWN4KSm4KWA,l-end/sample.jpg" - assert url == expected - - def test_should_use_plain_encoding_when_explicitly_specified_for_text_overlay(self): - """Test explicit plain encoding for text.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": "HelloWorld", "encoding": "plain"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-text,i-HelloWorld,l-end/sample.jpg" - assert url == expected - - def test_should_use_base64_encoding_when_explicitly_specified_for_text_overlay(self): - """Test explicit base64 encoding for text.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "text", "text": "HelloWorld", "encoding": "base64"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-text,ie-SGVsbG9Xb3JsZA%3D%3D,l-end/sample.jpg" - assert url == expected - - def test_should_use_plain_encoding_when_explicitly_specified_for_image_overlay(self): - """Test explicit plain encoding for image.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "image", "input": "/customer/logo.png", "encoding": "plain"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-image,i-customer@@logo.png,l-end/sample.jpg" - assert url == expected - - def test_should_use_base64_encoding_when_explicitly_specified_for_image_overlay(self): - """Test explicit base64 encoding for image.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "image", "input": "/customer/logo.png", "encoding": "base64"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-image,ie-Y3VzdG9tZXIvbG9nby5wbmc%3D,l-end/sample.jpg" - assert url == expected - - def test_should_use_base64_encoding_when_explicitly_specified_for_video_overlay(self): - """Test explicit base64 encoding for video.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.mp4", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "video", "input": "/path/to/video.mp4", "encoding": "base64"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-video,ie-cGF0aC90by92aWRlby5tcDQ%3D,l-end/sample.mp4" - assert url == expected - - def test_should_use_base64_encoding_when_explicitly_specified_for_subtitle_overlay(self): - """Test explicit base64 encoding for subtitle.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.mp4", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "subtitle", "input": "sub.srt", "encoding": "base64"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-subtitle,ie-c3ViLnNydA%3D%3D,l-end/sample.mp4" - assert url == expected - - def test_should_use_plain_encoding_when_explicitly_specified_for_subtitle_overlay(self): - """Test explicit plain encoding for subtitle overlay.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.mp4", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="path", - transformation=[{"overlay": {"type": "subtitle", "input": "/sub.srt", "encoding": "plain"}}], - ) - expected = "https://ik.imagekit.io/demo/tr:l-subtitle,i-sub.srt,l-end/sample.mp4" - assert url == expected - - def test_should_properly_encode_overlay_text_when_transformations_are_in_query_parameters(self): - """Test text overlay encoding with query position.""" - url = self.client.helper.build_url( - src="/https/github.com/sample.jpg", - url_endpoint="https://ik.imagekit.io/demo", - transformation_position="query", - transformation=[{"overlay": {"type": "text", "text": "Minimal Text"}}], - ) - expected = "https://ik.imagekit.io/demo/sample.jpg?tr=l-text,i-Minimal%20Text,l-end" - assert url == expected diff --git a/tests/custom/url_generation/test_signing.py b/tests/custom/url_generation/test_signing.py deleted file mode 100644 index 8df8b6d3..00000000 --- a/tests/custom/url_generation/test_signing.py +++ /dev/null @@ -1,168 +0,0 @@ -"""Signing URL tests - converted from Ruby SDK.""" - -from typing import TYPE_CHECKING - -import pytest - -from imagekitio import ImageKit - -if TYPE_CHECKING: - from imagekitio._client import ImageKit as ImageKitType - - -class TestSigning: - """Test URL signing functionality.""" - - client: "ImageKitType" - - @pytest.fixture(autouse=True) - def setup(self) -> None: - """Set up test client.""" - self.client = ImageKit(private_key="dummy-key") - - def test_should_generate_a_signed_url_when_signed_is_true_without_expires_in(self) -> None: - """Should generate a signed URL when signed is true without expires_in.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", url_endpoint="https://ik.imagekit.io/demo/", signed=True - ) - - expected = "https://ik.imagekit.io/demo/sdk-testing-files/future-search.png?ik-s=32dbbbfc5f945c0403c71b54c38e76896ef2d6b0" - assert url == expected - - def test_should_generate_a_signed_url_when_signed_is_true_with_expires_in(self) -> None: - """Should generate a signed URL when signed is true with expires_in.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", - url_endpoint="https://ik.imagekit.io/demo/", - signed=True, - expires_in=3600, - ) - - # Expect ik-t exist in the URL. We don't assert signature because it will keep changing. - assert "ik-t" in url - - def test_should_generate_a_signed_url_when_expires_in_is_above_0_and_even_if_signed_is_false(self) -> None: - """Should generate a signed URL when expires_in is above 0 and even if signed is false.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", - url_endpoint="https://ik.imagekit.io/demo/", - signed=False, - expires_in=3600, - ) - - # Expect ik-t exist in the URL. We don't assert signature because it will keep changing. - assert "ik-t" in url - - def test_should_generate_signed_url_with_special_characters_in_filename(self) -> None: - """Should generate signed URL with special characters in filename.""" - url = self.client.helper.build_url( - src="sdk-testing-files/हिन्दी.png", url_endpoint="https://ik.imagekit.io/demo/", signed=True - ) - - expected = "https://ik.imagekit.io/demo/sdk-testing-files/%E0%A4%B9%E0%A4%BF%E0%A4%A8%E0%A5%8D%E0%A4%A6%E0%A5%80.png?ik-s=3fff2f31da1f45e007adcdbe95f88c8c330e743c" - assert url == expected - - def test_should_generate_signed_url_with_text_overlay_containing_special_characters(self) -> None: - """Should generate signed URL with text overlay containing special characters.""" - url = self.client.helper.build_url( - src="sdk-testing-files/हिन्दी.png", - url_endpoint="https://ik.imagekit.io/demo/", - transformation=[ - { - "overlay": { - "type": "text", - "text": "हिन्दी", - "transformation": [ - { - "font_color": "red", - "font_size": "32", - "font_family": "sdk-testing-files/Poppins-Regular_Q15GrYWmL.ttf", - } - ], - } - } - ], - signed=True, - ) - - expected = "https://ik.imagekit.io/demo/sdk-testing-files/%E0%A4%B9%E0%A4%BF%E0%A4%A8%E0%A5%8D%E0%A4%A6%E0%A5%80.png?tr=l-text,ie-4KS54KS%2F4KSo4KWN4KSm4KWA,co-red,fs-32,ff-sdk-testing-files@@Poppins-Regular_Q15GrYWmL.ttf,l-end&ik-s=ac9f24a03080102555e492185533c1ae6bd93fa7" - assert url == expected - - def test_should_generate_signed_url_with_text_overlay_and_special_characters_using_path_transformation_position( - self, - ) -> None: - """Should generate signed URL with text overlay and special characters using path transformation position.""" - url = self.client.helper.build_url( - src="sdk-testing-files/हिन्दी.png", - url_endpoint="https://ik.imagekit.io/demo/", - transformation_position="path", - transformation=[ - { - "overlay": { - "type": "text", - "text": "हिन्दी", - "transformation": [ - { - "font_color": "red", - "font_size": "32", - "font_family": "sdk-testing-files/Poppins-Regular_Q15GrYWmL.ttf", - } - ], - } - } - ], - signed=True, - ) - - expected = "https://ik.imagekit.io/demo/tr:l-text,ie-4KS54KS%2F4KSo4KWN4KSm4KWA,co-red,fs-32,ff-sdk-testing-files@@Poppins-Regular_Q15GrYWmL.ttf,l-end/sdk-testing-files/%E0%A4%B9%E0%A4%BF%E0%A4%A8%E0%A5%8D%E0%A4%A6%E0%A5%80.png?ik-s=69f2ecbb7364bbbad24616e1f7f1bac5a560fc71" - assert url == expected - - def test_should_generate_signed_url_with_query_parameters(self) -> None: - """Should generate signed URL with query parameters.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", - url_endpoint="https://ik.imagekit.io/demo/", - query_parameters={"version": "1.0", "cache": "false"}, - signed=True, - ) - - expected = "https://ik.imagekit.io/demo/sdk-testing-files/future-search.png?version=1.0&cache=false&ik-s=f2e5a1b8b6a0b03fd63789dfc6413a94acef9fd8" - assert url == expected - - def test_should_generate_signed_url_with_transformations_and_query_parameters(self) -> None: - """Should generate signed URL with transformations and query parameters.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", - url_endpoint="https://ik.imagekit.io/demo/", - transformation=[{"width": 300, "height": 200}], - query_parameters={"version": "2.0"}, - signed=True, - ) - - expected = "https://ik.imagekit.io/demo/sdk-testing-files/future-search.png?version=2.0&tr=w-300,h-200&ik-s=601d97a7834b7554f4dabf0d3fc3a219ceeb6b31" - assert url == expected - - def test_should_not_sign_url_when_signed_is_false(self) -> None: - """Should not sign URL when signed is false.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", url_endpoint="https://ik.imagekit.io/demo/", signed=False - ) - - expected = "https://ik.imagekit.io/demo/sdk-testing-files/future-search.png" - assert url == expected - assert "ik-s=" not in url - assert "ik-t=" not in url - - def test_should_generate_signed_url_with_transformations_in_path_position_and_query_parameters(self) -> None: - """Should generate signed URL with transformations in path position and query parameters.""" - url = self.client.helper.build_url( - src="sdk-testing-files/future-search.png", - url_endpoint="https://ik.imagekit.io/demo/", - transformation=[{"width": 300, "height": 200}], - transformation_position="path", - query_parameters={"version": "2.0"}, - signed=True, - ) - - expected = "https://ik.imagekit.io/demo/tr:w-300,h-200/sdk-testing-files/future-search.png?version=2.0&ik-s=dd1ee8f83d019bc59fd57a5fc4674a11eb8a3496" - assert url == expected