Upgrading pyldraw3 from 1.1.0 to 1.2.0¶
pyldraw3 1.2.0 is a purely additive release. Unlike 1.1.0, it changes
no existing behavior and removes no public API — every code path that
worked against 1.1.0 keeps working unchanged. The headline is a new
session and setup API (LDrawSession / ensure_library) for
embedding pyldraw in applications, backed by structured progress
events. Alongside it ship whole-model geometry summaries, per-piece
model occurrences with transform and source context, part-file
reference introspection, and a handful of colour and catalog convenience
accessors.
This guide covers every new API in the release: what it does, why it exists, and what to build with it.
At a glance¶
New APIs
| API | What it does |
|---|---|
ensure_library(...) |
One call that downloads, generates, and indexes everything a configured library needs |
LDrawSession |
Manage one configured catalog + generated library; state(), load(), rebuild_index(), open_model() |
LDrawState / LDrawStateReason |
Classify what (if anything) is stale or missing before doing work |
LDrawPaths |
Resolved filesystem paths for one configuration |
ProgressEvent / ProgressStage |
Structured progress updates from long-running setup operations |
Model.iter_occurrences() |
Yield placed leaf parts with world transform, colour, step, and source context |
ModelOccurrence |
Value object for one placed part occurrence |
Model.source_line_for(piece) |
The source line a parsed piece came from |
ModelSummary / model_bounds() |
Whole-model bounds, part counts, colour usage, skipped-geometry report |
SkippedGeometry |
A part whose geometry could not be folded into the model bounds |
Parts.references_for(code) |
Type-1 subfile references a part places, optionally recursive |
PartReference / PartReferenceKind |
Value object + classification (part / subpart / primitive / unknown) for those references |
Parts.resolve_colour(colour) |
Enrich a bare colour code with catalog metadata |
CatalogEntry.symbol_name / .module_path / .import_statement() |
Generated-library symbol, module, and import line for a catalog entry |
ldraw.snippets |
symbol_name, module_path, suggested_import, symbol_name_for_description helpers |
Colour.display_label / swatch_rgb / swatch_alpha / is_transparent |
UI-facing colour accessors |
Other changes
| Change | Impact |
|---|---|
download() / generate() gained an on_progress= keyword |
Additive; the default CLI path is unchanged |
| Python 3.14 added to the supported/tested matrix | Nothing to do |
Coverage moved from Coveralls to Codecov; coveralls dev dep dropped |
Contributors only |
Removals affecting your code: none.
Upgrade checklist¶
- Upgrade the package:
uv add pyldraw3>=1.2.0(orpip install -U pyldraw3). - Do nothing for caches. The generated
ldraw.librarypackage and thecatalog.sqliteindex both self-heal (unchanged from 1.1.0). The newLDrawSession/ensure_librarymachinery uses the same fingerprints, so it agrees withldraw generateabout when a rebuild is needed. - There is no migration step. Optionally, adopt the new APIs below where they replace hand-rolled setup, geometry, or introspection code.
New APIs and what to build with them¶
Session and setup: ensure_library() and LDrawSession¶
The centerpiece of 1.2.0. Setting pyldraw up in an application previously
meant orchestrating download, generate, catalog indexing, and
LibraryImporter.set_config yourself, and knowing which of those were
already done. The new ldraw.session module packages that.
ensure_library() is the one-call "make everything ready" entry point. It
inspects the current state and does only the work that is actually
missing — download the library if absent, (re)generate ldraw.library if
stale, rebuild the catalog index if stale — then wires up the importer:
from ldraw import ensure_library
session = ensure_library() # download + generate + index as needed
parts = session.load() # ready to use
model = session.open_model("castle.mpd")
Its keyword arguments mirror the choices the CLI makes:
config/config_file— supply an explicitConfigor a config path instead of the default.version=— which LDraw release to download if one is missing (defaults to the complete release).force_generate=True— regenerateldraw.libraryeven if it looks fresh.write_config=True— persist the resolved configuration to disk.on_progress=— a callback receivingProgressEvents (see below).
Setup failures are normalized to RuntimeError with a readable message
(wrapping the underlying requests/zip/UnwritableOutputError cause).
LDrawSession is the lower-level handle when you want to inspect or
control the steps individually:
from ldraw import LDrawSession
session = LDrawSession() # uses Config.load() by default
state = session.state()
if state.ready:
parts = session.load()
else:
if state.needs_generation:
... # call ensure_library() to fix, or regenerate
if state.needs_index_rebuild:
parts = session.rebuild_index()
session.state()returns anLDrawStateclassifying the library, catalog index, and generated package. Its booleans —ready,library_available,needs_index_rebuild,needs_generation— let you branch without catching exceptions or stat-ing files yourself. The exact cause is instate.reasonsasLDrawStateReasonmembers (LIBRARY_MISSING,INDEX_STALE,GENERATED_LIBRARY_MISSING, …).session.pathsreturnsLDrawPaths— the resolvedparts.lst, generated-library, catalog-db, and other paths for the configuration, so you don't reconstruct them fromConfigby hand.session.load()loadsParts, building and persisting the catalog index when needed.session.rebuild_index(force=..., on_progress=...)rebuilds the persistent index (unlinking a stale DB first) and returns the loaded parts.session.open_model(path)is a thin wrapper overread_model.
Uses: application/library bootstrap on first launch, a "check status before doing expensive work" gate, health checks, or a settings screen that reports whether the library is installed and current.
Progress events for embeddable setup¶
ldraw.progress defines the structured events the setup flows emit.
Pass an on_progress callback to ensure_library, download, or
generate and you receive a typed ProgressEvent at each stage:
from ldraw import ProgressEvent, ProgressStage, ensure_library
def report(event: ProgressEvent) -> None:
if event.total:
print(f"[{event.stage}] {event.message}: {event.current}/{event.total}")
else:
print(f"[{event.stage}] {event.message}")
ensure_library(on_progress=report)
ProgressStageis aStrEnum:DOWNLOAD,UNPACK,PARTS_LIST,LIBRARY_GENERATION,INDEX_REBUILD,DONE.ProgressEventcarriesstage,message, optionalcurrent/totalbyte counts (for the download stage), and an optionalpath.
Download progress is throttled: at most one event per megabyte, with
the first (current=0) and final byte counts always forced through, so an
~80 MB download produces ~80 events rather than tens of thousands. When no
callback is supplied, no events are built at all — the default CLI path
pays nothing.
Uses: driving a GUI/TUI progress bar or spinner during first-run setup, logging setup steps, or surfacing "downloading… generating…" status in an embedding application.
Model occurrences: Model.iter_occurrences()¶
iter_occurrences() walks a model and yields one ModelOccurrence per
placed leaf part, with everything a geometry or analysis tool needs
already computed:
from ldraw import read_model
model = read_model("castle.mpd")
for occ in model.iter_occurrences():
print(occ.part_code, occ.colour.code, occ.position, occ.step)
Each ModelOccurrence (a frozen dataclass) carries:
piece,part_code,reference— the source piece and its code.colour— the effective colour, with the main-colour code (16) resolved to the inherited colour from the enclosing submodel reference.position/matrix— the world transform, composed down through every submodel reference (not the piece-local transform).source_model/source_line— which submodel the piece lives in and the line it was parsed from (Nonefor pieces built in code).step/source_step— the building step this occurrence belongs to.
Two keyword flags control traversal:
expand_submodels=False— treat submodel references as leaves (yield the reference itself) instead of recursing into them.include_steps=False— skip0 STEPaccounting (step/source_stepbecomeNone).
Submodel cycles raise SubmodelCycleError, as elsewhere in Model.
The related Model.source_line_for(piece) returns the parsed source line
for a given piece (source-line tracking is now threaded through parsing and
preserved by submodel_view).
Uses: exporting to renderers or other formats where you need world-space placements, per-step snapshots, mapping a piece back to its file/line for diagnostics, or any analysis that wants flattened placements with inherited colour already resolved.
Whole-model geometry: ModelSummary and model_bounds()¶
Built on top of occurrences, ModelSummary folds a model's real geometry
into a reusable summary:
from ldraw import ModelSummary, model_bounds, read_model
model = read_model("castle.mpd")
summary = ModelSummary.from_model(model, parts)
summary.bounds # transformed axis-aligned bounds (BoundingBox | None)
summary.size_ldu # overall size as a Vector, in LDU
summary.size_mm # same, in millimetres (× 0.4)
summary.part_counts # {part_code: count}
summary.colour_usage # {colour_code_or_rgb: count}
summary.occurrence_count
summary.skipped_geometry # parts whose geometry couldn't be resolved
boundsis the true transformed extent — each part's bounding-box corners are transformed by the occurrence's world matrix before folding, so rotated parts are accounted for.origin_boundsis the cheaper bounds of just the placement origins.- Parts whose geometry can't be resolved (a missing part, a part with no
geometry) don't abort the summary — they're collected as
SkippedGeometryrecords (part,source_model,source_line,reason) so you can report exactly what was left out.
model_bounds(model, parts) is the shorthand when all you want is the
BoundingBox.
Uses: camera framing / auto-centering, real-world dimension readouts,
sanity-checking a generated model's size, or a BOM-plus-dimensions report.
This supersedes hand-folding per-piece bounding_box() calls.
Part-file reference introspection: Parts.references_for()¶
references_for() exposes the type-1 subfile references inside a part
file — what a part is built out of:
for ref in parts.references_for("3001"):
print(ref.kind, ref.code, ref.description)
# Recurse through subparts and primitives:
tree = parts.references_for("3001", recursive=True)
Each PartReference carries parent_code, code, reference, kind,
description, the resolved colour, the position/matrix transform,
and a depth. PartReferenceKind classifies each reference as PART,
SUBPART, PRIMITIVE, or UNKNOWN (using the catalog, the primitive
index, and the 8/48/p primitive path conventions). Recursion follows
everything except UNKNOWN references and guards against cycles.
The supporting Parts.resolve_colour(colour) takes a Colour or a bare
integer code and returns the catalog's full metadata for that colour when
known, or the input unchanged otherwise.
Uses: exploded-view or part-composition analysis, dependency graphs between parts and primitives, or auditing which primitives a part relies on.
Catalog entry symbol/import helpers and ldraw.snippets¶
CatalogEntry now knows how to name and import itself in the generated
library:
entry.symbol_name # e.g. "Brick2X4"
entry.module_path # "ldraw.library.parts.bricks" (or None if uncategorized)
entry.import_statement() # "from ldraw.library.parts.bricks import Brick2X4"
The new ldraw.snippets module offers the same as free functions —
symbol_name(entry), module_path(entry), suggested_import(entry), and
symbol_name_for_description(description) — which is the exact
transformation code generation applies (and now shares), so your derived
symbol names always match what ldraw generate emits. The ldraw parts
CLI now routes through suggested_import for its import suggestions.
Uses: building "add this to your imports" hints in a UI, generating docs from the catalog, or resolving a catalog entry to its importable class.
Colour UI accessors¶
Colour gained four presentation-oriented properties for building palette
pickers and inspectors:
colour.display_label # "Bright Red" (name, spaces for underscores) / "Colour 4" / rgb / "Unknown colour"
colour.swatch_rgb # rgb hex for a swatch, or None
colour.swatch_alpha # alpha 0–255, defaulting to fully opaque when unset
colour.is_transparent # True when swatch_alpha is below opaque
These complement Colour.is_solid from 1.1.0: is_solid answers "is this
plain opaque paint (no alpha, no finish)?" for filtering, while these four
answer "how do I show this colour to a user?".
Uses: colour swatches and labels in TUIs/GUIs, palette pickers, and BOM or inspector displays.
Platform and tooling notes¶
- Python 3.14 is now part of the supported and CI-tested matrix (3.12+ remains the floor).
- Coverage reporting moved from Coveralls to Codecov; the
coverallsdev dependency was dropped andmemory-profiler/psutiladded for benchmarking. These affect contributors only, not consumers of the package.