Files
tarot/src/utils/query.py
nose 79d4f1a09e k
2025-11-25 22:19:36 -08:00

300 lines
9.9 KiB
Python

"""
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)