The MetadataParser Classes#

A MetadataParser extracts BDF metadata fields (such as start_time) from a data file. The base MetadataParser never matches, and returns nothing; concrete subclasses own all file I/O for their source type.

Two concrete parsers are provided:

  • TxtPreambleParser – reads metadata from the head bytes of the data file itself (magic token + regex extraction)

  • JsonSidecarParser – reads metadata from a .metadata.json file adjacent to the data file

import re
import json
import tempfile
from pathlib import Path

from bdf.metadata_parsers import MetadataParser, TxtPreambleParser, JsonSidecarParser, MetadataSchema
from bdf.file_utils import resolve_source

BIOLOGIC_URL = (
    "https://zenodo.org/api/records/18986774/files/"
    "SINTEF__NaCR32140-MP10-04__2025-08-25__GITT_0p05C_25degC__BioLogic.mpt/content"
)
# The base MetadataParser never matches and extracts nothing
parser = MetadataParser()
biologic_file = resolve_source(BIOLOGIC_URL)

print("matches():", parser.matches(biologic_file))
print("parse():  ", parser.parse(biologic_file))
matches(): False
parse():   {}

MetadataSchema — the field name registry#

MetadataSchema is a generic Pydantic model that declares one field per supported BDF metadata field. It is the single source of truth for metadata field names (analogous to TableNormalizer’s mr_name fields).

Because the model is configured with extra="forbid", a typo in a field name raises a validation error at construction time rather than silently producing an empty parser.

# extra="forbid" catches typos immediately
try:
    MetadataSchema(strat_time="typo")  # misspelled field name
except Exception as e:
    print("Caught:", type(e).__name__)

# Available field names
print("BDF metadata fields:", list(MetadataSchema.model_fields.keys()))
Caught: ValidationError
BDF metadata fields: ['start_time']

TxtPreambleParser — extract metadata from the file preamble#

Construct a TxtPreambleParser with:

  • magic – one or more tokens; matches() returns True when any token is found in the file’s head bytes

  • regex_patterns – a MetadataSchema mapping each desired field to a compiled regex whose group(1) is the extracted value

preamble_parser = TxtPreambleParser(
    magic=("BT-Lab ASCII FILE",),
    regex_patterns=MetadataSchema(
        start_time=re.compile(r"Acquisition started on\s*:\s*(.+)"),
    ),
)
preamble_parser
TxtPreambleParser(kind='txt_preamble', magic=('BT-Lab ASCII FILE',), encoding='utf-8', regex_patterns=MetadataSchema[Pattern[str]](start_time=re.compile('Acquisition started on\\s*:\\s*(.+)')))
# matches() scans head bytes for the magic token
preamble_parser.matches(biologic_file)
True
# parse() applies the regex patterns and returns the extracted values
preamble_parser.parse(biologic_file)
{'start_time': '06/23/2025 09:17:30.015'}

JsonSidecarParser — extract metadata from an adjacent JSON file#

JsonSidecarParser looks for a .metadata.json file at path.with_suffix(".metadata.json"). key_synonyms maps each BDF field to an ordered tuple of candidate JSON keys; the first matching key wins.

Below we write a temporary sidecar file to demonstrate the API, then clean it up.

sidecar_parser = JsonSidecarParser(
    key_synonyms=MetadataSchema(
        start_time=("acquisition_started_on", "start_time"),
    ),
)

# Create a temp data file and write a .metadata.json sidecar alongside it
with tempfile.NamedTemporaryFile(suffix=".data", delete=False) as tf:
    data_path = Path(tf.name)

sidecar_path = data_path.with_suffix(".metadata.json")
try:
    sidecar_path.write_text(json.dumps({"acquisition_started_on": "05/13/2024 11:19:51.602"}))
    print("matches():", sidecar_parser.matches(data_path))
    print("parse():  ", sidecar_parser.parse(data_path))
finally:
    data_path.unlink(missing_ok=True)
    sidecar_path.unlink(missing_ok=True)
matches(): True
parse():   {'start_time': '05/13/2024 11:19:51.602'}