Skip to content

teff.checkpoint.file

teff.checkpoint.file

JSON-file checkpointing — zero dependencies, atomic via tempfile + rename.

Classes:

Name Description
JSONFileCheckpointer

Store checkpoints as one JSON file per (owner, checkpoint ID).

JSONFileCheckpointer

Bases: Checkpointer

Store checkpoints as one JSON file per (owner, checkpoint ID).

Writes go to a temp file in the same directory and are atomically renamed over the target, so a crash never leaves a corrupt file. Each owner gets its own subdirectory, so IDs only need to be unique within an owner. See :class:~teff.checkpoint.Checkpointer for how to pick an owner.

Methods:

Name Description
cleanup

Delete stale checkpoints; returns how many were removed.

list

Return all checkpoint IDs persisted for owner.

Source code in teff/checkpoint/file.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
class JSONFileCheckpointer(Checkpointer):
    """Store checkpoints as one JSON file per (owner, checkpoint ID).

    Writes go to a temp file in the same directory and are atomically
    renamed over the target, so a crash never leaves a corrupt file.
    Each *owner* gets its own subdirectory, so IDs only need to be
    unique within an owner.  See :class:`~teff.checkpoint.Checkpointer`
    for how to pick an owner.
    """

    def __init__(self, directory: str, suffix: str = ".json"):
        self._directory = Path(directory)
        self._directory.mkdir(parents=True, exist_ok=True)
        self._suffix = suffix

    def _path(self, checkpoint_id: str, owner: str = DEFAULT_OWNER) -> Path:
        safe = checkpoint_id.replace(os.sep, "_").replace("/", "_")
        owner_dir = self._directory / self._safe_owner(owner)
        owner_dir.mkdir(parents=True, exist_ok=True)
        return owner_dir / f"{safe}{self._suffix}"

    @staticmethod
    def _safe_owner(owner: str) -> str:
        return owner.replace(os.sep, "_").replace("/", "_").replace(".", "_")

    async def save(
        self,
        checkpoint_id: str,
        checkpoint: Checkpoint,
        *,
        owner: str = DEFAULT_OWNER,
    ) -> None:
        target = self._path(checkpoint_id, owner)
        tmp = target.with_suffix(f"{self._suffix}.tmp")

        def _save() -> None:
            tmp.write_text(
                json.dumps(checkpoint_to_dict(checkpoint), ensure_ascii=False),
                encoding="utf-8",
            )
            os.replace(tmp, target)

        await asyncio.to_thread(_save)

    async def load(
        self, checkpoint_id: str, *, owner: str = DEFAULT_OWNER
    ) -> Checkpoint | None:
        path = self._path(checkpoint_id, owner)

        def _load() -> Checkpoint | None:
            if not path.exists():
                return None
            data = json.loads(path.read_text(encoding="utf-8"))
            return checkpoint_from_dict(data)

        return await asyncio.to_thread(_load)

    async def delete(self, checkpoint_id: str, *, owner: str = DEFAULT_OWNER) -> None:
        path = self._path(checkpoint_id, owner)

        def _delete() -> None:
            if path.exists():
                path.unlink()

        await asyncio.to_thread(_delete)

    async def list(self, owner: str = DEFAULT_OWNER) -> list[str]:
        """Return all checkpoint IDs persisted for *owner*."""
        base = self._directory / self._safe_owner(owner)

        def _list() -> list[str]:
            if not base.exists():
                return []
            return sorted(
                p.name[: -len(self._suffix)]
                for p in base.glob(f"*{self._suffix}")
                if not p.name.endswith(f"{self._suffix}.tmp")
            )

        return await asyncio.to_thread(_list)

    def _owners(self) -> List[str]:
        if not self._directory.exists():
            return []
        return sorted(p.name for p in self._directory.iterdir() if p.is_dir())

    def _owner_checkpoints(self, owner: str) -> List[Tuple[str, float]]:
        """Return ``(checkpoint_id, mtime)`` pairs for one owner."""
        base = self._directory / self._safe_owner(owner)
        if not base.exists():
            return []
        pairs = []
        for p in base.glob(f"*{self._suffix}"):
            if p.name.endswith(f"{self._suffix}.tmp"):
                continue
            pairs.append((p.name[: -len(self._suffix)], p.stat().st_mtime))
        pairs.sort(key=lambda item: item[1], reverse=True)
        return pairs

    async def cleanup(
        self,
        *,
        owner: str | None = None,
        max_age: float | None = None,
        keep_last: int | None = None,
    ) -> int:
        """Delete stale checkpoints; returns how many were removed."""
        if max_age is None and keep_last is None:
            return 0

        def _cleanup() -> int:
            removed = 0
            owners = [owner] if owner is not None else self._owners()
            now = time.time()
            for own in owners:
                pairs = self._owner_checkpoints(own)
                to_delete: List[str] = []
                for idx, (cid, mtime) in enumerate(pairs):
                    if max_age is not None and now - mtime > max_age:
                        to_delete.append(cid)
                    elif keep_last is not None and idx >= keep_last:
                        to_delete.append(cid)
                for cid in to_delete:
                    path = self._path(cid, own)
                    if path.exists():
                        path.unlink()
                        removed += 1
            return removed

        return await asyncio.to_thread(_cleanup)

cleanup async

cleanup(*, owner=None, max_age=None, keep_last=None)

Delete stale checkpoints; returns how many were removed.

Source code in teff/checkpoint/file.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
async def cleanup(
    self,
    *,
    owner: str | None = None,
    max_age: float | None = None,
    keep_last: int | None = None,
) -> int:
    """Delete stale checkpoints; returns how many were removed."""
    if max_age is None and keep_last is None:
        return 0

    def _cleanup() -> int:
        removed = 0
        owners = [owner] if owner is not None else self._owners()
        now = time.time()
        for own in owners:
            pairs = self._owner_checkpoints(own)
            to_delete: List[str] = []
            for idx, (cid, mtime) in enumerate(pairs):
                if max_age is not None and now - mtime > max_age:
                    to_delete.append(cid)
                elif keep_last is not None and idx >= keep_last:
                    to_delete.append(cid)
            for cid in to_delete:
                path = self._path(cid, own)
                if path.exists():
                    path.unlink()
                    removed += 1
        return removed

    return await asyncio.to_thread(_cleanup)

list async

list(owner=DEFAULT_OWNER)

Return all checkpoint IDs persisted for owner.

Source code in teff/checkpoint/file.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
async def list(self, owner: str = DEFAULT_OWNER) -> list[str]:
    """Return all checkpoint IDs persisted for *owner*."""
    base = self._directory / self._safe_owner(owner)

    def _list() -> list[str]:
        if not base.exists():
            return []
        return sorted(
            p.name[: -len(self._suffix)]
            for p in base.glob(f"*{self._suffix}")
            if not p.name.endswith(f"{self._suffix}.tmp")
        )

    return await asyncio.to_thread(_list)