Files
notebook-snapshot-extension/snapshot/storage/base.py
T
tao.chenandClaude cd1aba6698 dev: initial implementation of notebook-snapshot extension
Manual-commit notebook snapshot extension (JupyterLab 4):

Backend
- Storage adapter layer (Local + S3 via boto3, pip extra)
  - Dumb put/get/list/delete contract; key validation
  - S3Storage: lazy boto3 import, S3ConnectionError with friendly
    messages + connectivity check at startup
- SnapshotStore
  - Strip code outputs/execution_count, MD5 on cleaned JSON
  - Gzipped envelope (id/timestamp/name/description/hash/size/notebook)
  - Append-only manifest.json (rebuildable from version files)
  - SnapshotUnchangedError when new hash matches most recent version
- REST API (commit / list / content) under /snapshot/ namespace
- traitlets config (storage_type, local_root, s3_* + S3_* env fallback)

Frontend
- snapshot:commit command (toolbar button + command palette)
- Dialog for name + description
- SnapshotPanel (left sidebar timeline) + DiffWidget (main area)
- Cell diff: id-match first, LCS fallback; jsdiff for line diff
- Restore via model.fromJSON() + context.save() (no refresh)
- All AGENTS.md hard conventions enforced

Tests: 33 backend pytest passing (storage + store + routes)

Docs: AGENTS.md, design.md, README.md synced with implementation.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 17:33:55 +08:00

128 lines
3.1 KiB
Python

"""Abstract storage adapter for notebook snapshot backends.
This module defines the common interface for dumb bytes buckets. It has no
knowledge of notebooks, versions, manifests, compression, or hashing.
"""
from abc import ABC, abstractmethod
def validate_key(key: str) -> str:
"""Validate and return a POSIX-style relative storage key.
Rejects empty keys, absolute paths, parent-directory segments, and
backslashes. The returned value is the same string passed in.
Parameters
----------
key: str
The storage key to validate.
Returns
-------
str
The validated key.
Raises
------
ValueError
If the key is empty, absolute, contains ``..``, or contains
backslashes.
"""
if not key:
raise ValueError("Storage key must not be empty")
if key.startswith("/"):
raise ValueError(f"Storage key must be relative, got absolute path: {key!r}")
if "\\" in key:
raise ValueError(f"Storage key must use POSIX separators, got backslash: {key!r}")
if ".." in key.split("/"):
raise ValueError(f"Storage key must not contain parent segments: {key!r}")
return key
class StorageAdapter(ABC):
"""Abstract base class for byte-oriented storage backends."""
@abstractmethod
def put(self, key: str, data: bytes) -> None:
"""Store ``data`` under ``key``.
Parameters
----------
key: str
The storage key.
data: bytes
The payload to store.
"""
raise NotImplementedError
@abstractmethod
def get(self, key: str) -> bytes:
"""Return the bytes stored under ``key``.
Parameters
----------
key: str
The storage key.
Returns
-------
bytes
The stored payload.
Raises
------
KeyError
If the key does not exist.
"""
raise NotImplementedError
@abstractmethod
def list_keys(self, prefix: str = "") -> list[str]:
"""Return a sorted list of keys starting with ``prefix``.
Parameters
----------
prefix: str, optional
Filter keys to those beginning with this value. The empty string
matches all keys.
Returns
-------
list[str]
Sorted matching keys.
"""
raise NotImplementedError
@abstractmethod
def delete(self, key: str) -> None:
"""Delete the object at ``key``.
This method is idempotent: deleting a missing key does not raise.
Parameters
----------
key: str
The storage key.
"""
raise NotImplementedError
def exists(self, key: str) -> bool:
"""Return whether ``key`` exists in storage.
Parameters
----------
key: str
The storage key.
Returns
-------
bool
``True`` if the key exists, ``False`` otherwise.
"""
try:
self.get(key)
return True
except KeyError:
return False