Skip to content

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

The repository CHANGELOG.md is the full Keep a Changelog record (including 0.0.x). This page is the user-facing summary.

Unreleased

0.6.0 - 2026-08-26

Opt-in write-404: same-params hit is write_rejected, not AuthorizationError. Custom-route action classify. Trackers: #166 #171.

Upgrade notes

  • Opt-in classify_write_404: a same-params GET hit is now NotFoundError (classified_from=write_rejected), not AuthorizationError. Remaining-draft delete / publish is still AuthorizationError (classified_from=write_404). Catch classified_from, not the 0.5.0 auth type.
  • Custom StrapiClient implementors must add publish / unpublish / discard_draft (with classify_write_404).

Added

  • Opt-in classify_write_404 on custom-route unpublish() / discard_draft() (sync + async). Probes the document path (write addressing params, then draft), never /actions/*. Default False still surfaces the stock 404/405. A document miss keeps the original missing-route 404. Draft-only is NotFoundError / classified_from=draft_only (nothing published to unpublish or restore via discard). A readable published variant is NotFoundError / classified_from=write_rejected (refused write, no permission copy). Helpers remain custom-route only (#166).
  • publish / unpublish / discard_draft (with classify_write_404) on the StrapiClient protocol.

Fixed

  • Opt-in classify_write_404 no longer raises AuthorizationError when the same-params GET hits. That arm is NotFoundError with classified_from=write_rejected and copy that names no permission cause ("{operation} was refused"). A remaining-draft delete / publish is still AuthorizationError / write_404 (#171).

0.5.0 - 2026-08-26

Locale-aware existence checks and write-404 classification. Trackers:

161 #163.

exists_in_locale(collection, document_id, locale=None) on the sync and async clients (and StrapiClient) is the same published-then-draft probe as exists(), plus optional locale. Non-i18n types retry without locale on Invalid key locale. exists() stays document-level.

Opt-in classify_write_404 no longer treats every readable draft as a missing token. The probe uses the write's addressing params, then the draft variant. Draft-only updates stay NotFoundError (classified_from=draft_only). A publish() 404 with only a draft readable is AuthorizationError — stock PUT ?status=published publishes drafts, so a remaining draft means the write did not run. A DELETE 404 while the document is still readable stays AuthorizationError. remove accepts optional query for locale-scoped deletes. AuthorizationError copy names the operation (Update / Delete / Publish).

Upgrade notes

  • exists() stays document-level. Use exists_in_locale(..., locale=) for a locale-aware probe.
  • Opt-in classify_write_404: draft-only update 404s stay NotFoundError; publish 404 with a remaining draft is AuthorizationError.

0.4.0 - 2026-08-19

i18n localizations, nested component/dynamic-zone relations, dest media writes, FAIL-write missing locales, dry-run / JSONL preflight, and Docker e2e CI. Tracker: #144.

Import restores i18n localizations of a shared documentId (first locale creates; later locales PUT {destDoc}?locale=). SKIP is per-locale. FAIL writes missing locales, does not overwrite existing locales or their outbound relations, then raises after the full write pass. exists() no longer treats an unrelated draft 400 as “does not exist.” Import writes nested component/dynamic-zone relations, posts dest media ids (media_write()), and falls back to numeric relation PUTs on a v4 destination. The e2e Docker fixture includes an i18n localized-articles type. Export metadata includes walked component schemas. Media remapping follows FieldType.MEDIA when a schema is present. FAIL dry-run probes locale conflicts and raises without writing. Dry-run no longer maps missing dests to id 0 or the source documentId; existing dests still map real dest ids. Dry-run reports unresolved dest relations as warnings (relations_unresolved) and counts entities_to_publish. success on dry-run is write-safety, not “relations would apply.” Incomplete dest-relation fields are not written. Live entities_to_publish increments only when a dest documentId is queued. JSONL export persists total_entities / total_media on the metadata line (None is unknown, 0 is empty; import still recounts when those fields are None or 0). Finished JSON / in-memory export also snapshots both counts (total_media == 0 when there is no media). Relation IDs keep numeric-looking documentIds as strings. RelationId is a Pydantic input type, not an isinstance target; extract rejects bool. JSONL import shares preflight validation with import_data and does not pre-create empty mapping dicts. Component extract/strip unwraps v4 {data} / {data, meta} wrappers and logs unexpected payload shapes. The e2e workflow also runs on library path changes and keeps the compose stack until logs are collected. The compose probe retries docker compose and does not fall back to a missing docker-compose v1 binary.

Upgrade notes

  • Extra locales of a shared documentId are restored (PUT {destDoc}?locale=).
  • SKIP is per-locale. FAIL writes missing locales then raises.
  • Dry-run success is write-safety; check relations_unresolved.
  • JSONL totals: None is unknown, 0 is empty.
  • Relation IDs are StrictInt | StrictStr; "5" stays a string.

0.3.0 - 2026-08-16

v5 export/import populate contract. Trackers: #96 #97 #98 #99 #100 #101.

Upgrade notes

  • Export/import require pluralName from the content-type schema (no UID path invention).
  • Import writes relations with relation_write() (v5 documentId strings).
  • ExportedEntity stores published_at / locale; import publishes live source documents after relation writes.
  • Extra locales of the same documentId are not restored as i18n localizations (follow-up).
  • Short stream pages before total raise instead of stopping early.
  • upload_file / upload_files re-raise auth / not-found / server errors.

0.2.0 - 2026-08-16

Strapi 5 connector surface: Draft & Publish, Content-Type Builder discovery, origin-path probe, relation writes, blocks ↔ markdown, and complete stream/export. Tracker: #55.

Upgrade notes

  • stream_entities / StrapiExporter default to document_status=DocumentStatus.DRAFT (v5 status=draft, confirmed v4 publicationState=preview). Pass document_status=None for published-only (the previous implicit default).
  • publish() is stock REST PUT ?status=published. unpublish() and discard_draft() still need custom /actions/* routes (not stock REST).
  • Non-JSON 2xx responses raise UnstructuredResponseError (not FormatError).
  • get_components() and get_content_types() raise on unparsable items unless skip_unparsable=True.
  • StrapiQuery.filter() / .populate() fail immediately on the wrong type.
  • Import existence checks are draft-inclusive (published GET, then status=draft). Auth / 5xx / network errors on the probe are no longer treated as “does not exist”.
  • Export/import still extracts relations and media from the v4 {data: ...} populate shape. Flat Strapi 5 populate=* objects are a follow-up (do not treat 0.2.0 as a complete v5 migrate path).

Added

  • Typed Blocks nodes (BlockNode, TextNode, …) (#87).
  • ContentTypeOptions with extra="allow"; Draft & Publish is stripped from options (#88).
  • Stream/export document_status (#84, #85).
  • UnstructuredResponseError.reason (#86).
  • Stock REST publish() (#65).
  • Stream/export completeness via pagination echo (#81, #67).
  • Inline markdown_to_blocks (#77).
  • Relation write helper relation_write() / RelationWriteOp (#54).
  • assert_pagination_echo() (#48).
  • is_uniqueness_violation() and ValidationError.field_errors (#53, #76).
  • Origin-path api_prefix=False and get_admin_information() (#46).
  • collection_endpoint() / document_endpoint() from pluralName only (#49).
  • First-class CTB draft_and_publish (True / False / None) (#45).
  • Blocks ↔ markdown and FieldType.BLOCKS (#51).
  • DocumentStatus, PublicationFilter, exists(), write-404 classify, publish / unpublish / discard_draft, wire enums, MethodNotAllowedError, UnstructuredResponseError.
  • Percent-encoded document_id= on typed CRUD (#50).

Changed

  • markdown_to_blocks lifts images to root siblings (#89).
  • v5 multi-page streams keep status=draft after detect; auto + v4 re-fetch page 1 with publicationState (#93).
  • get_components() raises unless skip_unparsable=True (#79).
  • CTB options lift schema-root keys (#80).
  • filter() fail-fast (#60); shared document path encoder (#82); write data object + parser wrap (#58, #59).
  • Line-prefix escaping in blocks_to_markdown (#78).
  • Every HTTP error carries status_code. Non-JSON 2xx is UnstructuredResponseError. Default CI / make test runs pytest tests/unit only.

Fixed

  • Concurrent AsyncClient writes no longer stamp the wrong HTTP status on UnstructuredResponseError.
  • join_document_path rejects whitespace-only collection names.
  • Streamers raise on empty later pages (or an empty first page with total > 0).
  • Default status=draft is dropped after a first-page 400 (Draft & Publish off).
  • Trailing /api stripped from StrapiConfig.base_url (#47).
  • Document IDs containing /, ?, #, or % are percent-encoded (#50).

0.1.0 - 2026-02-04

Fixed

  • Race conditions in async bulk operations (#30).
  • JSONL media manifest no longer consumes the entity stream.
  • v5 string relation IDs (documentId) on import.
  • JSONL import path traversal protection and two-pass streaming (#29).
  • update_media version detection and streaming downloads (#28).

Added

  • Schema-driven relation extraction and JSONL streaming export/import (#28).

0.0.6 - 2026-02-03

Added

  • Content-Type Builder API, UID conversion utilities, SEO detection.

Fixed

  • Extra STRAPI_* env vars no longer break StrapiConfig.
  • v5 Content-Type Builder list flattening.