Skip to content

recording

Recording: a project's videos opened as streams, with the frame count they agree on.

open_recording opens the videos a project's manifest names (ADR-0081): each pattern is matched through glob.glob, so .. and absolute patterns work; a bare directory is refused, since a folder of stills is a glob; and videos that disagree on the frame count are refused.

Recording dataclass

Videos opened as streams, keyed by video name, with their agreed frame count.

Attributes:

Name Type Description
name str

What the stages' log and error lines call it: the project's name in the invocation (project_names), its leaf when it is opened on its own.

directory Path

The project folder, absolute and normalized.

videos dict[str, Video]

name -> Video in [videos] order, one per physical camera; the V axis is the rig's, never this.

n_frames int

The frame count every video has.

project Project

The project the videos are of.

fps float | None

The videos' declared frame rate, or None (an image sequence).

Source code in src/deeperfly/recording.py
@dataclass(frozen=True)
class Recording:
    """Videos opened as streams, keyed by video name, with their agreed frame count.

    Attributes:
        name: What the stages' log and error lines call it: the project's name in
            the invocation ([project_names][deeperfly.project.project_names]), its
            leaf when it is opened on its own.
        directory: The project folder, absolute and normalized.
        videos: `name -> Video` in `[videos]` order, one per physical camera; the `V`
            axis is the rig's, never this.
        n_frames: The frame count every video has.
        project: The project the videos are of.
        fps: The videos' declared frame rate, or `None` (an image sequence).
    """

    name: str
    directory: Path
    videos: dict[str, Video]
    n_frames: int
    project: Project
    fps: float | None = None

match_patterns

match_patterns(
    base: Path,
    video: str,
    patterns: Sequence[str],
    where: str,
) -> list[Path]

The files one video's patterns name, relative to base, in stream order.

A pattern is a file or a glob, relative to base or absolute, matched through glob.glob, which takes .. and absolute patterns; base itself is taken literally, so a file whose name holds *?[ is written escaped (glob.escape). A .. climbs out of the folder the filesystem reaches, a symlinked base being followed, and a file is reported the same way whether a glob or its literal name found it. A glob's matches are sorted and an explicit list of parts keeps its order. where prefixes every message.

Raises:

Type Description
FileNotFoundError

If a pattern matches no file.

ValueError

If a pattern names a directory.

Source code in src/deeperfly/recording.py
def match_patterns(
    base: Path, video: str, patterns: Sequence[str], where: str
) -> list[Path]:
    """The files one video's patterns name, relative to `base`, in stream order.

    A pattern is a file or a glob, relative to `base` or absolute, matched through
    `glob.glob`, which takes `..` and absolute patterns; `base` itself is taken
    literally, so a file whose name holds `*?[` is written escaped (`glob.escape`). A
    `..` climbs out of the folder the filesystem reaches, a symlinked `base` being
    followed, and a file is reported the same way whether a glob or its literal name
    found it. A glob's matches are sorted and an explicit list of parts keeps its
    order. `where` prefixes every message.

    Raises:
        FileNotFoundError: If a pattern matches no file.
        ValueError: If a pattern names a directory.
    """
    paths: list[Path] = []
    for pattern in patterns:
        typed = Path(pattern).expanduser()
        if any(ch in pattern for ch in GLOB_CHARS):
            # `base` is a literal: a folder named `fly[1]` must not act as a glob.
            rooted = (
                typed if typed.is_absolute() else Path(glob.escape(str(base))) / typed
            )
            found = sorted(
                _climbed(m) for m in glob.glob(str(rooted)) if os.path.isfile(m)
            )
        else:
            joined = under_folder(typed, base)
            if joined.is_dir():
                raise ValueError(
                    f"{where}: [videos] {video} = {pattern!r} is a directory; a video "
                    "is a file or a glob; for a folder of stills write `../top/*.png`"
                )
            found = [joined] if joined.is_file() else []
        if not found:
            raise FileNotFoundError(
                f"{where}: [videos] {video} = {pattern!r} matches no file"
            )
        paths.extend(found)
    return paths

normalized

normalized(path: str | Path) -> Path

An absolute, ..-free path that does not follow symlinks: the one typed.

Source code in src/deeperfly/recording.py
def normalized(path: str | Path) -> Path:
    """An absolute, `..`-free path that does not follow symlinks: the one typed."""
    return Path(os.path.normpath(Path(path).expanduser().absolute()))

open_recording

open_recording(
    project: Project, *, name: str | None = None
) -> Recording

Open the videos a project's manifest names, in its [videos] order.

Probing is eager, a project being the unit of failure (ADR-0037), and once per video: a hexaview project probes two videos for its six views, and nothing stays open (ADR-0101). name is what the recording is called, the project's own name by default.

Raises:

Type Description
FileNotFoundError

If a pattern matches no file.

ValueError

If a pattern names a directory, two videos claim one file, a frame count cannot be read, or the videos disagree on it.

Source code in src/deeperfly/recording.py
def open_recording(project: Project, *, name: str | None = None) -> Recording:
    """Open the videos a project's manifest names, in its `[videos]` order.

    Probing is eager, a project being the unit of failure (ADR-0037), and once per
    video: a hexaview project probes two videos for its six views, and nothing stays
    open (ADR-0101). `name` is what the recording is called, the project's own name by
    default.

    Raises:
        FileNotFoundError: If a pattern matches no file.
        ValueError: If a pattern names a directory, two videos claim one file, a
            frame count cannot be read, or the videos disagree on it.
    """
    matched = {video: project.video_paths(video) for video in project.manifest.videos}
    streams, n_frames, fps = _open_streams(str(project.manifest_path), matched)
    return Recording(
        name=project.name if name is None else name,
        directory=project.folder,
        videos=streams,
        n_frames=n_frames,
        fps=fps,
        project=project,
    )

under_folder

under_folder(value: str | Path, folder: Path) -> Path

A file path of a folder's document, against folder when it is relative.

Each .. is taken as the filesystem takes it, climbing out of the folder a symlinked folder points at, as for a video pattern (match_patterns); a name is not globbed.

Source code in src/deeperfly/recording.py
def under_folder(value: str | Path, folder: Path) -> Path:
    """A file path of a folder's document, against `folder` when it is relative.

    Each `..` is taken as the filesystem takes it, climbing out of the folder a
    symlinked `folder` points at, as for a video pattern
    ([match_patterns][deeperfly.recording.match_patterns]); a name is not globbed.
    """
    typed = Path(value).expanduser()
    return _climbed(str(typed if typed.is_absolute() else folder / typed))