mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-06 07:15:09 +00:00
* 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>
156 lines
5.2 KiB
Python
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']
|
|
|