"""
Registry for curated notable births and events.
Provides access to a compendium of famous births and historical events
that can be used for examples, testing, and research.
"""
import importlib.resources
from pathlib import Path
import yaml
from stellium.core.native import Notable
[docs]
class NotableRegistry:
"""
Registry for curated notable births and events.
This provides access to a compendium of famous births and historical
events that can be used for examples, testing, and research.
Example:
>>> from stellium.data import get_notable_registry
>>> registry = get_notable_registry()
>>> einstein = registry.get_by_name("Albert Einstein")
>>> print(einstein.name, einstein.category)
Albert Einstein scientist
"""
def __init__(self):
self._notables: list[Notable] = []
self._load_all()
def _get_notables_path(self) -> Path | None:
"""
Get the path to the notables data directory.
Uses importlib.resources to find package data, which works both
in development (editable install) and when installed as a package.
Returns:
Path to notables directory, or None if not found
"""
try:
# Use importlib.resources to find package data
files = importlib.resources.files("stellium.data")
notables_path = files / "notables"
# For editable installs, this returns the actual filesystem path
# For installed packages, we may need to use as_file()
if hasattr(notables_path, "_path"):
real_path = Path(notables_path._path)
if real_path.exists():
return real_path
# Try traversable interface
with importlib.resources.as_file(notables_path) as path:
if path.exists():
return path
return None
except (TypeError, FileNotFoundError, AttributeError):
return None
def _load_all(self) -> None:
"""Load all YAML files and create Notable objects."""
data_dir = self._get_notables_path()
if data_dir is None:
# No notable data found - this is OK for minimal installs
return
# Load all YAML files from births/ and events/ subdirectories
for yaml_file in data_dir.rglob("*.yaml"):
try:
with open(yaml_file) as f:
entries = yaml.safe_load(f) or []
except Exception as e:
print(f"Warning: Failed to read {yaml_file}: {e}")
continue
for entry in entries:
# Determine location input format
location_input = self._parse_location(entry)
if location_input is None:
print(
f"Warning: No valid location data in {yaml_file} for {entry.get('name', 'unknown')}"
)
continue
try:
# Create Notable - it calls Native.__init__ internally!
notable = Notable(
name=entry["name"],
event_type=entry["event_type"],
year=entry["year"],
month=entry["month"],
day=entry["day"],
hour=entry["hour"],
minute=entry["minute"],
location_input=location_input, # Native handles it!
category=entry["category"],
subcategories=entry.get("subcategories"),
notable_for=entry.get("notable_for", ""),
astrological_notes=entry.get("astrological_notes", ""),
data_quality=entry.get("data_quality", "C"),
sources=entry.get("sources"),
verified=entry.get("verified", False),
)
self._notables.append(notable)
except Exception as e:
print(
f"Warning: Failed to load notable "
f"'{entry.get('name', 'unknown')}' from {yaml_file.name}: {e}"
)
def _parse_location(self, entry: dict) -> str | tuple[float, float] | dict | None:
"""
Parse location from YAML entry.
Supports multiple formats:
- location: "City, Country" (geocoded)
- latitude/longitude: as tuple
- latitude/longitude/timezone: as tuple (timezone will be found by Native)
"""
# Option 1: location name string
if "location" in entry:
return entry["location"]
# Option 2: lat/long with timezone (BEST - avoids TimezoneFinder)
if "latitude" in entry and "longitude" in entry and "timezone" in entry:
return {
"latitude": entry["latitude"],
"longitude": entry["longitude"],
"name": entry.get("location_name", ""),
"timezone": entry["timezone"],
}
# Option 3: lat/long tuple
if "latitude" in entry and "longitude" in entry:
return (entry["latitude"], entry["longitude"])
# No valid location format
return None
[docs]
def get_by_name(self, name: str) -> Notable | None:
"""
Get notable by name (case-insensitive).
Args:
name: Name of person or event
Returns:
Notable object or None if not found
"""
for notable in self._notables:
if notable.name.lower() == name.lower():
return notable
return None
[docs]
def get_by_category(self, category: str) -> list[Notable]:
"""
Get all notables in a category.
Args:
category: Category name (scientist, artist, leader, etc.)
Returns:
List of Notable objects in that category
"""
return [n for n in self._notables if n.category == category]
[docs]
def get_by_event_type(self, event_type: str) -> list[Notable]:
"""
Get all births or all events.
Args:
event_type: "birth" or "event"
Returns:
List of Notable objects of that type
"""
return [n for n in self._notables if n.event_type == event_type]
[docs]
def get_births(self) -> list[Notable]:
"""Get all birth records."""
return self.get_by_event_type("birth")
[docs]
def get_events(self) -> list[Notable]:
"""Get all event records."""
return self.get_by_event_type("event")
[docs]
def search(self, **filters) -> list[Notable]:
"""
Search with arbitrary filters.
Examples:
>>> registry.search(category="scientist")
>>> registry.search(event_type="birth", verified=True)
>>> registry.search(data_quality="AA")
Args:
**filters: Keyword arguments matching Notable attributes
Returns:
List of Notable objects matching all filters
"""
results = self._notables
for key, value in filters.items():
results = [n for n in results if getattr(n, key, None) == value]
return results
[docs]
def get_all(self) -> list[Notable]:
"""Get all notable data entries."""
return self._notables.copy()
def __len__(self) -> int:
"""Number of notables in registry."""
return len(self._notables)
def __repr__(self) -> str:
births = len(self.get_births())
events = len(self.get_events())
return f"<NotableRegistry: {births} births, {events} events>"
# Singleton instance
_registry: NotableRegistry | None = None
[docs]
def get_notable_registry() -> NotableRegistry:
"""
Get the global notable registry instance.
Returns:
The singleton NotableRegistry instance
Example:
>>> registry = get_notable_registry()
>>> einstein = registry.get_by_name("Albert Einstein")
"""
global _registry
if _registry is None:
_registry = NotableRegistry()
return _registry