Skip to content

Python 0.1 to 0.2

This guide accompanies the native SDK. Capability, update migration and package validation evidence is recorded in the implementation log. Implementation and verification do not publish a package or release tag.

Python 0.2 owns Rust Project, MediaRef and Artifact objects in a private native extension. Identity, validation, media registration and writing use the Rust implementation. Python 0.1 remains the compatibility line for the bundled CLI, RuntimeOverride, ProductDocument export and modification after adding an input.

Finish editing a Note or NoteType before passing it to add_note or add_notetype. Later edits to that Python input do not affect the Project. project.notes and project.notetypes return detached observations; changing an observed object does not change the project. Project construction settings are read-only. To produce an update, construct the desired Project with the same stable project and note identities.

Duplicate IDs, unknown fields, missing custom identity recipes and invalid templates are rejected at addition with ProjectAddError. The exception keeps the core code, diagnostic, path and span. A rejected addition leaves the Project usable. NoteType.validate() also delegates to this core add boundary.

project.validate() returns a ValidationReport with diagnostics, has_errors and ensure_success(). It performs aggregate authoring validation; normalization, media availability, writer checks and update safety still run at build time. It does not create an APKG.

The default key is now derived by the corresponding Rust Field or Template constructor. Existing punctuation and repeated whitespace can produce different keys than Python 0.1:

Name Kind Python 0.1 Python 0.2
Back Extra Field back_extra back__extra
C++ Card Template c_card c++_card

For an existing deck, retain the old keys, including template keys:

Field("Back Extra", key="back_extra")
Template("C++ Card", key="c_card", front="{{Prompt}}", back="{{Back Extra}}")

Do not derive replacement keys from new display names during migration. Keep the existing APKG and identity lockfile for update verification. The test fixtures in tests/fixtures/python01 were generated by the actual 0.1 implementation. The unchanged migration keeps GUIDs, model/template IDs and note revision evidence. Two changes are explicitly checked: preserved GUID metadata records previous_apkg, and the core persists inferred all generation requirements for the two custom templates where the old ProductDocument path omitted them. This is an expected serialized template change, so migration does not imply that every observed field or the entire archive is byte-identical.

Field(identity=True) continues to select the default custom identity fields. An explicit NoteType.identity(IdentityRecipe.fields([...])) takes precedence; note.identity([...]) supplies the per-note override. An explicit stable ID uses the core’s identity precedence. Python does not compute identity hashes.

Custom note fields accept exact declared keys or display names, including Unicode and whitespace. These references are not trimmed; semantic validation and default-key derivation use Rust. Stock convenience keys such as front and back_extra remain accepted, as do exact Rust field names such as Front and Back Extra. Template and CSS source whitespace is preserved.

Setters canonicalize stock aliases so the latest edit is retained. If direct edits to the public fields dictionary introduce both aliases for one stock field, add_note rejects that ambiguous input; a setter can resolve it. Detached stock observations and the IO builder consistently expose Python’s lower-case keys such as front, back_extra and occlusion.

Field(optional=True) preserves the core declaration and readback. Setting both required=True and optional=True is rejected. Optional does not introduce a new card-generation rule beyond the core’s existing behavior.

add_file reads and fingerprints the source immediately. Missing, empty and non-regular files raise MediaError; changing or deleting a registered source is detected again during build. Keep sources available until the build finishes.

add_bytes accepts bytes or bytearray, takes a snapshot and applies the core’s 64 KiB limit. Use files for larger payloads. MediaRegistry.inline_limit_bytes() exposes the core limit. Empty payloads are rejected.

Equal export filenames produce equal references. Registering the same name and content again retains the first registration’s source evidence; a different payload for that name raises MEDIA.DUPLICATE_FILENAME_CONFLICT. A reference from another Project is resolved by its export filename in the destination Project, which must register that name. Equality does not imply shared content or source ownership across projects.

Relative media paths use the Project’s base_dir, captured at construction. Changing the process working directory later does not redirect registrations.

Content.text/html, note.field, and MediaRef.image/sound use core rendering. The image occlusion builder now returns core ProductNoteError codes for missing stable IDs, empty masks, empty rectangles and duplicate rectangles. Rectangle coordinates and sizes must be integers within the Rust u32 range.

The Project image occlusion builder requires an explicit stable ID. The separate Deck facade now provides automatic identity and image-bound checks through Rust. The shared core currently rejects grouped cloze markup generated by hide_one_guess_one; the binding preserves this failure rather than changing the renderer.

project.build(BuildOptions(...)) supports temporary output by default. write_apkg(path, ...) retains the compare/risk/lockfile keyword arguments and also accepts options=BuildOptions(...). Conflicting duplicate options raise ValidationError before publication.

InspectLimits exposes all 11 budgets with defaults from Rust. Values must be Python integers in the u64 range; bool, float, negative and overflowing values are rejected. inspect=False suppresses the summary but retains final package checks. Both current and baseline packages use the configured inspection budgets.

BuildReport retains complete metrics, policy, raw JSON and version information. Valid failed builds return their report. ensure_success() raises BuildError, a DiagnosticsError subclass holding that report, code and failure_cause. The legacy report_only and report-only spellings are both accepted.

An older lockfile missing revision evidence cannot safely update by itself in strict mode. Supply its original APKG via compare_to to recover that evidence. The migration tests cover unchanged, answer/tag changes, repeated builds and reverting content: GUIDs stay fixed and only changed note content advances mtime.

Retain an Artifact object while using its temporary file, or call artifact.persist_to(path) to make a persistent atomic copy. Relative persistence paths use the originating Project’s base directory. Failure leaves the original Artifact usable. A temporary Artifact cannot be persisted onto itself or a file alias; persisted and explicit output files survive close.

copy.copy(artifact) and copy.deepcopy(artifact) create independent owning handles. Ordinary assignment shares a handle, so closing it affects its aliases. Copying a BuildReport copies its Artifact handle. report.close() releases only that report’s reference; a separately retained Artifact still holds its file. Reports and artifacts both support with blocks. A failed build exception keeps its report and any recoverable Artifact alive. Serialized JSON contains only paths and does not acquire ownership.

project.to_apkg_bytes() returns the complete package in memory. project.write_to(binary_sink) returns the number of bytes written and copies the completed artifact in chunks of at most 64 KiB. It supports non-seekable sinks and short writes, raises on no progress or invalid write counts, and propagates the caller’s write exception. It cleans its temporary Artifact and keeps the sink open. This is bounded copying after a build, not simultaneous package generation and streaming.

anki_forge.project.Project and anki_forge.media expose the same native classes as root imports. RuntimeOverride, to_product_document, the public CLI runtime module and its staging script belong to 0.1; they are not bundled or silently emulated in 0.2. Keep 0.1 pinned for those workflows. The repository’s dev-only anki_forge_python wrapper is unchanged and excluded from the wheel.

Create MediaRef values through registration instead of constructing local sequence IDs. media_id now identifies the export filename. The old MediaRegistry.items / MediaItem serializer view (including duplicated bytes and Python SHA-256 fields) is retired. Retain caller input if you need those bytes; use BuildReport.media for core-observed entries, source modes, sizes and counts. NoteType construction still defaults its display name to the supplied type ID. Field/template source names and CSS preserve whitespace; old implicit keys need explicit migration as described above.

Deck exposes stock authoring with its own core identity rules. Basic/Cloze text is HTML, and Deck media uses basename/file or named bytes registration. This is a distinct API from Project’s typed Content and bounded inline registration. BasicIdentityOverride supplies both selected fields and an audit reason. IO can omit stable_id and uses registered image dimensions for bounds checks. Project.from_deck(deck) snapshots through Rust conversion, retaining the Deck and preserving imported identities/media when custom Project notes are appended.

import_template_bundle is atomic and preserves template source locations. diff_against_apkg returns a lossless ProjectDiffReport, builds an internal temporary candidate, and neither publishes APKGs nor writes identity lockfiles.

Native installation and process boundaries

Section titled “Native installation and process boundaries”

The wheel includes py.typed, _native.pyi, licenses and dependency notices. Build helpers return BuildReport[ApkgArtifact]; BuildReport.from_json returns BuildReport[Mapping[str, Any]], so type checkers do not invent ownership for serialized paths. versions() reports binding/core/contract versions. Missing or mismatched native files fail at import with an actionable BINDING.* message.

One operation per Project/Deck is permitted; independent objects run while Rust releases the GIL. Forked Project/Deck/Artifact handles reject reuse. Child-process Artifact destruction does not delete the parent’s temporary files; create new objects in child processes. Ordinary CPython 3.11/3.12 is the tested scope; free-threaded interpreters, subinterpreters and cancellation are not claimed.

The current core includes selected field display names in custom derived identity payloads. Reordering fields while retaining keys/names preserves the GUID; renaming an identity field can derive a new GUID even if its key and value are unchanged. Python and Rust have the same behavior. Explicit stable IDs remain stable across those display-name changes. For an existing derived-ID deck, keep its identity field names unchanged or plan an explicit identity migration with baseline evidence and inspect the diff/risk report. Merely retaining old keys is sufficient for the punctuation-key migration fixture above, not every schema edit.