from __future__ import annotations
from typing import List, Optional, Dict, TypedDict
import uuid
import humps
from .pitch import Pitch
from ..constants import MIN_FUNDAMENTAL_HZ, MAX_FUNDAMENTAL_HZ
class ChikariOptionsType(TypedDict, total=False):
pitches: List[Pitch] | List[Dict]
fundamental: float
unique_id: str
[docs]
class Chikari:
[docs]
def __init__(self, options: Optional[ChikariOptionsType] = None) -> None:
opts = humps.decamelize(options or {})
# Parameter validation
self._validate_parameters(opts)
default_pitches = [
Pitch({'swara': 's', 'oct': 2}),
Pitch({'swara': 's', 'oct': 1}),
Pitch({'swara': 'p', 'oct': 0}),
Pitch({'swara': 'g', 'oct': 0}),
]
pitches_in = opts.get('pitches', default_pitches)
fundamental = opts.get('fundamental', Pitch().fundamental)
unique_id = opts.get('unique_id')
self.unique_id: str = str(unique_id) if unique_id is not None else str(uuid.uuid4())
self.fundamental: float = fundamental
self.pitches: List[Pitch] = []
for p in pitches_in:
if not isinstance(p, Pitch):
p = Pitch(p) # type: ignore[arg-type]
p.fundamental = self.fundamental
self.pitches.append(p)
def _validate_parameters(self, opts: dict) -> None:
"""Validate constructor parameters and provide helpful error messages."""
if not opts:
return
# Define allowed parameter names
allowed_keys = {'pitches', 'fundamental', 'unique_id'}
provided_keys = set(opts.keys())
invalid_keys = provided_keys - allowed_keys
# Check for invalid parameter names
if invalid_keys:
error_messages = []
for key in invalid_keys:
if key == 'fundamental_freq':
error_messages.append(f"Parameter '{key}' not supported. Did you mean 'fundamental'?")
elif key == 'pitch_list':
error_messages.append(f"Parameter '{key}' not supported. Did you mean 'pitches'?")
else:
error_messages.append(f"Invalid parameter: '{key}'")
error_msg = "; ".join(error_messages)
error_msg += f". Allowed parameters: {sorted(allowed_keys)}"
raise ValueError(error_msg)
# Validate parameter types and values
if 'pitches' in opts and opts['pitches'] is not None:
if not isinstance(opts['pitches'], list):
raise TypeError(f"Parameter 'pitches' must be a list, got {type(opts['pitches']).__name__}")
if 'fundamental' in opts and opts['fundamental'] is not None:
if not isinstance(opts['fundamental'], (int, float)):
raise TypeError(f"Parameter 'fundamental' must be a number, got {type(opts['fundamental']).__name__}")
if opts['fundamental'] <= 0:
raise ValueError(f"Parameter 'fundamental' must be positive, got {opts['fundamental']}")
if opts['fundamental'] < MIN_FUNDAMENTAL_HZ or opts['fundamental'] > MAX_FUNDAMENTAL_HZ:
import warnings
warnings.warn(
f"Fundamental frequency {opts['fundamental']}Hz is outside typical range ({MIN_FUNDAMENTAL_HZ}-{MAX_FUNDAMENTAL_HZ}Hz)",
UserWarning
)
if 'unique_id' in opts and opts['unique_id'] is not None:
if not isinstance(opts['unique_id'], str):
raise TypeError(f"Parameter 'unique_id' must be a string, got {type(opts['unique_id']).__name__}")
# ------------------------------------------------------------------
[docs]
def to_json(self) -> Dict:
return {
'fundamental': self.fundamental,
'uniqueId': self.unique_id,
}
[docs]
@staticmethod
def from_json(obj: Dict) -> 'Chikari':
opts = humps.decamelize(obj)
# Handle old format (with pitches) for backward compatibility
pitches_data = opts.get('pitches')
if pitches_data:
pitches = [Pitch.from_json(p) for p in pitches_data]
opts['pitches'] = pitches
return Chikari(opts) # type: ignore[arg-type]