mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:07:15 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
"""CLI handler for the ``sync`` subcommand.
|
|
|
|
Split out from :mod:`diet_guard._cli` to keep that module under the repo's
|
|
500-line cap (see ``CLAUDE.md``'s "feat: split oversized modules" history) --
|
|
the same reason the gate window logic lives across ``_gatelock*.py`` instead
|
|
of one file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from importlib import import_module
|
|
import sys
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
import argparse
|
|
from collections.abc import Callable
|
|
|
|
# ``_cli_args`` imports this module just to register the subparser, but
|
|
# ``crdt_sync``/``_sync`` drag in ``requests`` (~78ms). Only the ``sync``
|
|
# command itself needs them, so every other subcommand -- notably the gate's
|
|
# frequent ``--check`` -- no longer pays for an HTTP stack it will not use.
|
|
#
|
|
# A module-level ``__getattr__`` (PEP 562) rather than function-local imports
|
|
# so ``patch.object(_cli_sync, "run_sync", ...)`` still resolves.
|
|
_LAZY_ATTRS = {
|
|
"run_sync": ("diet_guard._sync", "run_sync"),
|
|
"SyncError": ("diet_guard._sync", "SyncError"),
|
|
"RemoteSyncError": ("crdt_sync", "RemoteSyncError"),
|
|
}
|
|
|
|
|
|
def __getattr__(name: str) -> object:
|
|
"""Resolve a deferred sync import on first attribute access."""
|
|
target = _LAZY_ATTRS.get(name)
|
|
if target is None:
|
|
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
raise AttributeError(msg)
|
|
module_name, attr = target
|
|
return getattr(import_module(module_name), attr)
|
|
|
|
|
|
def register_sync_subparser(sub: argparse._SubParsersAction) -> None:
|
|
"""Register the ``sync`` subcommand on ``sub``."""
|
|
sub.add_parser(
|
|
"sync",
|
|
help="Pull/merge/push the log with other devices via GitHub.",
|
|
)
|
|
|
|
|
|
def cmd_sync(emit: Callable[[str], None]) -> int:
|
|
"""Run one sync tick and report what happened via ``emit``.
|
|
|
|
Errors are caught here rather than left to propagate: a sync failure
|
|
(missing PAT, network error, repo misconfigured) is routine enough on a
|
|
timer-driven command that the CLI should report it and exit non-zero,
|
|
not crash with a traceback.
|
|
|
|
Args:
|
|
emit: A one-line output sink (``_cli._emit``, kept private to that
|
|
module -- passed in rather than imported, so this module has no
|
|
reach-in dependency on ``_cli``'s internals).
|
|
|
|
Returns:
|
|
0 on a successful sync, 1 if it could not run or failed partway.
|
|
"""
|
|
module = sys.modules[__name__]
|
|
try:
|
|
merged = module.run_sync()
|
|
except module.SyncError as exc:
|
|
emit(f"sync not configured: {exc}")
|
|
return 1
|
|
except module.RemoteSyncError as exc:
|
|
# The shared base, not GitHubSyncError: Firebase's errors are siblings
|
|
# of GitHub's, so catching only the latter let the primary backend's
|
|
# failures crash this timer-driven command with a traceback.
|
|
emit(f"sync failed: {exc}")
|
|
return 1
|
|
total_entries = sum(len(entries) for entries in merged.values())
|
|
emit(f"synced: {total_entries} entries across {len(merged)} day(s).")
|
|
return 0
|