API Reference
Use croissant-baker as a Python library to generate Croissant metadata
programmatically — without the CLI.
MetadataGenerator
croissant_baker.metadata_generator.MetadataGenerator
Generates Croissant metadata for datasets with automatic type inference.
Discovers files, delegates format-specific logic to registered handlers via the build_croissant protocol, and assembles the final JSON-LD.
scan_report
property
Coverage of the last generate_metadata() call.
One entry per file the scan found, each carrying its outcome and, where it was not described, the reason. Populated before the "No supported files found in the dataset" guard fires, so a caller catching that error can still ask why. Empty before the first call.
reference_report
property
What foreign-key detection found, or None when it did not run.
Three states, because they mean different things to a reader: None
for a bake that never ran the pass (detect_references off, or
assembly raised before it), an empty report for one that ran and
found nothing, and a populated one otherwise.
__init__(dataset_path, name=None, description=None, url=None, license=None, citation=None, version=None, date_published=None, date_created=None, date_modified=None, creators=None, publisher=None, keywords=None, in_language=None, same_as=None, sd_license=None, sd_version=None, alternate_name=None, is_live_dataset=None, temporal_coverage=None, usage_info=None, identifier=None, conditions_of_access=None, is_accessible_for_free=None, included_in_data_catalog=None, profiles=None, field_mappings=None, count_csv_rows=False, max_workers=None, detect_references=False, includes=None, excludes=None, rai_fields=None, handlers=None)
Initialize the metadata generator for a dataset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dataset_path
|
str
|
Path to the directory containing dataset files. |
required |
name
|
Optional[str]
|
Dataset name (defaults to directory name). |
None
|
description
|
Optional[str]
|
Dataset description. |
None
|
url
|
Optional[str]
|
Dataset URL. |
None
|
license
|
Optional[str]
|
License URL or SPDX identifier (e.g. "CC-BY-4.0"). |
None
|
citation
|
Optional[str]
|
Citation text, preferably BibTeX format. |
None
|
version
|
Optional[str]
|
Dataset version string. |
None
|
date_published
|
Optional[str]
|
Publication date in ISO format ("2023-12-15" or "2023-12-15T10:30:00"). |
None
|
date_created
|
Optional[str]
|
Creation date in ISO format. |
None
|
date_modified
|
Optional[str]
|
Last-modified date in ISO format. |
None
|
creators
|
Optional[List[Dict[str, str]]]
|
List of dicts with "name", "email", and/or "url" keys. |
None
|
publisher
|
Optional[str]
|
Name of the publishing organization (schema.org/Organization). |
None
|
keywords
|
Optional[List[str]]
|
Topical keywords for dataset discovery (schema.org/keywords). |
None
|
in_language
|
Optional[List[str]]
|
BCP 47 language code(s) (e.g. "en"). Multiple supported. |
None
|
same_as
|
Optional[List[str]]
|
URLs of equivalent dataset records (e.g. DOI, mirror landing pages). Multiple values supported per schema.org/sameAs. |
None
|
sd_license
|
Optional[str]
|
License of the metadata description itself, distinct from the data license (schema.org/sdLicense). |
None
|
sd_version
|
Optional[str]
|
Version of the metadata description, distinct from
|
None
|
alternate_name
|
Optional[str]
|
Short alias for the dataset (schema.org/alternateName). |
None
|
is_live_dataset
|
Optional[bool]
|
Mark dataset as a live, evolving stream. |
None
|
temporal_coverage
|
Optional[str]
|
Time period the data covers — schema.org accepts free text or ISO 8601 (e.g., "2008/2019", "2023-01-15"). |
None
|
usage_info
|
Optional[str]
|
URL of a usage/consent policy (e.g., a DUO term URL, ODRL Offer URL). |
None
|
identifier
|
Optional[List[str]]
|
Accessions or persistent identifiers the dataset is known by (e.g. a dbGaP phs number, an EGA study accession, a DOI). Deduplicated in the order given; one value is emitted as a string, several as a list. |
None
|
conditions_of_access
|
Optional[str]
|
How access is obtained, in free text (e.g. the data access agreement and committee for a controlled release). schema.org/conditionsOfAccess. |
None
|
is_accessible_for_free
|
Optional[bool]
|
Whether the data can be had without payment or an access agreement. Tri-state: None leaves the key absent. |
None
|
included_in_data_catalog
|
Optional[str]
|
URL of a catalog entry that lists this
dataset. Emitted as a |
None
|
profiles
|
Union[str, List[str], None]
|
Additional profiles the document declares in
|
None
|
field_mappings
|
Optional[Dict[str, Dict[str, object]]]
|
Per-column overrides keyed by field name. Each value
is a dict with optional |
None
|
count_csv_rows
|
bool
|
If True, scan each CSV fully for exact row counts. Defaults to False for performance. |
False
|
max_workers
|
Optional[int]
|
Maximum worker threads for per-file metadata extraction. None (default) auto-sizes from the CPU count; 1 forces serial. Output is identical regardless of this value. |
None
|
detect_references
|
bool
|
If True, run conservative foreign-key detection and emit cr:references links between RecordSets that share a key column with a name-identifiable parent table. Defaults to False. |
False
|
includes
|
Optional[List[str]]
|
Glob patterns to include. Applied before excludes. |
None
|
excludes
|
Optional[List[str]]
|
Glob patterns to exclude. Applied after includes. |
None
|
rai_fields
|
Optional[Dict[str, object]]
|
Native mlcroissant RAI metadata fields, passed through
to |
None
|
handlers
|
Optional[HandlerRegistry]
|
Which handlers to consult, and in what order. Defaults to the built-in registry. Supply one to bake with a narrower set, or with a handler the baker does not ship. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If dataset_path is not a directory, or a named profile
is not one of |
generate_metadata(progress_callback=None)
Generate complete Croissant metadata for the dataset.
Per-file metadata extraction (handler selection, whole-file SHA-256,
header/schema reads) is I/O-bound and independent across files, so it
runs on a thread pool sized by max_workers. Results are reassembled
in discovery order before any FileObject @id is assigned, so the
document is identical regardless of worker count. Nothing is reported
per file as it goes: what became of each one is in
:attr:scan_report, which the CLI prints under --verbose.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
progress_callback
|
Optional callback with signature (completed: int, total: int, file_path: str) -> None invoked once per file as it finishes extraction. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If nothing in the dataset could be described, or a declared profile's minimum fields are missing from the document that was assembled. |
save_metadata(output_path, validate=True)
Generate and save Croissant metadata to a file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
output_path
|
str
|
Path where the JSON-LD metadata file will be written. |
required |
validate
|
bool
|
If True (default), validates with mlcroissant before saving. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If validation fails or the file cannot be saved. |
Scan coverage
Every file the scan finds gets one entry carrying what became of it. The report survives the "no supported files" error, so a bake that described nothing can still explain itself.
croissant_baker.scan.ScanReport
dataclass
A view over resolved scan entries: what was described, and what was not.
:meth:summary_lines is for the terminal, and its length depends on how
many kinds of problem occurred, never on how many files did. A
diagnostic is counted there the same way: one line per code.
:meth:to_dict carries the per-file detail, for --report and for
downstream tools checking coverage.
total
property
How many files the scan found.
described
property
Entries a handler described and whose nodes were assembled.
linked
property
Entries carried as another described file in a different form.
referenced
property
Entries another file's handler put in the document.
undescribed
property
Entries the document does not carry, in scan order.
Membership of the document, not of a record set: a file with a
FileObject is in there whether it got there on its own
(DESCRIBED), as another form of a described file (LINKED), or
as part of a multi-file record (REFERENCED).
counts()
Number of entries per reason, in declaration order.
Over the undescribed only: a reason answers why a file is not in the document, so counting one against a file that is in there says the opposite of what it means. A linked file keeps its own reason — that is the evidence it was linked on — and is not counted here.
summary_lines()
A header plus at most one line per reason.
The header names every way into the document that happened, so a run
that linked or referenced files says so rather than folding them into
one count. described and not described are always stated; the
two middle buckets appear only when non-zero.
to_dict()
Every discovered file with its outcome.
reason is one of a finite set a caller can branch on; detail is
the sentence for a human. described, linked and referenced
are the three ways into the document and sum with undescribed to
total; by_reason accounts for the undescribed alone.
by_diagnostic is orthogonal to all of them, and a file's
diagnostics key appears only when it has one.
croissant_baker.entries.Outcome
Bases: str, Enum
What became of a file the scan found.
str mixin so an outcome serialises to its own value in the JSON report.
PENDING, READY and WOULD_PROCESS are working states; a completed
bake leaves none of them behind.
croissant_baker.entries.Reason
Bases: str, Enum
Why a file was not described, as one of a finite set of categories.
The entry's detail names the file and the exception; this is what the
summary counts, so terminal output stays bounded by the number of kinds
of problem.
Foreign-key detection
Opt-in, via --detect-references or detect_references=True. The report is
None when the pass did not run, so "found nothing" and "never asked" stay
distinguishable. Shared key columns it declines to link are carried here rather
than dropped.
croissant_baker.references.ReferenceReport
dataclass
What the foreign-key pass linked, and what it declined to link.
Held by the generator and rendered by the CLI, the way
:class:croissant_baker.scan.ScanReport is: the library states what it
found, and only the CLI writes to a terminal. :meth:summary_lines is one
line by default, whatever the dataset, and names a column per line only
under verbose.
links = field(default_factory=list)
class-attribute
instance-attribute
unresolved = field(default_factory=list)
class-attribute
instance-attribute
summary_lines(verbose=False)
One line saying what was linked, and under verbose what was not.
The invitation to re-run is folded into that line rather than added as
its own Tip:, because the coverage section above may already end in
one and two competing tips read as noise.
croissant_baker.references.Unlinkable
Bases: str, Enum
Why a shared key column was reported instead of linked.
str mixin so a reason serialises to its own value, as
:class:croissant_baker.entries.Reason does.
File Discovery
croissant_baker.files.discover_files(dir_path, include_patterns=None, exclude_patterns=None)
Recursively discover all files in a directory (skipping hidden directories) and return their relative paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dir_path
|
str
|
Path to the directory to scan. |
required |
include_patterns
|
Optional[List[str]]
|
Optional list of glob patterns to include. |
None
|
exclude_patterns
|
Optional[List[str]]
|
Optional list of glob patterns to exclude. |
None
|
Returns:
| Type | Description |
|---|---|
List[Path]
|
List of relative file paths found in the directory. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the directory does not exist or is not a directory. |
PermissionError
|
If the directory cannot be accessed. |
Handler Interface
A handler answers three questions about one format, none of them involving
compression: the pipeline resolves that first and hands over a
FileSource.
can_handle(path) and extract_metadata(path) are the previous names for the
first two. They still work — in both directions — and warn once per handler
class. See DEVELOPMENT.md
for how to write a handler.
croissant_baker.handlers.base_handler.FileTypeHandler
Bases: ABC
Abstract base class for file type handlers.
Each handler is responsible for three things:
claims: decide if this handler owns a given fileextract: read one file's structure from a sourcebuild_croissant: turn that metadata into FileSets + RecordSets
The generator owns FileObject creation and @id assignment.
claims and extract never see a compression wrapper: both are given
a source built from the logical name.
build_croissant is given both names. relative_path is logical and
derives identifiers, so a wrapped file and its plain twin describe one
table; stored_name is the file as it sits on disk and belongs in
descriptions. Use
:func:~croissant_baker.handlers.utils.display_name for the latter.
can_handle(path) and extract_metadata(path) are the previous names
for the first two. They still work, in both directions, and warn once per
class.
Adding a new format: subclass this, implement all three methods, add the
instance to builtin_handlers() in registry.py.
Subclasses set these class attributes for documentation and dispatch:
EXTENSIONS: format suffixes this handler claims, e.g.(".csv",). Compression is stripped before a handler is asked, so".csv.gz"is never a valid entry.FORMAT_NAME,FORMAT_DESCRIPTION: for the generated docs table.INPUT_KIND: see :class:InputKind. Defaults toSTREAM.
claims(source)
Whether this handler describes the given file.
Match on source.suffix, the logical suffix: .csv for
data.csv and data.csv.gz alike. Use source.peek() when the
extension alone is not enough.
extract(source, **kwargs)
Describe one file's structure.
Read through source.open() or source.open_text(), which are
already decompressed, and take file_name, file_size and
sha256 from the source. Report only the format's own media type in
encoding_format; the generator adds the compression one.
Thread-safety: may be called concurrently across files on a single shared handler instance, so keep per-call state local.
Returns:
| Type | Description |
|---|---|
dict
|
Extracted metadata. For tabular data this should include |
dict
|
|
build_croissant(file_metas, file_ids)
abstractmethod
Build Croissant FileSets and RecordSets for every file this handler read.
Called once per handler, after the FileObject loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_metas
|
list[dict]
|
metadata dicts from |
required |
file_ids
|
list[str]
|
FileObject @ids assigned by the generator, aligned by
position with |
required |
Returns:
| Type | Description |
|---|---|
tuple
|
|
tuple
|
A handler that describes some of its batch but not all returns a |
tuple
|
third element, |
tuple
|
file it passed over, indexed into |
tuple
|
reported as failures and the rest of the batch is still described. |
tuple
|
Raising instead fails the whole batch. |
can_handle(file_path)
Deprecated. Implement and call :meth:claims instead.
extract_metadata(file_path, **kwargs)
Deprecated. Implement and call :meth:extract instead.
croissant_baker.handlers.base_handler.InputKind
Bases: str, Enum
What a handler needs in order to read a file.
A handler declares the input it consumes rather than whether it supports compression; compression support follows from the declaration.
Sources
croissant_baker.sources.FileSource
dataclass
One file, offered to a handler with compression already resolved.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The logical basename. |
relative_path |
Path
|
The logical path relative to the dataset root. |
size |
int
|
Size in bytes of the file as stored, so compressed size for a
wrapped file. This is what goes in |
exists |
bool
|
Whether the file was present when the source was built. |
Compared by identity: two different files can share a logical name, a size and an existence flag, and the openers that tell them apart are closures.
name
instance-attribute
relative_path
instance-attribute
suffix
property
The logical suffix, lowercased. .csv for data.csv.gz.
size
instance-attribute
sha256
cached
property
SHA-256 of the bytes as stored, so the digest identifies the artefact that was actually acquired.
open()
Open the file as binary, already decompressed.
open_text(encoding=compression.DEFAULT_TEXT_ENCODING)
Open the file as text, already decompressed.
peek(size)
Read the first size decompressed bytes, then close.
Returns fewer bytes than asked for at end of file, and b"" if the
file cannot be read at all.
croissant_baker.sources.PathSource
dataclass
Bases: FileSource
A source that also exposes a real path, for handlers that need one.
Built only for uncompressed files, so path never points at a wrapper —
an invariant :func:make_source enforces.
Handler registry
croissant_baker.handlers.registry.HandlerRegistry
The handlers to consult, in the order to consult them.
Order is registration order, and it decides overlapping claims.
__init__(handlers=None)
register(handler)
Add handler, unless one of its class is already registered.
Identity is the class, not the instance: callers construct a fresh instance each time, so deduplicating by instance would not deduplicate.
handlers()
Every registered handler, in dispatch order.
select(file_path, relative_path=None)
Find the handler that owns file_path, or say why none does.
Every handler is asked about the logical file through a plain
:class:~croissant_baker.sources.FileSource. Only a winner declaring
InputKind.PATH gets a
:class:~croissant_baker.sources.PathSource, and only for an
uncompressed file; a compressed one is refused with its own reason.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
file_path
|
Path
|
Path to the file, wrapper suffix included. |
required |
relative_path
|
Optional[Path]
|
Its path relative to the dataset root, for the source. |
None
|
croissant_baker.handlers.registry.builtin_handlers()
The handlers the baker ships with, in dispatch order.
croissant_baker.handlers.registry.HandlerSelection
dataclass
The outcome of asking the registry who owns a file.
When nothing claimed it, reason is the category the scan summary counts
and refusal is the sentence a human reads.
Compression
Adding a compression is one call: dispatch strips the new suffix, streams
decompress through it, encodingFormat gains its media type, and FileSet globs
expand to cover it.
croissant_baker.compression.Compression
dataclass
One supported compression wrapper.
media_type accompanies the format's own media type rather than
replacing it. opener has the signature of :func:gzip.open.
croissant_baker.compression.register_compression(comp)
Add a compression to the registry.
Everything downstream follows: dispatch strips the new suffix, streams
decompress through it, encodingFormat gains its media type, and FileSet
globs expand to cover it. No handler changes.
croissant_baker.compression.compressions()
Every registered compression, in match order.