Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,44 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.3.0](https://github.com/uploadcare/pyuploadcare/compare/v6.2.1...v6.3.0) - 2026-08-04

### Added

- Support for [file tags](https://uploadcare.com/docs/file-tags/):
- `TagsAPI` (`uploadcare.tags_api`) with `get()`, `replace()` and `update()` methods, covering
`GET`, `PUT` and `PATCH` on `/files/{uuid}/tags/`.
- For `File`: a `tags` property plus `get_tags()`, `set_tags()` and `update_tags()` methods.
- A `tags` argument for `Uploadcare.upload()`, `Uploadcare.upload_files()` and
`Uploadcare.multipart_upload()`. Uploads from url do not support tags and raise
`InvalidParamError` instead of silently dropping them.
- `tags` in `FileInfo`.
- New `ucare` commands `get_file_tags`, `set_file_tags` and `update_file_tags`, and a `--tags`
option for `ucare upload`.
- `TagValidationError`, raised when tags exceed the API limits (50 tags per file, 100 characters
per tag, Latin letters, digits, `-`, `_` and `.` only).
- Support for [file search](https://uploadcare.com/docs/file-search/):
- `Uploadcare.search_files()`, returning one page of results, and
`Uploadcare.iterate_search_files()`, which walks pages. As with the other list APIs, `limit`
is the total number of results to yield and `request_limit` is the page size.
- `FilesAPI.search()` for `POST /files/search/`.
- Typed requests: `FileSearchRequest` with `query`, `phrase`, `exact`, `datetime_uploaded`,
`size`, `is_image` and `tags` conditions plus the `fuzziness` and `sort` modifiers, built from
`SearchPhrase`, `SearchExact`, `DatetimeRange`, `SizeRange`, `TagsFilter` and `SearchSort`.
A plain dict in the same shape is accepted too. Requests are validated locally against the
documented API constraints before a request is made.
- `FileSearchResponse` with `next`, `previous`, `total`, `per_page` and `results`, where each
result is a `FileSearchInfo` — file info plus a `SearchHighlight`.
- `iterate_search_files()` warns when asked to page through a filter-only request without
`sort`, whose result order the API leaves undefined.
- A new `ucare search_files` command.

### Changed

- `FileInfo.model_dump()`, and therefore `File.info`, now always contains a `tags` key. It is
`None` for responses that do not report tags, such as upload responses, and `[]` for files
without tags.

## [6.2.1](https://github.com/uploadcare/pyuploadcare/compare/v6.2.0...v6.2.1) - 2025-09-02

### Added
Expand Down
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Build file handling in minutes. Upload or accept user-generated content, store,
- [Requirements](#requirements)
- [Usage](#usage)
- [Basic usage](#basic-usage)
- [File tags](#file-tags)
- [File search](#file-search)
- [Django integration](#django-integration)
- [Testing](#testing)
- [Demo app](#demo-app)
Expand Down Expand Up @@ -163,6 +165,95 @@ print(ucare_file.cdn_url) # https://demo.ucarecd.net/640fe4b7-7352-42ca-8d87-0e

There’s a lot more to uncover. For more information please refer to the [documentation](#documentation).

#### File tags

Files can carry a list of [tags](https://uploadcare.com/docs/file-tags/) you can later filter on:

```python
file_ = uploadcare.file("640fe4b7-7352-42ca-8d87-0e4387957157")

file_.get_tags() # ['cat', 'animal']
file_.set_tags(["cat", "animal", "cute"]) # replace all tags
file_.update_tags(add=["pet"], delete=["animal"]) # add and delete atomically
file_.set_tags([]) # clear all tags
```

`set_tags()` and `update_tags()` return the resulting tag list along with what changed:

```python
response = file_.update_tags(add=["pet"], delete=["animal"])
print(response.tags, response.added, response.deleted)
# ['cat', 'cute', 'pet'] ['pet'] ['animal']
```

Tags can also be attached at upload time:

```python
with open("sample-file.jpeg", "rb") as file_object:
ucare_file = uploadcare.upload(file_object, tags=["cat", "cute"])
```

Tags are lowercased, trimmed and deduplicated. A file can hold up to 50 tags of up to 100
characters each, made of Latin letters, digits, `-`, `_` and `.`.

#### File search

A single [search](https://uploadcare.com/docs/file-search/) request can combine full-text search,
exact matching, range filters and tag filters. At least one condition is required:

```python
from pyuploadcare import FileSearchRequest, SizeRange, TagsFilter

response = uploadcare.search_files(
FileSearchRequest(
query="sunset",
tags=TagsFilter(all_=["cat"], none_=["draft"]),
size=SizeRange(gt=1024),
is_image=True,
fuzziness=True,
sort=["-score", "size"],
),
limit=50,
)

print(response.total, response.per_page)

for file_info in response.results:
print(file_info.original_filename, file_info.tags)
if file_info.highlight:
print(file_info.highlight.original_filename) # ['<em>sunset</em>.jpg']
```

Requests can also be plain dicts:

```python
response = uploadcare.search_files({"tags": {"all": ["cat"]}})
```

`search_files()` returns a single page: `limit` is the page size (1–100, defaults to 20) and
`offset + limit` must not exceed 1000.

To walk pages, use `iterate_search_files()`. There, following the SDK's other list APIs, `limit` is
the total number of results to yield and `request_limit` is the page size; the iterator stops on
its own once it reaches the 1000-result window:

```python
request = {"tags": {"all": ["cat"]}, "sort": ["-datetime_uploaded"]}

for file_info in uploadcare.iterate_search_files(request, limit=200):
print(file_info.uuid)
```

Always pass an explicit `sort` when paging through a filter-only request (one without `query` or
`phrase`): there is no relevance to rank by, so the order is undefined and paging can skip or
repeat files. The SDK emits a `UserWarning` if you don't.

Either way, search reaches the first 1000 results only. Narrow the query rather than paging
deeper.

> `highlight` values contain your users' filenames and metadata wrapped in `<em>` tags by the
> server. Treat them as untrusted text and escape them before rendering as HTML.

### Django integration

Let's add [File Uploader](https://uploadcare.com/docs/file-uploader/) to an existing Django project.
Expand Down
188 changes: 188 additions & 0 deletions docs/core_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,194 @@ But you should better use a special attribute of `File.info`::
file_metadata = file.info["metadata"]


File tags
---------

Each file can carry a list of tags (`file tags documentation`_), which you can later use as a
filter when searching files.

Tags may be initially set while uploading::

with open('file.txt', 'rb') as file_object:
ucare_file: File = uploadcare.upload(file_object, tags=['cat', 'cute'])

While uploading multiple files at once, the tags are applied to every file in the collection::

file1 = open('file1.txt', 'rb')
file2 = open('file2.txt', 'rb')
ucare_files: List[File] = uploadcare.upload_files([file1, file2], tags=['cat'])
# don't forget to close the files, of course

Uploads from url do not support tags. Use ``File.set_tags()`` after such an upload instead.

Read the current tags of a file::

file = uploadcare.file('740e1b8c-1ad8-4324-b7ec-112c79d8eac2')
tags: List[str] = file.get_tags()

Or, if the file info is already loaded, without an extra request::

tags = file.tags

``File.tags`` reads from the cached file info, fetching it first if nothing is cached yet. It is
``[]`` when the file has no tags, and ``None`` only when the cached info came from a response that
does not report tags at all. In practice that happens after a multipart upload, whose response is
cached as the file info and carries no ``tags`` — call ``File.update_info()`` or
``File.get_tags()`` there. A direct upload caches nothing, so reading ``File.tags`` after it
fetches the info and returns the stored tags.

Replace all tags of a file. Passing an empty list clears them::

response = file.set_tags(['cat', 'animal', 'cute'])
print(response.tags, response.added, response.deleted)

file.set_tags([])

Add and/or delete tags atomically::

response = file.update_tags(add=['pet'], delete=['animal'])

Both methods return a response carrying the resulting ``tags`` plus the ``added`` and ``deleted``
tags. The same operations are available on the API directly::

uploadcare.tags_api.get(file_id)
uploadcare.tags_api.replace(file_id, ['cat', 'animal'])
uploadcare.tags_api.update(file_id, add=['pet'], delete=['animal'])

Tags are normalized before being sent: they are lowercased, trimmed, deduplicated keeping the
first occurrence, and empty ones are dropped. A file can hold up to 50 tags of up to 100
characters each, containing Latin letters, digits, ``-``, ``_`` and ``.`` only. Anything else
raises ``TagValidationError`` before a request is made.

Tags are available from the CLI as well::

ucare get_file_tags 740e1b8c-1ad8-4324-b7ec-112c79d8eac2
ucare set_file_tags 740e1b8c-1ad8-4324-b7ec-112c79d8eac2 cat animal
ucare update_file_tags 740e1b8c-1ad8-4324-b7ec-112c79d8eac2 --add pet --delete animal
ucare upload file.txt --tags cat cute

.. _file tags documentation: https://uploadcare.com/docs/file-tags/


Searching files
---------------

A single search request (`file search documentation`_) can combine full-text search, exact
matching, range filters and tag filters. At least one condition is required: ``query``,
``phrase``, ``exact``, ``datetime_uploaded``, ``size``, ``is_image`` or ``tags``. ``fuzziness``
and ``sort`` are modifiers and do not count as conditions::

from pyuploadcare import (
DatetimeRange,
FileSearchRequest,
SearchExact,
SearchPhrase,
SizeRange,
TagsFilter,
)

response = uploadcare.search_files(
FileSearchRequest(
query='sunset',
tags=TagsFilter(all_=['cat'], none_=['draft']),
size=SizeRange(gt=1024, lte=10 * 1024 * 1024),
is_image=True,
fuzziness=True,
sort=['-score', 'size'],
),
limit=50,
)

print(response.total, response.per_page, response.next)

for file_info in response.results:
print(file_info.original_filename, file_info.tags)

``query`` searches across all searchable fields and must be at least 4 characters. ``phrase``
performs an ordered full-text match on a specific field, and ``exact`` matches values exactly::

request = FileSearchRequest(
phrase=SearchPhrase(original_filename='holiday photo'),
exact=SearchExact(detected_mime_type=['image/jpeg', 'image/png']),
datetime_uploaded=DatetimeRange(gte=datetime(2024, 1, 1)),
)

A field cannot appear in both ``phrase`` and ``exact`` in the same request.

``exact`` can also match metadata values. In the SDK this is a nested mapping, which is
serialized into the ``metadata[<key>]`` keys the API expects::

SearchExact(metadata={'album': ['holiday'], 'color': ['red']})

A request can equally be a plain dict, using the same shape::

response = uploadcare.search_files({
'exact': {'metadata': {'album': ['holiday']}},
'tags': {'all': ['cat']},
})

``sort`` accepts 1 to 4 keys out of ``score``, ``datetime_uploaded``, ``size`` and
``original_filename``, each optionally prefixed with ``-`` for descending order. Keys must be
unique and the same key must not be given in both directions.

Without ``sort``, results are ordered by relevance. A filter-only request has no ``query`` or
``phrase`` to rank by, so its order is undefined — always give such a request an explicit
``sort``.

Search indexing is asynchronous, so a file is not findable the instant it is uploaded — expect a
delay on the order of seconds. Do not search for a file you have just uploaded without retrying.

``search_files`` returns a single page. Its ``limit`` is the page size, 1 to 100, and the server
defaults to 20; ``offset + limit`` must not exceed 1000. Search cannot reach past the first 1000
results, so narrow the query instead of paging deeper.

To walk pages, use ``iterate_search_files``. Its ``limit`` means something different: as elsewhere
in the SDK, ``limit`` is the total number of results to yield and ``request_limit`` is the number
retrieved per request. The iterator applies the 1000-result window itself, clamping each page so
that no request exceeds it::

for file_info in uploadcare.iterate_search_files(
{'tags': {'all': ['cat']}, 'sort': ['-datetime_uploaded']}, limit=200
):
print(file_info.uuid)

.. warning::

Paging through a filter-only request without ``sort`` is unreliable: the order is undefined
between requests, so files may be skipped or repeated. ``iterate_search_files`` emits a
``UserWarning`` in that case.

Pass ``include_appdata=True`` to embed application data in every result::

response = uploadcare.search_files(request, include_appdata=True)
print(response.results[0].appdata)

Each result carries a ``highlight`` with the matched tokens wrapped in ``<em>`` tags. A field in it
is populated only when that field matched a full-text condition, so a filter-only search
highlights nothing — expect either no ``highlight`` at all or one whose every field is ``None``::

highlight = response.results[0].highlight
if highlight:
print(highlight.original_filename) # ['<em>sunset</em>-cat.jpg']
print(highlight.metadata) # {'album': 'summer <em>sunset</em>'}

.. warning::

``highlight`` values contain user-controlled content (filenames, metadata) plus markup added
by the server. They are not trusted HTML — escape them before rendering.

Search is available from the CLI as well. Because a descending sort key starts with a dash, pass
it with ``--sort=`` so it is not read as another option::

ucare search_files --query sunset --tags_any cat dog --is_image true \
--size_gt 1024 --sort=-score --limit 50

The CLI covers every condition except ``exact.metadata``, whose bracketed keys do not map onto a
command line option; use the Python API for that.

.. _file search documentation: https://uploadcare.com/docs/file-search/


Video conversion
----------------

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyuploadcare"
version = "6.2.1"
version = "6.3.0"
description = "Python library for Uploadcare.com"
authors = ["Uploadcare Inc <hello@uploadcare.com>"]
readme = "README.md"
Expand Down
11 changes: 10 additions & 1 deletion pyuploadcare/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
# isort: skip_file
__version__ = "6.2.1"
__version__ = "6.3.0"

from pyuploadcare.resources.file import File # noqa: F401
from pyuploadcare.resources.file_group import FileGroup # noqa: F401
from pyuploadcare.resources.file_list import FileList # noqa: F401
from pyuploadcare.resources.group_list import GroupList # noqa: F401
from pyuploadcare.api.entities import Webhook, ProjectInfo # noqa: F401
from pyuploadcare.api.search_entities import ( # noqa: F401
DatetimeRange,
FileSearchRequest,
SearchExact,
SearchPhrase,
SearchSort,
SizeRange,
TagsFilter,
)
from pyuploadcare.client import Uploadcare # noqa: F401
1 change: 1 addition & 0 deletions pyuploadcare/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
GroupsAPI,
MetadataAPI,
ProjectAPI,
TagsAPI,
UploadAPI,
VideoConvertAPI,
WebhooksAPI,
Expand Down
Loading
Loading