from __future__ import annotations import hashlib import importlib.metadata import json from pathlib import Path import shutil import platform import tempfile from PIL import features def renderer_identity(fonts, style) -> dict: root = Path(__file__).resolve().parents[1] versions = {name: importlib.metadata.version(name) for name in ('Pillow', 'fonttools', 'aggdraw')} versions.update({name: features.version(name) for name in features.get_supported()}) sources = [*root.joinpath('engraving').glob('*.py'), root / 'model.py', root / 'harmony.py', root / 'requirements.txt'] return {'fonts_style': fonts.fingerprint(style), 'python': {'implementation': platform.python_implementation(), 'version': platform.python_version()}, 'dependencies': versions, 'sources': {path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(sources)}} def renderer_fingerprint(fonts, style) -> str: return hashlib.sha256(json.dumps(renderer_identity(fonts, style), sort_keys=True).encode()).hexdigest() def renderer_namespace(cache: Path, renderer: str) -> Path: namespace = cache / renderer if namespace.parent.is_symlink() or namespace.is_symlink(): raise ValueError('engraving cache namespaces must not be symlinks') return namespace def score_fingerprint(document: dict, renderer: str) -> str: music = {key: value for key, value in document.items() if key != 'playback'} return hashlib.sha256(json.dumps([renderer, music], sort_keys=True, ensure_ascii=False).encode()).hexdigest() def restore_pages(entry: Path, destination: Path) -> list[dict] | None: from generator.engraving import check_page try: if entry.is_symlink(): return None manifest = entry / 'pages.json' if manifest.is_symlink(): return None pages = json.loads(manifest.read_text(encoding='utf-8')) if not isinstance(pages, list) or not 0 < len(pages) <= 10000: return None destination.mkdir(parents=True, exist_ok=True) for index, page in enumerate(pages, 1): if not isinstance(page, dict) or set(page) != {'path', 'sha256'} or page['path'] != f'{index:04}.png': return None path = entry / page['path'] if path.is_symlink(): return None target = destination / page['path'] shutil.copyfile(path, target) if hashlib.sha256(target.read_bytes()).hexdigest() != page['sha256']: return None check_page(target) return pages except (OSError, ValueError, TypeError): return None def store_pages(entry: Path, destination: Path, pages: list[dict]) -> None: entry.parent.mkdir(parents=True, exist_ok=True) if entry.is_symlink() or entry.is_file(): entry.unlink(missing_ok=True) entry.mkdir(exist_ok=True) with tempfile.TemporaryDirectory(prefix='.engraving-', dir=entry.parent) as temporary: staged = Path(temporary) for page in pages: shutil.copyfile(destination / page['path'], staged / page['path']) (staged / 'pages.json').write_text(json.dumps(pages), encoding='utf-8') # Identical keys have identical pages; publish complete files, then the manifest. for name in [page['path'] for page in pages] + ['pages.json']: target = entry / name if target.is_dir() and not target.is_symlink(): shutil.rmtree(target, ignore_errors=True) (staged / name).replace(target)