""" Generic query builder for fluent, chainable access to tarot data. Provides Query and QueryResult classes for building filters and accessing data in a dynamic, expressive way. Usage: # By name result = letter.iching().name('peace') result = letter.alphabet().name('english') # By filter expressions result = letter.iching().filter('number:1') result = letter.alphabet().filter('name:hebrew') result = number.number().filter('value:5') # Get all results results = letter.iching().all() # Dict[int, Hexagram] results = letter.iching().list() # List[Hexagram] """ from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union from utils.object_formatting import format_value, get_object_attributes, is_nested_object T = TypeVar('T') class QueryResult: """Single result from a query.""" def __init__(self, data: Any) -> None: self.data = data def __repr__(self) -> str: if hasattr(self.data, '__repr__'): return repr(self.data) return f"{self.__class__.__name__}({self.data})" def __str__(self) -> str: if hasattr(self.data, '__str__'): return str(self.data) return repr(self) def __getattr__(self, name: str) -> Any: """Pass through attribute access to the wrapped data.""" if name.startswith('_'): raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") return getattr(self.data, name) class Query: """ Fluent query builder for accessing and filtering tarot data. Supports chaining: .filter() → .name() → .get() """ def __init__(self, data: Union[Dict[Any, T], List[T]]) -> None: """Initialize with data source (dict or list).""" self._original_data = data self._data = data if isinstance(data, list) else list(data.values()) self._filters: List[Callable[[T], bool]] = [] def filter(self, expression: str) -> 'Query': """ Filter by key:value expression. Examples: .filter('name:peace') .filter('number:1') .filter('sephera:gevurah') .filter('value:5') Supports multiple filters by chaining: .filter('number:1').filter('name:creative') """ key, value = expression.split(':', 1) if ':' in expression else (expression, '') def filter_func(item: T) -> bool: # Special handling for 'name' key if key == 'name': if hasattr(item, 'name'): value_lower = value.lower() item_name = str(item.name).lower() return value_lower == item_name or value_lower in item_name return False if not hasattr(item, key): return False item_value = getattr(item, key) # Flexible comparison if isinstance(item_value, str): return value.lower() in str(item_value).lower() else: return str(value) in str(item_value) self._filters.append(filter_func) return self def name(self, value: str) -> Optional['QueryResult']: """ Deprecated: Use .filter('name:value') instead. Find item by name (exact or partial match, case-insensitive). Returns QueryResult wrapping the found item, or None if not found. """ return self.filter(f'name:{value}').first() def get(self) -> Optional['QueryResult']: """ Get first result matching all applied filters. Returns QueryResult or None if no match. """ for item in self._data: if all(f(item) for f in self._filters): return QueryResult(item) return None def all(self) -> Dict[Any, T]: """ Get all results matching filters as dict. Returns original dict structure (if input was dict) with filtered values. """ filtered = {} if isinstance(self._original_data, dict): for key, item in self._original_data.items(): if all(f(item) for f in self._filters): filtered[key] = item else: for i, item in enumerate(self._data): if all(f(item) for f in self._filters): filtered[i] = item return filtered def list(self) -> List[T]: """ Get all results matching filters as list. Returns list of filtered items. """ return [item for item in self._data if all(f(item) for f in self._filters)] def first(self) -> Optional['QueryResult']: """Alias for get() - returns first matching item.""" return self.get() def count(self) -> int: """Count items matching all filters.""" return len(self.list()) def __repr__(self) -> str: return f"Query({self.count()} items)" def __str__(self) -> str: items = self.list() if not items: return "Query(0 items)" return f"Query({self.count()} items): {items[0]}" class CollectionAccessor(Generic[T]): """Context-aware accessor that provides a scoped .query() interface.""" def __init__(self, data_provider: Callable[[], Union[Dict[Any, T], List[T]]]) -> None: self._data_provider = data_provider def _new_query(self) -> Query: data = self._data_provider() return Query(data) def query(self) -> Query: """Return a new Query scoped to this collection.""" return self._new_query() def filter(self, expression: str) -> Query: """Shortcut to run a filtered query.""" return self.query().filter(expression) def name(self, value: str) -> Optional[QueryResult]: """Shortcut to look up by name.""" return self.query().name(value) def all(self) -> Dict[Any, T]: """Return all entries (optionally filtered).""" return self.query().all() def list(self) -> List[T]: """Return all entries as a list.""" return self.query().list() def first(self) -> Optional[QueryResult]: """Return first matching entry.""" return self.query().first() def count(self) -> int: """Count entries for this collection.""" return self.query().count() def display(self) -> str: """ Format all entries for user-friendly display with proper indentation. Returns a formatted string with each item separated by blank lines. Nested objects are indented and separated with their own sections. """ from utils.object_formatting import is_nested_object, get_object_attributes data = self.all() if not data: return "(empty collection)" lines = [] for key, item in data.items(): lines.append(f"--- {key} ---") # Format all attributes with proper nesting for attr_name, attr_value in get_object_attributes(item): if is_nested_object(attr_value): lines.append(f" {attr_name}:") lines.append(f" --- {attr_name.replace('_', ' ').title()} ---") nested = format_value(attr_value, indent=4) lines.append(nested) else: lines.append(f" {attr_name}: {attr_value}") lines.append("") # Blank line between items return "\n".join(lines) def __repr__(self) -> str: """Full detailed representation.""" return self.display() def __str__(self) -> str: """Full detailed string representation.""" return self.display() class FilterableDict(dict): """Dict subclass that provides .filter() method for dynamic querying.""" def filter(self, expression: str = '') -> Query: """ Filter dict values by attribute:value expression. Examples: data.filter('name:peace') data.filter('number:1') data.filter('') # Returns query of all items """ return Query(self).filter(expression) if expression else Query(self) def display(self) -> str: """ Format all items in the dict for user-friendly display. Returns a formatted string with each item separated by blank lines. Nested objects are indented and separated with their own sections. """ from utils.object_formatting import is_nested_object, get_object_attributes, format_value if not self: return "(empty collection)" lines = [] for key, item in self.items(): lines.append(f"--- {key} ---") # Format all attributes with proper nesting for attr_name, attr_value in get_object_attributes(item): if is_nested_object(attr_value): lines.append(f" {attr_name}:") lines.append(f" --- {attr_name.replace('_', ' ').title()} ---") nested = format_value(attr_value, indent=4) lines.append(nested) else: lines.append(f" {attr_name}: {attr_value}") lines.append("") # Blank line between items return "\n".join(lines) def make_filterable(data: Union[Dict[Any, T], List[T]]) -> Union['FilterableDict', Query]: """ Convert dict or list to a filterable object with .filter() support. Examples: walls = make_filterable(Cube.wall()) peace = walls.filter('name:North').first() """ if isinstance(data, dict): # Create a FilterableDict from the regular dict filterable = FilterableDict(data) return filterable else: # For lists, wrap in a Query return Query(data)