"""Content-addressed, non-executable catalog compilation. Only validated production XML is compiled here. The cache contains no song, role, candidate, reference, or fitted musical result. A checksum and decoder version protect stale/corrupt payloads; no pickle or executable serialization. """ from __future__ import annotations import dataclasses from fractions import Fraction as F import functools import hashlib import json import os from pathlib import Path import tempfile from generator import rhythm, vocabulary from generator.scales import library as scales from generator.solo import sequences VERSION = 'catalog-json-1' def identity(directory): directory=Path(directory) return {name:vocabulary.fingerprint([(p.relative_to(directory/name).as_posix(), hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted((directory/name).rglob('*.xml'))]) for name in ('scales','sequences','rhythms')} def encode(libraries): scale,sequence,rhythms=libraries return {'scales':[dataclasses.asdict(p) for p in scale.patterns], 'sequences':[dataclasses.asdict(p) for values in sequence.paths.values() for p in values], 'rhythms':[dataclasses.asdict(p) for values in rhythms.snippets.values() for p in values]} def decode(value): scale=scales.Library(tuple(scales.Pattern(p['family'],p['mode'],tuple(p['intervals'])) for p in value['scales'])) sequence=sequences.Library(tuple(sequences.Sequence(p['id'],tuple(p['steps'])) for p in value['sequences'])) snippets=[] for p in value['rhythms']: bars=[] for b in p['bars']: events=tuple(rhythm.Event(F(e['onset']),F(e['duration']),e['sounding'],e['written_type'],e['dots'], tuple(tuple(v) for v in e['beams']),e['continuation'],F(e['ratio'])) for e in b['events']) bars.append(rhythm.Bar(events,F(b['length']))) snippets.append(rhythm.Snippet(p['id'],p['meter'],tuple(bars))) return scale,sequence,rhythm.Library(tuple(snippets)) def load(directory, fingerprints=None, cache_dir=None): directory=Path(directory) fingerprints=fingerprints or identity(directory) # Code edits invalidate compiled importer semantics, but do not reseed music. root=Path(__file__).parent code=[root/n for n in ('resources.py','rhythm.py','musicxml.py','model.py','pitch.py','harmony.py','solo/sequences.py','scales/library.py')] key=vocabulary.fingerprint({'version':VERSION,'inputs':fingerprints, 'decoder':[(p.relative_to(root).as_posix(),hashlib.sha256(p.read_bytes()).hexdigest()) for p in sorted(code)]}) return _load(str(directory.resolve()),key,str(Path(cache_dir).resolve()) if cache_dir else '') @functools.lru_cache(maxsize=4) def _load(directory,key,cache_dir): path=Path(cache_dir)/'_resources'/(key+'.json') if cache_dir else None if path and path.is_file() and not path.is_symlink(): try: entry=json.loads(path.read_text()) if entry['key']==key and vocabulary.fingerprint(entry['value'])==entry['digest']: return decode(entry['value']) except (ValueError,KeyError,TypeError,OverflowError): pass root=Path(directory) libraries=(scales.load_library(root/'scales'),sequences.load_library(root/'sequences'),rhythm.load_library(root/'rhythms')) if path: value=json.loads(json.dumps(encode(libraries),default=str)) path.parent.mkdir(parents=True,exist_ok=True) with tempfile.NamedTemporaryFile('w',dir=path.parent,delete=False) as out: json.dump({'key':key,'digest':vocabulary.fingerprint(value),'value':value},out,separators=(',',':')) temporary=out.name os.replace(temporary,path) return libraries