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.
Inputs are captured when added
Section titled “Inputs are captured when added”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.
Preserve existing keys explicitly
Section titled “Preserve existing keys explicitly”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.
Media registration is eager
Section titled “Media registration is eager”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.
Typed content and image occlusion
Section titled “Typed content and image occlusion”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.
Build configuration and update evidence
Section titled “Build configuration and update evidence”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.
Artifact ownership and binary output
Section titled “Artifact ownership and binary output”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.
Public modules and retired CLI seams
Section titled “Public modules and retired CLI seams”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, bundle and independent comparison
Section titled “Deck, bundle and independent comparison”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.
Renaming identity fields
Section titled “Renaming identity fields”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.