utils/crdt-sync/crdt_sync/_remote.py
Krzysztof kuhy Rudnicki 6154dc09bf Pin pre-commit's ruff to 0.16.7 and satisfy the CPY001 it brings
The three Python packages pinned ruff twice at different versions:
requirements.txt at 0.16.7, .pre-commit-config.yaml at v0.15.2. Only the
older one ever ran, so `select = ["ALL"]` was enforced two minor versions
behind. Measured before aligning: v0.15.2 reports 0 findings, every 0.16.x
reports CPY001 on each file -- 70 crdt-sync, 23 freedays, 71 gatelock, and
no other rule.

Resolved by adding the notice rather than ignoring the rule, so the gate
keeps its teeth.

Two gatelock modules sat at exactly the 250-line cap, so a one-line header
tipped them to 251. Both are split along a real seam rather than shaved:

  _widget_fills.py    colour arithmetic over LockConfig, no tkinter import
  _surfaces_focus.py  selects among existing surfaces, never mutates them

The focus split keeps _surfaces.py's documented containment (it stays the
only module that creates, moves or destroys windows), and the static guard
in test_recovery_invariants.py globs _surfaces*.py, so the new sibling is
covered with no list to extend. widgets.py re-exports the fills helpers,
which keeps gatelock.widgets the single public surface its tests import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kMvSMtsVMB3d2uoZ56pbp
2026-09-12 18:11:24 +02:00

84 lines
3.3 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki
"""Backend-neutral remote-storage contract that sync talks through.
Deliberately tiny: :func:`crdt_sync.sync_log` and every app-level sync
service talk only to :class:`RemoteStore`, so swapping the storage backend
(GitHub Contents API, Firebase Realtime Database, a dual-writing mirror of
both) is a constructor change at the call site and nothing more.
Mirrors ``crdt_sync_dart``'s ``lib/src/remote_store.dart``; keep the two in
step.
"""
from __future__ import annotations
from typing import Protocol, runtime_checkable
class RemoteSyncError(Exception):
"""Raised for a remote-storage failure the caller must not ignore.
The backend-neutral base of the error hierarchy: catch this to handle
"the sync transport failed" regardless of which backend is configured.
Backends narrow it -- :class:`crdt_sync.GitHubSyncError` and
:class:`crdt_sync.FirebaseSyncError` both derive from it -- so existing
``except GitHubSyncError`` handlers keep working while a caller that has
been migrated can catch the base type instead.
"""
class RemoteNotFoundError(RemoteSyncError):
"""Raised when the configured remote itself is unreachable.
Distinguished from a missing *path* (nothing pushed there yet, which is
benign -- it just means no other device has synced before) so the caller
can tell "the repo/database is wrong or the credential isn't scoped to
it" apart from "no other device has synced yet".
"""
@runtime_checkable
class RemoteStore(Protocol):
"""Dumb keyed storage of UTF-8 text blobs, with directory listing.
Implementations raise :class:`RemoteSyncError` (or a subclass) for any
failure the caller must not silently ignore, and treat a missing path as
a benign ``None`` / empty list rather than an error.
A ``Protocol`` rather than an ABC so the existing
:class:`crdt_sync.GitHubSyncClient` satisfies it structurally, with no
base class to add and no change to its own tests.
"""
def list_directory(self, path: str) -> list[str]:
"""Return the entry names directly under ``path``.
Returns an empty list when nothing has been written there yet.
Includes both files and subdirectories: the
``<path_prefix>/<device_id>/<filename>`` layout needs to discover
device *directories*, not just files.
"""
def get_file_text(self, path: str) -> str | None:
"""Return the text stored at ``path``, or ``None`` if absent."""
def put_file_text(self, path: str, text: str, *, message: str) -> None:
"""Create or replace the text at ``path``.
``message`` is a human-readable reason for the write. Backends that
record one (GitHub commits) use it; backends that do not (Firebase)
ignore it.
"""
def delete_file(self, path: str, *, message: str = ...) -> None:
"""Delete ``path``. A no-op when ``path`` does not exist."""
def can_access_remote(self) -> bool:
"""Return whether the configured credential can reach the remote.
A lightweight connection test for a settings "Test connection"
button: it probes the remote root, so it succeeds even before any
file has been pushed. Never raises -- a network failure or missing
remote returns ``False``.
"""