k
35
src/__init__.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
PY-Tarot: Comprehensive Tarot library with hierarchical namespaces.
|
||||
|
||||
Provides four root namespaces for different domains:
|
||||
|
||||
number - Numerology (digital root, colors, Sepheric attributes)
|
||||
letter - Alphabets, ciphers (English, Hebrew, Greek), words, I Ching
|
||||
kaballah - Tree of Life and Cube of Space
|
||||
tarot - Tarot-specific (deck, cards, temporal)
|
||||
|
||||
Quick Start:
|
||||
|
||||
from tarot import number, letter, kaballah, Tarot
|
||||
|
||||
# Number
|
||||
num = number.number(5)
|
||||
root = number.digital_root(256)
|
||||
|
||||
# Letter
|
||||
letter_obj = letter.letter('A')
|
||||
result = letter.word('MAGICK').cipher('english_simple')
|
||||
|
||||
# Kaballah
|
||||
sephera = kaballah.Tree.sephera(1)
|
||||
wall = kaballah.Cube.wall('North')
|
||||
|
||||
# Tarot
|
||||
card = Tarot.deck.card(3)
|
||||
major5 = Tarot.deck.card.major(5)
|
||||
cups2 = Tarot.deck.card.minor.cups(2)
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "PY-Tarot Contributors"
|
||||
__all__ = []
|
||||
22
src/kaballah/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Kaballah namespace - Tree of Life and Cube of Space.
|
||||
|
||||
Provides fluent query interface for:
|
||||
- Tree of Life with Sephiroth and Paths
|
||||
- Cube of Space with walls and areas
|
||||
- Kabbalistic correspondences
|
||||
|
||||
Usage:
|
||||
from tarot import kaballah
|
||||
|
||||
sephera = kaballah.Tree.sephera(1)
|
||||
path = kaballah.Tree.path(11)
|
||||
wall = kaballah.Cube.wall("North")
|
||||
direction = kaballah.Cube.direction("North", "East")
|
||||
"""
|
||||
|
||||
from .tree import Tree
|
||||
from .cube import Cube
|
||||
|
||||
# Export classes for fluent access
|
||||
__all__ = ["Tree", "Cube"]
|
||||
215
src/kaballah/attributes.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Kabbalistic attributes and data structures.
|
||||
|
||||
This module defines attributes specific to the Kabbalah module,
|
||||
including Sephira, Paths, and Tree of Life structures.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
from utils.attributes import (
|
||||
Element,
|
||||
ElementType,
|
||||
Planet,
|
||||
Color,
|
||||
Colorscale,
|
||||
Perfume,
|
||||
God,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Sephera:
|
||||
"""Represents a Sephira on the Tree of Life."""
|
||||
number: int
|
||||
name: str
|
||||
hebrew_name: str
|
||||
meaning: str
|
||||
archangel: str
|
||||
order_of_angels: str
|
||||
mundane_chakra: str
|
||||
element: Optional['ElementType'] = None
|
||||
planetary_ruler: Optional[str] = None
|
||||
tarot_trump: Optional[str] = None
|
||||
colorscale: Optional['Colorscale'] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PeriodicTable:
|
||||
"""Represents a Sephirothic position in Kabbalah with cross-correspondences."""
|
||||
number: int
|
||||
name: str
|
||||
sephera: Optional[Sephera]
|
||||
element: Optional['ElementType'] = None
|
||||
planet: Optional['Planet'] = None
|
||||
color: Optional['Color'] = None
|
||||
tarot_trump: Optional[str] = None
|
||||
hebrew_letter: Optional[str] = None
|
||||
divine_name: Optional[str] = None
|
||||
archangel: Optional[str] = None
|
||||
order_of_angels: Optional[str] = None
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeOfLife:
|
||||
"""Represents the Tree of Life structure."""
|
||||
sephiroth: Dict[int, str]
|
||||
paths: Dict[Tuple[int, int], str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Correspondences:
|
||||
"""Represents Kabbalistic correspondences."""
|
||||
number: int
|
||||
sephira: str
|
||||
element: Optional[str]
|
||||
planet: Optional[str]
|
||||
zodiac: Optional[str]
|
||||
tarot_trump: Optional[str]
|
||||
archangel: Optional[str]
|
||||
order_of_angels: Optional[str]
|
||||
divine_name: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Path:
|
||||
"""Represents one of the 22 Paths on the Tree of Life with full correspondences."""
|
||||
number: int # 11-32
|
||||
hebrew_letter: str # Hebrew letter name (Aleph through Tau)
|
||||
transliteration: str # English transliteration
|
||||
tarot_trump: str # Major Arcana card (0-XXI)
|
||||
sephera_from: Optional['Sephera'] = None # Lower Sephira
|
||||
sephera_to: Optional['Sephera'] = None # Upper Sephira
|
||||
element: Optional['ElementType'] = None # Element (Air, Fire, Water, Earth)
|
||||
planet: Optional['Planet'] = None # Planetary ruler
|
||||
zodiac_sign: Optional[str] = None # Zodiac sign (12 paths only)
|
||||
colorscale: Optional['Colorscale'] = None # Golden Dawn color scales
|
||||
perfumes: List['Perfume'] = field(default_factory=list)
|
||||
gods: Dict[str, List['God']] = field(default_factory=dict)
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 11 <= self.number <= 32:
|
||||
raise ValueError(f"Path number must be between 11 and 32, got {self.number}")
|
||||
|
||||
def is_elemental_path(self) -> bool:
|
||||
"""Check if this is one of the 4 elemental paths."""
|
||||
elemental_numbers = {11, 23, 31, 32} # Aleph, Mem, Shin, 32-bis
|
||||
return self.number in elemental_numbers
|
||||
|
||||
def is_planetary_path(self) -> bool:
|
||||
"""Check if this path has planetary correspondence."""
|
||||
return self.planet is not None
|
||||
|
||||
def is_zodiacal_path(self) -> bool:
|
||||
"""Check if this path has zodiac correspondence."""
|
||||
return self.zodiac_sign is not None
|
||||
|
||||
def add_god(self, god: 'God') -> None:
|
||||
"""Attach a god to this path grouped by culture."""
|
||||
culture_key = god.culture_key()
|
||||
culture_bucket = self.gods.setdefault(culture_key, [])
|
||||
if god not in culture_bucket:
|
||||
culture_bucket.append(god)
|
||||
|
||||
def add_perfume(self, perfume: 'Perfume') -> None:
|
||||
"""Attach a perfume correspondence if it is not already present."""
|
||||
if perfume not in self.perfumes:
|
||||
self.perfumes.append(perfume)
|
||||
|
||||
def get_gods(self, culture: Optional[str] = None) -> List['God']:
|
||||
"""Return all gods for this path, optionally filtered by culture."""
|
||||
if culture:
|
||||
return list(self.gods.get(culture.lower(), []))
|
||||
merged: List['God'] = []
|
||||
for values in self.gods.values():
|
||||
merged.extend(values)
|
||||
return merged
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return nicely formatted string representation of the Path."""
|
||||
lines = []
|
||||
|
||||
# Header with path number and letter
|
||||
lines.append(f"--- Path {self.number}: {self.hebrew_letter} ({self.transliteration}) ---")
|
||||
lines.append("")
|
||||
|
||||
# Basic correspondences
|
||||
lines.append(f"tarot_trump: {self.tarot_trump}")
|
||||
|
||||
# Connections
|
||||
if self.sephera_from or self.sephera_to:
|
||||
seph_from = self.sephera_from.name if self.sephera_from else "Unknown"
|
||||
seph_to = self.sephera_to.name if self.sephera_to else "Unknown"
|
||||
lines.append(f"connects: {seph_from} ↔ {seph_to}")
|
||||
|
||||
# Element
|
||||
if self.element:
|
||||
element_name = self.element.name if hasattr(self.element, 'name') else str(self.element)
|
||||
lines.append(f"element: {element_name}")
|
||||
|
||||
# Planet
|
||||
if self.planet:
|
||||
lines.append("")
|
||||
lines.append("--- Planet ---")
|
||||
for line in str(self.planet).split("\n"):
|
||||
lines.append(f" {line}")
|
||||
|
||||
# Zodiac
|
||||
if self.zodiac_sign:
|
||||
lines.append(f"zodiac_sign: {self.zodiac_sign}")
|
||||
|
||||
# Colorscale
|
||||
if self.colorscale:
|
||||
lines.append("")
|
||||
lines.append("--- Colorscale ---")
|
||||
lines.append(f" name: {self.colorscale.name}")
|
||||
lines.append(f" king_scale: {self.colorscale.king_scale}")
|
||||
lines.append(f" queen_scale: {self.colorscale.queen_scale}")
|
||||
lines.append(f" emperor_scale: {self.colorscale.emperor_scale}")
|
||||
lines.append(f" empress_scale: {self.colorscale.empress_scale}")
|
||||
if self.colorscale.sephirotic_color:
|
||||
lines.append(f" sephirotic_color: {self.colorscale.sephirotic_color}")
|
||||
lines.append(f" type: {self.colorscale.type}")
|
||||
if self.colorscale.keywords:
|
||||
lines.append(f" keywords: {', '.join(self.colorscale.keywords)}")
|
||||
if self.colorscale.description:
|
||||
lines.append(f" description: {self.colorscale.description}")
|
||||
|
||||
# Perfumes
|
||||
if self.perfumes:
|
||||
lines.append("")
|
||||
lines.append("--- Perfumes ---")
|
||||
for perfume in self.perfumes:
|
||||
for line in str(perfume).split("\n"):
|
||||
lines.append(f" {line}")
|
||||
lines.append("")
|
||||
|
||||
# Gods
|
||||
if self.gods:
|
||||
lines.append("")
|
||||
lines.append("--- Gods ---")
|
||||
for culture, god_list in self.gods.items():
|
||||
lines.append(f" {culture}:")
|
||||
for god in god_list:
|
||||
for line in str(god).split("\n"):
|
||||
lines.append(f" {line}")
|
||||
lines.append("")
|
||||
|
||||
# Keywords
|
||||
if self.keywords:
|
||||
lines.append("")
|
||||
lines.append("--- Keywords ---")
|
||||
lines.append(f" {', '.join(self.keywords)}")
|
||||
|
||||
# Description
|
||||
if self.description:
|
||||
lines.append("")
|
||||
lines.append("--- Description ---")
|
||||
lines.append(f" {self.description}")
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
6
src/kaballah/cube/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Cube namespace - access Cube of Space walls and areas."""
|
||||
|
||||
from .cube import Cube
|
||||
from .attributes import CubeOfSpace, Wall, WallDirection
|
||||
|
||||
__all__ = ["Cube", "CubeOfSpace", "Wall", "WallDirection"]
|
||||
513
src/kaballah/cube/attributes.py
Normal file
@@ -0,0 +1,513 @@
|
||||
"""
|
||||
Cube of Space attributes and data structures.
|
||||
|
||||
Defines the CubeOfSpace, Wall, and WallDirection classes for the Cube of Space
|
||||
Kabbalistic model with hierarchical wall and direction structure.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True, repr=False)
|
||||
class WallDirection:
|
||||
"""
|
||||
Represents a single direction within a Wall of the Cube of Space.
|
||||
|
||||
Each wall has 5 directions: North, South, East, West, Center.
|
||||
Each direction has a Hebrew letter and zodiac correspondence.
|
||||
"""
|
||||
name: str # "North", "South", "East", "West", "Center"
|
||||
letter: str # Hebrew letter (e.g., "Aleph", "Bet", etc.)
|
||||
zodiac: Optional[str] = None # Zodiac sign if applicable
|
||||
element: Optional[str] = None # Associated element if any
|
||||
planet: Optional[str] = None # Associated planet if any
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
VALID_DIRECTION_NAMES = {"North", "South", "East", "West", "Center"}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.name not in self.VALID_DIRECTION_NAMES:
|
||||
raise ValueError(
|
||||
f"Invalid direction name '{self.name}'. "
|
||||
f"Valid names: {', '.join(sorted(self.VALID_DIRECTION_NAMES))}"
|
||||
)
|
||||
if not self.letter or not isinstance(self.letter, str):
|
||||
raise ValueError(f"Direction must have a letter, got {self.letter}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Custom repr showing key attributes."""
|
||||
return f"WallDirection({self.name}, {self.letter})"
|
||||
|
||||
|
||||
@dataclass(frozen=True, repr=False)
|
||||
class Wall:
|
||||
"""
|
||||
Represents one of the 6 walls of the Cube of Space.
|
||||
|
||||
Each wall has 5 directions: North, South, East, West, Center.
|
||||
The 6 walls are: North, South, East, West, Above, Below.
|
||||
Opposite walls: North↔South, East↔West, Above↔Below.
|
||||
Each direction has a Hebrew letter and zodiac correspondence.
|
||||
"""
|
||||
name: str # "North", "South", "East", "West", "Above", "Below"
|
||||
side: str # Alias for name, used for filtering (e.g., "north", "south")
|
||||
opposite: str # Opposite wall name (e.g., "South" for North wall)
|
||||
element: Optional[str] = None # Associated element
|
||||
planet: Optional[str] = None # Associated planet
|
||||
archangel: Optional[str] = None # Associated archangel
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
directions: Dict[str, "WallDirection"] = field(default_factory=dict)
|
||||
|
||||
VALID_WALL_NAMES = {"North", "South", "East", "West", "Above", "Below"}
|
||||
|
||||
# Opposite wall mappings
|
||||
OPPOSITE_WALLS = {
|
||||
"North": "South",
|
||||
"South": "North",
|
||||
"East": "West",
|
||||
"West": "East",
|
||||
"Above": "Below",
|
||||
"Below": "Above",
|
||||
}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.name not in self.VALID_WALL_NAMES:
|
||||
raise ValueError(
|
||||
f"Invalid wall name '{self.name}'. "
|
||||
f"Valid walls: {', '.join(sorted(self.VALID_WALL_NAMES))}"
|
||||
)
|
||||
|
||||
# Validate side matches name (case-insensitive)
|
||||
if self.side.capitalize() != self.name:
|
||||
raise ValueError(
|
||||
f"Wall side '{self.side}' must match name '{self.name}'"
|
||||
)
|
||||
|
||||
# Validate opposite wall
|
||||
expected_opposite = self.OPPOSITE_WALLS.get(self.name)
|
||||
if self.opposite != expected_opposite:
|
||||
raise ValueError(
|
||||
f"Wall '{self.name}' must have opposite '{expected_opposite}', got '{self.opposite}'"
|
||||
)
|
||||
|
||||
# Ensure all 5 directions exist
|
||||
if len(self.directions) != 5:
|
||||
raise ValueError(
|
||||
f"Wall '{self.name}' must have exactly 5 directions (North, South, East, West, Center), "
|
||||
f"got {len(self.directions)}"
|
||||
)
|
||||
|
||||
required_directions = {"North", "South", "East", "West", "Center"}
|
||||
if set(self.directions.keys()) != required_directions:
|
||||
raise ValueError(
|
||||
f"Wall '{self.name}' must have directions: {required_directions}, "
|
||||
f"got {set(self.directions.keys())}"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Custom repr showing wall name and element."""
|
||||
return f"Wall({self.name}, {self.element})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Custom string representation for printing wall details with recursive direction details."""
|
||||
keywords_str = ", ".join(self.keywords) if self.keywords else "None"
|
||||
lines = [
|
||||
f"Wall: {self.name}",
|
||||
f" Side: {self.side}",
|
||||
f" Opposite: {self.opposite}",
|
||||
f" Element: {self.element}",
|
||||
f" Planet: {self.planet}",
|
||||
f" Archangel: {self.archangel}",
|
||||
f" Keywords: {keywords_str}",
|
||||
]
|
||||
|
||||
# Add directions with their details recursively
|
||||
if self.directions:
|
||||
lines.append(" Directions:")
|
||||
# Order: Center, North, South, East, West
|
||||
direction_order = ["Center", "North", "South", "East", "West"]
|
||||
for dir_name in direction_order:
|
||||
if dir_name in self.directions:
|
||||
direction = self.directions[dir_name]
|
||||
lines.append(f" --- {direction.name} ---")
|
||||
lines.append(f" Letter: {direction.letter}")
|
||||
if direction.zodiac:
|
||||
lines.append(f" Zodiac: {direction.zodiac}")
|
||||
if direction.element:
|
||||
lines.append(f" Element: {direction.element}")
|
||||
if direction.planet:
|
||||
lines.append(f" Planet: {direction.planet}")
|
||||
if direction.keywords:
|
||||
keywords = ", ".join(direction.keywords)
|
||||
lines.append(f" Keywords: {keywords}")
|
||||
if direction.description:
|
||||
lines.append(f" Description: {direction.description}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def direction(self, direction_name: str) -> Optional["WallDirection"]:
|
||||
"""Get a specific direction by name. Usage: wall.direction("North")"""
|
||||
return self.directions.get(direction_name.capitalize())
|
||||
|
||||
def all_directions(self) -> list:
|
||||
"""Return all 5 directions as a list."""
|
||||
return list(self.directions.values())
|
||||
|
||||
# Aliases for backward compatibility
|
||||
def get_direction(self, direction_name: str) -> Optional["WallDirection"]:
|
||||
"""Deprecated: use direction() instead."""
|
||||
return self.direction(direction_name)
|
||||
|
||||
def get_opposite_wall_name(self) -> str:
|
||||
"""Deprecated: use the opposite property instead."""
|
||||
return self.opposite
|
||||
|
||||
|
||||
@dataclass
|
||||
class CubeOfSpace:
|
||||
"""
|
||||
Represents the Cube of Space with 6 walls.
|
||||
|
||||
The Cube of Space is a 3D sacred geometry model consisting of:
|
||||
- 6 walls (North, South, East, West, Above, Below)
|
||||
- Each wall contains 5 areas (center, above, below, east, west)
|
||||
- Opposite walls: North↔South, East↔West, Above↔Below
|
||||
- Total: 30 positions plus central core
|
||||
"""
|
||||
walls: Dict[str, Wall] = field(default_factory=dict)
|
||||
center: Optional[WallDirection] = None # Central core position
|
||||
|
||||
# Built-in wall definitions with all correspondences
|
||||
_WALL_DEFINITIONS = {
|
||||
"North": {
|
||||
"element": "Air",
|
||||
"planet": "Mercury",
|
||||
"archangel": "Raphael",
|
||||
"keywords": ["Thought", "Communication", "Intellect"],
|
||||
"description": "Northern Wall - Air element, Mercury correspondence",
|
||||
"areas": {
|
||||
"center": {
|
||||
"element": "Spirit",
|
||||
"keywords": ["Integration", "Balance", "Foundation"],
|
||||
"description": "Center of North wall - synthesis of thought",
|
||||
},
|
||||
"above": {
|
||||
"element": "Fire",
|
||||
"keywords": ["Higher Mind", "Spiritual Thought", "Ascent"],
|
||||
"description": "Above area - elevated intellectual consciousness",
|
||||
},
|
||||
"below": {
|
||||
"element": "Earth",
|
||||
"keywords": ["Practical Thought", "Grounded Mind", "Implementation"],
|
||||
"description": "Below area - material manifestation of thought",
|
||||
},
|
||||
"east": {
|
||||
"element": "Air",
|
||||
"keywords": ["Clarity", "Awakening", "Dawn Thought"],
|
||||
"description": "East area - morning mind, new perspectives",
|
||||
},
|
||||
"west": {
|
||||
"element": "Water",
|
||||
"keywords": ["Emotional Thought", "Reflection", "Dreams"],
|
||||
"description": "West area - intuitive mind, introspection",
|
||||
},
|
||||
},
|
||||
},
|
||||
"South": {
|
||||
"element": "Fire",
|
||||
"planet": "Mars",
|
||||
"archangel": "Samael",
|
||||
"keywords": ["Will", "Action", "Passion"],
|
||||
"description": "Southern Wall - Fire element, Mars correspondence",
|
||||
"areas": {
|
||||
"center": {
|
||||
"element": "Spirit",
|
||||
"keywords": ["Pure Will", "Center of Power", "Drive"],
|
||||
"description": "Center of South wall - focal point of action",
|
||||
},
|
||||
"above": {
|
||||
"element": "Fire",
|
||||
"keywords": ["Divine Will", "Higher Purpose", "Spiritual Force"],
|
||||
"description": "Above area - transcendent power and courage",
|
||||
},
|
||||
"below": {
|
||||
"element": "Earth",
|
||||
"keywords": ["Physical Action", "Embodied Will", "Manifestation"],
|
||||
"description": "Below area - action in material world",
|
||||
},
|
||||
"east": {
|
||||
"element": "Air",
|
||||
"keywords": ["Active Mind", "Strategic Will", "Beginning Action"],
|
||||
"description": "East area - dawn of new endeavors",
|
||||
},
|
||||
"west": {
|
||||
"element": "Water",
|
||||
"keywords": ["Passionate Emotion", "Emotional Drive", "Desire"],
|
||||
"description": "West area - feeling-guided action",
|
||||
},
|
||||
},
|
||||
},
|
||||
"East": {
|
||||
"element": "Air",
|
||||
"planet": "Venus",
|
||||
"archangel": "Haniel",
|
||||
"keywords": ["Dawn", "Beginning", "Ascent"],
|
||||
"description": "Eastern Wall - Air element, new beginnings",
|
||||
"areas": {
|
||||
"center": {
|
||||
"element": "Spirit",
|
||||
"keywords": ["New Potential", "Morning Star", "Awakening"],
|
||||
"description": "Center of East wall - point of emergence",
|
||||
},
|
||||
"above": {
|
||||
"element": "Fire",
|
||||
"keywords": ["Spiritual Dawn", "Divine Light", "Inspiration"],
|
||||
"description": "Above area - celestial promise",
|
||||
},
|
||||
"below": {
|
||||
"element": "Earth",
|
||||
"keywords": ["Material Growth", "Physical Sunrise", "Earthly Beginning"],
|
||||
"description": "Below area - manifestation of new potential",
|
||||
},
|
||||
"east": {
|
||||
"element": "Air",
|
||||
"keywords": ["Pure Beginning", "First Breath", "Clarity"],
|
||||
"description": "East area - absolute dawn principle",
|
||||
},
|
||||
"west": {
|
||||
"element": "Water",
|
||||
"keywords": ["Emotional Renewal", "Feelings Awakening", "Hope"],
|
||||
"description": "West area - emotional opening toward new day",
|
||||
},
|
||||
},
|
||||
},
|
||||
"West": {
|
||||
"element": "Water",
|
||||
"planet": "Venus",
|
||||
"archangel": "Uriel",
|
||||
"keywords": ["Emotion", "Decline", "Closure"],
|
||||
"description": "Western Wall - Water element, endings and emotions",
|
||||
"areas": {
|
||||
"center": {
|
||||
"element": "Spirit",
|
||||
"keywords": ["Emotional Core", "Sunset Synthesis", "Integration"],
|
||||
"description": "Center of West wall - emotional balance point",
|
||||
},
|
||||
"above": {
|
||||
"element": "Fire",
|
||||
"keywords": ["Spiritual Emotion", "Divine Love", "Transcendence"],
|
||||
"description": "Above area - love beyond form",
|
||||
},
|
||||
"below": {
|
||||
"element": "Earth",
|
||||
"keywords": ["Physical Emotion", "Embodied Feeling", "Sensuality"],
|
||||
"description": "Below area - emotion in material form",
|
||||
},
|
||||
"east": {
|
||||
"element": "Air",
|
||||
"keywords": ["Mental Emotion", "Thoughts Felt", "Understanding Feeling"],
|
||||
"description": "East area - intellectual understanding of emotion",
|
||||
},
|
||||
"west": {
|
||||
"element": "Water",
|
||||
"keywords": ["Pure Emotion", "Deep Feeling", "Subconscious"],
|
||||
"description": "West area - pure emotional depths",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Above": {
|
||||
"element": "Fire",
|
||||
"planet": "Sun",
|
||||
"archangel": "Michael",
|
||||
"keywords": ["Heaven", "Spirit", "Light"],
|
||||
"description": "Upper Wall - Fire element, divine consciousness",
|
||||
"areas": {
|
||||
"center": {
|
||||
"element": "Spirit",
|
||||
"keywords": ["Divine Center", "Oneness", "Source"],
|
||||
"description": "Center of Above wall - point of divine unity",
|
||||
},
|
||||
"above": {
|
||||
"element": "Fire",
|
||||
"keywords": ["Pure Spirit", "Infinite Light", "Transcendence"],
|
||||
"description": "Above area - highest divine realm",
|
||||
},
|
||||
"below": {
|
||||
"element": "Earth",
|
||||
"keywords": ["Spirit in Matter", "Divine Manifestation", "Incarnation"],
|
||||
"description": "Below area - spirit descending to form",
|
||||
},
|
||||
"east": {
|
||||
"element": "Air",
|
||||
"keywords": ["Divine Thought", "Holy Wisdom", "Inspiration"],
|
||||
"description": "East area - divine intellect and inspiration",
|
||||
},
|
||||
"west": {
|
||||
"element": "Water",
|
||||
"keywords": ["Divine Love", "Compassion", "Mercy"],
|
||||
"description": "West area - divine love and compassion",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Below": {
|
||||
"element": "Earth",
|
||||
"planet": "Saturn",
|
||||
"archangel": "Cassiel",
|
||||
"keywords": ["Matter", "Foundation", "Manifestation"],
|
||||
"description": "Lower Wall - Earth element, material foundation",
|
||||
"areas": {
|
||||
"center": {
|
||||
"element": "Spirit",
|
||||
"keywords": ["Material Foundation", "Grounding", "Embodiment"],
|
||||
"description": "Center of Below wall - anchor point of manifestation",
|
||||
},
|
||||
"above": {
|
||||
"element": "Fire",
|
||||
"keywords": ["Active Force", "Will to Build", "Transformation"],
|
||||
"description": "Above area - active principle in matter",
|
||||
},
|
||||
"below": {
|
||||
"element": "Earth",
|
||||
"keywords": ["Pure Earth", "Deep Matter", "Grounding Root"],
|
||||
"description": "Below area - deepest material foundation",
|
||||
},
|
||||
"east": {
|
||||
"element": "Air",
|
||||
"keywords": ["Material Structure", "Physical Form", "Manifestation"],
|
||||
"description": "East area - form emerging into manifestation",
|
||||
},
|
||||
"west": {
|
||||
"element": "Water",
|
||||
"keywords": ["Nurturing Matter", "Fertile Ground", "Sustenance"],
|
||||
"description": "West area - material nourishment and growth",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate that all 6 walls are present."""
|
||||
required_walls = {"North", "South", "East", "West", "Above", "Below"}
|
||||
if set(self.walls.keys()) != required_walls:
|
||||
raise ValueError(
|
||||
f"CubeOfSpace must have all 6 walls, got: {set(self.walls.keys())}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_default(cls) -> "CubeOfSpace":
|
||||
"""
|
||||
Create a CubeOfSpace with all 6 walls fully populated with built-in definitions.
|
||||
|
||||
Each wall has 5 directions (North, South, East, West, Center) positioned on that wall.
|
||||
Each direction has a Hebrew letter and optional zodiac correspondence.
|
||||
|
||||
Returns:
|
||||
CubeOfSpace: Fully initialized cube with all walls and directions
|
||||
"""
|
||||
walls = {}
|
||||
|
||||
# Direction name mapping - same 5 directions on every wall
|
||||
# Maps old area names to consistent direction names
|
||||
direction_map = {
|
||||
"center": {"name": "Center", "letter": "Aleph", "zodiac": None},
|
||||
"above": {"name": "North", "letter": "Bet", "zodiac": None},
|
||||
"below": {"name": "South", "letter": "Gimel", "zodiac": None},
|
||||
"east": {"name": "East", "letter": "Daleth", "zodiac": "Aries"},
|
||||
"west": {"name": "West", "letter": "He", "zodiac": "Pisces"}
|
||||
}
|
||||
|
||||
for wall_name, wall_data in cls._WALL_DEFINITIONS.items():
|
||||
# Create directions for this wall
|
||||
# Each wall has the same 5 directions: North, South, East, West, Center
|
||||
directions = {}
|
||||
for old_name, direction_config in direction_map.items():
|
||||
if old_name in wall_data["areas"]:
|
||||
direction_data = wall_data["areas"][old_name]
|
||||
direction = WallDirection(
|
||||
name=direction_config["name"],
|
||||
letter=direction_config["letter"],
|
||||
zodiac=direction_config.get("zodiac"),
|
||||
element=direction_data.get("element"),
|
||||
keywords=direction_data.get("keywords", []),
|
||||
description=direction_data.get("description", ""),
|
||||
)
|
||||
# Use the direction name as key so every wall has North, South, East, West, Center
|
||||
directions[direction_config["name"]] = direction
|
||||
|
||||
# Create the wall
|
||||
wall = Wall(
|
||||
name=wall_name,
|
||||
side=wall_name.lower(),
|
||||
opposite=Wall.OPPOSITE_WALLS[wall_name],
|
||||
element=wall_data.get("element"),
|
||||
planet=wall_data.get("planet"),
|
||||
archangel=wall_data.get("archangel"),
|
||||
keywords=wall_data.get("keywords", []),
|
||||
description=wall_data.get("description", ""),
|
||||
directions=directions,
|
||||
)
|
||||
walls[wall_name] = wall
|
||||
|
||||
# Create central core
|
||||
central_core = WallDirection(
|
||||
name="Center",
|
||||
letter="Aleph",
|
||||
element="Spirit",
|
||||
keywords=["Unity", "Source", "All"],
|
||||
description="Central core of the Cube of Space - synthesis of all forces",
|
||||
)
|
||||
|
||||
return cls(walls=walls, center=central_core)
|
||||
|
||||
def wall(self, wall_name: str) -> Optional[Wall]:
|
||||
"""Get a wall by name. Usage: cube.wall("north")"""
|
||||
return self.walls.get(wall_name)
|
||||
|
||||
def opposite(self, wall_name: str) -> Optional[Wall]:
|
||||
"""Get the opposite wall. Usage: cube.opposite("north")"""
|
||||
opposite_name = Wall.OPPOSITE_WALLS.get(wall_name)
|
||||
if not opposite_name:
|
||||
return None
|
||||
return self.walls.get(opposite_name)
|
||||
|
||||
def direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
|
||||
"""Get a specific direction from a specific wall. Usage: cube.direction("north", "center")"""
|
||||
wall = self.wall(wall_name)
|
||||
if not wall:
|
||||
return None
|
||||
return wall.direction(direction_name)
|
||||
|
||||
def walls_all(self) -> List[Wall]:
|
||||
"""Return all 6 walls as a list."""
|
||||
return list(self.walls.values())
|
||||
|
||||
def directions(self, wall_name: str) -> list:
|
||||
"""Return all 5 directions for a specific wall. Usage: cube.directions("north")"""
|
||||
wall = self.wall(wall_name)
|
||||
if not wall:
|
||||
return []
|
||||
return wall.all_directions()
|
||||
|
||||
# Aliases for backward compatibility
|
||||
def get_wall(self, wall_name: str) -> Optional[Wall]:
|
||||
"""Deprecated: use wall() instead."""
|
||||
return self.wall(wall_name)
|
||||
|
||||
def get_direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
|
||||
"""Deprecated: use direction() instead."""
|
||||
return self.direction(wall_name, direction_name)
|
||||
|
||||
def get_opposite_wall(self, wall_name: str) -> Optional[Wall]:
|
||||
"""Deprecated: use opposite() instead."""
|
||||
return self.opposite(wall_name)
|
||||
|
||||
def all_walls(self) -> List[Wall]:
|
||||
"""Deprecated: use walls_all() instead."""
|
||||
return self.walls_all()
|
||||
|
||||
def all_directions_for_wall(self, wall_name: str) -> list:
|
||||
"""Deprecated: use directions() instead."""
|
||||
return self.directions(wall_name)
|
||||
359
src/kaballah/cube/cube.py
Normal file
@@ -0,0 +1,359 @@
|
||||
"""
|
||||
Tarot Cube of Space module.
|
||||
|
||||
Provides hierarchical access to Cube > Wall > Direction structure.
|
||||
|
||||
Usage:
|
||||
from tarot.cube import Cube
|
||||
|
||||
# Access walls
|
||||
Tarot.cube.wall("North") # Get specific wall
|
||||
Tarot.cube.wall().filter(element="Air") # Filter all walls
|
||||
|
||||
# Access directions (NEW - replaces old "area" concept)
|
||||
wall = Tarot.cube.wall("North")
|
||||
wall.filter("East") # Filter by direction
|
||||
wall.filter(element="Fire") # Filter by attribute
|
||||
wall.direction("East") # Get specific direction
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
|
||||
|
||||
class CubeMeta(type):
|
||||
"""Metaclass to add __str__ to Cube class itself."""
|
||||
|
||||
def __str__(cls) -> str:
|
||||
"""Return readable representation when Cube is converted to string."""
|
||||
cls._ensure_initialized()
|
||||
if cls._cube is None:
|
||||
return "Cube of Space (not initialized)"
|
||||
|
||||
walls = cls._cube.walls if hasattr(cls._cube, 'walls') else {}
|
||||
lines = [
|
||||
"Cube of Space",
|
||||
"=" * 60,
|
||||
f"Walls: {len(walls)} (North, South, East, West, Above, Below)",
|
||||
"",
|
||||
"Structure:",
|
||||
]
|
||||
|
||||
# Show walls with their elements and areas
|
||||
for wall_name in ["North", "South", "East", "West", "Above", "Below"]:
|
||||
wall = walls.get(wall_name)
|
||||
if wall:
|
||||
element = f" [{wall.element}]" if hasattr(wall, 'element') else ""
|
||||
areas = len(wall.directions) if hasattr(wall, 'directions') else 0
|
||||
lines.append(f" {wall_name}{element}: {areas} areas")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def __repr__(cls) -> str:
|
||||
"""Return object representation."""
|
||||
cls._ensure_initialized()
|
||||
if cls._cube is None:
|
||||
return "Cube(not initialized)"
|
||||
walls = cls._cube.walls if hasattr(cls._cube, 'walls') else {}
|
||||
return f"Cube(walls={len(walls)})"
|
||||
|
||||
|
||||
class DirectionAccessor:
|
||||
"""Fluent accessor for filtering and accessing directions within a specific wall."""
|
||||
|
||||
_wall: Optional[Any] = None
|
||||
|
||||
def __init__(self, wall: Any):
|
||||
"""Initialize with a Wall object."""
|
||||
self._wall = wall
|
||||
|
||||
def all(self) -> list:
|
||||
"""Get all directions in this wall."""
|
||||
if self._wall is None or not hasattr(self._wall, 'directions'):
|
||||
return []
|
||||
return list(self._wall.directions.values())
|
||||
|
||||
def filter(self, direction_name: Optional[str] = None, **kwargs) -> list:
|
||||
"""
|
||||
Filter directions in this wall by name or any WallDirection attribute.
|
||||
|
||||
Args:
|
||||
direction_name: Specific direction (North, South, East, West, Center)
|
||||
**kwargs: Any WallDirection attribute
|
||||
|
||||
Returns:
|
||||
List of WallDirection objects matching filters
|
||||
"""
|
||||
from utils.filter import universal_filter
|
||||
|
||||
all_dirs = self.all()
|
||||
|
||||
# Filter by direction name if provided
|
||||
if direction_name:
|
||||
all_dirs = [
|
||||
d for d in all_dirs
|
||||
if d.name.lower() == direction_name.lower()
|
||||
]
|
||||
|
||||
# Apply other filters
|
||||
if kwargs:
|
||||
all_dirs = universal_filter(all_dirs, **kwargs)
|
||||
|
||||
return all_dirs
|
||||
|
||||
def display(self) -> str:
|
||||
"""Display all directions in this wall formatted."""
|
||||
from utils.filter import format_results
|
||||
|
||||
return format_results(self.all())
|
||||
|
||||
def display_filter(self, direction_name: Optional[str] = None, **kwargs) -> str:
|
||||
"""
|
||||
Filter directions and display results nicely formatted.
|
||||
|
||||
Args:
|
||||
direction_name: Direction name to filter
|
||||
**kwargs: Any WallDirection attribute
|
||||
|
||||
Example:
|
||||
print(wall.display_filter("East"))
|
||||
"""
|
||||
from utils.filter import format_results
|
||||
|
||||
results = self.filter(direction_name, **kwargs)
|
||||
return format_results(results)
|
||||
|
||||
def __call__(self, direction_name: Optional[str] = None) -> Optional[Any]:
|
||||
"""Get specific direction by name."""
|
||||
if direction_name is None:
|
||||
return self.all()
|
||||
if self._wall is None or not hasattr(self._wall, 'directions'):
|
||||
return None
|
||||
return self._wall.directions.get(direction_name.capitalize())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return friendly representation."""
|
||||
directions = self.all()
|
||||
dir_names = ", ".join([d.name for d in directions])
|
||||
wall_name = self._wall.name if self._wall else "Unknown"
|
||||
return f"DirectionAccessor({wall_name}: {dir_names})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return formatted string of all directions."""
|
||||
return self.display()
|
||||
|
||||
|
||||
class WallWrapper:
|
||||
"""Wraps a Wall object to add DirectionAccessor for hierarchical access."""
|
||||
|
||||
def __init__(self, wall: Any):
|
||||
"""Initialize with a Wall object."""
|
||||
self._wall = wall
|
||||
self._direction_accessor = DirectionAccessor(wall)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Delegate attribute access to the wrapped wall."""
|
||||
if name in ('_wall', '_direction_accessor'):
|
||||
return object.__getattribute__(self, name)
|
||||
return getattr(self._wall, name)
|
||||
|
||||
def filter(self, direction_name: Optional[str] = None, **kwargs) -> list:
|
||||
"""
|
||||
Filter directions in this wall.
|
||||
|
||||
Usage:
|
||||
wall.filter("East") # Get East direction
|
||||
wall.filter(element="Fire") # Get Fire directions
|
||||
"""
|
||||
return self._direction_accessor.filter(direction_name, **kwargs)
|
||||
|
||||
def display_filter(self, direction_name: Optional[str] = None, **kwargs) -> str:
|
||||
"""
|
||||
Filter directions and display results nicely formatted.
|
||||
|
||||
Usage:
|
||||
wall.display_filter("East")
|
||||
wall.display_filter(element="Fire")
|
||||
"""
|
||||
return self._direction_accessor.display_filter(direction_name, **kwargs)
|
||||
|
||||
def direction(self, direction_name: str) -> Optional[Any]:
|
||||
"""Get a specific direction."""
|
||||
return self._direction_accessor(direction_name)
|
||||
|
||||
def all_directions(self) -> list:
|
||||
"""Get all directions in this wall."""
|
||||
return self._direction_accessor.all()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return friendly representation."""
|
||||
return f"Wall({self._wall.name}, {self._wall.element})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return formatted string of wall details."""
|
||||
wall = self._wall
|
||||
lines = [
|
||||
f"--- {wall.name} ---",
|
||||
f" name: {wall.name}",
|
||||
f" side: {wall.side}",
|
||||
f" element: {wall.element}",
|
||||
f" planet: {wall.planet}",
|
||||
f" opposite: {wall.opposite}",
|
||||
f" archangel: {wall.archangel}",
|
||||
f" keywords: {', '.join(wall.keywords) if wall.keywords else 'None'}",
|
||||
f" description: {wall.description}",
|
||||
f" directions: {', '.join(wall.directions.keys())}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class WallAccessor:
|
||||
"""Fluent accessor for filtering and accessing Cube walls."""
|
||||
|
||||
_cube: Optional["CubeOfSpace"] = None # type: ignore
|
||||
_initialized: bool = False
|
||||
|
||||
@classmethod
|
||||
def _ensure_initialized(cls) -> None:
|
||||
"""Lazy-initialize the Cube on first access."""
|
||||
if cls._initialized:
|
||||
return
|
||||
from kaballah.cube.attributes import CubeOfSpace
|
||||
|
||||
WallAccessor._cube = CubeOfSpace.create_default()
|
||||
WallAccessor._initialized = True
|
||||
|
||||
def all(self) -> list:
|
||||
"""Get all walls."""
|
||||
self._ensure_initialized()
|
||||
if WallAccessor._cube is None:
|
||||
return []
|
||||
return WallAccessor._cube.all_walls()
|
||||
|
||||
def filter(self, **kwargs) -> list:
|
||||
"""
|
||||
Filter walls by any Wall attribute.
|
||||
|
||||
Uses the universal filter for consistency across the project.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Wall attribute with its value
|
||||
|
||||
Usage:
|
||||
Cube.wall.filter(element="Air")
|
||||
Cube.wall.filter(planet="Mercury")
|
||||
|
||||
Returns:
|
||||
List of Wall objects matching all filters
|
||||
"""
|
||||
from utils.filter import universal_filter
|
||||
|
||||
return universal_filter(self.all(), **kwargs)
|
||||
|
||||
def display(self) -> str:
|
||||
"""Display all walls formatted."""
|
||||
from utils.filter import format_results
|
||||
|
||||
return format_results(self.all())
|
||||
|
||||
def display_filter(self, **kwargs) -> str:
|
||||
"""
|
||||
Filter walls and display results nicely formatted.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Wall attribute with its value
|
||||
|
||||
Returns:
|
||||
Formatted string with filtered walls
|
||||
|
||||
Example:
|
||||
print(Cube.wall.display_filter(element="Air"))
|
||||
"""
|
||||
results = self.filter(**kwargs)
|
||||
# Use the custom __str__ method of Wall objects for proper line-by-line formatting
|
||||
return "\n\n".join([str(wall) for wall in results])
|
||||
|
||||
def __call__(self, wall_name: Optional[str] = None) -> Optional[Any]:
|
||||
"""Get a specific wall by name or return all walls.
|
||||
|
||||
Deprecated: Use filter(side="north") instead.
|
||||
"""
|
||||
self._ensure_initialized()
|
||||
|
||||
if wall_name is None:
|
||||
return self.all()
|
||||
|
||||
if WallAccessor._cube is None:
|
||||
return None
|
||||
return WallAccessor._cube.wall(wall_name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return friendly representation showing all walls."""
|
||||
walls = self.all()
|
||||
wall_names = ", ".join([w.name for w in walls])
|
||||
return f"WallAccessor({wall_names})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return formatted string of all walls."""
|
||||
return self.display()
|
||||
|
||||
|
||||
class Cube(metaclass=CubeMeta):
|
||||
"""
|
||||
Unified accessor for Cube of Space correspondences.
|
||||
|
||||
Hierarchical structure: Cube > Wall > Direction
|
||||
|
||||
Usage:
|
||||
# Filter walls by side
|
||||
north = Cube.wall.filter(side="north")[0] # Get north wall
|
||||
air_walls = Cube.wall.filter(element="Air") # Filter by element
|
||||
|
||||
# Access all walls
|
||||
all_walls = Cube.wall.all() # Get all 6 walls
|
||||
|
||||
# Work with directions within a wall
|
||||
wall = Cube.wall.filter(side="north")[0]
|
||||
east_dir = wall.direction("East") # Get direction
|
||||
fire_dirs = wall.filter(element="Fire") # Filter directions
|
||||
"""
|
||||
|
||||
_cube: Optional["CubeOfSpace"] = None # type: ignore
|
||||
_initialized: bool = False
|
||||
_wall_accessor: Optional[WallAccessor] = None
|
||||
|
||||
@classmethod
|
||||
def _ensure_initialized(cls) -> None:
|
||||
"""Lazy-initialize the Cube of Space on first access."""
|
||||
if cls._initialized:
|
||||
return
|
||||
|
||||
from kaballah.cube.attributes import CubeOfSpace
|
||||
|
||||
cls._cube = CubeOfSpace.create_default()
|
||||
cls._initialized = True
|
||||
|
||||
@classmethod
|
||||
def _get_wall_accessor(cls) -> "WallAccessor":
|
||||
"""Get or create the wall accessor."""
|
||||
cls._ensure_initialized()
|
||||
if cls._wall_accessor is None:
|
||||
cls._wall_accessor = WallAccessor()
|
||||
return cls._wall_accessor
|
||||
|
||||
# Use a descriptor to make wall work like a property on the class
|
||||
class WallProperty:
|
||||
"""Descriptor that returns wall accessor when accessed."""
|
||||
def __get__(self, obj: Any, objtype: Optional[type] = None) -> "WallAccessor":
|
||||
if objtype is None:
|
||||
objtype = type(obj)
|
||||
return objtype._get_wall_accessor()
|
||||
|
||||
wall = WallProperty()
|
||||
|
||||
@classmethod
|
||||
def opposite_wall(cls, wall_name: str) -> Optional[object]:
|
||||
"""Get the opposite wall."""
|
||||
cls._ensure_initialized()
|
||||
if cls._cube is None:
|
||||
return None
|
||||
return cls._cube.opposite_wall(wall_name)
|
||||
5
src/kaballah/tree/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Tree namespace - access Tree of Life, Sephiroth, and Paths."""
|
||||
|
||||
from .tree import Tree
|
||||
|
||||
__all__ = ["Tree"]
|
||||
136
src/kaballah/tree/tree.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Tarot Tree of Life module.
|
||||
|
||||
Provides access to Sephiroth, Paths, and Tree of Life correspondences.
|
||||
|
||||
Usage:
|
||||
from tarot.tree import Tree
|
||||
|
||||
sephera = Tree.sephera(1) # Get Sephira 1 (Kether)
|
||||
path = Tree.path(11) # Get Path 11
|
||||
all_sepheras = Tree.sephera() # Get all Sephiroth
|
||||
|
||||
print(Tree()) # Display Tree structure
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Union, overload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.attributes import Sephera, Path
|
||||
from tarot.card.data import CardDataLoader
|
||||
from utils.query import QueryResult, Query
|
||||
|
||||
|
||||
class TreeMeta(type):
|
||||
"""Metaclass to add __str__ to Tree class itself."""
|
||||
|
||||
def __str__(cls) -> str:
|
||||
"""Return readable representation when Tree is converted to string."""
|
||||
# Access Tree class attributes through type.__getattribute__
|
||||
Tree._ensure_initialized()
|
||||
sepheras = type.__getattribute__(cls, '_sepheras')
|
||||
paths = type.__getattribute__(cls, '_paths')
|
||||
lines = [
|
||||
"Tree of Life",
|
||||
"=" * 60,
|
||||
f"Sephiroth: {len(sepheras)} nodes",
|
||||
f"Paths: {len(paths)} connections",
|
||||
"",
|
||||
"Structure:",
|
||||
]
|
||||
|
||||
# Show Sephira hierarchy
|
||||
for num in sorted(sepheras.keys()):
|
||||
seph = sepheras[num]
|
||||
lines.append(f" {num}. {seph.name} ({seph.hebrew_name})")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def __repr__(cls) -> str:
|
||||
"""Return object representation."""
|
||||
Tree._ensure_initialized()
|
||||
sepheras = type.__getattribute__(cls, '_sepheras')
|
||||
paths = type.__getattribute__(cls, '_paths')
|
||||
return f"Tree(sepheras={len(sepheras)}, paths={len(paths)})"
|
||||
|
||||
|
||||
class Tree(metaclass=TreeMeta):
|
||||
"""
|
||||
Unified accessor for Tree of Life correspondences.
|
||||
|
||||
All methods are class methods, so Tree is accessed as a static namespace:
|
||||
|
||||
sephera = Tree.sephera(1)
|
||||
path = Tree.path(11)
|
||||
print(Tree()) # Displays tree structure
|
||||
"""
|
||||
|
||||
_sepheras: Dict[int, 'Sephera'] = {} # type: ignore
|
||||
_paths: Dict[int, 'Path'] = {} # type: ignore
|
||||
_initialized: bool = False
|
||||
_loader: Optional['CardDataLoader'] = None # type: ignore
|
||||
|
||||
@classmethod
|
||||
def _ensure_initialized(cls) -> None:
|
||||
"""Lazy-load data from CardDataLoader on first access."""
|
||||
if cls._initialized:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
cls._loader = CardDataLoader()
|
||||
cls._sepheras = cls._loader._sephera
|
||||
cls._paths = cls._loader._paths
|
||||
cls._initialized = True
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def sephera(cls, number: int) -> Optional['Sephera']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def sephera(cls, number: None = ...) -> Dict[int, 'Sephera']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def sephera(cls, number: Optional[int] = None) -> Union[Optional['Sephera'], Dict[int, 'Sephera']]:
|
||||
"""Return a Sephira or all Sephiroth."""
|
||||
cls._ensure_initialized()
|
||||
if number is None:
|
||||
return cls._sepheras.copy()
|
||||
return cls._sepheras.get(number)
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def path(cls, number: int) -> Optional['Path']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def path(cls, number: None = ...) -> Dict[int, 'Path']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def path(cls, number: Optional[int] = None) -> Union[Optional['Path'], Dict[int, 'Path']]:
|
||||
"""Return a Path or all Paths."""
|
||||
cls._ensure_initialized()
|
||||
if number is None:
|
||||
return cls._paths.copy()
|
||||
return cls._paths.get(number)
|
||||
|
||||
@classmethod
|
||||
def filter(cls, expression: str) -> 'Query':
|
||||
"""
|
||||
Filter Sephiroth by attribute:value expression.
|
||||
|
||||
Examples:
|
||||
Tree.filter('name:Kether').first()
|
||||
Tree.filter('number:1').first()
|
||||
Tree.filter('sphere:1').all()
|
||||
|
||||
Returns a Query object for chaining.
|
||||
"""
|
||||
from tarot.query import Query
|
||||
cls._ensure_initialized()
|
||||
# Create a query from all Sephiroth
|
||||
return Query(cls._sepheras).filter(expression)
|
||||
27
src/letter/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Letter namespace - Alphabets, Letters, Ciphers, I Ching, Periodic Table, Words.
|
||||
|
||||
Provides fluent query interface for:
|
||||
- Alphabets (English, Hebrew, Greek)
|
||||
- Ciphers and word encoding
|
||||
- Hebrew letters with Tarot correspondences (paths)
|
||||
- I Ching trigrams and hexagrams
|
||||
- Periodic table with Sephiroth
|
||||
- Word analysis and cipher operations
|
||||
|
||||
Usage:
|
||||
from tarot import letter
|
||||
|
||||
letter.alphabet('english')
|
||||
letter.words.word('MAGICK').cipher('english_simple')
|
||||
letter.iching.hexagram(1)
|
||||
letter.paths('aleph') # Get Hebrew letter with Tarot correspondences
|
||||
"""
|
||||
|
||||
from .letter import letter
|
||||
from .iChing import trigram, hexagram
|
||||
from .words import word
|
||||
from .paths import letters
|
||||
|
||||
__all__ = ["letter", "trigram", "hexagram", "word", "letters"]
|
||||
|
||||
249
src/letter/attributes.py
Normal file
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
Letter attributes and data structures.
|
||||
|
||||
This module defines attributes specific to the Letter module,
|
||||
including Alphabets, Enochian letters, and Double Letter Trumps.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
|
||||
from utils.attributes import (
|
||||
Element,
|
||||
ElementType,
|
||||
Planet,
|
||||
Meaning,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Letter:
|
||||
"""Represents a letter with its attributes."""
|
||||
character: str
|
||||
position: int
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnglishAlphabet:
|
||||
"""English alphabet with Tarot/Kabbalistic correspondence."""
|
||||
letter: str
|
||||
position: int
|
||||
sound: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not (1 <= self.position <= 26):
|
||||
raise ValueError(f"Position must be between 1 and 26, got {self.position}")
|
||||
if len(self.letter) != 1 or not self.letter.isalpha():
|
||||
raise ValueError(f"Letter must be a single alphabetic character, got {self.letter}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class GreekAlphabet:
|
||||
"""Greek alphabet with Tarot/Kabbalistic correspondence."""
|
||||
letter: str
|
||||
position: int
|
||||
transliteration: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not (1 <= self.position <= 24):
|
||||
raise ValueError(f"Position must be between 1 and 24, got {self.position}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class HebrewAlphabet:
|
||||
"""Hebrew alphabet with Tarot/Kabbalistic correspondence."""
|
||||
letter: str
|
||||
position: int
|
||||
transliteration: str
|
||||
meaning: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not (1 <= self.position <= 22):
|
||||
raise ValueError(f"Position must be between 1 and 22, got {self.position}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DoublLetterTrump:
|
||||
"""Represents a Double Letter Trump (Yodh through Tau, 3-21 of Major Arcana)."""
|
||||
number: int # 3-21 (19 double letter trumps)
|
||||
name: str # Full name (e.g., "The Empress")
|
||||
hebrew_letter_1: str # First Hebrew letter (e.g., "Gimel")
|
||||
hebrew_letter_2: Optional[str] = None # Second Hebrew letter if applicable
|
||||
planet: Optional['Planet'] = None # Associated planet
|
||||
tarot_trump: Optional[str] = None # e.g., "III - The Empress"
|
||||
astrological_sign: Optional[str] = None # Zodiac sign if any
|
||||
element: Optional['ElementType'] = None # Associated element
|
||||
number_value: Optional[int] = None # Numerological value
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
meaning: Optional['Meaning'] = None # Upright and reversed meanings
|
||||
description: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 3 <= self.number <= 21:
|
||||
raise ValueError(f"Double Letter Trump number must be 3-21, got {self.number}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnochianLetter:
|
||||
"""Represents an Enochian letter with its properties."""
|
||||
name: str # Enochian letter name
|
||||
letter: str # The letter itself
|
||||
hebrew_equivalent: Optional[str] = None
|
||||
tarot_correspondence: Optional[str] = None
|
||||
planet: Optional[str] = None
|
||||
element: Optional[str] = None
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnochianSpirit:
|
||||
"""Represents an Enochian spirit or intelligence."""
|
||||
name: str # Spirit name
|
||||
rank: str # e.g., "King", "Prince", "Duke", "Intelligence"
|
||||
element: Optional[str] = None
|
||||
direction: Optional[str] = None # e.g., "East", "South", etc.
|
||||
sigil: Optional[str] = None # ASCII representation or description
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnochianArchetype:
|
||||
"""
|
||||
Archetypal form of an Enochian Tablet.
|
||||
|
||||
Provides a 4x4 grid with positions that can be filled with different
|
||||
visual representations (colors, images, symbols, etc.).
|
||||
"""
|
||||
name: str # e.g., "Tablet of Air Archetype"
|
||||
tablet_name: str # Reference to parent tablet
|
||||
grid: Dict[Tuple[int, int], 'EnochianGridPosition'] = field(default_factory=dict) # 4x4 grid
|
||||
row_correspondences: List[Dict[str, Any]] = field(default_factory=list) # Row meanings (4 rows)
|
||||
col_correspondences: List[Dict[str, Any]] = field(default_factory=list) # Column meanings (4 cols)
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
def get_position(self, row: int, col: int) -> Optional['EnochianGridPosition']:
|
||||
"""Get the grid position at (row, col)."""
|
||||
if not 0 <= row < 4 or not 0 <= col < 4:
|
||||
return None
|
||||
return self.grid.get((row, col))
|
||||
|
||||
def get_row_correspondence(self, row: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get the meaning/correspondence for a row."""
|
||||
if 0 <= row < len(self.row_correspondences):
|
||||
return self.row_correspondences[row]
|
||||
return None
|
||||
|
||||
def get_col_correspondence(self, col: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get the meaning/correspondence for a column."""
|
||||
if 0 <= col < len(self.col_correspondences):
|
||||
return self.col_correspondences[col]
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnochianGridPosition:
|
||||
"""
|
||||
Represents a single position in an Enochian Tablet grid.
|
||||
|
||||
A 4x4 grid cell with:
|
||||
- Center letter
|
||||
- Directional letters (N, S, E, W)
|
||||
- Archetypal correspondences (Tarot, element, etc.)
|
||||
"""
|
||||
row: int # Grid row (0-3)
|
||||
col: int # Grid column (0-3)
|
||||
center_letter: str # The main letter at this position
|
||||
north_letter: Optional[str] = None # Letter above
|
||||
south_letter: Optional[str] = None # Letter below
|
||||
east_letter: Optional[str] = None # Letter to the right
|
||||
west_letter: Optional[str] = None # Letter to the left
|
||||
tarot_card: Optional[str] = None # Associated Tarot card (e.g., "Ace of Swords")
|
||||
tarot_suit: Optional[str] = None # Suit correspondence (Swords, Wands, Cups, Pentacles)
|
||||
tarot_number: Optional[int] = None # Card number (0-13, 0=Ace)
|
||||
element: Optional[str] = None # Element correspondence
|
||||
zodiac_sign: Optional[str] = None # Zodiac correspondence
|
||||
planetary_hour: Optional[str] = None # Associated hour
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
meanings: List[str] = field(default_factory=list)
|
||||
|
||||
def get_all_letters(self) -> Dict[str, str]:
|
||||
"""Get all letters in this position: center and directional."""
|
||||
letters = {"center": self.center_letter}
|
||||
if self.north_letter:
|
||||
letters["north"] = self.north_letter
|
||||
if self.south_letter:
|
||||
letters["south"] = self.south_letter
|
||||
if self.east_letter:
|
||||
letters["east"] = self.east_letter
|
||||
if self.west_letter:
|
||||
letters["west"] = self.west_letter
|
||||
return letters
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnochianTablet:
|
||||
"""
|
||||
Represents an Enochian Tablet.
|
||||
|
||||
The Enochian system contains:
|
||||
- 4 elemental tablets (Earth, Water, Air, Fire)
|
||||
- 1 tablet of union (Aethyr)
|
||||
- Each tablet is 12x12 (144 squares) containing Enochian letters
|
||||
- Archetypal form with 4x4 grid for user customization
|
||||
"""
|
||||
name: str # e.g., "Tablet of Earth", "Tablet of Air", etc.
|
||||
number: int # Tablet identifier (1-5)
|
||||
element: Optional[str] = None # Earth, Water, Air, Fire, or Aethyr/Union
|
||||
rulers: List[str] = field(default_factory=list) # Names of spirits ruling this tablet
|
||||
archangels: List[str] = field(default_factory=list) # Associated archangels
|
||||
letters: Dict[Tuple[int, int], str] = field(default_factory=dict) # Grid of letters (row, col) -> letter
|
||||
planetary_hours: List[str] = field(default_factory=list) # Associated hours
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
archetype: Optional[EnochianArchetype] = None # Archetypal form for visualization
|
||||
|
||||
# Valid tablets
|
||||
VALID_TABLETS = {
|
||||
"Tablet of Union", # Aethyr
|
||||
"Tablet of Earth",
|
||||
"Tablet of Water",
|
||||
"Tablet of Air",
|
||||
"Tablet of Fire",
|
||||
}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.name not in self.VALID_TABLETS:
|
||||
raise ValueError(
|
||||
f"Invalid tablet '{self.name}'. "
|
||||
f"Valid tablets: {', '.join(self.VALID_TABLETS)}"
|
||||
)
|
||||
# Tablet of Union uses 0, elemental tablets use 1-5
|
||||
valid_range = (0, 0) if "Union" in self.name else (1, 5)
|
||||
if not valid_range[0] <= self.number <= valid_range[1]:
|
||||
raise ValueError(
|
||||
f"Tablet number must be {valid_range[0]}-{valid_range[1]}, got {self.number}"
|
||||
)
|
||||
|
||||
def is_elemental(self) -> bool:
|
||||
"""Check if this is an elemental tablet (not union)."""
|
||||
return self.element in {"Earth", "Water", "Air", "Fire"}
|
||||
|
||||
def is_union(self) -> bool:
|
||||
"""Check if this is the Tablet of Union (Aethyr)."""
|
||||
return self.element == "Aethyr" or "Union" in self.name
|
||||
|
||||
def get_letter(self, row: int, col: int) -> Optional[str]:
|
||||
"""Get letter at specific grid position."""
|
||||
return self.letters.get((row, col))
|
||||
|
||||
def get_row(self, row: int) -> List[Optional[str]]:
|
||||
"""Get all letters in a row."""
|
||||
return [self.letters.get((row, col)) for col in range(12)]
|
||||
|
||||
def get_column(self, col: int) -> List[Optional[str]]:
|
||||
"""Get all letters in a column."""
|
||||
return [self.letters.get((row, col)) for row in range(12)]
|
||||
220
src/letter/iChing.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""I Ching trigrams and hexagrams module.
|
||||
|
||||
Provides fluent query interface for I Ching trigrams and hexagrams,
|
||||
including Tarot correspondences and binary representations.
|
||||
|
||||
Usage:
|
||||
from letter.iChing import trigram, hexagram
|
||||
|
||||
qian = trigram.trigram('Qian')
|
||||
creative = hexagram.hexagram(1)
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from utils.query import CollectionAccessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card.data import CardDataLoader
|
||||
from tarot.attributes import Trigram, Hexagram
|
||||
|
||||
|
||||
def _line_diagram_from_binary(binary: str) -> str:
|
||||
"""Render a sideways ASCII diagram where top lines appear on the right."""
|
||||
if not binary:
|
||||
return ""
|
||||
cleaned = [bit for bit in binary if bit in {"0", "1"}]
|
||||
if not cleaned:
|
||||
return ""
|
||||
symbol_map = {"1": "|", "0": ":"}
|
||||
# Reverse so the right-most character represents the top line.
|
||||
return "".join(symbol_map[bit] for bit in reversed(cleaned))
|
||||
|
||||
|
||||
class _Trigram:
|
||||
"""Fluent query accessor for I Ching trigrams."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._trigrams: Dict[str, 'Trigram'] = {}
|
||||
self.trigram = CollectionAccessor(self._get_trigrams)
|
||||
|
||||
def _ensure_initialized(self) -> None:
|
||||
"""Load trigrams on first access."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._load_trigrams()
|
||||
self._initialized = True
|
||||
|
||||
def _get_trigrams(self):
|
||||
self._ensure_initialized()
|
||||
return self._trigrams.copy()
|
||||
|
||||
def _load_trigrams(self) -> None:
|
||||
"""Load the eight I Ching trigrams."""
|
||||
from tarot.attributes import Trigram
|
||||
|
||||
trigram_specs = [
|
||||
{"name": "Qian", "chinese": "乾", "pinyin": "Qián", "element": "Heaven", "attribute": "Creative", "binary": "111", "description": "Pure yang drive that initiates action."},
|
||||
{"name": "Dui", "chinese": "兑", "pinyin": "Duì", "element": "Lake", "attribute": "Joyous", "binary": "011", "description": "Open delight that invites community."},
|
||||
{"name": "Li", "chinese": "离", "pinyin": "Lí", "element": "Fire", "attribute": "Clinging", "binary": "101", "description": "Radiant clarity that adheres to insight."},
|
||||
{"name": "Zhen", "chinese": "震", "pinyin": "Zhèn", "element": "Thunder", "attribute": "Arousing", "binary": "001", "description": "Sudden awakening that shakes stagnation."},
|
||||
{"name": "Xun", "chinese": "巽", "pinyin": "Xùn", "element": "Wind", "attribute": "Gentle", "binary": "110", "description": "Penetrating influence that persuades subtly."},
|
||||
{"name": "Kan", "chinese": "坎", "pinyin": "Kǎn", "element": "Water", "attribute": "Abysmal", "binary": "010", "description": "Depth, risk, and sincere feeling."},
|
||||
{"name": "Gen", "chinese": "艮", "pinyin": "Gèn", "element": "Mountain", "attribute": "Stillness", "binary": "100", "description": "Grounded rest that establishes boundaries."},
|
||||
{"name": "Kun", "chinese": "坤", "pinyin": "Kūn", "element": "Earth", "attribute": "Receptive", "binary": "000", "description": "Vast receptivity that nurtures form."},
|
||||
]
|
||||
self._trigrams = {}
|
||||
for spec in trigram_specs:
|
||||
name = spec.get("name")
|
||||
if not name:
|
||||
raise ValueError("Trigram spec missing 'name'")
|
||||
binary = spec.get("binary", "")
|
||||
self._trigrams[name.lower()] = Trigram(
|
||||
name=name,
|
||||
chinese_name=spec.get("chinese", ""),
|
||||
pinyin=spec.get("pinyin", ""),
|
||||
element=spec.get("element", ""),
|
||||
attribute=spec.get("attribute", ""),
|
||||
binary=binary,
|
||||
description=spec.get("description", ""),
|
||||
line_diagram=_line_diagram_from_binary(binary),
|
||||
)
|
||||
|
||||
|
||||
class _Hexagram:
|
||||
"""Fluent query accessor for I Ching hexagrams."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._hexagrams: Dict[int, 'Hexagram'] = {}
|
||||
self.hexagram = CollectionAccessor(self._get_hexagrams)
|
||||
|
||||
def _ensure_initialized(self) -> None:
|
||||
"""Load hexagrams on first access."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._load_hexagrams()
|
||||
self._initialized = True
|
||||
|
||||
def _get_hexagrams(self):
|
||||
self._ensure_initialized()
|
||||
return self._hexagrams.copy()
|
||||
|
||||
def _load_hexagrams(self) -> None:
|
||||
"""Load all 64 I Ching hexagrams."""
|
||||
from tarot.attributes import Hexagram
|
||||
from tarot.card.data import CardDataLoader, calculate_digital_root
|
||||
|
||||
# Ensure trigrams are loaded first
|
||||
trigram._ensure_initialized()
|
||||
|
||||
# Load planets for hexagram correspondences
|
||||
loader = CardDataLoader()
|
||||
|
||||
hex_specs = [
|
||||
{"number": 1, "name": "Creative Force", "chinese": "乾", "pinyin": "Qián", "judgement": "Initiative succeeds when anchored in integrity.", "image": "Heaven above and below mirrors unstoppable drive.", "upper": "Qian", "lower": "Qian", "keywords": "Leadership|Momentum|Clarity"},
|
||||
{"number": 2, "name": "Receptive Field", "chinese": "坤", "pinyin": "Kūn", "judgement": "Grounded support flourishes through patience.", "image": "Earth layered upon earth offers fertile space.", "upper": "Kun", "lower": "Kun", "keywords": "Nurture|Support|Yielding"},
|
||||
{"number": 3, "name": "Sprouting", "chinese": "屯", "pinyin": "Zhūn", "judgement": "Challenges at the start need perseverance.", "image": "Water over thunder shows storms that germinate seeds.", "upper": "Kan", "lower": "Zhen", "keywords": "Beginnings|Struggle|Resolve"},
|
||||
{"number": 4, "name": "Youthful Insight", "chinese": "蒙", "pinyin": "Méng", "judgement": "Ignorance yields to steady guidance.", "image": "Mountain above water signals learning via restraint.", "upper": "Gen", "lower": "Kan", "keywords": "Study|Mentorship|Humility"},
|
||||
{"number": 5, "name": "Waiting", "chinese": "需", "pinyin": "Xū", "judgement": "Hold position until nourishment arrives.", "image": "Water above heaven depicts clouds gathering provision.", "upper": "Kan", "lower": "Qian", "keywords": "Patience|Faith|Preparation"},
|
||||
{"number": 6, "name": "Conflict", "chinese": "訟", "pinyin": "Sòng", "judgement": "Clarity and fairness prevent escalation.", "image": "Heaven above water shows tension seeking balance.", "upper": "Qian", "lower": "Kan", "keywords": "Debate|Justice|Boundaries"},
|
||||
{"number": 7, "name": "Collective Force", "chinese": "師", "pinyin": "Shī", "judgement": "Coordinated effort requires disciplined leadership.", "image": "Earth over water mirrors troops marshaling supplies.", "upper": "Kun", "lower": "Kan", "keywords": "Discipline|Leadership|Community"},
|
||||
{"number": 8, "name": "Union", "chinese": "比", "pinyin": "Bǐ", "judgement": "Shared values attract loyal allies.", "image": "Water over earth highlights bonds formed through empathy.", "upper": "Kan", "lower": "Kun", "keywords": "Alliance|Affinity|Trust"},
|
||||
{"number": 9, "name": "Small Accumulating", "chinese": "小畜", "pinyin": "Xiǎo Chù", "judgement": "Gentle restraint nurtures gradual gains.", "image": "Wind over heaven indicates tender guidance on great power.", "upper": "Xun", "lower": "Qian", "keywords": "Restraint|Cultivation|Care"},
|
||||
{"number": 10, "name": "Treading", "chinese": "履", "pinyin": "Lǚ", "judgement": "Walk with awareness when near power.", "image": "Heaven over lake shows respect between ranks.", "upper": "Qian", "lower": "Dui", "keywords": "Conduct|Respect|Sensitivity"},
|
||||
{"number": 11, "name": "Peace", "chinese": "泰", "pinyin": "Tài", "judgement": "Harmony thrives when resources circulate freely.", "image": "Earth over heaven signals prosperity descending.", "upper": "Kun", "lower": "Qian", "keywords": "Harmony|Prosperity|Flourish"},
|
||||
{"number": 12, "name": "Standstill", "chinese": "否", "pinyin": "Pǐ", "judgement": "When channels close, conserve strength.", "image": "Heaven over earth reveals blocked exchange.", "upper": "Qian", "lower": "Kun", "keywords": "Stagnation|Reflection|Pause"},
|
||||
{"number": 13, "name": "Fellowship", "chinese": "同人", "pinyin": "Tóng Rén", "judgement": "Shared purpose unites distant hearts.", "image": "Heaven over fire shows clarity within community.", "upper": "Qian", "lower": "Li", "keywords": "Community|Shared Vision|Openness"},
|
||||
{"number": 14, "name": "Great Possession", "chinese": "大有", "pinyin": "Dà Yǒu", "judgement": "Generosity cements lasting influence.", "image": "Fire over heaven reflects radiance sustained by ethics.", "upper": "Li", "lower": "Qian", "keywords": "Wealth|Stewardship|Confidence"},
|
||||
{"number": 15, "name": "Modesty", "chinese": "謙", "pinyin": "Qiān", "judgement": "Balance is found by lowering the proud.", "image": "Earth over mountain reveals humility safeguarding strength.", "upper": "Kun", "lower": "Gen", "keywords": "Humility|Balance|Service"},
|
||||
{"number": 16, "name": "Enthusiasm", "chinese": "豫", "pinyin": "Yù", "judgement": "Inspired music rallies the people.", "image": "Thunder over earth depicts drums stirring hearts.", "upper": "Zhen", "lower": "Kun", "keywords": "Inspiration|Celebration|Momentum"},
|
||||
{"number": 17, "name": "Following", "chinese": "隨", "pinyin": "Suí", "judgement": "Adapt willingly to timely leadership.", "image": "Lake over thunder points to joyful allegiance.", "upper": "Dui", "lower": "Zhen", "keywords": "Adaptation|Loyalty|Flow"},
|
||||
{"number": 18, "name": "Repairing", "chinese": "蠱", "pinyin": "Gǔ", "judgement": "Address decay with responsibility and care.", "image": "Mountain over wind shows correction of lineages.", "upper": "Gen", "lower": "Xun", "keywords": "Restoration|Accountability|Healing"},
|
||||
{"number": 19, "name": "Approach", "chinese": "臨", "pinyin": "Lín", "judgement": "Leaders draw near to listen sincerely.", "image": "Earth over lake signifies compassion visiting the people.", "upper": "Kun", "lower": "Dui", "keywords": "Empathy|Guidance|Presence"},
|
||||
{"number": 20, "name": "Contemplation", "chinese": "觀", "pinyin": "Guān", "judgement": "Observation inspires ethical alignment.", "image": "Wind over earth is the elevated view of the sage.", "upper": "Xun", "lower": "Kun", "keywords": "Perspective|Ritual|Vision"},
|
||||
{"number": 21, "name": "Biting Through", "chinese": "噬嗑", "pinyin": "Shì Kè", "judgement": "Decisive action cuts through obstruction.", "image": "Fire over thunder shows justice enforced with clarity.", "upper": "Li", "lower": "Zhen", "keywords": "Decision|Justice|Resolve"},
|
||||
{"number": 22, "name": "Grace", "chinese": "賁", "pinyin": "Bì", "judgement": "Beauty adorns substance when humility remains.", "image": "Mountain over fire highlights poise and restraint.", "upper": "Gen", "lower": "Li", "keywords": "Aesthetics|Poise|Form"},
|
||||
{"number": 23, "name": "Splitting Apart", "chinese": "剝", "pinyin": "Bō", "judgement": "When decay spreads, strip away excess.", "image": "Mountain over earth signals outer shells falling.", "upper": "Gen", "lower": "Kun", "keywords": "Decline|Release|Truth"},
|
||||
{"number": 24, "name": "Return", "chinese": "復", "pinyin": "Fù", "judgement": "Cycles renew when rest follows completion.", "image": "Earth over thunder marks the turning of the year.", "upper": "Kun", "lower": "Zhen", "keywords": "Renewal|Rhythm|Faith"},
|
||||
{"number": 25, "name": "Innocence", "chinese": "無妄", "pinyin": "Wú Wàng", "judgement": "Sincerity triumphs over scheming.", "image": "Heaven over thunder shows spontaneous virtue.", "upper": "Qian", "lower": "Zhen", "keywords": "Authenticity|Spontaneity|Trust"},
|
||||
{"number": 26, "name": "Great Taming", "chinese": "大畜", "pinyin": "Dà Chù", "judgement": "Conserve strength until action serves wisdom.", "image": "Mountain over heaven portrays restraint harnessing power.", "upper": "Gen", "lower": "Qian", "keywords": "Discipline|Reserve|Mastery"},
|
||||
{"number": 27, "name": "Nourishment", "chinese": "頤", "pinyin": "Yí", "judgement": "Words and food alike must be chosen with care.", "image": "Mountain over thunder emphasizes mindful sustenance.", "upper": "Gen", "lower": "Zhen", "keywords": "Nutrition|Speech|Mindfulness"},
|
||||
{"number": 28, "name": "Great Exceeding", "chinese": "大過", "pinyin": "Dà Guò", "judgement": "Bearing heavy loads demands flexibility.", "image": "Lake over wind shows a beam bending before it breaks.", "upper": "Dui", "lower": "Xun", "keywords": "Weight|Adaptability|Responsibility"},
|
||||
{"number": 29, "name": "The Abyss", "chinese": "坎", "pinyin": "Kǎn", "judgement": "Repeated trials teach sincere caution.", "image": "Water over water is the perilous gorge.", "upper": "Kan", "lower": "Kan", "keywords": "Trial|Honesty|Depth"},
|
||||
{"number": 30, "name": "Radiance", "chinese": "離", "pinyin": "Lí", "judgement": "Clarity is maintained by tending the flame.", "image": "Fire over fire represents brilliance sustained through care.", "upper": "Li", "lower": "Li", "keywords": "Illumination|Culture|Attention"},
|
||||
{"number": 31, "name": "Influence", "chinese": "咸", "pinyin": "Xián", "judgement": "Sincere attraction arises from mutual respect.", "image": "Lake over mountain highlights responsive hearts.", "upper": "Dui", "lower": "Gen", "keywords": "Attraction|Mutuality|Sensitivity"},
|
||||
{"number": 32, "name": "Duration", "chinese": "恒", "pinyin": "Héng", "judgement": "Commitment endures when balanced.", "image": "Thunder over wind speaks of constancy amid change.", "upper": "Zhen", "lower": "Xun", "keywords": "Commitment|Consistency|Rhythm"},
|
||||
{"number": 33, "name": "Retreat", "chinese": "遯", "pinyin": "Dùn", "judgement": "Strategic withdrawal preserves integrity.", "image": "Heaven over mountain shows noble retreat.", "upper": "Qian", "lower": "Gen", "keywords": "Withdrawal|Strategy|Self-care"},
|
||||
{"number": 34, "name": "Great Power", "chinese": "大壯", "pinyin": "Dà Zhuàng", "judgement": "Strength must remain aligned with virtue.", "image": "Thunder over heaven affirms action matched with purpose.", "upper": "Zhen", "lower": "Qian", "keywords": "Power|Ethics|Momentum"},
|
||||
{"number": 35, "name": "Progress", "chinese": "晉", "pinyin": "Jìn", "judgement": "Advancement arrives through clarity and loyalty.", "image": "Fire over earth depicts dawn spreading across the plain.", "upper": "Li", "lower": "Kun", "keywords": "Advancement|Visibility|Service"},
|
||||
{"number": 36, "name": "Darkening Light", "chinese": "明夷", "pinyin": "Míng Yí", "judgement": "Protect the inner light when circumstances grow harsh.", "image": "Earth over fire shows brilliance concealed for safety.", "upper": "Kun", "lower": "Li", "keywords": "Protection|Subtlety|Endurance"},
|
||||
{"number": 37, "name": "Family", "chinese": "家人", "pinyin": "Jiā Rén", "judgement": "Clear roles nourish household harmony.", "image": "Wind over fire indicates rituals ordering the home.", "upper": "Xun", "lower": "Li", "keywords": "Home|Roles|Care"},
|
||||
{"number": 38, "name": "Opposition", "chinese": "睽", "pinyin": "Kuí", "judgement": "Recognize difference without hostility.", "image": "Fire over lake reflects contrast seeking balance.", "upper": "Li", "lower": "Dui", "keywords": "Contrast|Perspective|Tolerance"},
|
||||
{"number": 39, "name": "Obstruction", "chinese": "蹇", "pinyin": "Jiǎn", "judgement": "Turn hindrance into training.", "image": "Water over mountain shows difficult ascent.", "upper": "Kan", "lower": "Gen", "keywords": "Obstacle|Effort|Learning"},
|
||||
{"number": 40, "name": "Deliverance", "chinese": "解", "pinyin": "Xiè", "judgement": "Relief comes when knots are untied.", "image": "Thunder over water portrays release after storm.", "upper": "Zhen", "lower": "Kan", "keywords": "Release|Solution|Breath"},
|
||||
{"number": 41, "name": "Decrease", "chinese": "損", "pinyin": "Sǔn", "judgement": "Voluntary simplicity restores balance.", "image": "Mountain over lake shows graceful sharing of resources.", "upper": "Gen", "lower": "Dui", "keywords": "Simplicity|Offering|Balance"},
|
||||
{"number": 42, "name": "Increase", "chinese": "益", "pinyin": "Yì", "judgement": "Blessings multiply when shared.", "image": "Wind over thunder reveals generous expansion.", "upper": "Xun", "lower": "Zhen", "keywords": "Growth|Generosity|Opportunity"},
|
||||
{"number": 43, "name": "Breakthrough", "chinese": "夬", "pinyin": "Guài", "judgement": "Speak truth boldly to clear corruption.", "image": "Lake over heaven highlights decisive proclamation.", "upper": "Dui", "lower": "Qian", "keywords": "Resolution|Declaration|Courage"},
|
||||
{"number": 44, "name": "Encounter", "chinese": "姤", "pinyin": "Gòu", "judgement": "Unexpected influence requires discernment.", "image": "Heaven over wind shows potent visitors arriving.", "upper": "Qian", "lower": "Xun", "keywords": "Encounter|Discernment|Temptation"},
|
||||
{"number": 45, "name": "Gathering", "chinese": "萃", "pinyin": "Cuì", "judgement": "Unity grows when motive is sincere.", "image": "Lake over earth signifies assembly around shared cause.", "upper": "Dui", "lower": "Kun", "keywords": "Assembly|Devotion|Focus"},
|
||||
{"number": 46, "name": "Ascending", "chinese": "升", "pinyin": "Shēng", "judgement": "Slow steady progress pierces obstacles.", "image": "Earth over wind shows roots pushing upward.", "upper": "Kun", "lower": "Xun", "keywords": "Growth|Perseverance|Aspiration"},
|
||||
{"number": 47, "name": "Oppression", "chinese": "困", "pinyin": "Kùn", "judgement": "Constraints refine inner resolve.", "image": "Lake over water indicates fatigue relieved only by integrity.", "upper": "Dui", "lower": "Kan", "keywords": "Constraint|Endurance|Faith"},
|
||||
{"number": 48, "name": "The Well", "chinese": "井", "pinyin": "Jǐng", "judgement": "Communal resources must be maintained.", "image": "Water over wind depicts a well drawing fresh insight.", "upper": "Kan", "lower": "Xun", "keywords": "Resource|Maintenance|Depth"},
|
||||
{"number": 49, "name": "Revolution", "chinese": "革", "pinyin": "Gé", "judgement": "Change succeeds when timing and virtue align.", "image": "Lake over fire indicates shedding the old skin.", "upper": "Dui", "lower": "Li", "keywords": "Change|Timing|Renewal"},
|
||||
{"number": 50, "name": "The Vessel", "chinese": "鼎", "pinyin": "Dǐng", "judgement": "Elevated service transforms the culture.", "image": "Fire over wind depicts the cauldron that refines offerings.", "upper": "Li", "lower": "Xun", "keywords": "Service|Transformation|Heritage"},
|
||||
{"number": 51, "name": "Arousing Thunder", "chinese": "震", "pinyin": "Zhèn", "judgement": "Shock awakens the heart to reverence.", "image": "Thunder over thunder doubles the drumbeat of alertness.", "upper": "Zhen", "lower": "Zhen", "keywords": "Shock|Awakening|Movement"},
|
||||
{"number": 52, "name": "Still Mountain", "chinese": "艮", "pinyin": "Gèn", "judgement": "Cultivate stillness to master desire.", "image": "Mountain over mountain shows unmoving focus.", "upper": "Gen", "lower": "Gen", "keywords": "Stillness|Meditation|Boundaries"},
|
||||
{"number": 53, "name": "Gradual Development", "chinese": "漸", "pinyin": "Jiàn", "judgement": "Lasting progress resembles a tree growing rings.", "image": "Wind over mountain displays slow maturation.", "upper": "Xun", "lower": "Gen", "keywords": "Patience|Evolution|Commitment"},
|
||||
{"number": 54, "name": "Marrying Maiden", "chinese": "歸妹", "pinyin": "Guī Mèi", "judgement": "Adjust expectations when circumstances limit rank.", "image": "Thunder over lake spotlights unequal partnerships.", "upper": "Zhen", "lower": "Dui", "keywords": "Transition|Adaptation|Protocol"},
|
||||
{"number": 55, "name": "Abundance", "chinese": "豐", "pinyin": "Fēng", "judgement": "Radiant success must be handled with balance.", "image": "Thunder over fire illuminates the hall at noon.", "upper": "Zhen", "lower": "Li", "keywords": "Splendor|Responsibility|Timing"},
|
||||
{"number": 56, "name": "The Wanderer", "chinese": "旅", "pinyin": "Lǚ", "judgement": "Travel lightly and guard reputation.", "image": "Fire over mountain marks a traveler tending the campfire.", "upper": "Li", "lower": "Gen", "keywords": "Travel|Restraint|Awareness"},
|
||||
{"number": 57, "name": "Gentle Wind", "chinese": "巽", "pinyin": "Xùn", "judgement": "Persistent influence accomplishes what force cannot.", "image": "Wind over wind indicates subtle penetration.", "upper": "Xun", "lower": "Xun", "keywords": "Penetration|Diplomacy|Subtlety"},
|
||||
{"number": 58, "name": "Joyous Lake", "chinese": "兌", "pinyin": "Duì", "judgement": "Openhearted dialogue dissolves resentment.", "image": "Lake over lake celebrates shared delight.", "upper": "Dui", "lower": "Dui", "keywords": "Joy|Conversation|Trust"},
|
||||
{"number": 59, "name": "Dispersion", "chinese": "渙", "pinyin": "Huàn", "judgement": "Loosen rigid structures so spirit can move.", "image": "Wind over water shows breath dispersing fear.", "upper": "Xun", "lower": "Kan", "keywords": "Dissolve|Freedom|Relief"},
|
||||
{"number": 60, "name": "Limitation", "chinese": "節", "pinyin": "Jié", "judgement": "Clear boundaries enable real freedom.", "image": "Water over lake portrays calibrated vessels.", "upper": "Kan", "lower": "Dui", "keywords": "Boundaries|Measure|Discipline"},
|
||||
{"number": 61, "name": "Inner Truth", "chinese": "中孚", "pinyin": "Zhōng Fú", "judgement": "Trustworthiness unites disparate groups.", "image": "Wind over lake depicts resonance within the heart.", "upper": "Xun", "lower": "Dui", "keywords": "Sincerity|Empathy|Alignment"},
|
||||
{"number": 62, "name": "Small Exceeding", "chinese": "小過", "pinyin": "Xiǎo Guò", "judgement": "Attend to details when stakes are delicate.", "image": "Thunder over mountain reveals careful movement.", "upper": "Zhen", "lower": "Gen", "keywords": "Detail|Caution|Adjustment"},
|
||||
{"number": 63, "name": "After Completion", "chinese": "既濟", "pinyin": "Jì Jì", "judgement": "Success endures only if vigilance continues.", "image": "Water over fire displays balance maintained through work.", "upper": "Kan", "lower": "Li", "keywords": "Completion|Maintenance|Balance"},
|
||||
{"number": 64, "name": "Before Completion", "chinese": "未濟", "pinyin": "Wèi Jì", "judgement": "Stay attentive as outcomes crystallize.", "image": "Fire over water illustrates the final push before harmony.", "upper": "Li", "lower": "Kan", "keywords": "Transition|Focus|Preparation"},
|
||||
]
|
||||
planet_cycle = ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Earth"]
|
||||
self._hexagrams = {}
|
||||
for spec in hex_specs:
|
||||
number = spec.get("number")
|
||||
name = spec.get("name")
|
||||
upper_name = spec.get("upper")
|
||||
lower_name = spec.get("lower")
|
||||
if number is None or not name or not upper_name or not lower_name:
|
||||
continue
|
||||
upper = trigram._trigrams.get(upper_name.lower())
|
||||
lower = trigram._trigrams.get(lower_name.lower())
|
||||
if upper is None or lower is None:
|
||||
continue
|
||||
assoc_number = loader._numbers.get(calculate_digital_root(number))
|
||||
planet_name = spec.get("planet") or planet_cycle[(number - 1) % len(planet_cycle)]
|
||||
planet = loader._planets.get(planet_name.lower()) if planet_name else None
|
||||
keywords_field = spec.get("keywords")
|
||||
keywords = keywords_field.split("|") if keywords_field else []
|
||||
line_diagram = _line_diagram_from_binary(upper.binary + lower.binary)
|
||||
self._hexagrams[number] = Hexagram(
|
||||
number=number,
|
||||
name=name,
|
||||
chinese_name=spec.get("chinese", ""),
|
||||
pinyin=spec.get("pinyin", ""),
|
||||
judgement=spec.get("judgement", ""),
|
||||
image=spec.get("image", ""),
|
||||
upper_trigram=upper,
|
||||
lower_trigram=lower,
|
||||
keywords=keywords,
|
||||
associated_number=assoc_number,
|
||||
planetary_influence=planet,
|
||||
line_diagram=line_diagram,
|
||||
)
|
||||
|
||||
|
||||
# Create singleton instances
|
||||
trigram = _Trigram()
|
||||
hexagram = _Hexagram()
|
||||
42
src/letter/iChing_attributes.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
I Ching attributes and data structures.
|
||||
|
||||
This module defines attributes specific to the I Ching system,
|
||||
including Trigrams and Hexagrams.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
from utils.attributes import Number, Planet
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trigram:
|
||||
"""Represents one of the eight I Ching trigrams."""
|
||||
name: str
|
||||
chinese_name: str
|
||||
pinyin: str
|
||||
element: str
|
||||
attribute: str
|
||||
binary: str
|
||||
description: str = ""
|
||||
line_diagram: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Hexagram:
|
||||
"""Represents an I Ching hexagram with Tarot correspondence."""
|
||||
number: int
|
||||
name: str
|
||||
chinese_name: str
|
||||
pinyin: str
|
||||
judgement: str
|
||||
image: str
|
||||
upper_trigram: Trigram
|
||||
lower_trigram: Trigram
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
associated_number: Optional[Number] = None
|
||||
planetary_influence: Optional[Planet] = None
|
||||
notes: str = ""
|
||||
line_diagram: str = ""
|
||||
87
src/letter/letter.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Tarot letter namespace - fluent query interface."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from utils.query import CollectionAccessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card.data import CardDataLoader
|
||||
|
||||
|
||||
class Letter:
|
||||
"""Fluent query accessor for letters, alphabets, ciphers, and correspondences."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._loader: 'CardDataLoader | None' = None
|
||||
self.alphabet = CollectionAccessor(self._get_alphabets)
|
||||
self.cipher = CollectionAccessor(self._get_ciphers)
|
||||
self.letter = CollectionAccessor(self._get_letters)
|
||||
self.iching = CollectionAccessor(self._get_hexagrams)
|
||||
self.periodic = CollectionAccessor(self._get_periodic)
|
||||
|
||||
def _ensure_initialized(self) -> None:
|
||||
"""Lazy-load data from CardDataLoader on first access."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
self._loader = CardDataLoader()
|
||||
self._initialized = True
|
||||
|
||||
def _require_loader(self) -> 'CardDataLoader':
|
||||
self._ensure_initialized()
|
||||
assert self._loader is not None, "Loader not initialized"
|
||||
return self._loader
|
||||
|
||||
def _get_alphabets(self):
|
||||
loader = self._require_loader()
|
||||
return loader._alphabets.copy()
|
||||
|
||||
def _get_ciphers(self):
|
||||
loader = self._require_loader()
|
||||
return loader._ciphers.copy()
|
||||
|
||||
def _get_letters(self):
|
||||
loader = self._require_loader()
|
||||
return dict(loader.letter())
|
||||
|
||||
def _get_hexagrams(self):
|
||||
loader = self._require_loader()
|
||||
return loader._hexagrams.copy()
|
||||
|
||||
def _get_periodic(self):
|
||||
loader = self._require_loader()
|
||||
return loader._periodic_table.copy()
|
||||
|
||||
def word(self, text: str, *, alphabet: str = 'english'):
|
||||
"""
|
||||
Start a fluent cipher request for the given text.
|
||||
|
||||
Usage:
|
||||
letter.word('MAGICK').cipher('english_simple')
|
||||
letter.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
|
||||
"""
|
||||
loader = self._require_loader()
|
||||
return loader.word(text, alphabet=alphabet)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a nice summary of the letter accessor."""
|
||||
return (
|
||||
"Letter Namespace - Alphabets, Letters, Ciphers, I Ching, Periodic Table, Words\n\n"
|
||||
"Access methods:\n"
|
||||
" letter.alphabet - English, Hebrew, Greek alphabets\n"
|
||||
" letter.cipher - Cipher systems (English simple, Hebrew, etc.)\n"
|
||||
" letter.letter - Hebrew letters (Aleph through Tau)\n"
|
||||
" letter.word(text) - Encode text with cipher systems\n"
|
||||
" letter.iching - I Ching trigrams and hexagrams\n"
|
||||
" letter.periodic - Periodic table with Sephiroth"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a nice representation of the letter accessor."""
|
||||
return self.__str__()
|
||||
|
||||
|
||||
# Create singleton instance
|
||||
letter = Letter()
|
||||
340
src/letter/paths.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
Tarot Letters namespace - Hebrew letters with full correspondences.
|
||||
|
||||
Provides fluent access to Hebrew letters (Paths on Tree of Life) organized by type:
|
||||
- Mother Letters (3): Aleph, Mem, Shin
|
||||
- Double Letters (7): Beth through Tzadi
|
||||
- Simple Letters (12): Yodh through Tau
|
||||
|
||||
Data is sourced from CardDataLoader.paths() for a single source of truth.
|
||||
Each letter has attributes like:
|
||||
- Hebrew Letter
|
||||
- Zodiac (for simple letters)
|
||||
- Trump (Major Arcana card)
|
||||
- Four Color System (King, Queen, Prince, Princess)
|
||||
- Cube of Space correspondence
|
||||
- Intelligence/Archangel
|
||||
- Musical Note
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Union, TYPE_CHECKING
|
||||
from dataclasses import dataclass, field
|
||||
from utils.filter import universal_filter, get_filterable_fields, format_results
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from utils.query import CollectionAccessor
|
||||
from tarot.attributes import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class TarotLetter:
|
||||
"""
|
||||
Represents a Hebrew letter with full Tarot correspondences.
|
||||
|
||||
Wraps Path objects from CardDataLoader to provide a letter-focused interface
|
||||
while maintaining a single source of truth.
|
||||
"""
|
||||
path: 'Path' # Reference to the actual Path object from CardDataLoader
|
||||
letter_type: str # "Mother", "Double", or "Simple" (derived from path)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Validate that path is set."""
|
||||
if not self.path:
|
||||
raise ValueError("TarotLetter requires a valid Path object")
|
||||
|
||||
@property
|
||||
def hebrew_letter(self) -> str:
|
||||
"""Get Hebrew letter character."""
|
||||
return self.path.hebrew_letter or ""
|
||||
|
||||
@property
|
||||
def transliteration(self) -> str:
|
||||
"""Get transliterated name."""
|
||||
return self.path.transliteration or ""
|
||||
|
||||
@property
|
||||
def position(self) -> int:
|
||||
"""Get position (1-22 for paths)."""
|
||||
return self.path.number
|
||||
|
||||
@property
|
||||
def trump(self) -> Optional[str]:
|
||||
"""Get Tarot trump designation."""
|
||||
return self.path.tarot_trump
|
||||
|
||||
@property
|
||||
def element(self) -> Optional[str]:
|
||||
"""Get element name if applicable."""
|
||||
return self.path.element.name if self.path.element else None
|
||||
|
||||
@property
|
||||
def planet(self) -> Optional[str]:
|
||||
"""Get planet name if applicable."""
|
||||
return self.path.planet.name if self.path.planet else None
|
||||
|
||||
@property
|
||||
def zodiac(self) -> Optional[str]:
|
||||
"""Get zodiac sign if applicable."""
|
||||
return self.path.zodiac_sign
|
||||
|
||||
@property
|
||||
def intelligence(self) -> Optional[str]:
|
||||
"""Get archangel/intelligence name from associated gods."""
|
||||
# Extract first god's name from the path's associated gods
|
||||
all_gods = self.path.get_gods()
|
||||
if all_gods:
|
||||
return all_gods[0].name
|
||||
return None
|
||||
|
||||
@property
|
||||
def meaning(self) -> Optional[str]:
|
||||
"""Get path meaning/description."""
|
||||
return self.path.description
|
||||
|
||||
@property
|
||||
def keywords(self) -> List[str]:
|
||||
"""Get keywords associated with path."""
|
||||
return self.path.keywords or []
|
||||
|
||||
def display(self) -> str:
|
||||
"""Format letter for display."""
|
||||
lines = [
|
||||
f"Hebrew: {self.hebrew_letter}",
|
||||
f"Name: {self.transliteration}",
|
||||
f"Type: {self.letter_type}",
|
||||
f"Position: {self.position}",
|
||||
]
|
||||
|
||||
if self.trump:
|
||||
lines.append(f"Trump: {self.trump}")
|
||||
if self.zodiac:
|
||||
lines.append(f"Zodiac: {self.zodiac}")
|
||||
if self.planet:
|
||||
lines.append(f"Planet: {self.planet}")
|
||||
if self.element:
|
||||
lines.append(f"Element: {self.element}")
|
||||
if self.intelligence:
|
||||
lines.append(f"Intelligence: {self.intelligence}")
|
||||
if self.meaning:
|
||||
lines.append(f"Meaning: {self.meaning}")
|
||||
if self.keywords:
|
||||
lines.append(f"Keywords: {', '.join(self.keywords)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class LetterAccessor:
|
||||
"""Fluent accessor for Tarot letters."""
|
||||
|
||||
def __init__(self, letters_dict: Dict[str, TarotLetter]) -> None:
|
||||
self._letters = letters_dict
|
||||
|
||||
def __call__(self, transliteration: str) -> Optional[TarotLetter]:
|
||||
"""Get a letter by transliteration (e.g., 'aleph', 'beth', 'gimel')."""
|
||||
return self._letters.get(transliteration.lower())
|
||||
|
||||
def __getitem__(self, key: Union[str, int]) -> Optional[TarotLetter]:
|
||||
"""Get letter by name or position."""
|
||||
if isinstance(key, int):
|
||||
# Get by position (1-22)
|
||||
for letter in self._letters.values():
|
||||
if letter.position == key:
|
||||
return letter
|
||||
return None
|
||||
return self(key)
|
||||
|
||||
def all(self) -> List[TarotLetter]:
|
||||
"""Get all letters."""
|
||||
return sorted(self._letters.values(), key=lambda x: x.position)
|
||||
|
||||
def by_type(self, letter_type: str) -> List[TarotLetter]:
|
||||
"""Filter by type: 'Mother', 'Double', or 'Simple'."""
|
||||
return [l for l in self._letters.values() if l.letter_type == letter_type]
|
||||
|
||||
def by_zodiac(self, zodiac: str) -> Optional[TarotLetter]:
|
||||
"""Get letter by zodiac sign."""
|
||||
for letter in self._letters.values():
|
||||
if letter.zodiac and zodiac.lower() in letter.zodiac.lower():
|
||||
return letter
|
||||
return None
|
||||
|
||||
def by_planet(self, planet: str) -> List[TarotLetter]:
|
||||
"""Get letters by planet."""
|
||||
return [l for l in self._letters.values() if l.planet and planet.lower() in l.planet.lower()]
|
||||
|
||||
def by_trump(self, trump: str) -> Optional[TarotLetter]:
|
||||
"""Get letter by tarot trump."""
|
||||
return next((l for l in self._letters.values() if l.trump == trump), None)
|
||||
|
||||
def get_filterable_fields(self) -> List[str]:
|
||||
"""
|
||||
Dynamically get all filterable fields from TarotLetter.
|
||||
|
||||
Returns the same fields as the universal filter utility.
|
||||
Useful for introspection and validation.
|
||||
"""
|
||||
return get_filterable_fields(TarotLetter)
|
||||
|
||||
def filter(self, **kwargs) -> List[TarotLetter]:
|
||||
"""
|
||||
Filter letters by any TarotLetter attribute.
|
||||
|
||||
Uses the universal filter from utils.filter for consistency
|
||||
across the entire project.
|
||||
|
||||
The filter automatically handles all fields from the TarotLetter dataclass:
|
||||
- letter_type, element, trump, zodiac, planet
|
||||
- king, queen, prince, princess
|
||||
- cube, intelligence, note, meaning, hebrew_letter, transliteration, position
|
||||
- keywords (list matching)
|
||||
|
||||
Args:
|
||||
**kwargs: Any TarotLetter attribute with its value
|
||||
|
||||
Usage:
|
||||
Tarot.letters.filter(letter_type="Simple")
|
||||
Tarot.letters.filter(element="Fire")
|
||||
Tarot.letters.filter(letter_type="Double", planet="Mars")
|
||||
Tarot.letters.filter(element="Air", letter_type="Mother")
|
||||
Tarot.letters.filter(intelligence="Metatron")
|
||||
Tarot.letters.filter(position=1)
|
||||
|
||||
Returns:
|
||||
List of TarotLetter objects matching all filters
|
||||
"""
|
||||
return universal_filter(self.all(), **kwargs)
|
||||
|
||||
def display_filter(self, **kwargs) -> str:
|
||||
"""
|
||||
Filter letters and display results nicely formatted.
|
||||
|
||||
Combines filtering and formatting in one call.
|
||||
|
||||
Args:
|
||||
**kwargs: Any TarotLetter attribute with its value
|
||||
|
||||
Returns:
|
||||
Formatted string with filtered letters
|
||||
|
||||
Example:
|
||||
print(Tarot.letters.display_filter(element="Fire"))
|
||||
"""
|
||||
results = self.filter(**kwargs)
|
||||
return format_results(results)
|
||||
|
||||
def display_all(self) -> str:
|
||||
"""Display all letters formatted."""
|
||||
lines = []
|
||||
for letter in self.all():
|
||||
lines.append(letter.display())
|
||||
lines.append("-" * 50)
|
||||
return "\n".join(lines)
|
||||
|
||||
def display_by_type(self, letter_type: str) -> str:
|
||||
"""Display all letters of a specific type."""
|
||||
letters = self.by_type(letter_type)
|
||||
if not letters:
|
||||
return f"No letters found with type: {letter_type}"
|
||||
|
||||
lines = [f"\n{letter_type.upper()} LETTERS ({len(letters)} total)"]
|
||||
lines.append("=" * 50)
|
||||
for letter in letters:
|
||||
lines.append(letter.display())
|
||||
lines.append("-" * 50)
|
||||
return "\n".join(lines)
|
||||
|
||||
@property
|
||||
def iChing(self):
|
||||
"""Access I Ching trigrams and hexagrams."""
|
||||
return IChing()
|
||||
|
||||
|
||||
class IChing:
|
||||
"""Namespace for I Ching trigrams and hexagrams access.
|
||||
|
||||
Provides fluent query interface for accessing I Ching trigrams and hexagrams
|
||||
with Tarot correspondences.
|
||||
|
||||
Usage:
|
||||
trigrams = Tarot.letters.iChing.trigram
|
||||
qian = trigrams.name('Qian')
|
||||
all_trigrams = trigrams.all()
|
||||
|
||||
hexagrams = Tarot.letters.iChing.hexagram
|
||||
hex1 = hexagrams.all()[1]
|
||||
all_hex = hexagrams.list()
|
||||
"""
|
||||
|
||||
trigram: 'CollectionAccessor'
|
||||
hexagram: 'CollectionAccessor'
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize iChing accessor with trigram and hexagram collections."""
|
||||
from tarot.letter import iChing as iching_module
|
||||
self.trigram = iching_module.trigram.trigram
|
||||
self.hexagram = iching_module.hexagram.hexagram
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Clean representation of iChing namespace."""
|
||||
return "IChing(trigram, hexagram)"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of iChing namespace."""
|
||||
return "I Ching (trigrams and hexagrams)"
|
||||
|
||||
|
||||
class LettersRegistry:
|
||||
"""Registry and accessor for all Hebrew letters with Tarot correspondences."""
|
||||
|
||||
_instance: Optional['LettersRegistry'] = None
|
||||
_letters: Dict[str, TarotLetter] = {}
|
||||
_initialized: bool = False
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._initialize_letters()
|
||||
self._initialized = True
|
||||
|
||||
def _initialize_letters(self) -> None:
|
||||
"""Initialize all 22 Hebrew letters by wrapping Path objects from CardDataLoader."""
|
||||
from tarot.card.data import CardDataLoader
|
||||
|
||||
loader = CardDataLoader()
|
||||
paths = loader.path() # Get all 22 paths
|
||||
|
||||
self._letters = {}
|
||||
|
||||
# Map each path (11-32) to a TarotLetter with appropriate type
|
||||
for path_number, path in paths.items():
|
||||
# Determine letter type based on path number
|
||||
# Mother letters: 11 (Aleph), 23 (Mem), 31 (Shin)
|
||||
# Double letters: 12, 13, 14, 15, 18, 21, 22
|
||||
# Simple (Zodiacal/Planetary): 16, 17, 19, 20, 24, 25, 26, 27, 28, 29, 30, 32
|
||||
|
||||
if path_number in {11, 23, 31}:
|
||||
letter_type = "Mother"
|
||||
elif path_number in {12, 13, 14, 15, 18, 21, 22}:
|
||||
letter_type = "Double"
|
||||
else:
|
||||
letter_type = "Simple"
|
||||
|
||||
# Create TarotLetter wrapping the path
|
||||
letter_key = path.transliteration.lower()
|
||||
self._letters[letter_key] = TarotLetter(path=path, letter_type=letter_type)
|
||||
|
||||
def accessor(self) -> LetterAccessor:
|
||||
"""Get the letter accessor."""
|
||||
return LetterAccessor(self._letters)
|
||||
|
||||
|
||||
def letters() -> LetterAccessor:
|
||||
"""Get the letters accessor for fluent queries."""
|
||||
registry = LettersRegistry()
|
||||
return registry.accessor()
|
||||
5
src/letter/words/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Words namespace - word cipher and gematria operations."""
|
||||
|
||||
from .word import word
|
||||
|
||||
__all__ = ["word"]
|
||||
40
src/letter/words/word.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Tarot word namespace - fluent cipher operations."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card.data import CardDataLoader
|
||||
|
||||
|
||||
class _Word:
|
||||
"""Fluent accessor for word analysis and cipher operations."""
|
||||
|
||||
_loader: 'CardDataLoader | None' = None
|
||||
_initialized: bool = False
|
||||
|
||||
@classmethod
|
||||
def _ensure_initialized(cls) -> None:
|
||||
"""Lazy-load CardDataLoader on first access."""
|
||||
if cls._initialized:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
cls._loader = CardDataLoader()
|
||||
cls._initialized = True
|
||||
|
||||
@classmethod
|
||||
def word(cls, text: str, *, alphabet: str = 'english'):
|
||||
"""
|
||||
Start a fluent cipher request for the given text.
|
||||
|
||||
Usage:
|
||||
word.word('MAGICK').cipher('english_simple')
|
||||
word.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
|
||||
"""
|
||||
cls._ensure_initialized()
|
||||
assert cls._loader is not None, "Loader not initialized"
|
||||
return cls._loader.word(text, alphabet=alphabet)
|
||||
|
||||
|
||||
# Create singleton instance
|
||||
word = _Word()
|
||||
19
src/number/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
Number namespace - Numerology and number correspondences.
|
||||
|
||||
Provides fluent query interface for:
|
||||
- Numbers 1-9 with Sepheric attributes
|
||||
- Digital root calculation
|
||||
- Colors and correspondences
|
||||
|
||||
Usage:
|
||||
from tarot import number
|
||||
|
||||
num = number.number(5)
|
||||
root = number.digital_root(256)
|
||||
colors = number.color()
|
||||
"""
|
||||
|
||||
from .number import number, calculate_digital_root
|
||||
|
||||
__all__ = ["number", "calculate_digital_root"]
|
||||
198
src/number/loader.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Numbers loader - access to numerology and number correspondences."""
|
||||
|
||||
from typing import Dict, Optional, Union, overload
|
||||
from utils.filter import universal_filter
|
||||
|
||||
|
||||
def calculate_digital_root(value: int) -> int:
|
||||
"""
|
||||
Calculate the digital root of a number by repeatedly summing its digits.
|
||||
|
||||
Digital root reduces any number to a single digit (1-9) by repeatedly
|
||||
summing its digits until a single digit remains.
|
||||
|
||||
Args:
|
||||
value: The number to reduce to digital root
|
||||
|
||||
Returns:
|
||||
The digital root (1-9)
|
||||
|
||||
Examples:
|
||||
>>> calculate_digital_root(14) # 1+4 = 5
|
||||
5
|
||||
>>> calculate_digital_root(99) # 9+9 = 18, 1+8 = 9
|
||||
9
|
||||
>>> calculate_digital_root(5)
|
||||
5
|
||||
"""
|
||||
if value < 1:
|
||||
raise ValueError(f"Value must be positive, got {value}")
|
||||
|
||||
while value >= 10:
|
||||
value = sum(int(digit) for digit in str(value))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class Numbers:
|
||||
"""
|
||||
Unified accessor for numerology, numbers, and color correspondences.
|
||||
|
||||
All methods are class methods, so Numbers is accessed as a static namespace:
|
||||
|
||||
num = Numbers.number(5)
|
||||
root = Numbers.digital_root(256)
|
||||
color = Numbers.color_by_number(root)
|
||||
"""
|
||||
|
||||
# These are populated on first access from CardDataLoader
|
||||
_numbers: Dict[int, 'Number'] = {} # type: ignore
|
||||
_colors: Dict[int, 'Color'] = {} # type: ignore
|
||||
_initialized: bool = False
|
||||
|
||||
@classmethod
|
||||
def _ensure_initialized(cls) -> None:
|
||||
"""Lazy-load data from CardDataLoader on first access."""
|
||||
if cls._initialized:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
loader = CardDataLoader()
|
||||
cls._numbers = loader.number()
|
||||
cls._colors = loader.color()
|
||||
cls._initialized = True
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def number(cls, value: int) -> Optional['Number']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def number(cls, value: None = ...) -> Dict[int, 'Number']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def number(cls, value: Optional[int] = None) -> Union[Optional['Number'], Dict[int, 'Number']]:
|
||||
"""Return an individual Number or the full numerology table."""
|
||||
cls._ensure_initialized()
|
||||
if value is None:
|
||||
return cls._numbers.copy()
|
||||
return cls._numbers.get(value)
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def color(cls, sephera_number: int) -> Optional['Color']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def color(cls, sephera_number: None = ...) -> Dict[int, 'Color']:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def color(cls, sephera_number: Optional[int] = None) -> Union[Optional['Color'], Dict[int, 'Color']]:
|
||||
"""Return a single color correspondence or the entire map."""
|
||||
cls._ensure_initialized()
|
||||
if sephera_number is None:
|
||||
return cls._colors.copy()
|
||||
return cls._colors.get(sephera_number)
|
||||
|
||||
@classmethod
|
||||
def color_by_number(cls, number: int) -> Optional['Color']:
|
||||
"""Get a Color by mapping a number through digital root."""
|
||||
root = calculate_digital_root(number)
|
||||
return cls.color(root)
|
||||
|
||||
@classmethod
|
||||
def number_by_digital_root(cls, value: int) -> Optional['Number']:
|
||||
"""Get a Number object using digital root calculation."""
|
||||
root = calculate_digital_root(value)
|
||||
return cls.number(root)
|
||||
|
||||
@classmethod
|
||||
def digital_root(cls, value: int) -> int:
|
||||
"""Get the digital root of a value."""
|
||||
return calculate_digital_root(value)
|
||||
|
||||
@classmethod
|
||||
def filter_numbers(cls, **kwargs) -> list:
|
||||
"""
|
||||
Filter numbers by any Number attribute.
|
||||
|
||||
Uses the universal filter from utils.filter for consistency
|
||||
across the entire project.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Number attribute with its value
|
||||
|
||||
Usage:
|
||||
Numbers.filter_numbers(element="Fire")
|
||||
Numbers.filter_numbers(sephera_number=5)
|
||||
|
||||
Returns:
|
||||
List of Number objects matching all filters
|
||||
"""
|
||||
cls._ensure_initialized()
|
||||
return universal_filter(list(cls._numbers.values()), **kwargs)
|
||||
|
||||
@classmethod
|
||||
def display_filter_numbers(cls, **kwargs) -> str:
|
||||
"""
|
||||
Filter numbers and display results nicely formatted.
|
||||
|
||||
Combines filtering and formatting in one call.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Number attribute with its value
|
||||
|
||||
Returns:
|
||||
Formatted string with filtered numbers
|
||||
|
||||
Example:
|
||||
print(Numbers.display_filter_numbers(element="Fire"))
|
||||
"""
|
||||
from utils.filter import format_results
|
||||
results = cls.filter_numbers(**kwargs)
|
||||
return format_results(results)
|
||||
|
||||
@classmethod
|
||||
def filter_colors(cls, **kwargs) -> list:
|
||||
"""
|
||||
Filter colors by any Color attribute.
|
||||
|
||||
Uses the universal filter from utils.filter for consistency
|
||||
across the entire project.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Color attribute with its value
|
||||
|
||||
Usage:
|
||||
Numbers.filter_colors(element="Water")
|
||||
Numbers.filter_colors(sephera_number=3)
|
||||
|
||||
Returns:
|
||||
List of Color objects matching all filters
|
||||
"""
|
||||
cls._ensure_initialized()
|
||||
return universal_filter(list(cls._colors.values()), **kwargs)
|
||||
|
||||
@classmethod
|
||||
def display_filter_colors(cls, **kwargs) -> str:
|
||||
"""
|
||||
Filter colors and display results nicely formatted.
|
||||
|
||||
Combines filtering and formatting in one call.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Color attribute with its value
|
||||
|
||||
Returns:
|
||||
Formatted string with filtered colors
|
||||
|
||||
Example:
|
||||
print(Numbers.display_filter_colors(element="Water"))
|
||||
"""
|
||||
from utils.filter import format_results
|
||||
results = cls.filter_colors(**kwargs)
|
||||
return format_results(results)
|
||||
84
src/number/number.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Tarot number namespace - fluent query interface for numerology."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from utils.query import CollectionAccessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card.data import CardDataLoader
|
||||
|
||||
|
||||
def calculate_digital_root(value: int) -> int:
|
||||
"""
|
||||
Calculate the digital root of a number by repeatedly summing its digits.
|
||||
|
||||
Digital root reduces any number to a single digit (1-9) by repeatedly
|
||||
summing its digits until a single digit remains.
|
||||
"""
|
||||
if value < 1:
|
||||
raise ValueError(f"Value must be positive, got {value}")
|
||||
|
||||
while value >= 10:
|
||||
value = sum(int(digit) for digit in str(value))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class _Number:
|
||||
"""Fluent query accessor for numerology and number correspondences."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._loader: 'CardDataLoader | None' = None
|
||||
self.number = CollectionAccessor(self._get_numbers)
|
||||
self.color = CollectionAccessor(self._get_colors)
|
||||
self.cipher = CollectionAccessor(self._get_ciphers)
|
||||
|
||||
def _ensure_initialized(self) -> None:
|
||||
"""Lazy-load data from CardDataLoader on first access."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
self._loader = CardDataLoader()
|
||||
self._initialized = True
|
||||
|
||||
def _require_loader(self) -> 'CardDataLoader':
|
||||
self._ensure_initialized()
|
||||
assert self._loader is not None, "Loader not initialized"
|
||||
return self._loader
|
||||
|
||||
def _get_numbers(self):
|
||||
loader = self._require_loader()
|
||||
return loader.number().copy()
|
||||
|
||||
def _get_colors(self):
|
||||
loader = self._require_loader()
|
||||
return loader.color().copy()
|
||||
|
||||
def _get_ciphers(self):
|
||||
loader = self._require_loader()
|
||||
return loader._ciphers.copy()
|
||||
|
||||
def digital_root(self, value: int) -> int:
|
||||
"""Get the digital root of a value."""
|
||||
return calculate_digital_root(value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a nice summary of the number accessor."""
|
||||
return (
|
||||
"Number Namespace - Numerology and Number Correspondences\n\n"
|
||||
"Access methods:\n"
|
||||
" number.number(n) - Get number 1-9 with correspondences\n"
|
||||
" number.color() - Get color correspondences\n"
|
||||
" number.cipher() - Get cipher systems\n"
|
||||
" number.digital_root(n) - Calculate digital root of any number"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a nice representation of the number accessor."""
|
||||
return self.__str__()
|
||||
|
||||
|
||||
# Create singleton instance
|
||||
number = _Number()
|
||||
179
src/tarot/__init__.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
PY-Tarot: A comprehensive Python library for Tarot card reading and interpretation.
|
||||
|
||||
This library provides:
|
||||
- Full 78-card Tarot deck with Major and Minor Arcana
|
||||
- Kabbalistic correspondences and Tree of Life data
|
||||
- Multiple configurable cipher systems (Hebrew, English, Greek, Reduction)
|
||||
- Tarot alphabets (English, Greek, Hebrew) with meanings
|
||||
- Numbers 1-9 with Sepheric attributes and digital root calculation
|
||||
- Crowley 777 color system with Sephiroth correspondences
|
||||
- Full type hints for IDE support and type checking
|
||||
|
||||
Unified Namespaces (singular names):
|
||||
number - Numerology and number correspondences
|
||||
letter - Alphabets (English, Hebrew, Greek), ciphers, and word analysis
|
||||
Tarot - Tarot-specific (deck, cards, tree, cube, temporal)
|
||||
|
||||
Usage:
|
||||
from tarot import number, letter, words, Tarot
|
||||
|
||||
num = number.number(5)
|
||||
result = letter.words.word('MAGICK').cipher('english_simple')
|
||||
card = Tarot.deck.card(3)
|
||||
"""
|
||||
|
||||
from .deck import Deck, Card, MajorCard, MinorCard, DLT
|
||||
from .attributes import (
|
||||
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter,
|
||||
Sephera, PeriodicTable, Degree, AstrologicalInfluence,
|
||||
TreeOfLife, Correspondences, CardImage, DoublLetterTrump,
|
||||
EnglishAlphabet, GreekAlphabet, HebrewAlphabet,
|
||||
Trigram, Hexagram,
|
||||
EnochianTablet, EnochianGridPosition, EnochianArchetype, Path,
|
||||
)
|
||||
# Import shared attributes from utils
|
||||
from utils.attributes import (
|
||||
Note, Element, ElementType, Number, Color, Colorscale,
|
||||
Planet, God, Cipher, CipherResult, Perfume,
|
||||
)
|
||||
from kaballah.cube.attributes import CubeOfSpace, WallDirection, Wall
|
||||
from .card.data import CardDataLoader, calculate_digital_root
|
||||
from .tarot_api import Tarot
|
||||
|
||||
# Import from card module (includes details, loader, and image_loader)
|
||||
from .card import (
|
||||
CardAccessor,
|
||||
CardDetailsRegistry,
|
||||
load_card_details,
|
||||
load_deck_details,
|
||||
get_cards_by_suit,
|
||||
filter_cards_by_keywords,
|
||||
print_card_details,
|
||||
get_card_info,
|
||||
ImageDeckLoader,
|
||||
load_deck_images,
|
||||
)
|
||||
|
||||
# Import from namespace folders
|
||||
from letter import letter, trigram, hexagram
|
||||
from number import number, calculate_digital_root
|
||||
import kaballah
|
||||
from kaballah import Tree, Cube
|
||||
from temporal import ThalemaClock, Zodiac as AstrologyZodiac, PlanetPosition
|
||||
|
||||
|
||||
def display(obj):
|
||||
"""
|
||||
Pretty print any tarot object by showing all its attributes.
|
||||
|
||||
Automatically detects dataclass objects and displays their fields
|
||||
with values in a readable format.
|
||||
|
||||
Usage:
|
||||
from tarot import display, number
|
||||
num = number.number(5)
|
||||
display(num) # Shows all attributes nicely formatted
|
||||
"""
|
||||
from dataclasses import fields
|
||||
if hasattr(obj, '__dataclass_fields__'):
|
||||
# It's a dataclass - show all fields
|
||||
print(f"{obj.__class__.__name__}:")
|
||||
for field in fields(obj):
|
||||
value = getattr(obj, field.name)
|
||||
print(f" {field.name}: {value}")
|
||||
else:
|
||||
print(obj)
|
||||
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "PY-Tarot Contributors"
|
||||
__all__ = [
|
||||
# Namespaces (singular)
|
||||
"number",
|
||||
"letter",
|
||||
"kaballah",
|
||||
"Tarot",
|
||||
"trigram",
|
||||
"hexagram",
|
||||
|
||||
# Temporal and astrological
|
||||
"ThalemaClock",
|
||||
"AstrologyZodiac",
|
||||
"PlanetPosition",
|
||||
|
||||
# Card details and loading
|
||||
"CardDetailsRegistry",
|
||||
"load_card_details",
|
||||
"load_deck_details",
|
||||
"get_cards_by_suit",
|
||||
"filter_cards_by_keywords",
|
||||
"print_card_details",
|
||||
"get_card_info",
|
||||
|
||||
# Image loading
|
||||
"ImageDeckLoader",
|
||||
"load_deck_images",
|
||||
|
||||
# Utilities
|
||||
"display",
|
||||
"CardAccessor",
|
||||
"Tree",
|
||||
"Cube",
|
||||
|
||||
# Deck classes
|
||||
"Deck",
|
||||
"Card",
|
||||
"MajorCard",
|
||||
"MinorCard",
|
||||
"DLT",
|
||||
|
||||
# Calendar/attribute classes
|
||||
"Month",
|
||||
"Day",
|
||||
"Weekday",
|
||||
"Hour",
|
||||
"ClockHour",
|
||||
"Zodiac",
|
||||
"Suit",
|
||||
"Meaning",
|
||||
"Letter",
|
||||
"Note",
|
||||
"CubeOfSpace",
|
||||
"WallDirection",
|
||||
"Wall",
|
||||
|
||||
# Sepheric classes
|
||||
"Sephera",
|
||||
"PeriodicTable",
|
||||
"Degree",
|
||||
"Element",
|
||||
"ElementType",
|
||||
"AstrologicalInfluence",
|
||||
"TreeOfLife",
|
||||
"Correspondences",
|
||||
"CardImage",
|
||||
"DoublLetterTrump",
|
||||
"EnochianTablet",
|
||||
"EnochianGridPosition",
|
||||
"EnochianArchetype",
|
||||
|
||||
# Alphabet classes
|
||||
"EnglishAlphabet",
|
||||
"GreekAlphabet",
|
||||
"HebrewAlphabet",
|
||||
|
||||
# Number and color classes
|
||||
"Number",
|
||||
"Color",
|
||||
"Planet",
|
||||
"God",
|
||||
"Trigram",
|
||||
"Hexagram",
|
||||
"Cipher",
|
||||
"CipherResult",
|
||||
|
||||
# Data loader and functions
|
||||
"CardDataLoader",
|
||||
"calculate_digital_root",
|
||||
]
|
||||
129
src/tarot/attributes.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Tarot card attributes and Kabbalistic data structures.
|
||||
|
||||
This module re-exports shared attributes from utils and defines Tarot-specific
|
||||
attribute classes for cards.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
# Re-export shared attributes from utils
|
||||
from utils.attributes import (
|
||||
Element,
|
||||
ElementType,
|
||||
Number,
|
||||
Color,
|
||||
Colorscale,
|
||||
Planet,
|
||||
God,
|
||||
Cipher,
|
||||
CipherResult,
|
||||
Perfume,
|
||||
Note,
|
||||
Meaning,
|
||||
)
|
||||
|
||||
# Re-export attributes from other modules for convenience/backward compatibility
|
||||
from kaballah.attributes import (
|
||||
Sephera,
|
||||
PeriodicTable,
|
||||
TreeOfLife,
|
||||
Correspondences,
|
||||
Path,
|
||||
)
|
||||
from letter.attributes import (
|
||||
Letter,
|
||||
EnglishAlphabet,
|
||||
GreekAlphabet,
|
||||
HebrewAlphabet,
|
||||
DoublLetterTrump,
|
||||
EnochianLetter,
|
||||
EnochianSpirit,
|
||||
EnochianTablet,
|
||||
EnochianGridPosition,
|
||||
EnochianArchetype,
|
||||
)
|
||||
from letter.iChing_attributes import (
|
||||
Trigram,
|
||||
Hexagram,
|
||||
)
|
||||
from temporal.attributes import (
|
||||
Month,
|
||||
Weekday,
|
||||
Hour,
|
||||
ClockHour,
|
||||
Zodiac,
|
||||
Degree,
|
||||
AstrologicalInfluence,
|
||||
)
|
||||
|
||||
# Alias Day to Weekday for backward compatibility (Day in this context was Day of Week)
|
||||
Day = Weekday
|
||||
|
||||
__all__ = [
|
||||
# Re-exported from utils
|
||||
"Element",
|
||||
"ElementType",
|
||||
"Number",
|
||||
"Color",
|
||||
"Colorscale",
|
||||
"Planet",
|
||||
"God",
|
||||
"Cipher",
|
||||
"CipherResult",
|
||||
"Perfume",
|
||||
"Note",
|
||||
# Re-exported from kaballah
|
||||
"Sephera",
|
||||
"PeriodicTable",
|
||||
"TreeOfLife",
|
||||
"Correspondences",
|
||||
"Path",
|
||||
# Re-exported from letter
|
||||
"Letter",
|
||||
"EnglishAlphabet",
|
||||
"GreekAlphabet",
|
||||
"HebrewAlphabet",
|
||||
"DoublLetterTrump",
|
||||
"EnochianLetter",
|
||||
"EnochianSpirit",
|
||||
"EnochianTablet",
|
||||
"EnochianGridPosition",
|
||||
"EnochianArchetype",
|
||||
# Re-exported from letter.iChing
|
||||
"Trigram",
|
||||
"Hexagram",
|
||||
# Re-exported from temporal
|
||||
"Month",
|
||||
"Day",
|
||||
"Weekday",
|
||||
"Hour",
|
||||
"ClockHour",
|
||||
"Zodiac",
|
||||
"Degree",
|
||||
"AstrologicalInfluence",
|
||||
# Tarot-core classes defined below
|
||||
"Suit",
|
||||
"Meaning",
|
||||
"CardImage",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Suit:
|
||||
"""Represents a tarot suit."""
|
||||
name: str
|
||||
element: 'ElementType'
|
||||
tarot_correspondence: str
|
||||
number: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class CardImage:
|
||||
"""Represents an image associated with a card."""
|
||||
filename: str
|
||||
artist: str
|
||||
deck_name: str
|
||||
url: Optional[str] = None
|
||||
|
||||
26
src/tarot/card/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Card namespace - access Tarot cards and deck information."""
|
||||
|
||||
from .card import CardAccessor
|
||||
from .details import CardDetailsRegistry
|
||||
from .loader import (
|
||||
load_card_details,
|
||||
load_deck_details,
|
||||
get_cards_by_suit,
|
||||
filter_cards_by_keywords,
|
||||
print_card_details,
|
||||
get_card_info,
|
||||
)
|
||||
from .image_loader import ImageDeckLoader, load_deck_images
|
||||
|
||||
__all__ = [
|
||||
"CardAccessor",
|
||||
"CardDetailsRegistry",
|
||||
"load_card_details",
|
||||
"load_deck_details",
|
||||
"get_cards_by_suit",
|
||||
"filter_cards_by_keywords",
|
||||
"print_card_details",
|
||||
"get_card_info",
|
||||
"ImageDeckLoader",
|
||||
"load_deck_images",
|
||||
]
|
||||
331
src/tarot/card/card.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""
|
||||
Tarot deck and card accessor module.
|
||||
|
||||
Provides fluent access to Tarot cards through Tarot.deck namespace.
|
||||
|
||||
Usage:
|
||||
from tarot.card import Deck, Card
|
||||
|
||||
card = Deck.card(3) # Get card 3
|
||||
cards = Deck.card.filter(arcana="Major") # Get all Major Arcana
|
||||
cards = Deck.card.filter(arcana="Minor") # Get all Minor Arcana
|
||||
cards = Deck.card.filter(suit="Cups") # Get all Cups
|
||||
cards = Deck.card.filter(arcana="Minor", suit="Wands", pip=5) # 5 of Wands
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from utils.filter import universal_filter, format_results
|
||||
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
|
||||
|
||||
|
||||
class CardList(list):
|
||||
"""Custom list class for cards that formats nicely when printed."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format card list for display."""
|
||||
if not self:
|
||||
return "(no cards)"
|
||||
return _format_cards(self)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return string representation."""
|
||||
return self.__str__()
|
||||
|
||||
|
||||
def _format_cards(cards: List['Card']) -> str:
|
||||
"""
|
||||
Format a list of cards for user-friendly display.
|
||||
|
||||
Args:
|
||||
cards: List of Card objects to format
|
||||
|
||||
Returns:
|
||||
Formatted string with each card separated by blank lines
|
||||
"""
|
||||
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
|
||||
|
||||
lines = []
|
||||
for card in cards:
|
||||
card_num = getattr(card, 'number', '?')
|
||||
card_name = getattr(card, 'name', 'Unknown')
|
||||
lines.append(f"--- {card_num}: {card_name} ---")
|
||||
|
||||
# Format all attributes with proper nesting
|
||||
for attr_name, attr_value in get_object_attributes(card):
|
||||
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)
|
||||
|
||||
|
||||
class CardAccessor:
|
||||
"""
|
||||
Fluent accessor for Tarot cards in the deck.
|
||||
|
||||
Usage:
|
||||
Tarot.deck.card(3) # Get card 3
|
||||
Tarot.deck.card.filter(arcana="Major") # Get all Major Arcana
|
||||
Tarot.deck.card.filter(arcana="Minor") # Get all Minor Arcana
|
||||
Tarot.deck.card.filter(suit="Cups") # Get all Cups
|
||||
Tarot.deck.card.filter(arcana="Minor", suit="Wands") # Get all Wand cards
|
||||
Tarot.deck.card.display_filter(arcana="Major") # Display Major Arcana
|
||||
"""
|
||||
|
||||
_deck: Optional['Deck'] = None
|
||||
_initialized: bool = False
|
||||
|
||||
def _ensure_initialized(self) -> None:
|
||||
"""Lazy-load the Deck on first access."""
|
||||
if not self._initialized:
|
||||
from tarot.deck import Deck as DeckClass
|
||||
CardAccessor._deck = DeckClass()
|
||||
CardAccessor._initialized = True
|
||||
|
||||
def __call__(self, number: int) -> Optional['Card']:
|
||||
"""Get a card by number."""
|
||||
self._ensure_initialized()
|
||||
if self._deck is None:
|
||||
return None
|
||||
for card in self._deck.cards:
|
||||
if card.number == number:
|
||||
return card
|
||||
return None
|
||||
|
||||
def filter(self, **kwargs) -> CardList:
|
||||
"""
|
||||
Filter cards by any Card attribute.
|
||||
|
||||
Uses the universal filter from utils.filter for consistency
|
||||
across the entire project.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Card attribute with its value
|
||||
|
||||
Usage:
|
||||
Tarot.deck.card.filter(arcana="Major")
|
||||
Tarot.deck.card.filter(arcana="Minor", suit="Cups")
|
||||
Tarot.deck.card.filter(number=5)
|
||||
Tarot.deck.card.filter(element="Fire")
|
||||
Tarot.deck.card.filter(pip=3)
|
||||
|
||||
Returns:
|
||||
CardList of Card objects matching all filters
|
||||
"""
|
||||
self._ensure_initialized()
|
||||
if self._deck is None:
|
||||
return CardList()
|
||||
return CardList(universal_filter(self._deck.cards, **kwargs))
|
||||
|
||||
def display_filter(self, **kwargs) -> str:
|
||||
"""
|
||||
Filter cards and display results nicely formatted.
|
||||
|
||||
Combines filtering and formatting in one call.
|
||||
|
||||
Args:
|
||||
**kwargs: Any Card attribute with its value
|
||||
|
||||
Returns:
|
||||
Formatted string with filtered cards
|
||||
|
||||
Example:
|
||||
print(Tarot.deck.card.display_filter(arcana="Major"))
|
||||
"""
|
||||
results = self.filter(**kwargs)
|
||||
return format_results(results)
|
||||
|
||||
def display(self) -> str:
|
||||
"""
|
||||
Format all cards in the deck for user-friendly display.
|
||||
|
||||
Returns a formatted string with each card separated by blank lines.
|
||||
Nested objects are indented and separated with their own sections.
|
||||
"""
|
||||
self._ensure_initialized()
|
||||
if self._deck is None:
|
||||
return "(deck not initialized)"
|
||||
return _format_cards(self._deck.cards)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the complete Tarot deck structure built from actual cards."""
|
||||
self._ensure_initialized()
|
||||
if self._deck is None:
|
||||
return "CardAccessor (deck not initialized)"
|
||||
|
||||
lines = [
|
||||
"Tarot Deck Structure",
|
||||
"=" * 60,
|
||||
"",
|
||||
"The 78-card Tarot deck organized by structure and correspondence:",
|
||||
"",
|
||||
]
|
||||
|
||||
# Build structure from actual cards
|
||||
major_arcana = [c for c in self._deck.cards if c.arcana == "Major"]
|
||||
minor_arcana = [c for c in self._deck.cards if c.arcana == "Minor"]
|
||||
|
||||
# Major Arcana
|
||||
if major_arcana:
|
||||
lines.append(f"MAJOR ARCANA ({len(major_arcana)} cards):")
|
||||
fool = next((c for c in major_arcana if c.number == 0), None)
|
||||
world = next((c for c in major_arcana if c.number == 21), None)
|
||||
if fool and world:
|
||||
lines.append(f" Special Pair: {fool.name} ({fool.number}) - {world.name} ({world.number})")
|
||||
|
||||
double_letter_trumps = [c for c in major_arcana if 3 <= c.number <= 21]
|
||||
lines.append(f" Double Letter Trumps ({len(double_letter_trumps)} cards): Cards 3-21")
|
||||
lines.append("")
|
||||
|
||||
# Minor Arcana
|
||||
if minor_arcana:
|
||||
lines.append(f"MINOR ARCANA ({len(minor_arcana)} cards - 4 suits × 14 ranks):")
|
||||
lines.append("")
|
||||
|
||||
# Aces
|
||||
aces = [c for c in minor_arcana if hasattr(c, 'pip') and c.pip == 1]
|
||||
if aces:
|
||||
lines.append(f" ACES ({len(aces)} cards - The Root Powers):")
|
||||
for ace in aces:
|
||||
suit_name = ace.suit.name if hasattr(ace.suit, 'name') else str(ace.suit)
|
||||
lines.append(f" Ace of {suit_name}")
|
||||
lines.append("")
|
||||
|
||||
# Pips (2-10)
|
||||
pips = [c for c in minor_arcana if hasattr(c, 'pip') and 2 <= c.pip <= 10]
|
||||
if pips:
|
||||
lines.append(f" PIPS ({len(pips)} cards - 2-10 of each suit):")
|
||||
# Group by suit
|
||||
suits_dict = {}
|
||||
for pip in pips:
|
||||
suit_name = pip.suit.name if hasattr(pip.suit, 'name') else str(pip.suit)
|
||||
if suit_name not in suits_dict:
|
||||
suits_dict[suit_name] = []
|
||||
suits_dict[suit_name].append(pip)
|
||||
|
||||
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
|
||||
if suit_name in suits_dict:
|
||||
pip_nums = sorted([p.pip for p in suits_dict[suit_name]])
|
||||
lines.append(f" {suit_name}: {', '.join(str(n) for n in pip_nums)}")
|
||||
lines.append("")
|
||||
|
||||
# Court Cards
|
||||
courts = [c for c in minor_arcana if hasattr(c, 'court_rank') and c.court_rank]
|
||||
if courts:
|
||||
lines.append(f" COURT CARDS ({len(courts)} cards - 4 ranks × 4 suits):")
|
||||
# Get unique ranks and their order
|
||||
rank_order = {"Knight": 0, "Prince": 1, "Princess": 2, "Queen": 3}
|
||||
lines.append(" Rank order per suit: Knight, Prince, Princess, Queen")
|
||||
lines.append("")
|
||||
|
||||
# Group by suit
|
||||
suits_dict = {}
|
||||
for court in courts:
|
||||
suit_name = court.suit.name if hasattr(court.suit, 'name') else str(court.suit)
|
||||
if suit_name not in suits_dict:
|
||||
suits_dict[suit_name] = []
|
||||
suits_dict[suit_name].append(court)
|
||||
|
||||
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
|
||||
if suit_name in suits_dict:
|
||||
suit_courts = sorted(suits_dict[suit_name],
|
||||
key=lambda c: rank_order.get(c.court_rank, 99))
|
||||
court_names = [c.court_rank for c in suit_courts]
|
||||
lines.append(f" {suit_name}: {', '.join(court_names)}")
|
||||
lines.append("")
|
||||
|
||||
# Element correspondences
|
||||
lines.append("SUIT CORRESPONDENCES:")
|
||||
suits_info = {}
|
||||
for card in minor_arcana:
|
||||
if hasattr(card, 'suit') and card.suit:
|
||||
suit_name = card.suit.name if hasattr(card.suit, 'name') else str(card.suit)
|
||||
if suit_name not in suits_info:
|
||||
# Extract element info
|
||||
element_name = "Unknown"
|
||||
if hasattr(card.suit, 'element') and card.suit.element:
|
||||
if hasattr(card.suit.element, 'name'):
|
||||
element_name = card.suit.element.name
|
||||
else:
|
||||
element_name = str(card.suit.element)
|
||||
|
||||
# Extract zodiac signs
|
||||
zodiac_signs = []
|
||||
if hasattr(card.suit, 'element') and card.suit.element:
|
||||
if hasattr(card.suit.element, 'zodiac_signs'):
|
||||
zodiac_signs = card.suit.element.zodiac_signs
|
||||
|
||||
# Extract keywords
|
||||
keywords = []
|
||||
if hasattr(card.suit, 'element') and card.suit.element:
|
||||
if hasattr(card.suit.element, 'keywords'):
|
||||
keywords = card.suit.element.keywords
|
||||
|
||||
suits_info[suit_name] = {
|
||||
'element': element_name,
|
||||
'zodiac': zodiac_signs,
|
||||
'keywords': keywords
|
||||
}
|
||||
|
||||
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
|
||||
if suit_name in suits_info:
|
||||
info = suits_info[suit_name]
|
||||
lines.append(f" {suit_name} ({info['element']}):")
|
||||
if info['zodiac']:
|
||||
lines.append(f" Zodiac: {', '.join(info['zodiac'])}")
|
||||
if info['keywords']:
|
||||
lines.append(f" Keywords: {', '.join(info['keywords'])}")
|
||||
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"Total: {len(self._deck.cards)} cards")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a nice representation of the deck accessor."""
|
||||
return self.__str__()
|
||||
|
||||
def spread(self, spread_name: str) -> str:
|
||||
"""
|
||||
Draw a Tarot card reading for a spread.
|
||||
|
||||
Automatically draws random cards for each position in the spread,
|
||||
with random reversals. Returns formatted reading with card details.
|
||||
|
||||
Args:
|
||||
spread_name: Name of the spread (case-insensitive, underscores or spaces)
|
||||
Examples: 'Celtic Cross', 'golden dawn', 'three_card', 'tree of life'
|
||||
|
||||
Returns:
|
||||
Formatted string with spread positions, drawn cards, and interpretations
|
||||
|
||||
Raises:
|
||||
ValueError: If spread name not found
|
||||
|
||||
Examples:
|
||||
print(Tarot.deck.card.spread('Celtic Cross'))
|
||||
print(Tarot.deck.card.spread('golden dawn'))
|
||||
print(Tarot.deck.card.spread('three card'))
|
||||
print(Tarot.deck.card.spread('tree of life'))
|
||||
"""
|
||||
from tarot.card.spread import Spread, draw_spread, SpreadReading
|
||||
|
||||
# Initialize deck if needed
|
||||
self._ensure_initialized()
|
||||
|
||||
# Create spread object
|
||||
spread = Spread(spread_name)
|
||||
|
||||
# Draw cards for the spread
|
||||
drawn_cards = draw_spread(spread, self._deck.cards if self._deck else None)
|
||||
|
||||
# Create and return reading
|
||||
reading = SpreadReading(spread, drawn_cards)
|
||||
return str(reading)
|
||||
1750
src/tarot/card/data.py
Normal file
557
src/tarot/card/details.py
Normal file
@@ -0,0 +1,557 @@
|
||||
"""Card details and interpretations for all 78 Tarot cards.
|
||||
|
||||
This module provides interpretive data (explanations, keywords, guidance) for cards.
|
||||
Registry is keyed by card position (1-78), independent of deck-specific names.
|
||||
|
||||
Deck order: Cups (1-14), Pentacles (15-28), Swords (29-42),
|
||||
Major Arcana (43-64), Wands (65-78)
|
||||
|
||||
Usage:
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
|
||||
registry = CardDetailsRegistry()
|
||||
details = registry.get_by_position(44) # Get details for card at position 44
|
||||
|
||||
# Or load into a card object:
|
||||
from tarot.deck import Deck
|
||||
deck = Deck()
|
||||
card = deck.cards[43] # Card at position 44 (0-indexed)
|
||||
registry.load_into_card(card)
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.deck import Card
|
||||
|
||||
|
||||
class CardDetailsRegistry:
|
||||
"""Registry for storing interpretive data for all 78 Tarot cards.
|
||||
|
||||
Uses card position (1-78) as the unique identifier, independent of deck names.
|
||||
This allows the same card details to apply across different deck variants.
|
||||
|
||||
Deck order:
|
||||
- 1-14: Cups (Ace, Ten, 2-9, Knight, Prince, Princess, Queen)
|
||||
- 15-28: Pentacles (same structure)
|
||||
- 29-42: Swords (same structure)
|
||||
- 43-64: Major Arcana (Fool through Universe)
|
||||
- 65-78: Wands (same structure)
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the card details registry with interpretive data."""
|
||||
self._details: Dict[str, Dict[str, Any]] = self._build_registry()
|
||||
# Map card positions (1-78) to registry keys
|
||||
self._position_map = self._build_position_map()
|
||||
|
||||
@staticmethod
|
||||
def key_to_roman(key: int) -> str:
|
||||
"""
|
||||
Convert a numeric key to Roman numerals.
|
||||
|
||||
Args:
|
||||
key: The numeric key (0-21 for major arcana)
|
||||
|
||||
Returns:
|
||||
Roman numeral representation (e.g., 21 -> "XXI", 0 -> "o")
|
||||
"""
|
||||
# Special case: 0 -> "o" (letter O for The Fool)
|
||||
if key == 0:
|
||||
return "o"
|
||||
|
||||
val = [
|
||||
1000, 900, 500, 400,
|
||||
100, 90, 50, 40,
|
||||
10, 9, 5, 4,
|
||||
1
|
||||
]
|
||||
syms = [
|
||||
"M", "CM", "D", "CD",
|
||||
"C", "XC", "L", "XL",
|
||||
"X", "IX", "V", "IV",
|
||||
"I"
|
||||
]
|
||||
roman_num = ''
|
||||
i = 0
|
||||
while key > 0:
|
||||
for _ in range(key // val[i]):
|
||||
roman_num += syms[i]
|
||||
key -= val[i]
|
||||
i += 1
|
||||
return roman_num if roman_num else "o"
|
||||
|
||||
def _build_position_map(self) -> Dict[int, str]:
|
||||
"""
|
||||
Build a mapping from card position (1-78) to registry key.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping position to registry key
|
||||
"""
|
||||
position_map = {}
|
||||
|
||||
# Positions 1-14: Cups (Ace, Ten, 2-9, Knight, Prince, Princess, Queen)
|
||||
cups_names = ["Ace of Cups", "Ten of Cups", "Two of Cups", "Three of Cups",
|
||||
"Four of Cups", "Five of Cups", "Six of Cups", "Seven of Cups",
|
||||
"Eight of Cups", "Nine of Cups", "Knight of Cups", "Prince of Cups",
|
||||
"Princess of Cups", "Queen of Cups"]
|
||||
for pos, name in enumerate(cups_names, start=1):
|
||||
position_map[pos] = name
|
||||
|
||||
# Positions 15-28: Pentacles (same structure)
|
||||
pentacles_names = ["Ace of Pentacles", "Ten of Pentacles", "Two of Pentacles", "Three of Pentacles",
|
||||
"Four of Pentacles", "Five of Pentacles", "Six of Pentacles", "Seven of Pentacles",
|
||||
"Eight of Pentacles", "Nine of Pentacles", "Knight of Pentacles", "Prince of Pentacles",
|
||||
"Princess of Pentacles", "Queen of Pentacles"]
|
||||
for pos, name in enumerate(pentacles_names, start=15):
|
||||
position_map[pos] = name
|
||||
|
||||
# Positions 29-42: Swords (same structure)
|
||||
swords_names = ["Ace of Swords", "Ten of Swords", "Two of Swords", "Three of Swords",
|
||||
"Four of Swords", "Five of Swords", "Six of Swords", "Seven of Swords",
|
||||
"Eight of Swords", "Nine of Swords", "Knight of Swords", "Prince of Swords",
|
||||
"Princess of Swords", "Queen of Swords"]
|
||||
for pos, name in enumerate(swords_names, start=29):
|
||||
position_map[pos] = name
|
||||
|
||||
# Positions 43-64: Major Arcana (mapped to Roman numerals)
|
||||
major_arcana_keys = ["o", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX",
|
||||
"X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX",
|
||||
"XX", "XXI"]
|
||||
for pos, key in enumerate(major_arcana_keys, start=43):
|
||||
position_map[pos] = key
|
||||
|
||||
# Positions 65-78: Wands (same structure)
|
||||
wands_names = ["Ace of Wands", "Ten of Wands", "Two of Wands", "Three of Wands",
|
||||
"Four of Wands", "Five of Wands", "Six of Wands", "Seven of Wands",
|
||||
"Eight of Wands", "Nine of Wands", "Knight of Wands", "Prince of Wands",
|
||||
"Princess of Wands", "Queen of Wands"]
|
||||
for pos, name in enumerate(wands_names, start=65):
|
||||
position_map[pos] = name
|
||||
|
||||
return position_map
|
||||
|
||||
def get_by_position(self, position: int) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get details for a card by its position (1-78).
|
||||
|
||||
Args:
|
||||
position: Card position (1-78)
|
||||
|
||||
Returns:
|
||||
Dictionary containing card details, or None if not found
|
||||
"""
|
||||
registry_key = self._position_map.get(position)
|
||||
if registry_key is None:
|
||||
return None
|
||||
return self._details.get(registry_key)
|
||||
|
||||
def _build_registry(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Build the interpretive data registry (card structure comes from Deck).
|
||||
|
||||
Stores only unique interpretive data (explanation, keywords, guidance).
|
||||
Card names and structure are sourced from Deck for DRY compliance.
|
||||
"""
|
||||
return {
|
||||
# Major Arcana (0-21) - Interpretive data only
|
||||
"o": {
|
||||
"explanation": "The Fool represents new beginnings, innocence, and spontaneity. This card signifies a fresh start or embarking on a new journey with optimism and faith.",
|
||||
"interpretation": "Beginning of the Great Work, innocence; a fool for love; divine madness. Reason is transcended. Take the leap. Gain or loss through foolish actions.",
|
||||
"keywords": ["new beginnings", "innocence", "faith", "spontaneity", "potential"],
|
||||
"reversed_keywords": ["recklessness", "naivety", "poor judgment", "folly"],
|
||||
"guidance": "Trust in the unfolding of your path. Embrace new opportunities with awareness and openness.",
|
||||
},
|
||||
"I": {
|
||||
"explanation": "The Magician embodies manifestation, resourcefulness, and personal power. This card shows mastery of skills and the ability to turn ideas into reality.",
|
||||
"interpretation": "Communication; Conscious Will; the process of continuous creation; ambiguity; deceptionl Things may not be as they appear. Concentration, meditation; mind used to direct the Will. Manipulation; crafty maneuverings.",
|
||||
"keywords": ["manifestation", "resourcefulness", "power", "inspired action", "concentration"],
|
||||
"reversed_keywords": ["manipulation", "poor planning", "untapped talents", "lack of direction"],
|
||||
"guidance": "Focus your energy and intention on what you want to manifest. You have the tools and talents you need.",
|
||||
},
|
||||
"II": {
|
||||
"explanation": "The High Priestess represents intuition, sacred knowledge, and the subconscious mind. She embodies mystery and inner wisdom.",
|
||||
"interpretation": "Symbol of highest initiation; link between the archetypal and Formative Worlds. An initiatrixl Wooing by enchantment. possibility. The Idea behind the Form. Fluctuationl Time may not be right for a decision concerning mundane matters.",
|
||||
"keywords": ["intuition", "sacred knowledge", "divine feminine", "the subconscious", "mystery"],
|
||||
"reversed_keywords": ["hidden information", "silence", "disconnection from intuition", "superficiality"],
|
||||
"guidance": "Listen to your inner voice. The answers you seek lie within. Trust the wisdom of your intuition.",
|
||||
},
|
||||
"III": {
|
||||
"explanation": "The Empress symbolizes abundance, fertility, and nurturing energy. She represents creativity, sensuality, and the power of manifestation through nurturing.",
|
||||
"interpretation": "The Holy Grail. love unites the Will. Love; beauty; friendship; success; passive balance. The feminine point of view. The door is open. Disregard the details and concentrate on the big picture.",
|
||||
"keywords": ["abundance", "fertility", "femininity", "beauty", "nature", "creativity"],
|
||||
"reversed_keywords": ["dependency", "creative block", "neediness", "underdevelopment"],
|
||||
"guidance": "Nurture yourself and others. Allow yourself to enjoy the fruits of your labor and appreciate beauty.",
|
||||
},
|
||||
"IV": {
|
||||
"explanation": "The Emperor represents authority, leadership, and established power. He embodies structure, discipline, and protection through strength and control.",
|
||||
"interpretation": "Creative wisdom radiating upon the organized man and woman. Domination after conquest; quarrelsomeness; paternal love; ambition. Thought ruled by creative, masculine, fiery energy. Stubbornness; war; authority; energy in its most temporal form. Swift immpermaent action over confidence.",
|
||||
"keywords": ["authority", "leadership", "power", "structure", "protection", "discipline"],
|
||||
"reversed_keywords": ["weakness", "ineffectual leadership", "lack of discipline", "tyranny"],
|
||||
"guidance": "Step into your power with confidence. Establish clear boundaries and structure. Lead by example.",
|
||||
},
|
||||
"V": {
|
||||
"explanation": "The Hierophant represents tradition, conventional wisdom, and spiritual authority. This card embodies education, ceremony, and moral values.",
|
||||
"interpretation": "The Holy Guardian Angel. The uniting of t hat which is above with that which is below. Love is indicated, but the nature of that love is not yet to be revealed. Inspiration; teaching; organization; discipline; strength; endurance; toil; help from superiors.",
|
||||
"keywords": ["tradition", "spirituality", "wisdom", "ritual", "morality", "ethics"],
|
||||
"reversed_keywords": ["rebellion", "unconventionality", "questioning authority", "dogmatism"],
|
||||
"guidance": "Seek guidance from established wisdom. Respect traditions while finding your own spiritual path.",
|
||||
},
|
||||
"VI": {
|
||||
"explanation": "The Lovers represents relationships, values alignment, and the union of opposites. It signifies choice, intimacy, and deep connection.",
|
||||
"interpretation": "Intuition. Be open to your own inner voice. A well-intended, arranged marriage. An artificial union. The need to make a choice with awareness of consequences union; analysis followed by synthesis; indecision; instability; superficiality.",
|
||||
"keywords": ["relationships", "love", "union", "values", "choice", "alignment"],
|
||||
"reversed_keywords": ["disharmony", "misalignment", "conflict", "communication breakdown"],
|
||||
"guidance": "Choose with your heart aligned with your values. Deep connection requires vulnerability and honesty.",
|
||||
},
|
||||
"VII": {
|
||||
"explanation": "The Chariot embodies willpower, determination, and control through focused intention. It represents triumph through discipline and forward momentum.",
|
||||
"interpretation": "Light in the darkness. The burden you carry may be the Holy Grail. Faithfulness; hope; obedience; a protective relationship; firm, even violent adherance to dogma or tradition. Glory; riches; englightened civilization; victory; triumph; chain of command.",
|
||||
"keywords": ["determination", "willpower", "control", "momentum", "victory", "focus"],
|
||||
"reversed_keywords": ["lack of control", "haste", "resistance", "moving backward"],
|
||||
"guidance": "Take the reins of your life. Move forward with determination and clear direction. You have the power.",
|
||||
},
|
||||
"VIII": {
|
||||
"explanation": "Strength represents inner power, courage, and compassion. It shows mastery through gentleness and the ability to face challenges with calm confidence.",
|
||||
"interpretation": "Equilibrium; karmic law; the dance of life; all possibilities. The woman satisfied. Balance; weigh each thought against its opposite. Lawsuits; treaties. Pause and look before you leap.",
|
||||
"keywords": ["strength", "courage", "patience", "compassion", "control", "confidence"],
|
||||
"reversed_keywords": ["weakness", "self-doubt", "lack of composure", "poor control"],
|
||||
"guidance": "True strength comes from within. Face challenges with courage and compassion for yourself and others.",
|
||||
},
|
||||
"IX": {
|
||||
"explanation": "The Hermit represents introspection, spiritual seeking, and inner guidance. This card embodies solitude, wisdom gained through reflection, and self-discovery.",
|
||||
"interpretation": "Divine seed of all things. By silence comes inspiration and wisdom. Wandering alone; temporary solitude; creative contemplation; a virgin. Retirement from involvement in current events.",
|
||||
"keywords": ["introspection", "spiritual seeking", "inner light", "wisdom", "solitude", "truth"],
|
||||
"reversed_keywords": ["loneliness", "isolation", "lost", "paranoia", "disconnection"],
|
||||
"guidance": "Take time for introspection and self-discovery. Your inner light guides your path. Seek solitude for wisdom.",
|
||||
},
|
||||
"X": {
|
||||
"explanation": "The Wheel of Fortune represents cycles, destiny, and the turning points of life. It embodies luck, karma, and the natural ebb and flow of existence.",
|
||||
"interpretation": "Continual change. In the midst of revolving phenomena, reaach joyously the motionless center. Carefree love; wanton pleasure; amusement; fun; change of fortune, usually good.",
|
||||
"keywords": ["fate", "destiny", "cycles", "fortune", "karma", "turning point"],
|
||||
"reversed_keywords": ["bad luck", "resistance to change", "broken cycles", "misfortune"],
|
||||
"guidance": "Trust in the cycles of life. What goes up must come down. Embrace change as part of your journey.",
|
||||
},
|
||||
"XI": {
|
||||
"explanation": "Justice represents fairness, truth, and balance. It embodies accountability, clear judgment, and the consequences of actions both past and present.",
|
||||
"interpretation": "Understanding; the Will of New Aeon; passion; sense smitten with ecstasy. let love devour all. Energy independent of reason. Strength; courage; utilization of magical power.",
|
||||
"keywords": ["justice", "fairness", "truth", "cause and effect", "balance", "accountability"],
|
||||
"reversed_keywords": ["injustice", "bias", "lack of accountability", "dishonesty"],
|
||||
"guidance": "Seek the truth and act with fairness. Take responsibility for your actions. Balance is key.",
|
||||
},
|
||||
"XII": {
|
||||
"explanation": "The Hanged Man represents suspension, letting go, and seeing things from a new perspective. It embodies surrender, pause, and gaining wisdom through sacrifice.",
|
||||
"interpretation": "Redemption, sacrifice, annihilation in the beloved; martyrdom; loss; torment; suspension; death; suffering.",
|
||||
"keywords": ["suspension", "restriction", "letting go", "new perspective", "surrender", "pause"],
|
||||
"reversed_keywords": ["resistance", "stalling", "unwillingness to change", "impatience"],
|
||||
"guidance": "Pause and reflect. What are you holding onto? Surrender control and trust the process.",
|
||||
},
|
||||
"XIII": {
|
||||
"explanation": "Death represents transformation, endings, and new beginnings. This card embodies major life transitions, the release of the old, and inevitable change.",
|
||||
"interpretation": "End of cycle; transformation; raw sexuality. Sex is death. Stress becomes intolerable. Any change is welcome. Time; age; unexpected change; death.",
|
||||
"keywords": ["transformation", "transition", "endings", "beginnings", "change", "acceptance"],
|
||||
"reversed_keywords": ["resistance to change", "stagnation", "missed opportunity", "delay"],
|
||||
"guidance": "Release what no longer serves you. Transformation is inevitable. Trust in the cycle of death and rebirth.",
|
||||
},
|
||||
"XIV": {
|
||||
"explanation": "Temperance represents balance, moderation, and harmony. It embodies blending of opposites, inner peace through balance, and finding your rhythm.",
|
||||
"interpretation": "Transmutation through union of opposites. A perfect marriage exalts and transforms each partner. The scientific method. Success follows complex maneuvers.",
|
||||
"keywords": ["balance", "moderation", "harmony", "patience", "timing", "peace"],
|
||||
"reversed_keywords": ["imbalance", "excess", "conflict", "intemperance", "discord"],
|
||||
"guidance": "Seek balance in all things. Blend opposing forces. Find your rhythm through moderation and patience.",
|
||||
},
|
||||
"XV": {
|
||||
"explanation": "The Devil represents bondage, materialism, and shadow aspects of self. It embodies addictions, illusions, and the consequences of giving away personal power.",
|
||||
"interpretation": "Thou hast no right but to do thy will. Obession; temptation; ecstasy found in every phenomenon; creative action, yet sublimely careless of result; unscrupulous ambition; strength.",
|
||||
"keywords": ["bondage", "materialism", "playfulness", "shadow self", "sexuality", "excess"],
|
||||
"reversed_keywords": ["freedom", "detachment", "reclaiming power", "breaking free"],
|
||||
"guidance": "Examine what binds you. Acknowledge your shadow. You hold the key to your own freedom.",
|
||||
},
|
||||
"XVI": {
|
||||
"explanation": "The Tower represents sudden disruption, revelation, and breakthrough through crisis. It embodies sudden change, truth revealed, and necessary destruction.",
|
||||
"interpretation": "Escape from the prison of organized life; renunciation of love; quarreling. Plans are destroyed. War; danger; sudden death.",
|
||||
"keywords": ["sudden change", "upheaval", "revelation", "breakdown", "breakthrough", "chaos"],
|
||||
"reversed_keywords": ["resistance to change", "averted crisis", "delay", "stagnation"],
|
||||
"guidance": "Crisis brings clarity. Though change is sudden and jarring, it clears away the false and brings truth.",
|
||||
},
|
||||
"XVII": {
|
||||
"explanation": "The Star represents hope, guidance, and inspiration. It embodies clarity of purpose, spiritual insight, and the light that guides your path forward.",
|
||||
"interpretation": "Clairvoyance; visions; drams; hope; love; yearning; realization of inexhaustible possibilities; dreaminess; unexpected help; renewal.",
|
||||
"keywords": ["hope", "faith", "inspiration", "vision", "guidance", "spirituality"],
|
||||
"reversed_keywords": ["hopelessness", "despair", "lack of direction", "lost", "obscured"],
|
||||
"guidance": "Let your inner light shine. Trust in your vision. Hope and guidance light your path forward.",
|
||||
},
|
||||
"XVIII": {
|
||||
"explanation": "The Moon represents illusion, intuition, and the subconscious mind. It embodies mystery, dreams, and navigating by inner knowing rather than sight.",
|
||||
"interpretation": "The Dark night of the soul; deception; falsehood; illusion; madness; the threshold of significant change.",
|
||||
"keywords": ["illusion", "intuition", "uncertainty", "subconscious", "dreams", "mystery"],
|
||||
"reversed_keywords": ["clarity", "truth revealed", "release from illusion", "awakening"],
|
||||
"guidance": "Trust your intuition to navigate mystery. What appears illusory contains deeper truths worth exploring.",
|
||||
},
|
||||
"XIX": {
|
||||
"explanation": "The Sun represents joy, clarity, and vitality. It embodies success, positive energy, and the radiance of authentic self-expression.",
|
||||
"interpretation": "Lord of the New Aeon. Spiritual emancipation. Pleasure; shamelessness; vanity; frankness. Freedom brings sanity. Glory; riches; enlightened civilization.",
|
||||
"keywords": ["success", "joy", "clarity", "vitality", "warmth", "authenticity"],
|
||||
"reversed_keywords": ["temporary darkness", "lost vitality", "setback", "sadness"],
|
||||
"guidance": "Celebrate your success. Let your authentic self shine. Joy and clarity light your way.",
|
||||
},
|
||||
"XX": {
|
||||
"explanation": "Judgement represents awakening, calling, and significant decisions. It embodies reckoning, rebirth, and responding to a higher calling.",
|
||||
"interpretation": "Let every act be an act of Worship; let every act be an act of Love. Final decision; judgement. Learn from the past. Prepare for the future.",
|
||||
"keywords": ["awakening", "calling", "judgment", "rebirth", "evaluation", "absolution"],
|
||||
"reversed_keywords": ["doubt", "self-doubt", "harsh judgment", "reluctance to change"],
|
||||
"guidance": "Answer your higher calling. Evaluate with compassion. A significant awakening or decision awaits.",
|
||||
},
|
||||
"XXI": {
|
||||
"explanation": "The World represents completion, wholeness, and fulfillment. It embodies the end of a cycle, achievement of goals, and a sense of unity.",
|
||||
"interpretation": "Completion of the Greatk Work; patience; perseverance; stubbornness; serious meditation. Work accomplished.",
|
||||
"keywords": ["completion", "fulfillment", "wholeness", "travel", "unity", "achievement"],
|
||||
"reversed_keywords": ["incomplete", "blocked", "separation", "seeking closure"],
|
||||
"guidance": "A significant cycle completes. You have achieved wholeness. Yet every ending is a new beginning.",
|
||||
},
|
||||
|
||||
# Minor Arcana - Swords
|
||||
"Ace of Swords": {
|
||||
"explanation": "The Ace of Swords represents clarity, breakthrough, and new ideas. It embodies truth emerging, mental clarity, and the power of honest communication.",
|
||||
"interpretation": "New idea or perspective, clarity and truth, breakthrough thinking, mental clarity",
|
||||
"keywords": ["breakthrough", "clarity", "truth", "new ideas", "communication"],
|
||||
"reversed_keywords": ["confusion", "unclear communication", "hidden truth", "mental fog"],
|
||||
"guidance": "A breakthrough arrives. Speak your truth with clarity. Mental clarity reveals new possibilities.",
|
||||
},
|
||||
"Two of Swords": {
|
||||
"explanation": "The Two of Swords represents stalemate, difficult choices, and mental struggle. It embodies indecision, conflicting information, and the need for perspective.",
|
||||
"interpretation": "Stalemate and indecision, difficult choices ahead, conflicting perspectives, mental struggle",
|
||||
"keywords": ["stalemate", "indecision", "confusion", "difficult choice", "standoff"],
|
||||
"reversed_keywords": ["clarity emerging", "decision made", "moving forward", "resolution"],
|
||||
"guidance": "Step back from the conflict. You need more information or perspective before deciding.",
|
||||
},
|
||||
"Three of Swords": {
|
||||
"explanation": "The Three of Swords represents heartbreak, difficult truths, and mental anguish. It embodies challenging communication, painful revelations, and clarity that hurts.",
|
||||
"interpretation": "Difficult truths and heartbreak, communication challenges, mental anguish, clarity through pain",
|
||||
"keywords": ["heartbreak", "sorrow", "difficult truth", "mental anguish", "separation"],
|
||||
"reversed_keywords": ["healing", "moving forward", "forgiveness", "reconciliation"],
|
||||
"guidance": "Difficult truths are emerging. Allow yourself to feel the pain. Healing follows acknowledgment.",
|
||||
},
|
||||
"Four of Swords": {
|
||||
"explanation": "The Four of Swords represents rest, recovery, and mental respite. It embodies the need for pause, recuperation, and gathering strength.",
|
||||
"interpretation": "Rest and recovery, pause and contemplation, gathering strength, needed respite",
|
||||
"keywords": ["rest", "pause", "recovery", "contemplation", "respite"],
|
||||
"reversed_keywords": ["restlessness", "stress", "unwillingness to rest", "agitation"],
|
||||
"guidance": "Take time to rest and recover. Your mind and spirit need respite. Gather your strength.",
|
||||
},
|
||||
"Five of Swords": {
|
||||
"explanation": "The Five of Swords represents conflict, victory at a cost, and difficult truths after battle. It embodies competition with consequences and the emptiness of winning wrongly.",
|
||||
"interpretation": "Conflict and competition, pyrrhic victory, harsh truths, aftermath of conflict",
|
||||
"keywords": ["conflict", "defeat", "victory at cost", "awkwardness", "tension"],
|
||||
"reversed_keywords": ["reconciliation", "resolution", "forgiveness", "peace"],
|
||||
"guidance": "Sometimes victory costs more than it's worth. Seek reconciliation over conquest.",
|
||||
},
|
||||
"Six of Swords": {
|
||||
"explanation": "The Six of Swords represents moving forward, healing journey, and leaving troubles behind. It embodies transition, mental resolution, and the path to better days.",
|
||||
"interpretation": "Moving forward and transition, leaving trouble behind, journey and travel, mental resolution",
|
||||
"keywords": ["transition", "healing journey", "moving forward", "travel", "freedom"],
|
||||
"reversed_keywords": ["stuck", "resistance to change", "delays", "unresolved issues"],
|
||||
"guidance": "A journey of healing begins. Move forward. Leave the past behind. Better days await.",
|
||||
},
|
||||
"Seven of Swords": {
|
||||
"explanation": "The Seven of Swords represents deception, cunning, and strategic retreat. It embodies hidden agendas, betrayal, and escape from difficult situations.",
|
||||
"interpretation": "Deception and cunning, hidden agendas, strategic retreat, betrayal or self-deception",
|
||||
"keywords": ["deception", "cunning", "betrayal", "hidden agenda", "strategy"],
|
||||
"reversed_keywords": ["coming clean", "honesty", "truth revealed", "facing consequences"],
|
||||
"guidance": "Look for hidden truths. Deception may be at play. Where are you deceiving yourself?",
|
||||
},
|
||||
"Eight of Swords": {
|
||||
"explanation": "The Eight of Swords represents restriction, bondage, and self-imposed limitations. It embodies feeling trapped, mental imprisonment, and powerlessness.",
|
||||
"interpretation": "Restriction and bondage, self-imposed limitations, feeling trapped, helplessness",
|
||||
"keywords": ["bondage", "restriction", "trapped", "helplessness", "powerlessness"],
|
||||
"reversed_keywords": ["freedom", "release", "empowerment", "breaking free"],
|
||||
"guidance": "You have more power than you believe. The restrictions may be self-imposed. Free yourself.",
|
||||
},
|
||||
"Nine of Swords": {
|
||||
"explanation": "The Nine of Swords represents anxiety, nightmares, and mental torment. It embodies overthinking, worry, and the burden of negative thoughts.",
|
||||
"interpretation": "Anxiety and worry, nightmares and turmoil, overthinking, mental burden",
|
||||
"keywords": ["anxiety", "worry", "nightmares", "overthinking", "despair"],
|
||||
"reversed_keywords": ["relief", "healing", "moving past", "mental clarity"],
|
||||
"guidance": "Your mind is your greatest torment. Seek support. This darkness passes. Morning follows night.",
|
||||
},
|
||||
"Ten of Swords": {
|
||||
"explanation": "The Ten of Swords represents complete mental/emotional defeat, rock bottom, and the end of suffering. It embodies the culmination of difficulty and the promise of renewal.",
|
||||
"interpretation": "Defeat and rock bottom, end of suffering, difficult conclusion, release from burden",
|
||||
"keywords": ["defeat", "rock bottom", "ending", "relief", "betrayal"],
|
||||
"reversed_keywords": ["recovery", "beginning again", "healing", "hope"],
|
||||
"guidance": "The worst has passed. You've hit bottom. From here, only recovery is possible.",
|
||||
},
|
||||
"Page of Swords": {
|
||||
"explanation": "The Page of Swords represents curious inquiry, new ideas, and youthful intellectual energy. It embodies investigation, learning, and the drive to understand.",
|
||||
"interpretation": "Curiosity and new learning, investigation and inquiry, youthful energy, intellectual development",
|
||||
"keywords": ["curiosity", "inquiry", "new learning", "messages", "vigilance"],
|
||||
"reversed_keywords": ["cynicism", "misinformation", "scattered thinking", "mischief"],
|
||||
"guidance": "Curiosity leads to discovery. Ask questions and investigate. Knowledge empowers.",
|
||||
},
|
||||
"Knight of Swords": {
|
||||
"explanation": "The Knight of Swords represents swift action, directness, and intellectual courage. It embodies confrontation, truth-seeking, and the willingness to challenge.",
|
||||
"interpretation": "Direct communication and action, intellectual courage, challenging situations, swift movement",
|
||||
"keywords": ["action", "impulsiveness", "courage", "conflict", "truth"],
|
||||
"reversed_keywords": ["scatter-brained", "dishonest", "confusion", "retreat"],
|
||||
"guidance": "Speak your truth directly. Act with courage. Swift action brings results.",
|
||||
},
|
||||
"Queen of Swords": {
|
||||
"explanation": "The Queen of Swords represents intellectual power, clarity, and independent thinking. It embodies wisdom gained through experience and clear perception.",
|
||||
"interpretation": "Intellectual power and clarity, independence and perception, wisdom and experience, communication",
|
||||
"keywords": ["clarity", "intelligence", "independence", "truth", "perception"],
|
||||
"reversed_keywords": ["bitter", "manipulative", "cold", "cruel"],
|
||||
"guidance": "Trust your keen intellect. Speak your truth with grace. Clarity empowers.",
|
||||
},
|
||||
"King of Swords": {
|
||||
"explanation": "The King of Swords represents mental mastery, authority through intellect, and the power of truth. It embodies leadership, clear judgment, and strategic thinking.",
|
||||
"interpretation": "Mental mastery and intellect, authority and leadership, justice and fairness, clear judgment",
|
||||
"keywords": ["authority", "intellect", "truth", "leadership", "justice"],
|
||||
"reversed_keywords": ["tyrant", "manipulation", "abuse of power", "cruelty"],
|
||||
"guidance": "Lead with intellect and integrity. Your clarity creates order. Speak truth with authority.",
|
||||
},
|
||||
"Princess of Swords": {
|
||||
"explanation": "The Princess of Swords represents intellectual potential, youthful curiosity, and emerging clarity. It embodies the development of mental acuity and the pursuit of knowledge.",
|
||||
"interpretation": "Intellectual development and potential, emerging clarity, youthful inquiry, pursuit of truth",
|
||||
"keywords": ["clarity emerging", "intellectual potential", "investigation", "truth-seeking", "perception"],
|
||||
"reversed_keywords": ["confusion", "scattered thoughts", "deception", "lack of focus"],
|
||||
"guidance": "Your ability to perceive truth is developing. Stay curious and focused. Clarity is emerging.",
|
||||
},
|
||||
|
||||
# Minor Arcana - Cups
|
||||
"Ace of Cups": {
|
||||
"explanation": "The Ace of Cups represents new emotional beginning, love, and spiritual awakening. It embodies the opening of the heart and new emotional connections.",
|
||||
"interpretation": "New emotional beginning, love and compassion, spiritual awakening, emotional clarity",
|
||||
"keywords": ["love", "new emotion", "compassion", "beginning", "spirituality"],
|
||||
"reversed_keywords": ["blocked emotion", "closed heart", "emotional confusion"],
|
||||
"guidance": "Your heart opens to new possibilities. Emotional connections deepen. Love flows.",
|
||||
},
|
||||
"Two of Cups": {
|
||||
"explanation": "The Two of Cups represents partnership, mutual respect, and emotional connection. It embodies balance, harmony, and the foundation of relationships.",
|
||||
"interpretation": "Partnership and connection, mutual respect and harmony, emotional balance, agreements",
|
||||
"keywords": ["partnership", "love", "connection", "harmony", "commitment"],
|
||||
"reversed_keywords": ["imbalance", "separation", "misalignment", "broken agreement"],
|
||||
"guidance": "Deep connection and harmony are possible. Mutual respect forms the foundation.",
|
||||
},
|
||||
"Three of Cups": {
|
||||
"explanation": "The Three of Cups represents celebration, friendship, and community. It embodies joy, shared experiences, and the warmth of connection.",
|
||||
"interpretation": "Celebration and community, friendship and joy, shared experiences, social harmony",
|
||||
"keywords": ["celebration", "community", "friendship", "joy", "creativity"],
|
||||
"reversed_keywords": ["isolation", "loneliness", "overindulgence", "discord"],
|
||||
"guidance": "Celebrate with friends. Community and connection bring joy. Share in the abundance.",
|
||||
},
|
||||
|
||||
# Minor Arcana - Pentacles
|
||||
"Ace of Pentacles": {
|
||||
"explanation": "The Ace of Pentacles represents new prosperity, material opportunity, and earthly beginnings. It embodies abundance, security, and practical gifts.",
|
||||
"interpretation": "New material opportunity, abundance and prosperity, earthly beginnings, practical gifts",
|
||||
"keywords": ["abundance", "opportunity", "prosperity", "security", "gift"],
|
||||
"reversed_keywords": ["lost opportunity", "scarcity", "blocked prosperity"],
|
||||
"guidance": "Material opportunity arrives. Seize it. Abundance begins with gratitude.",
|
||||
},
|
||||
"Two of Pentacles": {
|
||||
"explanation": "The Two of Pentacles represents balance, flexibility, and managing resources. It embodies juggling priorities, adaptability, and resourcefulness.",
|
||||
"interpretation": "Balance and flexibility, managing resources, adaptability, juggling priorities",
|
||||
"keywords": ["balance", "flexibility", "adaptability", "resourcefulness", "management"],
|
||||
"reversed_keywords": ["imbalance", "mismanagement", "chaos", "loss"],
|
||||
"guidance": "Balance your priorities carefully. Flexibility allows you to manage what comes.",
|
||||
},
|
||||
|
||||
# Minor Arcana - Wands
|
||||
"Ace of Wands": {
|
||||
"explanation": "The Ace of Wands represents new inspiration, creative spark, and passionate new beginning. It embodies potential, growth, and spiritual fire.",
|
||||
"interpretation": "New creative spark, inspiration and potential, passionate beginning, growth opportunity",
|
||||
"keywords": ["inspiration", "potential", "growth", "new beginning", "creativity"],
|
||||
"reversed_keywords": ["blocked inspiration", "delays", "lost potential"],
|
||||
"guidance": "Creative inspiration ignites. Channel this energy into action. Your passion becomes power.",
|
||||
},
|
||||
"Two of Wands": {
|
||||
"explanation": "The Two of Wands represents planning, future vision, and resourcefulness. It embodies potential growth, decisions about direction, and careful preparation.",
|
||||
"interpretation": "Planning and vision, resource management, decisions about direction, future planning",
|
||||
"keywords": ["vision", "planning", "potential", "resourcefulness", "future"],
|
||||
"reversed_keywords": ["lack of vision", "poor planning", "blocked growth"],
|
||||
"guidance": "Plan your future with vision. You have resources to build something great.",
|
||||
},
|
||||
}
|
||||
|
||||
def get(self, card_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get details for a specific card by name.
|
||||
|
||||
Args:
|
||||
card_name: The card's name (e.g., "Princess of Swords", "Ace of Cups")
|
||||
|
||||
Returns:
|
||||
Dictionary containing card details, or None if not found
|
||||
"""
|
||||
return self._details.get(card_name)
|
||||
|
||||
def get_key_as_roman(self, card_name: str) -> Optional[str]:
|
||||
"""
|
||||
Get the card's key displayed as Roman numerals.
|
||||
|
||||
Args:
|
||||
card_name: The card's name (e.g., "The Fool", "The Magician")
|
||||
|
||||
Returns:
|
||||
Roman numeral representation of the key (e.g., "XXI" for 21), or None if not found
|
||||
"""
|
||||
details = self.get(card_name)
|
||||
if details and "key" in details:
|
||||
return self.key_to_roman(details["key"])
|
||||
return None
|
||||
|
||||
def get_all_by_suit(self, suit_name: str) -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Get all details for cards in a specific suit.
|
||||
|
||||
Args:
|
||||
suit_name: The suit name ("Cups", "Pentacles", "Swords", "Wands")
|
||||
|
||||
Returns:
|
||||
Dictionary of card details for that suit
|
||||
"""
|
||||
return {
|
||||
name: details for name, details in self._details.items()
|
||||
if suit_name.lower() in name.lower()
|
||||
}
|
||||
|
||||
def _get_registry_key_for_card(self, card: 'Card') -> Optional[str]:
|
||||
"""
|
||||
Get the registry key for a card based on deck position (1-78).
|
||||
|
||||
Card position is independent of deck-specific names, allowing different
|
||||
deck variants to use the same registry entries.
|
||||
|
||||
Args:
|
||||
card: The Card object to look up
|
||||
|
||||
Returns:
|
||||
Registry key string, or None if card cannot be mapped
|
||||
"""
|
||||
return self._position_map.get(card.number)
|
||||
|
||||
def load_into_card(self, card: 'Card') -> bool:
|
||||
"""
|
||||
Load details from registry into a Card object using its position.
|
||||
|
||||
Args:
|
||||
card: The Card object to populate
|
||||
|
||||
Returns:
|
||||
True if details were found and loaded, False otherwise
|
||||
"""
|
||||
# Use position-based lookup instead of name-based
|
||||
details = self.get_by_position(card.number)
|
||||
if not details:
|
||||
return False
|
||||
|
||||
card.explanation = details.get("explanation", "")
|
||||
card.interpretation = details.get("interpretation", "")
|
||||
card.keywords = details.get("keywords", [])
|
||||
card.reversed_keywords = details.get("reversed_keywords", [])
|
||||
card.guidance = details.get("guidance", "")
|
||||
|
||||
return True
|
||||
|
||||
def __getitem__(self, card_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Allow dict-like access: registry['Princess of Swords']"""
|
||||
return self.get(card_name)
|
||||
346
src/tarot/card/image_loader.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""Image deck loader for matching Tarot card images to cards.
|
||||
|
||||
This module provides intelligent image matching and loading, supporting:
|
||||
- Numbered format: 0.jpg, 1.jpg, ... or 00_foolish.jpg, 01_magic_man.jpg
|
||||
- Custom naming with override: ##_custom_name.jpg overrides default card names
|
||||
- Intelligent fuzzy matching for card identification
|
||||
- Hybrid modes with intelligent fallbacks
|
||||
|
||||
Usage:
|
||||
from tarot.card.image_loader import load_deck_images
|
||||
|
||||
deck = Deck()
|
||||
count = load_deck_images(deck, "/path/to/deck/folder")
|
||||
print(f"Loaded {count} card images")
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.deck import Card, Deck
|
||||
|
||||
|
||||
class ImageDeckLoader:
|
||||
"""Loader for matching Tarot card images to deck cards."""
|
||||
|
||||
# Supported image extensions
|
||||
SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
|
||||
|
||||
# Regex patterns for file matching
|
||||
NUMBERED_PATTERN = re.compile(r'^(\d+)(?:_(.+))?\.(?:jpg|jpeg|png|gif|bmp|webp)$', re.IGNORECASE)
|
||||
|
||||
def __init__(self, deck_folder: str) -> None:
|
||||
"""
|
||||
Initialize the image deck loader.
|
||||
|
||||
Args:
|
||||
deck_folder: Path to the folder containing card images
|
||||
|
||||
Raises:
|
||||
ValueError: If folder doesn't exist or is not a directory
|
||||
"""
|
||||
self.deck_folder = Path(deck_folder)
|
||||
|
||||
if not self.deck_folder.exists():
|
||||
raise ValueError(f"Deck folder does not exist: {deck_folder}")
|
||||
|
||||
if not self.deck_folder.is_dir():
|
||||
raise ValueError(f"Deck path is not a directory: {deck_folder}")
|
||||
|
||||
self.image_files = self._scan_folder()
|
||||
self.card_mapping: Dict[int, Tuple[str, bool]] = {} # card_number -> (path, has_custom_name)
|
||||
self._build_mapping()
|
||||
|
||||
def _scan_folder(self) -> List[Path]:
|
||||
"""Scan folder for image files."""
|
||||
images = []
|
||||
for ext in self.SUPPORTED_EXTENSIONS:
|
||||
images.extend(self.deck_folder.glob(f'*{ext}'))
|
||||
images.extend(self.deck_folder.glob(f'*{ext.upper()}'))
|
||||
|
||||
# Sort by filename for consistent ordering
|
||||
return sorted(images)
|
||||
|
||||
def _parse_filename(self, filename: str) -> Tuple[Optional[int], Optional[str], bool]:
|
||||
"""
|
||||
Parse image filename to extract card number and optional custom name.
|
||||
|
||||
Args:
|
||||
filename: The filename (without path)
|
||||
|
||||
Returns:
|
||||
Tuple of (card_number, custom_name, has_custom_name)
|
||||
- card_number: Parsed number if found, else None
|
||||
- custom_name: Custom name if present (e.g., "foolish" from "00_foolish.jpg")
|
||||
- has_custom_name: True if custom name was found
|
||||
|
||||
Examples:
|
||||
"0.jpg" -> (0, None, False)
|
||||
"00_foolish.jpg" -> (0, "foolish", True)
|
||||
"01_magic_man.jpg" -> (1, "magic_man", True)
|
||||
"invalid.jpg" -> (None, None, False)
|
||||
"""
|
||||
match = self.NUMBERED_PATTERN.match(filename)
|
||||
|
||||
if not match:
|
||||
return None, None, False
|
||||
|
||||
card_number = int(match.group(1))
|
||||
custom_name = match.group(2)
|
||||
has_custom_name = custom_name is not None
|
||||
|
||||
return card_number, custom_name, has_custom_name
|
||||
|
||||
def _build_mapping(self) -> None:
|
||||
"""Build mapping from card numbers to image file paths."""
|
||||
for image_path in self.image_files:
|
||||
card_num, custom_name, has_custom_name = self._parse_filename(image_path.name)
|
||||
|
||||
if card_num is not None:
|
||||
# Store path and whether it has a custom name
|
||||
self.card_mapping[card_num] = (str(image_path), has_custom_name)
|
||||
|
||||
def _normalize_card_name(self, name: str) -> str:
|
||||
"""
|
||||
Normalize card name for matching.
|
||||
|
||||
Converts to lowercase, removes special characters, collapses whitespace.
|
||||
|
||||
Args:
|
||||
name: Original card name
|
||||
|
||||
Returns:
|
||||
Normalized name
|
||||
|
||||
Examples:
|
||||
"The Fool" -> "the fool"
|
||||
"Princess of Swords" -> "princess of swords"
|
||||
"Ace of Cups" -> "ace of cups"
|
||||
"""
|
||||
# Convert to lowercase
|
||||
normalized = name.lower()
|
||||
|
||||
# Replace special characters with spaces
|
||||
normalized = re.sub(r'[^\w\s]', ' ', normalized)
|
||||
|
||||
# Collapse multiple spaces
|
||||
normalized = re.sub(r'\s+', ' ', normalized).strip()
|
||||
|
||||
return normalized
|
||||
|
||||
def _find_fuzzy_match(self, card_name_normalized: str) -> Optional[int]:
|
||||
"""
|
||||
Find matching card number using fuzzy name matching.
|
||||
|
||||
This is a fallback when card names don't parse as numbers.
|
||||
|
||||
Args:
|
||||
card_name_normalized: Normalized card name
|
||||
|
||||
Returns:
|
||||
Card number if a match is found, else None
|
||||
"""
|
||||
best_match = None
|
||||
best_score = 0
|
||||
threshold = 0.6
|
||||
|
||||
# Check all parsed custom names
|
||||
for card_num, (_, has_custom_name) in self.card_mapping.items():
|
||||
if not has_custom_name:
|
||||
continue
|
||||
|
||||
# Get the actual filename to extract custom name
|
||||
for image_path in self.image_files:
|
||||
parsed_num, custom_name, _ = self._parse_filename(image_path.name)
|
||||
|
||||
if parsed_num == card_num and custom_name:
|
||||
normalized_custom = self._normalize_card_name(custom_name)
|
||||
|
||||
# Simple similarity score: words that match
|
||||
query_words = set(card_name_normalized.split())
|
||||
custom_words = set(normalized_custom.split())
|
||||
|
||||
if query_words and custom_words:
|
||||
intersection = len(query_words & custom_words)
|
||||
union = len(query_words | custom_words)
|
||||
score = intersection / union if union > 0 else 0
|
||||
|
||||
if score > best_score and score >= threshold:
|
||||
best_score = score
|
||||
best_match = card_num
|
||||
|
||||
return best_match
|
||||
|
||||
def get_image_path(self, card: 'Card') -> Optional[str]:
|
||||
"""
|
||||
Get the image path for a specific card.
|
||||
|
||||
Matches cards by:
|
||||
1. Card number (primary method)
|
||||
2. Fuzzy matching on card name (fallback)
|
||||
|
||||
Args:
|
||||
card: The Card object to find an image for
|
||||
|
||||
Returns:
|
||||
Full path to image file, or None if not found
|
||||
"""
|
||||
# Try direct number match first
|
||||
if card.number in self.card_mapping:
|
||||
path, _ = self.card_mapping[card.number]
|
||||
return path
|
||||
|
||||
# Try fuzzy match on name as fallback
|
||||
normalized_name = self._normalize_card_name(card.name)
|
||||
fuzzy_match = self._find_fuzzy_match(normalized_name)
|
||||
|
||||
if fuzzy_match is not None and fuzzy_match in self.card_mapping:
|
||||
path, _ = self.card_mapping[fuzzy_match]
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
def should_override_name(self, card_number: int) -> bool:
|
||||
"""
|
||||
Check if card name should be overridden from filename.
|
||||
|
||||
Returns True only if:
|
||||
- Image file has a custom name component (##_name.jpg format)
|
||||
- Not just a plain number (##.jpg format)
|
||||
|
||||
Args:
|
||||
card_number: The card's number
|
||||
|
||||
Returns:
|
||||
True if name should be overridden from filename, False otherwise
|
||||
"""
|
||||
if card_number not in self.card_mapping:
|
||||
return False
|
||||
|
||||
_, has_custom_name = self.card_mapping[card_number]
|
||||
return has_custom_name
|
||||
|
||||
def get_custom_name(self, card_number: int) -> Optional[str]:
|
||||
"""
|
||||
Get the custom card name from the filename.
|
||||
|
||||
Args:
|
||||
card_number: The card's number
|
||||
|
||||
Returns:
|
||||
Custom name if present, None otherwise
|
||||
|
||||
Example:
|
||||
If filename is "00_the_foolish.jpg", returns "the_foolish"
|
||||
If filename is "00.jpg", returns None
|
||||
"""
|
||||
if card_number not in self.card_mapping:
|
||||
return None
|
||||
|
||||
# Find the image file for this card number
|
||||
for image_path in self.image_files:
|
||||
_, custom_name, _ = self._parse_filename(image_path.name)
|
||||
|
||||
parsed_num, _, _ = self._parse_filename(image_path.name)
|
||||
if parsed_num == card_number and custom_name:
|
||||
# Convert underscore-separated name to title case
|
||||
name_words = custom_name.split('_')
|
||||
return ' '.join(word.capitalize() for word in name_words)
|
||||
|
||||
return None
|
||||
|
||||
def load_into_deck(self, deck: 'Deck',
|
||||
override_names: bool = True,
|
||||
verbose: bool = False) -> int:
|
||||
"""
|
||||
Load image paths into all cards in a deck.
|
||||
|
||||
Args:
|
||||
deck: The Deck to load images into
|
||||
override_names: If True, use custom names from filenames when available
|
||||
verbose: If True, print progress information
|
||||
|
||||
Returns:
|
||||
Number of cards that had images loaded
|
||||
|
||||
Example:
|
||||
>>> loader = ImageDeckLoader("/path/to/deck")
|
||||
>>> deck = Deck()
|
||||
>>> count = loader.load_into_deck(deck, override_names=True)
|
||||
>>> print(f"Loaded {count} card images")
|
||||
"""
|
||||
loaded_count = 0
|
||||
|
||||
for card in deck.cards:
|
||||
image_path = self.get_image_path(card)
|
||||
|
||||
if image_path:
|
||||
card.image_path = image_path
|
||||
loaded_count += 1
|
||||
|
||||
# Override name if appropriate
|
||||
if override_names and self.should_override_name(card.number):
|
||||
custom_name = self.get_custom_name(card.number)
|
||||
if custom_name:
|
||||
if verbose:
|
||||
print(f" {card.number}: {card.name} -> {custom_name}")
|
||||
card.name = custom_name
|
||||
elif verbose:
|
||||
print(f" ✓ {card.number}: {card.name}")
|
||||
|
||||
return loaded_count
|
||||
|
||||
def get_summary(self) -> Dict[str, any]:
|
||||
"""
|
||||
Get a summary of loaded images and statistics.
|
||||
|
||||
Returns:
|
||||
Dictionary with loader statistics
|
||||
"""
|
||||
total_images = len(self.image_files)
|
||||
mapped_cards = len(self.card_mapping)
|
||||
custom_named = sum(1 for _, has_custom in self.card_mapping.values() if has_custom)
|
||||
|
||||
return {
|
||||
'deck_folder': str(self.deck_folder),
|
||||
'total_image_files': total_images,
|
||||
'total_image_filenames': len(set(f.name for f in self.image_files)),
|
||||
'mapped_card_numbers': mapped_cards,
|
||||
'cards_with_custom_names': custom_named,
|
||||
'cards_with_generic_numbers': mapped_cards - custom_named,
|
||||
'image_extensions_found': list(set(f.suffix.lower() for f in self.image_files)),
|
||||
}
|
||||
|
||||
|
||||
def load_deck_images(deck: 'Deck',
|
||||
deck_folder: str,
|
||||
override_names: bool = True,
|
||||
verbose: bool = False) -> int:
|
||||
"""
|
||||
Convenience function to load deck images.
|
||||
|
||||
Args:
|
||||
deck: The Deck object to load images into
|
||||
deck_folder: Path to folder containing card images
|
||||
override_names: If True, use custom names from filenames when available
|
||||
verbose: If True, print progress information
|
||||
|
||||
Returns:
|
||||
Number of cards that had images loaded
|
||||
|
||||
Raises:
|
||||
ValueError: If deck_folder doesn't exist or is invalid
|
||||
|
||||
Example:
|
||||
>>> from tarot import Deck
|
||||
>>> from tarot.card.image_loader import load_deck_images
|
||||
>>>
|
||||
>>> deck = Deck()
|
||||
>>> count = load_deck_images(deck, "/path/to/deck/images")
|
||||
>>> print(f"Loaded {count} card images")
|
||||
"""
|
||||
loader = ImageDeckLoader(deck_folder)
|
||||
return loader.load_into_deck(deck, override_names=override_names, verbose=verbose)
|
||||
259
src/tarot/card/loader.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""Card loader for populating card details from the registry.
|
||||
|
||||
This module provides utilities to load card details from the CardDetailsRegistry
|
||||
into Card objects, supporting both individual cards and full decks.
|
||||
|
||||
Usage:
|
||||
from tarot.card.loader import load_card_details, load_deck_details
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
|
||||
# Load single card
|
||||
loader = CardDetailsRegistry()
|
||||
card = my_deck.minor.swords(11)
|
||||
load_card_details(card, loader)
|
||||
|
||||
# Load entire deck
|
||||
load_deck_details(my_deck, loader)
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card.card import Card
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
from tarot.deck import Deck
|
||||
|
||||
|
||||
def load_card_details(
|
||||
card: 'Card',
|
||||
registry: Optional['CardDetailsRegistry'] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Load details for a single card from the registry.
|
||||
|
||||
Args:
|
||||
card: The Card object to populate with details
|
||||
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
||||
|
||||
Returns:
|
||||
True if details were found and loaded, False otherwise
|
||||
|
||||
Example:
|
||||
>>> from tarot import Deck
|
||||
>>> deck = Deck()
|
||||
>>> card = deck.major[0] # The Fool
|
||||
>>> load_card_details(card)
|
||||
True
|
||||
>>> print(card.keywords)
|
||||
['new beginnings', 'innocence', 'faith', ...]
|
||||
"""
|
||||
if registry is None:
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
registry = CardDetailsRegistry()
|
||||
|
||||
return registry.load_into_card(card)
|
||||
|
||||
|
||||
def load_deck_details(
|
||||
deck: 'Deck',
|
||||
registry: Optional['CardDetailsRegistry'] = None,
|
||||
verbose: bool = False
|
||||
) -> int:
|
||||
"""
|
||||
Load details for all cards in a deck.
|
||||
|
||||
Args:
|
||||
deck: The Deck object containing cards to populate
|
||||
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
||||
verbose: If True, prints information about each card loaded
|
||||
|
||||
Returns:
|
||||
Number of cards successfully loaded with details
|
||||
|
||||
Example:
|
||||
>>> from tarot import Deck
|
||||
>>> deck = Deck()
|
||||
>>> count = load_deck_details(deck, verbose=True)
|
||||
>>> print(f"Loaded details for {count} cards")
|
||||
"""
|
||||
if registry is None:
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
registry = CardDetailsRegistry()
|
||||
|
||||
loaded_count = 0
|
||||
failed_cards = []
|
||||
|
||||
# Load all cards from the deck
|
||||
for card in deck.cards:
|
||||
if load_card_details(card, registry):
|
||||
loaded_count += 1
|
||||
if verbose:
|
||||
print(f"✓ Loaded: {card.name}")
|
||||
else:
|
||||
failed_cards.append(card.name)
|
||||
if verbose:
|
||||
print(f"✗ Failed: {card.name}")
|
||||
|
||||
if verbose and failed_cards:
|
||||
print(f"\n{len(failed_cards)} cards failed to load:")
|
||||
for name in failed_cards:
|
||||
print(f" - {name}")
|
||||
|
||||
return loaded_count
|
||||
|
||||
|
||||
def get_cards_by_suit(
|
||||
deck: 'Deck',
|
||||
suit_name: str
|
||||
) -> List['Card']:
|
||||
"""
|
||||
Get all cards from a specific suit in the deck.
|
||||
|
||||
Args:
|
||||
deck: The Deck object
|
||||
suit_name: The suit name ("Cups", "Pentacles", "Swords", "Wands")
|
||||
|
||||
Returns:
|
||||
List of Card objects from that suit
|
||||
|
||||
Example:
|
||||
>>> from tarot import Deck
|
||||
>>> from tarot.card.loader import get_cards_by_suit
|
||||
>>> deck = Deck()
|
||||
>>> swords = get_cards_by_suit(deck, "Swords")
|
||||
>>> print(len(swords)) # Should be 14
|
||||
14
|
||||
"""
|
||||
if hasattr(deck, 'suit') and callable(deck.suit):
|
||||
# Deck has a suit method, use it
|
||||
return deck.suit(suit_name)
|
||||
|
||||
# Fallback: filter cards manually
|
||||
return [card for card in deck.cards if hasattr(card, 'suit') and
|
||||
card.suit and card.suit.name == suit_name]
|
||||
|
||||
|
||||
def filter_cards_by_keywords(
|
||||
cards: List['Card'],
|
||||
keyword: str
|
||||
) -> List['Card']:
|
||||
"""
|
||||
Filter a list of cards by keyword.
|
||||
|
||||
Args:
|
||||
cards: List of Card objects to filter
|
||||
keyword: The keyword to search for (case-insensitive)
|
||||
|
||||
Returns:
|
||||
List of cards that have the keyword
|
||||
|
||||
Example:
|
||||
>>> from tarot import Deck
|
||||
>>> deck = Deck()
|
||||
>>> all_cards = list(deck.major.cards()) + list(deck.minor.cups.cards())
|
||||
>>> love_cards = filter_cards_by_keywords(all_cards, "love")
|
||||
"""
|
||||
keyword_lower = keyword.lower()
|
||||
return [
|
||||
card for card in cards
|
||||
if hasattr(card, 'keywords') and card.keywords and
|
||||
any(keyword_lower in kw.lower() for kw in card.keywords)
|
||||
]
|
||||
|
||||
|
||||
def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
|
||||
"""
|
||||
Pretty print card details to console.
|
||||
|
||||
Args:
|
||||
card: The Card object to print
|
||||
include_reversed: If True, also print reversed keywords and interpretation
|
||||
|
||||
Example:
|
||||
>>> from tarot import Deck
|
||||
>>> deck = Deck()
|
||||
>>> card = deck.major[0] # The Fool
|
||||
>>> print_card_details(card)
|
||||
"""
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" {card.name}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# Define attributes to print with their formatting
|
||||
attributes = {
|
||||
'explanation': ('Explanation', False),
|
||||
'interpretation': ('Interpretation', False),
|
||||
'guidance': ('Guidance', False),
|
||||
}
|
||||
|
||||
# Add reversed attributes only if requested
|
||||
if include_reversed:
|
||||
attributes['reversed_interpretation'] = ('Reversed Interpretation', False)
|
||||
|
||||
# List attributes (joined with commas)
|
||||
list_attributes = {
|
||||
'keywords': 'Keywords',
|
||||
'reversed_keywords': ('Reversed Keywords', include_reversed),
|
||||
}
|
||||
|
||||
# Numeric attributes
|
||||
numeric_attributes = {
|
||||
'numerology': 'Numerology',
|
||||
}
|
||||
|
||||
# Print text attributes
|
||||
for attr_name, (display_name, _) in attributes.items():
|
||||
if hasattr(card, attr_name):
|
||||
value = getattr(card, attr_name)
|
||||
if value:
|
||||
print(f"\n{display_name}:\n{value}")
|
||||
|
||||
# Print list attributes
|
||||
for attr_name, display_info in list_attributes.items():
|
||||
if isinstance(display_info, tuple):
|
||||
display_name, should_show = display_info
|
||||
if not should_show:
|
||||
continue
|
||||
else:
|
||||
display_name = display_info
|
||||
|
||||
if hasattr(card, attr_name):
|
||||
value = getattr(card, attr_name)
|
||||
if value:
|
||||
print(f"\n{display_name}: {', '.join(value)}")
|
||||
|
||||
# Print numeric attributes
|
||||
for attr_name, display_name in numeric_attributes.items():
|
||||
if hasattr(card, attr_name):
|
||||
value = getattr(card, attr_name)
|
||||
if value is not None:
|
||||
print(f"\n{display_name}: {value}")
|
||||
|
||||
print(f"\n{'=' * 60}\n")
|
||||
|
||||
|
||||
def get_card_info(
|
||||
card_name: str,
|
||||
registry: Optional['CardDetailsRegistry'] = None
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Get card information by card name.
|
||||
|
||||
Args:
|
||||
card_name: The name of the card (e.g., "Princess of Swords")
|
||||
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
||||
|
||||
Returns:
|
||||
Dictionary containing card details, or None if not found
|
||||
|
||||
Example:
|
||||
>>> from tarot.card.loader import get_card_info
|
||||
>>> info = get_card_info("Princess of Swords")
|
||||
>>> if info:
|
||||
... print(info['explanation'])
|
||||
"""
|
||||
if registry is None:
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
registry = CardDetailsRegistry()
|
||||
|
||||
return registry.get(card_name)
|
||||
323
src/tarot/card/spread.py
Normal file
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
Tarot spread definitions and management with card drawing.
|
||||
|
||||
Provides predefined spreads like Celtic Cross, Golden Dawn (3-card), etc.
|
||||
with position meanings and automatic card drawing.
|
||||
|
||||
Usage:
|
||||
from tarot import Tarot
|
||||
|
||||
# Draw cards for a spread
|
||||
reading = Tarot.deck.card.spread('Celtic Cross')
|
||||
print(reading)
|
||||
|
||||
# Can also access spread with/without cards
|
||||
from tarot.card.spread import Spread, draw_spread
|
||||
|
||||
spread = Spread('Celtic Cross')
|
||||
reading = draw_spread(spread) # Returns list of (position, card) tuples
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card import Card
|
||||
|
||||
|
||||
@dataclass
|
||||
class SpreadPosition:
|
||||
"""Represents a position in a Tarot spread."""
|
||||
number: int
|
||||
name: str
|
||||
meaning: str
|
||||
reversed_meaning: Optional[str] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = f"{self.number}. {self.name}: {self.meaning}"
|
||||
if self.reversed_meaning:
|
||||
result += f"\n (Reversed: {self.reversed_meaning})"
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class DrawnCard:
|
||||
"""Represents a card drawn for a spread position."""
|
||||
position: SpreadPosition
|
||||
card: 'Card'
|
||||
is_reversed: bool
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Format the drawn card with position and interpretation."""
|
||||
card_name = self.card.name
|
||||
if self.is_reversed:
|
||||
card_name += " (Reversed)"
|
||||
|
||||
return f"{self.position.number}. {self.position.name}\n" \
|
||||
f" └─ {card_name}\n" \
|
||||
f" └─ Position: {self.position.meaning}"
|
||||
|
||||
|
||||
class Spread:
|
||||
"""Represents a Tarot spread with positions and meanings."""
|
||||
|
||||
# Define all available spreads
|
||||
SPREADS: Dict[str, Dict] = {
|
||||
'three card': {
|
||||
'name': '3-Card Spread',
|
||||
'description': 'Simple 3-card spread for past, present, future or situation, action, outcome',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'First Position', 'Past, Foundation, or Situation'),
|
||||
SpreadPosition(2, 'Second Position', 'Present, Action, or Influence'),
|
||||
SpreadPosition(3, 'Third Position', 'Future, Outcome, or Advice'),
|
||||
]
|
||||
},
|
||||
'golden dawn': {
|
||||
'name': 'Golden Dawn 3-Card',
|
||||
'description': 'Three card spread used in Golden Dawn tradition',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'Supernal Triangle', 'Spiritual/Divine aspect'),
|
||||
SpreadPosition(2, 'Pillar of Severity', 'Challenging/Active force'),
|
||||
SpreadPosition(3, 'Pillar of Mercy', 'Supportive/Passive force'),
|
||||
]
|
||||
},
|
||||
'celtic cross': {
|
||||
'name': 'Celtic Cross',
|
||||
'description': 'Classic 10-card spread for in-depth reading',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'The Significator', 'The main situation or person'),
|
||||
SpreadPosition(2, 'The Cross', 'The challenge or heart of the matter'),
|
||||
SpreadPosition(3, 'Crowning Influence', 'Conscious hopes/ideals'),
|
||||
SpreadPosition(4, 'Beneath the Cross', 'Unconscious or hidden aspects'),
|
||||
SpreadPosition(5, 'Behind', 'Past influences'),
|
||||
SpreadPosition(6, 'Before', 'Future influences'),
|
||||
SpreadPosition(7, 'Self/Attitude', 'How the querent sees themselves'),
|
||||
SpreadPosition(8, 'Others/Environment', 'External factors/opinions'),
|
||||
SpreadPosition(9, 'Hopes and Fears', 'What the querent hopes for or fears'),
|
||||
SpreadPosition(10, 'Outcome', 'Final outcome or resolution'),
|
||||
]
|
||||
},
|
||||
'horseshoe': {
|
||||
'name': 'Horseshoe',
|
||||
'description': '7-card spread in horseshoe formation for past, present, future insight',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'Distant Past', 'Ancient influences and foundations'),
|
||||
SpreadPosition(2, 'Recent Past', 'Recent events and circumstances'),
|
||||
SpreadPosition(3, 'Present Situation', 'Current state of affairs'),
|
||||
SpreadPosition(4, 'Immediate Future', 'Near-term developments'),
|
||||
SpreadPosition(5, 'Distant Future', 'Long-term outcome'),
|
||||
SpreadPosition(6, 'Inner Influence', 'Self/thoughts/emotions'),
|
||||
SpreadPosition(7, 'Outer Influence', 'External forces and environment'),
|
||||
]
|
||||
},
|
||||
'pentagram': {
|
||||
'name': 'Pentagram',
|
||||
'description': '5-card spread based on Earth element pentagram',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'Spirit', 'Core essence or spiritual truth'),
|
||||
SpreadPosition(2, 'Fire', 'Action and willpower'),
|
||||
SpreadPosition(3, 'Water', 'Emotions and intuition'),
|
||||
SpreadPosition(4, 'Air', 'Intellect and communication'),
|
||||
SpreadPosition(5, 'Earth', 'Physical manifestation and grounding'),
|
||||
]
|
||||
},
|
||||
'tree of life': {
|
||||
'name': 'Tree of Life',
|
||||
'description': '10-card spread mapping Sephiroth on the Tree of Life',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'Kether (Crown)', 'Divine will and unity'),
|
||||
SpreadPosition(2, 'Chokmah (Wisdom)', 'Creative force and impulse'),
|
||||
SpreadPosition(3, 'Binah (Understanding)', 'Form and structure'),
|
||||
SpreadPosition(4, 'Chesed (Mercy)', 'Expansion and abundance'),
|
||||
SpreadPosition(5, 'Gevurah (Severity)', 'Reduction and discipline'),
|
||||
SpreadPosition(6, 'Tiphareth (Beauty)', 'Core self and integration'),
|
||||
SpreadPosition(7, 'Netzach (Victory)', 'Desire and passion'),
|
||||
SpreadPosition(8, 'Hod (Splendor)', 'Intellect and communication'),
|
||||
SpreadPosition(9, 'Yesod (Foundation)', 'Subconscious and dreams'),
|
||||
SpreadPosition(10, 'Malkuth (Kingdom)', 'Manifestation and physical reality'),
|
||||
]
|
||||
},
|
||||
'relationship': {
|
||||
'name': 'Relationship',
|
||||
'description': '5-card spread for relationship insight',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'You', 'Your position, feelings, or role'),
|
||||
SpreadPosition(2, 'Them', 'Their position, feelings, or perspective'),
|
||||
SpreadPosition(3, 'The Relationship', 'The dynamic and connection'),
|
||||
SpreadPosition(4, 'Challenge', 'Current challenge or friction point'),
|
||||
SpreadPosition(5, 'Outcome', 'Where the relationship is heading'),
|
||||
]
|
||||
},
|
||||
'yes or no': {
|
||||
'name': 'Yes or No',
|
||||
'description': '1-card spread for simple yes/no answers',
|
||||
'positions': [
|
||||
SpreadPosition(1, 'Answer', 'Major Arcana = Yes, Minor Arcana = No, Court Cards = Maybe'),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, spread_name: str) -> None:
|
||||
"""
|
||||
Initialize a spread by name (case-insensitive).
|
||||
|
||||
Args:
|
||||
spread_name: Name of the spread to use
|
||||
|
||||
Raises:
|
||||
ValueError: If spread name not found
|
||||
"""
|
||||
# Normalize name (case-insensitive, allow underscores or spaces)
|
||||
normalized_name = spread_name.lower().replace('_', ' ')
|
||||
|
||||
# Find matching spread
|
||||
spread_data = None
|
||||
for key, data in self.SPREADS.items():
|
||||
if key == normalized_name or data['name'].lower() == normalized_name:
|
||||
spread_data = data
|
||||
break
|
||||
|
||||
if not spread_data:
|
||||
available = ', '.join(f"'{k}'" for k in self.SPREADS.keys())
|
||||
raise ValueError(
|
||||
f"Spread '{spread_name}' not found. Available spreads: {available}"
|
||||
)
|
||||
|
||||
self.name = spread_data['name']
|
||||
self.description = spread_data['description']
|
||||
self.positions: List[SpreadPosition] = spread_data['positions']
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return formatted spread information."""
|
||||
lines = [
|
||||
f"═══════════════════════════════════════════",
|
||||
f" {self.name}",
|
||||
f"═══════════════════════════════════════════",
|
||||
f"",
|
||||
f"{self.description}",
|
||||
f"",
|
||||
f"Positions ({len(self.positions)} cards):",
|
||||
f"",
|
||||
]
|
||||
|
||||
for pos in self.positions:
|
||||
lines.append(f" {pos}")
|
||||
|
||||
lines.append(f"")
|
||||
lines.append(f"═══════════════════════════════════════════")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Spread('{self.name}')"
|
||||
|
||||
@classmethod
|
||||
def available_spreads(cls) -> str:
|
||||
"""Return list of all available spreads."""
|
||||
lines = [
|
||||
"Available Tarot Spreads:",
|
||||
"═" * 50,
|
||||
""
|
||||
]
|
||||
|
||||
for key, data in cls.SPREADS.items():
|
||||
lines.append(f" • {data['name']}")
|
||||
lines.append(f" Name for API: '{key}'")
|
||||
lines.append(f" Positions: {len(data['positions'])}")
|
||||
lines.append(f" {data['description']}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_position(self, position_number: int) -> Optional[SpreadPosition]:
|
||||
"""Get a specific position by number."""
|
||||
for pos in self.positions:
|
||||
if pos.number == position_number:
|
||||
return pos
|
||||
return None
|
||||
|
||||
|
||||
def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
|
||||
"""
|
||||
Draw cards for all positions in a spread.
|
||||
|
||||
Args:
|
||||
spread: The Spread object with positions defined
|
||||
deck: Optional list of Card objects. If None, uses Tarot.deck.cards
|
||||
|
||||
Returns:
|
||||
List of DrawnCard objects (one per position) with random cards and reversals
|
||||
"""
|
||||
import random
|
||||
|
||||
# Load deck if not provided
|
||||
if deck is None:
|
||||
from tarot.deck import Deck
|
||||
deck_instance = Deck()
|
||||
deck = deck_instance.cards
|
||||
|
||||
drawn_cards = []
|
||||
for position in spread.positions:
|
||||
# Draw random card
|
||||
card = random.choice(deck)
|
||||
# Random reversal (50% chance)
|
||||
is_reversed = random.choice([True, False])
|
||||
drawn_cards.append(DrawnCard(position, card, is_reversed))
|
||||
|
||||
return drawn_cards
|
||||
|
||||
|
||||
class SpreadReading:
|
||||
"""Represents a complete tarot reading with cards drawn for a spread."""
|
||||
|
||||
def __init__(self, spread: Spread, drawn_cards: List[DrawnCard]) -> None:
|
||||
"""
|
||||
Initialize a reading with a spread and drawn cards.
|
||||
|
||||
Args:
|
||||
spread: The Spread object
|
||||
drawn_cards: List of DrawnCard objects
|
||||
"""
|
||||
self.spread = spread
|
||||
self.drawn_cards = drawn_cards
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return formatted reading with all cards and interpretations."""
|
||||
lines = [
|
||||
f"╔═══════════════════════════════════════════╗",
|
||||
f"║ {self.spread.name:40}║",
|
||||
f"╚═══════════════════════════════════════════╝",
|
||||
f"",
|
||||
f"{self.spread.description}",
|
||||
f"",
|
||||
f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
||||
f"",
|
||||
]
|
||||
|
||||
for drawn in self.drawn_cards:
|
||||
card = drawn.card
|
||||
card_name = card.name
|
||||
if drawn.is_reversed:
|
||||
card_name += " ◄ REVERSED"
|
||||
|
||||
lines.append(f"Position {drawn.position.number}: {drawn.position.name}")
|
||||
lines.append(f" Card: {card_name}")
|
||||
lines.append(f" Meaning: {drawn.position.meaning}")
|
||||
|
||||
# Add card details if available
|
||||
if hasattr(card, 'number'):
|
||||
lines.append(f" Card #: {card.number}")
|
||||
if hasattr(card, 'arcana'):
|
||||
lines.append(f" Arcana: {card.arcana}")
|
||||
if hasattr(card, 'suit') and card.suit:
|
||||
lines.append(f" Suit: {card.suit.name}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SpreadReading({self.spread.name}, {len(self.drawn_cards)} cards)"
|
||||
32
src/tarot/deck/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Tarot deck module - Core card and deck classes.
|
||||
|
||||
Provides the Deck class for managing Tarot cards and the Card, MajorCard,
|
||||
MinorCard, and related classes for representing individual cards.
|
||||
"""
|
||||
|
||||
from .deck import (
|
||||
Card,
|
||||
MajorCard,
|
||||
MinorCard,
|
||||
PipCard,
|
||||
AceCard,
|
||||
CourtCard,
|
||||
CardQuery,
|
||||
TemporalQuery,
|
||||
DLT,
|
||||
Deck,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Card",
|
||||
"MajorCard",
|
||||
"MinorCard",
|
||||
"PipCard",
|
||||
"AceCard",
|
||||
"CourtCard",
|
||||
"CardQuery",
|
||||
"TemporalQuery",
|
||||
"DLT",
|
||||
"Deck",
|
||||
]
|
||||
734
src/tarot/deck/deck.py
Normal file
@@ -0,0 +1,734 @@
|
||||
"""
|
||||
Core Tarot deck and card classes.
|
||||
|
||||
This module defines the Deck class for managing Tarot cards and the Card,
|
||||
MajorCard, and MinorCard classes for representing individual cards.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Tuple, TYPE_CHECKING
|
||||
import random
|
||||
|
||||
from ..attributes import (
|
||||
Meaning, CardImage, Suit, Zodiac, Element, Path,
|
||||
Planet, Sephera, Color, PeriodicTable, ElementType, DoublLetterTrump
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..card.data import CardDataLoader
|
||||
|
||||
# Global CardDataLoader instance for accessing elements
|
||||
_card_data = None # Will be initialized lazily
|
||||
|
||||
|
||||
def _get_card_data():
|
||||
"""Get or initialize the global CardDataLoader instance."""
|
||||
global _card_data
|
||||
if _card_data is None:
|
||||
from ..card.data import CardDataLoader
|
||||
_card_data = CardDataLoader()
|
||||
return _card_data
|
||||
|
||||
|
||||
@dataclass
|
||||
class Card:
|
||||
"""Base class representing a Tarot card."""
|
||||
number: int
|
||||
name: str
|
||||
meaning: Meaning
|
||||
arcana: str # "Major" or "Minor"
|
||||
image: Optional[CardImage] = None
|
||||
|
||||
# These are overridden in subclasses but declared here for MinorCard compatibility
|
||||
suit: Optional[Suit] = None
|
||||
pip: int = 0
|
||||
|
||||
# Card-specific details
|
||||
explanation: str = ""
|
||||
interpretation: str = ""
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
reversed_keywords: List[str] = field(default_factory=list)
|
||||
guidance: str = ""
|
||||
numerology: Optional[int] = None
|
||||
|
||||
# Image path for custom deck images
|
||||
image_path: Optional[str] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.number}. {self.name}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Card({self.number}, '{self.name}')"
|
||||
|
||||
def key(self) -> str:
|
||||
"""
|
||||
Get the card's key as a Roman numeral representation.
|
||||
|
||||
Returns:
|
||||
Roman numeral string (e.g., "I", "XXI") for Major Arcana,
|
||||
or the pip number as string for Minor Arcana.
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from ..card.details import CardDetailsRegistry
|
||||
|
||||
# For Major Arcana cards, convert the key to Roman numerals
|
||||
if self.arcana == "Major":
|
||||
return CardDetailsRegistry.key_to_roman(self.number)
|
||||
|
||||
# For Minor Arcana, return the pip number as a formatted string
|
||||
if hasattr(self, 'pip') and self.pip > 0:
|
||||
pip_names = {
|
||||
2: "Two", 3: "Three", 4: "Four", 5: "Five",
|
||||
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten"
|
||||
}
|
||||
return pip_names.get(self.pip, str(self.pip))
|
||||
|
||||
return str(self.number)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Get the specific card type (Major, Pip, Ace, Court)."""
|
||||
if isinstance(self, MajorCard):
|
||||
return "Major"
|
||||
elif isinstance(self, AceCard):
|
||||
return "Ace"
|
||||
elif isinstance(self, CourtCard):
|
||||
return "Court"
|
||||
elif isinstance(self, PipCard):
|
||||
return "Pip"
|
||||
return "Unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MajorCard(Card):
|
||||
"""Represents a Major Arcana card."""
|
||||
kabbalistic_number: Optional[int] = None
|
||||
tarot_letter: Optional[str] = None
|
||||
tree_of_life_path: Optional[int] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Kabbalistic number should be 0-21, but deck position can be anywhere
|
||||
if self.kabbalistic_number is not None and (self.kabbalistic_number < 0 or self.kabbalistic_number > 21):
|
||||
raise ValueError(f"Major Arcana kabbalistic number must be 0-21, got {self.kabbalistic_number}")
|
||||
self.arcana = "Major"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinorCard(Card):
|
||||
"""Represents a Minor Arcana card - either Pip or Court card."""
|
||||
suit: Suit = None # type: ignore
|
||||
astrological_influence: Optional[str] = None
|
||||
element: Optional[Element] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.suit is None:
|
||||
raise ValueError("suit must be provided for MinorCard")
|
||||
self.arcana = "Minor"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipCard(MinorCard):
|
||||
"""Represents a Pip card (2 through 10) - has a pip number.
|
||||
|
||||
Pip cards represent numbered forces in their suit, from Two
|
||||
through its full development (10).
|
||||
"""
|
||||
pip: int = 0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not (2 <= self.pip <= 10):
|
||||
raise ValueError(f"Pip card number must be 2-10, got {self.pip}")
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class AceCard(MinorCard):
|
||||
"""Represents an Ace card - the root/foundation of the suit.
|
||||
|
||||
The Ace is the initial force of the suit and contains the potential
|
||||
for all other cards within that suit. Aces have pip=1 but are not
|
||||
technically pip cards.
|
||||
"""
|
||||
pip: int = 1
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.pip != 1:
|
||||
raise ValueError(f"AceCard must have pip 1, got {self.pip}")
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
@dataclass
|
||||
class CourtCard(MinorCard):
|
||||
"""Represents a Court Card - Knight, Prince, Princess, or Queen.
|
||||
|
||||
Court cards represent people/personalities and are the highest rank
|
||||
in the minor arcana. They do NOT have pips - they are archetypes.
|
||||
|
||||
Each court card is associated with an element and Hebrew letter (Path):
|
||||
- Knight: Fire + Yod (path 20)
|
||||
- Prince: Air + Vav (path 16)
|
||||
- Princess: Earth + Heh (path 15)
|
||||
- Queen: Water + Heh (path 15)
|
||||
"""
|
||||
|
||||
COURT_RANKS = {"Knight": 12, "Prince": 11, "Princess": 13, "Queen": 14}
|
||||
court_rank: str = ""
|
||||
associated_element: Optional[ElementType] = None
|
||||
hebrew_letter_path: Optional['Path'] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.court_rank not in self.COURT_RANKS:
|
||||
raise ValueError(
|
||||
f"CourtCard must have court_rank in {list(self.COURT_RANKS.keys())}, "
|
||||
f"got {self.court_rank}"
|
||||
)
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
|
||||
class CardQuery:
|
||||
"""Helper class for fluent card queries: deck.number(3).minor.wands"""
|
||||
|
||||
def __init__(self, deck: 'Deck', number: Optional[int] = None,
|
||||
arcana: Optional[str] = None) -> None:
|
||||
self.deck = deck
|
||||
self.number = number
|
||||
self.arcana = arcana
|
||||
|
||||
def _filter_cards(self) -> List[Card]:
|
||||
"""Get filtered cards based on current query state."""
|
||||
cards = self.deck.cards
|
||||
|
||||
if self.number is not None:
|
||||
cards = [c for c in cards if c.number == self.number or
|
||||
(hasattr(c, 'pip') and c.pip == self.number)]
|
||||
|
||||
if self.arcana is not None:
|
||||
cards = [c for c in cards if c.arcana == self.arcana]
|
||||
|
||||
return cards
|
||||
|
||||
@property
|
||||
def major(self) -> List[Card]:
|
||||
"""Filter to Major Arcana only."""
|
||||
return [c for c in self._filter_cards() if c.arcana == "Major"]
|
||||
|
||||
@property
|
||||
def minor(self) -> 'CardQuery':
|
||||
"""Filter to Minor Arcana, return new CardQuery for suit chaining."""
|
||||
return CardQuery(self.deck, self.number, "Minor")
|
||||
|
||||
@property
|
||||
def cups(self) -> List[Card]:
|
||||
"""Get cards in Cups suit."""
|
||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
||||
c.suit and c.suit.name == "Cups"]
|
||||
|
||||
@property
|
||||
def swords(self) -> List[Card]:
|
||||
"""Get cards in Swords suit."""
|
||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
||||
c.suit and c.suit.name == "Swords"]
|
||||
|
||||
@property
|
||||
def wands(self) -> List[Card]:
|
||||
"""Get cards in Wands suit."""
|
||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
||||
c.suit and c.suit.name == "Wands"]
|
||||
|
||||
@property
|
||||
def pentacles(self) -> List[Card]:
|
||||
"""Get cards in Pentacles suit."""
|
||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
||||
c.suit and c.suit.name == "Pentacles"]
|
||||
|
||||
def __iter__(self):
|
||||
"""Allow iteration over filtered cards."""
|
||||
return iter(self._filter_cards())
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return count of filtered cards."""
|
||||
return len(self._filter_cards())
|
||||
|
||||
def __getitem__(self, index: int) -> Card:
|
||||
"""Get card by index from filtered results."""
|
||||
return self._filter_cards()[index]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
cards = self._filter_cards()
|
||||
names = [c.name for c in cards]
|
||||
return f"CardQuery({names})"
|
||||
|
||||
|
||||
class TemporalQuery:
|
||||
"""Helper class for fluent temporal queries: loader.month(5).day(23).hour(15)"""
|
||||
|
||||
def __init__(self, loader: 'CardDataLoader', month_num: Optional[int] = None,
|
||||
day_num: Optional[int] = None, hour_num: Optional[int] = None) -> None:
|
||||
"""
|
||||
Initialize temporal query builder.
|
||||
|
||||
Args:
|
||||
loader: CardDataLoader instance for fetching temporal data
|
||||
month_num: Month number (1-12)
|
||||
day_num: Day number (1-31)
|
||||
hour_num: Hour number (0-23)
|
||||
"""
|
||||
self.loader = loader
|
||||
self.month_num = month_num
|
||||
self.day_num = day_num
|
||||
self.hour_num = hour_num
|
||||
|
||||
def month(self, num: int) -> 'TemporalQuery':
|
||||
"""Set month (1-12) and return new query for chaining."""
|
||||
return TemporalQuery(self.loader, month_num=num,
|
||||
day_num=self.day_num, hour_num=self.hour_num)
|
||||
|
||||
def day(self, num: int) -> 'TemporalQuery':
|
||||
"""Set day (1-31) and return new query for chaining."""
|
||||
if self.month_num is None:
|
||||
raise ValueError("Must set month before day")
|
||||
return TemporalQuery(self.loader, month_num=self.month_num,
|
||||
day_num=num, hour_num=self.hour_num)
|
||||
|
||||
def hour(self, num: int) -> 'TemporalQuery':
|
||||
"""Set hour (0-23) and return new query for chaining."""
|
||||
if self.month_num is None or self.day_num is None:
|
||||
raise ValueError("Must set month and day before hour")
|
||||
return TemporalQuery(self.loader, month_num=self.month_num,
|
||||
day_num=self.day_num, hour_num=num)
|
||||
|
||||
def weekday(self) -> Optional[str]:
|
||||
"""Get weekday name for current month/day combination using Zeller's congruence."""
|
||||
if self.month_num is None or self.day_num is None:
|
||||
raise ValueError("Must set month and day to get weekday")
|
||||
|
||||
# Zeller's congruence (adjusted for current calendar)
|
||||
month = self.month_num
|
||||
day = self.day_num
|
||||
year = 2024 # Use current year as reference
|
||||
|
||||
# Adjust month and year for March-based calculation
|
||||
if month < 3:
|
||||
month += 12
|
||||
year -= 1
|
||||
|
||||
# Zeller's formula
|
||||
q = day
|
||||
m = month
|
||||
k = year % 100
|
||||
j = year // 100
|
||||
|
||||
h = (q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) % 7
|
||||
|
||||
# Convert to weekday name (0=Saturday, 1=Sunday, 2=Monday, ..., 6=Friday)
|
||||
day_names = ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||
return day_names[h]
|
||||
|
||||
def month_info(self):
|
||||
"""Return month metadata for the configured query."""
|
||||
if self.month_num is None:
|
||||
return None
|
||||
return self.loader.month_info(self.month_num)
|
||||
|
||||
def day_info(self):
|
||||
"""Return day metadata for the configured query."""
|
||||
if self.day_num is None:
|
||||
return None
|
||||
return self.loader.day_info(self.day_num)
|
||||
|
||||
def hour_info(self):
|
||||
"""Return the planetary hour metadata for the configured query."""
|
||||
if self.hour_num is None:
|
||||
return None
|
||||
return self.loader.clock_hour(self.hour_num)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
parts = []
|
||||
if self.month_num:
|
||||
parts.append(f"month={self.month_num}")
|
||||
if self.day_num:
|
||||
parts.append(f"day={self.day_num}")
|
||||
if self.hour_num:
|
||||
parts.append(f"hour={self.hour_num}")
|
||||
return f"TemporalQuery({', '.join(parts)})"
|
||||
|
||||
|
||||
class DLT:
|
||||
"""
|
||||
Double Letter Trump (DLT) accessor.
|
||||
|
||||
Double Letter Trumps are Major Arcana cards 3-21 (19 cards total),
|
||||
each associated with a Hebrew letter and planetary/astrological force.
|
||||
|
||||
Usage:
|
||||
dlt = DLT(3) # Get the 3rd Double Letter Trump (The Empress)
|
||||
dlt = DLT(7) # Get the 7th Double Letter Trump (The Chariot)
|
||||
"""
|
||||
|
||||
def __init__(self, trump_number: int) -> None:
|
||||
"""
|
||||
Initialize a Double Letter Trump query.
|
||||
|
||||
Args:
|
||||
trump_number: Position in DLT sequence (3-21)
|
||||
|
||||
Raises:
|
||||
ValueError: If trump_number is not 3-21
|
||||
"""
|
||||
if not 3 <= trump_number <= 21:
|
||||
raise ValueError(f"DLT number must be 3-21, got {trump_number}")
|
||||
|
||||
self.trump_number = trump_number
|
||||
self._loader: Optional['CardDataLoader'] = None
|
||||
self._deck: Optional[Deck] = None
|
||||
|
||||
@property
|
||||
def loader(self) -> 'CardDataLoader':
|
||||
"""Lazy-load CardDataLoader on first access."""
|
||||
if self._loader is None:
|
||||
from ..card.data import CardDataLoader
|
||||
self._loader = CardDataLoader()
|
||||
return self._loader
|
||||
|
||||
@property
|
||||
def deck(self) -> 'Deck':
|
||||
"""Lazy-load Deck on first access."""
|
||||
if self._deck is None:
|
||||
self._deck = Deck()
|
||||
return self._deck
|
||||
|
||||
def card(self) -> Optional[Card]:
|
||||
"""Get the Tarot card for this DLT."""
|
||||
# Major Arcana cards are numbered 0-21, so DLT(3) = Major card 3
|
||||
for card in self.deck.cards:
|
||||
if card.arcana == "Major" and card.number == self.trump_number:
|
||||
return card
|
||||
return None
|
||||
|
||||
def periodic_entry(self) -> Optional[PeriodicTable]:
|
||||
"""Get the periodic table entry with cross-correspondences."""
|
||||
return self.loader.periodic_entry(self.trump_number)
|
||||
|
||||
def sephera(self) -> Optional[Sephera]:
|
||||
"""Get the Sephira associated with this DLT."""
|
||||
return self.loader.sephera(self.trump_number)
|
||||
|
||||
def planet(self) -> Optional[Planet]:
|
||||
"""Get the planetary ruler for this DLT."""
|
||||
periodic = self.periodic_entry()
|
||||
return periodic.planet if periodic else None
|
||||
|
||||
def element(self) -> Optional[ElementType]:
|
||||
"""Get the element associated with this DLT."""
|
||||
periodic = self.periodic_entry()
|
||||
return periodic.element if periodic else None
|
||||
|
||||
def hebrew_letter(self) -> Optional[str]:
|
||||
"""Get the Hebrew letter associated with this DLT."""
|
||||
periodic = self.periodic_entry()
|
||||
return periodic.hebrew_letter if periodic else None
|
||||
|
||||
def color(self) -> Optional[Color]:
|
||||
"""Get the color associated with this DLT."""
|
||||
periodic = self.periodic_entry()
|
||||
return periodic.color if periodic else None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
card = self.card()
|
||||
card_name = card.name if card else "Unknown"
|
||||
return f"DLT({self.trump_number}) - {card_name}"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
|
||||
class Deck:
|
||||
"""Represents a standard 78-card Tarot deck."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize a standard Tarot deck with all 78 cards."""
|
||||
self.cards: List[Card] = []
|
||||
self.discard_pile: List[Card] = []
|
||||
self._initialize_deck()
|
||||
|
||||
def _initialize_deck(self) -> None:
|
||||
"""Initialize the deck with all 78 Tarot cards.
|
||||
|
||||
Order: Cups (1-14), Pentacles/Disks (15-28), Swords (29-42),
|
||||
Major Arcana (43-64), Wands (65-78)
|
||||
|
||||
This puts Queen of Wands as card #78, the final card.
|
||||
"""
|
||||
# Minor Arcana - First three suits (Cups, Pentacles, Swords)
|
||||
# Organized logically: Ace, 10, 2-9, then court cards Knight, Prince, Princess, Queen
|
||||
# Get ElementType instances from CardDataLoader
|
||||
card_data = _get_card_data()
|
||||
water_element = card_data.element("Water")
|
||||
earth_element = card_data.element("Earth")
|
||||
air_element = card_data.element("Air")
|
||||
fire_element = card_data.element("Fire")
|
||||
|
||||
if not water_element or not earth_element or not air_element or not fire_element:
|
||||
raise RuntimeError("Failed to load element data from CardDataLoader")
|
||||
|
||||
# Get Hebrew letters (Paths) for court cards
|
||||
yod_path = card_data.path(20) # Yod
|
||||
vav_path = card_data.path(16) # Vav
|
||||
he_path = card_data.path(15) # He (Heh)
|
||||
|
||||
if not yod_path or not vav_path or not he_path:
|
||||
raise RuntimeError("Failed to load Hebrew letter/path data from CardDataLoader")
|
||||
|
||||
# Map court ranks to their associated elements and Hebrew letter paths
|
||||
# Knight -> Fire + Yod, Prince -> Air + Vav, Princess -> Earth + Heh, Queen -> Water + Heh
|
||||
court_rank_mappings = {
|
||||
"Knight": (fire_element, yod_path),
|
||||
"Prince": (air_element, vav_path),
|
||||
"Princess": (earth_element, he_path),
|
||||
"Queen": (water_element, he_path),
|
||||
}
|
||||
|
||||
suits_data_first = [
|
||||
("Cups", water_element, 2),
|
||||
("Pentacles", earth_element, 4),
|
||||
("Swords", air_element, 3),
|
||||
]
|
||||
|
||||
# Pip order: Ace (1), Ten (10), Two-Nine (2-9), Knight (12), Prince (11), Princess (13), Queen (14)
|
||||
pip_order = [1, 10, 2, 3, 4, 5, 6, 7, 8, 9, 12, 11, 13, 14]
|
||||
pip_names = {
|
||||
1: "Ace", 2: "Two", 3: "Three", 4: "Four", 5: "Five",
|
||||
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten",
|
||||
11: "Prince", 12: "Knight", 13: "Princess", 14: "Queen"
|
||||
}
|
||||
|
||||
card_number = 1
|
||||
# Pip order: Ace (1), Ten (10), Two-Nine (2-9), then Court cards Knight (12), Prince (11), Princess (13), Queen (14)
|
||||
# Map pip_order indices to actual pip numbers (1-10 only for pips)
|
||||
pip_index_to_number = {
|
||||
1: 1, # Ace
|
||||
10: 10, # Ten
|
||||
2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9 # Two through Nine
|
||||
}
|
||||
court_ranks = {
|
||||
12: "Knight", 11: "Prince", 13: "Princess", 14: "Queen"
|
||||
}
|
||||
|
||||
# Loop through first three suits
|
||||
for suit_name, element_name, suit_num in suits_data_first:
|
||||
suit = Suit(name=suit_name, element=element_name,
|
||||
tarot_correspondence=f"{suit_name} Suit", number=suit_num)
|
||||
|
||||
# Then loop through each position in the custom order
|
||||
for pip_index in pip_order:
|
||||
# Create appropriate card type based on pip_index
|
||||
if pip_index <= 10:
|
||||
# Pip card (Ace through 10)
|
||||
actual_pip = pip_index_to_number[pip_index]
|
||||
if pip_index == 1:
|
||||
# Ace card
|
||||
card = AceCard(
|
||||
number=card_number,
|
||||
name=f"{pip_names[pip_index]} of {suit_name}",
|
||||
meaning=Meaning(
|
||||
upright=f"{pip_names[pip_index]} of {suit_name} upright",
|
||||
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
|
||||
),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=actual_pip
|
||||
)
|
||||
else:
|
||||
# Regular pip card (2-10)
|
||||
card = PipCard(
|
||||
number=card_number,
|
||||
name=f"{pip_names[pip_index]} of {suit_name}",
|
||||
meaning=Meaning(
|
||||
upright=f"{pip_names[pip_index]} of {suit_name} upright",
|
||||
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
|
||||
),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=actual_pip
|
||||
)
|
||||
else:
|
||||
# Court card (no pip)
|
||||
court_rank = court_ranks[pip_index]
|
||||
associated_element, hebrew_letter_path = court_rank_mappings[court_rank]
|
||||
card = CourtCard(
|
||||
number=card_number,
|
||||
name=f"{pip_names[pip_index]} of {suit_name}",
|
||||
meaning=Meaning(
|
||||
upright=f"{pip_names[pip_index]} of {suit_name} upright",
|
||||
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
|
||||
),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
court_rank=court_rank,
|
||||
associated_element=associated_element,
|
||||
hebrew_letter_path=hebrew_letter_path
|
||||
)
|
||||
self.cards.append(card)
|
||||
card_number += 1
|
||||
|
||||
# Major Arcana (43-64)
|
||||
# Names match filenames in src/tarot/deck/default/
|
||||
major_arcana_names = [
|
||||
"Fool", "Magus", "Fortune", "Lust", "Hanged Man", "Death",
|
||||
"Art", "Devil", "Tower", "Star", "Moon", "Sun",
|
||||
"High Priestess", "Empress", "Emperor", "Hierophant",
|
||||
"Lovers", "Chariot", "Justice", "Hermit", "Aeon", "Universe"
|
||||
]
|
||||
|
||||
for i, name in enumerate(major_arcana_names):
|
||||
card = MajorCard(
|
||||
number=card_number,
|
||||
name=name,
|
||||
meaning=Meaning(
|
||||
upright=f"{name} upright meaning",
|
||||
reversed=f"{name} reversed meaning"
|
||||
),
|
||||
arcana="Major",
|
||||
kabbalistic_number=i
|
||||
)
|
||||
self.cards.append(card)
|
||||
card_number += 1
|
||||
|
||||
# Minor Arcana - Last suit (Wands, 65-78)
|
||||
# Organized logically: Ace, 10, 2-9, then court cards Knight, Prince, Princess, Queen
|
||||
suits_data_last = [
|
||||
("Wands", fire_element, 1),
|
||||
]
|
||||
|
||||
# Loop through last suit
|
||||
for suit_name, element_name, suit_num in suits_data_last:
|
||||
suit = Suit(name=suit_name, element=element_name,
|
||||
tarot_correspondence=f"{suit_name} Suit", number=suit_num)
|
||||
|
||||
# Then loop through each position in the custom order
|
||||
for pip_index in pip_order:
|
||||
# Create appropriate card type based on pip_index
|
||||
if pip_index <= 10:
|
||||
# Pip card (Ace through 10)
|
||||
actual_pip = pip_index_to_number[pip_index]
|
||||
if pip_index == 1:
|
||||
# Ace card
|
||||
card = AceCard(
|
||||
number=card_number,
|
||||
name=f"{pip_names[pip_index]} of {suit_name}",
|
||||
meaning=Meaning(
|
||||
upright=f"{pip_names[pip_index]} of {suit_name} upright",
|
||||
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
|
||||
),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=actual_pip
|
||||
)
|
||||
else:
|
||||
# Regular pip card (2-10)
|
||||
card = PipCard(
|
||||
number=card_number,
|
||||
name=f"{pip_names[pip_index]} of {suit_name}",
|
||||
meaning=Meaning(
|
||||
upright=f"{pip_names[pip_index]} of {suit_name} upright",
|
||||
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
|
||||
),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=actual_pip
|
||||
)
|
||||
else:
|
||||
# Court card (no pip)
|
||||
court_rank = court_ranks[pip_index]
|
||||
associated_element, hebrew_letter_path = court_rank_mappings[court_rank]
|
||||
card = CourtCard(
|
||||
number=card_number,
|
||||
name=f"{pip_names[pip_index]} of {suit_name}",
|
||||
meaning=Meaning(
|
||||
upright=f"{pip_names[pip_index]} of {suit_name} upright",
|
||||
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
|
||||
),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
court_rank=court_rank,
|
||||
associated_element=associated_element,
|
||||
hebrew_letter_path=hebrew_letter_path
|
||||
)
|
||||
self.cards.append(card)
|
||||
card_number += 1
|
||||
|
||||
|
||||
def shuffle(self) -> None:
|
||||
"""Shuffle the deck."""
|
||||
random.shuffle(self.cards)
|
||||
|
||||
def draw(self, num_cards: int = 1) -> List[Card]:
|
||||
"""
|
||||
Draw cards from the deck.
|
||||
|
||||
Args:
|
||||
num_cards: Number of cards to draw (default: 1)
|
||||
|
||||
Returns:
|
||||
List of drawn cards
|
||||
"""
|
||||
if num_cards < 1:
|
||||
raise ValueError("Must draw at least 1 card")
|
||||
|
||||
if num_cards > len(self.cards):
|
||||
raise ValueError(f"Cannot draw {num_cards} cards from deck with {len(self.cards)} cards")
|
||||
|
||||
drawn = []
|
||||
for _ in range(num_cards):
|
||||
drawn.append(self.cards.pop(0))
|
||||
|
||||
return drawn
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the deck to its initial state."""
|
||||
self.cards.clear()
|
||||
self.discard_pile.clear()
|
||||
self._initialize_deck()
|
||||
|
||||
def remaining(self) -> int:
|
||||
"""Return the number of cards remaining in the deck."""
|
||||
return len(self.cards)
|
||||
|
||||
def number(self, pip_value: int) -> CardQuery:
|
||||
"""
|
||||
Query cards by number (pip value).
|
||||
|
||||
Usage:
|
||||
deck.number(3) # All cards with 3
|
||||
deck.number(3).minor # All minor 3s
|
||||
deck.number(3).minor.wands # 3 of Wands
|
||||
"""
|
||||
return CardQuery(self, pip_value)
|
||||
|
||||
def suit(self, suit_name: str) -> List[Card]:
|
||||
"""
|
||||
Get all cards from a specific suit.
|
||||
|
||||
Usage:
|
||||
deck.suit("Wands")
|
||||
"""
|
||||
return [c for c in self.cards if hasattr(c, 'suit') and
|
||||
c.suit and c.suit.name == suit_name]
|
||||
|
||||
@property
|
||||
def major(self) -> List[Card]:
|
||||
"""Get all Major Arcana cards."""
|
||||
return [c for c in self.cards if c.arcana == "Major"]
|
||||
|
||||
@property
|
||||
def minor(self) -> List[Card]:
|
||||
"""Get all Minor Arcana cards."""
|
||||
return [c for c in self.cards if c.arcana == "Minor"]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of cards in the deck."""
|
||||
return len(self.cards)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Deck({len(self.cards)} cards remaining)"
|
||||
BIN
src/tarot/deck/default/01_Ace Cups.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/02_Ten Cups.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/03_Two Cups.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/04_Three Cups.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/tarot/deck/default/05_Four Cups.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/06_Five Cups.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/tarot/deck/default/07_Six Cups.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/08_Seven Cups.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/tarot/deck/default/09_Eight Cups.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
src/tarot/deck/default/10_Nine Cups.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/11_Knight Cups.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
src/tarot/deck/default/12_Prince Cups.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/tarot/deck/default/13_Princess Cups.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/14_Queen Cups.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src/tarot/deck/default/15_Ace Disks.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/tarot/deck/default/16_Ten Disks.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/17_Two Disks.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/18_Three Disks.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src/tarot/deck/default/19_Four Disks.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/tarot/deck/default/20_Five Disks.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/tarot/deck/default/21_Six Disks.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/22_Seven Disks.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
src/tarot/deck/default/23_Eight Disks.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/tarot/deck/default/24_Nine Disks.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/25_Knight Disks.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/26_Prince Disks.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
src/tarot/deck/default/27_Princess Disks.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/28_Queen Disks.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/29_Ace Swords.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
src/tarot/deck/default/30_Ten Swords.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/31_Two Swords.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/tarot/deck/default/32_Three Swords.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/tarot/deck/default/33_Four Swords.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/tarot/deck/default/34_Five Swords.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/tarot/deck/default/35_Six Swords.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/tarot/deck/default/36_Seven Swords.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
src/tarot/deck/default/37_Eight Swords.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/tarot/deck/default/38_Nine Swords.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/tarot/deck/default/39_Knight Swords.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
src/tarot/deck/default/40_Prince Swords.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/tarot/deck/default/41_Princess Swords.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/tarot/deck/default/42_Queen Swords.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/43_Fool.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src/tarot/deck/default/44_Magus.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/45_Fortune.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/46_Lust.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/47_Hanged Man.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/tarot/deck/default/48_Death.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/tarot/deck/default/49_Art.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/50_Devil.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/tarot/deck/default/51_Tower.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/tarot/deck/default/52_Star.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src/tarot/deck/default/53_Moon.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/tarot/deck/default/54_Sun.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
src/tarot/deck/default/55_High Priestess.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/tarot/deck/default/56_Aeon.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/tarot/deck/default/57_Universe.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
src/tarot/deck/default/58_Empress.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/59_Emperor.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/tarot/deck/default/60_Hierophant.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/61_Lovers.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/62_Chariot.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/tarot/deck/default/63_Justice.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/tarot/deck/default/64_Hermit.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
src/tarot/deck/default/65_Ace Wands.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/tarot/deck/default/66_Ten Wands.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
src/tarot/deck/default/67_Two Wands.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/68_Three Wands.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
src/tarot/deck/default/69_Four Wands.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/70_Five Wands.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |