Skip to content

db_ops

Metadata database read/write helpers for the editor UILink

Small FSDB helpers used by the metadata editor: load the flattened MIAPPE metadata of every scan, and write updated metadata back to metadata.json with a .bak backup and schema validation.

A database root must be a formal PlantDB (FSDB, i.e. a directory containing a romidb marker file). A loose directory that is not a proper ROMI DB is rejected.

all_scan_metadata Link

all_scan_metadata(db_path)

Return flattened scan metadata for every scan in the database.

Parameters:

Name Type Description Default

db_path Link

Path

Path to the FSDB to load.

required

Returns:

Type Description
dict of dict

A mapping of each scan id to its flattened MIAPPE metadata (see :func:plantdb.client.metadata_app.field_spec.flatten).

Source code in plantdb/client/metadata_app/db_ops.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def all_scan_metadata(db_path: Path) -> dict[str, dict[str, Any]]:
    """Return flattened scan metadata for every scan in the database.

    Parameters
    ----------
    db_path : Path
        Path to the FSDB to load.

    Returns
    -------
    dict of dict
        A mapping of each scan id to its flattened MIAPPE metadata
        (see :func:`plantdb.client.metadata_app.field_spec.flatten`).
    """
    flattened: dict[str, dict[str, Any]] = {}
    for scan in _connect(db_path).get_scans():
        flattened[scan.id] = flatten(scan.get_metadata())
    return flattened

apply_bulk Link

apply_bulk(db_path, scan_ids, path, value, backup=True)

Set the field at path to value on every scan in scan_ids.

Parameters:

Name Type Description Default

db_path Link

Path

Path to the FSDB containing the scans.

required

scan_ids Link

list of str

Scan ids to update.

required

path Link

str

Dot-separated field path to set.

required

value Link

Any

Value to set at the field.

required

backup Link

bool

Back up each scan's metadata before overwriting.

True

Returns:

Type Description
list of str

The scan ids that were actually modified (i.e. whose value changed). Writes are validated and backed up.

Source code in plantdb/client/metadata_app/db_ops.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
def apply_bulk(db_path: Path, scan_ids: list[str], path: str, value: Any,
               backup: bool = True) -> list[str]:
    """Set the field at ``path`` to ``value`` on every scan in ``scan_ids``.

    Parameters
    ----------
    db_path : Path
        Path to the FSDB containing the scans.
    scan_ids : list of str
        Scan ids to update.
    path : str
        Dot-separated field path to set.
    value : Any
        Value to set at the field.
    backup : bool, default True
        Back up each scan's metadata before overwriting.

    Returns
    -------
    list of str
        The scan ids that were actually modified (i.e. whose value changed).
        Writes are validated and backed up.
    """
    modified: list[str] = []
    db = _connect(db_path)
    for scan_id in scan_ids:
        scan = db.get_scan(scan_id)
        metadata = scan.get_metadata()
        if get_field(metadata, path) == value:
            continue
        set_field(metadata, path, value)
        write_scan_metadata(scan, metadata, backup=backup)
        modified.append(scan_id)
    return modified

close_db Link

close_db(db_path)

Disconnect and drop the cached FSDB for db_path, if any.

Used when the app switches to a different database so only one live connection stays in memory.

Parameters:

Name Type Description Default

db_path Link

Path

Path of the database whose cached connection to close.

required
Source code in plantdb/client/metadata_app/db_ops.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def close_db(db_path: Path) -> None:
    """Disconnect and drop the cached FSDB for ``db_path``, if any.

    Used when the app switches to a different database so only one live
    connection stays in memory.

    Parameters
    ----------
    db_path : Path
        Path of the database whose cached connection to close.
    """
    db_path = Path(db_path).resolve()
    with _DB_LOCK:
        db = _DB_CACHE.pop(db_path, None)
    if db is not None:
        db.disconnect()

get_field Link

get_field(metadata, path)

Return the value of the field at dot-path in a nested dict.

Parameters:

Name Type Description Default

metadata Link

dict of str to Any

The nested metadata dict to read.

required

path Link

str

Dot-separated path to the field, e.g. study.title.

required

Returns:

Type Description
Any

The field value, or None if the path does not exist.

Source code in plantdb/client/metadata_app/db_ops.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def get_field(metadata: dict[str, Any], path: str) -> Any:
    """Return the value of the field at dot-``path`` in a nested dict.

    Parameters
    ----------
    metadata : dict of str to Any
        The nested metadata dict to read.
    path : str
        Dot-separated path to the field, e.g. ``study.title``.

    Returns
    -------
    Any
        The field value, or ``None`` if the path does not exist.
    """
    node = metadata
    for part in path.split("."):
        if not isinstance(node, dict) or part not in node:
            return None
        node = node[part]
    return node

get_scan_dir Link

get_scan_dir(db_path, scan_id)

Return the on-disk directory of scan_id in the FSDB at db_path.

Parameters:

Name Type Description Default

db_path Link

Path

Path to the FSDB containing the scan.

required

scan_id Link

str

Identifier of the scan.

required

Returns:

Type Description
Path

Directory of the scan.

Source code in plantdb/client/metadata_app/db_ops.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def get_scan_dir(db_path: Path, scan_id: str) -> Path:
    """Return the on-disk directory of ``scan_id`` in the FSDB at ``db_path``.

    Parameters
    ----------
    db_path : Path
        Path to the FSDB containing the scan.
    scan_id : str
        Identifier of the scan.

    Returns
    -------
    Path
        Directory of the scan.
    """
    return _connect(db_path).get_scan(scan_id, owner_only=False).path()

migratable_scans Link

migratable_scans(db_path)

Return the ids of scans that still use the legacy (pre-MIAPPE) schema.

Parameters:

Name Type Description Default

db_path Link

Path

Path to the FSDB to inspect.

required

Returns:

Type Description
list of str

Scan ids that require migration.

Source code in plantdb/client/metadata_app/db_ops.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def migratable_scans(db_path: Path) -> list[str]:
    """Return the ids of scans that still use the legacy (pre-MIAPPE) schema.

    Parameters
    ----------
    db_path : Path
        Path to the FSDB to inspect.

    Returns
    -------
    list of str
        Scan ids that require migration.
    """
    return [sid for sid in _scan_ids(db_path) if scan_needs_migration(get_scan_dir(db_path, sid))]

migrate_scans_progress Link

migrate_scans_progress(db_path, scan_ids, logger, on_progress=None)

Migrate scan_ids scan by scan, reporting progress.

on_progress(done, total) is called after each scan is processed, so a caller can surface a live progress bar.

Parameters:

Name Type Description Default

db_path Link

Path

Path to the FSDB containing the scans.

required

scan_ids Link

list of str

Scan ids to migrate.

required

logger Link

Logger

Logger used to report each migrated scan.

required

on_progress Link

Callable[[int, int], None]

Callback invoked with (done, total) after each scan.

None

Returns:

Type Description
int

The number of scans that were actually migrated.

Source code in plantdb/client/metadata_app/db_ops.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
def migrate_scans_progress(db_path: Path, scan_ids: list[str], logger: logging.Logger,
                           on_progress: Callable[[int, int], None] | None = None) -> int:
    """Migrate ``scan_ids`` scan by scan, reporting progress.

    ``on_progress(done, total)`` is called after each scan is processed, so a
    caller can surface a live progress bar.

    Parameters
    ----------
    db_path : Path
        Path to the FSDB containing the scans.
    scan_ids : list of str
        Scan ids to migrate.
    logger : logging.Logger
        Logger used to report each migrated scan.
    on_progress : Callable[[int, int], None], optional
        Callback invoked with ``(done, total)`` after each scan.

    Returns
    -------
    int
        The number of scans that were actually migrated.
    """
    total = len(scan_ids)
    done = 0
    for sid in scan_ids:
        logger.info(f"Migrating {sid} ({done+1}/{total}): {get_scan_dir(db_path, sid)}")
        if migrate_scan_metadata(get_scan_dir(db_path, sid)):
            done += 1
        if on_progress is not None:
            on_progress(done, total)
    return done

scan_needs_migration Link

scan_needs_migration(scan)

Return True if a scan's metadata still holds a legacy object block.

Parameters:

Name Type Description Default

scan Link

Scan

Scan to inspect.

required

Returns:

Type Description
bool

True if the scan requires migration to the MIAPPE schema.

Source code in plantdb/client/metadata_app/db_ops.py
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def scan_needs_migration(scan: "Scan") -> bool:
    """Return ``True`` if a scan's metadata still holds a legacy ``object`` block.

    Parameters
    ----------
    scan : plantdb.commons.fsdb.core.Scan
        Scan to inspect.

    Returns
    -------
    bool
        ``True`` if the scan requires migration to the MIAPPE schema.
    """
    return migrate_metadata(_load_scan_metadata(scan))[1]

set_field Link

set_field(metadata, path, value)

Return metadata with the field at dot-path set to value.

Intermediate sections are created as needed.

Parameters:

Name Type Description Default

metadata Link

dict of str to Any

The nested metadata dict to modify (in place).

required

path Link

str

Dot-separated path to the field, e.g. study.title.

required

value Link

Any

Value to set at the field.

required

Returns:

Type Description
dict of str to Any

The same metadata dict, modified in place.

Source code in plantdb/client/metadata_app/db_ops.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def set_field(metadata: dict[str, Any], path: str, value: Any) -> dict[str, Any]:
    """Return ``metadata`` with the field at dot-``path`` set to ``value``.

    Intermediate sections are created as needed.

    Parameters
    ----------
    metadata : dict of str to Any
        The nested metadata dict to modify (in place).
    path : str
        Dot-separated path to the field, e.g. ``study.title``.
    value : Any
        Value to set at the field.

    Returns
    -------
    dict of str to Any
        The same ``metadata`` dict, modified in place.
    """
    parts = path.split(".")
    node = metadata
    for part in parts[:-1]:
        if not isinstance(node.get(part), dict):
            node[part] = {}
        node = node[part]
    node[parts[-1]] = value
    return metadata

update_biological Link

update_biological(metadata, tree)

Return metadata with the biological sections replaced by tree.

Non-biological keys (owner, created, ...) are preserved.

Parameters:

Name Type Description Default

metadata Link

dict of str to Any

The full scan metadata to update.

required

tree Link

dict of str to Any

New MIAPPE biological tree to install.

required

Returns:

Type Description
dict of str to Any

A copy of metadata with the biological sections replaced.

Source code in plantdb/client/metadata_app/db_ops.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def update_biological(metadata: dict[str, Any], tree: dict[str, Any]) -> dict[str, Any]:
    """Return ``metadata`` with the biological sections replaced by ``tree``.

    Non-biological keys (owner, created, ...) are preserved.

    Parameters
    ----------
    metadata : dict of str to Any
        The full scan metadata to update.
    tree : dict of str to Any
        New MIAPPE biological tree to install.

    Returns
    -------
    dict of str to Any
        A copy of ``metadata`` with the biological sections replaced.
    """
    updated = dict(metadata)
    for section in _BIOLOGICAL_SECTIONS:
        updated.pop(section, None)
    updated.update(tree)
    return updated

write_scan_metadata Link

write_scan_metadata(scan, metadata, backup=True)

Validate and write metadata to scan_dir/metadata/metadata.json.

A .bak copy of the previous file is written first when backup is True.

Parameters:

Name Type Description Default

scan Link

Scan

Scan instance to use for metadata modification.

required

metadata Link

dict of str to Any

The metadata to persist.

required

backup Link

bool

Write a .bak copy of the previous file before overwriting.

True

Raises:

Type Description
ValueError

If the MIAPPE biological block of metadata is invalid.

Source code in plantdb/client/metadata_app/db_ops.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def write_scan_metadata(scan: "Scan", metadata: dict[str, Any], backup: bool = True) -> None:
    """Validate and write ``metadata`` to ``scan_dir/metadata/metadata.json``.

    A ``.bak`` copy of the previous file is written first when ``backup`` is ``True``.

    Parameters
    ----------
    scan : plantdb.commons.fsdb.core.Scan
        Scan instance to use for metadata modification.
    metadata : dict of str to Any
        The metadata to persist.
    backup : bool, default True
        Write a ``.bak`` copy of the previous file before overwriting.

    Raises
    ------
    ValueError
        If the MIAPPE biological block of ``metadata`` is invalid.
    """
    validate_biological_metadata(metadata)
    md_path = _scan_metadata_path(scan)
    if backup and md_path.is_file():
        shutil.copy2(md_path, md_path.with_suffix(md_path.suffix + ".bak"))
    scan.set_metadata(metadata)