"""Versioned production vocabulary. This module never reads audition inputs.""" from __future__ import annotations import contextlib import contextvars import dataclasses import functools import hashlib import json from pathlib import Path from fractions import Fraction as F ROOT = Path(__file__).resolve().parents[1] / 'data' / 'composition' ACTIVE = contextvars.ContextVar('composition_vocabulary', default=None) def load(root=ROOT): # Content, not just the directory, keys memoization. Long-lived individual # role builds must observe edited production data without restarting Python. names = ('roles', 'harmony', 'lead_ins', 'accompaniment') return _load(tuple((name, (Path(root)/(name+'.json')).read_text()) for name in names)) @functools.lru_cache(maxsize=16) def _load(contents): result = {} for name, text in contents: value = json.loads(text) if value.get('version') != 1 or not value.get('provenance') or not value.get('license'): raise ValueError(f'{name}: unsupported or unprovenanced production vocabulary') entries = value.get('cells', value.get('patterns', value.get('colors', value.get('transformations', [])))) if len({v['id'] for v in entries}) != len(entries): raise ValueError(f'{name}: duplicate vocabulary ID') result[name] = value return result @functools.lru_cache(maxsize=1) def _default(): return {'data': load(), 'role': '', 'settings': {}} def current(): return ACTIVE.get() or _default() def data(name): return current()['data'][name] def role(name=None): active = current() name = name or active['role'] return {**active['data']['roles']['roles'].get(name, {}), **active['settings']} @contextlib.contextmanager def using(resources, name, settings=None): token = ACTIVE.set({'data': resources, 'role': name, 'settings': settings or {}}) try: yield finally: ACTIVE.reset(token) def fingerprint(value): return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()