From ba02c666093a2f528291f1ba75fd1f0aacfca3f3 Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Mon, 24 Aug 2026 15:01:28 +0300 Subject: [PATCH 1/2] docs: add agent-readable documentation and repository metadata Bring the repository up to the Cloudinary agent-readable documentation standard established in cloudinary_npm (PRs #742, #744). - Restructure README around the standard section order, with a runnable quick start and a stated read path for coding agents. - Add 15 task docs under cloudinary/docs/, shipped inside the published package so they install to site-packages/cloudinary/docs/ and are version-matched with the code. Includes a Django page, which has no equivalent in the Node SDK. - Add examples/ with 8 complete runnable files, one per "doing" task, cross-linked with their task docs. - Add AGENTS.md, CLAUDE.md, SECURITY.md, and context7.json. - Replace the three-line LICENSE.txt stub with canonical MIT text at LICENSE, preserving the vendored poster attribution. This is what GitHub and license scanners need to detect the license. - Ship docs via MANIFEST.in plus package-data; keep examples/ repo-only. - Set the PyPI summary to match the README one-liner. - Correct stale facts in CONTRIBUTING.md (Travis, setup.py test, and the supported version matrix). Every doc snippet and all 8 examples were executed against a live cloud. Documented behavior that differs from a first reading of the code, and is now stated in the docs: upload_large defaults to resource_type="raw" and silently stores a video as an opaque blob; re-uploading to the same public_id overwrites by default; the upload result exposes a moderation list rather than a flat moderation_status; undefined structured-metadata keys are rejected rather than ignored; the dict form of the video poster option ignores secure=True; Search rejects leading wildcards. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 113 ++++++++++ CLAUDE.md | 1 + CONTRIBUTING.md | 37 ++-- LICENSE | 21 ++ LICENSE.txt | 5 - MANIFEST.in | 2 + README.md | 203 ++++++++++-------- SECURITY.md | 50 +++++ cloudinary/docs/README.md | 63 ++++++ cloudinary/docs/configure-cloudinary.md | 86 ++++++++ cloudinary/docs/get-credentials.md | 147 +++++++++++++ cloudinary/docs/import-and-call.md | 29 +++ cloudinary/docs/moderate-upload.md | 127 +++++++++++ cloudinary/docs/platform-capabilities.md | 130 +++++++++++ cloudinary/docs/search-and-manage-assets.md | 189 ++++++++++++++++ cloudinary/docs/sign-browser-upload.md | 136 ++++++++++++ .../docs/transform-and-deliver-image.md | 148 +++++++++++++ .../docs/transform-and-deliver-video.md | 128 +++++++++++ cloudinary/docs/troubleshoot-errors.md | 159 ++++++++++++++ cloudinary/docs/upload-image.md | 110 ++++++++++ cloudinary/docs/upload-large-video.md | 111 ++++++++++ cloudinary/docs/use-structured-metadata.md | 144 +++++++++++++ cloudinary/docs/use-with-django.md | 178 +++++++++++++++ context7.json | 4 + examples/moderate-upload.py | 56 +++++ examples/provision-claimable-cloud.py | 69 ++++++ examples/search-and-manage-assets.py | 75 +++++++ examples/sign-browser-upload.py | 93 ++++++++ examples/transform-and-deliver-image.py | 75 +++++++ examples/transform-and-deliver-video.py | 52 +++++ examples/upload-image.py | 54 +++++ examples/upload-large-video.py | 67 ++++++ examples/use-structured-metadata.py | 84 ++++++++ pyproject.toml | 9 +- 34 files changed, 2849 insertions(+), 106 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 LICENSE delete mode 100644 LICENSE.txt create mode 100644 SECURITY.md create mode 100644 cloudinary/docs/README.md create mode 100644 cloudinary/docs/configure-cloudinary.md create mode 100644 cloudinary/docs/get-credentials.md create mode 100644 cloudinary/docs/import-and-call.md create mode 100644 cloudinary/docs/moderate-upload.md create mode 100644 cloudinary/docs/platform-capabilities.md create mode 100644 cloudinary/docs/search-and-manage-assets.md create mode 100644 cloudinary/docs/sign-browser-upload.md create mode 100644 cloudinary/docs/transform-and-deliver-image.md create mode 100644 cloudinary/docs/transform-and-deliver-video.md create mode 100644 cloudinary/docs/troubleshoot-errors.md create mode 100644 cloudinary/docs/upload-image.md create mode 100644 cloudinary/docs/upload-large-video.md create mode 100644 cloudinary/docs/use-structured-metadata.md create mode 100644 cloudinary/docs/use-with-django.md create mode 100644 context7.json create mode 100644 examples/moderate-upload.py create mode 100644 examples/provision-claimable-cloud.py create mode 100644 examples/search-and-manage-assets.py create mode 100644 examples/sign-browser-upload.py create mode 100644 examples/transform-and-deliver-image.py create mode 100644 examples/transform-and-deliver-video.py create mode 100644 examples/upload-image.py create mode 100644 examples/upload-large-video.py create mode 100644 examples/use-structured-metadata.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2815a26a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,113 @@ +# AGENTS.md + +This file is for agents contributing to this repository. If you are *using* the +installed `cloudinary` package in another project, read the bundled docs in the +installed package's `cloudinary/docs/` directory instead. + +## Commands + +```bash +pip install -e . # install for development +pip install tox pytest # test tooling +python -m pytest test # core test suite +python -m pytest test/test_uploader.py # a single module +tox # full matrix (Python 3.10-3.14, Django 4.2-6.0) +tox -e py312-core # one environment, as CI runs it +DJANGO_SETTINGS_MODULE=django_tests.settings \ + django-admin test -v2 django_tests # Django integration suite +python -m build # build sdist + wheel +``` + +Every suite needs a working `CLOUDINARY_URL` in the environment — there is no +offline or mocked tier. CI allocates a throwaway cloud per job via +`tools/get_test_cloud.sh`; locally, export your own or provision one with +`python -c "from cloudinary.provisioning import create_cloud; print(create_cloud())"`. + +## Testing + +- `test/` is the core suite and hits the live API. Tests namespace their assets with + `UNIQUE_TEST_ID` from `test/helper_test.py` and clean up after themselves — follow + that pattern rather than leaving fixtures behind. +- `django_tests/` runs as a real Django app (`django_tests.settings`, in-memory + sqlite). It covers `CloudinaryField`, the form fields, and migrations; the + templatetags have no coverage yet. +- Provisioning tests (`test/test_provisioning_api.py`) additionally need + `CLOUDINARY_ACCOUNT_URL`; they are skipped without it. +- Add-on tests (`test/addon_types.py`) require paid add-ons. Guard anything new with + the existing skip decorators rather than making the default suite fail. +- Nondeterministic AI output (auto-tagging, captioning, moderation verdicts) must be + asserted by request shape, state transition, and response schema — never exact + values. +- There is no lint tooling in this repo. Do not add a linter or reformat files + wholesale as part of an unrelated change. + +## Project structure + +- `cloudinary/` — the package. `uploader.py` (Upload API), `api.py` (Admin API), + `utils.py` (URL building, signing, transformation strings), `search.py`. +- `cloudinary/docs/` — version-matched Markdown task docs, shipped inside the + published package. Keep in sync with the code they document. +- `cloudinary/models.py`, `forms.py`, `templatetags/`, `templates/`, `static/` — the + Django integration. `static/` is generated by `prepare.sh` and gitignored. +- `cloudinary/provisioning/` — Account/Provisioning API, including `create_cloud` + (Claimable Clouds) and `create_agent_account`. +- `cloudinary/api_client/` — shared HTTP plumbing; `cloudinary/poster/` is vendored + MIT code for multipart streaming. +- `examples/` — complete runnable task examples, one per task doc. Repo-only, not + shipped in the package. +- `samples/` — legacy full sample applications; not part of the tested example set. +- `test/`, `django_tests/` — the two suites. `tools/` — release and CI scripts. +- The version lives in **three** places that must stay in sync: + `cloudinary/__init__.py`, `pyproject.toml`, and the legacy Python 2 branch of + `setup.py`. `tools/update_version.sh` bumps all three. `tools/get_test_cloud.sh` + greps the version out of `setup.py`, so do not reformat that line. + +## Code style + +- Python 2/3 compatible source: the package still imports `six` and + `cloudinary/compat.py`. Do not introduce f-strings, walrus operators, or + type-annotation syntax into `cloudinary/`. +- 4-space indent, `snake_case`, module-level functions for API surface (not classes). +- Public API convention: positional arguments first, then `**options` passed through + to the API call. + + ```python + def upload(file, **options): + params = utils.build_upload_params(**options) + return call_cacheable_api("upload", params, file=file, **options) + ``` +- New upload or transformation parameters must be added to the whitelists in + `cloudinary/utils.py` (`__SIMPLE_UPLOAD_PARAMS`, `__SERIALIZED_UPLOAD_PARAMS`, + `_SIMPLE_TRANSFORMATION_PARAMS`) or they are silently dropped. + +## Git workflow + +- Branch from `master`; one topic per pull request. +- Run `python -m pytest test` before opening a PR, and the Django suite when touching + `models.py`, `forms.py`, or `templatetags/`. +- Never rewrite published `CHANGELOG.md` entries; new entries go at the top and are + added by the release process, not by feature PRs. +- Never commit credentials, `.env`, real cloud names, build output (`dist/`, + `build/`, `*.egg-info/`), or `cloudinary/static/`. + +## Boundaries + +**Always** + +- Keep `cloudinary/docs/` and `examples/` consistent with the code they document. +- Update tests when public behavior changes. +- Keep API secrets out of examples, docs, tests, and fixtures. + +**Ask first** + +- Changing supported Python or Django versions, dependencies, classifiers, or + packaging configuration (`pyproject.toml`, `MANIFEST.in`, `setup.py`). +- Renaming or removing any public function, class, or module. +- Changing release, CI, or version-bump tooling in `tools/`. + +**Never** + +- Commit credentials or real account identifiers. +- Add a linter, formatter, or reformat unrelated files. +- Document a Cloudinary platform capability as an SDK method unless this package + implements it (see `cloudinary/docs/platform-capabilities.md`). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3815bb11..5870b996 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,9 +23,9 @@ Please be aware that the package is used in a wide variety of environments and t - Ensure the PR description clearly describes the bug / feature. Include the relevant issue number if applicable. - Provide test code that covers the new code - Make sure that your code works both with and without Django -- The code should support: - - Python >= 2.7 - - Django >= 1.8 +- The code should support the versions tested in CI: + - Python 3.10 - 3.14 + - Django 4.2, 5.0, 5.1, 5.2, 6.0 ## Code contribution @@ -71,29 +71,42 @@ We definitely appreciate pull requests that highlight or reproduce a problem, ev Implement your feature or bug fix. Try to follow [PEP8](https://pep8.org/). -Make sure that your code works both with and without Django -The code should support: +Make sure that your code works both with and without Django. +The code should support the versions tested in CI: - - Python >= 2.7 - - Django >= 1.8 + - Python 3.10 - 3.14 + - Django 4.2, 5.0, 5.1, 5.2, 6.0 Make sure that tests completes without errors. #### Write Documentation -Document any external behavior in the [README](README.md). +Document any external behavior in the bundled task docs under +[cloudinary/docs](cloudinary/docs) — these ship inside the published package and are the +source of truth for the installed version. Add a matching runnable file to +[examples](examples) when the change introduces a new task, and keep the two consistent. +The [README](README.md) links the task docs; update it only when the set of tasks +changes. #### Running the tests Run the basic test suite with your `CLOUDINARY_URL`: - CLOUDINARY_URL=cloudinary://apikey:apisecret@cloudname python setup.py test + CLOUDINARY_URL=cloudinary://apikey:apisecret@cloudname python -m pytest test + +The suite runs against the live API, so a working `CLOUDINARY_URL` is required. If you do +not have credentials, provision a throwaway cloud with +`python -c "from cloudinary.provisioning import create_cloud; print(create_cloud())"`. + +Run the Django suite separately: + + DJANGO_SETTINGS_MODULE=django_tests.settings django-admin test -v2 django_tests This only runs the tests for the current environment. -Travis-CI will run the full suite when you submit your pull request. +GitHub Actions will run the full suite when you submit your pull request. The full test suite takes a long time to run because it tests multiple combinations of Python and Django. -You need to have Python 2.7, 3.4, 3.5, 3.6, 3.7 installed to run all environments. Then run: +You need the Python versions listed above installed to run all environments. Then run: CLOUDINARY_URL=cloudinary://apikey:apisecret@cloudname tox @@ -139,7 +152,7 @@ git push origin my-feature-branch -f #### Check on Your Pull Request -Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. +Go back to your pull request after a few minutes and see whether it passed muster with GitHub Actions. Everything should look green, otherwise fix issues and amend your commit as described above. #### Be Patient diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..73aeece0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Cloudinary Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 2f379ed3..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,5 +0,0 @@ -Released under the MIT license. - -Contains MIT licensed code from https://bitbucket.org/chrisatlee/poster - - diff --git a/MANIFEST.in b/MANIFEST.in index 68decfa8..9734e991 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include *.txt +include LICENSE +recursive-include cloudinary/docs *.md recursive-include cloudinary/templates *.html recursive-include cloudinary/static *.html *.js prune django_tests diff --git a/README.md b/README.md index 13443b71..d71697a8 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,150 @@ +# Cloudinary Python SDK + +Upload, transform, optimize, and manage images and videos with Cloudinary from Python or Django — the `cloudinary` package on PyPI. + [![Tests](https://github.com/cloudinary/pycloudinary/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/cloudinary/pycloudinary/actions/workflows/test.yml) -[![PyPI Version](https://img.shields.io/pypi/v/cloudinary.svg)](https://pypi.python.org/pypi/cloudinary/) -[![PyPI PyVersions](https://img.shields.io/pypi/pyversions/cloudinary.svg)](https://pypi.python.org/pypi/cloudinary/) -[![PyPI DjangoVersions](https://img.shields.io/pypi/djversions/cloudinary.svg)](https://pypi.python.org/pypi/cloudinary/) -[![PyPI Version](https://img.shields.io/pypi/dm/cloudinary.svg)](https://pypi.python.org/pypi/cloudinary/) -[![PyPI License](https://img.shields.io/pypi/l/cloudinary.svg)](https://pypi.python.org/pypi/cloudinary/) +[![PyPI](https://img.shields.io/pypi/v/cloudinary.svg)](https://pypi.org/project/cloudinary/) +[![Downloads](https://img.shields.io/pypi/dm/cloudinary.svg)](https://pypi.org/project/cloudinary/) +[![License](https://img.shields.io/pypi/l/cloudinary.svg)](LICENSE) +## Install -Cloudinary Python SDK -================== +```bash +pip install cloudinary +``` -## About -The Cloudinary Python SDK allows you to quickly and easily integrate your application with Cloudinary. -Effortlessly optimize, transform, upload and manage your cloud's assets. +## Quick start +Set your API environment variable (Console > Settings > API Keys): -#### Note -This Readme provides basic installation and usage information. -For the complete documentation, see the [Python SDK Guide](https://cloudinary.com/documentation/django_integration). +```bash +export CLOUDINARY_URL=cloudinary://:@ +``` -## Table of Contents -- [Key Features](#key-features) -- [Version Support](#Version-Support) -- [Installation](#installation) -- [Usage](#usage) - - [Setup](#Setup) - - [Transform and Optimize Assets](#Transform-and-Optimize-Assets) - - [Django](#Django) +Upload an image and get an optimized delivery URL: +```python +import cloudinary +import cloudinary.uploader # cloudinary.uploader must be imported explicitly + +# Configuration is read from CLOUDINARY_URL automatically. + + +def main(): + # Upload a remote image (a local file path works the same way) + result = cloudinary.uploader.upload( + "https://res.cloudinary.com/demo/image/upload/sample.jpg", + public_id="quickstart-sample", + ) + print("Uploaded: {0}".format(result["public_id"])) + + # Build a 400x400 auto-cropped URL with automatic format and quality + url = cloudinary.CloudinaryImage(result["public_id"]).build_url( + width=400, + height=400, + crop="fill", + gravity="auto", + fetch_format="auto", + quality="auto", + secure=True, + ) + print("Optimized URL: {0}".format(url)) + + +if __name__ == "__main__": + try: + main() + except Exception as error: + print("Quick start failed: {0}".format(error)) + print("Check that CLOUDINARY_URL is set (Console > Settings > API Keys).") + raise SystemExit(1) +``` -## Key Features -- [Transform](https://cloudinary.com/documentation/django_video_manipulation#video_transformation_examples) and - [optimize](https://cloudinary.com/documentation/django_image_manipulation#image_optimizations) assets. -- Generate [image](https://cloudinary.com/documentation/django_image_manipulation#deliver_and_transform_images) and - [video](https://cloudinary.com/documentation/django_video_manipulation#django_video_transformation_code_examples) tags. -- [Asset Management](https://cloudinary.com/documentation/django_asset_administration). -- [Secure URLs](https://cloudinary.com/documentation/video_manipulation_and_delivery#generating_secure_https_urls_using_sdks). +Save as `quickstart.py` and run `python quickstart.py`. [Create a free account](https://cloudinary.com/users/register_free) if you don't have one — or call `cloudinary.provisioning.create_cloud()` to [provision one without signing up](cloudinary/docs/get-credentials.md). +## Common tasks +- [Get Cloudinary credentials](cloudinary/docs/get-credentials.md) +- [Upload an image](cloudinary/docs/upload-image.md) +- [Upload a large video](cloudinary/docs/upload-large-video.md) +- [Sign a browser upload](cloudinary/docs/sign-browser-upload.md) +- [Transform and deliver an image](cloudinary/docs/transform-and-deliver-image.md) +- [Transform and deliver a video](cloudinary/docs/transform-and-deliver-video.md) +- [Search and manage assets](cloudinary/docs/search-and-manage-assets.md) +- [Moderate an upload](cloudinary/docs/moderate-upload.md) +- [Use structured metadata](cloudinary/docs/use-structured-metadata.md) +- [Use with Django](cloudinary/docs/use-with-django.md) +- [Troubleshoot errors](cloudinary/docs/troubleshoot-errors.md) -## Version Support +Runnable versions live in [`examples/`](examples/) — each is a complete file you can run directly. -| SDK Version | Python 2.7 | Python 3.x | -|-------------|------------|------------| -| 1.x | ✔ | ✔ | +## When to use this SDK -| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x | Django 6.x | -|-------------|-------------|------------|------------|------------|------------|------------| -| 1.x | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +Use this package in **Python server-side code**, including Django: uploads, signed +operations, asset administration, search, moderation, and delivery URL generation. +For other jobs, better-fitting tools exist: -## Installation -```bash -pip install cloudinary -``` +- Browser or frontend framework rendering: the [frontend SDKs](https://cloudinary.com/documentation/frontend_sdks) ([md](https://cloudinary.com/documentation/frontend_sdks.md)). +- Complete in-browser upload UI: [Upload Widget](https://cloudinary.com/documentation/upload_widget) ([md](https://cloudinary.com/documentation/upload_widget.md)). +- Text-to-image generation and image-to-video: [platform APIs](https://cloudinary.com/documentation/image_generation_addon) ([md](https://cloudinary.com/documentation/image_generation_addon.md)), not wrapped by this package. +- Multi-step media workflow automation: [MediaFlows](https://cloudinary.com/documentation/mediaflows_user_guide) ([md](https://cloudinary.com/documentation/mediaflows_user_guide.md)). +- Interactive agent-driven asset operations: [Cloudinary MCP servers and Skills](https://cloudinary.com/documentation/cloudinary_llm_mcp) ([md](https://cloudinary.com/documentation/cloudinary_llm_mcp.md)). -# Usage +The full capability map — plus the Skills, MCP servers, and CLI worth setting up first — +is in [cloudinary/docs/platform-capabilities.md](cloudinary/docs/platform-capabilities.md). -### Setup -```python -import cloudinary -``` +## Status and compatibility -### Transform and Optimize Assets -- [See full documentation](https://cloudinary.com/documentation/django_image_manipulation). +Stable, actively maintained. See [CHANGELOG.md](CHANGELOG.md). -```python -cloudinary.utils.cloudinary_url("sample.jpg", width=100, height=150, crop="fill") -``` +| SDK version | Python | Django | +|-------------|--------|--------| +| 1.x | 3.10 – 3.14 | 4.2, 5.0, 5.1, 5.2, 6.0 | -### Upload -- [See full documentation](https://cloudinary.com/documentation/django_image_and_video_upload). -- [Learn more about configuring your uploads with upload presets](https://cloudinary.com/documentation/upload_presets). -```python -cloudinary.uploader.upload("my_picture.jpg") -``` +The table lists what CI tests on every commit. Older Python and Django releases are +supported on a best-effort basis. -### Django -- [See full documentation](https://cloudinary.com/documentation/django_image_and_video_upload#django_forms_and_models). +## Documentation -### Security options -- [See full documentation](https://cloudinary.com/documentation/solution_overview#security). +- [Bundled task docs](cloudinary/docs/README.md) — ship inside the package, version-matched. +- [Python and Django SDK guide](https://cloudinary.com/documentation/django_integration) — the full documentation ([md](https://cloudinary.com/documentation/django_integration.md)). +- [Python quick start](https://cloudinary.com/documentation/python_quickstart) — a short end-to-end walkthrough ([md](https://cloudinary.com/documentation/python_quickstart.md)). +- [Transformation and REST API references](https://cloudinary.com/documentation/cloudinary_references) — syntax and examples for all SDKs ([md](https://cloudinary.com/documentation/cloudinary_references.md)). -### Sample projects -- [Sample projects](https://github.com/cloudinary/pycloudinary/tree/master/samples). -- [Django Photo Album](https://github.com/cloudinary/cloudinary-django-sample). +Documentation links in this README point at the browsable HTML page, with an `(md)` +companion link that returns the same page as raw Markdown. Inside `cloudinary/docs/` and +`examples/` the links are Markdown-only, since those files are written to be read by +coding agents. Either form works for any page: add `.md` for Markdown, drop it for HTML. +## For AI coding agents -## Contributions -- Ensure tests run locally. -- Open a PR and ensure Travis tests pass. -- See [CONTRIBUTING](CONTRIBUTING.md). +- Contributing to this repo: read [AGENTS.md](AGENTS.md). +- Using the installed package: the Markdown docs in the installed package's + `cloudinary/docs/` directory match your installed version and are the source of truth. + Locate them with + `python -c "import cloudinary, pathlib; print(pathlib.Path(cloudinary.__file__).parent / 'docs')"`, + and start with [platform-capabilities](cloudinary/docs/platform-capabilities.md) before + assuming a feature exists. -## Get Help -If you run into an issue or have a question, you can either: -- Issues related to the SDK: [Open a GitHub issue](https://github.com/cloudinary/pycloudinary/issues). -- Issues related to your account: [Open a support ticket](https://cloudinary.com/contact). +## Support +- SDK bugs and feature requests: [GitHub issues](https://github.com/cloudinary/pycloudinary/issues) +- Account and platform questions: [Cloudinary support](https://support.cloudinary.com) +- Sample projects: [`samples/`](samples/) and the + [Django Photo Album](https://github.com/cloudinary/cloudinary-django-sample) +- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) -## About Cloudinary -Cloudinary is a powerful media API for websites and mobile apps alike, Cloudinary enables developers to efficiently -manage, transform, optimize, and deliver images and videos through multiple CDNs. Ultimately, viewers enjoy responsive -and personalized visual-media experiences—irrespective of the viewing device. +More resources: [Cloudinary Academy](https://training.cloudinary.com/) for courses, +[code explorers and feature demos](https://cloudinary.com/documentation/code_explorers_demos_index), +and the [Cloudinary roadmap](https://cloudinary.com/roadmap). +## Security -## Additional Resources -- [Cloudinary Transformation and REST API References](https://cloudinary.com/documentation/cloudinary_references): Comprehensive references, including syntax and examples for all SDKs. -- [MediaJams.dev](https://mediajams.dev/): Bite-size use-case tutorials written by and for Cloudinary Developers -- [DevJams](https://www.youtube.com/playlist?list=PL8dVGjLA2oMr09amgERARsZyrOz_sPvqw): Cloudinary developer podcasts on YouTube. -- [Cloudinary Academy](https://training.cloudinary.com/): Free self-paced courses, instructor-led virtual courses, and on-site courses. -- [Code Explorers and Feature Demos](https://cloudinary.com/documentation/code_explorers_demos_index): A one-stop shop for all code explorers, Postman collections, and feature demos found in the docs. -- [Cloudinary Roadmap](https://cloudinary.com/roadmap): Your chance to follow, vote, or suggest what Cloudinary should develop next. -- [Cloudinary Facebook Community](https://www.facebook.com/groups/CloudinaryCommunity): Learn from and offer help to other Cloudinary developers. -- [Cloudinary Account Registration](https://cloudinary.com/users/register/free): Free Cloudinary account registration. -- [Cloudinary Website](https://cloudinary.com): Learn about Cloudinary's products, partners, customers, pricing, and more. +See [SECURITY.md](SECURITY.md) for private vulnerability reporting. Keep your +`api_secret` in server-side code; for client uploads, use the server-signed pattern in +[Sign a browser upload](cloudinary/docs/sign-browser-upload.md). +## License -## Licence -Released under the MIT license. +Released under the MIT license — see [LICENSE](LICENSE). Copyright (c) Cloudinary Ltd. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..12efd64b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,50 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---------|-----------| +| 1.x | Yes | + +## Reporting a vulnerability + +Report security issues privately through +[GitHub private vulnerability reporting](https://github.com/cloudinary/pycloudinary/security/advisories/new). + +If you cannot use GitHub, open a ticket at +[support.cloudinary.com](https://support.cloudinary.com) and mark the ticket as a +security issue. + +Use these private channels for anything security-sensitive; public GitHub issues are +for regular bugs and feature requests. + +## What to include in a report + +- The affected package version and your Python version. +- A minimal reproduction or proof of concept. +- The impact you believe it has — for example: credential exposure, signature + bypass, request forgery. +- Any suggested remediation. + +## Response and disclosure process + +- We acknowledge reports and keep you informed while we investigate. +- Fixes ship as patched versions, with a changelog entry noting the security-relevant + change without exploit detail before users can upgrade. +- We ask for reasonable time to release a fix before public disclosure. + +## Security guidance for SDK users + +- Your `api_secret` belongs in server-side code only. Never ship it in a browser + bundle, mobile app, or public repository. +- Configure the SDK from the `CLOUDINARY_URL` environment variable rather than + hardcoding credentials. +- Sign browser and mobile uploads on your server — see + [cloudinary/docs/sign-browser-upload.md](cloudinary/docs/sign-browser-upload.md). +- If you use unsigned uploads, restrict the upload preset deliberately: allowed + formats, size limits, and a fixed folder. See + [upload presets](https://cloudinary.com/documentation/upload_presets) + ([md](https://cloudinary.com/documentation/upload_presets.md)). +- Platform-level security options are documented in + [Cloudinary security](https://cloudinary.com/documentation/control_access_to_media) + ([md](https://cloudinary.com/documentation/control_access_to_media.md)). diff --git a/cloudinary/docs/README.md b/cloudinary/docs/README.md new file mode 100644 index 00000000..20ac5a64 --- /dev/null +++ b/cloudinary/docs/README.md @@ -0,0 +1,63 @@ + + +# cloudinary — bundled documentation + +> **Version-matched:** these docs ship inside the package and always describe the +> version you have installed. Prefer them over anything remembered from training data +> or found for another version. + +Task documentation for the `cloudinary` Python SDK. Each page is self-contained: +imports, configuration, a complete runnable flow, expected results, and common +failures. To locate these files in an installed environment: + +```bash +python -c "import cloudinary, pathlib; print(pathlib.Path(cloudinary.__file__).parent / 'docs')" +``` + +Runnable versions of most tasks are in the [`examples/`](https://github.com/cloudinary/pycloudinary/tree/master/examples) +directory of the repository. + +## Start here + +- [What this SDK does and does not do](platform-capabilities.md) — the agent tooling to + set up first (Skills, MCP servers, CLI, documentation indexes), what this package + covers, and what lives elsewhere on the platform. +- [Get Cloudinary credentials](get-credentials.md) — no account needed: provision a cloud + with `cloudinary.provisioning.create_cloud()` and start building. +- [Import and call the SDK](import-and-call.md) — which modules to import, and why + `import cloudinary` alone is not enough. + +## Tasks + +- [Configure Cloudinary](configure-cloudinary.md) +- [Upload an image](upload-image.md) +- [Upload a large video](upload-large-video.md) +- [Sign a browser upload](sign-browser-upload.md) +- [Transform and deliver an image](transform-and-deliver-image.md) +- [Transform and deliver a video](transform-and-deliver-video.md) +- [Search and manage assets](search-and-manage-assets.md) +- [Moderate an upload](moderate-upload.md) +- [Use structured metadata](use-structured-metadata.md) +- [Use with Django](use-with-django.md) +- [Troubleshoot errors](troubleshoot-errors.md) + +## Security boundary + +This is a **server-side** SDK. It holds your `api_secret`, which belongs on your server +only. Frontend code should receive delivery URLs or short-lived signatures generated by +your server ([how](sign-browser-upload.md)). + +## Canonical docs + +- [Python and Django SDK guide](https://cloudinary.com/documentation/django_integration.md) +- [Python quick start](https://cloudinary.com/documentation/python_quickstart.md) +- [Full platform reference](https://cloudinary.com/documentation/cloudinary_references.md) + +**Link convention:** documentation links in these docs end in `.md` and return raw +Markdown — the preferred format for agents and for anything that parses text. Remove the +`.md` suffix for the same page as browsable HTML. The repository README links the HTML +form first, since it is read by people. diff --git a/cloudinary/docs/configure-cloudinary.md b/cloudinary/docs/configure-cloudinary.md new file mode 100644 index 00000000..0b7bdf43 --- /dev/null +++ b/cloudinary/docs/configure-cloudinary.md @@ -0,0 +1,86 @@ +# Configure Cloudinary + +## When to use + +Do this once per process before any upload, admin, or URL-generation call. + +**Prerequisite:** a `cloud_name`, `api_key`, and `api_secret`. If you do not have them, +see [Get Cloudinary credentials](get-credentials.md) — +`cloudinary.provisioning.create_cloud()` provisions a working cloud with no signup. + +## Recommended: environment variable + +Set `CLOUDINARY_URL` (from Console > Settings > API Keys, or from the +`api_environment_variable` field returned when you provision a cloud): + +```bash +export CLOUDINARY_URL=cloudinary://:@ +``` + +```python +import cloudinary + +# Configuration is read from CLOUDINARY_URL automatically on first use. +print(cloudinary.config().cloud_name) +``` + +## Alternative: explicit configuration + +```python +import os +import cloudinary + +cloudinary.config( + cloud_name="my-cloud", + api_key=os.environ["CLOUDINARY_API_KEY"], + api_secret=os.environ["CLOUDINARY_API_SECRET"], + secure=True, +) +``` + +## Behavior you should know + +- Configuration is **process-global**: `cloudinary.config()` affects every caller in the + process. Pass per-call options as keyword arguments when you need to override one + value for a single call. +- **Individual `CLOUDINARY_*` variables win over `CLOUDINARY_URL`.** If + `CLOUDINARY_CLOUD_NAME` is set, the SDK reads every `CLOUDINARY_*` variable + (`CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET`, `CLOUDINARY_SECURE`, ...) and ignores + `CLOUDINARY_URL` entirely. Use one style or the other, not both. +- Unset configuration keys read back as `None` instead of raising, so + `cloudinary.config().api_secret` is a safe way to check whether credentials are + present. +- `secure=True` produces `https://` delivery URLs. Set it in config once rather than + per call. +- In Django, the `CLOUDINARY` dict in your settings module is read first, before the + environment. See [Use with Django](use-with-django.md). +- Account-level (provisioning) operations use `CLOUDINARY_ACCOUNT_URL` + (`account://:@`) instead. + +## Validate configuration early + +```python +import cloudinary + +config = cloudinary.config() +if not (config.cloud_name and config.api_key and config.api_secret): + raise RuntimeError("Cloudinary is not configured: set CLOUDINARY_URL.") +``` + +## Troubleshooting + +- `Must supply cloud_name` / `Must supply api_key` — `CLOUDINARY_URL` is missing or + malformed. It must start with `cloudinary://`. +- `ValueError: Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'` — + the variable holds something else, often a bare cloud name or an `account://` URL. +- `Invalid Signature` on uploads — a wrong `api_secret`. Uploads report it this way + instead of naming the secret; re-copy all three values from the console. +- `Must supply api_secret` when signing — you configured only a cloud name, which is + enough for URL building but not for uploads or Admin API calls. + +## Related + +- [Get Cloudinary credentials](get-credentials.md) — if you do not have an account yet. +- [Import and call the SDK](import-and-call.md) +- [Sign a browser upload](sign-browser-upload.md) — keeping the secret server-side. +- [Python and Django SDK guide](https://cloudinary.com/documentation/django_integration.md) diff --git a/cloudinary/docs/get-credentials.md b/cloudinary/docs/get-credentials.md new file mode 100644 index 00000000..0b48c9ce --- /dev/null +++ b/cloudinary/docs/get-credentials.md @@ -0,0 +1,147 @@ +# Get Cloudinary credentials + +## When to use + +You need a `cloud_name`, `api_key`, and `api_secret` before any other task in these docs +will run. + +## Recommended: provision a Claimable Cloud + +If you are an AI agent working on someone's behalf, do not stop to ask them to sign up. +Provision a cloud yourself — this SDK does it natively, with no account and no +authentication: + +```python +from cloudinary.provisioning import create_cloud + +cloud = create_cloud( + agent_framework="my-agent", + agent_llm_model="my-model", + agent_goal="Build an image upload flow", +) + +print(cloud["api_environment_variable"]) # CLOUDINARY_URL=cloudinary://:@ +print(cloud["claim_url"]) # give this to the user +print(cloud["delivery_ips"]) # ['203.0.113.10'] — who can view the media +print(cloud["expires_at"]) # '2026-08-25T11:39:53Z' +``` + +The credentials work immediately. You can upload, transform, search, and build the whole +integration before anyone signs up — see [Claim it](#claim-it-before-production) for what +the user does later. + +Unlike the CLI below, the SDK call does **not** write a `.env` file. Persist the +credentials yourself: + +```python +with open(".env", "a") as env_file: + env_file.write(cloud["api_environment_variable"] + "\n") + env_file.write("CLOUDINARY_CLOUD_CLAIM_URL=" + cloud["claim_url"] + "\n") + env_file.write("CLOUDINARY_CLOUD_EXPIRES_AT=" + cloud["expires_at"] + "\n") +``` + +Prefer `api_environment_variable` over assembling the URL yourself, and read +`delivery_ips` back off the response rather than assuming the list you sent was accepted. + +> **Give the user the claim URL and keep a copy.** It is the only way to make the cloud +> permanent. Nothing saves it for you when you provision from Python, so write it +> somewhere durable — and report it to the user in your reply rather than leaving it in a +> file they will not look at. + +The response also contains `cloud_name`, `api_key`, `api_secret`, `account_id`, and a +`guidance` string describing these same limits. + +### Choosing who can view the media + +Delivery is locked to the IP addresses in `delivery_ips`. The caller's own resolved +address is appended automatically, so omitting the parameter is the usual call. Supply up +to three more for hosts other than the caller: + +```python +cloud = create_cloud(delivery_ips=["203.0.113.10"]) # a teammate or deployed viewer +``` + +Non-public addresses are dropped, and the call fails with +`delivery_ips must contain at least one public IP address` unless at least one public +address remains — so read `delivery_ips` back off the response rather than assuming the +list you sent was stored. The IPs are fixed for the cloud's lifetime; if they are wrong, +create another cloud or claim this one. + +## Two limits before the cloud is claimed + +- **Delivery is IP-locked.** Requests from anywhere else are blocked at the CDN edge. + That is the right default when the machine building the integration is also the one + viewing the media — but a teammate, a CI runner, or a deployed environment will not + load it. The restriction covers media delivery only: uploads, Admin API calls, and URL + generation work from anywhere. +- **It expires.** An unclaimed cloud is disabled at `expires_at`, **assets included**. + Claiming is what prevents that; there is no parameter to extend it. + +## Troubleshooting + +- `delivery_ips must contain at least one public IP address` — a VPN or secure gateway + made the request arrive from a private address. Pass your public address explicitly, as + shown above. +- Media returns 403, or `x-cld-error: ACL deny` on a delivery URL, while uploads succeed + — that is the IP restriction, not bad credentials. Deliver from an allowed address, or + claim the cloud to lift the lock. +- Clouds are rate-limited per IP. Store the credentials from the first call rather than + provisioning repeatedly. + +## Claim it before production + +Send the user the `claim_url`. They open it, enter their email address, and confirm. +No verification email is sent at creation time. + +After claiming, the `cloud_name`, `api_key`, and `api_secret` stay the same and the assets +already uploaded are retained — nothing in your code changes. The IP lock is removed so +media delivers globally, and the cloud becomes a permanent free account instead of +expiring. + +**Do not ship to production on an unclaimed cloud.** It will expire and stop serving. + +## Alternative: create a free account from the SDK + +`create_agent_account` registers a Free-plan account against an email address: + +```python +from cloudinary.provisioning import create_agent_account + +account = create_agent_account( + email="developer@example.com", + agent_framework="my-agent", + agent_llm_model="my-model", + agent_goal="Build an image upload flow", +) +``` + +The user receives a verification email. The returned credentials stay inert until they +verify, so this route does not let you start building immediately — prefer +`create_cloud()` when you want to work now. + +## Alternative: the CLI or a plain HTTP call + +```bash +npx @cloudinary/cloud # provisions a cloud and writes CLOUDINARY_URL into ./.env +``` + +```bash +curl -X POST https://api.cloudinary.com/v1_1/provisioning/clouds \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +Read the credentials from `product_environments[0]` if that key is present and from the +top level otherwise. + +## Alternative: sign up manually + +A person can create an account at +[cloudinary.com/users/register_free](https://cloudinary.com/users/register_free) and copy +the credentials from Console > Settings > API Keys. + +## Related + +- Runnable example: `examples/provision-claimable-cloud.py` +- [Configure Cloudinary](configure-cloudinary.md) — what to do with the credentials. +- [Claimable Cloud API reference](https://cloudinary.com/documentation/claimable_cloud_provisioning.md) diff --git a/cloudinary/docs/import-and-call.md b/cloudinary/docs/import-and-call.md new file mode 100644 index 00000000..92935d79 --- /dev/null +++ b/cloudinary/docs/import-and-call.md @@ -0,0 +1,29 @@ +# Import and call the SDK + +```python +import cloudinary +import cloudinary.uploader +import cloudinary.api + +cloudinary.uploader.upload("photo.jpg") +cloudinary.api.resource("photo") +cloudinary.CloudinaryImage("photo").build_url(width=400, crop="fill") +``` + +Import the submodules you use explicitly. `import cloudinary` on its own gives you +`config()`, `CloudinaryImage`, `CloudinaryVideo`, `Search`, and `cloudinary.utils`, +but **not** `cloudinary.uploader` or `cloudinary.api` — reaching for those raises +`AttributeError: module 'cloudinary' has no attribute 'uploader'` until you import +them. + +Calls are synchronous and return plain dictionaries. Positional arguments come first, +then keyword options that pass through to the API: +`cloudinary.uploader.upload("photo.jpg", public_id="hero", folder="products")`. + +Configuration is read from the `CLOUDINARY_URL` environment variable on first use, so +no setup call is needed. See [Configure Cloudinary](configure-cloudinary.md). + +## Related + +- [Configure Cloudinary](configure-cloudinary.md) +- [Python and Django SDK guide](https://cloudinary.com/documentation/django_integration.md) diff --git a/cloudinary/docs/moderate-upload.md b/cloudinary/docs/moderate-upload.md new file mode 100644 index 00000000..2186eb42 --- /dev/null +++ b/cloudinary/docs/moderate-upload.md @@ -0,0 +1,127 @@ +# Moderate an upload + +## When to use + +Holding user-generated content back from delivery until it is approved — either by a +person or by an automated add-on. + +An asset with a pending moderation status is uploaded and stored, but its delivery URL +returns 404 until it is approved. That is the mechanism: no separate quarantine bucket, no +second upload. + +For platform-wide moderation with review queues and rules, see +[Cloudinary Moderation](https://cloudinary.com/documentation/cloudinary_moderation.md) — +distinct from the per-asset flag this SDK sets. + +## Complete flow (manual review queue) + +```python +import cloudinary +import cloudinary.api +import cloudinary.uploader + +# Configuration is read from CLOUDINARY_URL automatically. + + +def main(): + result = cloudinary.uploader.upload( + "https://res.cloudinary.com/demo/image/upload/sample.jpg", + moderation="manual", # hold for review + folder="user-uploads", + ) + + # The upload result carries a `moderation` list, not a `moderation_status` key. + print(result["moderation"]) # [{'kind': 'manual', 'status': 'pending'}] + public_id = result["public_id"] + + # Later: list what is waiting for review. + pending = cloudinary.api.resources_by_moderation("manual", "pending", max_results=100) + print(len(pending["resources"])) + + # Approve it (or "rejected" to keep it dark). + approved = cloudinary.api.update(public_id, moderation_status="approved") + print(approved["moderation_status"]) # 'approved' — now deliverable + print(approved["moderation"]) # [{'kind': 'manual', 'status': 'approved', 'updated_at': ...}] + + return approved + + +if __name__ == "__main__": + try: + main() + except cloudinary.exceptions.Error as error: + print("Moderation flow failed: {0}".format(error)) + raise SystemExit(1) +``` + +## Result fields to keep + +Store `asset_id` and `public_id` alongside your own review record. + +Which field holds the status depends on the call: the **upload** result exposes only the +`moderation` list (`[{"kind": "manual", "status": "pending"}]`), while +`cloudinary.api.resource()` and `cloudinary.api.update()` return both `moderation` and a +flat `moderation_status`. Read `result["moderation"][0]["status"]` if you want one +expression that works everywhere. + +The three statuses are `pending`, `approved`, and `rejected`. Only `approved` delivers. + +## Automated moderation + +Replace `moderation="manual"` with an add-on. These are paid and must be enabled on the +account first: + +```python +cloudinary.uploader.upload(source, moderation="aws_rek") # images +cloudinary.uploader.upload(source, moderation="google_video_moderation", + resource_type="video") # video +cloudinary.uploader.upload(source, moderation="perception_point") # malware +``` + +Automated moderation is asynchronous: the upload returns `pending` and the verdict lands +seconds to minutes later. Do not block on it — set `notification_url` and react to the +webhook, or poll `cloudinary.api.resource(public_id)`. + +Assert on shape, not on verdicts. Model output varies between runs and versions, so check +that a `moderation` entry exists with a known status value rather than expecting a +specific score. + +## Combining moderation with tagging + +Moderation answers "may this be shown"; the analysis add-ons answer "what is in it". They +compose in one call: + +```python +cloudinary.uploader.upload( + source, + moderation="aws_rek", + categorization="google_tagging", + auto_tagging=0.7, # apply tags above this confidence + notification_url="https://example.com/cloudinary-hook", +) +``` + +## Troubleshooting + +- The URL 404s right after upload — that is moderation working. Approve the asset, or + check `moderation_status`. +- `You don't have an active subscription for ` (raised as `RateLimited`, not a + permission error) — the named add-on is not enabled on this account. + `moderation="manual"` needs no subscription and is the way to test the flow. +- `moderation_status` stays `pending` forever with an automated kind — the add-on is + queued or failed; check the `moderation` entry for an error, and confirm the add-on + supports the resource type. +- `Illegal moderation status: ` on update — the accepted values are `approved` + and `rejected`. +- `resources_by_moderation` returns nothing — the `kind` argument must match the + moderation used at upload (`manual`, `aws_rek`, ...), not the status. +- Rejected assets still occupy storage. Delete them with `cloudinary.uploader.destroy()` + if you do not need an audit trail. + +## Related + +- Runnable example: `examples/moderate-upload.py` +- [Upload an image](upload-image.md) +- [Search and manage assets](search-and-manage-assets.md) — finding and deleting what you + moderated. +- [Moderation add-ons](https://cloudinary.com/documentation/moderate_assets.md) diff --git a/cloudinary/docs/platform-capabilities.md b/cloudinary/docs/platform-capabilities.md new file mode 100644 index 00000000..0496bf49 --- /dev/null +++ b/cloudinary/docs/platform-capabilities.md @@ -0,0 +1,130 @@ +# What this SDK does and does not do + +Cloudinary is a platform with many surfaces. This package — the Python and Django +server-side SDK — implements some of them natively, expresses some through generic URL +building, and does not implement others. Verified against this package version. + +## Start here + +**Use this SDK for code that ships.** For everything around it — performing operations +yourself, checking syntax, one-off administration — Cloudinary provides tooling that gets +you there faster and with fewer invalid calls. Set it up before writing integration code. + +### No account yet? + +Do not stop to ask the user to sign up — provision a cloud from this SDK and build +immediately: + +```python +from cloudinary.provisioning import create_cloud + +cloud = create_cloud() +print(cloud["api_environment_variable"]) +``` + +Credentials work at once; the user claims the cloud later. See +[Get Cloudinary credentials](get-credentials.md). + +### Skills — get the patterns right + +```bash +npx skills add cloudinary-devs/skills +``` + +- `cloudinary-docs` — picks the right documentation pages for a question or integration. +- `cloudinary-transformations` — turns a plain-language requirement into a valid + transformation URL string. Faster and more reliable than assembling one by hand. +- `cloudinary-react`, `cloudinary-next` — frontend SDK patterns, for the parts this + package does not cover. + +### MCP servers — perform operations directly + +- **Asset Management** — upload and manage images, video, and raw files; advanced search. +- **Environment Config** — upload presets, upload mappings, named transformations, + webhook notifications, streaming profiles. +- **Structured Metadata** — metadata fields, values, and conditional rules. +- **Analysis** — AI tagging, moderation, safety checks, object detection. +- **MediaFlows** — build and manage workflow automations. + +Setup: [MCP servers and Skills](https://cloudinary.com/documentation/cloudinary_llm_mcp.md). + +### CLI — scripted and one-off work + +```bash +pipx install cloudinary-cli # command: cld +``` + +Admin, Upload, Search, and Provisioning operations from a terminal; good for batch jobs +and migrations. Run it locally or server-side only — it holds your `api_secret`. See the +[CLI guide](https://cloudinary.com/documentation/cloudinary_cli.md). + +### Documentation indexes + +Cloudinary publishes agent-readable indexes. Fetch these instead of guessing at URLs: + +- https://cloudinary.com/documentation/llms.txt — all products. +- https://cloudinary.com/documentation/llms-image-and-video-apis.txt — everything + relevant to this SDK. +- https://cloudinary.com/documentation/llms-troubleshooting.txt — diagnosing errors + across products. + +--- + +## Get media in + +| To do this | Use | Where to go | +|---|---|---| +| Upload a file path, file object, or remote URL | `uploader.upload` | [Upload an image](upload-image.md) | +| Upload something too large for one request | `uploader.upload_large` | [Upload a large video](upload-large-video.md) | +| Let a browser or mobile app upload directly, authorized by your server | `utils.api_sign_request` | [Sign a browser upload](sign-browser-upload.md) | +| Accept uploads through a Django model field or form | `CloudinaryField`, `CloudinaryFileField` | [Use with Django](use-with-django.md) | +| Review user-generated content before showing it | upload options + `api` | [Moderate an upload](moderate-upload.md) | + +## Deliver and transform + +| To do this | Use | Where to go | +|---|---|---| +| Build a resize, crop, overlay, or format-optimized image URL | `CloudinaryImage.build_url`, `utils.cloudinary_url` | [Transform and deliver an image](transform-and-deliver-image.md) | +| Build a video URL, player tag, poster frame, or HLS/DASH stream | `CloudinaryVideo.build_url`, `CloudinaryVideo.video` | [Transform and deliver a video](transform-and-deliver-video.md) | +| Apply generative edits (gen fill, background removal, ...) | `effect` / `raw_transformation` — **generic strings only, no typed builders** | [Transform and deliver an image](transform-and-deliver-image.md) | +| Render delivery URLs in Django templates | `{% cloudinary %}`, `{% cloudinary_url %}` | [Use with Django](use-with-django.md) | + +URL building is local: no network call, no `api_secret`. + +## Find and manage what you have + +| To do this | Use | Where to go | +|---|---|---| +| Query assets by field, tag, folder, or date | `Search` | [Search and manage assets](search-and-manage-assets.md) | +| Read, update, restore, or delete an asset; manage folders and presets | `api` — the Assets Admin API | [Search and manage assets](search-and-manage-assets.md) | +| Attach and query typed metadata fields | `api` + upload options | [Use structured metadata](use-structured-metadata.md) | +| Find visually similar assets | `api.visual_search` — needs the feature enabled | [Visual Search](https://cloudinary.com/documentation/visual_search.md) | + +## Analyze + +| To do this | Use | Where to go | +|---|---|---| +| Caption, tag, or detect content in an asset | `api.analyze` — **limited model set**, needs a subscription | [Analyze API guide](https://cloudinary.com/documentation/analyze_api_guide.md) | +| Tag, moderate, or transcribe at upload time | upload options: `auto_tagging`, `moderation`, `ocr`, `auto_transcription`, `background_removal` | [Moderate an upload](moderate-upload.md) | + +## Administer accounts + +| To do this | Use | Where to go | +|---|---|---| +| Create and manage sub-accounts and users | `provisioning`, via `CLOUDINARY_ACCOUNT_URL` | [Provisioning API docs](https://cloudinary.com/documentation/provisioning_api.md) | +| Provision a cloud with no account | `provisioning.create_cloud` | [Get Cloudinary credentials](get-credentials.md) | + +## Not in this package + +This package covers Cloudinary's Image and Video APIs. Cloudinary is a multi-product +platform, and the capabilities below are real but live elsewhere — whatever your training +data suggests, there is no method here for them. + +| Capability | Use instead | +|---|---| +| Text-to-image generation | [Image Generation API](https://cloudinary.com/documentation/image_generation_addon.md) | +| Image-to-video generation | [Image-to-Video API](https://cloudinary.com/documentation/image_to_video_addon.md) — async, credit-based, regional | +| Multi-step workflow automation | [MediaFlows](https://cloudinary.com/documentation/mediaflows_user_guide.md) — or its MCP server | +| Media Library UI, approval workflows, folder-based access control | [Cloudinary Assets (DAM)](https://cloudinary.com/documentation/digital_asset_management_overview.md) | +| Rule-based content review before publication | [Cloudinary Moderation](https://cloudinary.com/documentation/cloudinary_moderation.md) — distinct from the per-asset [moderation flag](moderate-upload.md) this SDK sets | +| Frontend rendering, responsive images, upload UI | [frontend SDKs](https://cloudinary.com/documentation/frontend_sdks.md), [Upload Widget](https://cloudinary.com/documentation/upload_widget.md) | diff --git a/cloudinary/docs/search-and-manage-assets.md b/cloudinary/docs/search-and-manage-assets.md new file mode 100644 index 00000000..55878fce --- /dev/null +++ b/cloudinary/docs/search-and-manage-assets.md @@ -0,0 +1,189 @@ +# Search and manage assets + +## When to use + +Finding assets you have already uploaded, and reading, updating, organizing, or deleting +them. + +Two different APIs do this, and picking the wrong one is the usual source of confusion: + +- **Search** (`cloudinary.Search`) — query by expression across fields, tags, folders, and + dates. This is what you want for "find the assets matching X". +- **Admin** (`cloudinary.api`) — read or change one asset, or list by a single axis + (tag, prefix, folder). This is what you want for "do something to this asset". + +## Complete flow + +```python +import cloudinary +import cloudinary.api # cloudinary.api must be imported explicitly + +# Configuration is read from CLOUDINARY_URL automatically. + + +def main(): + result = ( + cloudinary.Search() + .expression("resource_type:image AND tags=catalog") + .sort_by("created_at", "desc") + .max_results(30) + .execute() + ) + + print(result["total_count"]) # 12 — matches, not the page size + for asset in result["resources"]: + print(asset["asset_id"], asset["public_id"], asset["secure_url"]) + + return result + + +if __name__ == "__main__": + try: + main() + except cloudinary.exceptions.Error as error: + print("Search failed: {0}".format(error)) + raise SystemExit(1) +``` + +The fluent methods each return the same builder, so order does not matter; nothing is sent +until `.execute()`. + +## Result fields to keep + +`total_count` is the number of matches; `resources` holds one page of them. Store +`asset_id` for anything you need to reference later — it survives renames, `public_id` +does not. Keep the `public_id` too: delivery URLs, transformations, and the uploader +methods are all addressed by `public_id`, and there is no asset-id form of them. + +## Paging through everything + +Search pages with an opaque cursor, capped at 500 results per page: + +```python +search = cloudinary.Search().expression("resource_type:image").max_results(500) +cursor = None + +while True: + result = search.next_cursor(cursor).execute() + for asset in result["resources"]: + print(asset["asset_id"]) + cursor = result.get("next_cursor") + if not cursor: + break +``` + +Never loop on `total_count` — it is an estimate for large result sets. Loop until +`next_cursor` is absent. + +## Useful expressions + +```text +tags=catalog AND resource_type:image # tagged, images only +folder:products/* # a folder and everything under it +uploaded_at>1d AND bytes>1m # recent and large +format:mp4 AND duration>60 # long videos +-tags=archived # exclude a tag +context.alt:bag* # prefix match in context metadata +``` + +Ask for extra fields explicitly — `context`, `tags`, and `metadata` are omitted by +default: + +```python +cloudinary.Search().expression("tags=catalog").with_field("context").with_field("tags").execute() +``` + +## Reading and updating one asset + +Prefer `asset_id` as the handle: it is immutable, so a rename or a move cannot break the +reference. + +```python +import cloudinary.api + +asset = cloudinary.api.resource_by_asset_id("abcdef0123456789abcdef0123456789") +print(asset["bytes"], asset["width"], len(asset["derived"])) +``` + +`cloudinary.api.resource("products/leather-bag")` is the `public_id` equivalent, and +`cloudinary.api.resources_by_asset_ids([...])` reads up to 100 at once. + +Updating is `public_id`-only — there is no `update_by_asset_id`, so read the `public_id` +off the asset you just fetched rather than hardcoding it: + +```python +cloudinary.api.update( + asset["public_id"], + tags=["catalog", "autumn"], + context={"alt": "Brown leather shoulder bag"}, +) +``` + +## Organizing + +```python +import cloudinary.api +import cloudinary.uploader + +cloudinary.api.create_folder("products/bags") +cloudinary.api.subfolders("products") + +# rename_folder returns {"from": {...}, "to": {...}} — there is no "success" key. +cloudinary.api.rename_folder("products/bags", "products/handbags") + +cloudinary.uploader.rename("products/leather-bag", "products/handbags/leather-bag") +cloudinary.uploader.add_tag("featured", ["products/handbags/leather-bag"]) +cloudinary.api.tags(max_results=100) +``` + +## Deleting + +```python +import cloudinary.api +import cloudinary.uploader + +cloudinary.api.delete_resources_by_asset_ids([ # up to 100 + "abcdef0123456789abcdef0123456789", + "fedcba9876543210fedcba9876543210", +]) +cloudinary.api.delete_resources(["id-a", "id-b"]) # by public_id +cloudinary.api.delete_resources_by_prefix("temp/") # a whole prefix +cloudinary.api.delete_derived_resources([derived_id]) # transformed copies only + +cloudinary.uploader.destroy("products/leather-bag", invalidate=True) # public_id only +``` + +`invalidate=True` purges the CDN as well as the origin, and only `uploader.destroy` takes +it — the batch deletes above accept `invalidate` as an option but identify assets in bulk. +Deletion is permanent unless the asset is backed up, in which case +`cloudinary.api.restore_by_asset_ids([asset_id])` (or `restore(["id"])` by `public_id`) +brings it back. + +## Troubleshooting + +- Search returns nothing but the asset exists — the expression is wrong, not the index. + `tags=catalog` uses `=`, while `resource_type:image` uses `:`. Test with a bare + `public_id:my-asset` first. +- A brand-new asset is missing from results — the search index is eventually consistent + and lags an upload by a few seconds. Use `cloudinary.api.resource_by_asset_id()` with + the `asset_id` the upload returned for read-after-write. +- `context` or `tags` missing from results — request them with `.with_field()`. +- `Rate limit exceeded` — the Admin API allows roughly 500 requests per hour on free + plans. Prefer one Search call over many `resource()` calls, and batch deletes. +- `Invalid value for parameter next_cursor` — cursors expire and are not shareable + between differing queries; restart the loop. +- `Query Error (at position N)` — an unsupported expression. Leading wildcards are + rejected (`context.alt:*bag*` fails, `context.alt:bag*` works), and metadata matches + use `:` rather than `=`. +- `Cannot find source folder` from `rename_folder` — the `from_path` does not exist, often + because a previous run already renamed it. +- `Resource not found` on update or destroy — the wrong `resource_type` (`video`, `raw`) + or `type` (`private`, `authenticated`); both default to image/upload. + +## Related + +- Runnable example: `examples/search-and-manage-assets.py` +- [Use structured metadata](use-structured-metadata.md) — typed fields you can search on. +- [Upload an image](upload-image.md) +- [Search API reference](https://cloudinary.com/documentation/search_api.md) +- [Admin API reference](https://cloudinary.com/documentation/admin_api.md) diff --git a/cloudinary/docs/sign-browser-upload.md b/cloudinary/docs/sign-browser-upload.md new file mode 100644 index 00000000..b7b54d49 --- /dev/null +++ b/cloudinary/docs/sign-browser-upload.md @@ -0,0 +1,136 @@ +# Sign a browser upload + +## When to use + +Letting a browser or mobile app upload directly to Cloudinary, authorized by your server. +The file never passes through your application, but your `api_secret` never leaves it +either. + +Your server signs a set of upload parameters; the client posts the file plus that +signature to Cloudinary. For server-side uploads, see +[Upload an image](upload-image.md). + +## Complete flow + +The server endpoint that issues a signature: + +```python +import time + +import cloudinary +import cloudinary.utils # available after `import cloudinary` + +# Configuration is read from CLOUDINARY_URL automatically. + + +def signature_payload(): + config = cloudinary.config() + + # Every parameter signed here must be sent by the client, byte for byte. + params_to_sign = { + "timestamp": int(time.time()), + "folder": "user-uploads", + } + + signature = cloudinary.utils.api_sign_request(params_to_sign, config.api_secret) + + return { + "signature": signature, # 40-character hex (SHA-1) + "api_key": config.api_key, # public, safe to send + "cloud_name": config.cloud_name, + **params_to_sign, + } + + +if __name__ == "__main__": + print(signature_payload()) +``` + +The client posts to +`https://api.cloudinary.com/v1_1//image/upload` with the same parameters plus +`file`, `api_key`, and `signature`. Build that endpoint without hardcoding it: + +```python +endpoint = cloudinary.utils.cloudinary_api_url("upload", resource_type="image") +# https://api.cloudinary.com/v1_1//image/upload +``` + +## The rule that causes most failures + +**The signed parameters and the submitted parameters must match exactly.** If the client +adds `tags` that the server did not sign, or the server signs a `folder` the client omits, +Cloudinary returns `Invalid Signature`. Sign every parameter the client will send, and +send nothing extra. + +Signatures are timestamp-bound and valid for one hour. Issue one per upload rather than +caching. + +## Restricting what the client may do + +Signing authorizes an upload; it does not constrain it. To limit format, size, or +destination, sign a preset instead of raw parameters: + +```python +params_to_sign = {"timestamp": int(time.time()), "upload_preset": "user-avatars"} +``` + +Create the preset once with the folder, `allowed_formats`, and transformations you want: + +```python +import cloudinary.api + +cloudinary.api.create_upload_preset( + name="user-avatars", + unsigned=False, + folder="avatars", + allowed_formats="jpg,png,webp", + transformation=[{"width": 512, "height": 512, "crop": "fill", "gravity": "face"}], +) +``` + +## Unsigned uploads + +An unsigned preset removes the server round trip entirely — and with it your control over +who uploads. Use it only for genuinely public uploads, with a deliberately restricted +preset (`unsigned=True`, fixed folder, `allowed_formats`, size caps): + +```python +cloudinary.uploader.unsigned_upload("photo.jpg", "public-drop-box") +``` + +## Verifying the result + +Cloudinary can sign its response and its webhooks so your server can trust them: + +```python +cloudinary.utils.verify_api_response_signature( + result["public_id"], result["version"], result["signature"] +) +``` + +For webhook payloads use +`cloudinary.utils.verify_notification_signature(body, timestamp, signature)`. + +## Troubleshooting + +- `Invalid Signature . String to sign - ''` — the signed and submitted + parameters differ. The error prints the exact string the server signed; compare it with + what the client sent. This is almost always an extra or missing parameter, not a wrong + secret. +- `Must supply api_secret` — the signing process has no credentials configured. +- `Stale request` — the `timestamp` is more than an hour old, or the server clock is + wrong. +- `Upload preset not found` — the preset name does not exist on this cloud, or it is + unsigned while you signed the request. +- The client gets a CORS error — you are posting to the wrong host. Uploads go to + `api.cloudinary.com`, not `res.cloudinary.com`. + +## Related + +- Runnable example: `examples/sign-browser-upload.py` +- [Upload an image](upload-image.md) — server-side uploads. +- [Use with Django](use-with-django.md) — `CloudinaryJsFileField` renders a signed + direct-upload widget for you. +- [Upload Widget](https://cloudinary.com/documentation/upload_widget.md) — a complete + in-browser upload UI that consumes this signature. +- [Signature generation reference](https://cloudinary.com/documentation/signatures.md) diff --git a/cloudinary/docs/transform-and-deliver-image.md b/cloudinary/docs/transform-and-deliver-image.md new file mode 100644 index 00000000..e807be34 --- /dev/null +++ b/cloudinary/docs/transform-and-deliver-image.md @@ -0,0 +1,148 @@ +# Transform and deliver an image + +## When to use + +Building a delivery URL for an image already in Cloudinary: resized, cropped, format- +and quality-optimized, or generatively edited. + +URL generation is **local**. No network call, no `api_secret` — only the cloud name is +required. The derived image is created by Cloudinary on first request and then served +from CDN cache. + +For video, see [Transform and deliver a video](transform-and-deliver-video.md). For +rendering URLs in Django templates, see [Use with Django](use-with-django.md). + +## Optimized image URL + +```python +import cloudinary # configuration is read from CLOUDINARY_URL + +image = cloudinary.CloudinaryImage("products/leather-bag") + +url = image.build_url( + width=400, + height=400, + crop="fill", # fill the box, cropping the overflow + gravity="auto", # let Cloudinary choose the crop centre + fetch_format="auto", # WebP or AVIF when the browser supports it + quality="auto", # per-image quality tuning + secure=True, +) +print(url) +# https://res.cloudinary.com//image/upload/c_fill,f_auto,g_auto,h_400,q_auto,w_400/v1/products/leather-bag +``` + +`fetch_format="auto"` and `quality="auto"` are the two options worth setting on almost +every image: together they typically cut bytes by half with no visible change. + +Parameters are emitted in alphabetical order, not the order you pass them — the URL above +is stable and cache-friendly regardless of keyword order. + +The `/v1/` segment appears when the `public_id` contains a slash and no version is known. +It is a placeholder that delivers correctly. Pass the `version` from the upload result to +get a cache-busting URL, or `force_version=False` to leave it out for a top-level +`public_id`. + +## The functional alternative + +`cloudinary.utils.cloudinary_url` does the same job without an object. It returns a +**tuple** of `(url, remaining_options)`, so unpack it: + +```python +import cloudinary.utils + +url, _ = cloudinary.utils.cloudinary_url( + "products/leather-bag", width=400, crop="scale", fetch_format="auto", quality="auto", secure=True +) +``` + +Ignoring the second element and using the tuple as a URL is the most common mistake here. + +## An `` tag instead of a URL + +```python +tag = cloudinary.CloudinaryImage("products/leather-bag").image( + width=300, crop="scale", fetch_format="auto", quality="auto", secure=True, alt="Leather bag" +) +# Leather bag +``` + +Transformation options that affect the URL are consumed; presentation attributes such as +`alt` are rendered onto the tag. For responsive markup, pass `srcset={"breakpoints": [...]}` +or use `.picture(sources=[...])`. + +## Chained transformations + +Pass a list to `transformation=` when one step must complete before the next — cropping +first, then applying an effect: + +```python +url = cloudinary.CloudinaryImage("products/leather-bag").build_url( + transformation=[ + {"width": 600, "height": 600, "crop": "fill", "gravity": "auto"}, + {"effect": "sharpen"}, + ], + fetch_format="auto", + quality="auto", + secure=True, +) +# .../c_fill,g_auto,h_600,w_600/e_sharpen/f_auto,q_auto/v1/products/leather-bag +``` + +## Generative and AI edits + +These are **generic strings, not typed builders**. Pass the effect name exactly as the +transformation reference spells it: + +```python +image = cloudinary.CloudinaryImage("products/leather-bag") + +image.build_url(effect="gen_restore", secure=True) # e_gen_restore +image.build_url(effect="gen_remove:prompt_hat", secure=True) # e_gen_remove:prompt_hat +image.build_url(effect="background_removal", secure=True) # e_background_removal +image.build_url(background="gen_fill", width=1200, height=630, + crop="pad", secure=True) # b_gen_fill,c_pad,... +``` + +Anything the SDK does not model can be passed verbatim with +`raw_transformation="e_vectorize:colors:8"`. Generative add-ons are billed and some +require enabling on your account; the first request is slow while the derived asset is +produced. + +## Delivering a private or authenticated asset + +```python +url = cloudinary.CloudinaryImage("contracts/invoice").build_url( + type="authenticated", sign_url=True, secure=True +) +# .../image/authenticated/s----/v1/contracts/invoice +``` + +Signing needs the `api_secret`, so it belongs on your server. + +## Troubleshooting + +- The URL 404s — the `public_id` is wrong, or the asset is a different `resource_type` + (`video`, `raw`) or `type` (`private`, `authenticated`). Confirm with + `cloudinary.api.resource(public_id)`. +- `x-cld-error: ACL deny` — an unclaimed Claimable Cloud delivering to a + non-allow-listed address. See [Get Cloudinary credentials](get-credentials.md). +- The URL contains a literal `('https://...', {})` — you used `cloudinary_url` without + unpacking the tuple. +- A transformation silently disappears from the URL — the parameter is not one the SDK + recognizes. Check the spelling against the + [transformation reference](https://cloudinary.com/documentation/transformation_reference.md), + or use `raw_transformation`. +- `Invalid transformation component` on first request — the effect name or its argument + is not valid; Cloudinary reports this at delivery time, not when building the URL. +- The image is stale after re-uploading to the same `public_id` — CDN cache. Upload with + `invalidate=True`, or include `version=result["version"]` in the URL. + +## Related + +- Runnable example: `examples/transform-and-deliver-image.py` +- [Transform and deliver a video](transform-and-deliver-video.md) +- [Upload an image](upload-image.md) +- [Transformation reference](https://cloudinary.com/documentation/transformation_reference.md) + — every parameter and its accepted values. +- [Image optimization](https://cloudinary.com/documentation/image_optimization.md) diff --git a/cloudinary/docs/transform-and-deliver-video.md b/cloudinary/docs/transform-and-deliver-video.md new file mode 100644 index 00000000..5bbb77f4 --- /dev/null +++ b/cloudinary/docs/transform-and-deliver-video.md @@ -0,0 +1,128 @@ +# Transform and deliver a video + +## When to use + +Building delivery URLs, player markup, poster frames, or adaptive streaming manifests for +a video already in Cloudinary. + +Video needs `resource_type="video"` — that is the single most common source of 404s when +reusing image code. URL generation is local: no network call, no `api_secret`. + +For images, see [Transform and deliver an image](transform-and-deliver-image.md). To get +a video in, see [Upload a large video](upload-large-video.md). + +## Video URL only + +`CloudinaryVideo` sets the resource type for you: + +```python +import cloudinary # configuration is read from CLOUDINARY_URL + +video = cloudinary.CloudinaryVideo("marketing/product-tour") + +url = video.build_url( + width=1280, + crop="scale", + quality="auto", + secure=True, +) +print(url) +# https://res.cloudinary.com//video/upload/c_scale,q_auto,w_1280/v1/marketing/product-tour +``` + +With `CloudinaryImage` or `cloudinary_url`, pass `resource_type="video"` yourself. + +## Player markup + +```python +tag = cloudinary.CloudinaryVideo("marketing/product-tour").video( + width=640, controls=True, secure=True +) +``` + +This renders a `