f
This commit is contained in:
16
mytest.py
16
mytest.py
@@ -1,16 +0,0 @@
|
||||
from tarot import Tarot, letter, number, kaballah
|
||||
from temporal import ThalemaClock
|
||||
from datetime import datetime
|
||||
from utils import Personality, MBTIType
|
||||
|
||||
from tarot.ui import display_cards,display_cube
|
||||
from tarot.deck import Deck
|
||||
|
||||
# Get some cards
|
||||
deck = Deck()
|
||||
cards = Tarot.deck.card.filter(suit="cups",type="ace")
|
||||
|
||||
print(cards)
|
||||
# Display using default deck
|
||||
display_cards(cards)
|
||||
)
|
||||
@@ -11,19 +11,19 @@ Provides four root namespaces for different domains:
|
||||
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)
|
||||
|
||||
@@ -8,15 +8,15 @@ Provides fluent query interface for:
|
||||
|
||||
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
|
||||
from .tree import Tree
|
||||
|
||||
# Export classes for fluent access
|
||||
__all__ = ["Tree", "Cube"]
|
||||
|
||||
@@ -6,22 +6,22 @@ including Sephira, Paths, and Tree of Life structures.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from utils.attributes import (
|
||||
Element,
|
||||
ElementType,
|
||||
Planet,
|
||||
Color,
|
||||
Colorscale,
|
||||
Perfume,
|
||||
ElementType,
|
||||
God,
|
||||
Perfume,
|
||||
Planet,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Sephera:
|
||||
"""Represents a Sephira on the Tree of Life."""
|
||||
|
||||
number: int
|
||||
name: str
|
||||
hebrew_name: str
|
||||
@@ -29,21 +29,22 @@ class Sephera:
|
||||
archangel: str
|
||||
order_of_angels: str
|
||||
mundane_chakra: str
|
||||
element: Optional['ElementType'] = None
|
||||
element: Optional["ElementType"] = None
|
||||
planetary_ruler: Optional[str] = None
|
||||
tarot_trump: Optional[str] = None
|
||||
colorscale: Optional['Colorscale'] = 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
|
||||
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
|
||||
@@ -55,6 +56,7 @@ class PeriodicTable:
|
||||
@dataclass
|
||||
class TreeOfLife:
|
||||
"""Represents the Tree of Life structure."""
|
||||
|
||||
sephiroth: Dict[int, str]
|
||||
paths: Dict[Tuple[int, int], str]
|
||||
|
||||
@@ -62,6 +64,7 @@ class TreeOfLife:
|
||||
@dataclass
|
||||
class Correspondences:
|
||||
"""Represents Kabbalistic correspondences."""
|
||||
|
||||
number: int
|
||||
sephira: str
|
||||
element: Optional[str]
|
||||
@@ -76,55 +79,56 @@ class Correspondences:
|
||||
@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
|
||||
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)
|
||||
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:
|
||||
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:
|
||||
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']:
|
||||
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'] = []
|
||||
merged: List["God"] = []
|
||||
for values in self.gods.values():
|
||||
merged.extend(values)
|
||||
return merged
|
||||
@@ -132,36 +136,36 @@ class Path:
|
||||
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)
|
||||
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("")
|
||||
@@ -178,7 +182,7 @@ class Path:
|
||||
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("")
|
||||
@@ -187,7 +191,7 @@ class Path:
|
||||
for line in str(perfume).split("\n"):
|
||||
lines.append(f" {line}")
|
||||
lines.append("")
|
||||
|
||||
|
||||
# Gods
|
||||
if self.gods:
|
||||
lines.append("")
|
||||
@@ -198,18 +202,18 @@ class Path:
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Cube namespace - access Cube of Space walls and areas."""
|
||||
|
||||
from .cube import Cube
|
||||
from .attributes import CubeOfSpace, Wall, WallDirection
|
||||
from .cube import Cube
|
||||
|
||||
__all__ = ["Cube", "CubeOfSpace", "Wall", "WallDirection"]
|
||||
|
||||
@@ -13,10 +13,11 @@ from typing import Dict, List, Optional
|
||||
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
|
||||
@@ -24,9 +25,9 @@ class WallDirection:
|
||||
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(
|
||||
@@ -35,7 +36,7 @@ class WallDirection:
|
||||
)
|
||||
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})"
|
||||
@@ -45,12 +46,13 @@ class WallDirection:
|
||||
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)
|
||||
@@ -60,9 +62,9 @@ class Wall:
|
||||
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",
|
||||
@@ -72,45 +74,43 @@ class Wall:
|
||||
"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}'"
|
||||
)
|
||||
|
||||
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"
|
||||
@@ -123,7 +123,7 @@ class Wall:
|
||||
f" Archangel: {self.archangel}",
|
||||
f" Keywords: {keywords_str}",
|
||||
]
|
||||
|
||||
|
||||
# Add directions with their details recursively
|
||||
if self.directions:
|
||||
lines.append(" Directions:")
|
||||
@@ -145,22 +145,22 @@ class Wall:
|
||||
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
|
||||
@@ -170,16 +170,17 @@ class Wall:
|
||||
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": {
|
||||
@@ -387,28 +388,26 @@ class CubeOfSpace:
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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())}"
|
||||
)
|
||||
|
||||
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 = {
|
||||
@@ -416,9 +415,9 @@ class CubeOfSpace:
|
||||
"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"}
|
||||
"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
|
||||
@@ -436,7 +435,7 @@ class CubeOfSpace:
|
||||
)
|
||||
# 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,
|
||||
@@ -450,7 +449,7 @@ class CubeOfSpace:
|
||||
directions=directions,
|
||||
)
|
||||
walls[wall_name] = wall
|
||||
|
||||
|
||||
# Create central core
|
||||
central_core = WallDirection(
|
||||
name="Center",
|
||||
@@ -459,55 +458,55 @@ class CubeOfSpace:
|
||||
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)
|
||||
|
||||
@@ -5,11 +5,11 @@ 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
|
||||
@@ -17,19 +17,22 @@ Usage:
|
||||
wall.direction("East") # Get specific direction
|
||||
"""
|
||||
|
||||
from typing import Optional, Any
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from kaballah.cube.attributes import CubeOfSpace
|
||||
|
||||
|
||||
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 {}
|
||||
|
||||
walls = cls._cube.walls if hasattr(cls._cube, "walls") else {}
|
||||
lines = [
|
||||
"Cube of Space",
|
||||
"=" * 60,
|
||||
@@ -37,23 +40,23 @@ class CubeMeta(type):
|
||||
"",
|
||||
"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
|
||||
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 {}
|
||||
walls = cls._cube.walls if hasattr(cls._cube, "walls") else {}
|
||||
return f"Cube(walls={len(walls)})"
|
||||
|
||||
|
||||
@@ -68,7 +71,7 @@ class DirectionAccessor:
|
||||
|
||||
def all(self) -> list:
|
||||
"""Get all directions in this wall."""
|
||||
if self._wall is None or not hasattr(self._wall, 'directions'):
|
||||
if self._wall is None or not hasattr(self._wall, "directions"):
|
||||
return []
|
||||
return list(self._wall.directions.values())
|
||||
|
||||
@@ -89,10 +92,7 @@ class DirectionAccessor:
|
||||
|
||||
# Filter by direction name if provided
|
||||
if direction_name:
|
||||
all_dirs = [
|
||||
d for d in all_dirs
|
||||
if d.name.lower() == direction_name.lower()
|
||||
]
|
||||
all_dirs = [d for d in all_dirs if d.name.lower() == direction_name.lower()]
|
||||
|
||||
# Apply other filters
|
||||
if kwargs:
|
||||
@@ -126,7 +126,7 @@ class DirectionAccessor:
|
||||
"""Get specific direction by name."""
|
||||
if direction_name is None:
|
||||
return self.all()
|
||||
if self._wall is None or not hasattr(self._wall, 'directions'):
|
||||
if self._wall is None or not hasattr(self._wall, "directions"):
|
||||
return None
|
||||
return self._wall.directions.get(direction_name.capitalize())
|
||||
|
||||
@@ -152,7 +152,7 @@ class WallWrapper:
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Delegate attribute access to the wrapped wall."""
|
||||
if name in ('_wall', '_direction_accessor'):
|
||||
if name in ("_wall", "_direction_accessor"):
|
||||
return object.__getattribute__(self, name)
|
||||
return getattr(self._wall, name)
|
||||
|
||||
@@ -274,7 +274,7 @@ class WallAccessor:
|
||||
|
||||
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()
|
||||
@@ -307,10 +307,10 @@ class Cube(metaclass=CubeMeta):
|
||||
# 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
|
||||
@@ -339,15 +339,16 @@ class Cube(metaclass=CubeMeta):
|
||||
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
|
||||
|
||||
@@ -5,31 +5,31 @@ 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
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union, overload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.attributes import Sephera, Path
|
||||
from tarot.attributes import Path, Sephera
|
||||
from tarot.card.data import CardDataLoader
|
||||
from utils.query import QueryResult, Query
|
||||
from utils.query import 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')
|
||||
sepheras = type.__getattribute__(cls, "_sepheras")
|
||||
paths = type.__getattribute__(cls, "_paths")
|
||||
lines = [
|
||||
"Tree of Life",
|
||||
"=" * 60,
|
||||
@@ -38,99 +38,99 @@ class TreeMeta(type):
|
||||
"",
|
||||
"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')
|
||||
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
|
||||
|
||||
_sepheras: Dict[int, "Sephera"] = {} # type: ignore
|
||||
_paths: Dict[int, "Path"] = {} # type: ignore
|
||||
_initialized: bool = False
|
||||
_loader: Optional['CardDataLoader'] = None # type: ignore
|
||||
|
||||
_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']:
|
||||
...
|
||||
|
||||
def sephera(cls, number: int) -> Optional["Sephera"]: ...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def sephera(cls, number: None = ...) -> Dict[int, 'Sephera']:
|
||||
...
|
||||
|
||||
def sephera(cls, number: None = ...) -> Dict[int, "Sephera"]: ...
|
||||
|
||||
@classmethod
|
||||
def sephera(cls, number: Optional[int] = None) -> Union[Optional['Sephera'], Dict[int, 'Sephera']]:
|
||||
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']:
|
||||
...
|
||||
|
||||
def path(cls, number: int) -> Optional["Path"]: ...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def path(cls, number: None = ...) -> Dict[int, 'Path']:
|
||||
...
|
||||
|
||||
def path(cls, number: None = ...) -> Dict[int, "Path"]: ...
|
||||
|
||||
@classmethod
|
||||
def path(cls, number: Optional[int] = None) -> Union[Optional['Path'], Dict[int, 'Path']]:
|
||||
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':
|
||||
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)
|
||||
|
||||
@@ -11,17 +11,16 @@ Provides fluent query interface for:
|
||||
|
||||
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 .iChing import hexagram, trigram
|
||||
from .letter import letter
|
||||
from .iChing import trigram, hexagram
|
||||
from .words import word
|
||||
from .paths import letters
|
||||
from .words import word
|
||||
|
||||
__all__ = ["letter", "trigram", "hexagram", "word", "letters"]
|
||||
|
||||
|
||||
@@ -6,19 +6,19 @@ including Alphabets, Enochian letters, and Double Letter Trumps.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple, Any
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from utils.attributes import (
|
||||
Element,
|
||||
ElementType,
|
||||
Planet,
|
||||
Meaning,
|
||||
Planet,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Letter:
|
||||
"""Represents a letter with its attributes."""
|
||||
|
||||
character: str
|
||||
position: int
|
||||
name: str
|
||||
@@ -27,10 +27,11 @@ class Letter:
|
||||
@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}")
|
||||
@@ -41,10 +42,11 @@ class EnglishAlphabet:
|
||||
@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}")
|
||||
@@ -53,11 +55,12 @@ class GreekAlphabet:
|
||||
@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}")
|
||||
@@ -66,19 +69,20 @@ class HebrewAlphabet:
|
||||
@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
|
||||
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
|
||||
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
|
||||
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}")
|
||||
@@ -87,6 +91,7 @@ class DoublLetterTrump:
|
||||
@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
|
||||
@@ -100,6 +105,7 @@ class EnochianLetter:
|
||||
@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
|
||||
@@ -113,30 +119,33 @@ class EnochianSpirit:
|
||||
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
|
||||
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)
|
||||
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']:
|
||||
|
||||
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):
|
||||
@@ -148,12 +157,13 @@ class EnochianArchetype:
|
||||
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
|
||||
@@ -169,7 +179,7 @@ class EnochianGridPosition:
|
||||
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}
|
||||
@@ -188,24 +198,27 @@ class EnochianGridPosition:
|
||||
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
|
||||
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
|
||||
@@ -214,12 +227,11 @@ class EnochianTablet:
|
||||
"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)}"
|
||||
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)
|
||||
@@ -227,23 +239,23 @@ class EnochianTablet:
|
||||
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)]
|
||||
|
||||
@@ -5,18 +5,17 @@ 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 typing import TYPE_CHECKING, Dict
|
||||
|
||||
from utils.query import CollectionAccessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card.data import CardDataLoader
|
||||
from tarot.attributes import Trigram, Hexagram
|
||||
from tarot.attributes import Hexagram, Trigram
|
||||
|
||||
|
||||
def _line_diagram_from_binary(binary: str) -> str:
|
||||
@@ -36,14 +35,14 @@ class _Trigram:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._trigrams: Dict[str, 'Trigram'] = {}
|
||||
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
|
||||
|
||||
@@ -54,16 +53,80 @@ class _Trigram:
|
||||
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."},
|
||||
{
|
||||
"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:
|
||||
@@ -88,14 +151,14 @@ class _Hexagram:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._hexagrams: Dict[int, 'Hexagram'] = {}
|
||||
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
|
||||
|
||||
@@ -107,78 +170,718 @@ class _Hexagram:
|
||||
"""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"},
|
||||
{
|
||||
"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 = {}
|
||||
|
||||
@@ -14,6 +14,7 @@ from utils.attributes import Number, Planet
|
||||
@dataclass
|
||||
class Trigram:
|
||||
"""Represents one of the eight I Ching trigrams."""
|
||||
|
||||
name: str
|
||||
chinese_name: str
|
||||
pinyin: str
|
||||
@@ -27,6 +28,7 @@ class Trigram:
|
||||
@dataclass
|
||||
class Hexagram:
|
||||
"""Represents an I Ching hexagram with Tarot correspondence."""
|
||||
|
||||
number: int
|
||||
name: str
|
||||
chinese_name: str
|
||||
|
||||
@@ -13,7 +13,7 @@ class Letter:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._loader: 'CardDataLoader | None' = None
|
||||
self._loader: "CardDataLoader | None" = None
|
||||
self.alphabet = CollectionAccessor(self._get_alphabets)
|
||||
self.cipher = CollectionAccessor(self._get_ciphers)
|
||||
self.letter = CollectionAccessor(self._get_letters)
|
||||
@@ -26,10 +26,11 @@ class Letter:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
|
||||
self._loader = CardDataLoader()
|
||||
self._initialized = True
|
||||
|
||||
def _require_loader(self) -> 'CardDataLoader':
|
||||
def _require_loader(self) -> "CardDataLoader":
|
||||
self._ensure_initialized()
|
||||
assert self._loader is not None, "Loader not initialized"
|
||||
return self._loader
|
||||
@@ -54,10 +55,10 @@ class Letter:
|
||||
loader = self._require_loader()
|
||||
return loader._periodic_table.copy()
|
||||
|
||||
def word(self, text: str, *, alphabet: str = 'english'):
|
||||
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')
|
||||
|
||||
@@ -17,66 +17,68 @@ Each letter has attributes like:
|
||||
- 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
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
||||
|
||||
from utils.filter import format_results, get_filterable_fields, universal_filter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from utils.query import CollectionAccessor
|
||||
from tarot.attributes import Path
|
||||
from utils.query import CollectionAccessor
|
||||
|
||||
|
||||
@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
|
||||
|
||||
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."""
|
||||
@@ -85,17 +87,17 @@ class TarotLetter:
|
||||
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 = [
|
||||
@@ -104,7 +106,7 @@ class TarotLetter:
|
||||
f"Type: {self.letter_type}",
|
||||
f"Position: {self.position}",
|
||||
]
|
||||
|
||||
|
||||
if self.trump:
|
||||
lines.append(f"Trump: {self.trump}")
|
||||
if self.zodiac:
|
||||
@@ -119,20 +121,20 @@ class TarotLetter:
|
||||
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):
|
||||
@@ -142,55 +144,59 @@ class LetterAccessor:
|
||||
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]
|
||||
|
||||
return [letter for letter in self._letters.values() if letter.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()]
|
||||
|
||||
return [
|
||||
letter
|
||||
for letter in self._letters.values()
|
||||
if letter.planet and planet.lower() in letter.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)
|
||||
|
||||
return next((letter for letter in self._letters.values() if letter.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")
|
||||
@@ -198,30 +204,30 @@ class LetterAccessor:
|
||||
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 = []
|
||||
@@ -229,20 +235,20 @@ class LetterAccessor:
|
||||
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."""
|
||||
@@ -251,33 +257,34 @@ class LetterAccessor:
|
||||
|
||||
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'
|
||||
|
||||
|
||||
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)"
|
||||
@@ -285,50 +292,50 @@ class IChing:
|
||||
|
||||
class LettersRegistry:
|
||||
"""Registry and accessor for all Hebrew letters with Tarot correspondences."""
|
||||
|
||||
_instance: Optional['LettersRegistry'] = None
|
||||
|
||||
_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)
|
||||
|
||||
@@ -8,25 +8,26 @@ if TYPE_CHECKING:
|
||||
|
||||
class _Word:
|
||||
"""Fluent accessor for word analysis and cipher operations."""
|
||||
|
||||
_loader: 'CardDataLoader | None' = None
|
||||
|
||||
_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'):
|
||||
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')
|
||||
|
||||
@@ -8,12 +8,12 @@ Provides fluent query interface for:
|
||||
|
||||
Usage:
|
||||
from tarot import number
|
||||
|
||||
|
||||
num = number.number(5)
|
||||
root = number.digital_root(256)
|
||||
colors = number.color()
|
||||
"""
|
||||
|
||||
from .number import number, calculate_digital_root
|
||||
from .number import calculate_digital_root, number
|
||||
|
||||
__all__ = ["number", "calculate_digital_root"]
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
"""Numbers loader - access to numerology and number correspondences."""
|
||||
|
||||
from typing import Dict, Optional, Union, overload
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union, overload
|
||||
|
||||
from utils.filter import universal_filter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from utils.attributes import Color, Number
|
||||
|
||||
|
||||
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
|
||||
@@ -27,172 +31,173 @@ def calculate_digital_root(value: int) -> int:
|
||||
"""
|
||||
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
|
||||
_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']:
|
||||
...
|
||||
|
||||
def number(cls, value: int) -> Optional["Number"]: ...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def number(cls, value: None = ...) -> Dict[int, 'Number']:
|
||||
...
|
||||
|
||||
def number(cls, value: None = ...) -> Dict[int, "Number"]: ...
|
||||
|
||||
@classmethod
|
||||
def number(cls, value: Optional[int] = None) -> Union[Optional['Number'], Dict[int, 'Number']]:
|
||||
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']:
|
||||
...
|
||||
|
||||
def color(cls, sephera_number: int) -> Optional["Color"]: ...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def color(cls, sephera_number: None = ...) -> Dict[int, 'Color']:
|
||||
...
|
||||
|
||||
def color(cls, sephera_number: None = ...) -> Dict[int, "Color"]: ...
|
||||
|
||||
@classmethod
|
||||
def color(cls, sephera_number: Optional[int] = None) -> Union[Optional['Color'], Dict[int, 'Color']]:
|
||||
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']:
|
||||
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']:
|
||||
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)
|
||||
|
||||
@@ -11,16 +11,16 @@ if TYPE_CHECKING:
|
||||
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
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class _Number:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._initialized: bool = False
|
||||
self._loader: 'CardDataLoader | None' = None
|
||||
self._loader: "CardDataLoader | None" = None
|
||||
self.number = CollectionAccessor(self._get_numbers)
|
||||
self.color = CollectionAccessor(self._get_colors)
|
||||
self.cipher = CollectionAccessor(self._get_ciphers)
|
||||
@@ -40,10 +40,11 @@ class _Number:
|
||||
return
|
||||
|
||||
from tarot.card.data import CardDataLoader
|
||||
|
||||
self._loader = CardDataLoader()
|
||||
self._initialized = True
|
||||
|
||||
def _require_loader(self) -> 'CardDataLoader':
|
||||
def _require_loader(self) -> "CardDataLoader":
|
||||
self._ensure_initialized()
|
||||
assert self._loader is not None, "Loader not initialized"
|
||||
return self._loader
|
||||
|
||||
@@ -17,66 +17,99 @@ Unified Namespaces (singular names):
|
||||
|
||||
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 kaballah
|
||||
from kaballah import Cube, Tree
|
||||
from kaballah.cube.attributes import CubeOfSpace, Wall, WallDirection
|
||||
|
||||
# Import from namespace folders
|
||||
from letter import hexagram, letter, trigram
|
||||
from number import calculate_digital_root, number
|
||||
from temporal import PlanetPosition, ThalemaClock
|
||||
from temporal import Zodiac as AstrologyZodiac
|
||||
|
||||
# Import shared attributes from utils
|
||||
from utils.attributes import (
|
||||
Note, Element, ElementType, Number, Color, Colorscale,
|
||||
Planet, God, Cipher, CipherResult, Perfume,
|
||||
Cipher,
|
||||
CipherResult,
|
||||
Color,
|
||||
Colorscale,
|
||||
Element,
|
||||
ElementType,
|
||||
God,
|
||||
Note,
|
||||
Number,
|
||||
Perfume,
|
||||
Planet,
|
||||
)
|
||||
|
||||
from .attributes import (
|
||||
AstrologicalInfluence,
|
||||
CardImage,
|
||||
ClockHour,
|
||||
Correspondences,
|
||||
Day,
|
||||
Degree,
|
||||
DoublLetterTrump,
|
||||
EnglishAlphabet,
|
||||
EnochianArchetype,
|
||||
EnochianGridPosition,
|
||||
EnochianTablet,
|
||||
GreekAlphabet,
|
||||
HebrewAlphabet,
|
||||
Hexagram,
|
||||
Hour,
|
||||
Letter,
|
||||
Meaning,
|
||||
Month,
|
||||
Path,
|
||||
PeriodicTable,
|
||||
Sephera,
|
||||
Suit,
|
||||
TreeOfLife,
|
||||
Trigram,
|
||||
Weekday,
|
||||
Zodiac,
|
||||
)
|
||||
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,
|
||||
ImageDeckLoader,
|
||||
filter_cards_by_keywords,
|
||||
get_card_info,
|
||||
get_cards_by_suit,
|
||||
load_card_details,
|
||||
load_deck_details,
|
||||
get_cards_by_suit,
|
||||
filter_cards_by_keywords,
|
||||
print_card_details,
|
||||
get_card_info,
|
||||
ImageDeckLoader,
|
||||
load_deck_images,
|
||||
print_card_details,
|
||||
)
|
||||
|
||||
# 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
|
||||
from .card.data import CardDataLoader
|
||||
from .deck import DLT, Card, Deck, MajorCard, MinorCard
|
||||
from .tarot_api import Tarot
|
||||
|
||||
|
||||
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__'):
|
||||
|
||||
if hasattr(obj, "__dataclass_fields__"):
|
||||
# It's a dataclass - show all fields
|
||||
print(f"{obj.__class__.__name__}:")
|
||||
for field in fields(obj):
|
||||
@@ -96,12 +129,10 @@ __all__ = [
|
||||
"Tarot",
|
||||
"trigram",
|
||||
"hexagram",
|
||||
|
||||
# Temporal and astrological
|
||||
"ThalemaClock",
|
||||
"AstrologyZodiac",
|
||||
"PlanetPosition",
|
||||
|
||||
# Card details and loading
|
||||
"CardDetailsRegistry",
|
||||
"load_card_details",
|
||||
@@ -110,24 +141,20 @@ __all__ = [
|
||||
"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",
|
||||
@@ -142,7 +169,6 @@ __all__ = [
|
||||
"CubeOfSpace",
|
||||
"WallDirection",
|
||||
"Wall",
|
||||
|
||||
# Sepheric classes
|
||||
"Sephera",
|
||||
"PeriodicTable",
|
||||
@@ -157,12 +183,10 @@ __all__ = [
|
||||
"EnochianTablet",
|
||||
"EnochianGridPosition",
|
||||
"EnochianArchetype",
|
||||
|
||||
# Alphabet classes
|
||||
"EnglishAlphabet",
|
||||
"GreekAlphabet",
|
||||
"HebrewAlphabet",
|
||||
|
||||
# Number and color classes
|
||||
"Number",
|
||||
"Color",
|
||||
@@ -172,7 +196,6 @@ __all__ = [
|
||||
"Hexagram",
|
||||
"Cipher",
|
||||
"CipherResult",
|
||||
|
||||
# Data loader and functions
|
||||
"CardDataLoader",
|
||||
"calculate_digital_root",
|
||||
|
||||
@@ -8,54 +8,54 @@ 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,
|
||||
PeriodicTable,
|
||||
Sephera,
|
||||
TreeOfLife,
|
||||
)
|
||||
from letter.attributes import (
|
||||
Letter,
|
||||
EnglishAlphabet,
|
||||
GreekAlphabet,
|
||||
HebrewAlphabet,
|
||||
DoublLetterTrump,
|
||||
EnglishAlphabet,
|
||||
EnochianArchetype,
|
||||
EnochianGridPosition,
|
||||
EnochianLetter,
|
||||
EnochianSpirit,
|
||||
EnochianTablet,
|
||||
EnochianGridPosition,
|
||||
EnochianArchetype,
|
||||
GreekAlphabet,
|
||||
HebrewAlphabet,
|
||||
Letter,
|
||||
)
|
||||
from letter.iChing_attributes import (
|
||||
Trigram,
|
||||
Hexagram,
|
||||
Trigram,
|
||||
)
|
||||
from temporal.attributes import (
|
||||
AstrologicalInfluence,
|
||||
ClockHour,
|
||||
Degree,
|
||||
Hour,
|
||||
Month,
|
||||
Weekday,
|
||||
Hour,
|
||||
ClockHour,
|
||||
Zodiac,
|
||||
Degree,
|
||||
AstrologicalInfluence,
|
||||
)
|
||||
|
||||
# Re-export shared attributes from utils
|
||||
from utils.attributes import (
|
||||
Cipher,
|
||||
CipherResult,
|
||||
Color,
|
||||
Colorscale,
|
||||
Element,
|
||||
ElementType,
|
||||
God,
|
||||
Meaning,
|
||||
Note,
|
||||
Number,
|
||||
Perfume,
|
||||
Planet,
|
||||
)
|
||||
|
||||
# Alias Day to Weekday for backward compatibility (Day in this context was Day of Week)
|
||||
@@ -113,8 +113,9 @@ __all__ = [
|
||||
@dataclass
|
||||
class Suit:
|
||||
"""Represents a tarot suit."""
|
||||
|
||||
name: str
|
||||
element: 'ElementType'
|
||||
element: "ElementType"
|
||||
tarot_correspondence: str
|
||||
number: int
|
||||
|
||||
@@ -122,8 +123,8 @@ class Suit:
|
||||
@dataclass
|
||||
class CardImage:
|
||||
"""Represents an image associated with a card."""
|
||||
|
||||
filename: str
|
||||
artist: str
|
||||
deck_name: str
|
||||
url: Optional[str] = None
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
from .card import CardAccessor
|
||||
from .details import CardDetailsRegistry
|
||||
from .image_loader import ImageDeckLoader, load_deck_images
|
||||
from .loader import (
|
||||
filter_cards_by_keywords,
|
||||
get_card_info,
|
||||
get_cards_by_suit,
|
||||
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",
|
||||
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
@@ -13,43 +13,45 @@ Usage:
|
||||
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
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from utils.filter import format_results, universal_filter
|
||||
from utils.object_formatting import format_value, get_object_attributes, is_nested_object
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.deck import Card, Deck
|
||||
|
||||
|
||||
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:
|
||||
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')
|
||||
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):
|
||||
@@ -59,16 +61,16 @@ def _format_cards(cards: List['Card']) -> str:
|
||||
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
|
||||
@@ -77,18 +79,19 @@ class CardAccessor:
|
||||
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
|
||||
|
||||
_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']:
|
||||
|
||||
def __call__(self, number: int) -> Optional["Card"]:
|
||||
"""Get a card by number."""
|
||||
self._ensure_initialized()
|
||||
if self._deck is None:
|
||||
@@ -97,11 +100,11 @@ class CardAccessor:
|
||||
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.
|
||||
|
||||
@@ -122,11 +125,11 @@ class CardAccessor:
|
||||
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:
|
||||
@@ -140,11 +143,11 @@ class CardAccessor:
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
@@ -152,13 +155,13 @@ class CardAccessor:
|
||||
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,
|
||||
@@ -166,166 +169,168 @@ class CardAccessor:
|
||||
"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})")
|
||||
|
||||
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]
|
||||
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)
|
||||
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]
|
||||
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)
|
||||
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']:
|
||||
|
||||
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]
|
||||
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)
|
||||
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']:
|
||||
|
||||
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))
|
||||
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 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'):
|
||||
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'):
|
||||
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'):
|
||||
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
|
||||
"element": element_name,
|
||||
"zodiac": zodiac_signs,
|
||||
"keywords": keywords,
|
||||
}
|
||||
|
||||
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
|
||||
|
||||
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']:
|
||||
if info["zodiac"]:
|
||||
lines.append(f" Zodiac: {', '.join(info['zodiac'])}")
|
||||
if info['keywords']:
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
SpreadReading object containing the spread and drawn cards
|
||||
|
||||
|
||||
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
|
||||
|
||||
from tarot.card.spread import Spread, SpreadReading, draw_spread
|
||||
|
||||
# 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 reading
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,16 +8,15 @@ This module provides intelligent image matching and loading, supporting:
|
||||
|
||||
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
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.deck import Card, Deck
|
||||
@@ -25,58 +24,62 @@ if TYPE_CHECKING:
|
||||
|
||||
class ImageDeckLoader:
|
||||
"""Loader for matching Tarot card images to deck cards."""
|
||||
|
||||
|
||||
# Supported image extensions
|
||||
SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
|
||||
|
||||
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)
|
||||
|
||||
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.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()}'))
|
||||
|
||||
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)
|
||||
@@ -84,37 +87,37 @@ class ImageDeckLoader:
|
||||
"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"
|
||||
@@ -122,69 +125,69 @@ class ImageDeckLoader:
|
||||
"""
|
||||
# Convert to lowercase
|
||||
normalized = name.lower()
|
||||
|
||||
|
||||
# Replace special characters with spaces
|
||||
normalized = re.sub(r'[^\w\s]', ' ', normalized)
|
||||
|
||||
normalized = re.sub(r"[^\w\s]", " ", normalized)
|
||||
|
||||
# Collapse multiple spaces
|
||||
normalized = re.sub(r'\s+', ' ', normalized).strip()
|
||||
|
||||
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]:
|
||||
|
||||
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
|
||||
"""
|
||||
@@ -192,80 +195,80 @@ class ImageDeckLoader:
|
||||
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)
|
||||
|
||||
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:
|
||||
|
||||
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()
|
||||
@@ -273,14 +276,14 @@ class ImageDeckLoader:
|
||||
>>> 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)
|
||||
@@ -290,54 +293,53 @@ class ImageDeckLoader:
|
||||
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)),
|
||||
"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:
|
||||
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")
|
||||
|
||||
@@ -6,12 +6,12 @@ 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)
|
||||
"""
|
||||
@@ -24,20 +24,17 @@ if TYPE_CHECKING:
|
||||
from tarot.deck import Deck
|
||||
|
||||
|
||||
def load_card_details(
|
||||
card: 'Card',
|
||||
registry: Optional['CardDetailsRegistry'] = None
|
||||
) -> bool:
|
||||
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()
|
||||
@@ -49,27 +46,26 @@ def load_card_details(
|
||||
"""
|
||||
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
|
||||
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()
|
||||
@@ -78,11 +74,12 @@ def load_deck_details(
|
||||
"""
|
||||
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):
|
||||
@@ -93,29 +90,26 @@ def load_deck_details(
|
||||
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']:
|
||||
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
|
||||
@@ -124,29 +118,29 @@ def get_cards_by_suit(
|
||||
>>> print(len(swords)) # Should be 14
|
||||
14
|
||||
"""
|
||||
if hasattr(deck, 'suit') and callable(deck.suit):
|
||||
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]
|
||||
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']:
|
||||
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()
|
||||
@@ -155,20 +149,22 @@ def filter_cards_by_keywords(
|
||||
"""
|
||||
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)
|
||||
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:
|
||||
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()
|
||||
@@ -178,35 +174,35 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
|
||||
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),
|
||||
"explanation": ("Explanation", False),
|
||||
"interpretation": ("Interpretation", False),
|
||||
"guidance": ("Guidance", False),
|
||||
}
|
||||
|
||||
|
||||
# Add reversed attributes only if requested
|
||||
if include_reversed:
|
||||
attributes['reversed_interpretation'] = ('Reversed Interpretation', False)
|
||||
|
||||
attributes["reversed_interpretation"] = ("Reversed Interpretation", False)
|
||||
|
||||
# List attributes (joined with commas)
|
||||
list_attributes = {
|
||||
'keywords': 'Keywords',
|
||||
'reversed_keywords': ('Reversed Keywords', include_reversed),
|
||||
"keywords": "Keywords",
|
||||
"reversed_keywords": ("Reversed Keywords", include_reversed),
|
||||
}
|
||||
|
||||
|
||||
# Numeric attributes
|
||||
numeric_attributes = {
|
||||
'numerology': 'Numerology',
|
||||
"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:
|
||||
if attr_name == 'explanation' and isinstance(value, dict):
|
||||
if attr_name == "explanation" and isinstance(value, dict):
|
||||
print(f"\n{display_name}:")
|
||||
if "summary" in value:
|
||||
print(f"Summary: {value['summary']}")
|
||||
@@ -218,7 +214,7 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
|
||||
print(f"{k.capitalize()}: {v}")
|
||||
else:
|
||||
print(f"\n{display_name}:\n{value}")
|
||||
|
||||
|
||||
# Print list attributes
|
||||
for attr_name, display_info in list_attributes.items():
|
||||
if isinstance(display_info, tuple):
|
||||
@@ -227,36 +223,35 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
|
||||
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
|
||||
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")
|
||||
@@ -265,6 +260,7 @@ def get_card_info(
|
||||
"""
|
||||
if registry is None:
|
||||
from tarot.card.details import CardDetailsRegistry
|
||||
|
||||
registry = CardDetailsRegistry()
|
||||
|
||||
|
||||
return registry.get(card_name)
|
||||
|
||||
@@ -6,21 +6,21 @@ 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
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.card import Card
|
||||
@@ -29,11 +29,12 @@ if TYPE_CHECKING:
|
||||
@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:
|
||||
@@ -44,192 +45,194 @@ class SpreadPosition:
|
||||
@dataclass
|
||||
class DrawnCard:
|
||||
"""Represents a card drawn for a spread position."""
|
||||
|
||||
position: SpreadPosition
|
||||
card: 'Card'
|
||||
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}"
|
||||
|
||||
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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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'),
|
||||
]
|
||||
"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('_', ' ')
|
||||
|
||||
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:
|
||||
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']
|
||||
|
||||
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"═══════════════════════════════════════════")
|
||||
|
||||
|
||||
lines.append("")
|
||||
lines.append("═══════════════════════════════════════════")
|
||||
|
||||
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,
|
||||
""
|
||||
]
|
||||
|
||||
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:
|
||||
@@ -241,96 +244,94 @@ class Spread:
|
||||
def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
|
||||
"""
|
||||
Draw cards for all positions in a spread.
|
||||
|
||||
|
||||
Ensures all drawn cards are unique (no duplicates in a single 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
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If spread has more positions than cards in the deck
|
||||
"""
|
||||
import random
|
||||
|
||||
|
||||
# Load deck if not provided
|
||||
if deck is None:
|
||||
from tarot.deck import Deck
|
||||
|
||||
deck_instance = Deck()
|
||||
deck = deck_instance.cards
|
||||
|
||||
|
||||
# Validate that we have enough cards to draw from without duplicates
|
||||
num_positions = len(spread.positions)
|
||||
if num_positions > len(deck):
|
||||
raise ValueError(
|
||||
f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards"
|
||||
)
|
||||
|
||||
raise ValueError(f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards")
|
||||
|
||||
# Draw unique cards using random.sample (no replacements)
|
||||
drawn_deck = random.sample(deck, num_positions)
|
||||
|
||||
|
||||
drawn_cards = []
|
||||
for position, card in zip(spread.positions, drawn_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'):
|
||||
if hasattr(card, "number"):
|
||||
lines.append(f" Card #: {card.number}")
|
||||
if hasattr(card, 'arcana'):
|
||||
if hasattr(card, "arcana"):
|
||||
lines.append(f" Arcana: {card.arcana}")
|
||||
if hasattr(card, 'suit') and card.suit:
|
||||
if hasattr(card, "suit") and card.suit:
|
||||
lines.append(f" Suit: {card.suit.name}")
|
||||
|
||||
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
|
||||
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SpreadReading({self.spread.name}, {len(self.drawn_cards)} cards)"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"""
|
||||
Tarot deck module - Core card and deck classes.
|
||||
|
||||
Provides the Deck class for managing Tarot cards and the Card, MajorCard,
|
||||
Provides the Deck class for managing Tarot cards and the Card, MajorCard,
|
||||
MinorCard, and related classes for representing individual cards.
|
||||
"""
|
||||
|
||||
from .deck import (
|
||||
DLT,
|
||||
AceCard,
|
||||
Card,
|
||||
CardQuery,
|
||||
CourtCard,
|
||||
Deck,
|
||||
MajorCard,
|
||||
MinorCard,
|
||||
PipCard,
|
||||
AceCard,
|
||||
CourtCard,
|
||||
CardQuery,
|
||||
TemporalQuery,
|
||||
DLT,
|
||||
Deck,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -5,19 +5,27 @@ 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, Dict
|
||||
import random
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||
|
||||
from ..attributes import (
|
||||
Meaning, CardImage, Suit, Zodiac, Element, Path,
|
||||
Planet, Sephera, Color, PeriodicTable, ElementType, DoublLetterTrump
|
||||
CardImage,
|
||||
Color,
|
||||
Element,
|
||||
ElementType,
|
||||
Meaning,
|
||||
Path,
|
||||
PeriodicTable,
|
||||
Planet,
|
||||
Sephera,
|
||||
Suit,
|
||||
)
|
||||
from ..constants import (
|
||||
COURT_RANKS,
|
||||
MAJOR_ARCANA_NAMES,
|
||||
PIP_INDEX_TO_NUMBER,
|
||||
MINOR_RANK_NAMES,
|
||||
PIP_INDEX_TO_NUMBER,
|
||||
PIP_ORDER,
|
||||
SUITS_FIRST,
|
||||
SUITS_LAST,
|
||||
@@ -35,6 +43,7 @@ def _get_card_data():
|
||||
global _card_data
|
||||
if _card_data is None:
|
||||
from ..card.data import CardDataLoader
|
||||
|
||||
_card_data = CardDataLoader()
|
||||
return _card_data
|
||||
|
||||
@@ -42,16 +51,17 @@ def _get_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: Dict[str, str] = field(default_factory=dict)
|
||||
interpretation: str = ""
|
||||
@@ -59,41 +69,48 @@ class Card:
|
||||
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:
|
||||
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"
|
||||
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)."""
|
||||
@@ -111,24 +128,30 @@ class Card:
|
||||
@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}")
|
||||
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")
|
||||
@@ -138,12 +161,13 @@ class MinorCard(Card):
|
||||
@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}")
|
||||
@@ -153,13 +177,14 @@ class PipCard(MinorCard):
|
||||
@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}")
|
||||
@@ -169,22 +194,22 @@ class AceCard(MinorCard):
|
||||
@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
|
||||
|
||||
hebrew_letter_path: Optional["Path"] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.court_rank not in self.COURT_RANKS:
|
||||
raise ValueError(
|
||||
@@ -194,75 +219,90 @@ class CourtCard(MinorCard):
|
||||
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:
|
||||
|
||||
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)]
|
||||
|
||||
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':
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
||||
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]
|
||||
@@ -271,12 +311,17 @@ class CardQuery:
|
||||
|
||||
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:
|
||||
|
||||
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)
|
||||
@@ -287,71 +332,74 @@ class TemporalQuery:
|
||||
self.month_num = month_num
|
||||
self.day_num = day_num
|
||||
self.hour_num = hour_num
|
||||
|
||||
def month(self, num: int) -> 'TemporalQuery':
|
||||
|
||||
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':
|
||||
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':
|
||||
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)
|
||||
|
||||
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:
|
||||
@@ -366,47 +414,48 @@ class TemporalQuery:
|
||||
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._loader: Optional["CardDataLoader"] = None
|
||||
self._deck: Optional[Deck] = None
|
||||
|
||||
|
||||
@property
|
||||
def loader(self) -> 'CardDataLoader':
|
||||
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':
|
||||
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
|
||||
@@ -414,61 +463,61 @@ class DLT:
|
||||
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)
|
||||
|
||||
Minor suit sequencing (per suit): Ace, 2-10, Prince, Knight, Princess, Queen.
|
||||
|
||||
|
||||
This puts Queen of Wands as card #78, the final card.
|
||||
"""
|
||||
# Minor Arcana - First three suits (Cups, Pentacles, Swords)
|
||||
@@ -479,18 +528,18 @@ class Deck:
|
||||
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)
|
||||
|
||||
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 = {
|
||||
@@ -507,12 +556,16 @@ class Deck:
|
||||
"Fire": fire_element,
|
||||
}
|
||||
|
||||
def _suit_specs(suit_defs: List[Tuple[str, str, int]]) -> List[Tuple[str, ElementType, int]]:
|
||||
def _suit_specs(
|
||||
suit_defs: List[Tuple[str, str, int]],
|
||||
) -> List[Tuple[str, ElementType, int]]:
|
||||
specs: List[Tuple[str, ElementType, int]] = []
|
||||
for suit_name, element_key, suit_num in suit_defs:
|
||||
element_obj = element_lookup.get(element_key)
|
||||
if element_obj is None:
|
||||
raise RuntimeError(f"Failed to resolve element '{element_key}' for suit '{suit_name}'")
|
||||
raise RuntimeError(
|
||||
f"Failed to resolve element '{element_key}' for suit '{suit_name}'"
|
||||
)
|
||||
specs.append((suit_name, element_obj, suit_num))
|
||||
return specs
|
||||
|
||||
@@ -530,7 +583,7 @@ class Deck:
|
||||
card_number,
|
||||
court_rank_mappings,
|
||||
)
|
||||
|
||||
|
||||
# Major Arcana (43-64)
|
||||
# Names match filenames in src/tarot/deck/default/
|
||||
for i, name in enumerate(MAJOR_ARCANA_NAMES):
|
||||
@@ -538,15 +591,14 @@ class Deck:
|
||||
number=card_number,
|
||||
name=name,
|
||||
meaning=Meaning(
|
||||
upright=f"{name} upright meaning",
|
||||
reversed=f"{name} reversed meaning"
|
||||
upright=f"{name} upright meaning", reversed=f"{name} reversed meaning"
|
||||
),
|
||||
arcana="Major",
|
||||
kabbalistic_number=i
|
||||
kabbalistic_number=i,
|
||||
)
|
||||
self.cards.append(card)
|
||||
card_number += 1
|
||||
|
||||
|
||||
# Minor Arcana - Last suit (Wands, 65-78)
|
||||
# Organized logically: Ace, 2-10, then court cards Prince, Knight, Princess, Queen
|
||||
for suit_name, element_obj, suit_num in suits_data_last:
|
||||
@@ -557,16 +609,16 @@ class Deck:
|
||||
card_number,
|
||||
court_rank_mappings,
|
||||
)
|
||||
|
||||
|
||||
# Load detailed explanations and keywords from registry
|
||||
try:
|
||||
from ..card.loader import load_deck_details
|
||||
|
||||
load_deck_details(self)
|
||||
except ImportError:
|
||||
# Handle case where loader might not be available or circular import issues
|
||||
pass
|
||||
|
||||
|
||||
def _add_minor_cards_for_suit(
|
||||
self,
|
||||
suit_name: str,
|
||||
@@ -625,77 +677,77 @@ class Deck:
|
||||
|
||||
return card_number
|
||||
|
||||
|
||||
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")
|
||||
|
||||
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]
|
||||
|
||||
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)"
|
||||
|
||||
@@ -5,115 +5,121 @@ Unified accessor for Tarot-related data and operations.
|
||||
|
||||
Usage:
|
||||
from tarot import Tarot
|
||||
|
||||
|
||||
# Deck and cards
|
||||
card = Tarot.deck.card(3)
|
||||
major5 = Tarot.deck.card.major(5)
|
||||
cups2 = Tarot.deck.card.minor.cups(2)
|
||||
|
||||
|
||||
# Letters (Hebrew with correspondences)
|
||||
letter = Tarot.letters('aleph')
|
||||
simple_letters = Tarot.letters.filter(letter_type="Simple")
|
||||
all_letters = Tarot.letters.display_all()
|
||||
|
||||
|
||||
# Tree of Life
|
||||
sephera = Tarot.tree.sephera(1)
|
||||
path = Tarot.tree.path(11)
|
||||
|
||||
|
||||
# Cube of Space
|
||||
wall = Tarot.cube.wall('North')
|
||||
area = Tarot.cube.area('North', 'center')
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Union, overload, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union, overload
|
||||
|
||||
from .card import CardAccessor
|
||||
from kaballah import Tree, Cube
|
||||
from kaballah import Cube, Tree
|
||||
from letter import letters
|
||||
|
||||
from .card import CardAccessor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from utils.attributes import Planet, God
|
||||
from utils.attributes import God, Planet
|
||||
|
||||
from .attributes import Hexagram
|
||||
from .card.data import CardDataLoader
|
||||
|
||||
|
||||
class DeckAccessor:
|
||||
"""Accessor for deck and card operations."""
|
||||
|
||||
|
||||
# Card accessor (Tarot.deck.card, Tarot.deck.card.major, etc.)
|
||||
card = CardAccessor()
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a nice summary of the deck accessor."""
|
||||
return "Tarot Deck Accessor\n\nAccess methods:\n Tarot.deck.card(3) - Get card by number\n Tarot.deck.card.filter(...) - Filter cards\n Tarot.deck.card.display() - Display all cards\n Tarot.deck.card.spread(...) - Draw a spread"
|
||||
|
||||
return (
|
||||
"Tarot Deck Accessor\n\n"
|
||||
"Access methods:\n"
|
||||
" Tarot.deck.card(3) - Get card by number\n"
|
||||
" Tarot.deck.card.filter(...) - Filter cards\n"
|
||||
" Tarot.deck.card.display() - Display all cards\n"
|
||||
" Tarot.deck.card.spread(...) - Draw a spread"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return a nice representation of the deck accessor."""
|
||||
return self.__str__()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Tarot:
|
||||
"""
|
||||
Unified accessor for Tarot correspondences and data.
|
||||
|
||||
|
||||
Provides access to cards, letters, tree of life, and cube of space.
|
||||
|
||||
|
||||
Temporal and astrological functions are available through the temporal module:
|
||||
from temporal import ThalemaClock, Zodiac
|
||||
|
||||
|
||||
Attributes:
|
||||
deck: CardAccessor for card operations
|
||||
letters: Hebrew letter accessor
|
||||
tree: Tree of Life accessor
|
||||
cube: Cube of Space accessor
|
||||
"""
|
||||
|
||||
|
||||
deck = DeckAccessor()
|
||||
letters = letters()
|
||||
tree = Tree
|
||||
cube = Cube
|
||||
|
||||
_loader: Optional['CardDataLoader'] = None # type: ignore
|
||||
|
||||
_loader: Optional["CardDataLoader"] = None # type: ignore
|
||||
_initialized: bool = False
|
||||
|
||||
|
||||
@classmethod
|
||||
def _ensure_initialized(cls) -> None:
|
||||
"""Lazy-load CardDataLoader on first access."""
|
||||
if cls._initialized:
|
||||
return
|
||||
|
||||
|
||||
from .card.data import CardDataLoader
|
||||
|
||||
cls._loader = CardDataLoader()
|
||||
cls._initialized = True
|
||||
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def planet(cls, name: str) -> Optional['Planet']:
|
||||
...
|
||||
|
||||
def planet(cls, name: str) -> Optional["Planet"]: ...
|
||||
|
||||
@classmethod
|
||||
@overload
|
||||
def planet(cls, name: None = ...) -> Dict[str, 'Planet']:
|
||||
...
|
||||
|
||||
def planet(cls, name: None = ...) -> Dict[str, "Planet"]: ...
|
||||
|
||||
@classmethod
|
||||
def planet(cls, name: Optional[str] = None) -> Union[Optional['Planet'], Dict[str, 'Planet']]:
|
||||
def planet(cls, name: Optional[str] = None) -> Union[Optional["Planet"], Dict[str, "Planet"]]:
|
||||
"""Return a planet entry or all planets."""
|
||||
cls._ensure_initialized()
|
||||
return cls._loader.planet(name) # type: ignore
|
||||
|
||||
|
||||
@classmethod
|
||||
def god(cls, name: Optional[str] = None) -> Union[Optional['God'], Dict[str, 'God']]:
|
||||
def god(cls, name: Optional[str] = None) -> Union[Optional["God"], Dict[str, "God"]]:
|
||||
"""Return a god entry or all gods."""
|
||||
cls._ensure_initialized()
|
||||
return cls._loader.god(name) # type: ignore
|
||||
|
||||
|
||||
@classmethod
|
||||
def hexagram(cls, number: Optional[int] = None) -> Union[Optional['Hexagram'], Dict[int, 'Hexagram']]:
|
||||
def hexagram(
|
||||
cls, number: Optional[int] = None
|
||||
) -> Union[Optional["Hexagram"], Dict[int, "Hexagram"]]:
|
||||
"""Return a hexagram or all hexagrams."""
|
||||
cls._ensure_initialized()
|
||||
return cls._loader.hexagram(number) # type: ignore
|
||||
|
||||
|
||||
|
||||
535
src/tarot/ui.py
535
src/tarot/ui.py
File diff suppressed because it is too large
Load Diff
@@ -8,46 +8,46 @@ Access Patterns:
|
||||
from temporal import Year, Month, Day, Hour, Week
|
||||
from temporal import Calendar, TimeUtil, TemporalCoordinates, Season
|
||||
from temporal import ThalemaClock, Zodiac, PlanetPosition
|
||||
|
||||
|
||||
# Basic usage
|
||||
year = Year(2025)
|
||||
month = Month(11) # November
|
||||
day = Day(18)
|
||||
hour = Hour(14)
|
||||
|
||||
|
||||
# Calendar operations
|
||||
current = Calendar.now()
|
||||
is_leap = Calendar.is_leap_year(2025)
|
||||
|
||||
|
||||
# Temporal coordinates
|
||||
season = TemporalCoordinates.get_season(11, 18)
|
||||
days_until_winter = TemporalCoordinates.days_until_event(11, 18, Season.WINTER)
|
||||
|
||||
|
||||
# Astrological positions
|
||||
clock = ThalemaClock()
|
||||
print(clock) # Shows planetary positions
|
||||
"""
|
||||
|
||||
from .temporal import Year, Month, Day, Hour, Week
|
||||
from .astrology import PlanetPosition, ThalemaClock, Zodiac
|
||||
from .calendar import Calendar
|
||||
from .coordinates import Season, SolarEvent, TemporalCoordinates
|
||||
from .temporal import Day, Hour, Month, Week, Year
|
||||
from .time import TimeUtil
|
||||
from .coordinates import TemporalCoordinates, Season, SolarEvent
|
||||
from .astrology import ThalemaClock, Zodiac, PlanetPosition
|
||||
|
||||
__all__ = [
|
||||
# Temporal classes
|
||||
'Year',
|
||||
'Month',
|
||||
'Day',
|
||||
'Hour',
|
||||
'Week',
|
||||
'Calendar',
|
||||
'TimeUtil',
|
||||
'TemporalCoordinates',
|
||||
'Season',
|
||||
'SolarEvent',
|
||||
"Year",
|
||||
"Month",
|
||||
"Day",
|
||||
"Hour",
|
||||
"Week",
|
||||
"Calendar",
|
||||
"TimeUtil",
|
||||
"TemporalCoordinates",
|
||||
"Season",
|
||||
"SolarEvent",
|
||||
# Astrological classes
|
||||
'ThalemaClock',
|
||||
'Zodiac',
|
||||
'PlanetPosition',
|
||||
"ThalemaClock",
|
||||
"Zodiac",
|
||||
"PlanetPosition",
|
||||
]
|
||||
|
||||
@@ -5,27 +5,28 @@ Calculates current planetary positions with zodiac degrees and symbols.
|
||||
Usage:
|
||||
from clock import ThalemaClock
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
now = datetime.now()
|
||||
clock = ThalemaClock(now)
|
||||
print(clock) # Display formatted with degrees and symbols
|
||||
|
||||
|
||||
# Get individual planet info
|
||||
sun_info = clock.get_planet('Sun')
|
||||
print(sun_info) # "☉ 25°♏︎"
|
||||
|
||||
|
||||
# Custom planet order
|
||||
clock.display_format(['Moon', 'Mercury', 'Mars', 'Venus', 'Sun'])
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, List
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class Zodiac(Enum):
|
||||
"""Zodiac signs with degree ranges (0-360°)."""
|
||||
|
||||
ARIES = ("♈", 0, 30)
|
||||
TAURUS = ("♉", 30, 60)
|
||||
GEMINI = ("♊", 60, 90)
|
||||
@@ -63,6 +64,7 @@ class Zodiac(Enum):
|
||||
@dataclass
|
||||
class PlanetPosition:
|
||||
"""Represents a planet's position with degree and zodiac."""
|
||||
|
||||
planet_name: str
|
||||
planet_symbol: str
|
||||
zodiac: Zodiac
|
||||
@@ -219,7 +221,10 @@ class ThalemaClock:
|
||||
return result
|
||||
|
||||
def display_compact(self) -> str:
|
||||
"""Display in compact format like: ☉︎ 25°Scorpio : ☾︎ 23°Libra : ☿︎ 2°Sagittarius : ♀︎ 13°Scorpio : ♂︎ 9°Sagittarius"""
|
||||
"""Display in compact format.
|
||||
|
||||
Example: ☉︎ 25°Scorpio : ☾︎ 23°Libra : ☿︎ 2°Sagittarius : ♀︎ 13°Scorpio : ♂︎ 9°Sagittarius
|
||||
"""
|
||||
return self.display_format()
|
||||
|
||||
def display_verbose(self) -> str:
|
||||
@@ -228,7 +233,9 @@ class ThalemaClock:
|
||||
for planet_name in ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn"]:
|
||||
if planet_name in self.positions:
|
||||
pos = self.positions[planet_name]
|
||||
lines.append(f"{planet_name:9} {pos.zodiac.name:11} {pos.degree_in_sign:5.1f}° {pos}")
|
||||
lines.append(
|
||||
f"{planet_name:9} {pos.zodiac.name:11} {pos.degree_in_sign:5.1f}° {pos}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@@ -6,12 +6,13 @@ including Zodiac, Time cycles, and Astrological influences.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class Month:
|
||||
"""Represents a calendar month."""
|
||||
|
||||
number: int
|
||||
name: str
|
||||
zodiac_start: str
|
||||
@@ -21,6 +22,7 @@ class Month:
|
||||
@dataclass
|
||||
class Weekday:
|
||||
"""Represents weekday/weekend archetypes with planetary ties."""
|
||||
|
||||
number: int
|
||||
name: str
|
||||
planetary_correspondence: str
|
||||
@@ -35,6 +37,7 @@ class Weekday:
|
||||
@dataclass
|
||||
class Hour:
|
||||
"""Represents an hour with planetary correspondence."""
|
||||
|
||||
number: int
|
||||
name: str
|
||||
planetary_hours: List[str] = field(default_factory=list)
|
||||
@@ -43,6 +46,7 @@ class Hour:
|
||||
@dataclass
|
||||
class ClockHour:
|
||||
"""Represents a clock hour with both 24-hour and 12-hour phases."""
|
||||
|
||||
hour_24: int
|
||||
hour_12: int
|
||||
period: str # AM or PM
|
||||
@@ -63,6 +67,7 @@ class ClockHour:
|
||||
@dataclass
|
||||
class Zodiac:
|
||||
"""Represents a zodiac sign."""
|
||||
|
||||
name: str
|
||||
symbol: str
|
||||
element: str
|
||||
@@ -73,6 +78,7 @@ class Zodiac:
|
||||
@dataclass
|
||||
class Degree:
|
||||
"""Represents an astrological degree."""
|
||||
|
||||
number: int
|
||||
constellation: str
|
||||
ruling_planet: str
|
||||
@@ -82,6 +88,7 @@ class Degree:
|
||||
@dataclass
|
||||
class AstrologicalInfluence:
|
||||
"""Represents astrological influences."""
|
||||
|
||||
planet: str
|
||||
sign: str
|
||||
house: str
|
||||
|
||||
@@ -3,55 +3,56 @@
|
||||
This module provides calendar-related operations and utilities.
|
||||
"""
|
||||
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, Dict, Any
|
||||
from .temporal import Year, Month, Day, Week, Hour
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
from .temporal import Day, Hour, Month, Week, Year
|
||||
|
||||
|
||||
class Calendar:
|
||||
"""Calendar utilities for temporal calculations."""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def now() -> Dict[str, Any]:
|
||||
"""Get current date and time components.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with year, month, day, hour, minute, second
|
||||
"""
|
||||
now = datetime.now()
|
||||
return {
|
||||
'year': Year(now.year),
|
||||
'month': Month(now.month),
|
||||
'day': Day(now.day),
|
||||
'hour': Hour(now.hour, now.minute, now.second),
|
||||
'week': Calendar.get_week(now.year, now.month, now.day),
|
||||
'datetime': now,
|
||||
"year": Year(now.year),
|
||||
"month": Month(now.month),
|
||||
"day": Day(now.day),
|
||||
"hour": Hour(now.hour, now.minute, now.second),
|
||||
"week": Calendar.get_week(now.year, now.month, now.day),
|
||||
"datetime": now,
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_week(year: int, month: int, day: int) -> Week:
|
||||
"""Get the week number for a given date.
|
||||
|
||||
|
||||
Args:
|
||||
year: The year
|
||||
month: The month (1-12)
|
||||
day: The day of month (1-31)
|
||||
|
||||
|
||||
Returns:
|
||||
Week object containing week number and year
|
||||
"""
|
||||
d = date(year, month, day)
|
||||
week_num = d.isocalendar()[1]
|
||||
return Week(week_num, year)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def days_in_month(year: int, month: int) -> int:
|
||||
"""Get the number of days in a month.
|
||||
|
||||
|
||||
Args:
|
||||
year: The year
|
||||
month: The month (1-12)
|
||||
|
||||
|
||||
Returns:
|
||||
Number of days in that month
|
||||
"""
|
||||
@@ -59,14 +60,14 @@ class Calendar:
|
||||
# February - check for leap year
|
||||
return 29 if (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)) else 28
|
||||
return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_leap_year(year: int) -> bool:
|
||||
"""Check if a year is a leap year.
|
||||
|
||||
|
||||
Args:
|
||||
year: The year to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if leap year, False otherwise
|
||||
"""
|
||||
|
||||
@@ -6,37 +6,39 @@ and other astronomical/calendrical coordinates.
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Season(Enum):
|
||||
"""The four seasons of the year."""
|
||||
SPRING = "Spring" # Vernal Equinox (Mar 20/21)
|
||||
SUMMER = "Summer" # Summer Solstice (Jun 20/21)
|
||||
AUTUMN = "Autumn" # Autumnal Equinox (Sep 22/23)
|
||||
WINTER = "Winter" # Winter Solstice (Dec 21/22)
|
||||
|
||||
SPRING = "Spring" # Vernal Equinox (Mar 20/21)
|
||||
SUMMER = "Summer" # Summer Solstice (Jun 20/21)
|
||||
AUTUMN = "Autumn" # Autumnal Equinox (Sep 22/23)
|
||||
WINTER = "Winter" # Winter Solstice (Dec 21/22)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SolarEvent:
|
||||
"""Represents a solar event (solstice or equinox).
|
||||
|
||||
|
||||
Attributes:
|
||||
event_type: The type of solar event (solstice or equinox)
|
||||
date: The approximate date of the event
|
||||
season: The associated season
|
||||
"""
|
||||
|
||||
event_type: str # "solstice" or "equinox"
|
||||
date: tuple # (month, day)
|
||||
date: tuple # (month, day)
|
||||
season: Season
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.season.value} {self.event_type.title()} ({self.date[0]}/{self.date[1]})"
|
||||
|
||||
|
||||
class TemporalCoordinates:
|
||||
"""Temporal positioning and astronomical calculations."""
|
||||
|
||||
|
||||
# Approximate dates for solar events (can vary by ±1-2 days yearly)
|
||||
SOLAR_EVENTS = {
|
||||
Season.SPRING: SolarEvent("equinox", (3, 20), Season.SPRING),
|
||||
@@ -44,15 +46,15 @@ class TemporalCoordinates:
|
||||
Season.AUTUMN: SolarEvent("equinox", (9, 22), Season.AUTUMN),
|
||||
Season.WINTER: SolarEvent("solstice", (12, 21), Season.WINTER),
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_season(month: int, day: int) -> Season:
|
||||
"""Get the season for a given month and day.
|
||||
|
||||
|
||||
Args:
|
||||
month: Month number (1-12)
|
||||
day: Day of month (1-31)
|
||||
|
||||
|
||||
Returns:
|
||||
The Season enum value
|
||||
"""
|
||||
@@ -65,28 +67,28 @@ class TemporalCoordinates:
|
||||
return Season.AUTUMN
|
||||
else: # Winter
|
||||
return Season.WINTER
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_solar_event(season: Season) -> Optional[SolarEvent]:
|
||||
"""Get the solar event for a given season.
|
||||
|
||||
|
||||
Args:
|
||||
season: The Season enum value
|
||||
|
||||
|
||||
Returns:
|
||||
SolarEvent object for that season
|
||||
"""
|
||||
return TemporalCoordinates.SOLAR_EVENTS.get(season)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def days_until_event(month: int, day: int, target_season: Season) -> int:
|
||||
"""Calculate days until a solar event.
|
||||
|
||||
|
||||
Args:
|
||||
month: Current month (1-12)
|
||||
day: Current day (1-31)
|
||||
target_season: Target season for calculation
|
||||
|
||||
|
||||
Returns:
|
||||
Number of days until the target seasonal event
|
||||
"""
|
||||
@@ -94,28 +96,28 @@ class TemporalCoordinates:
|
||||
event = TemporalCoordinates.get_solar_event(target_season)
|
||||
if not event:
|
||||
return -1
|
||||
|
||||
|
||||
event_month, event_day = event.date
|
||||
|
||||
|
||||
# Simple day-of-year calculation
|
||||
current_doy = TemporalCoordinates._day_of_year(month, day)
|
||||
event_doy = TemporalCoordinates._day_of_year(event_month, event_day)
|
||||
|
||||
|
||||
if event_doy >= current_doy:
|
||||
return event_doy - current_doy
|
||||
else:
|
||||
return (365 - current_doy) + event_doy
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _day_of_year(month: int, day: int) -> int:
|
||||
"""Get the day of year (1-365/366).
|
||||
|
||||
|
||||
Args:
|
||||
month: Month (1-12)
|
||||
day: Day (1-31)
|
||||
|
||||
|
||||
Returns:
|
||||
Day of year
|
||||
"""
|
||||
days_in_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||
return sum(days_in_months[:month-1]) + day
|
||||
return sum(days_in_months[: month - 1]) + day
|
||||
|
||||
@@ -4,17 +4,17 @@ This module provides the core temporal domain classes used throughout the system
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
@dataclass
|
||||
class Year:
|
||||
"""Represents a year in the Gregorian calendar."""
|
||||
|
||||
value: int
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Year({self.value})"
|
||||
|
||||
@@ -22,24 +22,35 @@ class Year:
|
||||
@dataclass
|
||||
class Month:
|
||||
"""Represents a month (1-12)."""
|
||||
|
||||
value: int
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 1 <= self.value <= 12:
|
||||
raise ValueError(f"Month must be 1-12, got {self.value}")
|
||||
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the month name."""
|
||||
names = [
|
||||
"January", "February", "March", "April", "May", "June",
|
||||
"July", "August", "September", "October", "November", "December"
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]
|
||||
return names[self.value - 1]
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Month({self.value})"
|
||||
|
||||
@@ -47,16 +58,17 @@ class Month:
|
||||
@dataclass
|
||||
class Week:
|
||||
"""Represents a week in the calendar."""
|
||||
|
||||
number: int # 1-53
|
||||
year: int
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 1 <= self.number <= 53:
|
||||
raise ValueError(f"Week must be 1-53, got {self.number}")
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Week {self.number} of {self.year}"
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Week({self.number}, {self.year})"
|
||||
|
||||
@@ -64,20 +76,21 @@ class Week:
|
||||
@dataclass
|
||||
class Day:
|
||||
"""Represents a day of the month (1-31)."""
|
||||
|
||||
value: int
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 1 <= self.value <= 31:
|
||||
raise ValueError(f"Day must be 1-31, got {self.value}")
|
||||
|
||||
|
||||
@property
|
||||
def day_of_week(self) -> str:
|
||||
"""Get day of week name - requires full date context."""
|
||||
pass
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Day({self.value})"
|
||||
|
||||
@@ -85,10 +98,11 @@ class Day:
|
||||
@dataclass
|
||||
class Hour:
|
||||
"""Represents an hour in 24-hour format (0-23)."""
|
||||
|
||||
value: int
|
||||
minute: int = 0
|
||||
second: int = 0
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 0 <= self.value <= 23:
|
||||
raise ValueError(f"Hour must be 0-23, got {self.value}")
|
||||
@@ -96,9 +110,9 @@ class Hour:
|
||||
raise ValueError(f"Minute must be 0-59, got {self.minute}")
|
||||
if not 0 <= self.second <= 59:
|
||||
raise ValueError(f"Second must be 0-59, got {self.second}")
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.value:02d}:{self.minute:02d}:{self.second:02d}"
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Hour({self.value}, {self.minute}, {self.second})"
|
||||
|
||||
@@ -4,28 +4,28 @@ This module handles time-related operations and conversions.
|
||||
"""
|
||||
|
||||
from datetime import datetime, time
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class TimeUtil:
|
||||
"""Utilities for time operations."""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def current_time() -> time:
|
||||
"""Get current time.
|
||||
|
||||
|
||||
Returns:
|
||||
Current time object
|
||||
"""
|
||||
return datetime.now().time()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def seconds_to_hms(seconds: int) -> Dict[str, int]:
|
||||
"""Convert seconds to hours, minutes, seconds.
|
||||
|
||||
|
||||
Args:
|
||||
seconds: Total seconds
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with 'hours', 'minutes', 'seconds' keys
|
||||
"""
|
||||
@@ -33,34 +33,34 @@ class TimeUtil:
|
||||
remaining = seconds % 3600
|
||||
minutes = remaining // 60
|
||||
secs = remaining % 60
|
||||
|
||||
|
||||
return {
|
||||
'hours': hours,
|
||||
'minutes': minutes,
|
||||
'seconds': secs,
|
||||
"hours": hours,
|
||||
"minutes": minutes,
|
||||
"seconds": secs,
|
||||
}
|
||||
|
||||
|
||||
@staticmethod
|
||||
def hms_to_seconds(hours: int, minutes: int = 0, seconds: int = 0) -> int:
|
||||
"""Convert hours, minutes, seconds to total seconds.
|
||||
|
||||
|
||||
Args:
|
||||
hours: Hours component
|
||||
minutes: Minutes component (default 0)
|
||||
seconds: Seconds component (default 0)
|
||||
|
||||
|
||||
Returns:
|
||||
Total seconds
|
||||
"""
|
||||
return hours * 3600 + minutes * 60 + seconds
|
||||
|
||||
|
||||
@staticmethod
|
||||
def is_24_hour_format(hour: int) -> bool:
|
||||
"""Check if hour is valid in 24-hour format.
|
||||
|
||||
|
||||
Args:
|
||||
hour: The hour value to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if 0-23, False otherwise
|
||||
"""
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
"""Utility modules for the Tarot project."""
|
||||
|
||||
from .attributes import (
|
||||
Cipher,
|
||||
CipherResult,
|
||||
Color,
|
||||
Colorscale,
|
||||
Element,
|
||||
ElementType,
|
||||
God,
|
||||
Note,
|
||||
Number,
|
||||
Perfume,
|
||||
Planet,
|
||||
)
|
||||
from .filter import (
|
||||
universal_filter,
|
||||
get_filterable_fields,
|
||||
describe_filter_fields,
|
||||
filter_by,
|
||||
format_results,
|
||||
get_filter_autocomplete,
|
||||
describe_filter_fields,
|
||||
)
|
||||
from .attributes import (
|
||||
Note,
|
||||
Element,
|
||||
ElementType,
|
||||
Number,
|
||||
Color,
|
||||
Colorscale,
|
||||
Planet,
|
||||
God,
|
||||
Perfume,
|
||||
Cipher,
|
||||
CipherResult,
|
||||
get_filterable_fields,
|
||||
universal_filter,
|
||||
)
|
||||
from .misc import (
|
||||
Personality,
|
||||
MBTIType,
|
||||
Personality,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -7,12 +7,13 @@ exclusively to any single namespace.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
|
||||
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class Meaning:
|
||||
"""Represents the meaning of a card."""
|
||||
|
||||
upright: str
|
||||
reversed: str
|
||||
|
||||
@@ -20,6 +21,7 @@ class Meaning:
|
||||
@dataclass(frozen=True)
|
||||
class Note:
|
||||
"""Represents a musical note with its properties."""
|
||||
|
||||
name: str # e.g., "C", "D", "E", "F#", "G", "A", "B"
|
||||
frequency: float # Frequency in Hz (A4 = 440 Hz)
|
||||
semitone: int # Position in chromatic scale (0-11)
|
||||
@@ -29,7 +31,7 @@ class Note:
|
||||
chakra: Optional[str] = None # Associated chakra if any
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not 0 <= self.semitone <= 11:
|
||||
raise ValueError(f"Semitone must be 0-11, got {self.semitone}")
|
||||
@@ -40,6 +42,7 @@ class Note:
|
||||
@dataclass
|
||||
class Element:
|
||||
"""Represents one of the four elements."""
|
||||
|
||||
name: str
|
||||
symbol: str
|
||||
color: str
|
||||
@@ -50,6 +53,7 @@ class Element:
|
||||
@dataclass
|
||||
class ElementType:
|
||||
"""Represents an elemental force (Fire, Water, Air, Earth, Spirit)."""
|
||||
|
||||
name: str
|
||||
symbol: str
|
||||
direction: str
|
||||
@@ -68,16 +72,17 @@ class ElementType:
|
||||
@dataclass
|
||||
class Number:
|
||||
"""Represents a number (1-9) with Kabbalistic attributes."""
|
||||
|
||||
value: int
|
||||
sephera: str
|
||||
element: str
|
||||
compliment: int
|
||||
color: Optional['Color'] = None
|
||||
|
||||
color: Optional["Color"] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not (1 <= self.value <= 9):
|
||||
raise ValueError(f"Number value must be between 1 and 9, got {self.value}")
|
||||
|
||||
|
||||
# Auto-calculate compliment: numbers complement to sum to 9
|
||||
# 1↔8, 2↔7, 3↔6, 4↔5, 9↔9
|
||||
self.compliment = 9 - self.value if self.value != 9 else 9
|
||||
@@ -87,13 +92,14 @@ class Number:
|
||||
class Colorscale:
|
||||
"""
|
||||
Represents Golden Dawn color scales (King, Queen, Emperor, Empress).
|
||||
|
||||
|
||||
The four scales correspond to the four worlds/letters of Tetragrammaton:
|
||||
- King Scale (Yod): Father, originating impulse, pure archetype
|
||||
- Queen Scale (He): Mother, receptive, earthy counterpart
|
||||
- Emperor Scale (Vau): Son/Form, active expression, concrete manifestation
|
||||
- Empress Scale (He final): Daughter, physical manifestation, receptivity in Assiah
|
||||
"""
|
||||
|
||||
name: str # Sephira/Path name (e.g., "Kether", "Path of Aleph")
|
||||
number: int # 1-10 for Sephiroth, 11-32 for Paths
|
||||
king_scale: str # Yod - Father principle
|
||||
@@ -109,6 +115,7 @@ class Colorscale:
|
||||
@dataclass
|
||||
class Color:
|
||||
"""Represents a color with Kabbalistic correspondences."""
|
||||
|
||||
name: str
|
||||
hex_value: str
|
||||
rgb: Tuple[int, int, int]
|
||||
@@ -119,12 +126,12 @@ class Color:
|
||||
meaning: str
|
||||
tarot_associations: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Validate hex color
|
||||
if not self.hex_value.startswith("#") or len(self.hex_value) != 7:
|
||||
raise ValueError(f"Invalid hex color: {self.hex_value}")
|
||||
|
||||
|
||||
# Validate RGB values
|
||||
if not all(0 <= c <= 255 for c in self.rgb):
|
||||
raise ValueError(f"RGB values must be between 0 and 255, got {self.rgb}")
|
||||
@@ -133,6 +140,7 @@ class Color:
|
||||
@dataclass
|
||||
class Planet:
|
||||
"""Represents a planetary correspondence entry."""
|
||||
|
||||
name: str
|
||||
symbol: str
|
||||
element: str
|
||||
@@ -142,34 +150,35 @@ class Planet:
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
color: Optional[Color] = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return nicely formatted string representation of the Planet."""
|
||||
lines = []
|
||||
lines.append(f"{self.name} ({self.symbol})")
|
||||
lines.append(f" element: {self.element}")
|
||||
|
||||
|
||||
if self.ruling_zodiac:
|
||||
lines.append(f" ruling_zodiac: {', '.join(self.ruling_zodiac)}")
|
||||
|
||||
|
||||
if self.associated_letters:
|
||||
lines.append(f" associated_letters: {', '.join(self.associated_letters)}")
|
||||
|
||||
|
||||
if self.keywords:
|
||||
lines.append(f" keywords: {', '.join(self.keywords)}")
|
||||
|
||||
|
||||
if self.color:
|
||||
lines.append(f" color: {self.color.name}")
|
||||
|
||||
|
||||
if self.description:
|
||||
lines.append(f" description: {self.description}")
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class God:
|
||||
"""Unified deity representation that synchronizes multiple pantheons."""
|
||||
|
||||
name: str
|
||||
culture: str
|
||||
pantheon: str
|
||||
@@ -195,60 +204,65 @@ class God:
|
||||
def primary_number(self) -> Optional[Number]:
|
||||
"""Return the first associated number if one is available."""
|
||||
return self.associated_numbers[0] if self.associated_numbers else None
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return nicely formatted string representation of the God."""
|
||||
lines = []
|
||||
lines.append(f"{self.name}")
|
||||
lines.append(f" culture: {self.culture}")
|
||||
lines.append(f" pantheon: {self.pantheon}")
|
||||
|
||||
|
||||
if self.domains:
|
||||
lines.append(f" domains: {', '.join(self.domains)}")
|
||||
|
||||
|
||||
if self.epithets:
|
||||
lines.append(f" epithets: {', '.join(self.epithets)}")
|
||||
|
||||
|
||||
if self.mythology:
|
||||
lines.append(f" mythology: {self.mythology}")
|
||||
|
||||
|
||||
if self.sephera_numbers:
|
||||
lines.append(f" sephera_numbers: {', '.join(str(n) for n in self.sephera_numbers)}")
|
||||
|
||||
|
||||
if self.path_numbers:
|
||||
lines.append(f" path_numbers: {', '.join(str(n) for n in self.path_numbers)}")
|
||||
|
||||
|
||||
if self.planets:
|
||||
lines.append(f" planets: {', '.join(self.planets)}")
|
||||
|
||||
|
||||
if self.elements:
|
||||
lines.append(f" elements: {', '.join(self.elements)}")
|
||||
|
||||
|
||||
if self.zodiac_signs:
|
||||
lines.append(f" zodiac_signs: {', '.join(self.zodiac_signs)}")
|
||||
|
||||
|
||||
if self.associated_planet:
|
||||
lines.append(f" associated_planet: {self.associated_planet.name}")
|
||||
|
||||
|
||||
if self.associated_element:
|
||||
elem_name = self.associated_element.name if hasattr(self.associated_element, 'name') else str(self.associated_element)
|
||||
elem_name = (
|
||||
self.associated_element.name
|
||||
if hasattr(self.associated_element, "name")
|
||||
else str(self.associated_element)
|
||||
)
|
||||
lines.append(f" associated_element: {elem_name}")
|
||||
|
||||
|
||||
if self.tarot_trumps:
|
||||
lines.append(f" tarot_trumps: {', '.join(self.tarot_trumps)}")
|
||||
|
||||
|
||||
if self.keywords:
|
||||
lines.append(f" keywords: {', '.join(self.keywords)}")
|
||||
|
||||
|
||||
if self.description:
|
||||
lines.append(f" description: {self.description}")
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Perfume:
|
||||
"""Represents a perfume/incense correspondence in Kabbalah."""
|
||||
|
||||
name: str
|
||||
alternative_names: List[str] = field(default_factory=list)
|
||||
scent_profile: str = "" # e.g., "Resinous", "Floral", "Spicy", "Earthy"
|
||||
@@ -263,49 +277,49 @@ class Perfume:
|
||||
magical_uses: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
notes: str = ""
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return nicely formatted string representation of the Perfume."""
|
||||
lines = []
|
||||
lines.append(f"{self.name}")
|
||||
|
||||
|
||||
if self.alternative_names:
|
||||
lines.append(f" alternative_names: {', '.join(self.alternative_names)}")
|
||||
|
||||
|
||||
if self.scent_profile:
|
||||
lines.append(f" scent_profile: {self.scent_profile}")
|
||||
|
||||
|
||||
# Correspondences
|
||||
if self.sephera_number is not None:
|
||||
lines.append(f" sephera_number: {self.sephera_number}")
|
||||
|
||||
|
||||
if self.path_number is not None:
|
||||
lines.append(f" path_number: {self.path_number}")
|
||||
|
||||
|
||||
if self.element:
|
||||
lines.append(f" element: {self.element}")
|
||||
|
||||
|
||||
if self.planet:
|
||||
lines.append(f" planet: {self.planet}")
|
||||
|
||||
|
||||
if self.zodiac_sign:
|
||||
lines.append(f" zodiac_sign: {self.zodiac_sign}")
|
||||
|
||||
|
||||
if self.astrological_quality:
|
||||
lines.append(f" astrological_quality: {self.astrological_quality}")
|
||||
|
||||
|
||||
if self.keywords:
|
||||
lines.append(f" keywords: {', '.join(self.keywords)}")
|
||||
|
||||
|
||||
if self.magical_uses:
|
||||
lines.append(f" magical_uses: {', '.join(self.magical_uses)}")
|
||||
|
||||
|
||||
if self.description:
|
||||
lines.append(f" description: {self.description}")
|
||||
|
||||
|
||||
if self.notes:
|
||||
lines.append(f" notes: {self.notes}")
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -362,9 +376,7 @@ class Cipher:
|
||||
expanded.append(self.pattern[idx % len(self.pattern)])
|
||||
idx += 1
|
||||
return expanded
|
||||
raise ValueError(
|
||||
"Cipher pattern length does not match alphabet and cycling is disabled"
|
||||
)
|
||||
raise ValueError("Cipher pattern length does not match alphabet and cycling is disabled")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -10,51 +10,57 @@ Provides a single, reusable filter mechanism that works across all modules:
|
||||
|
||||
Usage:
|
||||
from utils.filter import universal_filter, get_filterable_fields
|
||||
|
||||
|
||||
# For TarotLetter
|
||||
results = universal_filter(Tarot.letters.all(), letter_type="Mother")
|
||||
|
||||
|
||||
# For Card
|
||||
results = universal_filter(Tarot.deck.cards, arcana="Major")
|
||||
|
||||
|
||||
# Get available fields for introspection
|
||||
fields = get_filterable_fields(TarotLetter)
|
||||
"""
|
||||
|
||||
from typing import List, Any, TypeVar, Union, Dict
|
||||
from dataclasses import is_dataclass, fields
|
||||
from utils.object_formatting import get_item_label, is_nested_object, get_object_attributes, format_value
|
||||
from dataclasses import fields, is_dataclass
|
||||
from typing import Any, Dict, List, TypeVar
|
||||
|
||||
T = TypeVar('T') # Generic type for any dataclass
|
||||
from utils.object_formatting import (
|
||||
format_value,
|
||||
get_item_label,
|
||||
get_object_attributes,
|
||||
is_nested_object,
|
||||
)
|
||||
|
||||
T = TypeVar("T") # Generic type for any dataclass
|
||||
|
||||
|
||||
def get_filterable_fields(dataclass_type) -> List[str]:
|
||||
"""
|
||||
Dynamically get all filterable fields from a dataclass.
|
||||
|
||||
|
||||
Args:
|
||||
dataclass_type: A dataclass type (e.g., TarotLetter, Card)
|
||||
|
||||
|
||||
Returns:
|
||||
List of field names available for filtering
|
||||
|
||||
|
||||
Raises:
|
||||
TypeError: If the type is not a dataclass
|
||||
|
||||
|
||||
Example:
|
||||
fields = get_filterable_fields(TarotLetter)
|
||||
# ['hebrew_letter', 'transliteration', 'letter_type', ...]
|
||||
"""
|
||||
if not is_dataclass(dataclass_type):
|
||||
raise TypeError(f"{dataclass_type} is not a dataclass")
|
||||
|
||||
|
||||
return [f.name for f in fields(dataclass_type)]
|
||||
|
||||
|
||||
def _matches_filter(obj: Any, key: str, value: Any) -> bool:
|
||||
"""
|
||||
Check if an object matches a filter criterion.
|
||||
|
||||
|
||||
Handles:
|
||||
- String matching (case-insensitive)
|
||||
- Numeric matching (exact)
|
||||
@@ -63,25 +69,25 @@ def _matches_filter(obj: Any, key: str, value: Any) -> bool:
|
||||
- None/null matching
|
||||
- Nested object attribute matching (e.g., suit="Cups" matches Suit(name="Cups"))
|
||||
- Multiple values (comma-separated strings or lists for OR logic)
|
||||
|
||||
|
||||
Args:
|
||||
obj: The object to check
|
||||
key: The attribute name
|
||||
value: The value to match against (string, int, list, or comma-separated string)
|
||||
|
||||
|
||||
Returns:
|
||||
True if the object matches the filter, False otherwise
|
||||
|
||||
|
||||
Examples:
|
||||
_matches_filter(card, "number", 3) # Single number
|
||||
_matches_filter(card, "number", [3, 5, 6]) # Multiple numbers (OR)
|
||||
_matches_filter(card, "number", "3,5,6") # Comma-separated (OR)
|
||||
"""
|
||||
attr_value = getattr(obj, key, None)
|
||||
|
||||
|
||||
if attr_value is None:
|
||||
return False
|
||||
|
||||
|
||||
# Parse multiple values (comma-separated string or list)
|
||||
values_to_check = []
|
||||
if isinstance(value, str) and "," in value:
|
||||
@@ -91,60 +97,57 @@ def _matches_filter(obj: Any, key: str, value: Any) -> bool:
|
||||
values_to_check = list(value)
|
||||
else:
|
||||
values_to_check = [value]
|
||||
|
||||
|
||||
# Check if attribute matches ANY of the provided values (OR logic)
|
||||
for check_value in values_to_check:
|
||||
# Handle list attributes (like keywords, colors, etc.)
|
||||
if isinstance(attr_value, list):
|
||||
if any(
|
||||
str(check_value).lower() == str(item).lower()
|
||||
for item in attr_value
|
||||
):
|
||||
if any(str(check_value).lower() == str(item).lower() for item in attr_value):
|
||||
return True
|
||||
continue
|
||||
|
||||
|
||||
# Handle numeric comparisons
|
||||
if isinstance(check_value, int) and isinstance(attr_value, int):
|
||||
if attr_value == check_value:
|
||||
return True
|
||||
continue
|
||||
|
||||
|
||||
# Handle boolean comparisons
|
||||
if isinstance(check_value, bool) and isinstance(attr_value, bool):
|
||||
if attr_value == check_value:
|
||||
return True
|
||||
continue
|
||||
|
||||
|
||||
# Handle nested object comparison: if attr_value has a 'name' attribute,
|
||||
# try matching the value against that (e.g., suit="Cups" vs Suit(name="Cups"))
|
||||
if hasattr(attr_value, 'name'):
|
||||
nested_name = getattr(attr_value, 'name', None)
|
||||
if hasattr(attr_value, "name"):
|
||||
nested_name = getattr(attr_value, "name", None)
|
||||
if nested_name is not None:
|
||||
if str(nested_name).lower() == str(check_value).lower():
|
||||
return True
|
||||
continue
|
||||
|
||||
|
||||
# Handle string comparisons
|
||||
if str(attr_value).lower() == str(check_value).lower():
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def universal_filter(items: List[T], **kwargs) -> List[T]:
|
||||
"""
|
||||
Universal filter function that works on any list of dataclass objects.
|
||||
|
||||
|
||||
Dynamically filters a list of objects by any combination of their attributes.
|
||||
Works with any dataclass-based type throughout the project.
|
||||
|
||||
|
||||
Args:
|
||||
items: List of objects to filter (typically all objects from a collection)
|
||||
**kwargs: Attribute filters (field_name=value or field_name=[value1, value2])
|
||||
|
||||
|
||||
Returns:
|
||||
Filtered list containing only items matching ALL criteria
|
||||
|
||||
|
||||
Features:
|
||||
- Case-insensitive string matching
|
||||
- Numeric exact matching
|
||||
@@ -153,20 +156,20 @@ def universal_filter(items: List[T], **kwargs) -> List[T]:
|
||||
- Multiple values per field (comma-separated string or list) for OR matching
|
||||
- Works with any dataclass object
|
||||
- Field aliases (e.g., 'type' -> 'arcana' for Cards)
|
||||
|
||||
|
||||
Examples:
|
||||
# Single values
|
||||
results = universal_filter(Tarot.deck.cards, arcana="Major")
|
||||
|
||||
|
||||
# Multiple values (OR logic) - comma-separated
|
||||
results = universal_filter(Tarot.deck.cards, number="3,5,6")
|
||||
|
||||
|
||||
# Multiple values (OR logic) - list
|
||||
results = universal_filter(Tarot.deck.cards, number=[3, 5, 6])
|
||||
|
||||
|
||||
# Combine multiple fields (AND logic)
|
||||
results = universal_filter(Tarot.deck.cards, suit="Cups", type="Court")
|
||||
|
||||
|
||||
# Multiple values in one field + other filters
|
||||
results = universal_filter(Tarot.deck.cards, number="3,5,6", suit="Wands")
|
||||
"""
|
||||
@@ -174,50 +177,47 @@ def universal_filter(items: List[T], **kwargs) -> List[T]:
|
||||
field_aliases = {
|
||||
# No aliases - use direct property/field names
|
||||
}
|
||||
|
||||
|
||||
results = items
|
||||
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
|
||||
# Apply field alias if it exists
|
||||
actual_key = field_aliases.get(key, key)
|
||||
|
||||
results = [
|
||||
obj for obj in results
|
||||
if _matches_filter(obj, actual_key, value)
|
||||
]
|
||||
|
||||
|
||||
results = [obj for obj in results if _matches_filter(obj, actual_key, value)]
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def format_results(items: List[Any]) -> str:
|
||||
"""
|
||||
Format a list of objects for user-friendly display.
|
||||
|
||||
|
||||
Works with any object type and recursively formats nested structures.
|
||||
Each object is separated by a blank line.
|
||||
|
||||
|
||||
Args:
|
||||
items: List of objects to format
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted string with proper indentation and hierarchy
|
||||
|
||||
|
||||
Example:
|
||||
results = universal_filter(Tarot.letters.all(), element="Fire")
|
||||
print(format_results(results))
|
||||
"""
|
||||
if not items:
|
||||
return "(no items)"
|
||||
|
||||
|
||||
lines = []
|
||||
for item in items:
|
||||
# Get label for item (handles name, transliteration, or str())
|
||||
label = get_item_label(item, fallback=str(item))
|
||||
lines.append(f"--- {label} ---")
|
||||
|
||||
|
||||
# Format all attributes with proper nesting
|
||||
for attr_name, attr_value in get_object_attributes(item):
|
||||
if is_nested_object(attr_value):
|
||||
@@ -227,9 +227,9 @@ def format_results(items: List[Any]) -> str:
|
||||
lines.append(nested)
|
||||
else:
|
||||
lines.append(f" {attr_name}: {attr_value}")
|
||||
|
||||
|
||||
lines.append("") # Blank line between items
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -240,16 +240,16 @@ filter_by = universal_filter
|
||||
def get_filter_autocomplete(dataclass_type) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get autocomplete suggestions for filtering a dataclass type.
|
||||
|
||||
|
||||
Returns a dictionary mapping field names to example values found in the dataclass.
|
||||
Useful for IDE autocomplete and CLI help.
|
||||
|
||||
|
||||
Args:
|
||||
dataclass_type: A dataclass type (e.g., TarotLetter, Card, Wall)
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with field names as keys and list of example values
|
||||
|
||||
|
||||
Example:
|
||||
autocomplete = get_filter_autocomplete(TarotLetter)
|
||||
# Returns:
|
||||
@@ -262,41 +262,41 @@ def get_filter_autocomplete(dataclass_type) -> Dict[str, List[str]]:
|
||||
"""
|
||||
if not is_dataclass(dataclass_type):
|
||||
raise TypeError(f"{dataclass_type} is not a dataclass")
|
||||
|
||||
|
||||
autocomplete = {}
|
||||
field_names = [f.name for f in fields(dataclass_type)]
|
||||
|
||||
|
||||
for field_name in field_names:
|
||||
autocomplete[field_name] = f"<value for {field_name}>"
|
||||
|
||||
|
||||
return autocomplete
|
||||
|
||||
|
||||
def describe_filter_fields(dataclass_type) -> str:
|
||||
"""
|
||||
Get a human-readable description of all filterable fields.
|
||||
|
||||
|
||||
Useful for help text and documentation.
|
||||
|
||||
|
||||
Args:
|
||||
dataclass_type: A dataclass type
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted string with field descriptions
|
||||
|
||||
|
||||
Example:
|
||||
print(describe_filter_fields(TarotLetter))
|
||||
"""
|
||||
if not is_dataclass(dataclass_type):
|
||||
raise TypeError(f"{dataclass_type} is not a dataclass")
|
||||
|
||||
|
||||
field_list = get_filterable_fields(dataclass_type)
|
||||
lines = [
|
||||
f"Filterable fields for {dataclass_type}:",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
for field_name in field_list:
|
||||
lines.append(f" • {field_name}")
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -5,8 +5,8 @@ This module contains specialized utilities that don't fit into other categories.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tarot.deck.deck import CourtCard
|
||||
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class MBTIType(Enum):
|
||||
"""16 MBTI personality types."""
|
||||
|
||||
ISTJ = "ISTJ"
|
||||
ISFJ = "ISFJ"
|
||||
INFJ = "INFJ"
|
||||
@@ -36,79 +37,76 @@ class MBTIType(Enum):
|
||||
class Personality:
|
||||
"""
|
||||
MBTI Personality Type mapped to a specific Tarot Court Card.
|
||||
|
||||
|
||||
This class creates a direct 1-to-1 relationship between each of the 16 MBTI
|
||||
personality types and their corresponding Tarot court cards. Based on the
|
||||
comprehensive system developed by Dante DiMatteo at 78 Revelations Per Minute:
|
||||
https://78revelationsaminute.wordpress.com/2015/07/08/personality-types-the-tarot-court-cards-and-the-myers-briggs-type-indicator/
|
||||
|
||||
|
||||
The mapping is based on:
|
||||
- SUITS correspond to Jung's 4 cognitive functions:
|
||||
* Wands: Intuition (N)
|
||||
* Cups: Feeling (F)
|
||||
* Swords: Thinking (T)
|
||||
* Pentacles: Sensation (S)
|
||||
|
||||
|
||||
- RANKS correspond to MBTI traits:
|
||||
* Kings (E + J): Extraverted Judgers
|
||||
* Queens (I + J): Introverted Judgers
|
||||
* Princes (E + P): Extraverted Perceivers
|
||||
* Princesses (I + P): Introverted Perceivers
|
||||
|
||||
|
||||
Attributes:
|
||||
mbti_type: The MBTI personality type (e.g., ENFP)
|
||||
court_card: The single CourtCard object representing this personality
|
||||
description: Brief description of the personality archetype
|
||||
"""
|
||||
|
||||
|
||||
mbti_type: MBTIType
|
||||
court_card: Optional['CourtCard'] = None
|
||||
court_card: Optional["CourtCard"] = None
|
||||
description: str = ""
|
||||
|
||||
|
||||
# Direct MBTI-to-CourtCard mapping (1-to-1 relationship)
|
||||
# Format: MBTI_TYPE -> (Rank, Suit)
|
||||
_MBTI_TO_CARD_MAPPING = {
|
||||
# KINGS (E + J) - Extraverted Judgers
|
||||
"ENTJ": ("Knight", "Wands"), # Fiery, forceful leadership
|
||||
"ENFJ": ("Knight", "Cups"), # Sensitive, mission-driven
|
||||
"ESTJ": ("Knight", "Swords"), # Practical, pragmatic
|
||||
"ENTJ": ("Knight", "Wands"), # Fiery, forceful leadership
|
||||
"ENFJ": ("Knight", "Cups"), # Sensitive, mission-driven
|
||||
"ESTJ": ("Knight", "Swords"), # Practical, pragmatic
|
||||
"ESFJ": ("Knight", "Pentacles"), # Sociable, consensus-seeking
|
||||
|
||||
# QUEENS (I + J) - Introverted Judgers
|
||||
"INTJ": ("Queen", "Wands"), # Analytical, self-motivated
|
||||
"INFJ": ("Queen", "Cups"), # Sensitive, interconnected
|
||||
"ISTJ": ("Queen", "Swords"), # Pragmatic, duty-fulfiller
|
||||
"ISFJ": ("Queen", "Pentacles"), # Caring, earth-mother type
|
||||
|
||||
"INTJ": ("Queen", "Wands"), # Analytical, self-motivated
|
||||
"INFJ": ("Queen", "Cups"), # Sensitive, interconnected
|
||||
"ISTJ": ("Queen", "Swords"), # Pragmatic, duty-fulfiller
|
||||
"ISFJ": ("Queen", "Pentacles"), # Caring, earth-mother type
|
||||
# PRINCES (E + P) - Extraverted Perceivers
|
||||
"ENTP": ("Prince", "Wands"), # Visionary, quick-study
|
||||
"ENFP": ("Prince", "Cups"), # Inspiring, intuitive
|
||||
"ESTP": ("Prince", "Swords"), # Action-oriented, risk-taker
|
||||
"ENTP": ("Prince", "Wands"), # Visionary, quick-study
|
||||
"ENFP": ("Prince", "Cups"), # Inspiring, intuitive
|
||||
"ESTP": ("Prince", "Swords"), # Action-oriented, risk-taker
|
||||
"ESFP": ("Prince", "Pentacles"), # Aesthete, sensualist
|
||||
|
||||
# PRINCESSES (I + P) - Introverted Perceivers
|
||||
"INTP": ("Princess", "Wands"), # Thinker par excellence
|
||||
"INFP": ("Princess", "Cups"), # Idealistic, devoted
|
||||
"ISTP": ("Princess", "Swords"), # Observer, mechanic
|
||||
"ISFP": ("Princess", "Pentacles"), # Aesthete, free spirit
|
||||
"INTP": ("Princess", "Wands"), # Thinker par excellence
|
||||
"INFP": ("Princess", "Cups"), # Idealistic, devoted
|
||||
"ISTP": ("Princess", "Swords"), # Observer, mechanic
|
||||
"ISFP": ("Princess", "Pentacles"), # Aesthete, free spirit
|
||||
}
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_mbti(cls, mbti_type: str, deck: Optional[object] = None) -> 'Personality':
|
||||
def from_mbti(cls, mbti_type: str, deck: Optional[object] = None) -> "Personality":
|
||||
"""
|
||||
Create a Personality from an MBTI type string.
|
||||
|
||||
|
||||
Args:
|
||||
mbti_type: MBTI type as string (e.g., "ENFP", "ISTJ")
|
||||
deck: Optional Tarot Deck to fetch the court card from. If not provided,
|
||||
court card will be fetched dynamically when accessed.
|
||||
|
||||
|
||||
Returns:
|
||||
Personality object with associated court card
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If mbti_type is not a valid MBTI type
|
||||
|
||||
|
||||
Example:
|
||||
>>> from tarot import Tarot
|
||||
>>> personality = Personality.from_mbti("ENFP", Tarot.deck)
|
||||
@@ -118,7 +116,7 @@ class Personality:
|
||||
Prince of Cups
|
||||
"""
|
||||
mbti_type = mbti_type.upper()
|
||||
|
||||
|
||||
# Validate MBTI type
|
||||
try:
|
||||
mbti_enum = MBTIType[mbti_type]
|
||||
@@ -127,63 +125,58 @@ class Personality:
|
||||
f"Invalid MBTI type: {mbti_type}. Must be one of: "
|
||||
f"{', '.join([t.value for t in MBTIType])}"
|
||||
)
|
||||
|
||||
|
||||
# Get the rank and suit for this MBTI type
|
||||
rank, suit = cls._MBTI_TO_CARD_MAPPING.get(mbti_type, (None, None))
|
||||
if not rank or not suit:
|
||||
raise ValueError(f"No court card mapping found for MBTI type {mbti_type}")
|
||||
|
||||
|
||||
# Get court card from deck if provided
|
||||
court_card = None
|
||||
if deck is not None:
|
||||
# Import here to avoid circular imports
|
||||
from tarot import Tarot
|
||||
|
||||
|
||||
# Use provided deck or default to Tarot
|
||||
d = deck if hasattr(deck, 'card') else Tarot.deck
|
||||
|
||||
d = deck if hasattr(deck, "card") else Tarot.deck
|
||||
|
||||
cards = d.card.filter(type="Court", court_rank=rank, suit=suit)
|
||||
if cards:
|
||||
court_card = cards[0]
|
||||
|
||||
|
||||
# Get description
|
||||
descriptions = {
|
||||
"ENTJ": "The Commander - Strategic, ambitious, leader of Wands",
|
||||
"ENFJ": "The Protagonist - Inspiring, empathetic, leader of Cups",
|
||||
"ESTJ": "The Supervisor - Practical, decisive, leader of Swords",
|
||||
"ESFJ": "The Consul - Sociable, cooperative, leader of Pentacles",
|
||||
|
||||
"INTJ": "The Architect - Strategic, logical, sage of Wands",
|
||||
"INFJ": "The Advocate - Insightful, idealistic, sage of Cups",
|
||||
"ISTJ": "The Logistician - Practical, reliable, sage of Swords",
|
||||
"ISFJ": "The Defender - Caring, conscientious, sage of Pentacles",
|
||||
|
||||
"ENTP": "The Debater - Innovative, quick-witted, explorer of Wands",
|
||||
"ENFP": "The Campaigner - Enthusiastic, social, explorer of Cups",
|
||||
"ESTP": "The Entrepreneur - Energetic, bold, explorer of Swords",
|
||||
"ESFP": "The Entertainer - Spontaneous, outgoing, explorer of Pentacles",
|
||||
|
||||
"INTP": "The Logician - Analytical, curious, seeker of Wands",
|
||||
"INFP": "The Mediator - Idealistic, authentic, seeker of Cups",
|
||||
"ISTP": "The Virtuoso - Practical, observant, seeker of Swords",
|
||||
"ISFP": "The Adventurer - Sensitive, spontaneous, seeker of Pentacles",
|
||||
}
|
||||
|
||||
|
||||
return cls(
|
||||
mbti_type=mbti_enum,
|
||||
court_card=court_card,
|
||||
description=descriptions.get(mbti_type, "")
|
||||
mbti_type=mbti_enum, court_card=court_card, description=descriptions.get(mbti_type, "")
|
||||
)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return string representation of personality with court card."""
|
||||
if self.court_card:
|
||||
card_str = f"{self.court_card.court_rank} of {self.court_card.suit.name}"
|
||||
else:
|
||||
card_str = "No court card loaded"
|
||||
|
||||
|
||||
return f"{self.mbti_type.value} - {self.description}\n Court Card: {card_str}"
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return detailed representation."""
|
||||
card_name = (
|
||||
|
||||
@@ -16,27 +16,26 @@ Usage:
|
||||
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
|
||||
# Type checking predicates
|
||||
SCALAR_TYPES = (str, int, float, bool, list, dict, type(None))
|
||||
|
||||
|
||||
def is_dataclass(obj: Any) -> bool:
|
||||
"""Check if object is a dataclass."""
|
||||
return hasattr(obj, '__dataclass_fields__')
|
||||
return hasattr(obj, "__dataclass_fields__")
|
||||
|
||||
|
||||
def is_nested_object(obj: Any) -> bool:
|
||||
"""
|
||||
Check if object is a nested/complex object (not a scalar type).
|
||||
|
||||
|
||||
Returns True for dataclasses, dicts, and objects with __dict__ that aren't scalars.
|
||||
"""
|
||||
if isinstance(obj, dict):
|
||||
return True
|
||||
if is_dataclass(obj):
|
||||
return True
|
||||
return hasattr(obj, '__dict__') and not isinstance(obj, SCALAR_TYPES)
|
||||
return hasattr(obj, "__dict__") and not isinstance(obj, SCALAR_TYPES)
|
||||
|
||||
|
||||
def is_scalar(obj: Any) -> bool:
|
||||
@@ -47,29 +46,29 @@ def is_scalar(obj: Any) -> bool:
|
||||
def get_item_label(item: Any, fallback: str = "item") -> str:
|
||||
"""
|
||||
Extract a display label for an item using priority order.
|
||||
|
||||
|
||||
Priority:
|
||||
1. item.name (most common in Tarot data)
|
||||
2. item.transliteration (used in letters/numbers)
|
||||
3. str(item) (fallback)
|
||||
|
||||
|
||||
Args:
|
||||
item: The object to get a label for
|
||||
fallback: Value to use if no attributes found (rarely used)
|
||||
|
||||
|
||||
Returns:
|
||||
A string suitable for display as an item label
|
||||
"""
|
||||
if hasattr(item, 'name'):
|
||||
return str(getattr(item, 'name', fallback))
|
||||
elif hasattr(item, 'transliteration'):
|
||||
return str(getattr(item, 'transliteration', fallback))
|
||||
if hasattr(item, "name"):
|
||||
return str(getattr(item, "name", fallback))
|
||||
elif hasattr(item, "transliteration"):
|
||||
return str(getattr(item, "transliteration", fallback))
|
||||
return str(item) if item is not None else fallback
|
||||
|
||||
|
||||
def get_dataclass_fields(obj: Any) -> List[str]:
|
||||
"""Get list of field names from a dataclass."""
|
||||
if hasattr(obj, '__dataclass_fields__'):
|
||||
if hasattr(obj, "__dataclass_fields__"):
|
||||
return list(obj.__dataclass_fields__.keys())
|
||||
return []
|
||||
|
||||
@@ -77,64 +76,69 @@ def get_dataclass_fields(obj: Any) -> List[str]:
|
||||
def get_object_attributes(obj: Any) -> List[Tuple[str, Any]]:
|
||||
"""
|
||||
Extract all public attributes from an object.
|
||||
|
||||
|
||||
Returns list of (name, value) tuples, skipping private attributes (starting with '_').
|
||||
Works with dataclasses, dicts, and regular objects with __dict__.
|
||||
"""
|
||||
attributes = []
|
||||
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return list(obj.items())
|
||||
|
||||
|
||||
if is_dataclass(obj):
|
||||
for field_name in obj.__dataclass_fields__:
|
||||
value = getattr(obj, field_name, None)
|
||||
attributes.append((field_name, value))
|
||||
elif hasattr(obj, '__dict__'):
|
||||
elif hasattr(obj, "__dict__"):
|
||||
for field_name, value in obj.__dict__.items():
|
||||
if not field_name.startswith('_'):
|
||||
if not field_name.startswith("_"):
|
||||
attributes.append((field_name, value))
|
||||
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def format_value(value: Any, indent: int = 2) -> str:
|
||||
"""
|
||||
Format a value for display, recursively handling nested objects.
|
||||
|
||||
|
||||
Handles:
|
||||
- Nested dataclasses and objects with custom __str__ methods
|
||||
- Lists and dicts
|
||||
- Scalar values
|
||||
- Proper indentation for nested structures
|
||||
|
||||
|
||||
Args:
|
||||
value: The value to format
|
||||
indent: Number of spaces for indentation (increases for nested objects)
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted string representation of the value
|
||||
"""
|
||||
indent_str = " " * indent
|
||||
|
||||
|
||||
# Check if object has a custom __str__ method (not the default object repr)
|
||||
if is_nested_object(value):
|
||||
# Classes that have custom __str__ implementations should use them
|
||||
obj_class = type(value).__name__
|
||||
has_custom_str = (
|
||||
hasattr(value, '__str__') and
|
||||
type(value).__str__ is not object.__str__
|
||||
)
|
||||
|
||||
if has_custom_str and obj_class in ['Path', 'Planet', 'Perfume', 'God', 'Colorscale', 'Sephera', 'ElementType']:
|
||||
has_custom_str = hasattr(value, "__str__") and type(value).__str__ is not object.__str__
|
||||
|
||||
if has_custom_str and obj_class in [
|
||||
"Path",
|
||||
"Planet",
|
||||
"Perfume",
|
||||
"God",
|
||||
"Colorscale",
|
||||
"Sephera",
|
||||
"ElementType",
|
||||
]:
|
||||
# Use the custom __str__ method and indent each line
|
||||
custom_output = str(value)
|
||||
lines = []
|
||||
for line in custom_output.split('\n'):
|
||||
for line in custom_output.split("\n"):
|
||||
if line.strip(): # Skip empty lines
|
||||
lines.append(f"{indent_str}{line}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Default behavior: iterate through attributes
|
||||
lines = []
|
||||
for attr_name, attr_value in get_object_attributes(value):
|
||||
@@ -146,7 +150,7 @@ def format_value(value: Any, indent: int = 2) -> str:
|
||||
else:
|
||||
lines.append(f"{indent_str}{attr_name}: {attr_value}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Scalar values
|
||||
return str(value)
|
||||
|
||||
@@ -154,20 +158,20 @@ def format_value(value: Any, indent: int = 2) -> str:
|
||||
def format_object_attributes(obj: Any, indent: int = 2) -> List[str]:
|
||||
"""
|
||||
Format all attributes of an object as a list of formatted lines.
|
||||
|
||||
|
||||
Handles nested objects with proper indentation and section headers.
|
||||
Used by display methods to format individual items consistently.
|
||||
|
||||
|
||||
Args:
|
||||
obj: The object to format
|
||||
indent: Base indentation level in spaces
|
||||
|
||||
|
||||
Returns:
|
||||
List of formatted lines ready to join with newlines
|
||||
"""
|
||||
lines = []
|
||||
indent_str = " " * indent
|
||||
|
||||
|
||||
for attr_name, attr_value in get_object_attributes(obj):
|
||||
if is_nested_object(attr_value):
|
||||
# Nested object - add section header and format recursively
|
||||
@@ -178,5 +182,5 @@ def format_object_attributes(obj: Any, indent: int = 2) -> List[str]:
|
||||
else:
|
||||
# Scalar value - just print
|
||||
lines.append(f"{indent_str}{attr_name}: {attr_value}")
|
||||
|
||||
|
||||
return lines
|
||||
|
||||
@@ -8,42 +8,43 @@ Usage:
|
||||
# By name
|
||||
result = letter.iching().name('peace')
|
||||
result = letter.alphabet().name('english')
|
||||
|
||||
|
||||
# By filter expressions
|
||||
result = letter.iching().filter('number:1')
|
||||
result = letter.alphabet().filter('name:hebrew')
|
||||
result = number.number().filter('value:5')
|
||||
|
||||
|
||||
# Get all results
|
||||
results = letter.iching().all() # Dict[int, Hexagram]
|
||||
results = letter.iching().list() # List[Hexagram]
|
||||
"""
|
||||
|
||||
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
|
||||
|
||||
from utils.object_formatting import format_value, get_object_attributes, is_nested_object
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class QueryResult:
|
||||
"""Single result from a query."""
|
||||
|
||||
|
||||
def __init__(self, data: Any) -> None:
|
||||
self.data = data
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if hasattr(self.data, '__repr__'):
|
||||
if hasattr(self.data, "__repr__"):
|
||||
return repr(self.data)
|
||||
return f"{self.__class__.__name__}({self.data})"
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
if hasattr(self.data, '__str__'):
|
||||
if hasattr(self.data, "__str__"):
|
||||
return str(self.data)
|
||||
return repr(self)
|
||||
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
"""Pass through attribute access to the wrapped data."""
|
||||
if name.startswith('_'):
|
||||
if name.startswith("_"):
|
||||
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||
return getattr(self.data, name)
|
||||
|
||||
@@ -51,40 +52,40 @@ class QueryResult:
|
||||
class Query:
|
||||
"""
|
||||
Fluent query builder for accessing and filtering tarot data.
|
||||
|
||||
|
||||
Supports chaining: .filter() → .name() → .get()
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, data: Union[Dict[Any, T], List[T]]) -> None:
|
||||
"""Initialize with data source (dict or list)."""
|
||||
self._original_data = data
|
||||
self._data = data if isinstance(data, list) else list(data.values())
|
||||
self._filters: List[Callable[[T], bool]] = []
|
||||
|
||||
def filter(self, expression: str) -> 'Query':
|
||||
|
||||
def filter(self, expression: str) -> "Query":
|
||||
"""
|
||||
Filter by key:value expression.
|
||||
|
||||
|
||||
Examples:
|
||||
.filter('name:peace')
|
||||
.filter('number:1')
|
||||
.filter('sephera:gevurah')
|
||||
.filter('value:5')
|
||||
|
||||
|
||||
Supports multiple filters by chaining:
|
||||
.filter('number:1').filter('name:creative')
|
||||
"""
|
||||
key, value = expression.split(':', 1) if ':' in expression else (expression, '')
|
||||
|
||||
key, value = expression.split(":", 1) if ":" in expression else (expression, "")
|
||||
|
||||
def filter_func(item: T) -> bool:
|
||||
# Special handling for 'name' key
|
||||
if key == 'name':
|
||||
if hasattr(item, 'name'):
|
||||
if key == "name":
|
||||
if hasattr(item, "name"):
|
||||
value_lower = value.lower()
|
||||
item_name = str(item.name).lower()
|
||||
return value_lower == item_name or value_lower in item_name
|
||||
return False
|
||||
|
||||
|
||||
if not hasattr(item, key):
|
||||
return False
|
||||
item_value = getattr(item, key)
|
||||
@@ -93,34 +94,34 @@ class Query:
|
||||
return value.lower() in str(item_value).lower()
|
||||
else:
|
||||
return str(value) in str(item_value)
|
||||
|
||||
|
||||
self._filters.append(filter_func)
|
||||
return self
|
||||
|
||||
def name(self, value: str) -> Optional['QueryResult']:
|
||||
|
||||
def name(self, value: str) -> Optional["QueryResult"]:
|
||||
"""
|
||||
Deprecated: Use .filter('name:value') instead.
|
||||
|
||||
|
||||
Find item by name (exact or partial match, case-insensitive).
|
||||
Returns QueryResult wrapping the found item, or None if not found.
|
||||
"""
|
||||
return self.filter(f'name:{value}').first()
|
||||
|
||||
def get(self) -> Optional['QueryResult']:
|
||||
return self.filter(f"name:{value}").first()
|
||||
|
||||
def get(self) -> Optional["QueryResult"]:
|
||||
"""
|
||||
Get first result matching all applied filters.
|
||||
|
||||
|
||||
Returns QueryResult or None if no match.
|
||||
"""
|
||||
for item in self._data:
|
||||
if all(f(item) for f in self._filters):
|
||||
return QueryResult(item)
|
||||
return None
|
||||
|
||||
|
||||
def all(self) -> Dict[Any, T]:
|
||||
"""
|
||||
Get all results matching filters as dict.
|
||||
|
||||
|
||||
Returns original dict structure (if input was dict) with filtered values.
|
||||
"""
|
||||
filtered = {}
|
||||
@@ -133,26 +134,26 @@ class Query:
|
||||
if all(f(item) for f in self._filters):
|
||||
filtered[i] = item
|
||||
return filtered
|
||||
|
||||
|
||||
def list(self) -> List[T]:
|
||||
"""
|
||||
Get all results matching filters as list.
|
||||
|
||||
|
||||
Returns list of filtered items.
|
||||
"""
|
||||
return [item for item in self._data if all(f(item) for f in self._filters)]
|
||||
|
||||
def first(self) -> Optional['QueryResult']:
|
||||
|
||||
def first(self) -> Optional["QueryResult"]:
|
||||
"""Alias for get() - returns first matching item."""
|
||||
return self.get()
|
||||
|
||||
|
||||
def count(self) -> int:
|
||||
"""Count items matching all filters."""
|
||||
return len(self.list())
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Query({self.count()} items)"
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
items = self.list()
|
||||
if not items:
|
||||
@@ -201,20 +202,18 @@ class CollectionAccessor(Generic[T]):
|
||||
def display(self) -> str:
|
||||
"""
|
||||
Format all entries for user-friendly display with proper indentation.
|
||||
|
||||
|
||||
Returns a formatted string with each item separated by blank lines.
|
||||
Nested objects are indented and separated with their own sections.
|
||||
"""
|
||||
from utils.object_formatting import is_nested_object, get_object_attributes
|
||||
|
||||
data = self.all()
|
||||
if not data:
|
||||
return "(empty collection)"
|
||||
|
||||
|
||||
lines = []
|
||||
for key, item in data.items():
|
||||
lines.append(f"--- {key} ---")
|
||||
|
||||
|
||||
# Format all attributes with proper nesting
|
||||
for attr_name, attr_value in get_object_attributes(item):
|
||||
if is_nested_object(attr_value):
|
||||
@@ -224,9 +223,9 @@ class CollectionAccessor(Generic[T]):
|
||||
lines.append(nested)
|
||||
else:
|
||||
lines.append(f" {attr_name}: {attr_value}")
|
||||
|
||||
|
||||
lines.append("") # Blank line between items
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -240,34 +239,32 @@ class CollectionAccessor(Generic[T]):
|
||||
|
||||
class FilterableDict(dict):
|
||||
"""Dict subclass that provides .filter() method for dynamic querying."""
|
||||
|
||||
def filter(self, expression: str = '') -> Query:
|
||||
|
||||
def filter(self, expression: str = "") -> Query:
|
||||
"""
|
||||
Filter dict values by attribute:value expression.
|
||||
|
||||
|
||||
Examples:
|
||||
data.filter('name:peace')
|
||||
data.filter('number:1')
|
||||
data.filter('') # Returns query of all items
|
||||
"""
|
||||
return Query(self).filter(expression) if expression else Query(self)
|
||||
|
||||
|
||||
def display(self) -> str:
|
||||
"""
|
||||
Format all items in the dict for user-friendly display.
|
||||
|
||||
|
||||
Returns a formatted string with each item separated by blank lines.
|
||||
Nested objects are indented and separated with their own sections.
|
||||
"""
|
||||
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
|
||||
|
||||
if not self:
|
||||
return "(empty collection)"
|
||||
|
||||
|
||||
lines = []
|
||||
for key, item in self.items():
|
||||
lines.append(f"--- {key} ---")
|
||||
|
||||
|
||||
# Format all attributes with proper nesting
|
||||
for attr_name, attr_value in get_object_attributes(item):
|
||||
if is_nested_object(attr_value):
|
||||
@@ -277,16 +274,16 @@ class FilterableDict(dict):
|
||||
lines.append(nested)
|
||||
else:
|
||||
lines.append(f" {attr_name}: {attr_value}")
|
||||
|
||||
|
||||
lines.append("") # Blank line between items
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def make_filterable(data: Union[Dict[Any, T], List[T]]) -> Union['FilterableDict', Query]:
|
||||
def make_filterable(data: Union[Dict[Any, T], List[T]]) -> Union["FilterableDict", Query]:
|
||||
"""
|
||||
Convert dict or list to a filterable object with .filter() support.
|
||||
|
||||
|
||||
Examples:
|
||||
walls = make_filterable(Cube.wall())
|
||||
peace = walls.filter('name:North').first()
|
||||
@@ -297,4 +294,4 @@ def make_filterable(data: Union[Dict[Any, T], List[T]]) -> Union['FilterableDict
|
||||
return filterable
|
||||
else:
|
||||
# For lists, wrap in a Query
|
||||
return Query(data)
|
||||
return Query(data)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
sys.path.insert(0, 'src')
|
||||
|
||||
# Test parsing logic
|
||||
test_cases = [
|
||||
('2,11,20', [2, 11, 20]),
|
||||
('1 5 10', [1, 5, 10]),
|
||||
('3, 7, 14', [3, 7, 14]),
|
||||
]
|
||||
|
||||
for test_input, expected in test_cases:
|
||||
# Simulate the parsing logic
|
||||
parts = ['display-cards'] + test_input.split()
|
||||
nums = []
|
||||
tokens = []
|
||||
for tok in parts[1:]:
|
||||
if ',' in tok:
|
||||
tokens.extend(tok.split(','))
|
||||
else:
|
||||
tokens.append(tok)
|
||||
|
||||
for tok in tokens:
|
||||
tok = tok.strip()
|
||||
if tok:
|
||||
try:
|
||||
nums.append(int(tok))
|
||||
except ValueError:
|
||||
nums.append(-1)
|
||||
|
||||
status = '✓' if nums == expected else '✗'
|
||||
print(f'{status} Input: "{test_input}" -> {nums} (expected {expected})')
|
||||
|
||||
# Test with actual cards
|
||||
from tarot.tarot_api import Tarot
|
||||
print("\n✓ Parsing logic works! Now test in REPL with:")
|
||||
print(" display-cards 2,11,20")
|
||||
print(" display-cards 1 5 10")
|
||||
@@ -1,10 +0,0 @@
|
||||
from tarot import Tarot
|
||||
from tarot.ui import display_spread
|
||||
|
||||
# Draw a spread
|
||||
print("Drawing Celtic Cross spread...")
|
||||
reading = Tarot.deck.card.spread("Celtic Cross")
|
||||
|
||||
# Display it
|
||||
print("Displaying spread...")
|
||||
display_spread(reading)
|
||||
@@ -3,19 +3,41 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from src.tarot.attributes import (
|
||||
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter, Sephera, Degree, Element,
|
||||
AstrologicalInfluence, TreeOfLife, Correspondences, CardImage,
|
||||
EnglishAlphabet, GreekAlphabet, HebrewAlphabet, Number, Color, Planet, God,
|
||||
Cipher, CipherResult,
|
||||
AstrologicalInfluence,
|
||||
CardImage,
|
||||
Cipher,
|
||||
CipherResult,
|
||||
ClockHour,
|
||||
Color,
|
||||
Correspondences,
|
||||
Day,
|
||||
Degree,
|
||||
Element,
|
||||
EnglishAlphabet,
|
||||
God,
|
||||
GreekAlphabet,
|
||||
HebrewAlphabet,
|
||||
Hour,
|
||||
Letter,
|
||||
Meaning,
|
||||
Month,
|
||||
Number,
|
||||
Planet,
|
||||
Sephera,
|
||||
Suit,
|
||||
TreeOfLife,
|
||||
Weekday,
|
||||
Zodiac,
|
||||
)
|
||||
from src.tarot.card.data import CardDataLoader, calculate_digital_root
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Basic Attribute Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestMonth:
|
||||
def test_month_creation(self):
|
||||
month = Month(1, "January", "Capricorn", "Aquarius")
|
||||
@@ -24,10 +46,7 @@ class TestMonth:
|
||||
assert month.zodiac_start == "Capricorn"
|
||||
|
||||
def test_month_all_months(self):
|
||||
months = [
|
||||
Month(i, f"Month_{i}", "Sign_1", "Sign_2")
|
||||
for i in range(1, 13)
|
||||
]
|
||||
months = [Month(i, f"Month_{i}", "Sign_1", "Sign_2") for i in range(1, 13)]
|
||||
assert len(months) == 12
|
||||
assert months[0].number == 1
|
||||
assert months[11].number == 12
|
||||
@@ -41,10 +60,7 @@ class TestDay:
|
||||
assert day.planetary_correspondence == "Sun"
|
||||
|
||||
def test_all_weekdays(self):
|
||||
days = [
|
||||
Day(i, f"Day_{i}", f"Planet_{i}")
|
||||
for i in range(1, 8)
|
||||
]
|
||||
days = [Day(i, f"Day_{i}", f"Planet_{i}") for i in range(1, 8)]
|
||||
assert len(days) == 7
|
||||
|
||||
|
||||
@@ -99,6 +115,7 @@ class TestMeaning:
|
||||
# Sepheric Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestSephera:
|
||||
def test_sephera_creation(self):
|
||||
sephera = Sephera(1, "Kether", "כתר", "Crown", "Metatron", "Chaioth", "Primum")
|
||||
@@ -118,6 +135,7 @@ class TestSephera:
|
||||
# Alphabet Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestEnglishAlphabet:
|
||||
def test_english_letter_creation(self):
|
||||
letter = EnglishAlphabet("A", 1, "ay")
|
||||
@@ -189,6 +207,7 @@ class TestHebrewAlphabet:
|
||||
# Number Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestNumber:
|
||||
def test_number_creation(self):
|
||||
num = Number(1, "Kether", "Spirit", 0) # compliment is auto-calculated
|
||||
@@ -220,6 +239,7 @@ class TestNumber:
|
||||
# Color Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestColor:
|
||||
def test_color_creation(self):
|
||||
color = Color("Red", "#FF0000", (255, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power")
|
||||
@@ -248,7 +268,9 @@ class TestColor:
|
||||
for r in [0, 128, 255]:
|
||||
for g in [0, 128, 255]:
|
||||
for b in [0, 128, 255]:
|
||||
color = Color("Test", "#000000", (r, g, b), "Sephera", 1, "Element", "Scale", "Meaning")
|
||||
color = Color(
|
||||
"Test", "#000000", (r, g, b), "Sephera", 1, "Element", "Scale", "Meaning"
|
||||
)
|
||||
assert color.rgb == (r, g, b)
|
||||
|
||||
|
||||
@@ -260,7 +282,9 @@ class TestColor:
|
||||
class TestPlanet:
|
||||
def test_planet_creation(self):
|
||||
number = Number(6, "Tiphareth", "Fire", 0)
|
||||
color = Color("Gold", "#FFD700", (255, 215, 0), "Tiphareth", 6, "Fire", "Yetzirah", "Beauty")
|
||||
color = Color(
|
||||
"Gold", "#FFD700", (255, 215, 0), "Tiphareth", 6, "Fire", "Yetzirah", "Beauty"
|
||||
)
|
||||
planet = Planet(
|
||||
name="Sun",
|
||||
symbol="☉",
|
||||
@@ -342,6 +366,7 @@ class TestGod:
|
||||
# Cipher Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestCipher:
|
||||
def test_cipher_mapping_basic(self):
|
||||
cipher = Cipher("Test", "test", [1, 2, 3])
|
||||
@@ -374,6 +399,7 @@ class TestCipherResult:
|
||||
# Digital Root Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestDigitalRoot:
|
||||
def test_digital_root_single_digit(self):
|
||||
"""Single digits should return themselves."""
|
||||
@@ -389,7 +415,7 @@ class TestDigitalRoot:
|
||||
|
||||
def test_digital_root_large_numbers(self):
|
||||
"""Test large numbers."""
|
||||
assert calculate_digital_root(99) == 9 # 9+9 = 18, 1+8 = 9
|
||||
assert calculate_digital_root(99) == 9 # 9+9 = 18, 1+8 = 9
|
||||
assert calculate_digital_root(100) == 1 # 1+0+0 = 1
|
||||
assert calculate_digital_root(123) == 6 # 1+2+3 = 6
|
||||
|
||||
@@ -398,13 +424,13 @@ class TestDigitalRoot:
|
||||
# Major Arcana cards 0-21
|
||||
assert calculate_digital_root(14) == 5 # Card 14 (Temperance) -> 5
|
||||
assert calculate_digital_root(21) == 3 # Card 21 (The World) -> 3
|
||||
assert calculate_digital_root(1) == 1 # Card 1 (Magician) -> 1
|
||||
assert calculate_digital_root(1) == 1 # Card 1 (Magician) -> 1
|
||||
|
||||
def test_digital_root_invalid_input(self):
|
||||
"""Test that invalid inputs raise errors."""
|
||||
with pytest.raises(ValueError):
|
||||
calculate_digital_root(0)
|
||||
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
calculate_digital_root(-5)
|
||||
|
||||
@@ -413,6 +439,7 @@ class TestDigitalRoot:
|
||||
# CardDataLoader Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestCardDataLoader:
|
||||
@pytest.fixture
|
||||
def loader(self):
|
||||
@@ -443,33 +470,35 @@ class TestCardDataLoader:
|
||||
sephera = loader.sephera(i)
|
||||
assert sephera is not None
|
||||
assert sephera.number == i
|
||||
|
||||
|
||||
def test_load_ciphers(self, loader):
|
||||
"""Ensure cipher catalog is populated."""
|
||||
ciphers = loader.cipher()
|
||||
assert "english_simple" in ciphers
|
||||
assert ciphers["english_simple"].default_alphabet == "english"
|
||||
|
||||
|
||||
def test_word_cipher_request(self, loader):
|
||||
"""word().cipher() should return meaningful totals."""
|
||||
result = loader.word("tarot").cipher("english_simple")
|
||||
assert isinstance(result, CipherResult)
|
||||
assert result.total == 74
|
||||
assert result.alphabet_name == "english"
|
||||
|
||||
|
||||
def test_word_cipher_custom_alphabet(self, loader):
|
||||
result = loader.word("אמש").cipher("kabbalah_three_mother")
|
||||
assert result.values == (1, 40, 300)
|
||||
|
||||
def test_trigram_line_diagram(self, loader):
|
||||
from letter import trigram
|
||||
|
||||
tri = trigram.trigram.name("Zhen") # Thunder
|
||||
assert tri is not None
|
||||
assert tri.data.line_diagram == "|::"
|
||||
|
||||
def test_hexagram_line_diagram(self, loader):
|
||||
from letter import hexagram
|
||||
hex_result = hexagram.hexagram.filter('number:1').first()
|
||||
|
||||
hex_result = hexagram.hexagram.filter("number:1").first()
|
||||
assert hex_result is not None
|
||||
assert hex_result.data.line_diagram == "||||||"
|
||||
|
||||
@@ -575,7 +604,7 @@ class TestCardDataLoader:
|
||||
assert "english" in alphabets
|
||||
assert "greek" in alphabets
|
||||
assert "hebrew" in alphabets
|
||||
|
||||
|
||||
assert len(alphabets["english"]) == 26
|
||||
assert len(alphabets["greek"]) == 24
|
||||
assert len(alphabets["hebrew"]) == 22
|
||||
@@ -592,16 +621,16 @@ class TestCardDataLoader:
|
||||
|
||||
class TestDigitalRootIntegration:
|
||||
"""Integration tests for digital root with Tarot cards."""
|
||||
|
||||
|
||||
def test_all_major_arcana_digital_roots(self):
|
||||
"""Test digital root for all Major Arcana cards (0-21)."""
|
||||
loader = CardDataLoader()
|
||||
|
||||
|
||||
# All Major Arcana cards should map to colors 1-9
|
||||
for card_num in range(22):
|
||||
if card_num == 0:
|
||||
continue # Skip The Fool (0)
|
||||
|
||||
|
||||
color = loader.color_by_number(card_num)
|
||||
assert color is not None
|
||||
assert 1 <= color.number <= 9
|
||||
@@ -609,7 +638,7 @@ class TestDigitalRootIntegration:
|
||||
def test_color_consistency(self):
|
||||
"""Test that equivalent numbers map to same color."""
|
||||
loader = CardDataLoader()
|
||||
|
||||
|
||||
# 5 and 14 should map to same color (both have digital root 5)
|
||||
color_5 = loader.color_by_number(5)
|
||||
color_14 = loader.color_by_number(14)
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
import pytest
|
||||
from tarot.ui import CardDisplay
|
||||
from tarot.deck import Card
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.deck import Card
|
||||
from tarot.ui import CardDisplay
|
||||
|
||||
|
||||
def test_card_display_delegation():
|
||||
"""Test that CardDisplay delegates to SpreadDisplay correctly."""
|
||||
with patch('tarot.ui.SpreadDisplay') as MockSpreadDisplay:
|
||||
with patch("tarot.ui.SpreadDisplay") as MockSpreadDisplay:
|
||||
# Mock HAS_PILLOW to True to ensure we proceed
|
||||
with patch('tarot.ui.HAS_PILLOW', True):
|
||||
with patch("tarot.ui.HAS_PILLOW", True):
|
||||
display = CardDisplay()
|
||||
|
||||
|
||||
# Create dummy card
|
||||
card = MagicMock(spec=Card)
|
||||
card.name = "The Fool"
|
||||
card.image_path = "fool.jpg"
|
||||
cards = [card]
|
||||
|
||||
|
||||
display.show_cards(cards, title="Test Spread")
|
||||
|
||||
|
||||
# Verify SpreadDisplay was instantiated
|
||||
assert MockSpreadDisplay.call_count == 1
|
||||
|
||||
|
||||
# Verify run was called
|
||||
MockSpreadDisplay.return_value.run.assert_called_once()
|
||||
|
||||
|
||||
# Verify arguments passed to SpreadDisplay
|
||||
args, _ = MockSpreadDisplay.call_args
|
||||
reading = args[0]
|
||||
deck_name = args[1]
|
||||
|
||||
|
||||
assert deck_name == "default"
|
||||
assert reading.spread.name == "Card List"
|
||||
assert reading.spread.description == "Test Spread"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_cube_display_init():
|
||||
cube = Tarot.cube
|
||||
@@ -8,38 +10,40 @@ def test_cube_display_init():
|
||||
assert display.current_wall_name == "North"
|
||||
assert display.deck_name == "default"
|
||||
|
||||
|
||||
def test_cube_navigation():
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
|
||||
|
||||
# North -> Right -> East
|
||||
display._navigate("Right")
|
||||
assert display.current_wall_name == "East"
|
||||
|
||||
|
||||
# East -> Up -> Above
|
||||
display._navigate("Up")
|
||||
assert display.current_wall_name == "Above"
|
||||
|
||||
|
||||
# Above -> Down -> North
|
||||
display._navigate("Down")
|
||||
assert display.current_wall_name == "North"
|
||||
|
||||
|
||||
# North -> Left -> West
|
||||
display._navigate("Left")
|
||||
assert display.current_wall_name == "West"
|
||||
|
||||
|
||||
def test_find_card_for_direction():
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
|
||||
|
||||
# North Wall, Center Direction -> Aleph -> The Fool
|
||||
wall = cube.wall("North")
|
||||
direction = wall.direction("Center") # Should be Aleph?
|
||||
direction = wall.direction("Center") # Should be Aleph?
|
||||
# Wait, let's check what Center of North is.
|
||||
# Actually, let's just mock a direction
|
||||
|
||||
|
||||
from kaballah.cube.attributes import WallDirection
|
||||
|
||||
|
||||
# Aleph -> The Fool
|
||||
d = WallDirection("Center", "Aleph")
|
||||
card = display._find_card_for_direction(d)
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_cube_zoom():
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
assert display.zoom_level == 1.0
|
||||
|
||||
|
||||
display._zoom(1.1)
|
||||
assert display.zoom_level > 1.0
|
||||
|
||||
|
||||
display._zoom(0.5)
|
||||
assert display.zoom_level < 1.0
|
||||
|
||||
|
||||
def test_cube_zoom_limits():
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
|
||||
|
||||
# Test upper limit
|
||||
for _ in range(20):
|
||||
display._zoom(1.5)
|
||||
assert display.zoom_level <= 3.0
|
||||
|
||||
|
||||
# Test lower limit
|
||||
for _ in range(20):
|
||||
display._zoom(0.5)
|
||||
|
||||
@@ -1,60 +1,131 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
from tarot.tarot_api import Tarot
|
||||
import tkinter as tk
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_zoom_limits():
|
||||
# Mock Tk root
|
||||
class MockRoot:
|
||||
def __init__(self):
|
||||
self.bindings = {}
|
||||
self.images = []
|
||||
def bind(self, key, callback): pass
|
||||
def title(self, _): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 800
|
||||
def winfo_reqheight(self): return 600
|
||||
def winfo_screenwidth(self): return 1920
|
||||
def winfo_screenheight(self): return 1080
|
||||
def geometry(self, _): pass
|
||||
def mainloop(self): pass
|
||||
def focus_force(self): pass
|
||||
|
||||
|
||||
def bind(self, key, callback):
|
||||
pass
|
||||
|
||||
def title(self, _):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 800
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 600
|
||||
|
||||
def winfo_screenwidth(self):
|
||||
return 1920
|
||||
|
||||
def winfo_screenheight(self):
|
||||
return 1080
|
||||
|
||||
def geometry(self, _):
|
||||
pass
|
||||
|
||||
def mainloop(self):
|
||||
pass
|
||||
|
||||
def focus_force(self):
|
||||
pass
|
||||
|
||||
# Mock Frame
|
||||
class MockFrame:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.children = []
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def place(self, **kwargs): pass
|
||||
def grid(self, **kwargs): pass
|
||||
def grid_propagate(self, flag): pass
|
||||
def winfo_children(self): return self.children
|
||||
def destroy(self): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 100
|
||||
def winfo_reqheight(self): return 100
|
||||
def bind(self, event, callback): pass
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def place(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid_propagate(self, flag):
|
||||
pass
|
||||
|
||||
def winfo_children(self):
|
||||
return self.children
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 100
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 100
|
||||
|
||||
def bind(self, event, callback):
|
||||
pass
|
||||
|
||||
# Mock Canvas
|
||||
class MockCanvas:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def bind(self, event, callback): pass
|
||||
def create_window(self, coords, **kwargs): return 1
|
||||
def config(self, **kwargs): pass
|
||||
def bbox(self, tag): return (0,0,100,100)
|
||||
def winfo_width(self): return 800
|
||||
def winfo_height(self): return 600
|
||||
def coords(self, item, x, y): pass
|
||||
def scan_mark(self, x, y): pass
|
||||
def scan_dragto(self, x, y, gain=1): pass
|
||||
def canvasx(self, x): return x
|
||||
def canvasy(self, y): return y
|
||||
def xview_moveto(self, fraction): pass
|
||||
def yview_moveto(self, fraction): pass
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def bind(self, event, callback):
|
||||
pass
|
||||
|
||||
def create_window(self, coords, **kwargs):
|
||||
return 1
|
||||
|
||||
def config(self, **kwargs):
|
||||
pass
|
||||
|
||||
def bbox(self, tag):
|
||||
return (0, 0, 100, 100)
|
||||
|
||||
def winfo_width(self):
|
||||
return 800
|
||||
|
||||
def winfo_height(self):
|
||||
return 600
|
||||
|
||||
def coords(self, item, x, y):
|
||||
pass
|
||||
|
||||
def scan_mark(self, x, y):
|
||||
pass
|
||||
|
||||
def scan_dragto(self, x, y, gain=1):
|
||||
pass
|
||||
|
||||
def canvasx(self, x):
|
||||
return x
|
||||
|
||||
def canvasy(self, y):
|
||||
return y
|
||||
|
||||
def xview_moveto(self, fraction):
|
||||
pass
|
||||
|
||||
def yview_moveto(self, fraction):
|
||||
pass
|
||||
|
||||
# Monkey patch tk
|
||||
original_tk = tk.Tk
|
||||
@@ -62,60 +133,68 @@ def test_zoom_limits():
|
||||
original_canvas = tk.Canvas
|
||||
original_label = tk.ttk.Label
|
||||
original_button = tk.ttk.Button
|
||||
|
||||
|
||||
# Mock Label and Button
|
||||
class MockWidget:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def place(self, **kwargs): pass
|
||||
def grid(self, **kwargs): pass
|
||||
def grid_propagate(self, flag): pass
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def place(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid_propagate(self, flag):
|
||||
pass
|
||||
|
||||
try:
|
||||
tk.Tk = MockRoot
|
||||
tk.ttk.Frame = MockFrame
|
||||
tk.Canvas = MockCanvas
|
||||
tk.ttk.Label = MockWidget
|
||||
tk.ttk.Button = MockWidget
|
||||
|
||||
|
||||
# Mock Image to avoid memory issues
|
||||
with patch('PIL.Image.open') as mock_open:
|
||||
with patch("PIL.Image.open") as mock_open:
|
||||
mock_img = MagicMock()
|
||||
mock_img.size = (100, 100)
|
||||
mock_img.resize.return_value = mock_img
|
||||
mock_open.return_value = mock_img
|
||||
|
||||
with patch('PIL.ImageTk.PhotoImage') as mock_photo:
|
||||
|
||||
|
||||
with patch("PIL.ImageTk.PhotoImage") as mock_photo:
|
||||
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
display.root = MockRoot()
|
||||
display.canvas = MockCanvas()
|
||||
display.content_frame = MockFrame()
|
||||
display.canvas_window = 1 # Mock window ID
|
||||
|
||||
display.canvas_window = 1 # Mock window ID
|
||||
|
||||
# Test initial zoom
|
||||
assert display.zoom_level == 1.0
|
||||
|
||||
|
||||
# Test zoom in
|
||||
display._zoom(1.22)
|
||||
assert display.zoom_level == 1.22
|
||||
|
||||
|
||||
# Test max limit (should be 50.0)
|
||||
# Zoom way in
|
||||
for _ in range(100):
|
||||
display._zoom(1.22)
|
||||
|
||||
|
||||
assert display.zoom_level == 50.0
|
||||
|
||||
|
||||
# Test min limit (should be 0.1)
|
||||
# Zoom way out
|
||||
for _ in range(200):
|
||||
display._zoom(0.5)
|
||||
|
||||
|
||||
assert display.zoom_level == 0.1
|
||||
|
||||
|
||||
finally:
|
||||
tk.Tk = original_tk
|
||||
tk.ttk.Frame = original_frame
|
||||
|
||||
@@ -3,8 +3,9 @@ Tests for Tarot deck and card classes.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from src.tarot.deck import Deck, Card, MajorCard, MinorCard, PipCard, AceCard, CourtCard
|
||||
from src.tarot.attributes import Meaning, Suit, CardImage
|
||||
|
||||
from src.tarot.attributes import CardImage, Meaning, Suit
|
||||
from src.tarot.deck import AceCard, Card, CourtCard, Deck, MajorCard, MinorCard, PipCard
|
||||
|
||||
|
||||
class TestCard:
|
||||
@@ -30,7 +31,7 @@ class TestMajorCard:
|
||||
name="The Magician",
|
||||
meaning=Meaning("Upright", "Reversed"),
|
||||
arcana="Major",
|
||||
kabbalistic_number=1
|
||||
kabbalistic_number=1,
|
||||
)
|
||||
assert card.number == 1
|
||||
assert card.arcana == "Major"
|
||||
@@ -42,7 +43,7 @@ class TestMajorCard:
|
||||
name="Test",
|
||||
meaning=Meaning("Up", "Rev"),
|
||||
arcana="Major",
|
||||
kabbalistic_number=-1
|
||||
kabbalistic_number=-1,
|
||||
)
|
||||
|
||||
def test_major_card_invalid_high(self):
|
||||
@@ -52,16 +53,13 @@ class TestMajorCard:
|
||||
name="Test",
|
||||
meaning=Meaning("Up", "Rev"),
|
||||
arcana="Major",
|
||||
kabbalistic_number=22
|
||||
kabbalistic_number=22,
|
||||
)
|
||||
|
||||
def test_major_card_valid_range(self):
|
||||
for i in range(22):
|
||||
card = MajorCard(
|
||||
number=i,
|
||||
name=f"Card {i}",
|
||||
meaning=Meaning("Up", "Rev"),
|
||||
arcana="Major"
|
||||
number=i, name=f"Card {i}", meaning=Meaning("Up", "Rev"), arcana="Major"
|
||||
)
|
||||
assert card.number == i
|
||||
|
||||
@@ -75,7 +73,7 @@ class TestMinorCard:
|
||||
meaning=Meaning("Upright", "Reversed"),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=1
|
||||
pip=1,
|
||||
)
|
||||
assert card.number == 1
|
||||
assert card.suit.name == "Cups"
|
||||
@@ -90,7 +88,7 @@ class TestMinorCard:
|
||||
meaning=Meaning("Up", "Rev"),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=0
|
||||
pip=0,
|
||||
)
|
||||
|
||||
def test_minor_card_invalid_pip_high(self):
|
||||
@@ -102,7 +100,7 @@ class TestMinorCard:
|
||||
meaning=Meaning("Up", "Rev"),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=15
|
||||
pip=15,
|
||||
)
|
||||
|
||||
def test_minor_card_valid_pips(self):
|
||||
@@ -114,7 +112,7 @@ class TestMinorCard:
|
||||
meaning=Meaning("Up", "Rev"),
|
||||
arcana="Minor",
|
||||
suit=suit,
|
||||
pip=i
|
||||
pip=i,
|
||||
)
|
||||
assert card.pip == i
|
||||
|
||||
@@ -137,10 +135,10 @@ class TestDeck:
|
||||
def test_deck_shuffle(self):
|
||||
deck1 = Deck()
|
||||
cards_before = [c.name for c in deck1.cards]
|
||||
|
||||
|
||||
deck1.shuffle()
|
||||
cards_after = [c.name for c in deck1.cards]
|
||||
|
||||
|
||||
# After shuffle, order should change (with high probability)
|
||||
# We don't assert they're different since shuffle could randomly give same order
|
||||
assert len(cards_after) == 78
|
||||
@@ -148,18 +146,18 @@ class TestDeck:
|
||||
def test_deck_draw_single(self):
|
||||
deck = Deck()
|
||||
initial_count = len(deck.cards)
|
||||
|
||||
|
||||
drawn = deck.draw(1)
|
||||
|
||||
|
||||
assert len(drawn) == 1
|
||||
assert len(deck.cards) == initial_count - 1
|
||||
|
||||
def test_deck_draw_multiple(self):
|
||||
deck = Deck()
|
||||
initial_count = len(deck.cards)
|
||||
|
||||
|
||||
drawn = deck.draw(5)
|
||||
|
||||
|
||||
assert len(drawn) == 5
|
||||
assert len(deck.cards) == initial_count - 5
|
||||
|
||||
@@ -177,28 +175,28 @@ class TestDeck:
|
||||
deck = Deck()
|
||||
deck.draw(5)
|
||||
assert len(deck.cards) < 78
|
||||
|
||||
|
||||
deck.reset()
|
||||
assert len(deck.cards) == 78
|
||||
|
||||
def test_deck_remaining(self):
|
||||
deck = Deck()
|
||||
assert deck.remaining() == 78
|
||||
|
||||
|
||||
deck.draw(1)
|
||||
assert deck.remaining() == 77
|
||||
|
||||
def test_deck_len(self):
|
||||
deck = Deck()
|
||||
assert len(deck) == 78
|
||||
|
||||
|
||||
deck.draw(1)
|
||||
assert len(deck) == 77
|
||||
|
||||
def test_deck_repr(self):
|
||||
deck = Deck()
|
||||
assert "78 cards" in repr(deck)
|
||||
|
||||
|
||||
deck.draw(1)
|
||||
assert "77 cards" in repr(deck)
|
||||
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.ui import CardDisplay
|
||||
|
||||
|
||||
def test_card_display_init():
|
||||
display = CardDisplay("default")
|
||||
assert display.deck_name == "default"
|
||||
# Check if path resolves correctly relative to src/tarot/ui.py
|
||||
# src/tarot/ui.py -> src/tarot -> src/tarot/deck/default
|
||||
expected_suffix = os.path.join("src", "tarot", "deck", "default")
|
||||
assert str(display.deck_path).endswith(expected_suffix) or str(display.deck_path).endswith("default")
|
||||
assert str(display.deck_path).endswith(expected_suffix) or str(display.deck_path).endswith(
|
||||
"default"
|
||||
)
|
||||
|
||||
|
||||
def test_card_display_resolve_path():
|
||||
display = CardDisplay("thoth")
|
||||
assert display.deck_name == "thoth"
|
||||
assert str(display.deck_path).endswith("thoth")
|
||||
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,60 +1,63 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
from tarot.tarot_api import Tarot
|
||||
import tkinter as tk
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_recursive_binding():
|
||||
# Mock Tk root and widgets
|
||||
class MockWidget:
|
||||
def __init__(self):
|
||||
self.children = []
|
||||
self.bindings = {}
|
||||
|
||||
|
||||
def bind(self, key, callback):
|
||||
self.bindings[key] = callback
|
||||
|
||||
|
||||
def winfo_children(self):
|
||||
return self.children
|
||||
|
||||
|
||||
def add_child(self, child):
|
||||
self.children.append(child)
|
||||
|
||||
# Monkey patch tk
|
||||
original_tk = tk.Tk
|
||||
original_frame = tk.ttk.Frame
|
||||
|
||||
|
||||
try:
|
||||
# We don't need to mock everything, just enough to test _bind_recursive
|
||||
|
||||
|
||||
cube = Tarot.cube
|
||||
# We can instantiate CubeDisplay without showing it
|
||||
display = CubeDisplay(cube)
|
||||
|
||||
|
||||
# Create a mock widget tree
|
||||
parent = MockWidget()
|
||||
child1 = MockWidget()
|
||||
child2 = MockWidget()
|
||||
grandchild = MockWidget()
|
||||
|
||||
|
||||
parent.add_child(child1)
|
||||
parent.add_child(child2)
|
||||
child1.add_child(grandchild)
|
||||
|
||||
|
||||
# Run recursive binding
|
||||
display._bind_recursive(parent)
|
||||
|
||||
|
||||
# Verify bindings
|
||||
assert "<ButtonPress-1>" in parent.bindings
|
||||
assert "<B1-Motion>" in parent.bindings
|
||||
|
||||
|
||||
assert "<ButtonPress-1>" in child1.bindings
|
||||
assert "<B1-Motion>" in child1.bindings
|
||||
|
||||
|
||||
assert "<ButtonPress-1>" in child2.bindings
|
||||
assert "<B1-Motion>" in child2.bindings
|
||||
|
||||
|
||||
assert "<ButtonPress-1>" in grandchild.bindings
|
||||
assert "<B1-Motion>" in grandchild.bindings
|
||||
|
||||
|
||||
finally:
|
||||
pass
|
||||
|
||||
@@ -1,72 +1,99 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
from tarot.tarot_api import Tarot
|
||||
import tkinter as tk
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_zoom_key_bindings():
|
||||
# This test verifies that the bindings are set up,
|
||||
# This test verifies that the bindings are set up,
|
||||
# but cannot easily simulate key presses in headless environment.
|
||||
# We check if the bind method was called with correct keys.
|
||||
|
||||
|
||||
# Mock Tk root
|
||||
class MockRoot:
|
||||
def __init__(self):
|
||||
self.bindings = {}
|
||||
|
||||
|
||||
def bind(self, key, callback):
|
||||
self.bindings[key] = callback
|
||||
|
||||
def title(self, _): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 800
|
||||
def winfo_reqheight(self): return 600
|
||||
def winfo_screenwidth(self): return 1920
|
||||
def winfo_screenheight(self): return 1080
|
||||
def geometry(self, _): pass
|
||||
def mainloop(self): pass
|
||||
def focus_force(self): pass
|
||||
|
||||
|
||||
def title(self, _):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 800
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 600
|
||||
|
||||
def winfo_screenwidth(self):
|
||||
return 1920
|
||||
|
||||
def winfo_screenheight(self):
|
||||
return 1080
|
||||
|
||||
def geometry(self, _):
|
||||
pass
|
||||
|
||||
def mainloop(self):
|
||||
pass
|
||||
|
||||
def focus_force(self):
|
||||
pass
|
||||
|
||||
# Mock Frame
|
||||
class MockFrame:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.children = []
|
||||
def pack(self, **kwargs): pass
|
||||
def winfo_children(self): return self.children
|
||||
def destroy(self): pass
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def winfo_children(self):
|
||||
return self.children
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
# Monkey patch tk
|
||||
original_tk = tk.Tk
|
||||
original_frame = tk.ttk.Frame
|
||||
|
||||
|
||||
try:
|
||||
tk.Tk = MockRoot
|
||||
tk.ttk.Frame = MockFrame
|
||||
|
||||
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
|
||||
|
||||
# We need to call show() to trigger bindings, but avoid mainloop
|
||||
# We can't easily mock show() without refactoring,
|
||||
# We can't easily mock show() without refactoring,
|
||||
# so we'll just inspect the code logic or trust the manual test.
|
||||
# However, we can manually call the binding logic if we extract it.
|
||||
|
||||
|
||||
# Since we can't easily mock the entire UI startup in a unit test without
|
||||
# a display, we'll rely on the fact that we added the bindings in the code.
|
||||
pass
|
||||
|
||||
|
||||
finally:
|
||||
tk.Tk = original_tk
|
||||
tk.ttk.Frame = original_frame
|
||||
|
||||
|
||||
def test_zoom_logic_direct():
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
display.zoom_level = 1.0
|
||||
|
||||
|
||||
# Simulate + key press effect
|
||||
display._zoom(1.1)
|
||||
assert display.zoom_level > 1.0
|
||||
|
||||
|
||||
# Simulate - key press effect
|
||||
display._zoom(0.9)
|
||||
assert display.zoom_level < 1.1
|
||||
|
||||
@@ -1,83 +1,140 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
from tarot.tarot_api import Tarot
|
||||
import tkinter as tk
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_canvas_structure():
|
||||
# Mock Tk root
|
||||
class MockRoot:
|
||||
def __init__(self):
|
||||
self.bindings = {}
|
||||
def bind(self, key, callback): pass
|
||||
def title(self, _): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 800
|
||||
def winfo_reqheight(self): return 600
|
||||
def winfo_screenwidth(self): return 1920
|
||||
def winfo_screenheight(self): return 1080
|
||||
def geometry(self, _): pass
|
||||
def mainloop(self): pass
|
||||
def focus_force(self): pass
|
||||
|
||||
|
||||
def bind(self, key, callback):
|
||||
pass
|
||||
|
||||
def title(self, _):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 800
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 600
|
||||
|
||||
def winfo_screenwidth(self):
|
||||
return 1920
|
||||
|
||||
def winfo_screenheight(self):
|
||||
return 1080
|
||||
|
||||
def geometry(self, _):
|
||||
pass
|
||||
|
||||
def mainloop(self):
|
||||
pass
|
||||
|
||||
def focus_force(self):
|
||||
pass
|
||||
|
||||
# Mock Frame
|
||||
class MockFrame:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.children = []
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def place(self, **kwargs): pass
|
||||
def winfo_children(self): return self.children
|
||||
def destroy(self): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 100
|
||||
def winfo_reqheight(self): return 100
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def place(self, **kwargs):
|
||||
pass
|
||||
|
||||
def winfo_children(self):
|
||||
return self.children
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 100
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 100
|
||||
|
||||
# Mock Canvas
|
||||
class MockCanvas:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def bind(self, event, callback): pass
|
||||
def create_window(self, coords, **kwargs): return 1
|
||||
def config(self, **kwargs): pass
|
||||
def bbox(self, tag): return (0,0,100,100)
|
||||
def winfo_width(self): return 800
|
||||
def winfo_height(self): return 600
|
||||
def coords(self, item, x, y): pass
|
||||
def scan_mark(self, x, y): pass
|
||||
def scan_dragto(self, x, y, gain=1): pass
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def bind(self, event, callback):
|
||||
pass
|
||||
|
||||
def create_window(self, coords, **kwargs):
|
||||
return 1
|
||||
|
||||
def config(self, **kwargs):
|
||||
pass
|
||||
|
||||
def bbox(self, tag):
|
||||
return (0, 0, 100, 100)
|
||||
|
||||
def winfo_width(self):
|
||||
return 800
|
||||
|
||||
def winfo_height(self):
|
||||
return 600
|
||||
|
||||
def coords(self, item, x, y):
|
||||
pass
|
||||
|
||||
def scan_mark(self, x, y):
|
||||
pass
|
||||
|
||||
def scan_dragto(self, x, y, gain=1):
|
||||
pass
|
||||
|
||||
# Monkey patch tk
|
||||
original_tk = tk.Tk
|
||||
original_frame = tk.ttk.Frame
|
||||
original_canvas = tk.Canvas
|
||||
|
||||
|
||||
try:
|
||||
tk.Tk = MockRoot
|
||||
tk.ttk.Frame = MockFrame
|
||||
tk.Canvas = MockCanvas
|
||||
|
||||
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
|
||||
|
||||
# Trigger show to build UI
|
||||
# We can't fully run show() because of mainloop, but we can instantiate parts
|
||||
# Actually, show() creates the root.
|
||||
# Let's just verify the structure by inspecting the code or trusting the manual test.
|
||||
# But we can test the pan methods directly.
|
||||
|
||||
|
||||
display.canvas = MockCanvas()
|
||||
|
||||
|
||||
# Test pan methods
|
||||
class MockEvent:
|
||||
x = 10
|
||||
y = 20
|
||||
x_root = 110
|
||||
y_root = 120
|
||||
|
||||
|
||||
display._start_pan(MockEvent())
|
||||
display._pan(MockEvent())
|
||||
|
||||
|
||||
finally:
|
||||
tk.Tk = original_tk
|
||||
tk.ttk.Frame = original_frame
|
||||
|
||||
@@ -1,68 +1,137 @@
|
||||
import pytest
|
||||
from tarot.ui import CubeDisplay
|
||||
from tarot.tarot_api import Tarot
|
||||
import tkinter as tk
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from tarot.tarot_api import Tarot
|
||||
from tarot.ui import CubeDisplay
|
||||
|
||||
|
||||
def test_wasd_panning():
|
||||
# Mock Tk root
|
||||
class MockRoot:
|
||||
def __init__(self):
|
||||
self.bindings = {}
|
||||
self.images = []
|
||||
def bind(self, key, callback):
|
||||
|
||||
def bind(self, key, callback):
|
||||
self.bindings[key] = callback
|
||||
def title(self, _): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 800
|
||||
def winfo_reqheight(self): return 600
|
||||
def winfo_screenwidth(self): return 1920
|
||||
def winfo_screenheight(self): return 1080
|
||||
def geometry(self, _): pass
|
||||
def mainloop(self): pass
|
||||
def focus_force(self): pass
|
||||
|
||||
|
||||
def title(self, _):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 800
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 600
|
||||
|
||||
def winfo_screenwidth(self):
|
||||
return 1920
|
||||
|
||||
def winfo_screenheight(self):
|
||||
return 1080
|
||||
|
||||
def geometry(self, _):
|
||||
pass
|
||||
|
||||
def mainloop(self):
|
||||
pass
|
||||
|
||||
def focus_force(self):
|
||||
pass
|
||||
|
||||
# Mock Frame
|
||||
class MockFrame:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.children = []
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def place(self, **kwargs): pass
|
||||
def grid(self, **kwargs): pass
|
||||
def grid_propagate(self, flag): pass
|
||||
def winfo_children(self): return self.children
|
||||
def destroy(self): pass
|
||||
def update_idletasks(self): pass
|
||||
def winfo_reqwidth(self): return 100
|
||||
def winfo_reqheight(self): return 100
|
||||
def bind(self, event, callback): pass
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def place(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid_propagate(self, flag):
|
||||
pass
|
||||
|
||||
def winfo_children(self):
|
||||
return self.children
|
||||
|
||||
def destroy(self):
|
||||
pass
|
||||
|
||||
def update_idletasks(self):
|
||||
pass
|
||||
|
||||
def winfo_reqwidth(self):
|
||||
return 100
|
||||
|
||||
def winfo_reqheight(self):
|
||||
return 100
|
||||
|
||||
def bind(self, event, callback):
|
||||
pass
|
||||
|
||||
# Mock Canvas
|
||||
class MockCanvas:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.master = master
|
||||
self.x_scrolls = []
|
||||
self.y_scrolls = []
|
||||
|
||||
def pack(self, **kwargs): pass
|
||||
def bind(self, event, callback): pass
|
||||
def create_window(self, coords, **kwargs): return 1
|
||||
def config(self, **kwargs): pass
|
||||
def bbox(self, tag): return (0,0,100,100)
|
||||
def winfo_width(self): return 800
|
||||
def winfo_height(self): return 600
|
||||
def coords(self, item, x, y): pass
|
||||
def scan_mark(self, x, y): pass
|
||||
def scan_dragto(self, x, y, gain=1): pass
|
||||
def canvasx(self, x): return x
|
||||
def canvasy(self, y): return y
|
||||
def xview_moveto(self, fraction): pass
|
||||
def yview_moveto(self, fraction): pass
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def bind(self, event, callback):
|
||||
pass
|
||||
|
||||
def create_window(self, coords, **kwargs):
|
||||
return 1
|
||||
|
||||
def config(self, **kwargs):
|
||||
pass
|
||||
|
||||
def bbox(self, tag):
|
||||
return (0, 0, 100, 100)
|
||||
|
||||
def winfo_width(self):
|
||||
return 800
|
||||
|
||||
def winfo_height(self):
|
||||
return 600
|
||||
|
||||
def coords(self, item, x, y):
|
||||
pass
|
||||
|
||||
def scan_mark(self, x, y):
|
||||
pass
|
||||
|
||||
def scan_dragto(self, x, y, gain=1):
|
||||
pass
|
||||
|
||||
def canvasx(self, x):
|
||||
return x
|
||||
|
||||
def canvasy(self, y):
|
||||
return y
|
||||
|
||||
def xview_moveto(self, fraction):
|
||||
pass
|
||||
|
||||
def yview_moveto(self, fraction):
|
||||
pass
|
||||
|
||||
def xview_scroll(self, number, what):
|
||||
self.x_scrolls.append((number, what))
|
||||
|
||||
|
||||
def yview_scroll(self, number, what):
|
||||
self.y_scrolls.append((number, what))
|
||||
|
||||
@@ -72,54 +141,62 @@ def test_wasd_panning():
|
||||
original_canvas = tk.Canvas
|
||||
original_label = tk.ttk.Label
|
||||
original_button = tk.ttk.Button
|
||||
|
||||
|
||||
# Mock Label and Button
|
||||
class MockWidget:
|
||||
def __init__(self, master=None, **kwargs):
|
||||
self.master = master
|
||||
def pack(self, **kwargs): pass
|
||||
def place(self, **kwargs): pass
|
||||
def grid(self, **kwargs): pass
|
||||
def grid_propagate(self, flag): pass
|
||||
|
||||
|
||||
def pack(self, **kwargs):
|
||||
pass
|
||||
|
||||
def place(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid(self, **kwargs):
|
||||
pass
|
||||
|
||||
def grid_propagate(self, flag):
|
||||
pass
|
||||
|
||||
try:
|
||||
tk.Tk = MockRoot
|
||||
tk.ttk.Frame = MockFrame
|
||||
tk.Canvas = MockCanvas
|
||||
tk.ttk.Label = MockWidget
|
||||
tk.ttk.Button = MockWidget
|
||||
|
||||
|
||||
# Mock Image to avoid memory issues
|
||||
with patch('PIL.Image.open') as mock_open:
|
||||
with patch("PIL.Image.open") as mock_open:
|
||||
mock_img = MagicMock()
|
||||
mock_img.size = (100, 100)
|
||||
mock_img.resize.return_value = mock_img
|
||||
mock_open.return_value = mock_img
|
||||
|
||||
with patch('PIL.ImageTk.PhotoImage') as mock_photo:
|
||||
|
||||
|
||||
with patch("PIL.ImageTk.PhotoImage") as mock_photo:
|
||||
|
||||
cube = Tarot.cube
|
||||
display = CubeDisplay(cube)
|
||||
display.root = MockRoot()
|
||||
display.canvas = MockCanvas()
|
||||
display.content_frame = MockFrame()
|
||||
display.canvas_window = 1
|
||||
|
||||
display.canvas_window = 1
|
||||
|
||||
# Manually trigger bindings (since we can't easily simulate key press in mock root without event loop)
|
||||
# But we can call _pan_key directly to test logic
|
||||
|
||||
|
||||
display._pan_key("up")
|
||||
assert display.canvas.y_scrolls[-1] == (-1, "units")
|
||||
|
||||
|
||||
display._pan_key("down")
|
||||
assert display.canvas.y_scrolls[-1] == (1, "units")
|
||||
|
||||
|
||||
display._pan_key("left")
|
||||
assert display.canvas.x_scrolls[-1] == (-1, "units")
|
||||
|
||||
|
||||
display._pan_key("right")
|
||||
assert display.canvas.x_scrolls[-1] == (1, "units")
|
||||
|
||||
|
||||
finally:
|
||||
tk.Tk = original_tk
|
||||
tk.ttk.Frame = original_frame
|
||||
|
||||
@@ -8,8 +8,8 @@ References:
|
||||
- Weekday planetary rulers
|
||||
"""
|
||||
|
||||
from datetime import datetime, date, timedelta, timezone
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
# Planetary symbols for weekdays (Sun=0, Mon=1, ..., Sat=6)
|
||||
|
||||
Reference in New Issue
Block a user