diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index eb50c37..48d5cb3 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -7,12 +7,15 @@ on:
jobs:
deploy:
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python: [3.6, 3.7, 3.8, 3.9, '3.10']
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
- python-version: "3.x"
+ python-version: ${{ matrix.python }}
- name: Install Tox and any other packages
run: |
python -m pip install --upgrade pip
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aced653..f158a7f 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,8 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python: [3.7, 3.8]
-
+ python: [3.6, 3.7, 3.8, 3.9, '3.10']
steps:
- uses: actions/checkout@v1
- name: Setup Python
diff --git a/.gitignore b/.gitignore
index d26187f..8992bc1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ parts/
sdist/
var/
wheels/
+sample/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
diff --git a/README.md b/README.md
index 717d808..c4f341f 100644
--- a/README.md
+++ b/README.md
@@ -1,349 +1,1088 @@
[
](https://imagekit.io)
+
# ImageKit.io Python SDK
-[![Python CI]()](https://github.com/imagekit-developer/imagekit-python/)
-[![imagekitio]()](https://pypi.org/project/imagekitio)
+[](https://github.com/imagekit-developer/imagekit-python/)
+[](https://pypi.org/project/imagekitio)
[](https://codecov.io/gh/imagekit-developer/imagekit-python)
[](https://opensource.org/licenses/MIT)
[](https://twitter.com/ImagekitIo)
-ImageKit Python SDK allows you to use [image resizing](https://docs.imagekit.io/features/image-transformations), [optimization](https://docs.imagekit.io/features/image-optimization), [file uploading](https://docs.imagekit.io/api-reference/upload-file-api) and other [ImageKit APIs](https://docs.imagekit.io/api-reference/api-introduction) from applications written in the Python language.
+ImageKit Python SDK allows you to use [Image Resizing](https://docs.imagekit.io/features/image-transformations), [Optimization](https://docs.imagekit.io/features/image-optimization), [File Uploading](https://docs.imagekit.io/api-reference/upload-file-api) and
+other [ImageKit APIs](https://docs.imagekit.io/api-reference/api-introduction) from software written in the Python
+language.
Supported Python Versions: >=3.6
Table of contents -
- * [Installation](#Installation)
- * [Initialization](#Initialization)
- * [URL Generation](#URL-generation)
- * [File Upload](#File-Upload)
- * [File Management](#File-Management)
- * [Utility Functions](#Utility-functions)
- * [Support](#Support)
- * [Links](#Links)
-
-
- ## Installation
- Go to your terminal and type the following command
+
+- [Installation](#installation)
+- [Initialization](#initialization)
+- [Change Log](#change-log)
+- [Usage](#usage)
+ - [URL Generation](#url-generation)
+ - [File Upload](#file-upload)
+ - [File Management](#file-management)
+ - [Utility Functions](#utility-functions)
+- [Handling errors](#handling-errors)
+- [Development](#development)
+ - [Tests](#tests)
+ - [Sample](#sample)
+- [Support](#support)
+- [Links](#links)
+
+## Installation
+
+Go to your terminal and type the following command.
+
```bash
pip install imagekitio
```
## Initialization
+
```python
from imagekitio import ImageKit
+
imagekit = ImageKit(
- private_key='your private_key',
- public_key='your public_key',
- url_endpoint = 'your url_endpoint'
+ private_key='your_private_key',
+ public_key='your_public_key',
+ url_endpoint='your_url_endpoint'
)
```
-## Usage
+## Change log
+
+This document presents a list of changes that break the existing functionality of previous versions. We try to minimize these disruptions, but they are sometimes unavoidable, especially in significant updates. Therefore, versions are marked semantically and tagged as major upgrades whenever such breaking changes occur.
+
+### Breaking History:
+
+Changes from `2.2.8 -> 3.0.0` are listed below
-You can use this Python SDK for 3 different kinds of methods - URL generation, file upload, and file management.
-The usage of the SDK has been explained below.
+1. Throw an Error:
-## URL generation
+**What changed**
-**1. Using Image path and image hostname or endpoint**
+- Before the upgrade, an `error` dict was coming in the return object of any function call. Now, SDK throws an exception in case of an error.
-This method allows you to create a URL using the path where the image exists and the URL
+**Who is affected?**
+
+- This affects any development in your software that calls APIs from ImageKit IO and handles errors based on what's returned.
+
+**How should I update my code?**
+
+- To avoid failures in an application, you could handle errors as [documented here](#handling-errors)
+
+# Usage
+
+You can use this Python SDK for three different kinds of methods:
+
+- [URL Generation](#url-generation)
+- [File Upload](#file-upload)
+- [File Management](#file-management)
+- [Utility Functions](#utility-functions)
+
+## URL Generation
+
+**1. Using Image path and endpoint (hostname)**
+
+This method allows you to create a URL using the relative file path where the image exists and the URL
endpoint(url_endpoint) you want to use to access the image. You can refer to the documentation
[here](https://docs.imagekit.io/integration/url-endpoints) to read more about URL endpoints
in ImageKit and the section about [image origins](https://docs.imagekit.io/integration/configure-origin) to understand
about paths with different kinds of origins.
-
+The File can be an image, video, or any other static file supported by ImageKit.
```python
imagekit_url = imagekit.url({
- "path": "/default-image.jpg",
- "url_endpoint": "https://ik.imagekit.io/your_imagekit_id/endpoint/",
- "transformation": [{"height": "300", "width": "400"}],
- }
-)
+ "path": "/default-image.jpg",
+ "url_endpoint": "https://ik.imagekit.io/your_imagekit_id/endpoint/",
+ "transformation": [{
+ "height": "300",
+ "width": "400",
+ "raw": "ar-4-3,q-40"
+ }],
+})
```
-The result in a URL like
+Sample Result URL -
+
```
-https://ik.imagekit.io/your_imagekit_id/endpoint/tr:h-300,w-400/default-image.jpg
+https://ik.imagekit.io/your_imagekit_id/endpoint/tr:h-300,w-400,ar-4-3,q-40/default-image.jpg
```
-**2.Using full image URL**
-This method allows you to add transformation parameters to an absolute URL using `src` parameter. This method should be used if you have the complete image URL stored in your database.
+**2. Using full image URL**
+
+This method allows you to add transformation parameters to an absolute URL using the `src` parameter. This method should be
+used if you have the complete image URL stored in your database.
```python
image_url = imagekit.url({
"src": "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg",
- "transformation" : [{
+ "transformation": [{
"height": "300",
- "width": "400"
+ "width": "400",
+ "raw": "ar-4-3,q-40"
}]
})
```
-The results in a URL like
+Sample Result URL -
```
-https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300%2Cw-400
+https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300%2Cw-400%2Car-4-3%2Cq-40
```
+The `.url()` method accepts the following parameters.
-The ```.url()``` method accepts the following parameters.
-
-| Option | Description |
-| :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| url_endpoint | Optional. The base URL to be appended before the path of the image. If not specified, the URL Endpoint specified at the time of SDK initialization is used. For example, https://ik.imagekit.io/your_imagekit_id/endpoint/ |
-| path | Conditional. This is the path at which the image exists. For example, `/path/to/image.jpg`. Either the `path` or `src` parameter needs to be specified for URL generation. |
-| src | Conditional. This is the complete URL of an image already mapped to ImageKit. For example, `https://ik.imagekit.io/your_imagekit_id/endpoint/path/to/image.jpg`. Either the `path` or `src` parameter needs to be specified for URL generation. |
-| transformation | Optional. An array of objects specifying the transformation to be applied in the URL. The transformation name and the value should be specified as a key-value pair in the object. Different steps of a [chained transformation](https://docs.imagekit.io/features/image-transformations/chained-transformations) can be specified as different objects of the array. The complete list of supported transformations in the SDK and some examples of using them are given later. If you use a transformation name that is not specified in the SDK, it gets applied as it is in the URL. |
-| transformation_position | Optional. The default value is `path` that places the transformation string as a path parameter in the URL. It can also be specified as `query`, which adds the transformation string as the query parameter `tr` in the URL. If you use the `src` parameter to create the URL, then the transformation string is always added as a query parameter. |
-| query_parameters | Optional. These are the other query parameters that you want to add to the final URL. These can be any query parameters and not necessarily related to ImageKit. Especially useful if you want to add some versioning parameter to your URLs. |
-| signed | Optional. Boolean. Default is `false`. If set to `true`, the SDK generates a signed image URL adding the image signature to the image URL. This can only be used if you are creating the URL with the `url_endpoint` and `path` parameters and not with the `src` parameter. |
-| expire_seconds | Optional. Integer. Meant to be used along with the `signed` parameter to specify the time in seconds from now when the URL should expire. If specified, the URL contains the expiry timestamp in the URL, and the image signature is modified accordingly. |
-
+| Option | Description |
+| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| url_endpoint | Optional. The prepended base URL before the path of the image. If not specified, the URL Endpoint specified during SDK initialization gets used. For example, https://ik.imagekit.io/your_imagekit_id/endpoint/ |
+| path | Conditional. A path at which the image exists. For example, `/path/to/image.jpg`. Specify a `path` or `src` parameter for URL generation. |
+| src | Conditional. Complete URL of an image already mapped to ImageKit. For example, `https://ik.imagekit.io/your_imagekit_id/endpoint/path/to/image.jpg`. Specify a `path` or `src` parameter for URL generation. |
+| transformation | Optional. Specify an array of objects with name and the value in key-value pair to apply transformation params in the URL. Append different steps of a [chained transformation](https://docs.imagekit.io/features/image-transformations/chained-transformations) as different objects of the array. This document includes a complete list of supported transformations in the SDK with some examples. If one uses an unspecified transformation name, it gets applied as it is in the URL. |
+| transformation_position | Optional. The default value is `path`, which places the transformation string as a path parameter in the URL. One can also specify it as a query, which adds the transformation string as the query parameter `tr` in the URL. Suppose one uses the `src` parameter to create the URL. In that case, the transformation string is always a query parameter. |
+| query_parameters | Optional. These are the other query parameters that one wants to add to the final URL. These can be any query parameters and are not necessarily related to ImageKit. Especially useful if one wants to add some versioning parameter to their URLs. |
+| signed | Optional. Boolean. The default is `false`. If set to `true`, the SDK generates a signed image URL adding the image signature to the image URL. One can only use this if they create the URL with the `url_endpoint` and `path` parameters, not the `src` parameter. |
+| expire_seconds | Optional. Integer. Used along with the `signed` parameter to specify the time in seconds from `now` when the URL should expire. If specified, the URL contains the expiry timestamp, and the image signature is modified accordingly. |
## Examples of generating URLs
+
**1. Chained Transformations as a query parameter**
```python
- image_url = imagekit.url({
- "path": "/default-image.jpg",
- "url_endpoint": "https://ik.imagekit.io/your_imagekit_id/endpoint/",
- "transformation": [{
- "height": "300",
- "width": "400"
- },
- {
- "rotation": 90
- }],
- "transformation_position ": "query"
- })
+image_url = imagekit.url({
+ "path": "/default-image.jpg",
+ "url_endpoint": "https://ik.imagekit.io/your_imagekit_id/endpoint/",
+ "transformation": [
+ {
+ "height": "300",
+ "width": "400"
+ },
+ {
+ "rotation": 90
+ }
+ ],
+ "transformation_position": "query"
+})
```
+
Sample Result URL -
+
```
https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300%2Cw-400%3Art-90
```
+**2. Sharpening and contrast transform and a progressive JPG image**
-
-**2. Sharpening and contrast transforms and a progressive JPG image**
-
-There are some transforms like [Sharpening](https://docs.imagekit.io/features/image-transformations/image-enhancement-and-color-manipulation)
-that can be added to the URL with or without any other value. To use such transforms without specifying a value, specify
-the value as "-" in the transformation object. Otherwise, specify the value that you want to be added to this transformation.
-
+Add transformations like [Sharpening](https://docs.imagekit.io/features/image-transformations/image-enhancement-and-color-manipulation) to the URL with or without any other value. To use such transforms without specifying a value, set it as "-" in the transformation object. Otherwise, use the value that one wants to add to this transformation.
```python
- image_url = imagekit.url({
- "src": "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg",
- "transformation": [{
- "format": "jpg",
- "progressive": "true",
- "effect_sharpen": "-",
- "effect_contrast": "1"
- }]
- })
+image_url = imagekit.url({
+ "src": "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg",
+ "transformation": [{
+ "format": "jpg",
+ "progressive": "true",
+ "effect_sharpen": "-",
+ "effect_contrast": "1"
+ }]
+})
```
+Sample Result URL -
+
```
-//Note that because `src` parameter was used, the transformation string gets added as a query parameter `tr`
+# Note that because `src` parameter is in effect, the transformation string gets added as a query parameter `tr`
+
https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=f-jpg%2Cpr-true%2Ce-sharpen%2Ce-contrast-1
```
**3. Signed URL that expires in 300 seconds with the default URL endpoint and other query parameters**
```python
- image_url = imagekit.url({
- "path": "/default-image",
- "query_parameters": {
- "p1": "123",
- "p2": "345"
- },
- "transformation": [{
- "height": "300",
- "width": "400"
- }],
- "signed": True,
- "expire_seconds": 300
- })
+image_url = imagekit.url({
+ "path": "/default-image.jpg",
+ "query_parameters": {
+ "p1": "123",
+ "p2": "345"
+ },
+ "transformation": [{
+ "height": "300",
+ "width": "400"
+ }],
+ "signed": True,
+ "expire_seconds": 300
+})
```
-**Sample Result URL**
+
+Sample Result URL -
+
```
-https://ik.imagekit.io/your_imagekit_id/tr:h-300,w-400/default-image.jpg?v=123&ik-t=1567358667&ik-s=f2c7cdacbe7707b71a83d49cf1c6110e3d701054
+https://ik.imagekit.io/your_imagekit_id/tr:h-300,w-400/default-image.jpg?p1=123&p2=345&ik-t=1658899345&ik-s=8f03aca28432d4e87f697a48143efb4497bbed9e
```
**List of transformations**
-The complete list of transformations supported and their usage in ImageKit can be found [here](https://docs.imagekit.io/features/image-transformations/resize-crop-and-other-transformations).
-The SDK gives a name to each transformation parameter, making the code simpler, making the code simpler, and readable.
-If a transformation is supported in ImageKit, but a name for it cannot be found in the table below, then use the
-transformation code from ImageKit docs as the name when using the `url` function.
-
-| Supported Transformation Name | Translates to parameter |
-| ----------------------------- | ----------------------- |
-| height | h|
-| width | w|
-| aspect_ratio | ar|
-| quality | q|
-| crop | c|
-| crop_mode | cm|
-| x | x|
-| y | y|
-| focus | fo|
-| format | f|
-| radius | r|
-| background | bg|
-| border | b|
-| rotation | rt|
-| blur | bl|
-| named | n|
-| overlay_image | oi|
-| overlay_image_aspect_ratio | oiar|
-| overlay_image_background | oibg|
-| overlay_image_border | oib|
-| overlay_image_dpr | oidpr|
-| overlay_image_quality | oiq|
-| overlay_image_cropping | oic|
-| overlay_image_trim | oit|
-| overlay_x | ox|
-| overlay_y | oy|
-| overlay_focus | ofo|
-| overlay_height | oh|
-| overlay_width | ow|
-| overlay_text | ot|
-| overlay_text_font_size | ots|
-| overlay_text_font_family | otf|
-| overlay_text_color | otc|
-| overlay_text_transparency | oa|
-| overlay_alpha | oa|
-| overlay_text_typography | ott|
-| overlay_background | obg|
-| overlay_image_trim | oit|
-| overlay_text_encoded | ote|
-| overlay_text_width | otw|
-| overlay_text_background | otbg|
-| overlay_text_padding | otp|
-| overlay_text_inner_alignment | otia|
-| overlay_radius | or|
-| progressive | pr|
-| lossless | lo|
-| trim | t|
-| metadata | md|
-| color_profile | cp|
-| default_image | di|
-| dpr | dpr|
-| effect_sharpen | e-sharpen|
-| effect_usm | e-usm|
-| effect_contrast | e-contrast|
-| effect_gray | e-grayscale|
-| original | orig|
+The complete list of transformations supported and their usage in ImageKit is available [here](https://docs.imagekit.io/features/image-transformations/resize-crop-and-other-transformations).
+The SDK gives a name to each transformation parameter, making the code simpler, more straightforward, and readable. If a transformation is supported in ImageKit, though it cannot be found in the table below, then use the transformation code from ImageKit docs as the name when using the `URL` function.
+
+If you want to generate transformations in your application and add them to the URL as it is, use the raw parameter.
+
+| Supported Transformation Name | Translates to parameter |
+| ----------------------------- | ------------------------------- |
+| height | h |
+| width | w |
+| aspect_ratio | ar |
+| quality | q |
+| crop | c |
+| crop_mode | cm |
+| x | x |
+| y | y |
+| focus | fo |
+| format | f |
+| radius | r |
+| background | bg |
+| border | b |
+| rotation | rt |
+| blur | bl |
+| named | n |
+| overlay_x | ox |
+| overlay_y | oy |
+| overlay_focus | ofo |
+| overlay_height | oh |
+| overlay_width | ow |
+| overlay_image | oi |
+| overlay_image_trim | oit |
+| overlay_image_aspect_ratio | oiar |
+| overlay_image_background | oibg |
+| overlay_image_border | oib |
+| overlay_image_dpr | oidpr |
+| overlay_image_quality | oiq |
+| overlay_image_cropping | oic |
+| overlay_image_focus | oifo |
+| overlay_text | ot |
+| overlay_text_font_size | ots |
+| overlay_text_font_family | otf |
+| overlay_text_color | otc |
+| overlay_text_transparency | oa |
+| overlay_alpha | oa |
+| overlay_text_typography | ott |
+| overlay_background | obg |
+| overlay_text_encoded | ote |
+| overlay_text_width | otw |
+| overlay_text_background | otbg |
+| overlay_text_padding | otp |
+| overlay_text_inner_alignment | otia |
+| overlay_radius | or |
+| progressive | pr |
+| lossless | lo |
+| trim | t |
+| metadata | md |
+| color_profile | cp |
+| default_image | di |
+| dpr | dpr |
+| effect_sharpen | e-sharpen |
+| effect_usm | e-usm |
+| effect_contrast | e-contrast |
+| effect_gray | e-grayscale |
+| original | orig |
+| raw | replaced by the parameter value |
## File Upload
The SDK provides a simple interface using the `.upload_file()` method to upload files to the ImageKit Media library. It
-accepts all the parameters supported by the [ImageKit Upload API](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload).
+accepts all the parameters supported by
+the [ImageKit Upload API](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload).
-The `upload_file()` method requires at least the `file` and the `file_name` parameter to upload a file and returns a Dict with error or success data. Use `options` parameter to pass other parameters supported by the [ImageKit Upload API](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload). Use the same parameter name as specified in the upload API documentation.
+The `upload_file()` method requires at least the `file` as (URL/Base64/Binary) and the `file_name` parameter to upload a
+file. The method returns a dict data in case of success, or it will throw a custom exception in case of failure.
+Use the `options` parameter to pass other parameters supported by
+the [ImageKit Upload API](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload). Use the same
+parameter name as specified in the upload API documentation.
Simple usage
```python
-imagekit.upload_file(
- file= "", # required
- file_name= "my_file_name.jpg", # required
- options= {
- "folder" : "/example-folder/",
- "tags": ["sample-tag"],
- "is_private_file": False,
- "use_unique_file_name": True,
- "response_fields": ["is_private_file", "tags"],
+from imagekitio.models.UploadFileRequestOptions import UploadFileRequestOptions
+
+extensions = [
+ {
+ 'name': 'remove-bg',
+ 'options': {
+ 'add_shadow': True,
+ 'bg_color': 'pink'
+ }
+ },
+ {
+ 'name': 'google-auto-tagging',
+ 'minConfidence': 80,
+ 'maxTags': 10
}
+]
+
+options = UploadFileRequestOptions(
+ use_unique_file_name=False,
+ tags=['abc', 'def'],
+ folder='/testing-python-folder/',
+ is_private_file=False,
+ custom_coordinates='10,10,20,20',
+ response_fields=['tags', 'custom_coordinates', 'is_private_file',
+ 'embedded_metadata', 'custom_metadata'],
+ extensions=extensions,
+ webhook_url='https://webhook.site/c78d617f-33bc-40d9-9e61-608999721e2e',
+ overwrite_file=True,
+ overwrite_ai_tags=False,
+ overwrite_tags=False,
+ overwrite_custom_metadata=True,
+ custom_metadata={'testss': 12},
)
+result = imagekit.upload_file(file='', # required
+ file_name='my_file_name.jpg', # required
+ options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print that uploaded file's ID
+print(result.file_id)
```
-If the upload succeeds, `error` will be `null,` and the `result` will be the same as what is received from ImageKit's servers.
-If the upload fails, `error` will be the same as what is received from ImageKit's servers, and the `result` will be null. Learn more from the sample app in this repository.
+If the upload succeeds, the `result` will be the `UploadFileResult` class.
+
+If the upload fails, the custom exception will be thrown with:
+
+- `response_help` for any kind of help
+- `response_metadata` with `raw`, `http_status_code` and `headers`
+- `message` can be called to get the error message received from ImageKit's servers.
## File Management
-The SDK provides a simple interface for all the [media APIs mentioned here](https://docs.imagekit.io/api-reference/media-api)
-to manage your files. This also returns `error` and `result`. The error will be `None` if API succeeds.
+The SDK provides a simple interface for all
+the [media APIs mentioned here](https://docs.imagekit.io/api-reference/media-api)
+to manage your files. This also returns `result`.
**1. List & Search Files**
-Accepts an object specifying the parameters to be used to list and search files. All parameters specified
-in the [documentation here](https://docs.imagekit.io/api-reference/media-api/list-and-search-files#list-and-search-file-api) can be passed as it is with the correct values to get the results.
+Accepts an object specifying the parameters used to list and search files. All parameters specified
+in
+the [documentation here](https://docs.imagekit.io/api-reference/media-api/list-and-search-files#list-and-search-file-api)
+can be passed with the correct values to get the results.
+
+```Python
+from imagekitio.models.ListAndSearchFileRequestOptions import ListAndSearchFileRequestOptions
+
+options = ListAndSearchFileRequestOptions(
+ type='file',
+ sort='ASC_CREATED',
+ path='/',
+ search_query="created_at >= '2d' OR size < '2mb' OR format='png'",
+ file_type='all',
+ limit=5,
+ skip=0,
+ tags='Software, Developer, Engineer',
+)
-```python
-imagekit.list_files({
- "skip": 10,
- "limit": 10,
-})
+result = imagekit.list_files(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the first file's ID
+print(result.list[0].file_id)
```
+
**2. Get File Details**
-Accepts the file ID and fetches the details as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-details)
+
+Accepts the file ID and fetches the details as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-details)
```python
-imagekit.get_file_details(file_id)
+file_id = "your_file_id"
+result = imagekit.get_file_details(file_id=file_id) # file_id required
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print that file's id
+print(result.file_id)
```
-**3. Get File Metadata**
-Accepts the file ID and fetches the metadata as per the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-for-uploaded-media-files)
+**3. Get File Versions**
+
+Accepts the file ID and fetches the details as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-versions)
+
```python
-imagekit.get_file_metadata(file_id)
+file_id = "your_file_id"
+result = imagekit.get_file_versions(file_id=file_id) # file_id required
+
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print that file's version id
+print(result.list[0].version_info.id)
```
+**4. Get File Version details**
-**3. Get File Metadata from remote url**
-Accepts the remote file url and fetches the metadata as per the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-from-remote-url)
+Accepts the `file_id` and `version_id` and fetches the details as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-version-details)
```python
-imagekit.get_remote_file_url_metadata(remote_file_url)
+result = imagekit.get_file_version_details(
+ file_id='file_id',
+ version_id='version_id'
+)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print that file's id
+print(result.file_id)
+
+# print that file's version id
+print(result.version_info.id)
```
-**4. Update File Details**
-Update parameters associated with the file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/update-file-details).
-The first argument to the `update_field_details` method is the file ID, and a second argument is an object with the
-parameters to be updated.
+**5. Update File Details**
+
+Accepts all the parameters as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/update-file-details).
+The first argument to the `update_file_details()` method is the file ID, and a second argument is an object with the
+parameters to be
+updated.
```python
-imagekit.update_file_details("file_id", {
- "tags": ["image_tag"],
- "custom_coordinates": "10,10,100, 100"
-})
+from imagekitio.models.UpdateFileRequestOptions import UpdateFileRequestOptions
+
+extensions = [
+ {
+ 'name': 'remove-bg',
+ 'options': {
+ 'add_shadow': True,
+ 'bg_color': 'red'
+ }
+ },
+ {
+ 'name': 'google-auto-tagging',
+ 'minConfidence': 80,
+ 'maxTags': 10
+ }
+]
+
+options = UpdateFileRequestOptions(
+ remove_ai_tags=['remove-ai-tag-1', 'remove-ai-tag-2'],
+ webhook_url='url',
+ extensions=extensions,
+ tags=['tag-1', 'tag-2'],
+ custom_coordinates='10,10,100,100',
+ custom_metadata={'test': 11},
+)
+
+result = imagekit.update_file_details(file_id='62cfd39819ca454d82a07182'
+ , options=options) # required
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print that file's id
+print(result.file_id)
+```
+
+**6. Add tags**
+
+Accepts a list of `file_ids` and `tags` as a parameter to be used to add tags. All parameters specified in
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/add-tags-bulk) can be passed to
+the `.add_tags()` functions to get the results.
+
+```python
+result = imagekit.add_tags(file_ids=['file-id-1', 'file-id-2'], tags=['add-tag-1', 'add-tag-2'])
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# list successfully updated file ids
+print(result.successfully_updated_file_ids)
+
+# print the first file's id
+print(result.successfully_updated_file_ids[0])
+```
+
+**7. Remove tags**
+
+Accepts a list of `file_ids` and `tags` as a parameter to be used to remove tags. All parameters specified in
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/remove-tags-bulk) can be passed to
+the `.remove_tags()` functions to get the results.
+
+```python
+result = imagekit.remove_tags(file_ids=['file-id-1', 'file-id-2'], tags=['remove-tag-1', 'remove-tag-2'])
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# list successfully updated file ids
+print(result.successfully_updated_file_ids)
+
+# print the first file's id
+print(result.successfully_updated_file_ids[0])
+```
+
+**8. Remove AI tags**
+
+Accepts a list of `file_ids` and `ai_tags` as a parameter to remove AI tags. All parameters specified in
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/remove-aitags-bulk) can be passed to
+the `.remove_ai_tags()` functions to get the results.
+
+```python
+result = imagekit.remove_ai_tags(file_ids=['file-id-1', 'file-id-2'], ai_tags=['remove-ai-tag-1', 'remove-ai-tag-2'])
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# list successfully updated file ids
+print(result.successfully_updated_file_ids)
+
+# print the first file's id
+print(result.successfully_updated_file_ids[0])
```
-**6. Delete File**
-Delete a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file). The method accepts the file ID of the file that has to be
+**9. Delete File**
+
+Delete a file according to the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file). It accepts the file ID of the File that has to be
deleted.
```python
-imagekit.delete_file(file_id)
+file_id = "file_id"
+result = imagekit.delete_file(file_id=file_id)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
+
+**10. Delete FileVersion**
+
+Delete a file version as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file-version).
+The method accepts the `file_id` and particular version id of the File that has to be deleted.
+
+```python
+result = imagekit.delete_file_version(file_id="file_id", version_id="version_id")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
+
+**11. Bulk File Delete by IDs**
+
+Delete a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-files-bulk).
+The method accepts a list of file IDs that have to be deleted.
+
+```python
+result = imagekit.bulk_file_delete(file_ids=["file_id1", "file_id2"])
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# list successfully deleted file ids
+print(result.successfully_deleted_file_ids)
+
+# print the first file's id
+print(result.successfully_deleted_file_ids[0])
+```
+
+**12. Copy file**
+
+Copy a file according to the [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-file).
+The method accepts `source_file_path`, `destination_path`, and `include_file_versions` of the File that has to be copied.
+
+```python
+from imagekitio.models.CopyFileRequestOptions import CopyFileRequestOptions
+
+options = \
+ CopyFileRequestOptions(source_file_path='/source_file_path.jpg',
+ destination_path='/destination_path',
+ include_file_versions=True)
+result = imagekit.copy_file(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
+
+**13. Move File**
+
+Move a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/move-file).
+The method accepts `source_file_path` and `destination_path` of the File that has to be moved.
+
+```python
+from imagekitio.models.MoveFileRequestOptions import MoveFileRequestOptions
+
+options = \
+ MoveFileRequestOptions(source_file_path='/source_file_path.jpg',
+ destination_path='/destination_path')
+result = imagekit.move_file(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
+
+**14. Rename File**
+
+Rename a file per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/rename-file).
+The method accepts the `file_path`, `new_file_name`, and `purge_cache` boolean that has to be renamed.
+
+```python
+from imagekitio.models.RenameFileRequestOptions import RenameFileRequestOptions
+
+options = RenameFileRequestOptions(file_path='/file_path.jpg',
+ new_file_name='new_file_name.jpg',
+ purge_cache=True)
+result = imagekit.rename_file(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the purge request id
+print(result.purge_request_id)
+```
+
+**15. Restore file Version**
+
+Restore a file as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/restore-file-version).
+The method accepts `file_id` and `version_id` of the File that has to be restored.
+
+```python
+result = imagekit.restore_file_version(file_id="file_id", version_id="version_id")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print that file's id
+print(result.file_id)
+```
+
+**16. Create Folder**
+
+Create a folder per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/create-folder).
+The method accepts `folder_name` and `parent_folder_path` as options that must be created.
+
+```Python
+from imagekitio.models.CreateFolderRequestOptions import CreateFolderRequestOptions
+
+options = CreateFolderRequestOptions(folder_name='test',
+ parent_folder_path='/')
+result = imagekit.create_folder(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
+
+**17. Delete Folder**
+
+Delete a folder as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-folder).
+The method accepts `folder_path` as an option that must be deleted.
+
+```python
+from imagekitio.models.DeleteFolderRequestOptions import DeleteFolderRequestOptions
+
+options = DeleteFolderRequestOptions(folder_path='/test/demo')
+result = imagekit.delete_folder(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
+
+**18. Copy Folder**
+
+Copy a folder as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-folder).
+The method accepts the `source_folder_path`, `destination_path`, and `include_file_versions` boolean as options that
+have to be copied.
+
+```python
+from imagekitio.models.CopyFolderRequestOptions import CopyFolderRequestOptions
+options = \
+ CopyFolderRequestOptions(source_folder_path='/source_folder_path',
+ destination_path='/destination/path',
+ include_file_versions=True)
+result = imagekit.copy_folder(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the job's id
+print(result.job_id)
```
-**6. Bulk File Delete by IDs**
-Delete a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-files-bulk). The method accepts list of file IDs of files that has to be deleted.
+**19. Move Folder**
+
+Move a folder as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/move-folder).
+The method accepts the `source_folder_path` and `destination_path` of a folder as options that must be moved.
```python
-imagekit.bulk_file_delete(["file_id1", "file_id2"])
+from imagekitio.models.MoveFolderRequestOptions import MoveFolderRequestOptions
+options = \
+ MoveFolderRequestOptions(source_folder_path='/source_folder_path',
+ destination_path='/destination_path')
+result = imagekit.move_folder(options=options)
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the job's id
+print(result.job_id)
+```
+
+**20. Get Bulk Job Status**
+
+Accepts the `job_id` to get bulk job status as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-move-folder-status).
+The method takes only jobId.
+
+```python
+result = imagekit.get_bulk_job_status(job_id="job_id")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the job's id
+print(result.job_id)
+
+# print the status
+print(result.status)
```
-**6. Purge Cache**
-Programmatically issue a cache clear request as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache).
-Accepts the full URL of the file for which the cache has to be cleared.
+**21. Purge Cache**
+
+Programmatically issue an explicit cache request as per
+the [API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache).
+Accepts the full URL of the File for which the cache has to be cleared.
+
```python
-imagekit.purge_file_cache(full_url)
+result = imagekit.purge_file_cache(file_url="full_url_of_file")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the purge file cache request id
+print(result.request_id)
```
-**7. Purge Cache Status**
-Get the purge cache request status using the request ID returned when a purge cache request gets submitted as pet the
+**22. Purge Cache Status**
+
+Get the purge cache request status using the `cache_request_id` returned when a purge cache request gets submitted as per the
[API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache-status)
```python
-imagekit.get_purge_file_cache_status(cache_request_id)
+result = imagekit.get_purge_file_cache_status(purge_cache_id="cache_request_id")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the purge file cache status
+print(result.status)
+```
+
+**23. Get File Metadata**
+
+Accepts the `file_id` and fetches the metadata as per
+the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-for-uploaded-media-files)
+
+```python
+result = imagekit.get_file_metadata(file_id="file_id")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the file metadata fields
+print(result.width)
+print(result.exif.image.x_resolution)
+```
+
+**24. Get File Metadata from remote URL**
+
+Accepts the `remote_file_url` and fetches the metadata as per
+the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-from-remote-url)
+
+```python
+result = imagekit.get_remote_file_url_metadata(remote_file_url="remote_file_url")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the file metadata fields
+print(result.width)
+print(result.exif.image.x_resolution)
+```
+
+**25. Create CustomMetaDataFields**
+
+Accepts an option specifying the parameters used to create custom metadata fields. All parameters specified in
+the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/create-custom-metadata-field)
+can be passed as it is with the correct values to get the results.
+
+Check for the [allowed values in the schema](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/create-custom-metadata-field#allowed-values-in-the-schema-object).
+
+**example:**
+
+```python
+# Example for type number
+
+from imagekitio.models.CreateCustomMetadataFieldsRequestOptions import CreateCustomMetadataFieldsRequestOptions
+from imagekitio.models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+from imagekitio.models.CustomMetaDataTypeEnum import CustomMetaDataTypeEnum
+schema = CustomMetadataFieldsSchema(type=CustomMetaDataTypeEnum.Number,
+ min_value=100,
+ max_value=200)
+options = CreateCustomMetadataFieldsRequestOptions(name='test',
+ label='test',
+ schema=schema)
+result = imagekit.create_custom_metadata_fields(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the id of created custom metadata fields
+print(result.id)
+
+# print the schema's type of created custom metadata fields
+print(result.schema.type)
+
+```
+
+```python
+# MultiSelect type Example
+
+from imagekitio.models.CreateCustomMetadataFieldsRequestOptions import CreateCustomMetadataFieldsRequestOptions
+from imagekitio.models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+from imagekitio.models.CustomMetaDataTypeEnum import CustomMetaDataTypeEnum
+
+schema = \
+ CustomMetadataFieldsSchema(type=CustomMetaDataTypeEnum.MultiSelect,
+ is_value_required=True,
+ default_value=['small', 30, True],
+ select_options=[
+ 'small',
+ 'medium',
+ 'large',
+ 30,
+ 40,
+ True,
+ ])
+options = \
+ CreateCustomMetadataFieldsRequestOptions(name='test-MultiSelect',
+ label='test-MultiSelect', schema=schema)
+result = imagekit.create_custom_metadata_fields(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the name of created custom metadata fields
+print(result.name)
+
+# print the schema's select options of created custom metadata fields
+print(result.schema.select_options)
+
+```
+
+```python
+# Date type Example
+
+from imagekitio.models.CreateCustomMetadataFieldsRequestOptions import CreateCustomMetadataFieldsRequestOptions
+from imagekitio.models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+from imagekitio.models.CustomMetaDataTypeEnum import CustomMetaDataTypeEnum
+
+schema = CustomMetadataFieldsSchema(type=CustomMetaDataTypeEnum.Date,
+ min_value='2022-11-29T10:11:10+00:00',
+ max_value='2022-11-30T10:11:10+00:00')
+options = CreateCustomMetadataFieldsRequestOptions(name='test-date',
+ label='test-date',
+ schema=schema)
+result = imagekit.create_custom_metadata_fields(options=options)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the label of created custom metadata fields
+print(result.label)
+
+# print the schema's min value of created custom metadata fields
+print(result.schema.min_value)
+
```
+**26. Get CustomMetaDataFields**
+
+Accepts the `include_deleted` boolean as initial parameter and fetches the metadata as per
+the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/get-custom-metadata-field)
+.
+
+```python
+result = imagekit.get_custom_metadata_fields() # in this case, it will consider includeDeleted as a False
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the first customMetadataField's id
+print(result.list[0].id)
+
+# print the first customMetadataField schema's type
+print(result.list[0].schema.type)
+```
+
+```python
+result = imagekit.get_custom_metadata_fields(include_deleted=True)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the first customMetadataField's name
+print(result.list[0].name)
+
+# print the first customMetadataField schema's default value
+print(result.list[0].schema.default_value)
+```
+
+**27. Update CustomMetaDataFields**
+
+Accepts an `field_id` and options for specifying the parameters to be used to edit custom metadata fields
+as per
+the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/update-custom-metadata-field)
+.
+
+```python
+
+from imagekitio.models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+from imagekitio.models.UpdateCustomMetadataFieldsRequestOptions import UpdateCustomMetadataFieldsRequestOptions
+
+schema = CustomMetadataFieldsSchema(min_value=100, max_value=200)
+options = UpdateCustomMetadataFieldsRequestOptions(
+ label='test-update',
+ schema=schema
+)
+result = imagekit.update_custom_metadata_fields(
+ field_id='id_of_custom_metadata_field',
+ options=options
+)
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+
+# print the label of updated custom metadata fields
+print(result.label)
+
+# print the schema's min value of updated custom metadata fields
+print(result.schema.min_value)
+```
+
+**28. Delete CustomMetaDataFields**
+
+Accepts the id to delete the custom metadata fields as per
+the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/delete-custom-metadata-field)
+.
+
+```python
+result = imagekit.delete_custom_metadata_field(field_id="id_of_custom_metadata_field")
+
+# Final Result
+print(result)
+
+# Raw Response
+print(result.response_metadata.raw)
+```
## Utility functions
@@ -351,17 +1090,16 @@ We have included the following commonly used utility functions in this package.
**Authentication parameter generation**
-In case you are looking to implement client-side file upload, you are going to need a token, expiry timestamp
-, and a valid signature for that upload. The SDK provides a simple method that you can use in your code to generate these
-authentication parameters for you.
+Suppose one wants to implement client-side file upload. In that case, one will need a token, expiry timestamp, and a valid signature for that upload. The SDK provides a simple method that one can use in their code to generate these authentication parameters.
-Note: The Private API Key should never be exposed in any client-side code. You must always generate these authentications parameters on the server-side
+Note: Any client-side code should never expose The Private API Key. One must always generate these authentications parameters on the server-side
authentication
`authentication_parameters = imagekit.get_authentication_parameters(token, expire)`
Returns
+
```python
{
"token": "unique_token",
@@ -370,19 +1108,21 @@ Returns
}
```
-Both the `token` and `expire` parameters are optional. If not specified, the SDK uses the UUID to generate a random token and also generates a valid expiry timestamp internally. The value of the token and expire used to generate the signature are always returned in the response, no matter if they are provided as an input to this method or not.
+Both the `token` and `expire` parameters are optional. If not specified, the SDK uses the UUID to generate a random token and internally generates a valid expiry timestamp. The `token` and `expire` used to generate `signature` is part of a response returned by the server.
-**Distance calculation between two pHash values**
+**Distance calculation between two `pHash` values**
Perceptual hashing allows you to construct a has value that uniquely identifies an input image based on the contents
-of an image. [imagekit.io metadata API](https://docs.imagekit.io/api-reference/metadata-api) returns the pHash
-value of an image in the response. You can use this value to [find a duplicate or similar image](https://docs.imagekit.io/api-reference/metadata-api#using-phash-to-find-similar-or-duplicate-images) by calculating the distance between the two images.
-
+of an image. [imagekit.io metadata API](https://docs.imagekit.io/api-reference/metadata-api) returns the `pHash`
+value of an image in the response. You can use this value
+to [find a duplicate or similar image](https://docs.imagekit.io/api-reference/metadata-api#using-phash-to-find-similar-or-duplicate-images)
+by calculating the distance between the two images.
-This SDK exposes phash_distance function to calculate the distance between two pHash value. It accepts two pHash hexadecimal
-strings and returns a numeric value indicative of the level of difference between the two images.
+This SDK exposes `phash_distance` function to calculate the distance between two `pHash` values. It accepts two `pHash`
+hexadecimal
+strings and returns a numeric value indicative of the difference between the two images.
-```python
+```Python
def calculate_distance():
# fetch metadata of two uploaded image files
...
@@ -396,7 +1136,8 @@ def calculate_distance():
```
**Distance calculation examples**
-```python
+
+```Python
imagekit.phash_distance('f06830ca9f1e3e90', 'f06830ca9f1e3e90')
# output: 0 (ame image)
@@ -407,22 +1148,157 @@ imagekit.phash_distance('a4a65595ac94518b', '7838873e791f8400')
# output: 37 (dissimilar images)
```
+**HTTP response metadata of Internal API**
+
+HTTP response metadata of the internal API call can be accessed using the \_response_metadata on the Result object.
+Example:
+
+```Python
+result = imagekit.upload_file(
+ file="",
+ file_name="my_file_name.jpg",
+)
+
+# Final Result
+print(result)
+print(result.response_metadata.raw)
+print(result.response_metadata.http_status_code)
+print(result.response_metadata.headers)
+```
+
### Sample Code Instruction
-To run `sample` code go to the sample directory and run
-```python
+
+To run `sample` code go to the code samples here are hosted on Github - https://github.com/imagekit-samples/quickstart/tree/master/python and run
+
+```shell
python sample.py
```
+
+## Handling errors
+
+Catch and respond to invalid data, internal problems, and more.
+
+ImageKit Python SDK raises exceptions for many reasons, such as not found, invalid parameters, authentication, and
+internal server errors. Therefore, we recommend writing code that gracefully handles all possible API exceptions.
+
+#### Example:
+
+```Python
+from imagekitio.exceptions.BadRequestException import BadRequestException
+from imagekitio.exceptions.UnauthorizedException import UnauthorizedException
+from imagekitio.exceptions.ForbiddenException import ForbiddenException
+from imagekitio.exceptions.TooManyRequestsException import TooManyRequestsException
+from imagekitio.exceptions.InternalServerException import InternalServerException
+from imagekitio.exceptions.PartialSuccessException import PartialSuccessException
+from imagekitio.exceptions.NotFoundException import NotFoundException
+from imagekitio.exceptions.UnknownException import UnknownException
+
+try:
+
+ # Use ImageKit's SDK to make requests...
+ print('Run image kit api')
+except BadRequestException, e:
+ # Missing or Invalid parameters were supplied to Imagekit.io's API
+ print('Status is: ' + e.response_metadata.http_status_code)
+ print('Message is: ' + e.message)
+ print('Headers are: ' + e.response_metadata.headers)
+ print('Raw body is: ' + e.response_metadata.raw)
+except UnauthorizedException, e:
+ print(e)
+except ForbiddenException, e:
+ # No valid API key was provided.
+ print(e)
+except TooManyRequestsException, e:
+ # Can be for the following reasons:
+ # ImageKit could not authenticate your account with the keys provided.
+ # An expired key (public or private) was used with the request.
+ # The account is disabled.
+ # If you use the upload API, the total storage limit (or upload limit) is exceeded.
+ print(e)
+except InternalServerException, e:
+ # Too many requests made to the API too quickly
+ print(e)
+except PartialSuccessException, e:
+ # Something went wrong with ImageKit.io API.
+ print(e)
+except NotFoundException, e:
+ # Error cases on partial success.
+ print(e)
+except UnknownException, e:
+ # If any of the field or parameter is not found in the data
+ print(e)
+
+# Something else happened, which can be unrelated to ImageKit; the reason will be indicated in the message field
+```
+
+## Development
+
+### Tests
+
+Tests are powered by [Tox](https://tox.wiki/en/latest/).
+
+```bash
+$ git clone https://github.com/imagekit-developer/imagekit-python && cd imagekit-python
+$ pip install tox
+$ tox
+```
+
+### Sample
+
+#### Get & Install local ImageKit Python SDK
+
+```bash
+$ git clone https://github.com/imagekit-developer/imagekit-python && cd imagekit-python
+$ pip install -e .
+```
+
+#### Get samples
+
+To integrate ImageKit Samples in the Python, the code samples covered here are hosted on Github - https://github.com/imagekit-samples/quickstart/tree/master/python.
+
+Open `python/sample.py` file and replace placeholder credentials with actual values. You can get the value of [URL-endpoint](https://imagekit.io/dashboard#url-endpoints) from your ImageKit dashboard. API keys can be obtained from the [developer](https://imagekit.io/dashboard/developer/api-keys) section in your ImageKit dashboard.
+
+In `python/sample.py` file, set the following parameters for authentication:
+
+```python
+from imagekitio import ImageKit
+imagekit = ImageKit(
+ private_key='your private_key',
+ public_key='your public_key',
+ url_endpoint = 'your url_endpoint'
+)
+```
+
+To install dependencies that are in the `python/requirements.txt` file can fire this command to install them:
+
+```shell
+pip install -r python/requirements.txt
+```
+
+Now run `python/sample.py`. If you are using CLI Tool (Terminal/Command prompt), open the project in CLI and execute it.
+
+```shell
+# if not installed already
+pip install imagekitio
+
+# if installing local sdk
+pip install -e
+
+# to run sample.py file
+python3 python/sample.py
+```
+
## Support
-For any feedback or to report any issues or general implementation support, please reach out to [support@imagekit.io]()
+For any feedback or to report any issues or general implementation support, please reach out
+to [support@imagekit.io](https://github.com/imagekit-developer/imagekit-python)
## Links
-* [Documentation](https://docs.imagekit.io/)
-
-* [Main Website](https://imagekit.io/)
+- [Documentation](https://docs.imagekit.io/)
+- [Main Website](https://imagekit.io/)
## License
-Released under the MIT license.
+Released under the MIT license.
diff --git a/imagekitio/client.py b/imagekitio/client.py
index 8e98419..c11a8df 100644
--- a/imagekitio/client.py
+++ b/imagekitio/client.py
@@ -2,6 +2,42 @@
from .constants.errors import ERRORS
from .file import File
+from .models.CopyFileRequestOptions import CopyFileRequestOptions
+from .models.CopyFolderRequestOptions import CopyFolderRequestOptions
+from .models.CreateCustomMetadataFieldsRequestOptions import (
+ CreateCustomMetadataFieldsRequestOptions,
+)
+from .models.CreateFolderRequestOptions import CreateFolderRequestOptions
+from .models.DeleteFolderRequestOptions import DeleteFolderRequestOptions
+from .models.ListAndSearchFileRequestOptions import ListAndSearchFileRequestOptions
+from .models.MoveFileRequestOptions import MoveFileRequestOptions
+from .models.MoveFolderRequestOptions import MoveFolderRequestOptions
+from .models.RenameFileRequestOptions import RenameFileRequestOptions
+from .models.UpdateCustomMetadataFieldsRequestOptions import (
+ UpdateCustomMetadataFieldsRequestOptions,
+)
+from .models.UpdateFileRequestOptions import UpdateFileRequestOptions
+from .models.UploadFileRequestOptions import UploadFileRequestOptions
+from .models.results.BulkDeleteFileResult import BulkDeleteFileResult
+from .models.results.CustomMetadataFieldsResultWithResponseMetadata import (
+ CustomMetadataFieldsResultWithResponseMetadata,
+)
+from .models.results.FileResultWithResponseMetadata import (
+ FileResultWithResponseMetadata,
+)
+from .models.results.FolderResult import FolderResult
+from .models.results.GetBulkJobStatusResult import GetBulkJobStatusResult
+from .models.results.GetMetadataResult import GetMetadataResult
+from .models.results.ListCustomMetadataFieldsResult import (
+ ListCustomMetadataFieldsResult,
+)
+from .models.results.ListFileResult import ListFileResult
+from .models.results.PurgeCacheResult import PurgeCacheResult
+from .models.results.PurgeCacheStatusResult import PurgeCacheStatusResult
+from .models.results.RenameFileResult import RenameFileResult
+from .models.results.ResponseMetadataResult import ResponseMetadataResult
+from .models.results.TagsResult import TagsResult
+from .models.results.UploadFileResult import UploadFileResult
from .resource import ImageKitRequest
from .url import Url
from .utils.calculation import get_authenticated_params, hamming_distance
@@ -27,96 +63,190 @@ def __init__(
self.file = File(self.ik_request)
self.url_obj = Url(self.ik_request)
- def upload(self, file=None, file_name=None, options=None) -> Dict[str, Any]:
- """Provides upload functionality
- """
+ def upload(self, file=None, file_name=None, options=None) -> UploadFileResult:
+ """Provides upload functionality"""
return self.file.upload(file, file_name, options)
- def upload_file(self, file=None, file_name=None, options=None) -> Dict[str, Any]:
- """Provides upload functionality
- """
- return self.file.upload(file, file_name, options)
+ def upload_file(
+ self, file=None, file_name=None, options: UploadFileRequestOptions = None
+ ) -> UploadFileResult:
+ """Provides upload functionality"""
+ return self.file.upload(
+ file, file_name, options if options is not None else None
+ )
- def list_files(self, options: Dict) -> Dict:
- """Get list(filtered if given param) of images of client
- """
+ def list_files(
+ self, options: ListAndSearchFileRequestOptions = None
+ ) -> ListFileResult:
+ """Get list(filtered if given param) of images of client"""
return self.file.list(options)
- def get_file_details(self, file_identifier: str = None) -> Dict:
- """Get file_detail by file_id or file_url
- """
- return self.file.details(file_identifier)
+ def get_file_details(self, file_id: str = None) -> FileResultWithResponseMetadata:
+ """Get file_detail by file_id or file_url"""
+ return self.file.details(file_id)
+
+ def get_file_versions(self, file_id: str = None) -> ListFileResult:
+ """Get file_version by file_id or file_url"""
+ return self.file.get_file_versions(file_id)
- def update_file_details(self, file_id: str, options: dict = None) -> Dict:
- """Update file detail by file id and options
- """
+ def get_file_version_details(
+ self, file_id: str = None, version_id: str = None
+ ) -> FileResultWithResponseMetadata:
+ """Get file_version details by file_id and version_id"""
+ return self.file.get_file_version_details(file_id, version_id)
+
+ def update_file_details(
+ self, file_id: str, options: UpdateFileRequestOptions = None
+ ) -> FileResultWithResponseMetadata:
+ """Update file details by file id and options"""
return self.file.update_file_details(file_id, options)
- def delete_file(self, file_id: str = None) -> Dict[str, Any]:
- """Delete file by file_id
- """
+ def add_tags(self, file_ids, tags) -> TagsResult:
+ """Add tags by file ids and tags"""
+ return self.file.manage_tags(file_ids, tags, "addTags")
+
+ def remove_tags(self, file_ids, tags) -> TagsResult:
+ """Remove tags by file ids and tags"""
+ return self.file.manage_tags(file_ids, tags, "removeTags")
+
+ def remove_ai_tags(self, file_ids, ai_tags) -> TagsResult:
+ """Remove AI tags by file ids and AI tags"""
+ return self.file.remove_ai_tags(file_ids, ai_tags)
+
+ def delete_file(self, file_id: str = None) -> ResponseMetadataResult:
+ """Delete file by file_id"""
return self.file.delete(file_id)
- def bulk_delete(self, file_ids: list = None):
- """Delete files in bulk by provided list of ids
- """
+ def delete_file_version(self, file_id, version_id) -> ResponseMetadataResult:
+ """Delete file version by provided file id and version id"""
+ return self.file.delete_file_version(file_id, version_id)
+
+ def bulk_delete(self, file_ids: list = None) -> BulkDeleteFileResult:
+ """Delete files in bulk by provided list of file ids"""
return self.file.batch_delete(file_ids)
- def bulk_file_delete(self, file_ids: list = None):
- """Delete files in bulk by provided list of ids
- """
+ def bulk_file_delete(self, file_ids: list = None) -> BulkDeleteFileResult:
+ """Delete files in bulk by provided list of file ids"""
return self.file.batch_delete(file_ids)
- def purge_cache(self, file_url: str = None) -> Dict[str, Any]:
- """Purge Cache from server by file url
- """
+ def copy_file(
+ self, options: CopyFileRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Copy file by provided sourceFilePath, destinationPath and includeFileVersions as an options"""
+ return self.file.copy_file(options)
+
+ def move_file(
+ self, options: MoveFileRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Move file by provided sourceFilePath and destinationPath as an options"""
+ return self.file.move_file(options)
+
+ def rename_file(self, options: RenameFileRequestOptions = None) -> RenameFileResult:
+ """Rename file by provided filePath, newFileName and purgeCache as an options"""
+ return self.file.rename_file(options)
+
+ def restore_file_version(
+ self, file_id, version_id
+ ) -> FileResultWithResponseMetadata:
+ """Restore file version by provided file id and version id"""
+ return self.file.restore_file_version(file_id, version_id)
+
+ def create_folder(
+ self, options: CreateFolderRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Create folder by provided folderName and parentFolderPath as an options"""
+ return self.file.create_folder(options)
+
+ def delete_folder(
+ self, options: DeleteFolderRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Delete folder by provided folderPath as an options"""
+ return self.file.delete_folder(options)
+
+ def copy_folder(self, options: CopyFolderRequestOptions = None) -> FolderResult:
+ """Copy folder by provided sourceFolderPath, destinationPath and includeFileVersions as an options"""
+ return self.file.copy_folder(options)
+
+ def move_folder(self, options: MoveFolderRequestOptions = None) -> FolderResult:
+ """Move folder by provided sourceFolderPath and destinationPath as an options"""
+ return self.file.move_folder(options)
+
+ def get_bulk_job_status(self, job_id) -> GetBulkJobStatusResult:
+ """Get bulk job status by provided only jobId"""
+ return self.file.get_bulk_job_status(job_id)
+
+ def purge_cache(self, file_url: str = None) -> PurgeCacheResult:
+ """Purge Cache from server by file url"""
return self.file.purge_cache(file_url)
- def purge_file_cache(self, file_url: str = None) -> Dict[str, Any]:
- """Purge Cache from server by file url
- """
+ def purge_file_cache(self, file_url: str = None) -> PurgeCacheResult:
+ """Purge Cache from server by file url"""
return self.file.purge_cache(file_url)
- def get_purge_cache_status(self, purge_cache_id: str = "") -> Dict[str, Any]:
- """Get Purge Cache status by purge cache request_id
- """
+ def get_purge_cache_status(
+ self, purge_cache_id: str = ""
+ ) -> PurgeCacheStatusResult:
+ """Get Purge Cache status by purge cache request_id"""
return self.file.get_purge_cache_status(str(purge_cache_id))
- def get_purge_file_cache_status(self, purge_cache_id: str = "") -> Dict[str, Any]:
- """Get Purge Cache status by purge cache request_id
- """
+ def get_purge_file_cache_status(
+ self, purge_cache_id: str = ""
+ ) -> PurgeCacheStatusResult:
+ """Get Purge Cache status by purge cache request_id"""
return self.file.get_purge_cache_status(str(purge_cache_id))
- def get_metadata(self, file_id: str = None) -> Dict[str, Any]:
- """Get Meta Data of a file by file id
- """
+ def get_metadata(self, file_id: str = None) -> GetMetadataResult:
+ """Get Meta Data of a file by file id"""
return self.file.get_metadata(str(file_id))
- def get_file_metadata(self, file_id: str = None) -> Dict[str, Any]:
- """Get Meta Data of a file by file id
- """
+ def get_file_metadata(self, file_id: str = None) -> GetMetadataResult:
+ """Get Meta Data of a file by file id"""
return self.file.get_metadata(str(file_id))
- def get_remote_url_metadata(self, remote_file_url: str = ""):
+ def get_remote_url_metadata(self, remote_file_url: str = "") -> GetMetadataResult:
return self.file.get_metadata_from_remote_url(remote_file_url)
- def get_remote_file_url_metadata(self, remote_file_url: str = ""):
+ def get_remote_file_url_metadata(
+ self, remote_file_url: str = ""
+ ) -> GetMetadataResult:
+ """Get remote metadata by provided remote_file_url"""
return self.file.get_metadata_from_remote_url(remote_file_url)
+ def create_custom_metadata_fields(
+ self, options: CreateCustomMetadataFieldsRequestOptions = None
+ ) -> CustomMetadataFieldsResultWithResponseMetadata:
+ """creates custom metadata fields by passing name, label and schema as an options"""
+ return self.file.create_custom_metadata_fields(options)
+
+ def get_custom_metadata_fields(
+ self, include_deleted: bool = False
+ ) -> ListCustomMetadataFieldsResult:
+ """get custom metadata fields"""
+ return self.file.get_custom_metadata_fields(include_deleted)
+
+ def update_custom_metadata_fields(
+ self, field_id, options: UpdateCustomMetadataFieldsRequestOptions = None
+ ) -> CustomMetadataFieldsResultWithResponseMetadata:
+ """updates custom metadata fields by passing id of custom metadata field and params as an options"""
+ return self.file.update_custom_metadata_fields(field_id, options)
+
+ def delete_custom_metadata_field(
+ self, field_id: str = ""
+ ) -> ResponseMetadataResult:
+ """Deletes custom metadata fields by passing field_id"""
+ return self.file.delete_custom_metadata_field(field_id)
+
def url(self, options: Dict[str, Any]) -> str:
- """Get generated Url from options parameter
- """
+ """Get generated Url from options parameter"""
return self.url_obj.generate_url(options)
@staticmethod
def phash_distance(first, second):
- """Get hamming distance between two phash(to check similarity)
- """
+ """Get hamming distance between two phash(to check similarity)"""
if not (first and second):
raise TypeError(ERRORS.MISSING_PHASH_VALUE.value)
return hamming_distance(first, second)
def get_authentication_parameters(self, token="", expire=0):
- """Get Authentication parameters
- """
+ """Get Authentication parameters"""
return get_authenticated_params(token, expire, self.ik_request.private_key)
diff --git a/imagekitio/constants/__init__.py b/imagekitio/constants/__init__.py
index e120938..e69de29 100644
--- a/imagekitio/constants/__init__.py
+++ b/imagekitio/constants/__init__.py
@@ -1 +0,0 @@
-from .errors import ERRORS
diff --git a/imagekitio/constants/defaults.py b/imagekitio/constants/defaults.py
index f0fed92..9406acd 100644
--- a/imagekitio/constants/defaults.py
+++ b/imagekitio/constants/defaults.py
@@ -9,8 +9,7 @@ class Default(enum.Enum):
QUERY_TRANSFORMATION_POSITION,
]
DEFAULT_TIMESTAMP = 9999999999
- SDK_VERSION_PARAMETER = "ik-sdk-version"
- SDK_VERSION = "python-2.2.8"
+ SDK_VERSION = "python-3.0.0"
TRANSFORMATION_PARAMETER = "tr"
CHAIN_TRANSFORM_DELIMITER = ":"
TRANSFORM_DELIMITER = ","
diff --git a/imagekitio/constants/errors.py b/imagekitio/constants/errors.py
index 1524a09..6d0e949 100644
--- a/imagekitio/constants/errors.py
+++ b/imagekitio/constants/errors.py
@@ -28,7 +28,7 @@ class ERRORS(enum.Enum):
}
FILE_ID_MISSING = {
"message": "Missing File ID parameter for this request",
- help: "",
+ "help": "",
}
UPDATE_DATA_MISSING = {
"message": "Missing file update data for this request",
@@ -53,13 +53,12 @@ class ERRORS(enum.Enum):
MISSING_UPLOAD_DATA = {"message": "Missing data for upload", help: ""}
MISSING_UPLOAD_FILE_PARAMETER = {
"message": "Missing file parameter for upload",
- help: "",
+ "help": "",
}
MISSING_UPLOAD_FILENAME_PARAMETER = {
"message": "Missing fileName parameter for upload",
- help: "",
+ "help": "",
}
-
INVALID_PHASH_VALUE = (
{
"message": "Invalid pHash value",
@@ -74,3 +73,11 @@ class ERRORS(enum.Enum):
"message": "Unequal pHash string length",
help: "For distance calculation, the two pHash strings must have equal length",
}
+ VERSION_ID_MISSING = {
+ "message": "Missing Version ID parameter for this request",
+ "help": "",
+ }
+ MISSING_CUSTOM_METADATA_FIELD_ID = {
+ "message": "Missing field_id for update_custom_metadata_fields",
+ "help": "",
+ }
diff --git a/imagekitio/constants/files.py b/imagekitio/constants/files.py
index 94d115b..755e885 100644
--- a/imagekitio/constants/files.py
+++ b/imagekitio/constants/files.py
@@ -1,11 +1,13 @@
VALID_FILE_OPTIONS = [
+ "type",
+ "sort",
"path",
+ "searchQuery",
"fileType",
- "tags",
- "includeFolder",
- "name",
"limit",
"skip",
+ "tags",
+ "includeFolder",
]
VALID_FILE_DETAIL_OPTIONS = ["fileID"]
@@ -19,5 +21,12 @@
"is_private_file",
"custom_coordinates",
"response_fields",
- "metadata",
+ "extensions",
+ "webhook_url",
+ "overwrite_file",
+ "overwrite_ai_tags",
+ "overwrite_tags",
+ "overwrite_custom_metadata",
+ "custom_metadata",
+ "embedded_metadata",
]
diff --git a/imagekitio/constants/supported_transform.py b/imagekitio/constants/supported_transform.py
index 5f99402..c85aab6 100644
--- a/imagekitio/constants/supported_transform.py
+++ b/imagekitio/constants/supported_transform.py
@@ -15,19 +15,20 @@
"rotation": "rt",
"blur": "bl",
"named": "n",
+ "overlay_x": "ox",
+ "overlay_y": "oy",
+ "overlay_focus": "ofo",
+ "overlay_height": "oh",
+ "overlay_width": "ow",
"overlay_image": "oi",
+ "overlay_image_trim": "oit",
"overlay_image_aspect_ratio": "oiar",
"overlay_image_background": "oibg",
"overlay_image_border": "oib",
"overlay_image_dpr": "oidpr",
"overlay_image_quality": "oiq",
"overlay_image_cropping": "oic",
- "overlay_image_trim": "oit",
- "overlay_x": "ox",
- "overlay_y": "oy",
- "overlay_focus": "ofo",
- "overlay_height": "oh",
- "overlay_width": "ow",
+ "overlay_image_focus": "oifo",
"overlay_text": "ot",
"overlay_text_font_size": "ots",
"overlay_text_font_family": "otf",
@@ -36,7 +37,6 @@
"overlay_alpha": "oa",
"overlay_text_typography": "ott",
"overlay_background": "obg",
- "overlay_image_trim": "oit",
"overlay_text_encoded": "ote",
"overlay_text_width": "otw",
"overlay_text_background": "otbg",
@@ -55,4 +55,5 @@
"effect_contrast": "e-contrast",
"effect_gray": "e-grayscale",
"original": "orig",
+ "raw": "raw",
}
diff --git a/imagekitio/constants/url.py b/imagekitio/constants/url.py
index 932df02..f1fe624 100644
--- a/imagekitio/constants/url.py
+++ b/imagekitio/constants/url.py
@@ -1,9 +1,4 @@
-from enum import Enum
-
-
-class URL(Enum):
- BASE_URL = "https://api.imagekit.io/v1/files"
- PURGE_CACHE = "/purge"
- UPLOAD_URL = "https://upload.imagekit.io/api/v1/files/upload"
+class URL:
+ API_BASE_URL = "https://api.imagekit.io"
+ UPLOAD_BASE_URL = "https://upload.imagekit.io"
BULK_FILE_DELETE = "/batch/deleteByFileIds"
- REMOTE_METADATA_FULL_URL = "https://api.imagekit.io/v1/metadata"
diff --git a/imagekitio/exceptions/BadRequestException.py b/imagekitio/exceptions/BadRequestException.py
new file mode 100644
index 0000000..df61561
--- /dev/null
+++ b/imagekitio/exceptions/BadRequestException.py
@@ -0,0 +1,16 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class BadRequestException(Exception):
+ def __init__(
+ self,
+ message,
+ response_help,
+ response_metadata: ResponseMetadata = ResponseMetadata(None, None, None),
+ ):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/ConflictException.py b/imagekitio/exceptions/ConflictException.py
new file mode 100644
index 0000000..4d1fb93
--- /dev/null
+++ b/imagekitio/exceptions/ConflictException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class ConflictException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/ForbiddenException.py b/imagekitio/exceptions/ForbiddenException.py
new file mode 100644
index 0000000..6f8bc45
--- /dev/null
+++ b/imagekitio/exceptions/ForbiddenException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class ForbiddenException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/InternalServerException.py b/imagekitio/exceptions/InternalServerException.py
new file mode 100644
index 0000000..e841083
--- /dev/null
+++ b/imagekitio/exceptions/InternalServerException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class InternalServerException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/NotFoundException.py b/imagekitio/exceptions/NotFoundException.py
new file mode 100644
index 0000000..0a37f52
--- /dev/null
+++ b/imagekitio/exceptions/NotFoundException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class NotFoundException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/PartialSuccessException.py b/imagekitio/exceptions/PartialSuccessException.py
new file mode 100644
index 0000000..7ecf4c5
--- /dev/null
+++ b/imagekitio/exceptions/PartialSuccessException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class PartialSuccessException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/TooManyRequestsException.py b/imagekitio/exceptions/TooManyRequestsException.py
new file mode 100644
index 0000000..3405a76
--- /dev/null
+++ b/imagekitio/exceptions/TooManyRequestsException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class TooManyRequestsException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/UnauthorizedException.py b/imagekitio/exceptions/UnauthorizedException.py
new file mode 100644
index 0000000..66fd8f5
--- /dev/null
+++ b/imagekitio/exceptions/UnauthorizedException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class UnauthorizedException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/imagekitio/exceptions/UnknownException.py b/imagekitio/exceptions/UnknownException.py
new file mode 100644
index 0000000..ff507a2
--- /dev/null
+++ b/imagekitio/exceptions/UnknownException.py
@@ -0,0 +1,11 @@
+from ..models.results.ResponseMetadata import ResponseMetadata
+
+
+class UnknownException(Exception):
+ def __init__(self, message, response_help, response_metadata: ResponseMetadata):
+ self.message = message
+ self.response_help = response_help
+ self.response_metadata = response_metadata
+
+ def __str__(self):
+ return str(self.message)
diff --git a/sample/__init__.py b/imagekitio/exceptions/__init__.py
similarity index 100%
rename from sample/__init__.py
rename to imagekitio/exceptions/__init__.py
diff --git a/imagekitio/file.py b/imagekitio/file.py
index a18f1bb..8a7741b 100644
--- a/imagekitio/file.py
+++ b/imagekitio/file.py
@@ -1,255 +1,782 @@
+import ast
from json import dumps
from typing import Any, Dict
+from requests_toolbelt import MultipartEncoder
+
from .constants.errors import ERRORS
from .constants.files import VALID_FILE_OPTIONS, VALID_UPLOAD_OPTIONS
from .constants.url import URL
+from .exceptions.BadRequestException import BadRequestException
+from .exceptions.ConflictException import ConflictException
+from .exceptions.NotFoundException import NotFoundException
+from .exceptions.UnknownException import UnknownException
+from .models.CopyFileRequestOptions import CopyFileRequestOptions
+from .models.CopyFolderRequestOptions import CopyFolderRequestOptions
+from .models.CreateCustomMetadataFieldsRequestOptions import (
+ CreateCustomMetadataFieldsRequestOptions,
+)
+from .models.CreateFolderRequestOptions import CreateFolderRequestOptions
+from .models.DeleteFolderRequestOptions import DeleteFolderRequestOptions
+from .models.ListAndSearchFileRequestOptions import ListAndSearchFileRequestOptions
+from .models.MoveFileRequestOptions import MoveFileRequestOptions
+from .models.MoveFolderRequestOptions import MoveFolderRequestOptions
+from .models.RenameFileRequestOptions import RenameFileRequestOptions
+from .models.UpdateCustomMetadataFieldsRequestOptions import (
+ UpdateCustomMetadataFieldsRequestOptions,
+)
+from .models.UpdateFileRequestOptions import UpdateFileRequestOptions
+from .models.UploadFileRequestOptions import UploadFileRequestOptions
+from .models.results.BulkDeleteFileResult import BulkDeleteFileResult
+from .models.results.CustomMetadataFieldsResult import CustomMetadataFieldsResult
+from .models.results.CustomMetadataFieldsResultWithResponseMetadata import (
+ CustomMetadataFieldsResultWithResponseMetadata,
+)
+from .models.results.FileResult import FileResult
+from .models.results.FileResultWithResponseMetadata import (
+ FileResultWithResponseMetadata,
+)
+from .models.results.FolderResult import FolderResult
+from .models.results.GetBulkJobStatusResult import GetBulkJobStatusResult
+from .models.results.GetMetadataResult import GetMetadataResult
+from .models.results.ListCustomMetadataFieldsResult import (
+ ListCustomMetadataFieldsResult,
+)
+from .models.results.ListFileResult import ListFileResult
+from .models.results.PurgeCacheResult import PurgeCacheResult
+from .models.results.PurgeCacheStatusResult import PurgeCacheStatusResult
+from .models.results.RenameFileResult import RenameFileResult
+from .models.results.ResponseMetadataResult import ResponseMetadataResult
+from .models.results.TagsResult import TagsResult
+from .models.results.UploadFileResult import UploadFileResult
from .utils.formatter import (
- camel_dict_to_snake_dict,
request_formatter,
snake_to_lower_camel,
)
+from .utils.utils import (
+ general_api_throw_exception,
+ get_response_json,
+ populate_response_metadata,
+ convert_to_response_object,
+ convert_to_list_response_object,
+ throw_other_exception,
+ convert_to_response_metadata_result_object,
+)
-try:
- from simplejson.errors import JSONDecodeError
-except ImportError:
- from json import JSONDecodeError
class File(object):
def __init__(self, request_obj):
self.request = request_obj
- def upload(self, file, file_name, options) -> Dict:
+ def upload(
+ self, file, file_name, options: UploadFileRequestOptions = None
+ ) -> UploadFileResult:
"""Upload file to server using local image or url
:param file: either local file path or network file path
:param file_name: intended file name
:param options: intended options
- :return: json response from server
+ :return: UploadFileResult
"""
if not file:
raise TypeError(ERRORS.MISSING_UPLOAD_FILE_PARAMETER.value)
if not file_name:
raise TypeError(ERRORS.MISSING_UPLOAD_FILENAME_PARAMETER.value)
- url = URL.UPLOAD_URL.value
+ url = "%s%s" % (URL.UPLOAD_BASE_URL, "/api/v1/files/upload")
headers = self.request.create_headers()
-
files = {
"file": file,
- "fileName": (None, file_name),
+ "fileName": file_name,
}
-
if not options:
options = dict()
else:
- options = self.validate_upload(options)
+ options = self.validate_upload(options.__dict__)
if options is False:
raise ValueError("Invalid upload options")
if isinstance(file, str) or isinstance(file, bytes):
files.update({"file": (None, file)})
+ if "overwriteAiTags" in options:
+ options["overwriteAITags"] = options["overwriteAiTags"]
+ del options["overwriteAiTags"]
+ all_fields = {**files, **options}
+ multipart_data = MultipartEncoder(
+ fields=all_fields, boundary="--randomBoundary---------------------"
+ )
+ headers.update({"Content-Type": multipart_data.content_type})
resp = self.request.request(
- "Post", url=url, files=files, data=options, headers=headers
+ "Post", url=url, data=multipart_data.read(), headers=headers
)
-
- if resp.status_code > 200:
- try:
- error = resp.json()
- except JSONDecodeError:
- error = resp.text
- response = None
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, UploadFileResult)
+ return response
else:
- error = None
- response = resp.json()
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def list(self, options: dict) -> Dict:
+ def list(self, options: ListAndSearchFileRequestOptions = None) -> ListFileResult:
"""Returns list files on ImageKit Server
:param: options dictionary of options
- :return: list of the response
+ :return: ListFileResult
"""
-
- formatted_options = request_formatter(options)
- if not self.is_valid_list_options(formatted_options):
- raise ValueError("Invalid option for list_files")
- url = URL.BASE_URL.value
+ if options is not None:
+ if "tags" in options.__dict__ and isinstance(options.tags, list):
+ val = ", ".join(options.tags)
+ if val:
+ options.tags = val
+ formatted_options = request_formatter(options.__dict__)
+ if not self.is_valid_list_options(formatted_options):
+ raise ValueError("Invalid option for list_files")
+ else:
+ formatted_options = dict()
+ url = "{}/v1/files".format(URL.API_BASE_URL)
headers = self.request.create_headers()
+ resp = self.request.request(
+ method="GET", url=url, headers=headers, params=dumps(formatted_options)
+ )
+ if resp.status_code == 200:
+ response = convert_to_list_response_object(resp, FileResult, ListFileResult)
+ return response
+ else:
+ general_api_throw_exception(resp)
+ def details(self, file_id: str = None) -> FileResultWithResponseMetadata:
+ """returns file detail"""
+ if not file_id:
+ raise TypeError(ERRORS.FILE_ID_MISSING.value)
+ url = "{}/v1/files/{}/details".format(URL.API_BASE_URL, file_id)
resp = self.request.request(
- method="GET", url=url, headers=headers, params=options
+ method="GET",
+ url=url,
+ headers=self.request.create_headers(),
)
- if resp.status_code > 200:
- error = resp.json()
- response = None
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, FileResultWithResponseMetadata)
+ return response
else:
- error = None
- response = resp.json()
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def details(self, file_identifier: str = None) -> Dict:
- """returns file detail
- """
- if not file_identifier:
+ def get_file_versions(self, file_id: str = None) -> ListFileResult:
+ """returns file versions"""
+ if not file_id:
+ raise TypeError(ERRORS.FILE_ID_MISSING.value)
+ url = "{}/v1/files/{}/versions".format(URL.API_BASE_URL, file_id)
+ resp = self.request.request(
+ method="GET",
+ url=url,
+ headers=self.request.create_headers(),
+ )
+ if resp.status_code == 200:
+ response = convert_to_list_response_object(resp, FileResult, ListFileResult)
+ return response
+ elif resp.status_code == 404:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
+
+ def get_file_version_details(
+ self, file_id: str = None, version_id: str = None
+ ) -> FileResultWithResponseMetadata:
+ """returns file version detail"""
+ if not file_id:
raise TypeError(ERRORS.FILE_ID_MISSING.value)
- url = "{}/{}/details".format(URL.BASE_URL.value, file_identifier)
+ if not version_id:
+ raise TypeError(ERRORS.VERSION_ID_MISSING.value)
+ url = "{}/v1/files/{}/versions/{}".format(URL.API_BASE_URL, file_id, version_id)
resp = self.request.request(
- method="GET", url=url, headers=self.request.create_headers(),
+ method="GET",
+ url=url,
+ headers=self.request.create_headers(),
)
- if resp.status_code > 200:
- error = resp.json()
- response = None
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, FileResultWithResponseMetadata)
+ return response
+ elif resp.status_code == 404:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
else:
- error = None
- response = resp.json()
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def update_file_details(self, file_id: str, options: dict):
+ def update_file_details(
+ self, file_id: str, options: UpdateFileRequestOptions = None
+ ) -> FileResultWithResponseMetadata:
"""Update detail of a file(like tags, coordinates)
update details identified by file_id and options,
which is already uploaded
"""
if not file_id:
raise TypeError(ERRORS.FILE_ID_MISSING.value)
- url = "{}/{}/details/".format(URL.BASE_URL.value, file_id)
+ url = "{}/v1/files/{}/details/".format(URL.API_BASE_URL, file_id)
headers = {"Content-Type": "application/json"}
headers.update(self.request.get_auth_headers())
- data = dumps(request_formatter(options))
+ formatted_options = request_formatter(options.__dict__)
+ if "removeAiTags" in formatted_options:
+ remove_ai_tags_dict = {"removeAITags": formatted_options["removeAiTags"]}
+ del formatted_options["removeAiTags"]
+ request_data = {**remove_ai_tags_dict, **formatted_options}
+ else:
+ request_data = formatted_options
+ data = dumps(request_data) if options is not None else dict()
resp = self.request.request(method="Patch", url=url, headers=headers, data=data)
- if resp.status_code > 200:
- error = resp.json()
- response = None
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, FileResultWithResponseMetadata)
+ return response
+ else:
+ general_api_throw_exception(resp)
+
+ def manage_tags(self, file_ids, tags, action) -> TagsResult:
+ """Add or Remove tags of files
+ :param file_ids: array of file ids
+ :param tags: array of tags
+ :param action: to identify call either for removeTags or addTags
+ """
+ url = (
+ "{}/v1/files/removeTags".format(URL.API_BASE_URL)
+ if action == "removeTags"
+ else "{}/v1/files/addTags".format(URL.API_BASE_URL)
+ )
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.get_auth_headers())
+ data = dumps({"fileIds": file_ids, "tags": tags})
+ resp = self.request.request(method="Post", url=url, headers=headers, data=data)
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, TagsResult)
+ return response
+ elif resp.status_code == 207 or resp.status_code == 404:
+ throw_other_exception(resp)
+ else:
+ general_api_throw_exception(resp)
+
+ def remove_ai_tags(self, file_ids, ai_tags) -> TagsResult:
+ """Remove AI tags of files
+ :param file_ids: array of file ids
+ :param ai_tags: array of AI tags
+ """
+ url = "{}/v1/files/removeAITags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.get_auth_headers())
+ data = dumps({"fileIds": file_ids, "AITags": ai_tags})
+ resp = self.request.request(method="Post", url=url, headers=headers, data=data)
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, TagsResult)
+ return response
+ elif resp.status_code == 207 or resp.status_code == 404:
+ throw_other_exception(resp)
else:
- error = None
- response = resp.json()
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def delete(self, file_id: str = None) -> Dict:
+ def delete(self, file_id: str = None) -> ResponseMetadataResult:
"""Delete file by file_id
deletes file from imagekit server
"""
if not file_id:
raise TypeError(ERRORS.FILE_ID_MISSING.value)
- url = "{}/{}".format(URL.BASE_URL.value, file_id)
+ url = "{}/v1/files/{}".format(URL.API_BASE_URL, file_id)
resp = self.request.request(
method="Delete", url=url, headers=self.request.create_headers()
)
- if resp.status_code > 204:
- error = resp.text
- response = None
+ if resp.status_code == 204:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
else:
- error = None
- response = None
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def batch_delete(self, file_ids: list = None):
+ def delete_file_version(self, file_id, version_id) -> ResponseMetadataResult:
+ """Delete file version by file_id and version_id"""
+ url = "{}/v1/files/{}/versions/{}".format(URL.API_BASE_URL, file_id, version_id)
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ resp = self.request.request(method="Delete", url=url, headers=headers)
+ if resp.status_code == 204:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
+ elif resp.status_code == 400 or resp.status_code == 404:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ if resp.status_code == 400:
+ raise BadRequestException(
+ error_message, response_help, response_meta_data
+ )
+ elif resp.status_code == 404:
+ raise NotFoundException(
+ error_message, response_help, response_meta_data
+ )
+ else:
+ general_api_throw_exception(resp)
+
+ def batch_delete(self, file_ids: list = None) -> BulkDeleteFileResult:
"""Delete bulk files
Delete files by batch ids
"""
if not file_ids:
raise ValueError("Need to pass ids in list")
- url = URL.BASE_URL.value + URL.BULK_FILE_DELETE.value
+ url = URL.API_BASE_URL + "/v1/files" + URL.BULK_FILE_DELETE
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ data = dumps({"fileIds": file_ids})
+ resp = self.request.request(method="POST", url=url, headers=headers, data=data)
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, BulkDeleteFileResult)
+ return response
+ elif resp.status_code == 207 or resp.status_code == 404:
+ throw_other_exception(resp)
+ else:
+ general_api_throw_exception(resp)
+
+ def copy_file(
+ self, options: CopyFileRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Copy file by provided sourceFilePath, destinationPath and includeFileVersions as an options"""
+ url = "{}/v1/files/copy".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ formatted_options = (
+ dumps(request_formatter(options.__dict__))
+ if options is not None
+ else dict()
+ )
+ resp = self.request.request(
+ method="Post", url=url, headers=headers, data=formatted_options
+ )
+ if resp.status_code == 204:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
+ elif resp.status_code == 404:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
+
+ def move_file(
+ self, options: MoveFileRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Move file by provided sourceFilePath and destinationPath as an options"""
+ url = "{}/v1/files/move".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ formatted_options = (
+ dumps(request_formatter(options.__dict__))
+ if options is not None
+ else dict()
+ )
+ resp = self.request.request(
+ method="Post", url=url, headers=headers, data=formatted_options
+ )
+ if resp.status_code == 204:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
+ elif resp.status_code == 404:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
+
+ def rename_file(self, options: RenameFileRequestOptions = None) -> RenameFileResult:
+ """Rename file by provided filePath, newFileName and purgeCache as an options"""
+ url = "{}/v1/files/rename".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ formatted_options = (
+ dumps(request_formatter(options.__dict__))
+ if options is not None
+ else dict()
+ )
resp = self.request.request(
- method="POST",
+ method="Put", url=url, headers=headers, data=formatted_options
+ )
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, RenameFileResult)
+ return response
+ elif resp.status_code == 207 or resp.status_code == 404:
+ throw_other_exception(resp)
+ elif resp.status_code == 409:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ raise ConflictException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
+
+ def restore_file_version(
+ self, file_id, version_id
+ ) -> FileResultWithResponseMetadata:
+ """Restore file by provided fileId and versionId"""
+ url = "{}/v1/files/{}/versions/{}/restore".format(
+ URL.API_BASE_URL, file_id, version_id
+ )
+ headers = self.request.create_headers()
+ resp = self.request.request(
+ method="Put",
url=url,
- headers=self.request.create_headers(),
- data={"fileIds": file_ids},
+ headers=headers,
+ )
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, FileResultWithResponseMetadata)
+ return response
+ elif resp.status_code == 404:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
+
+ def create_folder(
+ self, options: CreateFolderRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Create folder by provided folderName and parentFolderPath as an options"""
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ headers = self.request.create_headers()
+ headers.update({"Content-Type": "application/json"})
+ formatted_data = (
+ dumps(request_formatter(options.__dict__)) if options is not None else dict()
+ )
+ resp = self.request.request(
+ method="Post", url=url, headers=headers, data=formatted_data
+ )
+ if resp.status_code == 201:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
+ else:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ raise UnknownException(error_message, response_help, response_meta_data)
+
+ def delete_folder(
+ self, options: DeleteFolderRequestOptions = None
+ ) -> ResponseMetadataResult:
+ """Delete folder by provided folderPath as an options"""
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ headers = self.request.create_headers()
+ headers.update({"Content-Type": "application/json"})
+ formatted_data = (
+ dumps(request_formatter(options.__dict__)) if options is not None else dict()
+ )
+ resp = self.request.request(
+ method="Delete", url=url, headers=headers, data=formatted_data
)
+ if resp.status_code == 204:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
+ elif resp.status_code == 404:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
- if resp.status_code > 204:
- error = resp.text
- response = None
+ def copy_folder(self, options: CopyFolderRequestOptions = None) -> FolderResult:
+ """Copy folder by provided sourceFolderPath, destinationPath and includeFileVersions as an options"""
+ url = "{}/v1/bulkJobs/copyFolder".format(URL.API_BASE_URL)
+ headers = self.request.create_headers()
+ headers.update({"Content-Type": "application/json"})
+ formatted_data = (
+ dumps(request_formatter(options.__dict__))
+ if options is not None
+ else dict()
+ )
+ resp = self.request.request(
+ method="Post", url=url, headers=headers, data=formatted_data
+ )
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, FolderResult)
+ return response
+ elif resp.status_code == 404:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
else:
- error = None
- response = resp.json()
+ general_api_throw_exception(resp)
- response = {"error": error, "response": response}
- return response
+ def move_folder(self, options: MoveFolderRequestOptions = None) -> FolderResult:
+ """Move folder by provided sourceFolderPath and destinationPath as an options"""
+ url = "{}/v1/bulkJobs/moveFolder".format(URL.API_BASE_URL)
+ headers = self.request.create_headers()
+ headers.update({"Content-Type": "application/json"})
+ formatted_data = (
+ dumps(request_formatter(options.__dict__))
+ if options is not None
+ else dict()
+ )
+ resp = self.request.request(
+ method="Post", url=url, headers=headers, data=formatted_data
+ )
- def purge_cache(self, file_url: str = None) -> Dict[str, Any]:
- """Use from child class to purge cache
- """
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, FolderResult)
+ return response
+ elif resp.status_code == 404:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ general_api_throw_exception(resp)
+
+ def get_bulk_job_status(self, job_id) -> GetBulkJobStatusResult:
+ """Get bulk job status by provided only jobId"""
+ url = "{}/v1/bulkJobs/{}".format(URL.API_BASE_URL, job_id)
+ headers = self.request.create_headers()
+ resp = self.request.request(
+ method="Get",
+ url=url,
+ headers=headers,
+ )
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, GetBulkJobStatusResult)
+ return response
+ else:
+ general_api_throw_exception(resp)
+
+ def purge_cache(self, file_url: str = None) -> PurgeCacheResult:
+ """Use from child class to purge cache"""
if not file_url:
raise TypeError(ERRORS.MISSING_FILE_URL.value)
- url = URL.BASE_URL.value + URL.PURGE_CACHE.value
+ url = URL.API_BASE_URL + "/v1/files/purge"
headers = {"Content-Type": "application/json"}
headers.update(self.request.get_auth_headers())
body = {"url": file_url}
- resp = self.request.request(
- "Post", headers=headers, url=url, data=dumps(body)
- )
- formatted_resp = camel_dict_to_snake_dict(resp.json())
- if resp.status_code > 204:
- error = formatted_resp
- response = None
+ resp = self.request.request("Post", headers=headers, url=url, data=dumps(body))
+ if resp.status_code == 201:
+ response = convert_to_response_object(resp, PurgeCacheResult)
+ return response
else:
- error = None
- response = formatted_resp
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def get_purge_cache_status(self, cache_request_id: str = None) -> Dict[str, Any]:
+ def get_purge_cache_status(
+ self, cache_request_id: str = None
+ ) -> PurgeCacheStatusResult:
"""Get purge cache status by cache_request_id
- :return: cache_request_id
+ :return: PurgeCacheStatusResult
"""
if not cache_request_id:
raise TypeError(ERRORS.CACHE_PURGE_STATUS_ID_MISSING.value)
- url = "{}/purge/{}".format(URL.BASE_URL.value, cache_request_id)
+ url = "{}/v1/files/purge/{}".format(URL.API_BASE_URL, cache_request_id)
headers = self.request.create_headers()
resp = self.request.request("GET", url, headers=headers)
- formatted_resp = camel_dict_to_snake_dict(resp.json())
-
- if resp.status_code > 200:
- error = formatted_resp
- response = None
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, PurgeCacheStatusResult)
+ return response
else:
- error = None
- response = formatted_resp
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def get_metadata(self, file_id: str = None):
- """Get metadata by file_id
- """
+ def get_metadata(self, file_id: str = None) -> GetMetadataResult:
+ """Get metadata by file_id"""
if not file_id:
raise TypeError(ERRORS.FILE_ID_MISSING.value)
- url = "{}/{}/metadata".format(URL.BASE_URL.value, file_id)
+ url = "{}/v1/files/{}/metadata".format(URL.API_BASE_URL, file_id)
resp = self.request.request("GET", url, headers=self.request.create_headers())
- formatted_resp = camel_dict_to_snake_dict(resp.json())
- if resp.status_code > 200:
- error = resp.json()
- response = None
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, GetMetadataResult)
+ return response
else:
- error = None
- response = resp.json()
- response = {"error": error, "response": response}
- return response
+ general_api_throw_exception(resp)
- def get_metadata_from_remote_url(self, remote_file_url: str):
+ def get_metadata_from_remote_url(self, remote_file_url: str) -> GetMetadataResult:
+ """Get remote metadata by provided remote_file_url"""
if not remote_file_url:
raise ValueError("You must provide remote url")
- url = URL.REMOTE_METADATA_FULL_URL.value
+ url = "{}/v1/metadata".format(URL.API_BASE_URL)
param = {"url": remote_file_url}
resp = self.request.request(
"GET", url, headers=self.request.create_headers(), params=param
)
+ if resp.status_code == 200:
+ response = convert_to_response_object(resp, GetMetadataResult)
+ return response
+ else:
+ general_api_throw_exception(resp)
+
+ def create_custom_metadata_fields(
+ self, options: CreateCustomMetadataFieldsRequestOptions = None
+ ) -> CustomMetadataFieldsResultWithResponseMetadata:
+ """creates custom metadata fields by passing name, label and schema as an options"""
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ if options is not None:
+ if "schema" in options.__dict__:
+ options.schema.__dict__ = request_formatter(options.schema.__dict__)
+ options_dict = options.__dict__
+ if "schema" in options_dict:
+ options_dict["schema"] = options.schema.__dict__
+ formatted_options = dumps(options_dict)
+ else:
+ formatted_options = dict()
+
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ resp = self.request.request(
+ method="Post", url=url, headers=headers, data=formatted_options
+ )
+ if resp.status_code == 201:
+ response = convert_to_response_object(
+ resp, CustomMetadataFieldsResultWithResponseMetadata
+ )
+ return response
+ else:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ if resp.status_code == 400:
+ raise BadRequestException(
+ error_message, response_help, response_meta_data
+ )
+ else:
+ raise UnknownException(error_message, response_help, response_meta_data)
+
+ def get_custom_metadata_fields(
+ self, include_deleted: bool = False
+ ) -> ListCustomMetadataFieldsResult:
+ """get custom metadata fields"""
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ param = {"includeDeleted": str(include_deleted).lower()}
+ resp = self.request.request(
+ method="GET", url=url, headers=self.request.create_headers(), params=param
+ )
+ if resp.status_code == 200:
+ response = convert_to_list_response_object(
+ resp, CustomMetadataFieldsResult, ListCustomMetadataFieldsResult
+ )
+ return response
+ else:
+ response = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response) == str:
+ response = ast.literal_eval(response)
+ error_message = response["message"] if type(response) == dict else ""
+ response_help = response["help"] if type(response) == dict else ""
+ raise UnknownException(error_message, response_help, response_meta_data)
+
+ def update_custom_metadata_fields(
+ self, field_id, options: UpdateCustomMetadataFieldsRequestOptions = None
+ ) -> CustomMetadataFieldsResultWithResponseMetadata:
+ """updates custom metadata fields by passing id of custom metadata field and params as an options"""
+ if not field_id:
+ raise ValueError(ERRORS.MISSING_CUSTOM_METADATA_FIELD_ID)
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, field_id)
+ if "schema" in options.__dict__:
+ options.schema.__dict__ = request_formatter(options.schema.__dict__)
+ options_dict = options.__dict__
+ if "schema" in options_dict:
+ options_dict["schema"] = options.schema.__dict__
+ formatted_options = dumps(request_formatter(options_dict))
+ headers = {"Content-Type": "application/json"}
+ headers.update(self.request.create_headers())
+ resp = self.request.request(
+ method="Patch", url=url, headers=headers, data=formatted_options
+ )
+ if resp.status_code == 200:
+ response = convert_to_response_object(
+ resp, CustomMetadataFieldsResultWithResponseMetadata
+ )
+ return response
+ else:
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ if resp.status_code == 400:
+ raise BadRequestException(
+ error_message, response_help, response_meta_data
+ )
+ elif resp.status_code == 404:
+ raise NotFoundException(
+ error_message, response_help, response_meta_data
+ )
+ else:
+ raise UnknownException(error_message, response_help, response_meta_data)
- if resp.status_code > 204:
- error = resp.json()
- response = None
+ def delete_custom_metadata_field(self, field_id: str) -> ResponseMetadataResult:
+ """Deletes custom metadata fields by passing field_id"""
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, field_id)
+ resp = self.request.request(
+ "Delete", url, headers=self.request.create_headers()
+ )
+ if resp.status_code == 204:
+ response = convert_to_response_metadata_result_object(resp)
+ return response
else:
- error = None
- response = resp.json()
- response = {"error": error, "response": response}
- return response
+ response_json = get_response_json(resp)
+ response_meta_data = populate_response_metadata(resp)
+ if type(response_json) == str:
+ response_json = ast.literal_eval(response_json)
+ error_message = (
+ response_json["message"] if type(response_json) == dict else ""
+ )
+ response_help = response_json["help"] if type(response_json) == dict else ""
+ if resp.status_code == 404:
+ raise NotFoundException(
+ error_message, response_help, response_meta_data
+ )
+ else:
+ raise UnknownException(error_message, response_help, response_meta_data)
def is_valid_list_options(self, options: Dict[str, Any]) -> bool:
- """Returns if options are valid
- """
+ """Returns if options are valid"""
valid_values = self.get_valid_list_values()
for key in options:
if key not in valid_values:
@@ -258,8 +785,7 @@ def is_valid_list_options(self, options: Dict[str, Any]) -> bool:
@staticmethod
def get_valid_list_values():
- """Returns valid options for list files
- """
+ """Returns valid options for list files"""
return VALID_FILE_OPTIONS
@staticmethod
@@ -272,6 +798,12 @@ def validate_upload(options):
for key, val in options.items():
if key not in VALID_UPLOAD_OPTIONS:
return False
+ if type(val) == dict or type(val) == tuple:
+ options[key] = dumps(val)
+ continue
+ if key == "extensions":
+ options[key] = dumps(val)
+ continue
if key == "response_fields":
for i, j in enumerate(options[key]):
if j not in VALID_UPLOAD_OPTIONS:
@@ -282,7 +814,7 @@ def validate_upload(options):
options[key] = ",".join(response_list)
continue
if isinstance(val, list):
- val = ",".join(val)
+ val = ",".join([str(i) for i in val])
if val:
options[key] = val
continue
diff --git a/imagekitio/models/CopyFileRequestOptions.py b/imagekitio/models/CopyFileRequestOptions.py
new file mode 100644
index 0000000..0a47b60
--- /dev/null
+++ b/imagekitio/models/CopyFileRequestOptions.py
@@ -0,0 +1,13 @@
+class CopyFileRequestOptions:
+ def __init__(
+ self,
+ source_file_path: str = None,
+ destination_path: str = None,
+ include_file_versions: bool = None,
+ ):
+ if source_file_path is not None:
+ self.source_file_path = source_file_path
+ if destination_path is not None:
+ self.destination_path = destination_path
+ if include_file_versions is not None:
+ self.include_file_versions = include_file_versions
diff --git a/imagekitio/models/CopyFolderRequestOptions.py b/imagekitio/models/CopyFolderRequestOptions.py
new file mode 100644
index 0000000..d4e4c30
--- /dev/null
+++ b/imagekitio/models/CopyFolderRequestOptions.py
@@ -0,0 +1,13 @@
+class CopyFolderRequestOptions:
+ def __init__(
+ self,
+ source_folder_path: str = None,
+ destination_path: str = None,
+ include_file_versions: bool = None,
+ ):
+ if source_folder_path is not None:
+ self.source_folder_path = source_folder_path
+ if destination_path is not None:
+ self.destination_path = destination_path
+ if include_file_versions is not None:
+ self.include_file_versions = include_file_versions
diff --git a/imagekitio/models/CreateCustomMetadataFieldsRequestOptions.py b/imagekitio/models/CreateCustomMetadataFieldsRequestOptions.py
new file mode 100644
index 0000000..ffcc3bf
--- /dev/null
+++ b/imagekitio/models/CreateCustomMetadataFieldsRequestOptions.py
@@ -0,0 +1,36 @@
+from ..models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+
+
+class CreateCustomMetadataFieldsRequestOptions:
+ def __init__(
+ self,
+ name: str = None,
+ label: str = None,
+ schema: CustomMetadataFieldsSchema = None,
+ ):
+ self.name = name
+ self.label = label
+ if schema is None or schema == {}:
+ CustomMetadataFieldsSchema(None, None, None, None, None, None, None, None)
+ else:
+ if type(schema) == CustomMetadataFieldsSchema:
+ self.schema = schema
+ else:
+ self.schema = (
+ CustomMetadataFieldsSchema(
+ schema["type"] if "type" in schema else None,
+ schema["select_options"]
+ if "select_options" in schema
+ else None,
+ schema["default_value"] if "default_value" in schema else None,
+ schema["is_value_required"]
+ if "is_value_required" in schema
+ else None,
+ schema["min_value"] if "min_value" in schema else None,
+ schema["max_value"] if "max_value" in schema else None,
+ schema["min_length"] if "min_length" in schema else None,
+ schema["max_length"],
+ )
+ if "max_length" in schema
+ else None
+ )
diff --git a/imagekitio/models/CreateFolderRequestOptions.py b/imagekitio/models/CreateFolderRequestOptions.py
new file mode 100644
index 0000000..de3fcef
--- /dev/null
+++ b/imagekitio/models/CreateFolderRequestOptions.py
@@ -0,0 +1,6 @@
+class CreateFolderRequestOptions:
+ def __init__(self, folder_name: str = None, parent_folder_path: str = None):
+ if folder_name is not None:
+ self.folder_name = folder_name
+ if parent_folder_path is not None:
+ self.parent_folder_path = parent_folder_path
diff --git a/imagekitio/models/CustomMetaDataTypeEnum.py b/imagekitio/models/CustomMetaDataTypeEnum.py
new file mode 100644
index 0000000..9740e58
--- /dev/null
+++ b/imagekitio/models/CustomMetaDataTypeEnum.py
@@ -0,0 +1,11 @@
+from enum import Enum
+
+
+class CustomMetaDataTypeEnum(Enum):
+ Text = 1
+ Textarea = 2
+ Number = 3
+ Date = 4
+ Boolean = 5
+ SingleSelect = 6
+ MultiSelect = 7
diff --git a/imagekitio/models/CustomMetadataFieldsSchema.py b/imagekitio/models/CustomMetadataFieldsSchema.py
new file mode 100644
index 0000000..9f667a6
--- /dev/null
+++ b/imagekitio/models/CustomMetadataFieldsSchema.py
@@ -0,0 +1,31 @@
+from ..models.CustomMetaDataTypeEnum import CustomMetaDataTypeEnum
+
+
+class CustomMetadataFieldsSchema:
+ def __init__(
+ self,
+ type: CustomMetaDataTypeEnum = None,
+ select_options=None,
+ default_value=None,
+ is_value_required: bool = None,
+ min_value=None,
+ max_value=None,
+ min_length: int = None,
+ max_length: int = None,
+ ):
+ if type is not None:
+ self.type = type.name
+ if select_options is not None:
+ self.select_options = select_options
+ if default_value is not None:
+ self.default_value = default_value
+ if is_value_required is not None:
+ self.is_value_required = is_value_required
+ if min_value is not None:
+ self.min_value = min_value
+ if max_value is not None:
+ self.max_value = max_value
+ if min_length is not None:
+ self.min_length = min_length
+ if max_length is not None:
+ self.max_length = max_length
diff --git a/imagekitio/models/DeleteFolderRequestOptions.py b/imagekitio/models/DeleteFolderRequestOptions.py
new file mode 100644
index 0000000..dd1fcd6
--- /dev/null
+++ b/imagekitio/models/DeleteFolderRequestOptions.py
@@ -0,0 +1,4 @@
+class DeleteFolderRequestOptions:
+ def __init__(self, folder_path: str = None):
+ if folder_path is not None:
+ self.folder_path = folder_path
diff --git a/imagekitio/models/ListAndSearchFileRequestOptions.py b/imagekitio/models/ListAndSearchFileRequestOptions.py
new file mode 100644
index 0000000..3b834f2
--- /dev/null
+++ b/imagekitio/models/ListAndSearchFileRequestOptions.py
@@ -0,0 +1,31 @@
+import array
+
+
+class ListAndSearchFileRequestOptions:
+ def __init__(
+ self,
+ type: str = None,
+ sort: str = None,
+ path: str = None,
+ search_query: str = None,
+ file_type: str = None,
+ limit: int = None,
+ skip: int = None,
+ tags=None,
+ ):
+ if type is not None:
+ self.type = type
+ if sort is not None:
+ self.sort = sort
+ if path is not None:
+ self.path = path
+ if search_query is not None:
+ self.search_query = search_query
+ if file_type is not None:
+ self.file_type = file_type
+ if limit is not None:
+ self.limit = limit
+ if skip is not None:
+ self.skip = skip
+ if tags is not None:
+ self.tags = tags
diff --git a/imagekitio/models/MoveFileRequestOptions.py b/imagekitio/models/MoveFileRequestOptions.py
new file mode 100644
index 0000000..cf68f31
--- /dev/null
+++ b/imagekitio/models/MoveFileRequestOptions.py
@@ -0,0 +1,6 @@
+class MoveFileRequestOptions:
+ def __init__(self, source_file_path: str = None, destination_path: str = None):
+ if source_file_path is not None:
+ self.source_file_path = source_file_path
+ if destination_path is not None:
+ self.destination_path = destination_path
diff --git a/imagekitio/models/MoveFolderRequestOptions.py b/imagekitio/models/MoveFolderRequestOptions.py
new file mode 100644
index 0000000..bfd9c58
--- /dev/null
+++ b/imagekitio/models/MoveFolderRequestOptions.py
@@ -0,0 +1,6 @@
+class MoveFolderRequestOptions:
+ def __init__(self, source_folder_path: str = None, destination_path: str = None):
+ if source_folder_path is not None:
+ self.source_folder_path = source_folder_path
+ if destination_path is not None:
+ self.destination_path = destination_path
diff --git a/imagekitio/models/RenameFileRequestOptions.py b/imagekitio/models/RenameFileRequestOptions.py
new file mode 100644
index 0000000..60d74b4
--- /dev/null
+++ b/imagekitio/models/RenameFileRequestOptions.py
@@ -0,0 +1,10 @@
+class RenameFileRequestOptions:
+ def __init__(
+ self, file_path: str = None, new_file_name: str = None, purge_cache: bool = None
+ ):
+ if file_path is not None:
+ self.file_path = file_path
+ if new_file_name is not None:
+ self.new_file_name = new_file_name
+ if purge_cache is not None:
+ self.purge_cache = purge_cache
diff --git a/imagekitio/models/UpdateCustomMetadataFieldsRequestOptions.py b/imagekitio/models/UpdateCustomMetadataFieldsRequestOptions.py
new file mode 100644
index 0000000..92294a1
--- /dev/null
+++ b/imagekitio/models/UpdateCustomMetadataFieldsRequestOptions.py
@@ -0,0 +1,30 @@
+from ..models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+
+
+class UpdateCustomMetadataFieldsRequestOptions:
+ def __init__(self, label: str = None, schema: CustomMetadataFieldsSchema = None):
+ self.label = label
+ if schema is None or schema == {}:
+ CustomMetadataFieldsSchema(None, None, None, None, None, None, None, None)
+ else:
+ if type(schema) == CustomMetadataFieldsSchema:
+ self.schema = schema
+ else:
+ self.schema = (
+ CustomMetadataFieldsSchema(
+ schema["type"] if "type" in schema else None,
+ schema["select_options"]
+ if "select_options" in schema
+ else None,
+ schema["default_value"] if "default_value" in schema else None,
+ schema["is_value_required"]
+ if "is_value_required" in schema
+ else None,
+ schema["min_value"] if "min_value" in schema else None,
+ schema["max_value"] if "max_value" in schema else None,
+ schema["min_length"] if "min_length" in schema else None,
+ schema["max_length"],
+ )
+ if "max_length" in schema
+ else None
+ )
diff --git a/imagekitio/models/UpdateFileRequestOptions.py b/imagekitio/models/UpdateFileRequestOptions.py
new file mode 100644
index 0000000..341f71a
--- /dev/null
+++ b/imagekitio/models/UpdateFileRequestOptions.py
@@ -0,0 +1,26 @@
+import json
+from typing import List
+
+
+class UpdateFileRequestOptions:
+ def __init__(
+ self,
+ remove_ai_tags: List[str] = None,
+ webhook_url: str = None,
+ extensions: json = None,
+ tags: List[str] = None,
+ custom_coordinates: str = None,
+ custom_metadata: json = None,
+ ):
+ if remove_ai_tags is not None:
+ self.remove_ai_tags = remove_ai_tags
+ if webhook_url is not None:
+ self.webhook_url = webhook_url
+ if extensions is not None:
+ self.extensions = extensions
+ if tags is not None:
+ self.tags = tags
+ if custom_coordinates is not None:
+ self.custom_coordinates = custom_coordinates
+ if custom_metadata is not None:
+ self.custom_metadata = custom_metadata
diff --git a/imagekitio/models/UploadFileRequestOptions.py b/imagekitio/models/UploadFileRequestOptions.py
new file mode 100644
index 0000000..222020c
--- /dev/null
+++ b/imagekitio/models/UploadFileRequestOptions.py
@@ -0,0 +1,47 @@
+import json
+from typing import List
+
+
+class UploadFileRequestOptions:
+ def __init__(
+ self,
+ use_unique_file_name: bool = None,
+ tags: List[str] = None,
+ folder: str = None,
+ is_private_file: bool = None,
+ custom_coordinates: str = None,
+ response_fields: List[str] = None,
+ extensions: json = None,
+ webhook_url: str = None,
+ overwrite_file: bool = None,
+ overwrite_ai_tags: bool = None,
+ overwrite_tags: bool = None,
+ overwrite_custom_metadata: bool = None,
+ custom_metadata: json = None,
+ ):
+ if use_unique_file_name is not None:
+ self.use_unique_file_name = use_unique_file_name
+ if tags is not None:
+ self.tags = tags
+ if folder is not None:
+ self.folder = folder
+ if is_private_file is not None:
+ self.is_private_file = is_private_file
+ if custom_coordinates is not None:
+ self.custom_coordinates = custom_coordinates
+ if response_fields is not None:
+ self.response_fields = response_fields
+ if extensions is not None:
+ self.extensions = extensions
+ if webhook_url is not None:
+ self.webhook_url = webhook_url
+ if overwrite_file is not None:
+ self.overwrite_file = overwrite_file
+ if overwrite_ai_tags is not None:
+ self.overwrite_ai_tags = overwrite_ai_tags
+ if overwrite_tags is not None:
+ self.overwrite_tags = overwrite_tags
+ if overwrite_custom_metadata is not None:
+ self.overwrite_custom_metadata = overwrite_custom_metadata
+ if custom_metadata is not None:
+ self.custom_metadata = custom_metadata
diff --git a/imagekitio/models/__init__.py b/imagekitio/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/imagekitio/models/results/AITags.py b/imagekitio/models/results/AITags.py
new file mode 100644
index 0000000..c9ace20
--- /dev/null
+++ b/imagekitio/models/results/AITags.py
@@ -0,0 +1,5 @@
+class AITags:
+ def __init__(self, name=None, confidence=None, source=None):
+ self.name = name
+ self.confidence = confidence
+ self.source = source
diff --git a/imagekitio/models/results/BulkDeleteFileResult.py b/imagekitio/models/results/BulkDeleteFileResult.py
new file mode 100644
index 0000000..696f389
--- /dev/null
+++ b/imagekitio/models/results/BulkDeleteFileResult.py
@@ -0,0 +1,15 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class BulkDeleteFileResult:
+ def __init__(self, successfully_deleted_file_ids=None):
+ self.successfully_deleted_file_ids = successfully_deleted_file_ids
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/CustomMetadataFieldsResult.py b/imagekitio/models/results/CustomMetadataFieldsResult.py
new file mode 100644
index 0000000..9cf96e5
--- /dev/null
+++ b/imagekitio/models/results/CustomMetadataFieldsResult.py
@@ -0,0 +1,24 @@
+from .CustomMetadataSchema import CustomMetadataSchema
+
+
+class CustomMetadataFieldsResult:
+ def __init__(
+ self,
+ id=None,
+ name=None,
+ label=None,
+ schema: dict = {},
+ ):
+ self.id = id
+ self.name = name
+ self.label = label
+ self.schema = CustomMetadataSchema(
+ schema["type"],
+ schema["selectOptions"] if "selectOptions" in schema else None,
+ schema["defaultValue"] if "defaultValue" in schema else None,
+ schema["isValueRequired"] if "isValueRequired" in schema else None,
+ schema["minValue"] if "minValue" in schema else None,
+ schema["maxValue"] if "maxValue" in schema else None,
+ schema["minLength"] if "minLength" in schema else None,
+ schema["maxValue"] if "maxValue" in schema else None,
+ )
diff --git a/imagekitio/models/results/CustomMetadataFieldsResultWithResponseMetadata.py b/imagekitio/models/results/CustomMetadataFieldsResultWithResponseMetadata.py
new file mode 100644
index 0000000..fce8e58
--- /dev/null
+++ b/imagekitio/models/results/CustomMetadataFieldsResultWithResponseMetadata.py
@@ -0,0 +1,19 @@
+from .CustomMetadataFieldsResult import CustomMetadataFieldsResult
+from .CustomMetadataSchema import CustomMetadataSchema
+from .ResponseMetadata import ResponseMetadata
+
+
+class CustomMetadataFieldsResultWithResponseMetadata(CustomMetadataFieldsResult):
+ def __init__(
+ self,
+ id=None,
+ name=None,
+ label=None,
+ schema: CustomMetadataSchema = CustomMetadataSchema(
+ None, None, None, None, None, None, None, None
+ ),
+ ):
+ super(CustomMetadataFieldsResultWithResponseMetadata, self).__init__(
+ id, name, label, schema
+ )
+ self.response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
diff --git a/imagekitio/models/results/CustomMetadataSchema.py b/imagekitio/models/results/CustomMetadataSchema.py
new file mode 100644
index 0000000..948b529
--- /dev/null
+++ b/imagekitio/models/results/CustomMetadataSchema.py
@@ -0,0 +1,20 @@
+class CustomMetadataSchema:
+ def __init__(
+ self,
+ type=None,
+ select_options=None,
+ default_value=None,
+ is_value_required=None,
+ min_value=None,
+ max_value=None,
+ min_length=None,
+ max_length=None,
+ ):
+ self.type = type
+ self.select_options = select_options
+ self.default_value = default_value
+ self.is_value_required = is_value_required
+ self.min_value = min_value
+ self.max_value = max_value
+ self.min_length = min_length
+ self.max_length = max_length
diff --git a/imagekitio/models/results/EmbeddedMetadata.py b/imagekitio/models/results/EmbeddedMetadata.py
new file mode 100644
index 0000000..0c5472d
--- /dev/null
+++ b/imagekitio/models/results/EmbeddedMetadata.py
@@ -0,0 +1,6 @@
+class EmbeddedMetadata:
+ def __init__(self, x_resolution=None, y_resolution=None, date_created=None, date_timecreated=None):
+ self.x_resolution = x_resolution
+ self.y_resolution = y_resolution
+ self.date_created = date_created
+ self.date_timecreated = date_timecreated
diff --git a/imagekitio/models/results/FileResult.py b/imagekitio/models/results/FileResult.py
new file mode 100644
index 0000000..4b7d9bf
--- /dev/null
+++ b/imagekitio/models/results/FileResult.py
@@ -0,0 +1,62 @@
+from typing import List
+from .AITags import AITags
+from .VersionInfo import VersionInfo
+
+
+class FileResult:
+ def __init__(
+ self,
+ type=None,
+ name=None,
+ created_at=None,
+ updated_at=None,
+ file_id=None,
+ tags=None,
+ ai_tags: List[AITags] = [],
+ version_info: dict = {},
+ embedded_metadata: dict = {},
+ custom_coordinates: str = "",
+ custom_metadata: dict = {},
+ is_private_file=False,
+ url: str = "",
+ thumbnail: str = "",
+ file_type: str = "",
+ file_path: str = "",
+ height: int = None,
+ width: int = None,
+ size: int = None,
+ has_alpha=False,
+ mime: str = None,
+ extension_status: dict = {},
+ ):
+ self.type = type
+ self.name = name
+ self.created_at = created_at
+ self.updated_at = updated_at
+ self.file_id = file_id
+ self.tags = tags
+ self.ai_tags: List[AITags] = []
+ if ai_tags is not None:
+ for i in ai_tags:
+ self.ai_tags.append(
+ AITags(
+ i["name"] if "name" in i else None,
+ i["confidence"] if "confidence" in i else None,
+ i["source"] if "source" in i else None,
+ )
+ )
+ self.version_info = VersionInfo(version_info["id"], version_info["name"])
+ self.embedded_metadata = embedded_metadata
+ self.custom_coordinates = custom_coordinates
+ self.custom_metadata = custom_metadata
+ self.is_private_file = is_private_file
+ self.url = url
+ self.thumbnail = thumbnail
+ self.file_type = file_type
+ self.file_path = file_path
+ self.height = height
+ self.width = width
+ self.size = size
+ self.has_alpha = has_alpha
+ self.mime = mime
+ self.extension_status = extension_status
diff --git a/imagekitio/models/results/FileResultWithResponseMetadata.py b/imagekitio/models/results/FileResultWithResponseMetadata.py
new file mode 100644
index 0000000..bd70c8e
--- /dev/null
+++ b/imagekitio/models/results/FileResultWithResponseMetadata.py
@@ -0,0 +1,59 @@
+from typing import List
+
+from .AITags import AITags
+from .FileResult import FileResult
+from .ResponseMetadata import ResponseMetadata
+from .VersionInfo import VersionInfo
+
+
+class FileResultWithResponseMetadata(FileResult):
+ def __init__(
+ self,
+ type=None,
+ name=None,
+ created_at=None,
+ updated_at=None,
+ file_id=None,
+ tags=None,
+ ai_tags: List[AITags] = AITags(None, None, None),
+ version_info: VersionInfo = VersionInfo(None, None),
+ embedded_metadata=None,
+ custom_coordinates: str = "",
+ custom_metadata=None,
+ is_private_file=False,
+ url: str = "",
+ thumbnail: str = "",
+ file_type: str = "",
+ file_path: str = "",
+ height: int = None,
+ width: int = None,
+ size: int = None,
+ has_alpha=False,
+ mime: str = None,
+ extension_status=None,
+ ):
+ super().__init__(
+ type,
+ name,
+ created_at,
+ updated_at,
+ file_id,
+ tags,
+ ai_tags,
+ version_info,
+ embedded_metadata,
+ custom_coordinates,
+ custom_metadata,
+ is_private_file,
+ url,
+ thumbnail,
+ file_type,
+ file_path,
+ height,
+ width,
+ size,
+ has_alpha,
+ mime,
+ extension_status,
+ )
+ self.response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
diff --git a/imagekitio/models/results/FolderResult.py b/imagekitio/models/results/FolderResult.py
new file mode 100644
index 0000000..705469e
--- /dev/null
+++ b/imagekitio/models/results/FolderResult.py
@@ -0,0 +1,15 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class FolderResult:
+ def __init__(self, job_id=None):
+ self.job_id = job_id
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/GetBulkJobStatusResult.py b/imagekitio/models/results/GetBulkJobStatusResult.py
new file mode 100644
index 0000000..4510d68
--- /dev/null
+++ b/imagekitio/models/results/GetBulkJobStatusResult.py
@@ -0,0 +1,17 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class GetBulkJobStatusResult:
+ def __init__(self, job_id=None, type=None, status=None):
+ self.job_id = job_id
+ self.type = type
+ self.status = status
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/GetMetadataResult.py b/imagekitio/models/results/GetMetadataResult.py
new file mode 100644
index 0000000..353603d
--- /dev/null
+++ b/imagekitio/models/results/GetMetadataResult.py
@@ -0,0 +1,49 @@
+from .MetadataExifExif import MetadataExifExif
+from .MetadataExifGPS import MetadataExifGPS
+from .MetadataExifInteroperability import MetadataExifInteroperability
+from .MetadataExifThumbnail import MetadataExifThumbnail
+from .MetadataExif import MetadataExif
+from .MetadataExifImage import MetadataExifImage
+from .ResponseMetadata import ResponseMetadata
+
+
+class GetMetadataResult:
+ def __init__(
+ self,
+ height=None,
+ width=None,
+ size=None,
+ format=None,
+ has_color_profile=None,
+ quality=None,
+ density=None,
+ has_transparency=None,
+ p_hash=None,
+ exif: dict = {},
+ ):
+ self.height = height
+ self.width = width
+ self.size = size
+ self.format = format
+ self.has_color_profile = has_color_profile
+ self.quality = quality
+ self.density = density
+ self.has_transparency = has_transparency
+ self.p_hash = p_hash
+ self.exif: MetadataExif = MetadataExif(
+ exif["image"] if "image" in exif else None,
+ exif["thumbnail"] if "thumbnail" in exif else None,
+ exif["exif"] if "exif" in exif else None,
+ exif["gps"] if "gps" in exif else None,
+ exif["interoperability"] if "interoperability" in exif else None,
+ exif["makernote"] if "makernote" in exif else None,
+ )
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/ListCustomMetadataFieldsResult.py b/imagekitio/models/results/ListCustomMetadataFieldsResult.py
new file mode 100644
index 0000000..23ba45f
--- /dev/null
+++ b/imagekitio/models/results/ListCustomMetadataFieldsResult.py
@@ -0,0 +1,18 @@
+from typing import List
+
+from .CustomMetadataFieldsResult import CustomMetadataFieldsResult
+from .ResponseMetadata import ResponseMetadata
+
+
+class ListCustomMetadataFieldsResult:
+ def __init__(self, list: List[CustomMetadataFieldsResult]):
+ self.list = list
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/ListFileResult.py b/imagekitio/models/results/ListFileResult.py
new file mode 100644
index 0000000..7db8cce
--- /dev/null
+++ b/imagekitio/models/results/ListFileResult.py
@@ -0,0 +1,17 @@
+from typing import List
+from .FileResult import FileResult
+from .ResponseMetadata import ResponseMetadata
+
+
+class ListFileResult:
+ def __init__(self, list: List[FileResult] = None):
+ self.list = list
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/MetadataExif.py b/imagekitio/models/results/MetadataExif.py
new file mode 100644
index 0000000..f1d019b
--- /dev/null
+++ b/imagekitio/models/results/MetadataExif.py
@@ -0,0 +1,165 @@
+from .MetadataExifExif import MetadataExifExif
+from .MetadataExifGPS import MetadataExifGPS
+from .MetadataExifInteroperability import MetadataExifInteroperability
+from .MetadataExifThumbnail import MetadataExifThumbnail
+from .MetadataExifImage import MetadataExifImage
+
+
+class MetadataExif:
+ def __init__(
+ self,
+ image: MetadataExifImage = None,
+ thumbnail: MetadataExifThumbnail = None,
+ exif: MetadataExifExif = None,
+ gps: MetadataExifGPS = None,
+ interoperability: MetadataExifInteroperability = None,
+ makernote=None,
+ ):
+ if makernote is None:
+ makernote = {}
+ if image is None or image == {}:
+ self.image = MetadataExifImage(
+ None, None, None, None, None, None, None, None, None, None, None
+ )
+ else:
+ if type(image) == MetadataExifImage:
+ self.image = image
+ else:
+ self.image = MetadataExifImage(
+ image["Make"] if "Make" in image else None,
+ image["Model"] if "Model" in image else None,
+ image["Orientation"] if "Orientation" in image else None,
+ image["XResolution"] if "XResolution" in image else None,
+ image["YResolution"] if "YResolution" in image else None,
+ image["ResolutionUnit"] if "ResolutionUnit" in image else None,
+ image["Software"] if "Software" in image else None,
+ image["ModifyDate"] if "ModifyDate" in image else None,
+ image["YCbCrPositioning"] if "YCbCrPositioning" in image else None,
+ image["ExifOffset"] if "ExifOffset" in image else None,
+ image["GPSInfo"] if "GPSInfo" in image else None,
+ )
+
+ if thumbnail is None or thumbnail == {}:
+ self.thumbnail = MetadataExifThumbnail(None, None, None, None, None, None)
+ else:
+ if type(thumbnail) == MetadataExifThumbnail:
+ self.thumbnail = thumbnail
+ else:
+ self.thumbnail = MetadataExifThumbnail(
+ thumbnail["Compression"] if "Compression" in thumbnail else None,
+ thumbnail["XResolution"] if "XResolution" in thumbnail else None,
+ thumbnail["YResolution"] if "YResolution" in thumbnail else None,
+ thumbnail["ResolutionUnit"]
+ if "ResolutionUnit" in thumbnail
+ else None,
+ thumbnail["ThumbnailOffset"]
+ if "ThumbnailOffset" in thumbnail
+ else None,
+ thumbnail["ThumbnailLength"]
+ if "ThumbnailLength" in thumbnail
+ else None,
+ )
+ if exif is None or exif == {}:
+ self.exif = MetadataExifExif(
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ )
+ else:
+ if type(exif) == MetadataExifExif:
+ self.exif = exif
+ else:
+ self.exif = MetadataExifExif(
+ exif["ExposureTime"] if "ExposureTime" in exif else None,
+ exif["FNumber"] if "FNumber" in exif else None,
+ exif["ExposureProgram"] if "ExposureProgram" in exif else None,
+ exif["ISO"] if "ISO" in exif else None,
+ exif["ExifVersion"] if "ExifVersion" in exif else None,
+ exif["DateTimeOriginal"] if "DateTimeOriginal" in exif else None,
+ exif["CreateDate"] if "CreateDate" in exif else None,
+ exif["ShutterSpeedValue"] if "ShutterSpeedValue" in exif else None,
+ exif["ApertureValue"] if "ApertureValue" in exif else None,
+ exif["ExposureCompensation"]
+ if "ExposureCompensation" in exif
+ else None,
+ exif["MeteringMode"] if "MeteringMode" in exif else None,
+ exif["Flash"] if "Flash" in exif else None,
+ exif["FocalLength"] if "FocalLength" in exif else None,
+ exif["SubSecTime"] if "SubSecTime" in exif else None,
+ exif["SubSecTimeOriginal"]
+ if "SubSecTimeOriginal" in exif
+ else None,
+ exif["SubSecTimeDigitized"]
+ if "SubSecTimeDigitized" in exif
+ else None,
+ exif["FlashpixVersion"] if "FlashpixVersion" in exif else None,
+ exif["ColorSpace"] if "ColorSpace" in exif else None,
+ exif["ExifImageWidth"] if "ExifImageWidth" in exif else None,
+ exif["ExifImageHeight"] if "ExifImageHeight" in exif else None,
+ exif["InteropOffset"] if "InteropOffset" in exif else None,
+ exif["FocalPlaneXResolution"]
+ if "FocalPlaneXResolution" in exif
+ else None,
+ exif["FocalPlaneYResolution"]
+ if "FocalPlaneYResolution" in exif
+ else None,
+ exif["FocalPlaneResolutionUnit"]
+ if "FocalPlaneResolutionUnit" in exif
+ else None,
+ exif["CustomRendered"] if "CustomRendered" in exif else None,
+ exif["ExposureMode"] if "ExposureMode" in exif else None,
+ exif["WhiteBalance"] if "WhiteBalance" in exif else None,
+ exif["SceneCaptureType"] if "SceneCaptureType" in exif else None,
+ )
+
+ if gps is None or gps == {}:
+ self.gps = MetadataExifGPS(None)
+ else:
+ if type(gps) == MetadataExifGPS:
+ self.gps = gps
+ else:
+ self.gps = MetadataExifGPS(
+ gps["GPSVersionID"] if "GPSVersionID" in gps else None
+ )
+
+ if interoperability is None or interoperability == {}:
+ self.interoperability = MetadataExifInteroperability(None, None)
+ else:
+ if type(interoperability) == MetadataExifInteroperability:
+ self.interoperability = interoperability
+ else:
+ self.interoperability = MetadataExifInteroperability(
+ interoperability["InteropIndex"]
+ if "InteropIndex" in interoperability
+ else None,
+ interoperability["InteropVersion"]
+ if "InteropVersion" in interoperability
+ else None,
+ )
+
+ self.makernote = makernote
diff --git a/imagekitio/models/results/MetadataExifExif.py b/imagekitio/models/results/MetadataExifExif.py
new file mode 100644
index 0000000..4f96b7b
--- /dev/null
+++ b/imagekitio/models/results/MetadataExifExif.py
@@ -0,0 +1,60 @@
+class MetadataExifExif:
+ def __init__(
+ self,
+ exposure_time=None,
+ f_number=None,
+ exposure_program=None,
+ iso=None,
+ exif_version=None,
+ date_time_original=None,
+ create_date=None,
+ shutter_speed_value=None,
+ aperture_value=None,
+ exposure_compensation=None,
+ metering_mode=None,
+ flash=None,
+ focal_length=None,
+ sub_sec_time=None,
+ sub_sec_time_original=None,
+ sub_sec_time_digitized=None,
+ flashpix_version=None,
+ color_space=None,
+ exif_image_width=None,
+ exif_image_height=None,
+ interop_offset=None,
+ focal_plane_x_resolution=None,
+ focal_plane_y_resolution=None,
+ focal_plane_resolution_unit=None,
+ custom_rendered=None,
+ exposure_mode=None,
+ white_balance=None,
+ scene_capture_type=None,
+ ):
+ self.exposure_time = exposure_time
+ self.f_number = f_number
+ self.exposure_program = exposure_program
+ self.iso = iso
+ self.exif_version = exif_version
+ self.date_time_original = date_time_original
+ self.create_date = create_date
+ self.shutter_speed_value = shutter_speed_value
+ self.aperture_value = aperture_value
+ self.exposure_compensation = exposure_compensation
+ self.metering_mode = metering_mode
+ self.flash = flash
+ self.focal_length = focal_length
+ self.sub_sec_time = sub_sec_time
+ self.sub_sec_time_original = sub_sec_time_original
+ self.sub_sec_time_digitized = sub_sec_time_digitized
+ self.flashpix_version = flashpix_version
+ self.color_space = color_space
+ self.exif_image_width = exif_image_width
+ self.exif_image_height = exif_image_height
+ self.interop_offset = interop_offset
+ self.focal_plane_x_resolution = focal_plane_x_resolution
+ self.focal_plane_y_resolution = focal_plane_y_resolution
+ self.focal_plane_resolution_unit = focal_plane_resolution_unit
+ self.custom_rendered = custom_rendered
+ self.exposure_mode = exposure_mode
+ self.white_balance = white_balance
+ self.scene_capture_type = scene_capture_type
diff --git a/imagekitio/models/results/MetadataExifGPS.py b/imagekitio/models/results/MetadataExifGPS.py
new file mode 100644
index 0000000..54831cf
--- /dev/null
+++ b/imagekitio/models/results/MetadataExifGPS.py
@@ -0,0 +1,10 @@
+from typing import List
+
+
+class MetadataExifGPS:
+ def __init__(self, gps_version_id=None):
+ if gps_version_id is None:
+ gps_version_id = []
+ self.gps_version_id: List[int] = []
+ for i in gps_version_id:
+ self.gps_version_id.append(i)
diff --git a/imagekitio/models/results/MetadataExifImage.py b/imagekitio/models/results/MetadataExifImage.py
new file mode 100644
index 0000000..e8d605c
--- /dev/null
+++ b/imagekitio/models/results/MetadataExifImage.py
@@ -0,0 +1,26 @@
+class MetadataExifImage:
+ def __init__(
+ self,
+ make=None,
+ model=None,
+ orientation=None,
+ x_resolution=None,
+ y_resolution=None,
+ resolution_unit=None,
+ software=None,
+ modify_date=None,
+ y_cb_cr_positioning=None,
+ exif_offset=None,
+ gps_info=None,
+ ):
+ self.make = make
+ self.model = model
+ self.orientation = orientation
+ self.x_resolution = x_resolution
+ self.y_resolution = y_resolution
+ self.resolution_unit = resolution_unit
+ self.software = software
+ self.modify_date = modify_date
+ self.y_cb_cr_positioning = y_cb_cr_positioning
+ self.exif_offset = exif_offset
+ self.gps_info = gps_info
diff --git a/imagekitio/models/results/MetadataExifInteroperability.py b/imagekitio/models/results/MetadataExifInteroperability.py
new file mode 100644
index 0000000..62dc7ed
--- /dev/null
+++ b/imagekitio/models/results/MetadataExifInteroperability.py
@@ -0,0 +1,4 @@
+class MetadataExifInteroperability:
+ def __init__(self, interop_index=None, interop_version=None):
+ self.interop_index = interop_index
+ self.interop_version = interop_version
diff --git a/imagekitio/models/results/MetadataExifThumbnail.py b/imagekitio/models/results/MetadataExifThumbnail.py
new file mode 100644
index 0000000..66baeab
--- /dev/null
+++ b/imagekitio/models/results/MetadataExifThumbnail.py
@@ -0,0 +1,16 @@
+class MetadataExifThumbnail:
+ def __init__(
+ self,
+ compression=None,
+ x_resolution=None,
+ y_resolution=None,
+ resolution_unit=None,
+ thumbnail_offset=None,
+ thumbnail_length=None,
+ ):
+ self.compression = compression
+ self.x_resolution = x_resolution
+ self.y_resolution = y_resolution
+ self.resolution_unit = resolution_unit
+ self.thumbnail_offset = thumbnail_offset
+ self.thumbnail_length = thumbnail_length
diff --git a/imagekitio/models/results/PurgeCacheResult.py b/imagekitio/models/results/PurgeCacheResult.py
new file mode 100644
index 0000000..ca7cd3c
--- /dev/null
+++ b/imagekitio/models/results/PurgeCacheResult.py
@@ -0,0 +1,15 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class PurgeCacheResult:
+ def __init__(self, request_id=None):
+ self.request_id = request_id
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/PurgeCacheStatusResult.py b/imagekitio/models/results/PurgeCacheStatusResult.py
new file mode 100644
index 0000000..bd2153f
--- /dev/null
+++ b/imagekitio/models/results/PurgeCacheStatusResult.py
@@ -0,0 +1,15 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class PurgeCacheStatusResult:
+ def __init__(self, status=None):
+ self.status = status
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/RenameFileResult.py b/imagekitio/models/results/RenameFileResult.py
new file mode 100644
index 0000000..ab2a7f6
--- /dev/null
+++ b/imagekitio/models/results/RenameFileResult.py
@@ -0,0 +1,15 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class RenameFileResult:
+ def __init__(self, purge_request_id: str = None):
+ self.purge_request_id = purge_request_id
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/ResponseMetadata.py b/imagekitio/models/results/ResponseMetadata.py
new file mode 100644
index 0000000..43925d4
--- /dev/null
+++ b/imagekitio/models/results/ResponseMetadata.py
@@ -0,0 +1,5 @@
+class ResponseMetadata:
+ def __init__(self, raw=None, http_status_code=None, headers=None):
+ self.raw = raw
+ self.http_status_code = http_status_code
+ self.headers = headers
diff --git a/imagekitio/models/results/ResponseMetadataResult.py b/imagekitio/models/results/ResponseMetadataResult.py
new file mode 100644
index 0000000..92ce25e
--- /dev/null
+++ b/imagekitio/models/results/ResponseMetadataResult.py
@@ -0,0 +1,14 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class ResponseMetadataResult:
+ def __init__(self):
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/TagsResult.py b/imagekitio/models/results/TagsResult.py
new file mode 100644
index 0000000..2a5cbe3
--- /dev/null
+++ b/imagekitio/models/results/TagsResult.py
@@ -0,0 +1,15 @@
+from .ResponseMetadata import ResponseMetadata
+
+
+class TagsResult:
+ def __init__(self, successfully_updated_file_ids=None):
+ self.successfully_updated_file_ids = successfully_updated_file_ids
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/UploadFileResult.py b/imagekitio/models/results/UploadFileResult.py
new file mode 100644
index 0000000..19e2858
--- /dev/null
+++ b/imagekitio/models/results/UploadFileResult.py
@@ -0,0 +1,73 @@
+from typing import List
+
+from .AITags import AITags
+from .EmbeddedMetadata import EmbeddedMetadata
+from .ResponseMetadata import ResponseMetadata
+from .VersionInfo import VersionInfo
+
+
+class UploadFileResult:
+ def __init__(
+ self,
+ file_id=None,
+ name=None,
+ url=None,
+ thumbnail_url: str = None,
+ height: int = None,
+ width: int = None,
+ size: int = None,
+ file_path: str = None,
+ tags: dict = None,
+ ai_tags: List[AITags] = AITags(None, None, None),
+ version_info: VersionInfo = VersionInfo(None, None),
+ is_private_file=False,
+ custom_coordinates: dict = None,
+ custom_metadata: dict = None,
+ embedded_metadata: EmbeddedMetadata = EmbeddedMetadata(None, None, None, None),
+ extension_status: dict = None,
+ file_type: str = None,
+ orientation: int = None
+ ):
+ self.file_id = file_id
+ self.name = name
+ self.url = url
+ self.thumbnail_url = thumbnail_url
+ self.height = height
+ self.width = width
+ self.size = size
+ self.file_path = file_path
+ self.tags = tags
+ self.ai_tags: List[AITags] = []
+ if ai_tags is not None:
+ for i in ai_tags:
+ self.ai_tags.append(AITags(i["name"], i["confidence"], i["source"]))
+ else:
+ self.ai_tags.append(AITags(None, None, None))
+ self.version_info = VersionInfo(version_info["id"], version_info["name"])
+ self.is_private_file = is_private_file
+ self.custom_coordinates = custom_coordinates
+ self.custom_metadata = custom_metadata
+ if embedded_metadata is None or embedded_metadata == {}:
+ self.embedded_metadata = EmbeddedMetadata(None, None, None, None)
+ else:
+ if type(embedded_metadata) == EmbeddedMetadata:
+ self.embedded_metadata = embedded_metadata
+ else:
+ self.embedded_metadata: EmbeddedMetadata = EmbeddedMetadata(
+ embedded_metadata["x_resolution"],
+ embedded_metadata["y_resolution"],
+ embedded_metadata["date_created"],
+ embedded_metadata["date_timecreated"],
+ )
+ self.extension_status = extension_status
+ self.file_type = file_type
+ self.orientation = orientation
+ self.__response_metadata: ResponseMetadata = ResponseMetadata("", "", "")
+
+ @property
+ def response_metadata(self):
+ return self.__response_metadata
+
+ @response_metadata.setter
+ def response_metadata(self, value):
+ self.__response_metadata = value
diff --git a/imagekitio/models/results/VersionInfo.py b/imagekitio/models/results/VersionInfo.py
new file mode 100644
index 0000000..81bbaad
--- /dev/null
+++ b/imagekitio/models/results/VersionInfo.py
@@ -0,0 +1,4 @@
+class VersionInfo:
+ def __init__(self, id=None, name=None):
+ self.id = id
+ self.name = name
diff --git a/imagekitio/models/results/__init__.py b/imagekitio/models/results/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/imagekitio/resource.py b/imagekitio/resource.py
index 4767b70..20855a7 100644
--- a/imagekitio/resource.py
+++ b/imagekitio/resource.py
@@ -30,8 +30,7 @@ def __init__(
raise ValueError(ERRORS.MANDATORY_INITIALIZATION_MISSING.value)
def create_headers(self):
- """Create headers dict and sets Authorization header
- """
+ """Create headers dict and sets Authorization header"""
headers = {"Accept-Encoding": "gzip, deflate"}
headers.update(self.get_auth_headers())
return headers
@@ -42,15 +41,14 @@ def get_auth_headers(self):
:return: dictionary of encoded private key
"""
- encoded_private_key = base64.b64encode((self.private_key + ":").encode()).decode(
- "utf-8"
- )
+ encoded_private_key = base64.b64encode(
+ (self.private_key + ":").encode()
+ ).decode("utf-8")
return {"Authorization": "Basic {}".format(encoded_private_key)}
@staticmethod
def request(method, url, headers, params=None, files=None, data=None) -> Response:
- """Requests from ImageKit server used,by internal methods
- """
+ """Requests from ImageKit server used,by internal methods"""
resp = requests.request(
method=method,
url=url,
diff --git a/imagekitio/url.py b/imagekitio/url.py
index 43636ab..6e5b399 100644
--- a/imagekitio/url.py
+++ b/imagekitio/url.py
@@ -5,11 +5,11 @@
from typing import Any, Dict, List
from urllib.parse import ParseResult, urlparse, urlunparse, parse_qsl, urlencode
-from imagekitio.constants.defaults import Default
-from imagekitio.constants.supported_transform import SUPPORTED_TRANS
-from imagekitio.utils.formatter import camel_dict_to_snake_dict, flatten_dict
+from .constants.defaults import Default
+from .constants.supported_transform import SUPPORTED_TRANS
+from .utils.formatter import camel_dict_to_snake_dict, flatten_dict
-from .constants import ERRORS
+from .constants.errors import ERRORS
class Url:
@@ -30,18 +30,20 @@ def build_url(self, options: dict) -> str:
"""
builds url for from all options,
"""
-
+
# important to strip the trailing slashes. later logic assumes no trailing slashes.
path = options.get("path", "").strip("/")
src = options.get("src", "").strip("/")
url_endpoint = options.get("url_endpoint", "").strip("/")
transformation_str = self.transformation_to_str(options.get("transformation"))
- transformation_position = options.get("transformation_position", Default.DEFAULT_TRANSFORMATION_POSITION.value)
+ transformation_position = options.get(
+ "transformation_position", Default.DEFAULT_TRANSFORMATION_POSITION.value
+ )
if transformation_position not in Default.VALID_TRANSFORMATION_POSITION.value:
raise ValueError(ERRORS.INVALID_TRANSFORMATION_POSITION.value)
- if (path == "" and src == ""):
+ if path == "" and src == "":
return ""
# if path is present then it is given priority over src parameter
@@ -51,13 +53,10 @@ def build_url(self, options: dict) -> str:
url_endpoint,
Default.TRANSFORMATION_PARAMETER.value,
transformation_str.strip("/"),
- path
+ path,
)
else:
- temp_url = "{}/{}".format(
- url_endpoint,
- path
- )
+ temp_url = "{}/{}".format(url_endpoint, path)
else:
temp_url = src
# if src parameter is used, then we force transformation position in query
@@ -67,9 +66,13 @@ def build_url(self, options: dict) -> str:
query_params = dict(parse_qsl(url_object.query))
query_params.update(options.get("query_parameters", {}))
- if transformation_position == Default.QUERY_TRANSFORMATION_POSITION.value and len(transformation_str) != 0:
- query_params.update({Default.TRANSFORMATION_PARAMETER.value: transformation_str})
- query_params.update({Default.SDK_VERSION_PARAMETER.value: Default.SDK_VERSION.value})
+ if (
+ transformation_position == Default.QUERY_TRANSFORMATION_POSITION.value
+ and len(transformation_str) != 0
+ ):
+ query_params.update(
+ {Default.TRANSFORMATION_PARAMETER.value: transformation_str}
+ )
# Update query params in the url
url_object = url_object._replace(query=urlencode(query_params))
@@ -87,16 +90,21 @@ def build_url(self, options: dict) -> str:
"""
If the expire_seconds parameter is specified then the output URL contains
- ik-t parameter (unix timestamp seconds when the URL expires) and
- the signature contains the timestamp for computation.
-
+ ik-t parameter (unix timestamp seconds when the URL expires) and
+ the signature contains the timestamp for computation.
+
If not present, then no ik-t parameter and the value 9999999999 is used.
"""
if expire_seconds:
- query_params.update({Default.TIMESTAMP_PARAMETER.value: expiry_timestamp, Default.SIGNATURE_PARAMETER.value: url_signature})
+ query_params.update(
+ {
+ Default.TIMESTAMP_PARAMETER.value: expiry_timestamp,
+ Default.SIGNATURE_PARAMETER.value: url_signature,
+ }
+ )
else:
query_params.update({Default.SIGNATURE_PARAMETER.value: url_signature})
-
+
# Update signature related query params
url_object = url_object._replace(query=urlencode(query_params))
@@ -106,7 +114,7 @@ def build_url(self, options: dict) -> str:
def get_signature_timestamp(expiry_seconds: int = None) -> int:
"""
this function returns the signature timestamp to be used
- with the generated url.
+ with the generated url.
If expiry_seconds is provided, it returns expiry_seconds added
to the current unix time, otherwise the default time stamp
is returned.
@@ -118,14 +126,14 @@ def get_signature_timestamp(expiry_seconds: int = None) -> int:
return current_timestamp + expiry_seconds
@staticmethod
- def get_signature(private_key, url, url_endpoint, expiry_timestamp : int) -> str:
- """"
+ def get_signature(private_key, url, url_endpoint, expiry_timestamp: int) -> str:
+ """ "
create signature(hashed hex key) from
private_key, url, url_endpoint and expiry_timestamp
"""
# ensure url_endpoint has a trailing slash
- if url_endpoint[-1] != '/':
- url_endpoint += '/'
+ if url_endpoint[-1] != "/":
+ url_endpoint += "/"
if expiry_timestamp < 1:
expiry_timestamp = Default.DEFAULT_TIMESTAMP.value
@@ -162,8 +170,8 @@ def is_valid_transformation_pos(trans_pos: str) -> bool:
@staticmethod
def transformation_to_str(transformation):
"""
- creates transformation_position string for url from
- transformation_position dictionary
+ creates transformation_position string for url from
+ transformation_position dictionary
"""
if not isinstance(transformation, list):
return ""
@@ -174,25 +182,29 @@ def transformation_to_str(transformation):
transform_key = SUPPORTED_TRANS.get(key, "")
if not transform_key:
transform_key = key
-
if transformation[i][key] == "-":
parsed_transform_step.append(transform_key)
else:
value = transformation[i][key]
if isinstance(value, bool):
value = str(value).lower()
- if transform_key == "oi" or transform_key == "di":
+ if transform_key == "oi" or transform_key == "di":
value = value.strip("/")
- value = value.replace("/","@@")
- parsed_transform_step.append(
- "{}{}{}".format(
- transform_key,
- Default.TRANSFORM_KEY_VALUE_DELIMITER.value,
- value,
+ value = value.replace("/", "@@")
+ if transform_key == "raw":
+ for i in value.split(","):
+ parsed_transform_step.append(i)
+ else:
+ parsed_transform_step.append(
+ "{}{}{}".format(
+ transform_key,
+ Default.TRANSFORM_KEY_VALUE_DELIMITER.value,
+ value,
+ )
)
- )
parsed_transforms.append(
- Default.TRANSFORM_DELIMITER.value.join(parsed_transform_step))
+ Default.TRANSFORM_DELIMITER.value.join(parsed_transform_step)
+ )
return Default.CHAIN_TRANSFORM_DELIMITER.value.join(parsed_transforms)
diff --git a/imagekitio/utils/calculation.py b/imagekitio/utils/calculation.py
index 0948096..8bf495a 100644
--- a/imagekitio/utils/calculation.py
+++ b/imagekitio/utils/calculation.py
@@ -3,14 +3,13 @@
import uuid
from datetime import datetime as dt
-from imagekitio.constants import ERRORS
+from ..constants.errors import ERRORS
DEFAULT_TIME_DIFF = 60 * 30
def hamming_distance(first: str, second: str) -> int:
- """Calculate Hamming Distance between to hex string
- """
+ """Calculate Hamming Distance between to hex string"""
try:
a = bin(int(first, 16))[2:].zfill(64)
b = bin(int(second, 16))[2:].zfill(64)
diff --git a/imagekitio/utils/formatter.py b/imagekitio/utils/formatter.py
index 56cfc8b..335956d 100644
--- a/imagekitio/utils/formatter.py
+++ b/imagekitio/utils/formatter.py
@@ -31,8 +31,7 @@ def request_formatter(data: dict) -> dict:
def camel_dict_to_snake_dict(data: dict) -> dict:
- """Convert the keys of dictionary from camel case to snake case
- """
+ """Convert the keys of dictionary from camel case to snake case"""
return {camel_to_snake(key): val for key, val in data.items()}
diff --git a/imagekitio/utils/utils.py b/imagekitio/utils/utils.py
new file mode 100644
index 0000000..2cfba0f
--- /dev/null
+++ b/imagekitio/utils/utils.py
@@ -0,0 +1,104 @@
+import ast
+from json import loads, dumps
+from requests.models import Response
+
+from ..exceptions.BadRequestException import BadRequestException
+from ..exceptions.ForbiddenException import ForbiddenException
+from ..exceptions.InternalServerException import InternalServerException
+from ..exceptions.NotFoundException import NotFoundException
+from ..exceptions.PartialSuccessException import PartialSuccessException
+from ..exceptions.TooManyRequestsException import TooManyRequestsException
+from ..exceptions.UnauthorizedException import UnauthorizedException
+from ..exceptions.UnknownException import UnknownException
+from ..models.results.ResponseMetadata import ResponseMetadata
+from ..models.results.ResponseMetadataResult import ResponseMetadataResult
+from ..utils.formatter import camel_dict_to_snake_dict
+
+try:
+ from simplejson.errors import JSONDecodeError
+except ImportError:
+ from json import JSONDecodeError
+
+
+def get_response_json(response: Response):
+ try:
+ resp = response.json()
+ except JSONDecodeError:
+ resp = response.text
+ return resp
+
+
+def populate_response_metadata(response: Response):
+ resp = get_response_json(response)
+ response_metadata = ResponseMetadata(resp, response.status_code, response.headers)
+ return response_metadata
+
+
+def general_api_throw_exception(response: Response):
+ resp = get_response_json(response)
+ response_meta_data = populate_response_metadata(response)
+ if type(resp) == str:
+ resp = ast.literal_eval(resp)
+ error_message = resp["message"] if type(resp) == dict else ""
+ response_help = resp["help"] if type(resp) == dict and "help" in resp else ""
+ if response.status_code == 400:
+ raise BadRequestException(error_message, response_help, response_meta_data)
+ elif response.status_code == 401:
+ raise UnauthorizedException(error_message, response_help, response_meta_data)
+ elif response.status_code == 403:
+ raise ForbiddenException(error_message, response_help, response_meta_data)
+ elif response.status_code == 429:
+ raise TooManyRequestsException(error_message, response_help, response_meta_data)
+ elif (
+ response.status_code == 500
+ or response.status_code == 502
+ or response.status_code == 503
+ or response.status_code == 504
+ ):
+ raise InternalServerException(error_message, response_help, response_meta_data)
+ else:
+ raise UnknownException(error_message, response_help, response_meta_data)
+
+
+def throw_other_exception(response: Response):
+ resp = get_response_json(response)
+ response_meta_data = populate_response_metadata(response)
+ if type(resp) == str:
+ resp = ast.literal_eval(resp)
+ error_message = resp["message"] if type(resp) == dict else ""
+ response_help = resp["help"] if type(resp) == dict else ""
+ if response.status_code == 207:
+ raise PartialSuccessException(error_message, response_help, response_meta_data)
+ elif response.status_code == 404:
+ raise NotFoundException(error_message, response_help, response_meta_data)
+ else:
+ raise UnknownException(error_message, response_help, response_meta_data)
+
+
+def convert_to_response_object(resp: Response, response_object):
+ res_new = loads(dumps(camel_dict_to_snake_dict(resp.json())))
+ u = response_object(**res_new)
+ u.response_metadata = ResponseMetadata(resp.json(), resp.status_code, resp.headers)
+ return u
+
+
+def convert_to_response_metadata_result_object(resp: Response = None):
+ u = ResponseMetadataResult()
+ u.response_metadata = ResponseMetadata(
+ resp.json() if resp.status_code != 204 else None, resp.status_code, resp.headers
+ )
+ return u
+
+
+def convert_to_list_response_object(
+ resp: Response, response_object, list_response_object
+):
+ response_list = []
+ for item in resp.json():
+ res_new = loads(dumps(camel_dict_to_snake_dict(item)))
+ u = response_object(**res_new)
+ response_list.append(u)
+
+ u = list_response_object(response_list)
+ u.response_metadata = ResponseMetadata(resp.json(), resp.status_code, resp.headers)
+ return u
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index d5c2bc9..6b33173 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -1 +1,2 @@
requests>=2.22.0
+requests_toolbelt==0.9.1
\ No newline at end of file
diff --git a/requirements/test.txt b/requirements/test.txt
index f24fbcf..16982d3 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -2,3 +2,5 @@ requests>=2.22.0
black==19.10b0
coverage==4.5.4
tox==3.14.2
+responses==0.17.0
+requests_toolbelt==0.9.1
\ No newline at end of file
diff --git a/sample/requirements.txt b/sample/requirements.txt
deleted file mode 100644
index 566083c..0000000
--- a/sample/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-requests==2.22.0
diff --git a/sample/sample.jpg b/sample/sample.jpg
deleted file mode 100644
index 1db39e0..0000000
Binary files a/sample/sample.jpg and /dev/null differ
diff --git a/sample/sample.py b/sample/sample.py
deleted file mode 100644
index 854d6ef..0000000
--- a/sample/sample.py
+++ /dev/null
@@ -1,242 +0,0 @@
-import base64
-import sys
-
-sys.path.append("..")
-
-# #### set your private_key public_key, url_endpoint, url ### ##
-private_key = "your_public_api_key"
-public_key = "your_private_api_key"
-url_endpoint = "https://ik.imagekit.io/your_imagekit_id/"
-# dummy image url
-url = "https://file-examples.com/wp-content/uploads/2017/10/file_example_JPG_100kB.jpg"
-
-if __name__ == "__main__":
- from imagekitio.client import ImageKit
-
- imagekit = ImageKit(
- private_key=private_key, public_key=public_key, url_endpoint=url_endpoint,
- )
-
- ### The signed url generated for this file doesn't work using the Python SDK
- upload = imagekit.upload_file(
- file=open("sample.jpg", "rb"),
- file_name="testing_upload_binary_signed_private.jpg",
- options={
- "response_fields": ["is_private_file", "tags"],
- "is_private_file": False,
- "folder" : "/testing-python-folder/",
- "tags": ["abc", "def"]
- },
- )
-
- print("-------------------------------------")
- print("Upload with binary")
- print("-------------------------------------")
- print(upload, end="\n\n")
-
- image_url = imagekit.url(
- {
- "path": upload['response']['filePath'],
- "query_parameters": {"v": "123"},
- "transformation": [{"height": "300", "width": "400"}],
- "signed": True,
- "expire_seconds": 3000,
- }
- )
-
- print("-------------------------------------")
- print("Signed url")
- print("-------------------------------------")
- print(image_url, end="\n\n")
-
-
- # URL generation using image path and image hostname
- image_url = imagekit.url(
- {
- "path": "default-image.jpg",
- "url_endpoint": url_endpoint,
- "transformation": [{"height": "300", "width": "400"}],
- }
- )
-
- print("-------------------------------------")
- print("Url using image path")
- print("-------------------------------------")
- print(image_url, end="\n\n")
-
- # 2 Using full image URL
- image_url = imagekit.url(
- {
- "src": url_endpoint.rstrip("/") + "/default-image.jpg",
- "transformation": [{"height": "300", "width": "400"}],
- }
- )
-
- print("-------------------------------------")
- print("Url using src")
- print("-------------------------------------")
- print(image_url, end="\n\n")
-
- image_url = imagekit.url(
- {
- "path": "/default-image.jpg",
- "url_endpoint": "https://www.example.com",
- "transformation": [{"height": "300", "width": "400"}, {"rotation": 90}],
- "transformation_position": "query",
- }
- )
-
- print("-------------------------------------")
- print("Chained transformation")
- print("-------------------------------------")
- print(image_url, end="\n\n")
-
- image_url = imagekit.url(
- {
- "src": url_endpoint.rstrip("/") + "/default-image.jpg",
- "transformation": [
- {
- "format": "jpg",
- "progressive": "true",
- "effect_sharpen": "-",
- "effect_contrast": "1",
- }
- ],
- }
- )
-
- print("-------------------------------------")
- print("Sharpening and contrast transformation")
- print("-------------------------------------")
- print(image_url, end="\n\n")
-
- list_files = imagekit.list_files({"skip": 0, "limit": 5})
- bulk_ids = [
- list_files["response"][3]["fileId"],
- list_files["response"][4]["fileId"],
- ]
-
- print("-------------------------------------")
- print("List files")
- print("-------------------------------------")
- print(list_files, end="\n\n")
-
- upload = imagekit.upload_file(
- file=open("sample.jpg", "rb"),
- file_name="testing-binary.jpg",
- options={
- "response_fields": ["is_private_file", "tags"],
- "tags": ["abc", "def"],
- "use_unique_file_name": False,
- },
- )
-
- print("-------------------------------------")
- print("Upload with binary")
- print("-------------------------------------")
- print(upload, end="\n\n")
-
- file_id = upload["response"]["fileId"]
-
- upload = imagekit.upload_file(
- file=url,
- file_name="testing-url.jpg",
- options={
- "response_fields": ["is_private_file"],
- "is_private_file": False,
- "tags": ["abc", "def"],
- },
- )
- image_url = upload["response"]["url"]
-
- print("-------------------------------------")
- print("Upload with url")
- print("-------------------------------------")
- print(upload, end="\n\n")
-
- with open("sample.jpg", mode="rb") as img:
- imgstr = base64.b64encode(img.read())
-
- upload_base64 = imagekit.upload_file(
- file=imgstr,
- file_name="testing-base64.jpg",
- options={
- "response_fields": ["is_private_file", "metadata", "tags"],
- "is_private_file": False,
- "tags": ["abc", "def"],
- },
- )
-
-
- print("-------------------------------------")
- print("Upload with base64")
- print("-------------------------------------")
- print(upload_base64, end="\n\n")
-
- updated_detail = imagekit.update_file_details(
- list_files["response"][0]["fileId"],
- {"tags": None, "custom_coordinates": "10,10,100,100"},
- )
-
- print("-------------------------------------")
- print("Update file details")
- print("-------------------------------------")
- print(updated_detail, end="\n\n")
-
- details = imagekit.get_file_details(list_files["response"][0]["fileId"])
- print("-------------------------------------")
- print("Get file details")
- print("-------------------------------------")
- print(details, end="\n\n")
-
- file_metadata = imagekit.get_file_metadata(list_files["response"][0]["fileId"])
- print("-------------------------------------")
- print("File metadata")
- print("-------------------------------------")
- print(file_metadata, end="\n\n")
-
-
- delete = imagekit.delete_file(list_files["response"][1]["fileId"])
- print("-------------------------------------")
- print("Delete file")
- print("-------------------------------------")
- print(delete, end="\n\n")
-
-
- purge_cache = imagekit.purge_file_cache(file_url=image_url)
- print("-------------------------------------")
- print("Purge cache")
- print("-------------------------------------")
- print(purge_cache, end="\n\n")
-
- request_id = purge_cache["response"]["request_id"]
- purge_cache_status = imagekit.get_purge_file_cache_status(request_id)
-
- print("-------------------------------------")
- print("Cache status")
- print("-------------------------------------")
- print(purge_cache_status, end="\n\n")
-
- auth_params = imagekit.get_authentication_parameters()
- print("-------------------------------------")
- print("Auth params")
- print("-------------------------------------")
- print(auth_params, end="\n\n")
-
- print("-------------------------------------")
- print("Phash distance")
- print("-------------------------------------")
- print(imagekit.phash_distance("f06830ca9f1e3e90", "f06830ca9f1e3e90"), end="\n\n")
-
-
-
- print("-------------------------------------")
- print("Bulk file delete")
- print("-------------------------------------")
- print(imagekit.bulk_file_delete(bulk_ids), end="\n\n")
-
- remote_file_url = upload["response"]["url"]
- print("-------------------------------------")
- print("Get metatdata via url")
- print("-------------------------------------")
- print(imagekit.get_remote_file_url_metadata(remote_file_url))
diff --git a/setup.py b/setup.py
index 5ad5ff2..7bb8bd8 100644
--- a/setup.py
+++ b/setup.py
@@ -8,7 +8,7 @@
setuptools.setup(
name="imagekitio",
- version="2.2.8",
+ version="3.0.0",
description="Python wrapper for the ImageKit API",
long_description=long_description,
long_description_content_type="text/markdown",
diff --git a/tests/helpers.py b/tests/helpers.py
index 59e7ed4..62fb542 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -1,22 +1,21 @@
+import base64
+import json
+import re
import unittest
-from typing import Any
-from unittest.mock import Mock, patch
-
-from requests import Response
+from unittest.mock import patch
from imagekitio.client import ImageKit
-from tests.dummy_data.file import AUTHENTICATION_ERR_MSG, SUCCESS_GENERIC_RESP
-try:
- from simplejson.errors import JSONDecodeError
-except ImportError:
- from json import JSONDecodeError
+from imagekitio.models.ListAndSearchFileRequestOptions import (
+ ListAndSearchFileRequestOptions,
+)
class ClientTestCase(unittest.TestCase):
"""
Base TestCase for Client
"""
- private_key="fake122"
+
+ private_key = "fake122"
@patch("imagekitio.file.File")
@patch("imagekitio.resource.ImageKitRequest")
@@ -24,44 +23,41 @@ def setUp(self, mock_file, mock_req):
"""
Tests if list_files work with skip and limit
"""
- self.options = {
- "skip": "10",
- "limit": "1",
- }
+ self.options = ListAndSearchFileRequestOptions(
+ type="file",
+ sort="ASC_CREATED",
+ path="/",
+ search_query="created_at >= '2d' OR size < '2mb' OR format='png'",
+ file_type="all",
+ limit=1,
+ skip=0,
+ tags="Tag-1, Tag-2, Tag-3",
+ )
+ self.opt = ListAndSearchFileRequestOptions(
+ type="file",
+ sort="ASC_CREATED",
+ path="/",
+ search_query="created_at >= '2d' OR size < '2mb' OR format='png'",
+ file_type="all",
+ limit=1,
+ skip=0,
+ tags=["Tag-1", "Tag-2", "Tag-3"],
+ )
self.client = ImageKit(
- public_key="fake122", private_key=ClientTestCase.private_key, url_endpoint="fake122",
+ public_key="fake122",
+ private_key=ClientTestCase.private_key,
+ url_endpoint="fake122",
)
-def get_mocked_failed_resp(message=None, status=401):
- """GET failed mocked response customized by parameter
- """
- mocked_resp = Mock(spec=Response)
- mocked_resp.status_code = status
- if not message:
- mocked_resp.json.return_value = AUTHENTICATION_ERR_MSG
- else:
- mocked_resp.json.return_value = message
- return mocked_resp
+def create_headers_for_test():
+ headers = {"Accept-Encoding": "gzip, deflate"}
+ headers.update(get_auth_headers_for_test())
+ return headers
-def get_mocked_failed_resp_text():
- """GET failed mocked response returned as text not json
- """
- mocked_resp = Mock(spec=Response)
- mocked_resp.status_code = 502
- mocked_resp.text = 'Bad Gateway'
- mocked_resp.json.side_effect = JSONDecodeError("Expecting value: ", "Bad Gateway", 0)
- return mocked_resp
-
-
-def get_mocked_success_resp(message: dict = None, status: int = 200):
- """GET success mocked response customize by parameter
- """
- mocked_resp = Mock(spec=Response)
- mocked_resp.status_code = status
- if not message:
- mocked_resp.json.return_value = SUCCESS_GENERIC_RESP
- else:
- mocked_resp.json.return_value = message
- return mocked_resp
+def get_auth_headers_for_test():
+ encoded_private_key = base64.b64encode(
+ (ClientTestCase.private_key + ":").encode()
+ ).decode("utf-8")
+ return {"Authorization": "Basic {}".format(encoded_private_key)}
diff --git a/tests/test_client.py b/tests/test_client.py
index 06afeb2..674bfae 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,19 +1,18 @@
import unittest
-from unittest.mock import MagicMock
from imagekitio.client import ImageKit
-from tests.dummy_data.file import SUCCESS_DETAIL_MSG
-from tests.helpers import ClientTestCase, get_mocked_success_resp
+from tests.helpers import ClientTestCase
imagekit_obj = ImageKit(
- private_key="private_fake:", public_key="public_fake123:", url_endpoint="fake.com",
+ private_key="private_fake:",
+ public_key="public_fake123:",
+ url_endpoint="fake.com",
)
class TestPHashDistance(unittest.TestCase):
def test_phash_distance(self):
- """Tests if phash_distance working properly
- """
+ """Tests if phash_distance working properly"""
a, b = ("33699c96619cc69e", "968e978414fe04ea")
c, d = ("33699c96619cc69e", "33699c96619cc69e")
e, f = ("a4a65595ac94518b", "7838873e791f8400")
diff --git a/tests/test_custom_metadata_fields_ops.py b/tests/test_custom_metadata_fields_ops.py
new file mode 100644
index 0000000..2d22233
--- /dev/null
+++ b/tests/test_custom_metadata_fields_ops.py
@@ -0,0 +1,900 @@
+import json
+
+import responses
+from responses import matchers
+
+from imagekitio.constants.url import URL
+from imagekitio.exceptions.BadRequestException import BadRequestException
+from imagekitio.exceptions.ForbiddenException import ForbiddenException
+from imagekitio.exceptions.NotFoundException import NotFoundException
+from imagekitio.exceptions.UnknownException import UnknownException
+from imagekitio.models.CreateCustomMetadataFieldsRequestOptions import (
+ CreateCustomMetadataFieldsRequestOptions,
+)
+from imagekitio.models.CustomMetaDataTypeEnum import CustomMetaDataTypeEnum
+from imagekitio.models.CustomMetadataFieldsSchema import CustomMetadataFieldsSchema
+from imagekitio.models.UpdateCustomMetadataFieldsRequestOptions import (
+ UpdateCustomMetadataFieldsRequestOptions,
+)
+from imagekitio.utils.formatter import camel_dict_to_snake_dict
+from tests.helpers import (
+ ClientTestCase,
+ create_headers_for_test,
+)
+
+
+class TestCustomMetadataFields(ClientTestCase):
+ """
+ TestCustomMetadataFields class used to test CRUD methods of custom metadata fields
+ """
+
+ field_id = "field_id"
+
+ @responses.activate
+ def test_get_custom_metadata_fields_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=403,
+ body="""{"message": "Your account cannot be authenticated."
+ , "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_custom_metadata_fields(True)
+ self.assertRaises(ForbiddenException)
+ except UnknownException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_custom_metadata_fields_succeeds(self):
+ """
+ Tests if get_custom_metadata_fields succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = create_headers_for_test()
+ responses.add(
+ responses.GET,
+ url,
+ body="""[{
+ "id": "62a9d5f6db485107347bb7f2",
+ "name": "test10",
+ "label": "test10",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": false,
+ "minValue": 10,
+ "maxValue": 1000
+ }
+ }, {
+ "id": "62aab2cfdb4851833b8f5e64",
+ "name": "test11",
+ "label": "test11",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": false,
+ "minValue": 10,
+ "maxValue": 1000
+ }
+ }]""",
+ match=[matchers.query_string_matcher("includeDeleted=false")],
+ headers=headers,
+ )
+ resp = self.client.get_custom_metadata_fields()
+
+ mock_response_metadata = {
+ "raw": [
+ {
+ "id": "62a9d5f6db485107347bb7f2",
+ "name": "test10",
+ "label": "test10",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": False,
+ "minValue": 10,
+ "maxValue": 1000,
+ },
+ },
+ {
+ "id": "62aab2cfdb4851833b8f5e64",
+ "name": "test11",
+ "label": "test11",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": False,
+ "minValue": 10,
+ "maxValue": 1000,
+ },
+ },
+ ],
+ "httpStatusCode": 200,
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("62a9d5f6db485107347bb7f2", resp.list[0].id)
+ self.assertEqual("62aab2cfdb4851833b8f5e64", resp.list[1].id)
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields?includeDeleted=false",
+ responses.calls[0].request.url,
+ )
+
+ @responses.activate
+ def test_get_custom_metadata_fields_succeeds_with_include_deleted_true(self):
+ """
+ Tests if get_custom_metadata_fields succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = create_headers_for_test()
+ responses.add(
+ responses.GET,
+ url,
+ body="""[{
+ "id": "62a9d5f6db485107347bb7f2",
+ "name": "test10",
+ "label": "test10",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": false,
+ "minValue": 10,
+ "maxValue": 1000
+ }
+ }, {
+ "id": "62aab2cfdb4851833b8f5e64",
+ "name": "test11",
+ "label": "test11",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": false,
+ "minValue": 10,
+ "maxValue": 1000
+ }
+ }]""",
+ match=[matchers.query_string_matcher("includeDeleted=true")],
+ headers=headers,
+ )
+ resp = self.client.get_custom_metadata_fields(include_deleted=True)
+
+ mock_response_metadata = {
+ "raw": [
+ {
+ "id": "62a9d5f6db485107347bb7f2",
+ "name": "test10",
+ "label": "test10",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": False,
+ "minValue": 10,
+ "maxValue": 1000,
+ },
+ },
+ {
+ "id": "62aab2cfdb4851833b8f5e64",
+ "name": "test11",
+ "label": "test11",
+ "schema": {
+ "type": "Number",
+ "isValueRequired": False,
+ "minValue": 10,
+ "maxValue": 1000,
+ },
+ },
+ ],
+ "httpStatusCode": 200,
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("62a9d5f6db485107347bb7f2", resp.list[0].id)
+ self.assertEqual("62aab2cfdb4851833b8f5e64", resp.list[1].id)
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields?includeDeleted=true",
+ responses.calls[0].request.url,
+ )
+
+ @responses.activate
+ def test_delete_custom_metadata_fields_succeeds(self):
+ """
+ Tests if delete_custom_metadata_fields succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, self.field_id)
+ headers = create_headers_for_test()
+ responses.add(responses.DELETE, url, status=204, headers=headers, body="{}")
+ resp = self.client.delete_custom_metadata_field(self.field_id)
+
+ mock_response_metadata = {
+ "raw": None,
+ "httpStatusCode": 204,
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields/field_id",
+ responses.calls[0].request.url,
+ )
+
+ @responses.activate
+ def test_delete_custom_metadata_fields_fails_with_404(self):
+ """
+ Tests if delete_custom_metadata_fields succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, self.field_id)
+ try:
+ headers = create_headers_for_test()
+ responses.add(
+ responses.DELETE,
+ url,
+ status=404,
+ headers=headers,
+ body="""{"message": "No such custom metadata field exists",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.delete_custom_metadata_field(self.field_id)
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("No such custom metadata field exists", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_fails_with_400(self):
+ """
+ Tests if create_custom_metadata_fields fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=400,
+ body="""{"message": "A custom metadata field with this name already exists"
+ , "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test",
+ label="test",
+ schema=CustomMetadataFieldsSchema(
+ type=CustomMetaDataTypeEnum.Number, min_value=100, max_value=200
+ ),
+ )
+ )
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "A custom metadata field with this name already exists", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_succeeds_with_type_number(self):
+ """
+ Tests if create_custom_metadata_fields succeeds with type number
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ headers=headers,
+ body="""{
+ "id": "62dfc03b1b02a58936efca37",
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "type": "Number",
+ "minValue": 100,
+ "maxValue": 200
+ }
+ }""",
+ )
+ resp = self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test",
+ label="test",
+ schema=CustomMetadataFieldsSchema(
+ type=CustomMetaDataTypeEnum.Number, min_value=100, max_value=200
+ ),
+ )
+ )
+
+ mock_response_metadata = {
+ "raw": {
+ "id": "62dfc03b1b02a58936efca37",
+ "name": "test",
+ "label": "test",
+ "schema": {"type": "Number", "minValue": 100, "maxValue": 200},
+ },
+ "httpStatusCode": 201,
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "type": "Number",
+ "minValue": 100,
+ "maxValue": 200
+ }
+ }"""
+ )
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields", responses.calls[0].request.url
+ )
+ self.assertMultiLineEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_succeeds_with_type_textarea(self):
+ """
+ Tests if create_custom_metadata_fields succeeds with type textarea
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ headers=headers,
+ body="""{
+ "id": "62e0d7ae1b02a589360dc1fd",
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "isValueRequired": true,
+ "defaultValue": "The",
+ "type": "Textarea",
+ "minLength": 3,
+ "maxLength": 200
+ }
+ }""",
+ )
+ resp = self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test",
+ label="test",
+ schema=CustomMetadataFieldsSchema(
+ is_value_required=True,
+ default_value="The",
+ type=CustomMetaDataTypeEnum.Textarea,
+ min_length=3,
+ max_length=200,
+ ),
+ )
+ )
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 201,
+ "raw": {
+ "id": "62e0d7ae1b02a589360dc1fd",
+ "label": "test",
+ "name": "test",
+ "schema": {
+ "defaultValue": "The",
+ "isValueRequired": True,
+ "maxLength": 200,
+ "minLength": 3,
+ "type": "Textarea",
+ },
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "type": "Textarea",
+ "defaultValue": "The",
+ "isValueRequired": true,
+ "minLength": 3,
+ "maxLength": 200
+ }
+ }"""
+ )
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_succeeds_with_type_date(self):
+ """
+ Tests if create_custom_metadata_fields succeeds with type date
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ headers=headers,
+ body="""{
+ "id": "62dfc9f41b02a58936f0d284",
+ "name": "test-date",
+ "label": "test-date",
+ "schema": {
+ "type": "Date",
+ "minValue": "2022-11-29T10:11:10+00:00",
+ "maxValue": "2022-11-30T10:11:10+00:00"
+ }
+ }""",
+ )
+ resp = self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test-date",
+ label="test-date",
+ schema=CustomMetadataFieldsSchema(
+ type=CustomMetaDataTypeEnum.Date,
+ min_value="2022-11-29T10:11:10+00:00",
+ max_value="2022-11-30T10:11:10+00:00",
+ ),
+ )
+ )
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 201,
+ "raw": {
+ "id": "62dfc9f41b02a58936f0d284",
+ "label": "test-date",
+ "name": "test-date",
+ "schema": {
+ "maxValue": "2022-11-30T10:11:10+00:00",
+ "minValue": "2022-11-29T10:11:10+00:00",
+ "type": "Date",
+ },
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "name": "test-date",
+ "label": "test-date",
+ "schema": {
+ "type": "Date",
+ "minValue": "2022-11-29T10:11:10+00:00",
+ "maxValue": "2022-11-30T10:11:10+00:00"
+ }
+ }"""
+ )
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_succeeds_with_type_boolean(self):
+ """
+ Tests if create_custom_metadata_fields succeeds with type boolean
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ headers=headers,
+ body="""{
+ "id": "62dfcb801b02a58936f0fc39",
+ "name": "test-boolean",
+ "label": "test-boolean",
+ "schema": {
+ "type": "Boolean",
+ "isValueRequired": true,
+ "defaultValue": true
+ }
+ }""",
+ )
+ resp = self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test-boolean",
+ label="test-boolean",
+ schema=CustomMetadataFieldsSchema(
+ type=CustomMetaDataTypeEnum.Boolean,
+ is_value_required=True,
+ default_value=True,
+ ),
+ )
+ )
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 201,
+ "raw": {
+ "id": "62dfcb801b02a58936f0fc39",
+ "label": "test-boolean",
+ "name": "test-boolean",
+ "schema": {
+ "defaultValue": True,
+ "isValueRequired": True,
+ "type": "Boolean",
+ },
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "name": "test-boolean",
+ "label": "test-boolean",
+ "schema": {
+ "type": "Boolean",
+ "defaultValue": true,
+ "isValueRequired": true
+ }
+ }"""
+ )
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_succeeds_with_type_single_select(self):
+ """
+ Tests if create_custom_metadata_fields succeeds with type SingleSelect
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ headers=headers,
+ body="""{
+ "id": "62dfcdb21b02a58936f14c97",
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "type": "SingleSelect",
+ "selectOptions": ["small", "medium", "large", 30, 40, true]
+ }
+ }""",
+ )
+ resp = self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test",
+ label="test",
+ schema=CustomMetadataFieldsSchema(
+ type=CustomMetaDataTypeEnum.SingleSelect,
+ select_options=["small", "medium", "large", 30, 40, True],
+ ),
+ )
+ )
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 201,
+ "raw": {
+ "id": "62dfcdb21b02a58936f14c97",
+ "label": "test",
+ "name": "test",
+ "schema": {
+ "selectOptions": ["small", "medium", "large", 30, 40, True],
+ "type": "SingleSelect",
+ },
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "name": "test",
+ "label": "test",
+ "schema":
+ {
+ "type": "SingleSelect",
+ "selectOptions": ["small", "medium", "large", 30, 40,
+ true]
+ }
+ }"""
+ )
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_create_custom_metadata_fields_succeeds_with_type_multi_select(self):
+ """
+ Tests if create_custom_metadata_fields succeeds with type MultiSelect
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ headers=headers,
+ body="""{
+ "id": "62dfcf001b02a58936f17808",
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "type": "MultiSelect",
+ "isValueRequired": true,
+ "defaultValue": ["small", 30, true],
+ "selectOptions": ["small", "medium", "large", 30, 40, true]
+ }
+ }""",
+ )
+ resp = self.client.create_custom_metadata_fields(
+ options=CreateCustomMetadataFieldsRequestOptions(
+ name="test",
+ label="test",
+ schema=CustomMetadataFieldsSchema(
+ type=CustomMetaDataTypeEnum.MultiSelect,
+ is_value_required=True,
+ default_value=["small", 30, True],
+ select_options=["small", "medium", "large", 30, 40, True],
+ ),
+ )
+ )
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 201,
+ "raw": {
+ "id": "62dfcf001b02a58936f17808",
+ "label": "test",
+ "name": "test",
+ "schema": {
+ "defaultValue": ["small", 30, True],
+ "isValueRequired": True,
+ "selectOptions": ["small", "medium", "large", 30, 40, True],
+ "type": "MultiSelect",
+ },
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "name": "test",
+ "label": "test",
+ "schema": {
+ "type": "MultiSelect",
+ "selectOptions": ["small", "medium", "large", 30, 40, true],
+ "defaultValue": ["small", 30, true],
+ "isValueRequired": true
+ }
+ }"""
+ )
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_update_custom_metadata_fields_succeeds(self):
+ """
+ Tests if update_custom_metadata_fields succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, self.field_id)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.PATCH,
+ url,
+ headers=headers,
+ body="""{
+ "id": "62a9d5f6db485107347bb7f2",
+ "name": "test",
+ "label": "test-update",
+ "schema": {
+ "minValue": 100,
+ "maxValue": 200,
+ "type": "Number"
+ }
+ }""",
+ )
+
+ resp = self.client.update_custom_metadata_fields(
+ self.field_id,
+ options=UpdateCustomMetadataFieldsRequestOptions(
+ label="test-update",
+ schema=CustomMetadataFieldsSchema(min_value=100, max_value=200),
+ ),
+ )
+
+ mock_response_metadata = {
+ "raw": {
+ "id": "62a9d5f6db485107347bb7f2",
+ "name": "test",
+ "label": "test-update",
+ "schema": {"minValue": 100, "maxValue": 200, "type": "Number"},
+ },
+ "httpStatusCode": 200,
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "label": "test-update",
+ "schema": {
+ "minValue": 100,
+ "maxValue": 200
+ }
+ }"""
+ )
+ )
+
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("62a9d5f6db485107347bb7f2", resp.id)
+ self.assertEqual(
+ "http://test.com/v1/customMetadataFields/field_id",
+ responses.calls[0].request.url,
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_update_custom_metadata_fields_fails_with_404(self):
+ """
+ Tests if update_custom_metadata_fields fails with 404
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, self.field_id)
+ try:
+ responses.add(
+ responses.PATCH,
+ url,
+ status=404,
+ body="""{
+ "message": "No such custom metadata field exists",
+ "help": "For support kindly contact us at support@imagekit.io ."
+ }""",
+ )
+
+ self.client.update_custom_metadata_fields(
+ self.field_id,
+ options=UpdateCustomMetadataFieldsRequestOptions(
+ label="test-update",
+ schema=CustomMetadataFieldsSchema(min_value=100, max_value=200),
+ ),
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("No such custom metadata field exists", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_update_custom_metadata_fields_fails_with_400(self):
+ """
+ Tests if update_custom_metadata_fields fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/customMetadataFields/{}".format(URL.API_BASE_URL, self.field_id)
+ try:
+ responses.add(
+ responses.PATCH,
+ url,
+ status=400,
+ body="""{
+ "message": "Your request contains invalid ID parameter.",
+ "help": "For support kindly contact us at support@imagekit.io ."
+ }""",
+ )
+
+ self.client.update_custom_metadata_fields(
+ self.field_id,
+ options=UpdateCustomMetadataFieldsRequestOptions(
+ label="test-update",
+ schema=CustomMetadataFieldsSchema(min_value=100, max_value=200),
+ ),
+ )
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual("Your request contains invalid ID parameter.", e.message)
+ self.assertEqual(400, e.response_metadata.http_status_code)
diff --git a/tests/test_files_ops.py b/tests/test_files_ops.py
index 02f0a55..223b143 100644
--- a/tests/test_files_ops.py
+++ b/tests/test_files_ops.py
@@ -1,28 +1,33 @@
import base64
import json
import os
-from unittest.mock import MagicMock
+
+import responses
+from responses import matchers
from imagekitio.client import ImageKit
from imagekitio.constants.url import URL
-from tests.dummy_data.file import (
- FAILED_DELETE_RESP,
- SUCCESS_DETAIL_MSG,
- SUCCESS_LIST_RESP_MESSAGE,
- SUCCESS_PURGE_CACHE_MSG,
- SUCCESS_PURGE_CACHE_STATUS_MSG,
-)
+from imagekitio.exceptions.BadRequestException import BadRequestException
+from imagekitio.exceptions.ConflictException import ConflictException
+from imagekitio.exceptions.ForbiddenException import ForbiddenException
+from imagekitio.exceptions.NotFoundException import NotFoundException
+from imagekitio.exceptions.UnknownException import UnknownException
+from imagekitio.models.CopyFileRequestOptions import CopyFileRequestOptions
+from imagekitio.models.MoveFileRequestOptions import MoveFileRequestOptions
+from imagekitio.models.RenameFileRequestOptions import RenameFileRequestOptions
+from imagekitio.models.UpdateFileRequestOptions import UpdateFileRequestOptions
+from imagekitio.models.UploadFileRequestOptions import UploadFileRequestOptions
+from imagekitio.utils.formatter import camel_dict_to_snake_dict
from tests.helpers import (
ClientTestCase,
- get_mocked_failed_resp,
- get_mocked_failed_resp_text,
- get_mocked_success_resp,
+ create_headers_for_test,
+ get_auth_headers_for_test,
)
-from imagekitio.utils.formatter import request_formatter
-
imagekit_obj = ImageKit(
- private_key="private_fake:", public_key="public_fake123:", url_endpoint="fake.com",
+ private_key="private_fake:",
+ public_key="public_fake123:",
+ url_endpoint="fake.com",
)
@@ -34,166 +39,392 @@ class TestUpload(ClientTestCase):
image = os.path.join(
os.path.dirname(os.path.realpath(__file__)), "dummy_data/image.png"
)
+
+ sample_image = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "sample.jpg"
+ )
filename = "test"
+ @responses.activate
def test_upload_fails_on_unauthenticated_request(self):
"""
Tests if the unauthenticated request restricted
-
"""
-
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
- )
- resp = self.client.upload(file=self.image, file_name=self.filename)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
-
+ URL.UPLOAD_BASE_URL = "http://test.com"
+ url = "%s%s" % (URL.UPLOAD_BASE_URL, "/api/v1/files/upload")
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{"message": "Your account cannot be authenticated."
+ , "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.upload_file(
+ file=self.image,
+ file_name=self.filename,
+ options=UploadFileRequestOptions(
+ use_unique_file_name=False,
+ tags=["abc", "def"],
+ folder="/testing-python-folder/",
+ is_private_file=False,
+ custom_coordinates="10,10,20,20",
+ response_fields=[
+ "tags",
+ "custom_coordinates",
+ "is_private_file",
+ "embedded_metadata",
+ "custom_metadata",
+ ],
+ extensions=(
+ {
+ "name": "remove-bg",
+ "options": {"add_shadow": True, "bg_color": "pink"},
+ },
+ {
+ "name": "google-auto-tagging",
+ "minConfidence": 80,
+ "maxTags": 10,
+ },
+ ),
+ webhook_url="https://webhook.site/c78d617f-33bc-40d9-9e61-608999721e2e",
+ overwrite_file=True,
+ overwrite_ai_tags=False,
+ overwrite_tags=False,
+ overwrite_custom_metadata=True,
+ custom_metadata={"testss": 12},
+ ),
+ )
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(e.response_metadata.http_status_code, 403)
+
+ @responses.activate
def test_binary_upload_succeeds(self):
"""
Tests if upload succeeds
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+ URL.UPLOAD_BASE_URL = "http://test.com"
+ url = "%s%s" % (URL.UPLOAD_BASE_URL, "/api/v1/files/upload")
+ headers = create_headers_for_test()
+ responses.add(
+ responses.POST,
+ url,
+ body="""{
+ "fileId": "fake_file_id1234",
+ "name": "file_name.jpg",
+ "size": 102117,
+ "versionInfo": {
+ "id": "62d670648cdb697522602b45",
+ "name": "Version 11"
+ },
+ "filePath": "/testing-python-folder/file_name.jpg",
+ "url": "https://ik.imagekit.io/your_imagekit_id/testing-python-folder/file_name.jpg",
+ "fileType": "image",
+ "height": 700,
+ "width": 1050,
+ "thumbnailUrl": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/testing-python-folder/file_name.jpg",
+ "tags": [
+ "abc",
+ "def"
+ ],
+ "AITags": [
+ {
+ "name": "Computer",
+ "confidence": 97.66,
+ "source": "google-auto-tagging"
+ },
+ {
+ "name": "Personal computer",
+ "confidence": 94.96,
+ "source": "google-auto-tagging"
+ }
+ ],
+ "isPrivateFile": true,
+ "extensionStatus": {
+ "remove-bg": "pending",
+ "google-auto-tagging": "success"
+ }
+ }""",
+ headers=headers,
)
- file = open(self.image, "rb")
- file.close()
- resp = self.client.upload(file=file, file_name=self.filename)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- def test_base64_upload_succeeds(self):
- """
- Tests if upload succeeds
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
- )
- with open(self.image, mode="rb") as img:
+ with open(self.sample_image, mode="rb") as img:
imgstr = base64.b64encode(img.read())
-
- resp = self.client.upload(file=imgstr, file_name=self.filename)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
-
- def test_url_upload_succeeds(self):
- """
- Tests if url upload succeeds
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
- )
- resp = self.client.upload(file="example.com/abc.jpg", file_name=self.filename)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
-
- def test_file_upload_succeeds(self):
- """
- Tests if file upload succeeds
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
- )
-
- # generate expected encoded private key for the auth headers
- private_key_file_upload = ClientTestCase.private_key
- if private_key_file_upload != ":":
- private_key_file_upload += ":"
- encoded_private_key = base64.b64encode(private_key_file_upload.encode()).decode(
- "utf-8"
+ resp = self.client.upload_file(
+ file=imgstr,
+ file_name="file_name.jpg",
+ options=UploadFileRequestOptions(
+ use_unique_file_name=False,
+ tags=["abc", "def"],
+ folder="/testing-python-folder/",
+ is_private_file=True,
+ response_fields=["is_private_file", "tags"],
+ extensions=(
+ {
+ "name": "remove-bg",
+ "options": {"add_shadow": True, "bg_color": "pink"},
+ },
+ {"name": "google-auto-tagging", "minConfidence": 80, "maxTags": 10},
+ ),
+ webhook_url="url",
+ overwrite_file=True,
+ overwrite_ai_tags=False,
+ overwrite_tags=False,
+ overwrite_custom_metadata=True,
+ custom_metadata={"test100": 11},
+ ),
)
-
- resp = self.client.upload_file(file=self.image, file_name=self.filename)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- self.client.ik_request.request.assert_called_once_with(
- "Post",
- url=URL.UPLOAD_URL.value,
- files={
- 'file': (None, self.image),
- 'fileName': (None, self.filename)
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "http_status_code": 200,
+ "raw": {
+ "AITags": [
+ {
+ "confidence": 97.66,
+ "name": "Computer",
+ "source": "google-auto-tagging",
+ },
+ {
+ "confidence": 94.96,
+ "name": "Personal computer",
+ "source": "google-auto-tagging",
+ },
+ ],
+ "extensionStatus": {
+ "google-auto-tagging": "success",
+ "remove-bg": "pending",
},
- data={},
- headers={'Accept-Encoding': 'gzip, deflate', 'Authorization': "Basic {}".format(encoded_private_key)}
+ "fileId": "fake_file_id1234",
+ "filePath": "/testing-python-folder/file_name.jpg",
+ "fileType": "image",
+ "height": 700,
+ "isPrivateFile": True,
+ "name": "file_name.jpg",
+ "size": 102117,
+ "tags": ["abc", "def"],
+ "thumbnailUrl": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/testing-python-folder/file_name.jpg",
+ "url": "https://ik.imagekit.io/your_imagekit_id/testing-python-folder/file_name.jpg",
+ "versionInfo": {"id": "62d670648cdb697522602b45", "name": "Version 11"},
+ "width": 1050,
+ },
+ }
+ request_body = b'----randomBoundary---------------------\r\nContent-Disposition: form-data; name="file"\r\n\r\n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\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="fileName"\r\n\r\nfile_name.jpg\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="useUniqueFileName"\r\n\r\nfalse\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="tags"\r\n\r\nabc,def\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="folder"\r\n\r\n/testing-python-folder/\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="isPrivateFile"\r\n\r\ntrue\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="responseFields"\r\n\r\nisPrivateFile,tags\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="extensions"\r\n\r\n[{"name": "remove-bg", "options": {"add_shadow": true, "bg_color": "pink"}}, {"name": "google-auto-tagging", "minConfidence": 80, "maxTags": 10}]\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="webhookUrl"\r\n\r\nurl\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteFile"\r\n\r\ntrue\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteTags"\r\n\r\nfalse\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteCustomMetadata"\r\n\r\ntrue\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="customMetadata"\r\n\r\n{"test100": 11}\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteAITags"\r\n\r\nfalse\r\n----randomBoundary-----------------------\r\n'
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
)
+ self.assertEqual(url, responses.calls[0].request.url)
-
- def test_upload_fails_without_file_or_file_name(self) -> None:
- """Test upload raises error on missing required params
+ @responses.activate
+ def test_upload_succeeds_with_url(self):
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
- )
- self.assertRaises(TypeError, self.client.upload, file_name=self.filename)
- self.assertRaises(TypeError, self.client.upload, file=self.image)
-
- def test_absence_of_params_gives_proper_resp(self) -> None:
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
- )
- resp = self.client.upload(
- file=self.image,
- file_name="x",
- options={
- "is_private_file": "",
- "tags": None,
- "custom_coordinates": None,
- "use_unique_file_name": None,
- "folder": None
-
- }
- )
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
-
- def test_all_params_being_passed_on_upload(self) -> None:
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
- )
- resp = self.client.upload(
- file=self.image,
- file_name="fileabc",
- options={
- "is_private_file": True,
- "tags": ["abc"],
- "response_fields": ["is_private_file", "tags"],
- "custom_coordinates": "10,10,100,100",
- "use_unique_file_name": True,
- "folder": "abc"
- }
- )
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
-
- def test_upload_file_fails_without_file_or_file_name(self) -> None:
- """Test upload raises error on missing required params
+ Tests if upload succeeds
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ URL.UPLOAD_BASE_URL = "http://test.com"
+ url = "%s%s" % (URL.UPLOAD_BASE_URL, "/api/v1/files/upload")
+ headers = create_headers_for_test()
+ responses.add(
+ responses.POST,
+ url,
+ body="""{
+ "fileId": "fake_file_id1234",
+ "name": "file_name.jpg",
+ "size": 102117,
+ "versionInfo": {
+ "id": "62d670648cdb697522602b45",
+ "name": "Version 11"
+ },
+ "filePath": "/testing-python-folder/file_name.jpg",
+ "url": "https://ik.imagekit.io/your_imagekit_id/testing-python-folder/file_name.jpg",
+ "fileType": "image",
+ "height": 700,
+ "width": 1050,
+ "thumbnailUrl": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/testing-python-folder/file_name.jpg",
+ "tags": [
+ "abc",
+ "def"
+ ],
+ "AITags": [
+ {
+ "name": "Computer",
+ "confidence": 97.66,
+ "source": "google-auto-tagging"
+ },
+ {
+ "name": "Personal computer",
+ "confidence": 94.96,
+ "source": "google-auto-tagging"
+ }
+ ],
+ "isPrivateFile": true,
+ "extensionStatus": {
+ "remove-bg": "pending",
+ "google-auto-tagging": "success"
+ }
+ }""",
+ headers=headers,
)
- self.assertRaises(TypeError, self.client.upload_file, file_name=self.filename)
- self.assertRaises(TypeError, self.client.upload_file, file=self.image)
- def test_upload_file_fails_without_json_response_from_server(self) -> None:
- """Test upload raises error on non json response
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp_text()
+ file_upload_url = "https://file-examples.com/wp-content/uploads/2017/10/file_example_JPG_100kB.jpg"
+ resp = self.client.upload_file(
+ file=file_upload_url,
+ file_name="file_name.jpg",
+ options=UploadFileRequestOptions(
+ use_unique_file_name=False,
+ tags=["abc", "def"],
+ folder="/testing-python-folder/",
+ is_private_file=True,
+ response_fields=["is_private_file", "tags"],
+ extensions=(
+ {
+ "name": "remove-bg",
+ "options": {"add_shadow": True, "bg_color": "pink"},
+ },
+ {"name": "google-auto-tagging", "minConfidence": 80, "maxTags": 10},
+ ),
+ webhook_url="url",
+ overwrite_file=True,
+ overwrite_ai_tags=False,
+ overwrite_tags=False,
+ overwrite_custom_metadata=True,
+ custom_metadata={"test100": 11},
+ ),
)
- resp = self.client.upload(
- file=self.image,
- file_name="fileabc",
- options={
- "is_private_file": True,
- "tags": ["abc"],
- "response_fields": ["is_private_file", "tags"],
- "custom_coordinates": "10,10,100,100",
- "use_unique_file_name": True,
- "folder": "abc"
- }
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "http_status_code": 200,
+ "raw": {
+ "AITags": [
+ {
+ "confidence": 97.66,
+ "name": "Computer",
+ "source": "google-auto-tagging",
+ },
+ {
+ "confidence": 94.96,
+ "name": "Personal computer",
+ "source": "google-auto-tagging",
+ },
+ ],
+ "extensionStatus": {
+ "google-auto-tagging": "success",
+ "remove-bg": "pending",
+ },
+ "fileId": "fake_file_id1234",
+ "filePath": "/testing-python-folder/file_name.jpg",
+ "fileType": "image",
+ "height": 700,
+ "isPrivateFile": True,
+ "name": "file_name.jpg",
+ "size": 102117,
+ "tags": ["abc", "def"],
+ "thumbnailUrl": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/testing-python-folder/file_name.jpg",
+ "url": "https://ik.imagekit.io/your_imagekit_id/testing-python-folder/file_name.jpg",
+ "versionInfo": {"id": "62d670648cdb697522602b45", "name": "Version 11"},
+ "width": 1050,
+ },
+ }
+ request_body = b'----randomBoundary---------------------\r\nContent-Disposition: form-data; name="file"\r\n\r\nhttps://file-examples.com/wp-content/uploads/2017/10/file_example_JPG_100kB.jpg\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="fileName"\r\n\r\nfile_name.jpg\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="useUniqueFileName"\r\n\r\nfalse\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="tags"\r\n\r\nabc,def\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="folder"\r\n\r\n/testing-python-folder/\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="isPrivateFile"\r\n\r\ntrue\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="responseFields"\r\n\r\nisPrivateFile,tags\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="extensions"\r\n\r\n[{"name": "remove-bg", "options": {"add_shadow": true, "bg_color": "pink"}}, {"name": "google-auto-tagging", "minConfidence": 80, "maxTags": 10}]\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="webhookUrl"\r\n\r\nurl\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteFile"\r\n\r\ntrue\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteTags"\r\n\r\nfalse\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteCustomMetadata"\r\n\r\ntrue\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="customMetadata"\r\n\r\n{"test100": 11}\r\n----randomBoundary---------------------\r\nContent-Disposition: form-data; name="overwriteAITags"\r\n\r\nfalse\r\n----randomBoundary-----------------------\r\n'
+
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
+ self.assertEqual(url, responses.calls[0].request.url)
+
+ def test_upload_fails_without_file_name(self) -> None:
+ """Test upload raises error on missing required params"""
+ try:
+ with open(self.sample_image, mode="rb") as img:
+ imgstr = base64.b64encode(img.read())
+ self.client.upload_file(file=imgstr)
+ except TypeError as e:
+ self.assertEqual(
+ {"message": "Missing fileName parameter for upload", "help": ""},
+ e.args[0],
+ )
+
+ def test_upload_fails_without_file(self) -> None:
+ """Test upload raises error on missing required params"""
+ try:
+ self.client.upload_file(file_name="file_name.jpg")
+ except TypeError as e:
+ self.assertEqual(
+ {"message": "Missing file parameter for upload", "help": ""}, e.args[0]
+ )
+
+ @responses.activate
+ def test_upload_fails_with_400_exception(self) -> None:
+ """Test upload raises 400 error"""
+
+ URL.UPLOAD_BASE_URL = "http://test.com"
+ url = "%s%s" % (URL.UPLOAD_BASE_URL, "/api/v1/files/upload")
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=400,
+ body="""{
+ "message": "A file with the same name already exists at the exact location. We "
+ "could not overwrite it because both overwriteFile and "
+ "useUniqueFileName are set to false."
+ }""",
+ )
+ self.client.upload_file(
+ file=self.image,
+ file_name=self.filename,
+ options=UploadFileRequestOptions(
+ use_unique_file_name=False,
+ tags=["abc", "def"],
+ folder="/testing-python-folder/",
+ is_private_file=False,
+ custom_coordinates="10,10,20,20",
+ response_fields=[
+ "tags",
+ "custom_coordinates",
+ "is_private_file",
+ "embedded_metadata",
+ "custom_metadata",
+ ],
+ extensions=(
+ {
+ "name": "remove-bg",
+ "options": {"add_shadow": True, "bg_color": "pink"},
+ },
+ {
+ "name": "google-auto-tagging",
+ "minConfidence": 80,
+ "maxTags": 10,
+ },
+ ),
+ webhook_url="https://webhook.site/c78d617f-33bc-40d9-9e61-608999721e2e",
+ overwrite_file=True,
+ overwrite_ai_tags=False,
+ overwrite_tags=False,
+ overwrite_custom_metadata=True,
+ custom_metadata={"testss": 12},
+ ),
+ )
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "A file with the same name already exists at the exact location. We could not overwrite "
+ "it because both overwriteFile and useUniqueFileName are set to false.",
+ e.message,
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
class TestListFiles(ClientTestCase):
@@ -201,49 +432,269 @@ class TestListFiles(ClientTestCase):
TestListFiles class used to test list_files method
"""
+ @responses.activate
def test_list_files_fails_on_unauthenticated_request(self) -> None:
- """ Tests unauthenticated request restricted for list_files method
+ """Tests unauthenticated request restricted for list_files method"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.list_files(self.options)
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual("Your account cannot be authenticated.", e.message)
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_list_files_succeeds_with_basic_request_tags_with_array(self) -> None:
+ """
+ Tests if list_files work with options which contains type, sort, path, searchQuery, fileType, limit, skip and tags
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files".format(URL.API_BASE_URL)
+
+ headers = create_headers_for_test()
+ responses.add(
+ responses.GET,
+ url,
+ body="""[{
+ "type": "file",
+ "name": "sample-cat-image_gr64HPlJS.jpg",
+ "createdAt": "2022-06-15T08:19:00.843Z",
+ "updatedAt": "2022-06-15T08:19:45.169Z",
+ "fileId": "62a995f4d875ec08dc587b72",
+ "tags": ["Tag_1", " Tag_2", " Tag_3", "tag-to-add-2"],
+ "AITags": "",
+ "versionInfo": {
+ "id": "62a995f4d875ec08dc587b72",
+ "name": "Version 1"
+ },
+ "embeddedMetadata": {
+ "XResolution": 250,
+ "YResolution": 250,
+ "DateCreated": "2022-06-15T08:19:01.523Z",
+ "DateTimeCreated": "2022-06-15T08:19:01.524Z"
+ },
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {
+ "test100": 10
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/your_imagekit_id/sample-cat-image_gr64HPlJS.jpg",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/sample-cat-image_gr64HPlJS.jpg",
+ "fileType": "image",
+ "filePath": "/sample-cat-image_gr64HPlJS.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 23023,
+ "hasAlpha": false,
+ "mime": "image/jpeg"
+ }]""",
+ headers=headers,
+ match=[
+ matchers.query_string_matcher(
+ "%7B%22type%22:%20%22file%22,%20%22sort%22:%20%22ASC_CREATED%22,%20%22path%22:%20%22/%22,%20%22searchQuery%22:%20%22created_at%20%3E=%20'2d'%20OR%20size%20%3C%20'2mb'%20OR%20format='png'%22,%20%22fileType%22:%20%22all%22,%20%22limit%22:%201,%20%22skip%22:%200,%20%22tags%22:%20%22Tag-1,%20Tag-2,%20Tag-3%22%7D"
+ )
+ ],
)
- resp = self.client.list_files(self.options)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
+ resp = self.client.list_files(self.opt)
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": [
+ {
+ "AITags": "",
+ "createdAt": "2022-06-15T08:19:00.843Z",
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {"test100": 10},
+ "embeddedMetadata": {
+ "DateCreated": "2022-06-15T08:19:01.523Z",
+ "DateTimeCreated": "2022-06-15T08:19:01.524Z",
+ "XResolution": 250,
+ "YResolution": 250,
+ },
+ "fileId": "62a995f4d875ec08dc587b72",
+ "filePath": "/sample-cat-image_gr64HPlJS.jpg",
+ "fileType": "image",
+ "hasAlpha": False,
+ "height": 354,
+ "isPrivateFile": False,
+ "mime": "image/jpeg",
+ "name": "sample-cat-image_gr64HPlJS.jpg",
+ "size": 23023,
+ "tags": ["Tag_1", " Tag_2", " Tag_3", "tag-to-add-2"],
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/sample-cat-image_gr64HPlJS.jpg",
+ "type": "file",
+ "updatedAt": "2022-06-15T08:19:45.169Z",
+ "url": "https://ik.imagekit.io/your_imagekit_id/sample-cat-image_gr64HPlJS.jpg",
+ "versionInfo": {
+ "id": "62a995f4d875ec08dc587b72",
+ "name": "Version " "1",
+ },
+ "width": 236,
+ }
+ ],
+ }
+ self.assertEqual(
+ "http://test.com/v1/files?%7B%22type%22:%20%22file%22,%20%22sort%22:%20%22ASC_CREATED%22,%20%22path%22:%20%22/%22,%20%22searchQuery%22:%20%22created_at%20%3E=%20'2d'%20OR%20size%20%3C%20'2mb'%20OR%20format='png'%22,%20%22fileType%22:%20%22all%22,%20%22limit%22:%201,%20%22skip%22:%200,%20%22tags%22:%20%22Tag-1,%20Tag-2,%20Tag-3%22%7D",
+ responses.calls[0].request.url,
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+
+ @responses.activate
def test_list_files_succeeds_with_basic_request(self) -> None:
"""
- Tests if list_files work with skip and limit
+ Tests if list_files work with options which contains type, sort, path, searchQuery, fileType, limit, skip and tags
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_LIST_RESP_MESSAGE)
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files".format(URL.API_BASE_URL)
+
+ headers = create_headers_for_test()
+ responses.add(
+ responses.GET,
+ url,
+ body="""[{
+ "type": "file",
+ "name": "sample-cat-image_gr64HPlJS.jpg",
+ "createdAt": "2022-06-15T08:19:00.843Z",
+ "updatedAt": "2022-06-15T08:19:45.169Z",
+ "fileId": "62a995f4d875ec08dc587b72",
+ "tags": ["Tag_1", " Tag_2", " Tag_3", "tag-to-add-2"],
+ "AITags": "",
+ "versionInfo": {
+ "id": "62a995f4d875ec08dc587b72",
+ "name": "Version 1"
+ },
+ "embeddedMetadata": {
+ "XResolution": 250,
+ "YResolution": 250,
+ "DateCreated": "2022-06-15T08:19:01.523Z",
+ "DateTimeCreated": "2022-06-15T08:19:01.524Z"
+ },
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {
+ "test100": 10
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/your_imagekit_id/sample-cat-image_gr64HPlJS.jpg",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/sample-cat-image_gr64HPlJS.jpg",
+ "fileType": "image",
+ "filePath": "/sample-cat-image_gr64HPlJS.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 23023,
+ "hasAlpha": false,
+ "mime": "image/jpeg"
+ }]""",
+ headers=headers,
+ match=[
+ matchers.query_string_matcher(
+ "%7B%22type%22:%20%22file%22,%20%22sort%22:%20%22ASC_CREATED%22,%20%22path%22:%20%22/%22,%20%22searchQuery%22:%20%22created_at%20%3E=%20'2d'%20OR%20size%20%3C%20'2mb'%20OR%20format='png'%22,%20%22fileType%22:%20%22all%22,%20%22limit%22:%201,%20%22skip%22:%200,%20%22tags%22:%20%22Tag-1,%20Tag-2,%20Tag-3%22%7D"
+ )
+ ],
)
resp = self.client.list_files(self.options)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
-
- def test_list_accepting_all_parameter(self):
- """
- checking if list accept all parameter
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
- )
- resp = self.client.list_files(
- options={
- "file_type": "image",
- "tags": ["tag1", "tag2"],
- "include_folder": True,
- "name": "new-dir",
- "limit": "1",
- "skip": "1",
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
},
+ "httpStatusCode": 200,
+ "raw": [
+ {
+ "AITags": "",
+ "createdAt": "2022-06-15T08:19:00.843Z",
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {"test100": 10},
+ "embeddedMetadata": {
+ "DateCreated": "2022-06-15T08:19:01.523Z",
+ "DateTimeCreated": "2022-06-15T08:19:01.524Z",
+ "XResolution": 250,
+ "YResolution": 250,
+ },
+ "fileId": "62a995f4d875ec08dc587b72",
+ "filePath": "/sample-cat-image_gr64HPlJS.jpg",
+ "fileType": "image",
+ "hasAlpha": False,
+ "height": 354,
+ "isPrivateFile": False,
+ "mime": "image/jpeg",
+ "name": "sample-cat-image_gr64HPlJS.jpg",
+ "size": 23023,
+ "tags": ["Tag_1", " Tag_2", " Tag_3", "tag-to-add-2"],
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/sample-cat-image_gr64HPlJS.jpg",
+ "type": "file",
+ "updatedAt": "2022-06-15T08:19:45.169Z",
+ "url": "https://ik.imagekit.io/your_imagekit_id/sample-cat-image_gr64HPlJS.jpg",
+ "versionInfo": {
+ "id": "62a995f4d875ec08dc587b72",
+ "name": "Version " "1",
+ },
+ "width": 236,
+ }
+ ],
+ }
+ self.assertEqual(
+ "http://test.com/v1/files?%7B%22type%22:%20%22file%22,%20%22sort%22:%20%22ASC_CREATED%22,%20%22path%22:%20%22/%22,%20%22searchQuery%22:%20%22created_at%20%3E=%20'2d'%20OR%20size%20%3C%20'2mb'%20OR%20format='png'%22,%20%22fileType%22:%20%22all%22,%20%22limit%22:%201,%20%22skip%22:%200,%20%22tags%22:%20%22Tag-1,%20Tag-2,%20Tag-3%22%7D",
+ responses.calls[0].request.url,
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
+ @responses.activate
+ def test_list_files_fails_with_400_exception(self) -> None:
+ """Test get list of files raises 400 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=400,
+ body="""{"message": "Invalid search query - createdAt field must have a valid date value. Make "
+ "sure the value is enclosed within quotes. Please refer to the "
+ "documentation for syntax specification.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ match=[
+ matchers.query_string_matcher(
+ "%7B%22type%22:%20%22file%22,%20%22sort%22:%20%22ASC_CREATED%22,%20%22path%22:%20%22/%22,%20%22searchQuery%22:%20%22created_at%20%3E=%20'2d'%20OR%20size%20%3C%20'2mb'%20OR%20format='png'%22,%20%22fileType%22:%20%22all%22,%20%22limit%22:%201,%20%22skip%22:%200,%20%22tags%22:%20%22Tag-1,%20Tag-2,%20Tag-3%22%7D"
+ )
+ ],
+ )
+ self.client.list_files(self.options)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "Invalid search query - createdAt field must have a valid date value. Make "
+ "sure the value is enclosed within quotes. Please refer to the "
+ "documentation for syntax specification.",
+ e.message,
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
class TestGetFileDetails(ClientTestCase):
@@ -254,381 +705,1849 @@ class TestGetFileDetails(ClientTestCase):
file_id = "fake_file_id1234"
file_url = "https://example.com/default.jpg"
+ @responses.activate
def test_get_file_details_fails_on_unauthenticated_request(self) -> None:
- """Tests if get_file_details raise error on unauthenticated request
- """
-
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
- )
- resp = self.client.get_file_details(self.file_id)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
-
+ """Tests of get_file_details raise error on unauthenticated request"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/details".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.get_file_details(self.file_id)
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
def test_file_details_succeeds_with_id(self) -> None:
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_DETAIL_MSG)
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/details".format(URL.API_BASE_URL, self.file_id)
+
+ headers = create_headers_for_test()
+ responses.add(
+ responses.GET,
+ url,
+ body="""{
+ "type": "file",
+ "name": "default-image.jpg",
+ "createdAt": "2022-06-15T08:19:00.843Z",
+ "updatedAt": "2022-08-19T12:19:22.726Z",
+ "fileId": "fake_file_id1234",
+ "tags": [
+ "{Software",
+ " Developer",
+ " Engineer}",
+ "tag-to-add-2"
+ ],
+ "AITags": null,
+ "versionInfo": {
+ "id": "62a995f4d875ec08dc587b72",
+ "name": "Version 1"
+ },
+ "embeddedMetadata": {
+ "XResolution": 250,
+ "YResolution": 250,
+ "DateCreated": "2022-06-15T08:19:01.523Z",
+ "DateTimeCreated": "2022-06-15T08:19:01.524Z"
+ },
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {
+ "test100": 10
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/xyxt2lnil/default-image.jpg",
+ "thumbnail": "https://ik.imagekit.io/xyxt2lnil/tr:n-ik_ml_thumbnail/default-image.jpg",
+ "fileType": "image",
+ "filePath": "/default-image.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 23023,
+ "hasAlpha": false,
+ "mime": "image/jpeg"
+ }""",
+ headers=headers,
)
resp = self.client.get_file_details(self.file_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- def test_file_details_succeeds_with_url(self) -> None:
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_DETAIL_MSG)
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "http_status_code": 200,
+ "raw": {
+ "AITags": None,
+ "createdAt": "2022-06-15T08:19:00.843Z",
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {"test100": 10},
+ "embeddedMetadata": {
+ "DateCreated": "2022-06-15T08:19:01.523Z",
+ "DateTimeCreated": "2022-06-15T08:19:01.524Z",
+ "XResolution": 250,
+ "YResolution": 250,
+ },
+ "fileId": "fake_file_id1234",
+ "filePath": "/default-image.jpg",
+ "fileType": "image",
+ "hasAlpha": False,
+ "height": 354,
+ "isPrivateFile": False,
+ "mime": "image/jpeg",
+ "name": "default-image.jpg",
+ "size": 23023,
+ "tags": ["{Software", " Developer", " Engineer}", "tag-to-add-2"],
+ "thumbnail": "https://ik.imagekit.io/xyxt2lnil/tr:n-ik_ml_thumbnail/default-image.jpg",
+ "type": "file",
+ "updatedAt": "2022-08-19T12:19:22.726Z",
+ "url": "https://ik.imagekit.io/xyxt2lnil/default-image.jpg",
+ "versionInfo": {"id": "62a995f4d875ec08dc587b72", "name": "Version 1"},
+ "width": 236,
+ },
+ }
+
+ self.assertEqual(
+ "http://test.com/v1/files/fake_file_id1234/details",
+ responses.calls[0].request.url,
)
- resp = self.client.get_file_details(self.file_url)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("fake_file_id1234", resp.file_id)
+
+ @responses.activate
+ def test_file_details_fails_with_400_exception(self) -> None:
+ """Test get file details raises 400 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/details".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=400,
+ body="""{"message": "Your request contains invalid fileId parameter.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_file_details(self.file_id)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "Your request contains invalid fileId parameter.", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
class TestDeleteFile(ClientTestCase):
file_id = "fax_abx1223"
-
bulk_delete_ids = ["fake_123", "fake_222"]
- def test_bulk_delete_fails_on_unauthenticated_request(self) -> None:
- """Test bulk_delete on unauthenticated request
- this function checks if raises error on unauthenticated request
- to check if bulk_delete is only restricted to authenticated
- requests
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
- )
- resp = self.client.bulk_delete(self.bulk_delete_ids)
-
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
-
+ @responses.activate
def test_bulk_file_delete_fails_on_unauthenticated_request(self) -> None:
"""Test bulk_file_delete on unauthenticated request
this function checks if raises error on unauthenticated request
to check if bulk_delete is only restricted to authenticated
requests
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+
+ URL.API_BASE_URL = "http://test.com"
+ url = URL.API_BASE_URL + "/v1/files" + URL.BULK_FILE_DELETE
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.bulk_file_delete(self.bulk_delete_ids)
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(e.response_metadata.http_status_code, 403)
+
+ @responses.activate
+ def test_bulk_file_delete_succeeds(self):
+ """Test bulk_delete on authenticated request
+ this function tests if bulk_file_delete working properly
+ """
+
+ URL.API_BASE_URL = "http://test.com"
+ url = URL.API_BASE_URL + "/v1/files" + URL.BULK_FILE_DELETE
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+
+ responses.add(
+ responses.POST,
+ url,
+ body='{"successfullyDeletedFileIds": ["fake_123", "fake_222"]}',
+ headers=headers,
)
+
resp = self.client.bulk_file_delete(self.bulk_delete_ids)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
+ mock_response_metadata = {
+ "raw": {"successfullyDeletedFileIds": ["fake_123", "fake_222"]},
+ "httpStatusCode": 200,
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+ self.assertEqual(
+ '{"fileIds": ["fake_123", "fake_222"]}', responses.calls[0].request.body
+ )
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(["fake_123", "fake_222"], resp.successfully_deleted_file_ids)
+ self.assertEqual(
+ "http://test.com/v1/files/batch/deleteByFileIds",
+ responses.calls[0].request.url,
+ )
- def test_file_delete_fails_on_item_not_found(self):
+ @responses.activate
+ def test_bulk_file_delete_fails_with_404_exception(self) -> None:
+ """Test bulk_file_delete raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = URL.API_BASE_URL + "/v1/files" + URL.BULK_FILE_DELETE
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ body="""{
+ "message": "The requested file(s) does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "missingFileIds": ["fake_123", "fake_222"]
+ }""",
+ headers=headers,
+ )
+ self.client.bulk_file_delete(self.bulk_delete_ids)
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested file(s) does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual(
+ ["fake_123", "fake_222"], e.response_metadata.raw["missingFileIds"]
+ )
+
+ @responses.activate
+ def test_file_delete_fails_with_400_exception(self):
"""Test delete_file on unavailable content
- this function raising expected error if the file
+ this function raising 400 if the file
is not available
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp(message=FAILED_DELETE_RESP)
- )
- resp = self.client.delete_file(self.file_id)
-
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}".format(URL.API_BASE_URL, self.file_id)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ try:
+ responses.add(
+ responses.DELETE,
+ url,
+ status=400,
+ body="""{
+ "message": "Your request contains invalid fileId parameter.",
+ "help": "For support kindly contact us at support@imagekit.io ."
+ }""",
+ headers=headers,
+ )
+ self.client.delete_file(self.file_id)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "Your request contains invalid fileId parameter.", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
def test_file_delete_succeeds(self):
"""Test delete file on authenticated request
this function tests if delete_file working properly
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp({"error": None, "response": None})
- )
- resp = self.client.delete_file(self.file_id)
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}".format(URL.API_BASE_URL, self.file_id)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
- self.assertIsNone(resp["error"])
- self.assertIsNone(resp["response"])
+ responses.add(responses.DELETE, url, body="{}", status=204, headers=headers)
- def test_bulk_file_delete_succeeds(self):
- """Test bulk_delete on authenticated request
- this function tests if bulk_file_delete working properly
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp({"error": None, "response": {'successfullyDeletedFileIds': ['5e785a03ed03082733b979ec', '5e787c4427dd2a6c2fc564a5']}})
- )
- resp = self.client.bulk_file_delete(self.bulk_delete_ids)
+ resp = self.client.delete_file(self.file_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 204,
+ "raw": None,
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/fax_abx1223", responses.calls[0].request.url
+ )
class TestPurgeCache(ClientTestCase):
fake_image_url = "https://example.com/fakeid/fakeimage.jpg"
- def test_purge_cache_fails_on_unauthenticated_request(self) -> None:
- """Test purge_cache unauthenticated request
- this function checks if raises error on unauthenticated request
- to check if purge_cache is only restricted to authenticated request
+ @responses.activate
+ def test_purge_file_cache_fails_on_unauthenticated_request(self):
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
- )
- resp = self.client.purge_cache(self.fake_image_url)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
-
- def test_purge_file_cache_fails_on_unauthenticated_request(self) -> None:
- """Test purge_cache unauthenticated request
- this function checks if raises error on unauthenticated request
- to check if purge_cache is only restricted to authenticated request
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = URL.API_BASE_URL + "/v1/files/purge"
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.purge_file_cache(self.fake_image_url)
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_purge_file_cache_fails_with_400(self):
+ """
+ Tests if the purge_file_cache fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = URL.API_BASE_URL + "/v1/files/purge"
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=400,
+ body='{"message": "Invalid url"}',
+ )
+ self.client.purge_file_cache("url")
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual("Invalid url", e.message)
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_purge_file_cache_succeeds(self):
+ """
+ Tests if purge_file_cache succeeds
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ URL.API_BASE_URL = "http://test.com"
+ url = URL.API_BASE_URL + "/v1/files/purge"
+ responses.add(
+ responses.POST,
+ url,
+ status=201,
+ body='{"requestId": "requestId"}',
)
resp = self.client.purge_file_cache(self.fake_image_url)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
-
- def test_purge_cache_fails_without_passing_file_url(self) -> None:
- """Test purge_cache raises error on invalid_body request
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ mock_response_metadata = {
+ "raw": {"requestId": "requestId"},
+ "httpStatusCode": 201,
+ "headers": {"Content-Type": "text/plain"},
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("requestId", resp.request_id)
+ self.assertEqual(
+ "http://test.com/v1/files/purge", responses.calls[0].request.url
)
- self.assertRaises(TypeError, self.client.purge_cache)
+ self.assertEqual(
+ '{"url": "https://example.com/fakeid/fakeimage.jpg"}',
+ responses.calls[0].request.body,
+ )
+
- def test_purge_file_cache_fails_without_passing_file_url(self) -> None:
- """Test purge_file_cache raises error on invalid_body request
+class TestPurgeCacheStatus(ClientTestCase):
+ cache_request_id = "fake1234"
+
+ @responses.activate
+ def test_purge_file_cache_status_fails_with_400(self):
+ """
+ Tests if the purge_file_cache_status fails with 400
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/purge/{}".format(URL.API_BASE_URL, self.cache_request_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=400,
+ body="""{"message": "No request found for this requestId.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_purge_file_cache_status(self.cache_request_id)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual("No request found for this requestId.", e.message)
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_purge_file_cache_status_succeeds(self):
+ """
+ Tests if purge_file_cache_status succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/purge/{}".format(URL.API_BASE_URL, self.cache_request_id)
+ responses.add(
+ responses.GET,
+ url,
+ body="""{"status": "Completed"}""",
+ )
+ resp = self.client.get_purge_file_cache_status(self.cache_request_id)
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {"status": "Completed"},
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("Completed", resp.status)
+ self.assertEqual(
+ "http://test.com/v1/files/purge/fake1234", responses.calls[0].request.url
)
- self.assertRaises(TypeError, self.client.purge_file_cache)
- def test_purge_cache_succeeds(self) -> None:
- """Test purge_cache working properly
+
+class TestGetMetaData(ClientTestCase):
+ file_id = "fake_file_xbc"
+
+ fake_image_url = "https://example.com/fakeid/fakeimage.jpg"
+
+ @responses.activate
+ def test_get_file_metadata_fails_with_400(self):
+ """
+ Tests if the get_file_metadata fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/metadata".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=400,
+ body="""{"message": "Your request contains invalid fileId parameter.",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "type": "INVALID_PARAM_ERROR"}""",
+ )
+ self.client.get_file_metadata(self.file_id)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "Your request contains invalid fileId parameter.", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+ self.assertEqual("INVALID_PARAM_ERROR", e.response_metadata.raw["type"])
+
+ @responses.activate
+ def test_get_file_metadata_succeeds(self):
+ """
+ Tests if get_file_metadata succeeds
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_PURGE_CACHE_MSG)
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/metadata".format(URL.API_BASE_URL, self.file_id)
+ responses.add(
+ responses.GET,
+ url,
+ body="""{
+ "height": 354,
+ "width": 236,
+ "size": 7390,
+ "format": "jpg",
+ "hasColorProfile": false,
+ "quality": 0,
+ "density": 250,
+ "hasTransparency": false,
+ "exif": {},
+ "pHash": "2e0ed1f12eda9525"
+ }""",
+ )
+ resp = self.client.get_file_metadata(self.file_id)
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {
+ "density": 250,
+ "exif": {},
+ "format": "jpg",
+ "hasColorProfile": False,
+ "hasTransparency": False,
+ "height": 354,
+ "pHash": "2e0ed1f12eda9525",
+ "quality": 0,
+ "size": 7390,
+ "width": 236,
+ },
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/fake_file_xbc/metadata",
+ responses.calls[0].request.url,
)
- resp = self.client.purge_cache(self.fake_image_url)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- self.assertIn("request_id", resp["response"])
- def test_purge_file_cache_succeeds(self) -> None:
- """Test purge_file_cache working properly
+ @responses.activate
+ def test_get_remote_file_url_metadata_fails_with_400(self):
+ """
+ Tests if the get_remote_file_url_metadata fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/metadata".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=400,
+ body="""{
+ "message": "https://example.com/fakeid/fakeimage.jpg should be accessible using your ImageKit.io account.",
+ "help": "For support kindly contact us at support@imagekit.io ."
+ }""",
+ match=[
+ matchers.query_string_matcher(
+ "url=https://example.com/fakeid/fakeimage.jpg"
+ )
+ ],
+ )
+ self.client.get_remote_file_url_metadata(self.fake_image_url)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "https://example.com/fakeid/fakeimage.jpg should be accessible using your ImageKit.io account.",
+ e.message,
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_remote_file_url_metadata_succeeds(self):
+ """
+ Tests if get_remote_file_url_metadata succeeds
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_PURGE_CACHE_MSG)
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/metadata".format(URL.API_BASE_URL)
+ responses.add(
+ responses.GET,
+ url,
+ body="""{
+ "height": 354,
+ "width": 236,
+ "size": 7390,
+ "format": "jpg",
+ "hasColorProfile": false,
+ "quality": 0,
+ "density": 250,
+ "hasTransparency": false,
+ "exif": {},
+ "pHash": "2e0ed1f12eda9525"
+ }""",
+ match=[
+ matchers.query_string_matcher(
+ "url=https://example.com/fakeid/fakeimage.jpg"
+ )
+ ],
+ )
+ resp = self.client.get_remote_file_url_metadata(self.fake_image_url)
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {
+ "density": 250,
+ "exif": {},
+ "format": "jpg",
+ "hasColorProfile": False,
+ "hasTransparency": False,
+ "height": 354,
+ "pHash": "2e0ed1f12eda9525",
+ "quality": 0,
+ "size": 7390,
+ "width": 236,
+ },
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/metadata?url=https%3A%2F%2Fexample.com%2Ffakeid%2Ffakeimage.jpg",
+ responses.calls[0].request.url,
)
- resp = self.client.purge_file_cache(self.fake_image_url)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- self.assertIn("request_id", resp["response"])
-class TestPurgeCacheStatus(ClientTestCase):
- cache_request_id = "fake1234"
+class TestUpdateFileDetails(ClientTestCase):
+ """
+ TestUpdateFileDetails class used to update file details method
+ """
- def test_get_purge_cache_status_fails_on_unauthenticated_request(self) -> None:
- """Test get_purge_cache_status unauthenticated request
- this function checks if raises error on unauthenticated request
- to check if get_purge_cache_status is only restricted to authenticated
- user
+ file_id = "fake_123"
+
+ valid_options = UpdateFileRequestOptions(
+ tags=["tag1", "tag2"], custom_coordinates="10,10,100,100"
+ )
+
+ @responses.activate
+ def test_update_file_details_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/details/".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.PATCH,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.update_file_details(
+ file_id=self.file_id, options=self.valid_options
+ )
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_update_file_details_succeeds_with_id(self):
+ """
+ Tests if update file details succeeds with file id
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/details/".format(URL.API_BASE_URL, self.file_id)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ responses.add(
+ responses.PATCH,
+ url,
+ body="""{
+ "type": "file",
+ "name": "default-image.jpg",
+ "createdAt": "2022-07-21T10:31:22.529Z",
+ "updatedAt": "2022-07-21T10:37:11.848Z",
+ "fileId": "fake_123",
+ "tags": ["tag1", "tag2"],
+ "AITags": [{
+ "name": "Corridor",
+ "confidence": 99.39,
+ "source": "aws-auto-tagging"
+ }, {
+ "name": "Floor",
+ "confidence": 97.59,
+ "source": "aws-auto-tagging"
+ }],
+ "versionInfo": {
+ "id": "versionId",
+ "name": "Version 2"
+ },
+ "embeddedMetadata": {
+ "XResolution": 1,
+ "YResolution": 1,
+ "DateCreated": "2022-07-21T10:35:34.497Z",
+ "DateTimeCreated": "2022-07-21T10:35:34.500Z"
+ },
+ "customCoordinates": "10,10,100,100",
+ "customMetadata": {
+ "test": 11
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/your_imagekit_id/default-image.jpg",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/default-image.jpg",
+ "fileType": "image",
+ "filePath": "/default-image.jpg",
+ "height": 1000,
+ "width": 1000,
+ "size": 184425,
+ "hasAlpha": false,
+ "mime": "image/jpeg",
+ "extensionStatus": {
+ "remove-bg": "pending",
+ "google-auto-tagging": "success"
+ }
+ }""",
+ headers=headers,
)
- resp = self.client.get_purge_cache_status(self.cache_request_id)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
- def test_get_purge_file_cache_status_fails_on_unauthenticated_request(self) -> None:
- """Test get_purge_file_cache_status unauthenticated request
- this function checks if raises error on unauthenticated request
- to check if get_purge_cache_status is only restricted to authenticated
- user
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "removeAITags": ["ai-tag1", "ai-tag2"],
+ "webhookUrl": "url",
+ "extensions": [{
+ "name": "remove-bg",
+ "options": {
+ "add_shadow": true,
+ "bg_color": "red"
+ }
+ }, {
+ "name": "google-auto-tagging",
+ "minConfidence": 80,
+ "maxTags": 10
+ }],
+ "tags": ["tag1", "tag2"],
+ "customCoordinates": "10,10,100,100",
+ "customMetadata": {
+ "test": 11
+ }
+ }"""
+ )
)
- resp = self.client.get_purge_file_cache_status(self.cache_request_id)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
+ resp = self.client.update_file_details(
+ file_id=self.file_id,
+ options=UpdateFileRequestOptions(
+ remove_ai_tags=["ai-tag1", "ai-tag2"],
+ webhook_url="url",
+ extensions=[
+ {
+ "name": "remove-bg",
+ "options": {"add_shadow": True, "bg_color": "red"},
+ },
+ {"name": "google-auto-tagging", "minConfidence": 80, "maxTags": 10},
+ ],
+ tags=["tag1", "tag2"],
+ custom_coordinates="10,10,100,100",
+ custom_metadata={"test": 11},
+ ),
+ )
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "http_status_code": 200,
+ "raw": {
+ "AITags": [
+ {
+ "confidence": 99.39,
+ "name": "Corridor",
+ "source": "aws-auto-tagging",
+ },
+ {
+ "confidence": 97.59,
+ "name": "Floor",
+ "source": "aws-auto-tagging",
+ },
+ ],
+ "createdAt": "2022-07-21T10:31:22.529Z",
+ "customCoordinates": "10,10,100,100",
+ "customMetadata": {"test": 11},
+ "embeddedMetadata": {
+ "DateCreated": "2022-07-21T10:35:34.497Z",
+ "DateTimeCreated": "2022-07-21T10:35:34.500Z",
+ "XResolution": 1,
+ "YResolution": 1,
+ },
+ "extensionStatus": {
+ "google-auto-tagging": "success",
+ "remove-bg": "pending",
+ },
+ "fileId": "fake_123",
+ "filePath": "/default-image.jpg",
+ "fileType": "image",
+ "hasAlpha": False,
+ "height": 1000,
+ "isPrivateFile": False,
+ "mime": "image/jpeg",
+ "name": "default-image.jpg",
+ "size": 184425,
+ "tags": ["tag1", "tag2"],
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/default-image.jpg",
+ "type": "file",
+ "updatedAt": "2022-07-21T10:37:11.848Z",
+ "url": "https://ik.imagekit.io/your_imagekit_id/default-image.jpg",
+ "versionInfo": {"id": "versionId", "name": "Version 2"},
+ "width": 1000,
+ },
+ }
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("fake_123", resp.file_id)
+ self.assertEqual(
+ "http://test.com/v1/files/fake_123/details/", responses.calls[0].request.url
+ )
+
+ @responses.activate
+ def test_update_file_details_fails_with_404_exception(self) -> None:
+ """Test update file details raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/details/".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.PATCH,
+ url,
+ status=404,
+ body="""{"message": "The requested file does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.update_file_details(
+ file_id=self.file_id,
+ options=UpdateFileRequestOptions(
+ remove_ai_tags=["ai-tag1", "ai-tag2"],
+ webhook_url="url",
+ extensions=[
+ {
+ "name": "remove-bg",
+ "options": {"add_shadow": True, "bg_color": "red"},
+ },
+ {
+ "name": "google-auto-tagging",
+ "minConfidence": 80,
+ "maxTags": 10,
+ },
+ ],
+ tags=["tag1", "tag2"],
+ custom_coordinates="10,10,100,100",
+ custom_metadata={"test": 11},
+ ),
+ )
+ self.assertRaises(UnknownException)
+ except UnknownException as e:
+ self.assertEqual("The requested file does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+
+class TestGetFileVersions(ClientTestCase):
+ """
+ TestGetFileVersions class used to get file versions and it's details
+ """
+
+ file_id = "fake_123"
+
+ version_id = "fake_version_123"
- def test_purge_cache_status_fails_without_passing_file_url(self) -> None:
- """Test purge_cache raises error on invalid_body request
+ valid_options = {"tags": ["tag1", "tag2"], "custom_coordinates": "10,10,100,100"}
+
+ @responses.activate
+ def test_get_file_versions_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.get_file_versions(self.file_id)
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_file_versions_succeeds_with_id(self):
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ Tests if get file versions succeeds with file id
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions".format(URL.API_BASE_URL, self.file_id)
+
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ responses.add(
+ responses.GET,
+ url,
+ body="""[{
+ "type": "file",
+ "name": "new_car.jpg",
+ "createdAt": "2022-06-15T11:34:36.294Z",
+ "updatedAt": "2022-07-04T10:15:50.067Z",
+ "fileId": "fake_123",
+ "tags": ["Tag_1", "Tag_2", "Tag_3"],
+ "AITags": [{
+ "name": "Clothing",
+ "confidence": 98.77,
+ "source": "google-auto-tagging"
+ }, {
+ "name": "Smile",
+ "confidence": 95.31,
+ "source": "google-auto-tagging"
+ }, {
+ "name": "Shoe",
+ "confidence": 95.2,
+ "source": "google-auto-tagging"
+ }],
+ "versionInfo": {
+ "id": "versionId",
+ "name": "Version 4"
+ },
+ "embeddedMetadata": {
+ "DateCreated": "2022-07-04T10:15:50.066Z",
+ "DateTimeCreated": "2022-07-04T10:15:50.066Z"
+ },
+ "customCoordinates": "",
+ "customMetadata": {
+ "test100": 10,
+ "test10": 11
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/your_imagekit_id/new_car.jpg",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/new_car.jpg",
+ "fileType": "image",
+ "filePath": "/new_car.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 7390,
+ "hasAlpha": false,
+ "mime": "image/jpeg"
+ }, {
+ "type": "file-version",
+ "name": "new_car.jpg",
+ "createdAt": "2022-07-04T10:15:49.698Z",
+ "updatedAt": "2022-07-04T10:15:49.734Z",
+ "fileId": "fileId",
+ "tags": ["Tag_1", "Tag_2", "Tag_3"],
+ "AITags": [{
+ "name": "Clothing",
+ "confidence": 98.77,
+ "source": "google-auto-tagging"
+ }, {
+ "name": "Smile",
+ "confidence": 95.31,
+ "source": "google-auto-tagging"
+ }, {
+ "name": "Shoe",
+ "confidence": 95.2,
+ "source": "google-auto-tagging"
+ }, {
+ "name": "Street light",
+ "confidence": 91.05,
+ "source": "google-auto-tagging"
+ }],
+ "versionInfo": {
+ "id": "62c2bdd5872375c6b8f40fd4",
+ "name": "Version 1"
+ },
+ "embeddedMetadata": {
+ "XResolution": 250,
+ "YResolution": 250,
+ "DateCreated": "2022-06-15T11:34:36.702Z",
+ "DateTimeCreated": "2022-06-15T11:34:36.702Z"
+ },
+ "customCoordinates": "10,10,40,40",
+ "customMetadata": {
+ "test100": 10,
+ "test10": 11
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/your_imagekit_id/new_car.jpg?ik-obj-version=dlkUlhiJ7I8OTejhKG38GZJBrsvDBcnz",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/new_car.jpg?ik-obj-version=dlkUlhiJ7I8OTejhKG38GZJBrsvDBcnz",
+ "fileType": "image",
+ "filePath": "/new_car.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 23023,
+ "hasAlpha": false,
+ "mime": "image/jpeg"
+ }]""",
+ headers=headers,
+ )
+ resp = self.client.get_file_versions(self.file_id)
+ mock_response_metadata = {
+ "raw": [
+ {
+ "type": "file",
+ "name": "new_car.jpg",
+ "createdAt": "2022-06-15T11:34:36.294Z",
+ "updatedAt": "2022-07-04T10:15:50.067Z",
+ "fileId": "fake_123",
+ "tags": ["Tag_1", "Tag_2", "Tag_3"],
+ "AITags": [
+ {
+ "name": "Clothing",
+ "confidence": 98.77,
+ "source": "google-auto-tagging",
+ },
+ {
+ "name": "Smile",
+ "confidence": 95.31,
+ "source": "google-auto-tagging",
+ },
+ {
+ "name": "Shoe",
+ "confidence": 95.2,
+ "source": "google-auto-tagging",
+ },
+ ],
+ "versionInfo": {"id": "versionId", "name": "Version 4"},
+ "embeddedMetadata": {
+ "DateCreated": "2022-07-04T10:15:50.066Z",
+ "DateTimeCreated": "2022-07-04T10:15:50.066Z",
+ },
+ "customCoordinates": "",
+ "customMetadata": {"test100": 10, "test10": 11},
+ "isPrivateFile": False,
+ "url": "https://ik.imagekit.io/your_imagekit_id/new_car.jpg",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/new_car.jpg",
+ "fileType": "image",
+ "filePath": "/new_car.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 7390,
+ "hasAlpha": False,
+ "mime": "image/jpeg",
+ },
+ {
+ "type": "file-version",
+ "name": "new_car.jpg",
+ "createdAt": "2022-07-04T10:15:49.698Z",
+ "updatedAt": "2022-07-04T10:15:49.734Z",
+ "fileId": "fileId",
+ "tags": ["Tag_1", "Tag_2", "Tag_3"],
+ "AITags": [
+ {
+ "name": "Clothing",
+ "confidence": 98.77,
+ "source": "google-auto-tagging",
+ },
+ {
+ "name": "Smile",
+ "confidence": 95.31,
+ "source": "google-auto-tagging",
+ },
+ {
+ "name": "Shoe",
+ "confidence": 95.2,
+ "source": "google-auto-tagging",
+ },
+ {
+ "name": "Street light",
+ "confidence": 91.05,
+ "source": "google-auto-tagging",
+ },
+ ],
+ "versionInfo": {
+ "id": "62c2bdd5872375c6b8f40fd4",
+ "name": "Version 1",
+ },
+ "embeddedMetadata": {
+ "XResolution": 250,
+ "YResolution": 250,
+ "DateCreated": "2022-06-15T11:34:36.702Z",
+ "DateTimeCreated": "2022-06-15T11:34:36.702Z",
+ },
+ "customCoordinates": "10,10,40,40",
+ "customMetadata": {"test100": 10, "test10": 11},
+ "isPrivateFile": False,
+ "url": "https://ik.imagekit.io/your_imagekit_id/new_car.jpg?ik-obj-version=dlkUlhiJ7I8OTejhKG38GZJBrsvDBcnz",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-ik_ml_thumbnail/new_car.jpg?ik-obj-version=dlkUlhiJ7I8OTejhKG38GZJBrsvDBcnz",
+ "fileType": "image",
+ "filePath": "/new_car.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 23023,
+ "hasAlpha": False,
+ "mime": "image/jpeg",
+ },
+ ],
+ "http_status_code": 200,
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("fake_123", resp.list[0].file_id)
+ self.assertEqual("fileId", resp.list[1].file_id)
+ self.assertEqual(
+ "http://test.com/v1/files/fake_123/versions", responses.calls[0].request.url
)
- self.assertRaises(TypeError, self.client.get_purge_cache_status)
- def test_purge_file_cache_status_fails_without_passing_file_url(self) -> None:
- """Test purge_file_cache raises error on invalid_body request
+ @responses.activate
+ def test_get_file_versions_fails_with_404_exception(self) -> None:
+ """Test get file versions raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions".format(URL.API_BASE_URL, self.file_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=404,
+ body="""{"message": "The requested asset does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_file_versions(self.file_id)
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested asset does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_file_version_details_fails_on_unauthenticated_request(self):
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
)
- self.assertRaises(TypeError, self.client.get_purge_file_cache_status)
-
- def test_purge_cache_status_succeeds(self) -> None:
- """Test get_purge_cache_status working properly
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.get_file_version_details(self.file_id, self.version_id)
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_file_version_details_succeeds_with_id(self):
"""
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_PURGE_CACHE_STATUS_MSG)
+ Tests if get file version details succeeds with file id
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
)
- resp = self.client.get_purge_cache_status(self.cache_request_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- def test_purge_cache_status_fails_without_passing_file_id(self) -> None:
- """Test purge_cache raises error on invalid_body request
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ responses.add(
+ responses.GET,
+ url,
+ body="""{
+ "type": "file-version",
+ "name": "new_car.jpg",
+ "createdAt": "2022-06-27T09:24:25.251Z",
+ "updatedAt": "2022-06-27T12:11:11.247Z",
+ "fileId": "fake_123",
+ "tags": ["tagg", "tagg1"],
+ "AITags": "",
+ "versionInfo": {
+ "id": "fake_version_123",
+ "name": "Version 1"
+ },
+ "embeddedMetadata": {
+ "XResolution": 250,
+ "YResolution": 250,
+ "DateCreated": "2022-06-15T11:34:36.702Z",
+ "DateTimeCreated": "2022-06-15T11:34:36.702Z"
+ },
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {
+ "test100": 10
+ },
+ "isPrivateFile": false,
+ "url": "https://ik.imagekit.io/your-imagekit-id/new_car.jpg?ik-obj-version=hzBNRjaJhZYg.JNu75L2nMDfhjJP4tJH",
+ "thumbnail": "https://ik.imagekit.io/your-imagekit-id/tr:n-ik_ml_thumbnail/new_car.jpg?ik-obj-version=hzBNRjaJhZYg.JNu75L2nMDfhjJP4tJH",
+ "fileType": "image",
+ "filePath": "/new_car.jpg",
+ "height": 354,
+ "width": 236,
+ "size": 23023,
+ "hasAlpha": false,
+ "mime": "image/jpeg"
+ }""",
+ headers=headers,
)
- self.assertRaises(TypeError, self.client.get_metadata())
+ resp = self.client.get_file_version_details(self.file_id, self.version_id)
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "http_status_code": 200,
+ "raw": {
+ "AITags": "",
+ "createdAt": "2022-06-27T09:24:25.251Z",
+ "customCoordinates": "10,10,20,20",
+ "customMetadata": {"test100": 10},
+ "embeddedMetadata": {
+ "DateCreated": "2022-06-15T11:34:36.702Z",
+ "DateTimeCreated": "2022-06-15T11:34:36.702Z",
+ "XResolution": 250,
+ "YResolution": 250,
+ },
+ "fileId": "fake_123",
+ "filePath": "/new_car.jpg",
+ "fileType": "image",
+ "hasAlpha": False,
+ "height": 354,
+ "isPrivateFile": False,
+ "mime": "image/jpeg",
+ "name": "new_car.jpg",
+ "size": 23023,
+ "tags": ["tagg", "tagg1"],
+ "thumbnail": "https://ik.imagekit.io/your-imagekit-id/tr:n-ik_ml_thumbnail/new_car.jpg?ik-obj-version=hzBNRjaJhZYg.JNu75L2nMDfhjJP4tJH",
+ "type": "file-version",
+ "updatedAt": "2022-06-27T12:11:11.247Z",
+ "url": "https://ik.imagekit.io/your-imagekit-id/new_car.jpg?ik-obj-version=hzBNRjaJhZYg.JNu75L2nMDfhjJP4tJH",
+ "versionInfo": {"id": "fake_version_123", "name": "Version 1"},
+ "width": 236,
+ },
+ }
- def test_purge_file_cache_status_succeeds(self) -> None:
- """Test get_purge_file_cache_status working properly
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp(message=SUCCESS_PURGE_CACHE_STATUS_MSG)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("fake_123", resp.file_id)
+ self.assertEqual("fake_version_123", resp.version_info.id)
+ self.assertEqual(
+ "http://test.com/v1/files/fake_123/versions/fake_version_123",
+ responses.calls[0].request.url,
)
- resp = self.client.get_purge_file_cache_status(self.cache_request_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
+ @responses.activate
+ def test_get_file_version_details_fails_with_404_exception(self) -> None:
+ """Test get file version details raises 404 error"""
-class TestGetMetaData(ClientTestCase):
- file_id = "fake_file_xbc"
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
+ )
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=404,
+ body="""{"message": "The requested asset does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_file_version_details(self.file_id, self.version_id)
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested asset does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_file_version_details_fails_with_400_exception(self) -> None:
+ """Test get file version details raises 400 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
+ )
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=400,
+ body="""{"message": "Your request contains invalid fileId parameter.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_file_version_details(self.file_id, self.version_id)
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "Your request contains invalid fileId parameter.", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+
+class TestDeleteFileVersion(ClientTestCase):
+ version_id = "fake_123_version_id"
+ file_id = "fax_abx1223"
- def test_get_metadata_fails_on_unauthenticated_request(self) -> None:
- """Tests get_metadata raise error on unauthenticated request
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ @responses.activate
+ def test_delete_file_version_fails_with_404_exception(self) -> None:
+ """Test delete_file_version raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
)
- resp = self.client.get_metadata(file_id=self.file_id)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
+ try:
+ responses.add(
+ responses.DELETE,
+ url,
+ status=404,
+ body="""{"message": "The requested file version does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.delete_file_version(self.file_id, self.version_id)
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested file version does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_delete_file_version_succeeds(self) -> None:
+ """Test delete_file_version succeeds with file and version Id"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
+ )
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(responses.DELETE, url, status=204, headers=headers, body="{}")
+ resp = self.client.delete_file_version(self.file_id, self.version_id)
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 204,
+ "raw": None,
+ }
- def test_get_file_metadata_fails_on_unauthenticated_request(self) -> None:
- """Tests get_file_metadata raise error on unauthenticated request
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/fax_abx1223/versions/fake_123_version_id",
+ responses.calls[0].request.url,
)
- resp = self.client.get_file_metadata(file_id=self.file_id)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
- def test_get_metadata_succeeds(self):
- """Tests if get_metadata working properly
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+class TestCopyFile(ClientTestCase):
+ source_file_path = "/source_file.jpg"
+
+ destination_path = "/destination_path"
+
+ @responses.activate
+ def test_copy_file_fails_with_404(self) -> None:
+ """Test copy_file raises 404"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/copy".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ headers=headers,
+ body="""{
+ "message": "No file found with filePath /source_file.jpg",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "reason": "SOURCE_FILE_MISSING"
+ }""",
+ )
+ try:
+ self.client.copy_file(
+ options=CopyFileRequestOptions(
+ source_file_path=self.source_file_path,
+ destination_path=self.destination_path,
+ include_file_versions=False,
+ )
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("No file found with filePath /source_file.jpg", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_copy_file_succeeds(self) -> None:
+ """Test copy_file succeeds"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/copy".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(responses.POST, url, status=204, headers=headers, body="{}")
+
+ resp = self.client.copy_file(
+ options=CopyFileRequestOptions(
+ source_file_path=self.source_file_path,
+ destination_path=self.destination_path,
+ include_file_versions=True,
+ )
)
- resp = self.client.get_metadata(file_id=self.file_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- def test_get_file_metadata_succeeds(self):
- """Tests if get_file_metadata working properly
- """
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 204,
+ "raw": None,
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFilePath": "/source_file.jpg",
+ "destinationPath": "/destination_path",
+ "includeFileVersions": true
+ }"""
+ )
+ )
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/copy", responses.calls[0].request.url
)
- resp = self.client.get_file_metadata(file_id=self.file_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- def test_get_remote_url_metadata_file_url(self) -> None:
- """Test get_remote_url_metadata_ raises error on invalid_body request
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ @responses.activate
+ def test_copy_file_succeeds_without_include_file_versions(self) -> None:
+ """Test copy_file succeeds"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/copy".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(responses.POST, url, status=204, headers=headers, body="{}")
+
+ resp = self.client.copy_file(
+ options=CopyFileRequestOptions(
+ source_file_path=self.source_file_path,
+ destination_path=self.destination_path,
+ )
)
- self.assertRaises(ValueError, self.client.get_remote_url_metadata)
- def test_get_remote_url_metadata_succeeds(self):
- """Tests if get_remote_url_metadata working properly
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 204,
+ "raw": None,
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFilePath": "/source_file.jpg",
+ "destinationPath": "/destination_path"
+ }"""
+ )
)
- resp = self.client.get_remote_url_metadata(
- remote_file_url="http://imagekit.io/default.jpg"
+
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/copy", responses.calls[0].request.url
)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone("response")
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+
+class TestMoveFile(ClientTestCase):
+ source_file_path = "/source_file.jpg"
+
+ destination_path = "/destination_path"
+
+ @responses.activate
+ def test_move_file_fails_with_404(self) -> None:
+ """Test move_file raises 404"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/move".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ headers=headers,
+ body="""{
+ "message": "No file found with filePath /source_file.jpg",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "reason": "SOURCE_FILE_MISSING"
+ }""",
+ )
+ try:
+ self.client.move_file(
+ options=MoveFileRequestOptions(
+ source_file_path=self.source_file_path,
+ destination_path=self.destination_path,
+ )
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("No file found with filePath /source_file.jpg", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_move_file_succeeds(self) -> None:
+ """Test move_file succeeds"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/move".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(responses.POST, url, status=204, headers=headers, body="{}")
+
+ resp = self.client.move_file(
+ options=MoveFileRequestOptions(
+ source_file_path=self.source_file_path,
+ destination_path=self.destination_path,
+ )
+ )
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 204,
+ "raw": None,
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFilePath": "/source_file.jpg",
+ "destinationPath": "/destination_path"
+ }"""
+ )
)
- resp = self.client.get_metadata(file_id=self.file_id)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- def test_get_remote_file_url_metadata_succeeds(self):
- """Tests if get_remote_url_metadata working properly
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
)
- resp = self.client.get_remote_file_url_metadata(
- remote_file_url="http://imagekit.io/default.jpg"
+ self.assertEqual(
+ "http://test.com/v1/files/move", responses.calls[0].request.url
)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone("response")
-class TestUpdateFileDetails(ClientTestCase):
- """
- TestUpdateFileDetails class used to update file details method
- """
+class TestRenameFile(ClientTestCase):
+ file_path = "/file_path.jpg"
+
+ new_file_name = "new_file.jpg"
+
+ @responses.activate
+ def test_rename_file_fails_with_409(self) -> None:
+ """Test rename_file raises 409"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/rename".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ try:
+ responses.add(
+ responses.PUT,
+ url,
+ status=409,
+ headers=headers,
+ body="""{
+ "message": "File with name testing-binary.jpg already exists at the same location.",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "reason": "FILE_ALREADY_EXISTS"
+ }""",
+ )
+ self.client.rename_file(
+ options=RenameFileRequestOptions(
+ file_path=self.file_path, new_file_name=self.new_file_name
+ )
+ )
+ self.assertRaises(ConflictException)
+ except ConflictException as e:
+ self.assertEqual(
+ "File with name testing-binary.jpg already exists at the same location.",
+ e.message,
+ )
+ self.assertEqual(409, e.response_metadata.http_status_code)
+ self.assertEqual("FILE_ALREADY_EXISTS", e.response_metadata.raw["reason"])
+
+ @responses.activate
+ def test_rename_file_succeeds_with_purge_cache_false(self) -> None:
+ """Test rename_file succeeds with Purge cache"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/rename".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.PUT,
+ url,
+ headers=headers,
+ body="{}",
+ )
+ resp = self.client.rename_file(
+ options=RenameFileRequestOptions(
+ file_path=self.file_path,
+ new_file_name=self.new_file_name,
+ purge_cache=False,
+ )
+ )
- file_id = "fake_123"
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {},
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "filePath": "/file_path.jpg",
+ "newFileName": "new_file.jpg",
+ "purgeCache": false
+ }"""
+ )
+ )
- valid_options = {"tags": ["tag1", "tag2"], "custom_coordinates": "10,10,100,100"}
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(None, resp.purge_request_id)
+ self.assertEqual(
+ "http://test.com/v1/files/rename", responses.calls[0].request.url
+ )
- def test_update_file_details_fails_on_unauthenticated_request(self):
- """
- Tests if the unauthenticated request restricted
+ @responses.activate
+ def test_rename_file_succeeds_with_purge_cache(self) -> None:
+ """Test rename_file succeeds with Purge cache"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/rename".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.PUT,
+ url,
+ headers=headers,
+ body='{"purgeRequestId": "62de3e986f68334a5a3339fb"}',
+ )
+ resp = self.client.rename_file(
+ options=RenameFileRequestOptions(
+ file_path=self.file_path,
+ new_file_name=self.new_file_name,
+ purge_cache=True,
+ )
+ )
- """
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {"purgeRequestId": "62de3e986f68334a5a3339fb"},
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "filePath": "/file_path.jpg",
+ "newFileName": "new_file.jpg",
+ "purgeCache": true
+ }"""
+ )
+ )
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_failed_resp()
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
)
- resp = self.client.update_file_details(
- file_id=self.file_id, options=self.valid_options
+ self.assertEqual("62de3e986f68334a5a3339fb", resp.purge_request_id)
+ self.assertEqual(
+ "http://test.com/v1/files/rename", responses.calls[0].request.url
)
- self.assertIsNotNone(resp["error"])
- self.assertIsNone(resp["response"])
- def test_update_file_details_succeeds_with_id(self):
- """
- Tests if update_file_details succeeds with file_url
- """
- self.client.ik_request.request = MagicMock(
- return_value=get_mocked_success_resp()
+ @responses.activate
+ def test_rename_file_succeeds(self) -> None:
+ """Test rename_file succeeds"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/rename".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(responses.PUT, url, headers=headers, body="{}")
+ resp = self.client.rename_file(
+ options=RenameFileRequestOptions(
+ file_path=self.file_path, new_file_name=self.new_file_name
+ )
+ )
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {},
+ }
+
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "filePath": "/file_path.jpg",
+ "newFileName": "new_file.jpg"
+ }"""
+ )
)
- # generate expected encoded private key for the auth headers
- private_key_file_upload = ClientTestCase.private_key
- if private_key_file_upload != ":":
- private_key_file_upload += ":"
- encoded_private_key = base64.b64encode(private_key_file_upload.encode()).decode(
- "utf-8"
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(None, resp.purge_request_id)
+ self.assertEqual(
+ "http://test.com/v1/files/rename", responses.calls[0].request.url
)
- resp = self.client.update_file_details(
- file_id=self.file_id, options=self.valid_options
+
+class TestRestoreFileVersion(ClientTestCase):
+ version_id = "fake_123_version_id"
+ file_id = "fax_abx1223"
+
+ @responses.activate
+ def test_restore_file_version_fails_with_404_exception(self) -> None:
+ """Test restore_file_version raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}/restore".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
)
- self.assertIsNone(resp["error"])
- self.assertIsNotNone(resp["response"])
- self.client.ik_request.request.assert_called_once_with(
- method="Patch",
- url="{}/{}/details/".format(URL.BASE_URL.value, self.file_id),
- headers={'Content-Type': 'application/json', 'Authorization': "Basic {}".format(encoded_private_key)},
- data=json.dumps(request_formatter(self.valid_options))
+ try:
+ responses.add(
+ responses.PUT,
+ url,
+ status=404,
+ body="""{"message": "The requested file version does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.restore_file_version(self.file_id, self.version_id)
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested file version does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_restore_file_version_succeeds(self) -> None:
+ """Test restore_file_version succeeds with file and version Id"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/{}/versions/{}/restore".format(
+ URL.API_BASE_URL, self.file_id, self.version_id
)
+ headers = {"Content-Type": "application/json"}
+ headers.update(create_headers_for_test())
+ responses.add(
+ responses.PUT,
+ url,
+ headers=headers,
+ body="""{
+ "fileId": "fileId",
+ "type": "file",
+ "name": "file1.jpg",
+ "filePath": "/images/file.jpg",
+ "tags": ["t-shirt", "round-neck", "sale2019"],
+ "AITags": [
+ {
+ "confidence": 90.12,
+ "source": "google-auto-tagging"
+ }],
+ "versionInfo": {
+ "id": "versionId",
+ "name": "Version 2"
+ },
+ "isPrivateFile": false,
+ "customCoordinates": "",
+ "url": "https://ik.imagekit.io/your_imagekit_id/images/products/file1.jpg",
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-media_library_thumbnail/images/products/file1.jpg",
+ "fileType": "image",
+ "hasAlpha": false,
+ "height": 100,
+ "isPrivateFile": false,
+ "mime": "image/jpeg",
+ "name": "file1.jpg",
+ "size": 100,
+ "hasAlpha": false,
+ "customMetadata": {
+ "brand": "Nike",
+ "color": "red"
+ },
+ "createdAt": "2019-08-24T06:14:41.313Z",
+ "updatedAt": "2019-09-24T06:14:41.313Z"
+ }""",
+ )
+ resp = self.client.restore_file_version(self.file_id, self.version_id)
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "http_status_code": 200,
+ "raw": {
+ "AITags": [{"confidence": 90.12, "source": "google-auto-tagging"}],
+ "createdAt": "2019-08-24T06:14:41.313Z",
+ "customCoordinates": "",
+ "customMetadata": {"brand": "Nike", "color": "red"},
+ "fileId": "fileId",
+ "filePath": "/images/file.jpg",
+ "fileType": "image",
+ "hasAlpha": False,
+ "height": 100,
+ "isPrivateFile": False,
+ "mime": "image/jpeg",
+ "name": "file1.jpg",
+ "size": 100,
+ "tags": ["t-shirt", "round-neck", "sale2019"],
+ "thumbnail": "https://ik.imagekit.io/your_imagekit_id/tr:n-media_library_thumbnail/images/products/file1.jpg",
+ "type": "file",
+ "updatedAt": "2019-09-24T06:14:41.313Z",
+ "url": "https://ik.imagekit.io/your_imagekit_id/images/products/file1.jpg",
+ "versionInfo": {"id": "versionId", "name": "Version 2"},
+ },
+ }
- def test_file_details_succeeds_with_url(self):
- self.client.ik_request = MagicMock(return_value=get_mocked_success_resp())
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("fileId", resp.file_id)
+ self.assertEqual("versionId", resp.version_info.id)
+ self.assertEqual(
+ "http://test.com/v1/files/fax_abx1223/versions/fake_123_version_id/restore",
+ responses.calls[0].request.url,
+ )
diff --git a/tests/test_folder_ops.py b/tests/test_folder_ops.py
new file mode 100644
index 0000000..5a5255b
--- /dev/null
+++ b/tests/test_folder_ops.py
@@ -0,0 +1,577 @@
+import json
+
+import responses
+
+from imagekitio import ImageKit
+from imagekitio.constants.url import URL
+from imagekitio.exceptions.BadRequestException import BadRequestException
+from imagekitio.exceptions.ForbiddenException import ForbiddenException
+from imagekitio.exceptions.InternalServerException import InternalServerException
+from imagekitio.exceptions.NotFoundException import NotFoundException
+from imagekitio.exceptions.UnknownException import UnknownException
+from imagekitio.models.CopyFolderRequestOptions import CopyFolderRequestOptions
+from imagekitio.models.CreateFolderRequestOptions import CreateFolderRequestOptions
+from imagekitio.models.DeleteFolderRequestOptions import DeleteFolderRequestOptions
+from imagekitio.models.MoveFolderRequestOptions import MoveFolderRequestOptions
+from imagekitio.utils.formatter import camel_dict_to_snake_dict
+from tests.helpers import (
+ ClientTestCase,
+ create_headers_for_test,
+)
+
+imagekit_obj = ImageKit(
+ private_key="private_fake:",
+ public_key="public_fake123:",
+ url_endpoint="fake.com",
+)
+
+
+class TestFolders(ClientTestCase):
+ """
+ TestFolders class used to test create and Delete folders
+ """
+
+ @responses.activate
+ def test_create_folder_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.create_folder(
+ options=CreateFolderRequestOptions(
+ folder_name="folder_name", parent_folder_path="/test"
+ )
+ )
+ self.assertRaises(ForbiddenException)
+ except UnknownException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(e.response_metadata.http_status_code, 403)
+
+ @responses.activate
+ def test_create_folder_succeeds(self):
+ """
+ Tests if create_folder succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ headers = create_headers_for_test()
+ responses.add(responses.POST, url, status=201, body="{}", headers=headers)
+ resp = self.client.create_folder(
+ options=CreateFolderRequestOptions(
+ folder_name="folder_name", parent_folder_path="/test"
+ )
+ )
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 201,
+ "raw": {},
+ }
+
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("http://test.com/v1/folder", responses.calls[0].request.url)
+ self.assertEqual(
+ '{"folderName": "folder_name", "parentFolderPath": "/test"}',
+ responses.calls[0].request.body,
+ )
+
+ @responses.activate
+ def test_create_folder_fails_with_400(self):
+ """
+ Tests if create folder fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=400,
+ body="""{"message": "folderName parameter cannot have a slash.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.create_folder(
+ options=CreateFolderRequestOptions(
+ folder_name="folder_name", parent_folder_path="/test"
+ )
+ )
+ self.assertRaises(BadRequestException)
+ except UnknownException as e:
+ self.assertEqual(e.message, "folderName parameter cannot have a slash.")
+ self.assertEqual(e.response_metadata.http_status_code, 400)
+
+ @responses.activate
+ def test_delete_folder_fails_with_400(self):
+ """
+ Tests if Delete folder fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.DELETE,
+ url,
+ status=404,
+ body="""{
+ "message": "No folder found with folderPath test",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "reason": "FOLDER_NOT_FOUND"
+ }""",
+ )
+ self.client.delete_folder(
+ options=DeleteFolderRequestOptions(folder_path="/test")
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("No folder found with folderPath test", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual("FOLDER_NOT_FOUND", e.response_metadata.raw["reason"])
+
+ @responses.activate
+ def test_delete_folder_succeeds(self):
+ """
+ Tests if Delete folder succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/folder".format(URL.API_BASE_URL)
+ responses.add(
+ responses.DELETE,
+ url,
+ status=204,
+ body="{}",
+ )
+ resp = self.client.delete_folder(
+ options=DeleteFolderRequestOptions(folder_path="/folderName")
+ )
+ mock_response_metadata = {
+ "raw": None,
+ "httpStatusCode": 204,
+ "headers": {"Content-Type": "text/plain"},
+ }
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("http://test.com/v1/folder", responses.calls[0].request.url)
+ self.assertEqual('{"folderPath": "/folderName"}', responses.calls[0].request.body)
+
+
+class TestCopyFolder(ClientTestCase):
+ """
+ TestCopyFolder class used to test copy folder
+ """
+
+ @responses.activate
+ def test_copy_folder_fails_with_400(self):
+ """
+ Tests if Copy folder fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/copyFolder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=400,
+ body="""{
+ "message": "sourceFolderPath and destinationPath cannot be same.",
+ "help": "For support kindly contact us at support@imagekit.io ."
+ }""",
+ )
+ self.client.copy_folder(
+ options=CopyFolderRequestOptions(
+ source_folder_path="/test",
+ destination_path="/test",
+ include_file_versions=False,
+ )
+ )
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "sourceFolderPath and destinationPath cannot be same.", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_copy_folder_fails_with_404(self):
+ """
+ Tests if Copy folder fails with 404
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/copyFolder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ body="""{
+ "message": "No files & folder found at sourceFolderPath /test",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "reason": "NO_FILES_FOLDER"
+ }""",
+ )
+ self.client.copy_folder(
+ options=CopyFolderRequestOptions(
+ source_folder_path="/test",
+ destination_path="/test1",
+ include_file_versions=False,
+ )
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual(
+ "No files & folder found at sourceFolderPath /test", e.message
+ )
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual("NO_FILES_FOLDER", e.response_metadata.raw["reason"])
+
+ @responses.activate
+ def test_copy_folder_succeeds(self):
+ """
+ Tests if Copy folder succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/copyFolder".format(URL.API_BASE_URL)
+ responses.add(
+ responses.POST,
+ url,
+ body='{"jobId": "62de84fb1b02a58936cc740c"}',
+ )
+ resp = self.client.copy_folder(
+ options=CopyFolderRequestOptions(
+ source_folder_path="/test",
+ destination_path="/test1",
+ include_file_versions=True,
+ )
+ )
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {"jobId": "62de84fb1b02a58936cc740c"},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFolderPath": "/test",
+ "destinationPath": "/test1",
+ "includeFileVersions": true
+ }"""
+ )
+ )
+ self.assertEqual("62de84fb1b02a58936cc740c", resp.job_id)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/bulkJobs/copyFolder", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_copy_folder_succeeds_with_include_file_versions_false(self):
+ """
+ Tests if Copy folder succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/copyFolder".format(URL.API_BASE_URL)
+ responses.add(
+ responses.POST,
+ url,
+ body='{"jobId": "62de84fb1b02a58936cc740c"}',
+ )
+ resp = self.client.copy_folder(
+ options=CopyFolderRequestOptions(
+ source_folder_path="/test",
+ destination_path="/test1",
+ include_file_versions=False,
+ )
+ )
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {"jobId": "62de84fb1b02a58936cc740c"},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFolderPath": "/test",
+ "destinationPath": "/test1",
+ "includeFileVersions": false
+ }"""
+ )
+ )
+ self.assertEqual("62de84fb1b02a58936cc740c", resp.job_id)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/bulkJobs/copyFolder", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+ @responses.activate
+ def test_copy_folder_succeeds_without_include_file_versions(self):
+ """
+ Tests if Copy folder succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/copyFolder".format(URL.API_BASE_URL)
+ responses.add(
+ responses.POST,
+ url,
+ body='{"jobId": "62de84fb1b02a58936cc740c"}',
+ )
+ resp = self.client.copy_folder(
+ options=CopyFolderRequestOptions(
+ source_folder_path="/test",
+ destination_path="/test1",
+ )
+ )
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {"jobId": "62de84fb1b02a58936cc740c"},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFolderPath": "/test",
+ "destinationPath": "/test1"
+ }"""
+ )
+ )
+ self.assertEqual("62de84fb1b02a58936cc740c", resp.job_id)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/bulkJobs/copyFolder", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+
+class TestMoveFolder(ClientTestCase):
+ """
+ TestMoveFolder class used to test move folder
+ """
+
+ @responses.activate
+ def test_move_folder_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/moveFolder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.move_folder(
+ options=MoveFolderRequestOptions(
+ source_folder_path="/test", destination_path="/test1"
+ )
+ )
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_move_folder_fails_with_400(self):
+ """
+ Tests if Move folder fails with 400
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/moveFolder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=400,
+ body="""{
+ "message": "sourceFolderPath and destinationPath cannot be same.",
+ "help": "For support kindly contact us at support@imagekit.io ."
+ }""",
+ )
+ self.client.move_folder(
+ options=MoveFolderRequestOptions(
+ source_folder_path="/test", destination_path="/test"
+ )
+ )
+ self.assertRaises(BadRequestException)
+ except BadRequestException as e:
+ self.assertEqual(
+ "sourceFolderPath and destinationPath cannot be same.", e.message
+ )
+ self.assertEqual(400, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_move_folder_fails_with_404(self):
+ """
+ Tests if Move folder fails with 404
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/moveFolder".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ body="""{
+ "message": "No files & folder found at sourceFolderPath /test",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "reason": "NO_FILES_FOLDER"
+ }""",
+ )
+ self.client.move_folder(
+ options=MoveFolderRequestOptions(
+ source_folder_path="/test", destination_path="/test1"
+ )
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual(
+ "No files & folder found at sourceFolderPath /test", e.message
+ )
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual("NO_FILES_FOLDER", e.response_metadata.raw["reason"])
+
+ @responses.activate
+ def test_move_folder_succeeds(self):
+ """
+ Tests if Move folder succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/moveFolder".format(URL.API_BASE_URL)
+ responses.add(
+ responses.POST,
+ url,
+ body='{"jobId": "62de84fb1b02a58936cc740c"}',
+ )
+ resp = self.client.move_folder(
+ options=MoveFolderRequestOptions(
+ source_folder_path="/test", destination_path="/test1"
+ )
+ )
+ mock_response_metadata = {
+ "headers": {"Content-Type": "text/plain"},
+ "httpStatusCode": 200,
+ "raw": {"jobId": "62de84fb1b02a58936cc740c"},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "sourceFolderPath": "/test",
+ "destinationPath": "/test1"
+ }"""
+ )
+ )
+ self.assertEqual("62de84fb1b02a58936cc740c", resp.job_id)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/bulkJobs/moveFolder", responses.calls[0].request.url
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+
+
+class TestGetBulkJobStatus(ClientTestCase):
+ """
+ TestGetBulkJobStatus class used to get bulk job status
+ """
+
+ job_id = "mock_job_id"
+
+ @responses.activate
+ def test_get_bulk_job_status_fails_with_500(self):
+ """
+ Tests if get_bulk_job_status succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/{}".format(URL.API_BASE_URL, self.job_id)
+ try:
+ responses.add(
+ responses.GET,
+ url,
+ status=500,
+ body="""{"message": "We have experienced an internal error while processing your request.",
+ "help": "For support kindly contact us at support@imagekit.io ."}""",
+ )
+ self.client.get_bulk_job_status(self.job_id)
+ self.assertRaises(InternalServerException)
+ except InternalServerException as e:
+ self.assertEqual(
+ "We have experienced an internal error while processing your request.",
+ e.message,
+ )
+ self.assertEqual(500, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_get_bulk_job_status_succeeds(self):
+ """
+ Tests if get_bulk_job_status succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/bulkJobs/{}".format(URL.API_BASE_URL, self.job_id)
+ headers = create_headers_for_test()
+ responses.add(
+ responses.GET,
+ url,
+ body="""{
+ "jobId": "mock_job_id",
+ "type": "COPY_FOLDER",
+ "status": "Completed"
+ }""",
+ headers=headers,
+ )
+ resp = self.client.get_bulk_job_status(self.job_id)
+
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain",
+ "Accept-Encoding": "gzip, deflate",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {
+ "jobId": "mock_job_id",
+ "status": "Completed",
+ "type": "COPY_FOLDER",
+ },
+ }
+
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual("mock_job_id", resp.job_id)
+ self.assertEqual("Completed", resp.status)
+ self.assertEqual("COPY_FOLDER", resp.type)
+ self.assertEqual(
+ "http://test.com/v1/bulkJobs/mock_job_id", responses.calls[0].request.url
+ )
diff --git a/tests/test_generate_url.py b/tests/test_generate_url.py
index bd0f905..040f056 100644
--- a/tests/test_generate_url.py
+++ b/tests/test_generate_url.py
@@ -20,36 +20,7 @@ def test_generate_url_with_path(self):
url = self.client.url(options)
self.assertEqual(
url,
- "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
- )
-
- def test_url_contains_ik_sdk_version(self):
- options = {
- "path": "/default-image.jpg",
- "transformation": [{"height": "300", "width": "400"}],
- }
- url = self.client.url(options)
- self.assertIn("ik-sdk-version", url)
- self.assertEqual(
- url,
- "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
- )
-
- def test_generate_url_without_leading_slash_in_path(self):
- options = {
- "path": "default-image.jpg",
- "transformation": [{"height": "300", "width": "400"}],
- }
- url = self.client.url(options)
- self.assertEqual(
- url,
- "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg",
)
def test_overriding_url_endpoint_generation_consists_new_url(self):
@@ -66,50 +37,19 @@ def test_overriding_url_endpoint_generation_consists_new_url(self):
url = self.client.url(options)
self.assertEqual(
- url,
- "https://ik.imagekit.io/new/endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
- )
-
- def test_overriding_url_endpoint_without_slash_generation_consists_new_url(self):
- """
- Overriding urlEndpoint parameter. Passing a urlEndpoint value without slash
- """
- options = {
- "path": "/default-image.jpg",
- "url_endpoint": "https://ik.imagekit.io/new/endpoint",
- "transformation": [{"height": "300", "width": "400"}],
- }
-
- url = self.client.url(options)
- self.assertEqual(
- url,
- "https://ik.imagekit.io/new/endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ url, "https://ik.imagekit.io/new/endpoint/tr:h-300,w-400/default-image.jpg"
)
def test_generate_url_query_parameters(self):
options = {
"path": "/default-image.jpg",
- "query_parameters": {
- "param1": "value1",
- "param2": "value2"
- },
- "transformation": [
- {
- "height": "300",
- "width": "400"
- }
- ],
+ "query_parameters": {"param1": "value1", "param2": "value2"},
+ "transformation": [{"height": "300", "width": "400"}],
}
url = self.client.url(options)
self.assertEqual(
url,
- "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?param1=value1¶m2=value2&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?param1=value1¶m2=value2",
)
def test_generate_url_with_src(self):
@@ -122,6 +62,7 @@ def test_generate_url_with_src(self):
"format": "jpg",
"progressive": "true",
"effect_contrast": "1",
+ "raw": "ar-4-3,q-40",
},
{"rotation": 90},
],
@@ -129,17 +70,14 @@ def test_generate_url_with_src(self):
url = self.client.url(options)
self.assertEqual(
url,
- "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1"
+ "%2Car-4-3%2Cq-40%3Art-90",
)
def test_generate_url_with_src_with_query_params_double(self):
options = {
"src": "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?queryparam1=value1",
- "query_parameters": {
- "param1": "value1"
- },
+ "query_parameters": {"param1": "value1"},
"transformation": [
{
"height": "300",
@@ -155,9 +93,8 @@ def test_generate_url_with_src_with_query_params_double(self):
# @TODO - adjust value of param1=value1 in test case but it should be there
self.assertEqual(
url,
- "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?queryparam1=value1¶m1=value1&tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- )
+ "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?queryparam1=value1¶m1=value1&tr=h-300%2Cw-400"
+ "%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90",
)
def test_generate_url_with_path_and_signed(self):
@@ -219,9 +156,7 @@ def test_url_with_new_transformation_returns_as_it_is(self):
self.assertIn("fake_xxxx", url)
self.assertEqual(
url,
- "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300%2Cfake_xxxx-400&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300%2Cfake_xxxx-400",
)
def test_query_url_generation_transformation_as_query_and_transformations_in_url(
@@ -237,9 +172,7 @@ def test_query_url_generation_transformation_as_query_and_transformations_in_url
url = self.client.url(options)
self.assertEqual(
url,
- "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://ik.imagekit.io/your_imagekit_id/endpoint/default-image.jpg?tr=h-300",
)
def test_generate_url_with_chained_transformations(self):
@@ -259,9 +192,8 @@ def test_generate_url_with_chained_transformations(self):
url = self.client.url(options)
self.assertEqual(
url,
- "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1"
+ "%3Art-90",
)
def test_url_check_query_param_are_added_correctly(self):
@@ -271,9 +203,10 @@ def test_url_check_query_param_are_added_correctly(self):
"transformation_position": "query",
}
url = self.client.url(options)
- self.assertEqual(url,
- "https://test-domain.com/test-endpoint/default-image.jpg?client=123&user=5&tr=h-300%2Cw-400&ik-sdk-version={}".format(
- Default.SDK_VERSION.value))
+ self.assertEqual(
+ url,
+ "https://test-domain.com/test-endpoint/default-image.jpg?client=123&user=5&tr=h-300%2Cw-400",
+ )
def test_generate_url_with_src_query_parameters_merge_correctly(self):
options = {
@@ -292,9 +225,8 @@ def test_generate_url_with_src_query_parameters_merge_correctly(self):
url = self.client.url(options)
self.assertEqual(
url,
- "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?client=123&ab=c&tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?client=123&ab=c&tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true"
+ "%2Ce-contrast-1%3Art-90",
)
def test_generate_url_with_src_and_transformation_position_path(self):
@@ -315,9 +247,7 @@ def test_generate_url_with_src_and_transformation_position_path(self):
url = self.client.url(options)
self.assertEqual(
url,
- "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90&ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg?tr=h-300%2Cw-400%2Cf-jpg%2Cpr-true%2Ce-contrast-1%3Art-90",
)
def test_url_with_invalid_trans_pos(self):
@@ -338,17 +268,6 @@ def test_url_without_path_and_src(self):
}
self.assertEqual(self.client.url(options), "")
- def test_url_contains_sdk_version(self):
- options = {
- "path": "/default-image.jpg",
- "url_endpoint": "https://ik.imagekit.io/your_imagekit_id/endpoint/",
- "transformation": [{"height": "300", "width": "400"}],
- "signed": True,
- "transformation_position": "query",
- }
-
- self.assertIn("ik-sdk-version", self.client.url(options))
-
def test_url_contains_slash_if_transformation_position_is_path(self):
options = {
"path": "/default-image.jpg",
@@ -395,9 +314,7 @@ def test_generate_url_with_path_and_src_uses_path(self):
url = self.client.url(options)
self.assertEqual(
url,
- "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://test-domain.com/test-endpoint/tr:h-300,w-400/default-image.jpg",
)
def test_generate_url_with_all_params(self):
@@ -407,115 +324,108 @@ def test_generate_url_with_all_params(self):
options = {
"path": "/test_path.jpg",
"src": "https://ik.imagekit.io/ldt7znpgpjs/test_YhNhoRxWt.jpg",
- "transformation": [{
- "height": 300,
- "width": 400,
- "aspect_ratio": '4-3',
- "quality": 40,
- "crop": 'force',
- "crop_mode": 'extract',
- "focus": 'left',
- "format": 'jpeg',
- "radius": 50,
- "bg": "A94D34",
- "border": "5-A94D34",
- "rotation": 90,
- "blur": 10,
- "named": "some_name",
- "overlay_x": 35,
- "overlay_y": 35,
- "overlay_focus": "bottom",
- "overlay_height": 20,
- "overlay_width": 20,
- "overlay_image": "/folder/file.jpg", # leading slash case
- "overlay_image_trim": False,
- "overlay_image_aspect_ratio": "4:3",
- "overlay_image_background": "0F0F0F",
- "overlay_image_border": "10_0F0F0F",
- "overlay_image_dpr": 2,
- "overlay_image_quality": 50,
- "overlay_image_cropping": "force",
- "overlay_text": "two words",
- "overlay_text_font_size": 20,
- "overlay_text_font_family": "Open Sans",
- "overlay_text_color": "00FFFF",
- "overlay_text_transparency": 5,
- "overlay_text_typography": "b",
- "overlay_background": "00AAFF55",
- "overlay_text_encoded": "b3ZlcmxheSBtYWRlIGVhc3k%3D",
- "overlay_text_width": 50,
- "overlay_text_background": "00AAFF55",
- "overlay_text_padding": 40,
- "overlay_text_inner_alignment": "left",
- "overlay_radius": 10,
- "progressive": "true",
- "lossless": "true",
- "trim": 5,
- "metadata": "true",
- "color_profile": "true",
- "default_image": "folder/file.jpg/", # trailing slash case
- "dpr": 3,
- "effect_sharpen": 10,
- "effect_usm": "2-2-0.8-0.024",
- "effect_contrast": "true",
- "effect_gray": "true",
- "original": True, ## Boolean handling
- }]
+ "transformation": [
+ {
+ "height": 300,
+ "width": 400,
+ "aspect_ratio": "4-3",
+ "quality": 40,
+ "crop": "force",
+ "crop_mode": "extract",
+ "focus": "left",
+ "format": "jpeg",
+ "radius": 50,
+ "bg": "A94D34",
+ "border": "5-A94D34",
+ "rotation": 90,
+ "blur": 10,
+ "named": "some_name",
+ "overlay_image": "/folder/file.jpg", # leading slash case
+ "overlay_image_aspect_ratio": "4:3",
+ "overlay_image_background": "0F0F0F",
+ "overlay_image_border": "10_0F0F0F",
+ "overlay_image_dpr": 2,
+ "overlay_image_quality": 50,
+ "overlay_image_cropping": "force",
+ "overlay_image_trim": False,
+ "overlay_x": 35,
+ "overlay_y": 35,
+ "overlay_focus": "bottom",
+ "overlay_height": 20,
+ "overlay_width": 20,
+ "overlay_text": "two words",
+ "overlay_text_font_size": 20,
+ "overlay_text_font_family": "Open Sans",
+ "overlay_text_color": "00FFFF",
+ "overlay_text_transparency": 5,
+ "overlay_text_typography": "b",
+ "overlay_background": "00AAFF55",
+ "overlay_text_encoded": "b3ZlcmxheSBtYWRlIGVhc3k%3D",
+ "overlay_text_width": 50,
+ "overlay_text_background": "00AAFF55",
+ "overlay_text_padding": 40,
+ "overlay_text_inner_alignment": "left",
+ "overlay_radius": 10,
+ "progressive": "true",
+ "lossless": "true",
+ "trim": 5,
+ "metadata": "true",
+ "color_profile": "true",
+ "default_image": "folder/file.jpg/", # trailing slash case
+ "dpr": 3,
+ "effect_sharpen": 10,
+ "effect_usm": "2-2-0.8-0.024",
+ "effect_contrast": "true",
+ "effect_gray": "true",
+ "original": True, # Boolean handling
+ "raw": "w-200,h-200",
+ }
+ ],
}
url = self.client.url(options)
- print(url)
self.assertEqual(
url,
- "https://test-domain.com/test-endpoint/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,ox-35,oy-35,ofo-bottom,oh-20,ow-20,oi-folder@@file.jpg,oit-false,oiar-4:3,oibg-0F0F0F,oib-10_0F0F0F,oidpr-2,oiq-50,oic-force,ot-two words,ots-20,otf-Open Sans,otc-00FFFF,oa-5,ott-b,obg-00AAFF55,ote-b3ZlcmxheSBtYWRlIGVhc3k%3D,otw-50,otbg-00AAFF55,otp-40,otia-left,or-10,pr-true,lo-true,t-5,md-true,cp-true,di-folder@@file.jpg,dpr-3,e-sharpen-10,e-usm-2-2-0.8-0.024,e-contrast-true,e-grayscale-true,orig-true/test_path.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
+ "https://test-domain.com/test-endpoint/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,oi-folder@@file.jpg,oiar-4:3,oibg-0F0F0F,oib-10_0F0F0F,"
+ "oidpr-2,oiq-50,oic-force,oit-false,ox-35,oy-35,ofo-bottom,oh-20,ow-20,ot-two words,ots-20,otf-Open Sans,"
+ "otc-00FFFF,oa-5,ott-b,obg-00AAFF55,ote-b3ZlcmxheSBtYWRlIGVhc3k%3D,otw-50,otbg-00AAFF55,otp-40,otia-left,"
+ "or-10,pr-true,lo-true,t-5,md-true,cp-true,di-folder@@file.jpg,dpr-3,e-sharpen-10,e-usm-2-2-0.8-0.024,"
+ "e-contrast-true,e-grayscale-true,orig-true,w-200,h-200/test_path.jpg",
)
def test_get_signature_with_100_expire_seconds(self):
url = "https://test-domain.com/test-endpoint/tr:w-100/test-signed-url.png"
signature = self.client.url_obj.get_signature(
- "private_key_test", url, "https://test-domain.com/test-endpoint/", 100)
+ "private_key_test", url, "https://test-domain.com/test-endpoint/", 100
+ )
self.assertEqual(signature, "5e5037a31a7121cbe2964e220b4338cc6e1ba66d")
def test_get_signature_without_expire_seconds(self):
url = "https://test-domain.com/test-endpoint/tr:w-100/test-signed-url.png"
signature = self.client.url_obj.get_signature(
- "private_key_test", url, "https://test-domain.com/test-endpoint/", 0)
+ "private_key_test", url, "https://test-domain.com/test-endpoint/", 0
+ )
self.assertEqual(signature, "41b3075c40bc84147eb71b8b49ae7fbf349d0f00")
def test_get_signature_without_expire_seconds_without_slash(self):
url = "https://test-domain.com/test-endpoint/tr:w-100/test-signed-url.png"
signature = self.client.url_obj.get_signature(
- "private_key_test", url, "https://test-domain.com/test-endpoint", 0)
+ "private_key_test", url, "https://test-domain.com/test-endpoint", 0
+ )
self.assertEqual(signature, "41b3075c40bc84147eb71b8b49ae7fbf349d0f00")
-
def test_generate_url_without_transforms(self):
- options = {
- "path": "/coffee.jpg",
- "signed": False,
- "expire_seconds": 10
- }
+ options = {"path": "/coffee.jpg", "signed": False, "expire_seconds": 10}
url = self.client.url(options)
- self.assertEqual(
- url,
- "https://test-domain.com/test-endpoint/coffee.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
- )
+ self.assertEqual(url, "https://test-domain.com/test-endpoint/coffee.jpg")
def test_generate_url_without_transforms_src(self):
options = {
"src": "https://test-domain.com/test-endpoint/coffee.jpg",
"signed": False,
- "expire_seconds": 10
+ "expire_seconds": 10,
}
url = self.client.url(options)
- self.assertEqual(
- url,
- "https://test-domain.com/test-endpoint/coffee.jpg?ik-sdk-version={}".format(
- Default.SDK_VERSION.value
- ),
- )
\ No newline at end of file
+ self.assertEqual(url, "https://test-domain.com/test-endpoint/coffee.jpg")
diff --git a/tests/test_tags_ops.py b/tests/test_tags_ops.py
new file mode 100644
index 0000000..b2816a1
--- /dev/null
+++ b/tests/test_tags_ops.py
@@ -0,0 +1,334 @@
+import json
+import os
+
+import responses
+
+from imagekitio.client import ImageKit
+from imagekitio.constants.url import URL
+from imagekitio.exceptions.ForbiddenException import ForbiddenException
+from imagekitio.exceptions.NotFoundException import NotFoundException
+from imagekitio.utils.formatter import camel_dict_to_snake_dict
+from tests.helpers import (
+ ClientTestCase,
+ get_auth_headers_for_test,
+)
+
+imagekit_obj = ImageKit(
+ private_key="private_fake:",
+ public_key="public_fake123:",
+ url_endpoint="fake.com",
+)
+
+
+class TestTags(ClientTestCase):
+ """
+ TestTags class used to test Add and Remove methods
+ """
+
+ filename = "test"
+
+ file_id = "fake_123"
+
+ @responses.activate
+ def test_add_tags_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/addTags".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.add_tags(
+ file_ids=[self.file_id], tags=["add-tag-1", "add-tag-2"]
+ )
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_add_tags_succeeds(self):
+ """
+ Tests if add tags succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/addTags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ body='{"successfullyUpdatedFileIds": ["fake_123"]}',
+ headers=headers,
+ )
+
+ resp = self.client.add_tags(
+ file_ids=[self.file_id], tags=["add-tag-1", "add-tag-2"]
+ )
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {"successfullyUpdatedFileIds": ["fake_123"]},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "fileIds": ["fake_123"],
+ "tags": ["add-tag-1", "add-tag-2"]
+ }"""
+ )
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(["fake_123"], resp.successfully_updated_file_ids)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/addTags", responses.calls[0].request.url
+ )
+
+ @responses.activate
+ def test_add_tags_fails_with_404_exception(self) -> None:
+ """Test add tags raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/addTags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ body="""{
+ "message": "The requested file(s) does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "missingFileIds": ["fake_123"]
+ }""",
+ headers=headers,
+ )
+ self.client.add_tags(
+ file_ids=[self.file_id], tags=["add-tag-1", "add-tag-2"]
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested file(s) does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual(["fake_123"], e.response_metadata.raw["missingFileIds"])
+
+ @responses.activate
+ def test_remove_tags_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/removeTags".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.remove_tags(
+ file_ids=[self.file_id], tags=["remove-tag-1", "remove-tag-2"]
+ )
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual("Your account cannot be authenticated.", e.message)
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_remove_tags_succeeds(self):
+ """
+ Tests if remove tags succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/removeTags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ body='{"successfullyUpdatedFileIds": ["fake_123"]}',
+ headers=headers,
+ )
+
+ resp = self.client.remove_tags(
+ file_ids=[self.file_id], tags=["remove-tag-1", "remove-tag-2"]
+ )
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {"successfullyUpdatedFileIds": ["fake_123"]},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "fileIds": ["fake_123"],
+ "tags": ["remove-tag-1", "remove-tag-2"]
+ }"""
+ )
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(["fake_123"], resp.successfully_updated_file_ids)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/removeTags", responses.calls[0].request.url
+ )
+
+ @responses.activate
+ def test_remove_tags_fails_with_404_exception(self) -> None:
+ """Test remove tags raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/removeTags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ body="""{
+ "message": "The requested file(s) does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "missingFileIds": ["fake_123"]
+ }""",
+ headers=headers,
+ )
+ self.client.remove_tags(
+ file_ids=[self.file_id], tags=["remove-tag-1", "remove-tag-2"]
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested file(s) does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual(["fake_123"], e.response_metadata.raw["missingFileIds"])
+
+
+class TestAITags(ClientTestCase):
+ """
+ TestAITags class used to test Remove AITags method
+ """
+
+ image = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), "dummy_data/image.png"
+ )
+ filename = "test"
+
+ file_id = "fake_123"
+
+ @responses.activate
+ def test_remove_ai_tags_fails_on_unauthenticated_request(self):
+ """
+ Tests if the unauthenticated request restricted
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/removeAITags".format(URL.API_BASE_URL)
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=403,
+ body="""{'message': 'Your account cannot be authenticated.'
+ , 'help': 'For support kindly contact us at support@imagekit.io .'}""",
+ )
+ self.client.remove_ai_tags(
+ file_ids=[self.file_id], ai_tags=["remove-ai-tag1", "remove-ai-tag2"]
+ )
+ self.assertRaises(ForbiddenException)
+ except ForbiddenException as e:
+ self.assertEqual(e.message, "Your account cannot be authenticated.")
+ self.assertEqual(403, e.response_metadata.http_status_code)
+
+ @responses.activate
+ def test_remove_ai_tags_succeeds(self):
+ """
+ Tests if Remove AI tags succeeds
+ """
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/removeAITags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ responses.add(
+ responses.POST,
+ url,
+ body='{"successfullyUpdatedFileIds": ["fake_123"]}',
+ headers=headers,
+ )
+
+ resp = self.client.remove_ai_tags(
+ file_ids=[self.file_id], ai_tags=["remove-ai-tag-1", "remove-ai-tag-2"]
+ )
+ mock_response_metadata = {
+ "headers": {
+ "Content-Type": "text/plain, application/json",
+ "Authorization": "Basic ZmFrZTEyMjo=",
+ },
+ "httpStatusCode": 200,
+ "raw": {"successfullyUpdatedFileIds": ["fake_123"]},
+ }
+ request_body = json.dumps(
+ json.loads(
+ """{
+ "fileIds": ["fake_123"],
+ "AITags": ["remove-ai-tag-1", "remove-ai-tag-2"]
+ }"""
+ )
+ )
+ self.assertEqual(request_body, responses.calls[0].request.body)
+ self.assertEqual(["fake_123"], resp.successfully_updated_file_ids)
+ self.assertEqual(
+ camel_dict_to_snake_dict(mock_response_metadata),
+ resp.response_metadata.__dict__,
+ )
+ self.assertEqual(
+ "http://test.com/v1/files/removeAITags", responses.calls[0].request.url
+ )
+
+ @responses.activate
+ def test_remove_ai_tags_fails_with_404_exception(self) -> None:
+ """Test Remove AI tags raises 404 error"""
+
+ URL.API_BASE_URL = "http://test.com"
+ url = "{}/v1/files/removeAITags".format(URL.API_BASE_URL)
+ headers = {"Content-Type": "application/json"}
+ headers.update(get_auth_headers_for_test())
+ try:
+ responses.add(
+ responses.POST,
+ url,
+ status=404,
+ body="""{
+ "message": "The requested file(s) does not exist.",
+ "help": "For support kindly contact us at support@imagekit.io .",
+ "missingFileIds": ["fake_123"]
+ }""",
+ headers=headers,
+ )
+ self.client.remove_ai_tags(
+ file_ids=[self.file_id], ai_tags=["remove-ai-tag-1", "remove-ai-tag-2"]
+ )
+ self.assertRaises(NotFoundException)
+ except NotFoundException as e:
+ self.assertEqual("The requested file(s) does not exist.", e.message)
+ self.assertEqual(404, e.response_metadata.http_status_code)
+ self.assertEqual(["fake_123"], e.response_metadata.raw["missingFileIds"])
diff --git a/tests/test_utils_calculation.py b/tests/test_utils_calculation.py
index 705c43f..9c77311 100644
--- a/tests/test_utils_calculation.py
+++ b/tests/test_utils_calculation.py
@@ -4,12 +4,15 @@
class TestUtilCalculation(unittest.TestCase):
-
def test_get_authenticated_params(self):
"""Test authenticated_params returning proper value
:return: param dict
"""
- result = get_authenticated_params(token='your_token', expire="1582269249", private_key="private_key_test")
- self.assertEqual(result['token'], 'your_token')
- self.assertEqual(result['expire'], '1582269249')
- self.assertEqual(result['signature'], 'e71bcd6031016b060d349d212e23e85c791decdd')
+ result = get_authenticated_params(
+ token="your_token", expire="1582269249", private_key="private_key_test"
+ )
+ self.assertEqual(result["token"], "your_token")
+ self.assertEqual(result["expire"], "1582269249")
+ self.assertEqual(
+ result["signature"], "e71bcd6031016b060d349d212e23e85c791decdd"
+ )
diff --git a/tox.ini b/tox.ini
index ec49579..b67c350 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py34,py35,py36
+envlist = py36, py37, py38, py39, py310
skipsdist = True
[testenv]
@@ -7,4 +7,4 @@ passenv = *
deps = -rrequirements/test.txt
commands =
coverage run --append -m unittest discover tests
- coverage report
\ No newline at end of file
+ coverage report