Source code for httk.workflow.id_keys

"""Stable ledger source keys from workflow collect coordinates.

The httk-store id ledger (``httk.store.IdLedger``) maps a stable, opaque
**source key** to a permanent entry id, so a database rebuilt from the same
sources keeps its ids. This module builds those keys in the httk₂-native
grammar

.. code-block:: text

    <workspace_id>:<job_id>[:<role>[:file:<relpath>]]

from the coordinates a collected job already carries: the workspace and job
identities pin the producing job, an optional declared output *role* pins one
output of that job, and an optional *relpath* pins one file, marked by a literal
``file:`` segment so a role and a file cannot be confused.

The one hard rule is anchoring: a key must derive from a *stable* identity, not
from a path that can silently move. A :class:`~httk.workflow.collecting.CollectedJob`
harvested from a v1 tree without a manifest carries ``identity_stable=False``;
:func:`ledger_key` refuses it (``force=True`` overrides), because a path-derived
key that goes stale would hand an old id to new content — the one unforgivable
ledger failure. Keys are opaque to the allocator, so embedded colons in a role
or relative path are harmless.
"""

import os
from pathlib import PurePosixPath

from .collecting import CollectedJob, JobRecord

__all__ = [
    "UnstableIdentityError",
    "ledger_key",
]


[docs] class UnstableIdentityError(ValueError): """A ledger key was refused because the job's identity is not stable. Raised by :func:`ledger_key` for a collected job whose ``identity_stable`` is ``False`` — a v1-harvested job with no manifest, whose only identity is its absolute path. A key built from it would go stale the moment the tree moved, so it is refused unless ``force=True`` is passed. """
def _coordinates(item: CollectedJob | JobRecord) -> tuple[str, str, bool | None]: """Return the workspace id, job id, and identity-stability of a collected job. :param item: The collected job or its mechanical record. :return: The workspace id, job id, and the identity-stable flag (``None`` for a live-collected job, which is always stable). """ record = item.record if isinstance(item, CollectedJob) else item identity_stable = item.identity_stable if isinstance(item, CollectedJob) else None return record.workspace_id, record.job_id, identity_stable
[docs] def ledger_key( item: CollectedJob | JobRecord, role: str | None = None, path: str | os.PathLike[str] | None = None, *, force: bool = False, ) -> str: """Build the native ledger source key for a collected job coordinate. The key is ``<workspace_id>:<job_id>``, extended with ``:<role>`` when a declared output role is named and with ``:file:<relpath>`` when a file is named, so the four shapes are the job, one of its outputs, one of its files, and one file of one output. :param item: The collected job (or its record) the key anchors on. :param role: The declared output role to pin, if any. :param path: The output-relative file path to pin, if any; its POSIX form is used verbatim. :param force: Build the key even when the job's identity is not stable. :return: The ledger source key. :raises UnstableIdentityError: If the job's identity is not stable and ``force`` is not set. """ workspace_id, job_id, identity_stable = _coordinates(item) if identity_stable is False and not force: raise UnstableIdentityError( f"refusing to build a ledger key for job {workspace_id}:{job_id}: its identity is not stable " "(a v1-harvested job with no manifest is identified only by its absolute path, so a key built " "from it would hand an old id to new content once the tree moves). Pass force=True to override." ) key = f"{workspace_id}:{job_id}" if role is not None: key += f":{role}" if path is not None: key += f":file:{PurePosixPath(path).as_posix()}" return key