Skip to content

Add support for file tags and file search - #319

Closed
loookashow wants to merge 4 commits into
mainfrom
feat/add-search-and-tags
Closed

Add support for file tags and file search#319
loookashow wants to merge 4 commits into
mainfrom
feat/add-search-and-tags

Conversation

@loookashow

Copy link
Copy Markdown
Contributor

Exposes two File Platform capabilities added in REST API v0.7 in the SDK: file tags and file search. Both require the Accept: application/vnd.uploadcare-v0.7+json header, which the SDK already sends (conf.api_version is 0.7), so no header work was needed.

Bumps the version to 6.3.0 (two additive features, no breaking changes).

File tags

  • TagsAPI (uploadcare.tags_api) mirroring MetadataAPI: get() / replace() / update(), covering GET / PUT / PATCH on /files/{uuid}/tags/.
  • On File: a tags property plus get_tags(), set_tags(), update_tags().
  • tags argument on Uploadcare.upload(), upload_files() and multipart_upload(), sent as the comma-separated tags form field.
  • tags on FileInfo.
  • New ucare commands get_file_tags, set_file_tags, update_file_tags, and --tags on ucare upload.
  • TagValidationError for tags exceeding the API limits (50 per file, 100 chars each, Latin letters/digits/-/_/.).

Notable decisions:

  • Tags are normalized (lowercase, trim, dedupe first-wins) before validation, so values the server would accept after its own normalization are not rejected locally.
  • Uploads from url do not support tags upstream, so passing them raises InvalidParamError rather than silently dropping them.
  • FileInfo.model_dump() — and therefore File.info — now always contains a tags key (None when the endpoint does not report tags, [] for a file without tags). Called out in HISTORY.

File search

  • Uploadcare.search_files(), returning one typed page, and Uploadcare.iterate_search_files(), which walks pages (limit = total to yield, request_limit = page size, as elsewhere in the SDK).
  • FilesAPI.search() for POST /files/search/.
  • Typed requests via FileSearchRequest (query, phrase, exact, datetime_uploaded, size, is_image, tags + fuzziness / sort), also accepting a plain dict. All documented API constraints are validated locally before a request is made.
  • FileSearchResponse (next, previous, total, per_page, results), each result a FileSearchInfo with a SearchHighlight.
  • New ucare search_files command.

Notable decisions:

  • Pagination never requests the response's absolute next URL — the REST client attaches credentials to any URL it is given, so next is used only as a has-more signal and the offset is computed locally, clamped to the 1000-result window.
  • total is not used as a stop condition, since the API documents it as approximate for large result sets.
  • iterate_search_files() warns when asked to page through a filter-only request without sort, whose result order the API leaves undefined.

Testing

  • Functional tests use unittest.mock for request shape (VCR matches on method + URI only) and hand-authored cassettes for response parsing.
  • Integration tests (tests/integration/) run the whole surface against the live REST API — tags CRUD, tags on upload, search conditions/modifiers/highlight/appdata/pagination, and every new CLI command. They create and delete their own files and upload a PNG rather than a text file so they work on projects that restrict uploadable types.
  • Two documentation corrections came out of live verification: File.tags after a direct upload fetches the info and returns the stored tags (only multipart leaves it None), and highlight for a filter-only match arrives as an empty object rather than being absent as the reference states. Search indexing is asynchronous (~10s), which the integration tests poll for and the docs now note.

make lint is clean; functional + Django suites pass; the integration suite passes against a live project.

🤖 Generated with Claude Code

Expose the REST API v0.7 file tags endpoints in the SDK.

- TagsAPI (uploadcare.tags_api) with get/replace/update, covering GET, PUT
  and PATCH on /files/{uuid}/tags/
- File: tags property plus get_tags(), set_tags() and update_tags()
- tags argument for Uploadcare.upload(), upload_files() and
  multipart_upload(), sent as the comma-separated tags form field
- tags in FileInfo
- ucare get_file_tags, set_file_tags and update_file_tags commands, and a
  --tags option for ucare upload
- TagValidationError for tags exceeding the API limits

Tags are normalized before validation, the way the API normalizes them, so
values it would accept after its own normalization are not rejected locally.

update() with no arguments sends an empty body, since the endpoint documents
both fields as optional; requiring at least one of --add/--delete is a CLI
concern instead. The 50-tags-per-file limit does not apply to PATCH delete,
which is a candidate list where absent tags are ignored.

Uploads from url do not support tags upstream, so passing them raises
InvalidParamError rather than dropping them silently.

FileInfo.model_dump(), and therefore File.info, now always contains a tags
key: None for responses that do not report tags, [] for files without any.
Expose the REST API v0.7 POST /files/search/ endpoint in the SDK.

- Uploadcare.search_files(), returning one typed page, and
  Uploadcare.iterate_search_files(), which walks pages
- FilesAPI.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 works
  too
- FileSearchResponse with next, previous, total, per_page and results, each
  result a FileSearchInfo carrying a SearchHighlight
- ucare search_files command

Requests are validated locally against the documented API constraints before
any request is made, so a malformed search fails fast with a clear message.

Paging never requests the response's next URL: it is an absolute,
server-supplied URL, and the REST client attaches credentials to whatever URL
it is given. next is used only as a has-more signal and the offset is computed
locally, clamped so that no request exceeds the 1000-result window. total is
not used as a stop condition because the API documents it as approximate for
large result sets.

iterate_search_files() warns when asked to page through a filter-only request
without sort, whose result order the API leaves undefined.
Verified against the live API: after a direct upload nothing is cached, so
reading File.tags fetches the file info and returns the stored tags. The
None case applies to multipart uploads, where the upload response is cached
as the file info and carries no tags key.
Pin both paths with tests.
Cover both features against the live REST API: tags CRUD, tags on upload,
tags in file info and listings, search conditions, modifiers, highlight,
appdata and pagination, plus the new CLI commands.

Tests create and delete their own files, and upload a PNG rather than a text
file so they work on projects that restrict uploadable types.

Search indexing is asynchronous, taking on the order of ten seconds, so
looking for a freshly uploaded file polls through wait_until_searchable while
the rest search over content the project already holds. Document the delay.

Correct the highlight documentation: the API reference says the field is
absent for filter-only matches, but the live API sends an empty object.
@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@dmitry-mukhin

Copy link
Copy Markdown
Member
+4,444
-9

😿

@loookashow

Copy link
Copy Markdown
Contributor Author

@dmitry-mukhin split into two reviewable PRs as requested:

Search's TagsFilter reuses validate_tags from the tags module, so it can't stand alone off main — hence the stack: review/merge #320 first, then #321 retargets to main automatically. Same commits and same final tree as here; the tags reconstruction is byte-identical, so #321's diff is search-only.

Closing this one in favour of those two.

🤖 Addressed by Claude Code

@loookashow loookashow closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants