Files
LEDMatrix/src/plugin_system/saved_repositories.py
T
ChuckandClaude Opus 5.5 b8c01c69fb ci: mypy ratchet -- keep type-clean modules clean (71 modules, 536 -> 442 errors) (#661)
* ci: mypy ratchet -- keep type-clean modules clean

mypy-clean.txt lists the 71 modules under src/ that type-check clean;
scripts/check_types.py runs mypy (--follow-imports=silent) on exactly
those files and fails on any error or a missing/unsorted/duplicate entry.
A new "Type check (mypy ratchet)" CI job runs it with mypy 1.20.2 and
pinned stubs; the manual pre-commit mypy hook now runs the same script
(a local hook, so mypy sees the installed requirements like CI does).

35 modules were made clean with annotation-only fixes: hints, typing.cast,
TYPE_CHECKING imports, implicit-Optional defaults made explicit, and
annotations widened (never guards removed) where mypy called a defensive
isinstance check unreachable. No runtime behaviour change.

mypy.ini: numpy and orjson are treated as Any (follow_imports=skip, also
for stubs). numpy 2.3+ stubs use 3.12 `type` statements that mypy won't
parse at python_version 3.10, and orjson is optional, so seeing its stubs
made the result depend on whether it was installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate check_types.py's list-form mypy subprocess

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:01:35 -04:00

156 lines
5.2 KiB
Python

"""
Saved Repositories Manager for LEDMatrix
Manages saved GitHub repository URLs for easy plugin discovery and installation.
"""
import json
import os
from pathlib import Path
from typing import List, Dict, Optional, cast
from src.logging_config import get_logger
from src.plugin_system.repo_urls import normalize_repo_url
class SavedRepositoriesManager:
"""Manages saved GitHub repository URLs."""
def __init__(self, config_path: str = "config/saved_repositories.json"):
"""
Initialize the saved repositories manager.
Args:
config_path: Path to JSON file storing saved repositories
"""
self.config_path = Path(config_path)
self.logger = get_logger(__name__)
self.repositories = self._load_repositories()
def _load_repositories(self) -> List[Dict[str, str]]:
"""Load saved repositories from file."""
try:
if self.config_path.exists():
with open(self.config_path, 'r') as f:
data = json.load(f)
# Ensure it's a list
if isinstance(data, list):
return data
elif isinstance(data, dict) and 'repositories' in data:
return cast(List[Dict[str, str]], data['repositories'])
else:
return []
return []
except Exception as e:
self.logger.error(f"Error loading saved repositories: {e}")
return []
def _save_repositories(self) -> bool:
"""Save repositories to file atomically.
Writes to a temp file in the same directory and os.replace()s it
over the target, so a failed write can never truncate or
half-overwrite an existing saved_repositories.json.
"""
tmp_path = self.config_path.with_suffix(self.config_path.suffix + '.tmp')
try:
# Ensure directory exists
self.config_path.parent.mkdir(parents=True, exist_ok=True)
with open(tmp_path, 'w') as f:
json.dump(self.repositories, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, self.config_path)
self.logger.info(f"Saved {len(self.repositories)} repositories to {self.config_path}")
return True
except Exception as e:
self.logger.error(f"Error saving repositories: {e}")
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
return False
def get_all(self) -> List[Dict[str, str]]:
"""Get all saved repositories."""
return self.repositories.copy()
def add(self, repo_url: str, name: Optional[str] = None) -> bool:
"""
Add a repository to saved list.
Args:
repo_url: GitHub repository URL
name: Optional friendly name for the repository
Returns:
True if added successfully
"""
repo_url = normalize_repo_url(repo_url)
# Check if already exists
for repo in self.repositories:
if repo.get('url') == repo_url:
self.logger.warning(f"Repository already exists: {repo_url}")
return False
# Extract name from URL if not provided
if not name:
parts = repo_url.split('/')
if len(parts) >= 2:
name = parts[-1]
else:
name = repo_url
# Add repository
self.repositories.append({
'url': repo_url,
'name': name,
'type': 'registry' if 'plugins.json' in repo_url or 'ledmatrix-plugins' in repo_url.lower() else 'single'
})
if not self._save_repositories():
# Keep memory consistent with disk: a failed save must not leave
# a phantom entry that only this process can see.
self.repositories.pop()
return False
return True
def remove(self, repo_url: str) -> bool:
"""
Remove a repository from saved list.
Args:
repo_url: GitHub repository URL to remove
Returns:
True if removed successfully
"""
repo_url = normalize_repo_url(repo_url)
previous = self.repositories
remaining = [r for r in previous if r.get('url') != repo_url]
if len(remaining) < len(previous):
self.repositories = remaining
if not self._save_repositories():
# Failed save: restore so memory matches disk.
self.repositories = previous
return False
return True
else:
self.logger.warning(f"Repository not found: {repo_url}")
return False
def has(self, repo_url: str) -> bool:
"""Check if a repository is already saved."""
repo_url = normalize_repo_url(repo_url)
return any(r.get('url') == repo_url for r in self.repositories)
def get_registry_repositories(self) -> List[Dict[str, str]]:
"""Get only registry-style repositories."""
return [r for r in self.repositories if r.get('type') == 'registry']