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:
|
Quick Start:
|
||||||
|
|
||||||
from tarot import number, letter, kaballah, Tarot
|
from tarot import number, letter, kaballah, Tarot
|
||||||
|
|
||||||
# Number
|
# Number
|
||||||
num = number.number(5)
|
num = number.number(5)
|
||||||
root = number.digital_root(256)
|
root = number.digital_root(256)
|
||||||
|
|
||||||
# Letter
|
# Letter
|
||||||
letter_obj = letter.letter('A')
|
letter_obj = letter.letter('A')
|
||||||
result = letter.word('MAGICK').cipher('english_simple')
|
result = letter.word('MAGICK').cipher('english_simple')
|
||||||
|
|
||||||
# Kaballah
|
# Kaballah
|
||||||
sephera = kaballah.Tree.sephera(1)
|
sephera = kaballah.Tree.sephera(1)
|
||||||
wall = kaballah.Cube.wall('North')
|
wall = kaballah.Cube.wall('North')
|
||||||
|
|
||||||
# Tarot
|
# Tarot
|
||||||
card = Tarot.deck.card(3)
|
card = Tarot.deck.card(3)
|
||||||
major5 = Tarot.deck.card.major(5)
|
major5 = Tarot.deck.card.major(5)
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ Provides fluent query interface for:
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import kaballah
|
from tarot import kaballah
|
||||||
|
|
||||||
sephera = kaballah.Tree.sephera(1)
|
sephera = kaballah.Tree.sephera(1)
|
||||||
path = kaballah.Tree.path(11)
|
path = kaballah.Tree.path(11)
|
||||||
wall = kaballah.Cube.wall("North")
|
wall = kaballah.Cube.wall("North")
|
||||||
direction = kaballah.Cube.direction("North", "East")
|
direction = kaballah.Cube.direction("North", "East")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .tree import Tree
|
|
||||||
from .cube import Cube
|
from .cube import Cube
|
||||||
|
from .tree import Tree
|
||||||
|
|
||||||
# Export classes for fluent access
|
# Export classes for fluent access
|
||||||
__all__ = ["Tree", "Cube"]
|
__all__ = ["Tree", "Cube"]
|
||||||
|
|||||||
@@ -6,22 +6,22 @@ including Sephira, Paths, and Tree of Life structures.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Dict, List, Optional, Tuple, Any
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from utils.attributes import (
|
from utils.attributes import (
|
||||||
Element,
|
|
||||||
ElementType,
|
|
||||||
Planet,
|
|
||||||
Color,
|
Color,
|
||||||
Colorscale,
|
Colorscale,
|
||||||
Perfume,
|
ElementType,
|
||||||
God,
|
God,
|
||||||
|
Perfume,
|
||||||
|
Planet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Sephera:
|
class Sephera:
|
||||||
"""Represents a Sephira on the Tree of Life."""
|
"""Represents a Sephira on the Tree of Life."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
hebrew_name: str
|
hebrew_name: str
|
||||||
@@ -29,21 +29,22 @@ class Sephera:
|
|||||||
archangel: str
|
archangel: str
|
||||||
order_of_angels: str
|
order_of_angels: str
|
||||||
mundane_chakra: str
|
mundane_chakra: str
|
||||||
element: Optional['ElementType'] = None
|
element: Optional["ElementType"] = None
|
||||||
planetary_ruler: Optional[str] = None
|
planetary_ruler: Optional[str] = None
|
||||||
tarot_trump: Optional[str] = None
|
tarot_trump: Optional[str] = None
|
||||||
colorscale: Optional['Colorscale'] = None
|
colorscale: Optional["Colorscale"] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PeriodicTable:
|
class PeriodicTable:
|
||||||
"""Represents a Sephirothic position in Kabbalah with cross-correspondences."""
|
"""Represents a Sephirothic position in Kabbalah with cross-correspondences."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
sephera: Optional[Sephera]
|
sephera: Optional[Sephera]
|
||||||
element: Optional['ElementType'] = None
|
element: Optional["ElementType"] = None
|
||||||
planet: Optional['Planet'] = None
|
planet: Optional["Planet"] = None
|
||||||
color: Optional['Color'] = None
|
color: Optional["Color"] = None
|
||||||
tarot_trump: Optional[str] = None
|
tarot_trump: Optional[str] = None
|
||||||
hebrew_letter: Optional[str] = None
|
hebrew_letter: Optional[str] = None
|
||||||
divine_name: Optional[str] = None
|
divine_name: Optional[str] = None
|
||||||
@@ -55,6 +56,7 @@ class PeriodicTable:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class TreeOfLife:
|
class TreeOfLife:
|
||||||
"""Represents the Tree of Life structure."""
|
"""Represents the Tree of Life structure."""
|
||||||
|
|
||||||
sephiroth: Dict[int, str]
|
sephiroth: Dict[int, str]
|
||||||
paths: Dict[Tuple[int, int], str]
|
paths: Dict[Tuple[int, int], str]
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ class TreeOfLife:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Correspondences:
|
class Correspondences:
|
||||||
"""Represents Kabbalistic correspondences."""
|
"""Represents Kabbalistic correspondences."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
sephira: str
|
sephira: str
|
||||||
element: Optional[str]
|
element: Optional[str]
|
||||||
@@ -76,55 +79,56 @@ class Correspondences:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Path:
|
class Path:
|
||||||
"""Represents one of the 22 Paths on the Tree of Life with full correspondences."""
|
"""Represents one of the 22 Paths on the Tree of Life with full correspondences."""
|
||||||
|
|
||||||
number: int # 11-32
|
number: int # 11-32
|
||||||
hebrew_letter: str # Hebrew letter name (Aleph through Tau)
|
hebrew_letter: str # Hebrew letter name (Aleph through Tau)
|
||||||
transliteration: str # English transliteration
|
transliteration: str # English transliteration
|
||||||
tarot_trump: str # Major Arcana card (0-XXI)
|
tarot_trump: str # Major Arcana card (0-XXI)
|
||||||
sephera_from: Optional['Sephera'] = None # Lower Sephira
|
sephera_from: Optional["Sephera"] = None # Lower Sephira
|
||||||
sephera_to: Optional['Sephera'] = None # Upper Sephira
|
sephera_to: Optional["Sephera"] = None # Upper Sephira
|
||||||
element: Optional['ElementType'] = None # Element (Air, Fire, Water, Earth)
|
element: Optional["ElementType"] = None # Element (Air, Fire, Water, Earth)
|
||||||
planet: Optional['Planet'] = None # Planetary ruler
|
planet: Optional["Planet"] = None # Planetary ruler
|
||||||
zodiac_sign: Optional[str] = None # Zodiac sign (12 paths only)
|
zodiac_sign: Optional[str] = None # Zodiac sign (12 paths only)
|
||||||
colorscale: Optional['Colorscale'] = None # Golden Dawn color scales
|
colorscale: Optional["Colorscale"] = None # Golden Dawn color scales
|
||||||
perfumes: List['Perfume'] = field(default_factory=list)
|
perfumes: List["Perfume"] = field(default_factory=list)
|
||||||
gods: Dict[str, List['God']] = field(default_factory=dict)
|
gods: Dict[str, List["God"]] = field(default_factory=dict)
|
||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 11 <= self.number <= 32:
|
if not 11 <= self.number <= 32:
|
||||||
raise ValueError(f"Path number must be between 11 and 32, got {self.number}")
|
raise ValueError(f"Path number must be between 11 and 32, got {self.number}")
|
||||||
|
|
||||||
def is_elemental_path(self) -> bool:
|
def is_elemental_path(self) -> bool:
|
||||||
"""Check if this is one of the 4 elemental paths."""
|
"""Check if this is one of the 4 elemental paths."""
|
||||||
elemental_numbers = {11, 23, 31, 32} # Aleph, Mem, Shin, 32-bis
|
elemental_numbers = {11, 23, 31, 32} # Aleph, Mem, Shin, 32-bis
|
||||||
return self.number in elemental_numbers
|
return self.number in elemental_numbers
|
||||||
|
|
||||||
def is_planetary_path(self) -> bool:
|
def is_planetary_path(self) -> bool:
|
||||||
"""Check if this path has planetary correspondence."""
|
"""Check if this path has planetary correspondence."""
|
||||||
return self.planet is not None
|
return self.planet is not None
|
||||||
|
|
||||||
def is_zodiacal_path(self) -> bool:
|
def is_zodiacal_path(self) -> bool:
|
||||||
"""Check if this path has zodiac correspondence."""
|
"""Check if this path has zodiac correspondence."""
|
||||||
return self.zodiac_sign is not None
|
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."""
|
"""Attach a god to this path grouped by culture."""
|
||||||
culture_key = god.culture_key()
|
culture_key = god.culture_key()
|
||||||
culture_bucket = self.gods.setdefault(culture_key, [])
|
culture_bucket = self.gods.setdefault(culture_key, [])
|
||||||
if god not in culture_bucket:
|
if god not in culture_bucket:
|
||||||
culture_bucket.append(god)
|
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."""
|
"""Attach a perfume correspondence if it is not already present."""
|
||||||
if perfume not in self.perfumes:
|
if perfume not in self.perfumes:
|
||||||
self.perfumes.append(perfume)
|
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."""
|
"""Return all gods for this path, optionally filtered by culture."""
|
||||||
if culture:
|
if culture:
|
||||||
return list(self.gods.get(culture.lower(), []))
|
return list(self.gods.get(culture.lower(), []))
|
||||||
merged: List['God'] = []
|
merged: List["God"] = []
|
||||||
for values in self.gods.values():
|
for values in self.gods.values():
|
||||||
merged.extend(values)
|
merged.extend(values)
|
||||||
return merged
|
return merged
|
||||||
@@ -132,36 +136,36 @@ class Path:
|
|||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return nicely formatted string representation of the Path."""
|
"""Return nicely formatted string representation of the Path."""
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# Header with path number and letter
|
# Header with path number and letter
|
||||||
lines.append(f"--- Path {self.number}: {self.hebrew_letter} ({self.transliteration}) ---")
|
lines.append(f"--- Path {self.number}: {self.hebrew_letter} ({self.transliteration}) ---")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Basic correspondences
|
# Basic correspondences
|
||||||
lines.append(f"tarot_trump: {self.tarot_trump}")
|
lines.append(f"tarot_trump: {self.tarot_trump}")
|
||||||
|
|
||||||
# Connections
|
# Connections
|
||||||
if self.sephera_from or self.sephera_to:
|
if self.sephera_from or self.sephera_to:
|
||||||
seph_from = self.sephera_from.name if self.sephera_from else "Unknown"
|
seph_from = self.sephera_from.name if self.sephera_from else "Unknown"
|
||||||
seph_to = self.sephera_to.name if self.sephera_to else "Unknown"
|
seph_to = self.sephera_to.name if self.sephera_to else "Unknown"
|
||||||
lines.append(f"connects: {seph_from} ↔ {seph_to}")
|
lines.append(f"connects: {seph_from} ↔ {seph_to}")
|
||||||
|
|
||||||
# Element
|
# Element
|
||||||
if self.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}")
|
lines.append(f"element: {element_name}")
|
||||||
|
|
||||||
# Planet
|
# Planet
|
||||||
if self.planet:
|
if self.planet:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("--- Planet ---")
|
lines.append("--- Planet ---")
|
||||||
for line in str(self.planet).split("\n"):
|
for line in str(self.planet).split("\n"):
|
||||||
lines.append(f" {line}")
|
lines.append(f" {line}")
|
||||||
|
|
||||||
# Zodiac
|
# Zodiac
|
||||||
if self.zodiac_sign:
|
if self.zodiac_sign:
|
||||||
lines.append(f"zodiac_sign: {self.zodiac_sign}")
|
lines.append(f"zodiac_sign: {self.zodiac_sign}")
|
||||||
|
|
||||||
# Colorscale
|
# Colorscale
|
||||||
if self.colorscale:
|
if self.colorscale:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -178,7 +182,7 @@ class Path:
|
|||||||
lines.append(f" keywords: {', '.join(self.colorscale.keywords)}")
|
lines.append(f" keywords: {', '.join(self.colorscale.keywords)}")
|
||||||
if self.colorscale.description:
|
if self.colorscale.description:
|
||||||
lines.append(f" description: {self.colorscale.description}")
|
lines.append(f" description: {self.colorscale.description}")
|
||||||
|
|
||||||
# Perfumes
|
# Perfumes
|
||||||
if self.perfumes:
|
if self.perfumes:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -187,7 +191,7 @@ class Path:
|
|||||||
for line in str(perfume).split("\n"):
|
for line in str(perfume).split("\n"):
|
||||||
lines.append(f" {line}")
|
lines.append(f" {line}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Gods
|
# Gods
|
||||||
if self.gods:
|
if self.gods:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
@@ -198,18 +202,18 @@ class Path:
|
|||||||
for line in str(god).split("\n"):
|
for line in str(god).split("\n"):
|
||||||
lines.append(f" {line}")
|
lines.append(f" {line}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Keywords
|
# Keywords
|
||||||
if self.keywords:
|
if self.keywords:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("--- Keywords ---")
|
lines.append("--- Keywords ---")
|
||||||
lines.append(f" {', '.join(self.keywords)}")
|
lines.append(f" {', '.join(self.keywords)}")
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
if self.description:
|
if self.description:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("--- Description ---")
|
lines.append("--- Description ---")
|
||||||
lines.append(f" {self.description}")
|
lines.append(f" {self.description}")
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Cube namespace - access Cube of Space walls and areas."""
|
"""Cube namespace - access Cube of Space walls and areas."""
|
||||||
|
|
||||||
from .cube import Cube
|
|
||||||
from .attributes import CubeOfSpace, Wall, WallDirection
|
from .attributes import CubeOfSpace, Wall, WallDirection
|
||||||
|
from .cube import Cube
|
||||||
|
|
||||||
__all__ = ["Cube", "CubeOfSpace", "Wall", "WallDirection"]
|
__all__ = ["Cube", "CubeOfSpace", "Wall", "WallDirection"]
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ from typing import Dict, List, Optional
|
|||||||
class WallDirection:
|
class WallDirection:
|
||||||
"""
|
"""
|
||||||
Represents a single direction within a Wall of the Cube of Space.
|
Represents a single direction within a Wall of the Cube of Space.
|
||||||
|
|
||||||
Each wall has 5 directions: North, South, East, West, Center.
|
Each wall has 5 directions: North, South, East, West, Center.
|
||||||
Each direction has a Hebrew letter and zodiac correspondence.
|
Each direction has a Hebrew letter and zodiac correspondence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str # "North", "South", "East", "West", "Center"
|
name: str # "North", "South", "East", "West", "Center"
|
||||||
letter: str # Hebrew letter (e.g., "Aleph", "Bet", etc.)
|
letter: str # Hebrew letter (e.g., "Aleph", "Bet", etc.)
|
||||||
zodiac: Optional[str] = None # Zodiac sign if applicable
|
zodiac: Optional[str] = None # Zodiac sign if applicable
|
||||||
@@ -24,9 +25,9 @@ class WallDirection:
|
|||||||
planet: Optional[str] = None # Associated planet if any
|
planet: Optional[str] = None # Associated planet if any
|
||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
VALID_DIRECTION_NAMES = {"North", "South", "East", "West", "Center"}
|
VALID_DIRECTION_NAMES = {"North", "South", "East", "West", "Center"}
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.name not in self.VALID_DIRECTION_NAMES:
|
if self.name not in self.VALID_DIRECTION_NAMES:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -35,7 +36,7 @@ class WallDirection:
|
|||||||
)
|
)
|
||||||
if not self.letter or not isinstance(self.letter, str):
|
if not self.letter or not isinstance(self.letter, str):
|
||||||
raise ValueError(f"Direction must have a letter, got {self.letter}")
|
raise ValueError(f"Direction must have a letter, got {self.letter}")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Custom repr showing key attributes."""
|
"""Custom repr showing key attributes."""
|
||||||
return f"WallDirection({self.name}, {self.letter})"
|
return f"WallDirection({self.name}, {self.letter})"
|
||||||
@@ -45,12 +46,13 @@ class WallDirection:
|
|||||||
class Wall:
|
class Wall:
|
||||||
"""
|
"""
|
||||||
Represents one of the 6 walls of the Cube of Space.
|
Represents one of the 6 walls of the Cube of Space.
|
||||||
|
|
||||||
Each wall has 5 directions: North, South, East, West, Center.
|
Each wall has 5 directions: North, South, East, West, Center.
|
||||||
The 6 walls are: North, South, East, West, Above, Below.
|
The 6 walls are: North, South, East, West, Above, Below.
|
||||||
Opposite walls: North↔South, East↔West, Above↔Below.
|
Opposite walls: North↔South, East↔West, Above↔Below.
|
||||||
Each direction has a Hebrew letter and zodiac correspondence.
|
Each direction has a Hebrew letter and zodiac correspondence.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str # "North", "South", "East", "West", "Above", "Below"
|
name: str # "North", "South", "East", "West", "Above", "Below"
|
||||||
side: str # Alias for name, used for filtering (e.g., "north", "south")
|
side: str # Alias for name, used for filtering (e.g., "north", "south")
|
||||||
opposite: str # Opposite wall name (e.g., "South" for North wall)
|
opposite: str # Opposite wall name (e.g., "South" for North wall)
|
||||||
@@ -60,9 +62,9 @@ class Wall:
|
|||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
directions: Dict[str, "WallDirection"] = field(default_factory=dict)
|
directions: Dict[str, "WallDirection"] = field(default_factory=dict)
|
||||||
|
|
||||||
VALID_WALL_NAMES = {"North", "South", "East", "West", "Above", "Below"}
|
VALID_WALL_NAMES = {"North", "South", "East", "West", "Above", "Below"}
|
||||||
|
|
||||||
# Opposite wall mappings
|
# Opposite wall mappings
|
||||||
OPPOSITE_WALLS = {
|
OPPOSITE_WALLS = {
|
||||||
"North": "South",
|
"North": "South",
|
||||||
@@ -72,45 +74,43 @@ class Wall:
|
|||||||
"Above": "Below",
|
"Above": "Below",
|
||||||
"Below": "Above",
|
"Below": "Above",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.name not in self.VALID_WALL_NAMES:
|
if self.name not in self.VALID_WALL_NAMES:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid wall name '{self.name}'. "
|
f"Invalid wall name '{self.name}'. "
|
||||||
f"Valid walls: {', '.join(sorted(self.VALID_WALL_NAMES))}"
|
f"Valid walls: {', '.join(sorted(self.VALID_WALL_NAMES))}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate side matches name (case-insensitive)
|
# Validate side matches name (case-insensitive)
|
||||||
if self.side.capitalize() != self.name:
|
if self.side.capitalize() != self.name:
|
||||||
raise ValueError(
|
raise ValueError(f"Wall side '{self.side}' must match name '{self.name}'")
|
||||||
f"Wall side '{self.side}' must match name '{self.name}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate opposite wall
|
# Validate opposite wall
|
||||||
expected_opposite = self.OPPOSITE_WALLS.get(self.name)
|
expected_opposite = self.OPPOSITE_WALLS.get(self.name)
|
||||||
if self.opposite != expected_opposite:
|
if self.opposite != expected_opposite:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Wall '{self.name}' must have opposite '{expected_opposite}', got '{self.opposite}'"
|
f"Wall '{self.name}' must have opposite '{expected_opposite}', got '{self.opposite}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure all 5 directions exist
|
# Ensure all 5 directions exist
|
||||||
if len(self.directions) != 5:
|
if len(self.directions) != 5:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Wall '{self.name}' must have exactly 5 directions (North, South, East, West, Center), "
|
f"Wall '{self.name}' must have exactly 5 directions (North, South, East, West, Center), "
|
||||||
f"got {len(self.directions)}"
|
f"got {len(self.directions)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
required_directions = {"North", "South", "East", "West", "Center"}
|
required_directions = {"North", "South", "East", "West", "Center"}
|
||||||
if set(self.directions.keys()) != required_directions:
|
if set(self.directions.keys()) != required_directions:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Wall '{self.name}' must have directions: {required_directions}, "
|
f"Wall '{self.name}' must have directions: {required_directions}, "
|
||||||
f"got {set(self.directions.keys())}"
|
f"got {set(self.directions.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Custom repr showing wall name and element."""
|
"""Custom repr showing wall name and element."""
|
||||||
return f"Wall({self.name}, {self.element})"
|
return f"Wall({self.name}, {self.element})"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Custom string representation for printing wall details with recursive direction details."""
|
"""Custom string representation for printing wall details with recursive direction details."""
|
||||||
keywords_str = ", ".join(self.keywords) if self.keywords else "None"
|
keywords_str = ", ".join(self.keywords) if self.keywords else "None"
|
||||||
@@ -123,7 +123,7 @@ class Wall:
|
|||||||
f" Archangel: {self.archangel}",
|
f" Archangel: {self.archangel}",
|
||||||
f" Keywords: {keywords_str}",
|
f" Keywords: {keywords_str}",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add directions with their details recursively
|
# Add directions with their details recursively
|
||||||
if self.directions:
|
if self.directions:
|
||||||
lines.append(" Directions:")
|
lines.append(" Directions:")
|
||||||
@@ -145,22 +145,22 @@ class Wall:
|
|||||||
lines.append(f" Keywords: {keywords}")
|
lines.append(f" Keywords: {keywords}")
|
||||||
if direction.description:
|
if direction.description:
|
||||||
lines.append(f" Description: {direction.description}")
|
lines.append(f" Description: {direction.description}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def direction(self, direction_name: str) -> Optional["WallDirection"]:
|
def direction(self, direction_name: str) -> Optional["WallDirection"]:
|
||||||
"""Get a specific direction by name. Usage: wall.direction("North")"""
|
"""Get a specific direction by name. Usage: wall.direction("North")"""
|
||||||
return self.directions.get(direction_name.capitalize())
|
return self.directions.get(direction_name.capitalize())
|
||||||
|
|
||||||
def all_directions(self) -> list:
|
def all_directions(self) -> list:
|
||||||
"""Return all 5 directions as a list."""
|
"""Return all 5 directions as a list."""
|
||||||
return list(self.directions.values())
|
return list(self.directions.values())
|
||||||
|
|
||||||
# Aliases for backward compatibility
|
# Aliases for backward compatibility
|
||||||
def get_direction(self, direction_name: str) -> Optional["WallDirection"]:
|
def get_direction(self, direction_name: str) -> Optional["WallDirection"]:
|
||||||
"""Deprecated: use direction() instead."""
|
"""Deprecated: use direction() instead."""
|
||||||
return self.direction(direction_name)
|
return self.direction(direction_name)
|
||||||
|
|
||||||
def get_opposite_wall_name(self) -> str:
|
def get_opposite_wall_name(self) -> str:
|
||||||
"""Deprecated: use the opposite property instead."""
|
"""Deprecated: use the opposite property instead."""
|
||||||
return self.opposite
|
return self.opposite
|
||||||
@@ -170,16 +170,17 @@ class Wall:
|
|||||||
class CubeOfSpace:
|
class CubeOfSpace:
|
||||||
"""
|
"""
|
||||||
Represents the Cube of Space with 6 walls.
|
Represents the Cube of Space with 6 walls.
|
||||||
|
|
||||||
The Cube of Space is a 3D sacred geometry model consisting of:
|
The Cube of Space is a 3D sacred geometry model consisting of:
|
||||||
- 6 walls (North, South, East, West, Above, Below)
|
- 6 walls (North, South, East, West, Above, Below)
|
||||||
- Each wall contains 5 areas (center, above, below, east, west)
|
- Each wall contains 5 areas (center, above, below, east, west)
|
||||||
- Opposite walls: North↔South, East↔West, Above↔Below
|
- Opposite walls: North↔South, East↔West, Above↔Below
|
||||||
- Total: 30 positions plus central core
|
- Total: 30 positions plus central core
|
||||||
"""
|
"""
|
||||||
|
|
||||||
walls: Dict[str, Wall] = field(default_factory=dict)
|
walls: Dict[str, Wall] = field(default_factory=dict)
|
||||||
center: Optional[WallDirection] = None # Central core position
|
center: Optional[WallDirection] = None # Central core position
|
||||||
|
|
||||||
# Built-in wall definitions with all correspondences
|
# Built-in wall definitions with all correspondences
|
||||||
_WALL_DEFINITIONS = {
|
_WALL_DEFINITIONS = {
|
||||||
"North": {
|
"North": {
|
||||||
@@ -387,28 +388,26 @@ class CubeOfSpace:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""Validate that all 6 walls are present."""
|
"""Validate that all 6 walls are present."""
|
||||||
required_walls = {"North", "South", "East", "West", "Above", "Below"}
|
required_walls = {"North", "South", "East", "West", "Above", "Below"}
|
||||||
if set(self.walls.keys()) != required_walls:
|
if set(self.walls.keys()) != required_walls:
|
||||||
raise ValueError(
|
raise ValueError(f"CubeOfSpace must have all 6 walls, got: {set(self.walls.keys())}")
|
||||||
f"CubeOfSpace must have all 6 walls, got: {set(self.walls.keys())}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_default(cls) -> "CubeOfSpace":
|
def create_default(cls) -> "CubeOfSpace":
|
||||||
"""
|
"""
|
||||||
Create a CubeOfSpace with all 6 walls fully populated with built-in definitions.
|
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 wall has 5 directions (North, South, East, West, Center) positioned on that wall.
|
||||||
Each direction has a Hebrew letter and optional zodiac correspondence.
|
Each direction has a Hebrew letter and optional zodiac correspondence.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
CubeOfSpace: Fully initialized cube with all walls and directions
|
CubeOfSpace: Fully initialized cube with all walls and directions
|
||||||
"""
|
"""
|
||||||
walls = {}
|
walls = {}
|
||||||
|
|
||||||
# Direction name mapping - same 5 directions on every wall
|
# Direction name mapping - same 5 directions on every wall
|
||||||
# Maps old area names to consistent direction names
|
# Maps old area names to consistent direction names
|
||||||
direction_map = {
|
direction_map = {
|
||||||
@@ -416,9 +415,9 @@ class CubeOfSpace:
|
|||||||
"above": {"name": "North", "letter": "Bet", "zodiac": None},
|
"above": {"name": "North", "letter": "Bet", "zodiac": None},
|
||||||
"below": {"name": "South", "letter": "Gimel", "zodiac": None},
|
"below": {"name": "South", "letter": "Gimel", "zodiac": None},
|
||||||
"east": {"name": "East", "letter": "Daleth", "zodiac": "Aries"},
|
"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():
|
for wall_name, wall_data in cls._WALL_DEFINITIONS.items():
|
||||||
# Create directions for this wall
|
# Create directions for this wall
|
||||||
# Each wall has the same 5 directions: North, South, East, West, Center
|
# 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
|
# Use the direction name as key so every wall has North, South, East, West, Center
|
||||||
directions[direction_config["name"]] = direction
|
directions[direction_config["name"]] = direction
|
||||||
|
|
||||||
# Create the wall
|
# Create the wall
|
||||||
wall = Wall(
|
wall = Wall(
|
||||||
name=wall_name,
|
name=wall_name,
|
||||||
@@ -450,7 +449,7 @@ class CubeOfSpace:
|
|||||||
directions=directions,
|
directions=directions,
|
||||||
)
|
)
|
||||||
walls[wall_name] = wall
|
walls[wall_name] = wall
|
||||||
|
|
||||||
# Create central core
|
# Create central core
|
||||||
central_core = WallDirection(
|
central_core = WallDirection(
|
||||||
name="Center",
|
name="Center",
|
||||||
@@ -459,55 +458,55 @@ class CubeOfSpace:
|
|||||||
keywords=["Unity", "Source", "All"],
|
keywords=["Unity", "Source", "All"],
|
||||||
description="Central core of the Cube of Space - synthesis of all forces",
|
description="Central core of the Cube of Space - synthesis of all forces",
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(walls=walls, center=central_core)
|
return cls(walls=walls, center=central_core)
|
||||||
|
|
||||||
def wall(self, wall_name: str) -> Optional[Wall]:
|
def wall(self, wall_name: str) -> Optional[Wall]:
|
||||||
"""Get a wall by name. Usage: cube.wall("north")"""
|
"""Get a wall by name. Usage: cube.wall("north")"""
|
||||||
return self.walls.get(wall_name)
|
return self.walls.get(wall_name)
|
||||||
|
|
||||||
def opposite(self, wall_name: str) -> Optional[Wall]:
|
def opposite(self, wall_name: str) -> Optional[Wall]:
|
||||||
"""Get the opposite wall. Usage: cube.opposite("north")"""
|
"""Get the opposite wall. Usage: cube.opposite("north")"""
|
||||||
opposite_name = Wall.OPPOSITE_WALLS.get(wall_name)
|
opposite_name = Wall.OPPOSITE_WALLS.get(wall_name)
|
||||||
if not opposite_name:
|
if not opposite_name:
|
||||||
return None
|
return None
|
||||||
return self.walls.get(opposite_name)
|
return self.walls.get(opposite_name)
|
||||||
|
|
||||||
def direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
|
def direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
|
||||||
"""Get a specific direction from a specific wall. Usage: cube.direction("north", "center")"""
|
"""Get a specific direction from a specific wall. Usage: cube.direction("north", "center")"""
|
||||||
wall = self.wall(wall_name)
|
wall = self.wall(wall_name)
|
||||||
if not wall:
|
if not wall:
|
||||||
return None
|
return None
|
||||||
return wall.direction(direction_name)
|
return wall.direction(direction_name)
|
||||||
|
|
||||||
def walls_all(self) -> List[Wall]:
|
def walls_all(self) -> List[Wall]:
|
||||||
"""Return all 6 walls as a list."""
|
"""Return all 6 walls as a list."""
|
||||||
return list(self.walls.values())
|
return list(self.walls.values())
|
||||||
|
|
||||||
def directions(self, wall_name: str) -> list:
|
def directions(self, wall_name: str) -> list:
|
||||||
"""Return all 5 directions for a specific wall. Usage: cube.directions("north")"""
|
"""Return all 5 directions for a specific wall. Usage: cube.directions("north")"""
|
||||||
wall = self.wall(wall_name)
|
wall = self.wall(wall_name)
|
||||||
if not wall:
|
if not wall:
|
||||||
return []
|
return []
|
||||||
return wall.all_directions()
|
return wall.all_directions()
|
||||||
|
|
||||||
# Aliases for backward compatibility
|
# Aliases for backward compatibility
|
||||||
def get_wall(self, wall_name: str) -> Optional[Wall]:
|
def get_wall(self, wall_name: str) -> Optional[Wall]:
|
||||||
"""Deprecated: use wall() instead."""
|
"""Deprecated: use wall() instead."""
|
||||||
return self.wall(wall_name)
|
return self.wall(wall_name)
|
||||||
|
|
||||||
def get_direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
|
def get_direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
|
||||||
"""Deprecated: use direction() instead."""
|
"""Deprecated: use direction() instead."""
|
||||||
return self.direction(wall_name, direction_name)
|
return self.direction(wall_name, direction_name)
|
||||||
|
|
||||||
def get_opposite_wall(self, wall_name: str) -> Optional[Wall]:
|
def get_opposite_wall(self, wall_name: str) -> Optional[Wall]:
|
||||||
"""Deprecated: use opposite() instead."""
|
"""Deprecated: use opposite() instead."""
|
||||||
return self.opposite(wall_name)
|
return self.opposite(wall_name)
|
||||||
|
|
||||||
def all_walls(self) -> List[Wall]:
|
def all_walls(self) -> List[Wall]:
|
||||||
"""Deprecated: use walls_all() instead."""
|
"""Deprecated: use walls_all() instead."""
|
||||||
return self.walls_all()
|
return self.walls_all()
|
||||||
|
|
||||||
def all_directions_for_wall(self, wall_name: str) -> list:
|
def all_directions_for_wall(self, wall_name: str) -> list:
|
||||||
"""Deprecated: use directions() instead."""
|
"""Deprecated: use directions() instead."""
|
||||||
return self.directions(wall_name)
|
return self.directions(wall_name)
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ Provides hierarchical access to Cube > Wall > Direction structure.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot.cube import Cube
|
from tarot.cube import Cube
|
||||||
|
|
||||||
# Access walls
|
# Access walls
|
||||||
Tarot.cube.wall("North") # Get specific wall
|
Tarot.cube.wall("North") # Get specific wall
|
||||||
Tarot.cube.wall().filter(element="Air") # Filter all walls
|
Tarot.cube.wall().filter(element="Air") # Filter all walls
|
||||||
|
|
||||||
# Access directions (NEW - replaces old "area" concept)
|
# Access directions (NEW - replaces old "area" concept)
|
||||||
wall = Tarot.cube.wall("North")
|
wall = Tarot.cube.wall("North")
|
||||||
wall.filter("East") # Filter by direction
|
wall.filter("East") # Filter by direction
|
||||||
@@ -17,19 +17,22 @@ Usage:
|
|||||||
wall.direction("East") # Get specific direction
|
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):
|
class CubeMeta(type):
|
||||||
"""Metaclass to add __str__ to Cube class itself."""
|
"""Metaclass to add __str__ to Cube class itself."""
|
||||||
|
|
||||||
def __str__(cls) -> str:
|
def __str__(cls) -> str:
|
||||||
"""Return readable representation when Cube is converted to string."""
|
"""Return readable representation when Cube is converted to string."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
if cls._cube is None:
|
if cls._cube is None:
|
||||||
return "Cube of Space (not initialized)"
|
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 = [
|
lines = [
|
||||||
"Cube of Space",
|
"Cube of Space",
|
||||||
"=" * 60,
|
"=" * 60,
|
||||||
@@ -37,23 +40,23 @@ class CubeMeta(type):
|
|||||||
"",
|
"",
|
||||||
"Structure:",
|
"Structure:",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Show walls with their elements and areas
|
# Show walls with their elements and areas
|
||||||
for wall_name in ["North", "South", "East", "West", "Above", "Below"]:
|
for wall_name in ["North", "South", "East", "West", "Above", "Below"]:
|
||||||
wall = walls.get(wall_name)
|
wall = walls.get(wall_name)
|
||||||
if wall:
|
if wall:
|
||||||
element = f" [{wall.element}]" if hasattr(wall, 'element') else ""
|
element = f" [{wall.element}]" if hasattr(wall, "element") else ""
|
||||||
areas = len(wall.directions) if hasattr(wall, 'directions') else 0
|
areas = len(wall.directions) if hasattr(wall, "directions") else 0
|
||||||
lines.append(f" {wall_name}{element}: {areas} areas")
|
lines.append(f" {wall_name}{element}: {areas} areas")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __repr__(cls) -> str:
|
def __repr__(cls) -> str:
|
||||||
"""Return object representation."""
|
"""Return object representation."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
if cls._cube is None:
|
if cls._cube is None:
|
||||||
return "Cube(not initialized)"
|
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)})"
|
return f"Cube(walls={len(walls)})"
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +71,7 @@ class DirectionAccessor:
|
|||||||
|
|
||||||
def all(self) -> list:
|
def all(self) -> list:
|
||||||
"""Get all directions in this wall."""
|
"""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 []
|
||||||
return list(self._wall.directions.values())
|
return list(self._wall.directions.values())
|
||||||
|
|
||||||
@@ -89,10 +92,7 @@ class DirectionAccessor:
|
|||||||
|
|
||||||
# Filter by direction name if provided
|
# Filter by direction name if provided
|
||||||
if direction_name:
|
if direction_name:
|
||||||
all_dirs = [
|
all_dirs = [d for d in all_dirs if d.name.lower() == direction_name.lower()]
|
||||||
d for d in all_dirs
|
|
||||||
if d.name.lower() == direction_name.lower()
|
|
||||||
]
|
|
||||||
|
|
||||||
# Apply other filters
|
# Apply other filters
|
||||||
if kwargs:
|
if kwargs:
|
||||||
@@ -126,7 +126,7 @@ class DirectionAccessor:
|
|||||||
"""Get specific direction by name."""
|
"""Get specific direction by name."""
|
||||||
if direction_name is None:
|
if direction_name is None:
|
||||||
return self.all()
|
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 None
|
||||||
return self._wall.directions.get(direction_name.capitalize())
|
return self._wall.directions.get(direction_name.capitalize())
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class WallWrapper:
|
|||||||
|
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
"""Delegate attribute access to the wrapped wall."""
|
"""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 object.__getattribute__(self, name)
|
||||||
return getattr(self._wall, name)
|
return getattr(self._wall, name)
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ class WallAccessor:
|
|||||||
|
|
||||||
def __call__(self, wall_name: Optional[str] = None) -> Optional[Any]:
|
def __call__(self, wall_name: Optional[str] = None) -> Optional[Any]:
|
||||||
"""Get a specific wall by name or return all walls.
|
"""Get a specific wall by name or return all walls.
|
||||||
|
|
||||||
Deprecated: Use filter(side="north") instead.
|
Deprecated: Use filter(side="north") instead.
|
||||||
"""
|
"""
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
@@ -307,10 +307,10 @@ class Cube(metaclass=CubeMeta):
|
|||||||
# Filter walls by side
|
# Filter walls by side
|
||||||
north = Cube.wall.filter(side="north")[0] # Get north wall
|
north = Cube.wall.filter(side="north")[0] # Get north wall
|
||||||
air_walls = Cube.wall.filter(element="Air") # Filter by element
|
air_walls = Cube.wall.filter(element="Air") # Filter by element
|
||||||
|
|
||||||
# Access all walls
|
# Access all walls
|
||||||
all_walls = Cube.wall.all() # Get all 6 walls
|
all_walls = Cube.wall.all() # Get all 6 walls
|
||||||
|
|
||||||
# Work with directions within a wall
|
# Work with directions within a wall
|
||||||
wall = Cube.wall.filter(side="north")[0]
|
wall = Cube.wall.filter(side="north")[0]
|
||||||
east_dir = wall.direction("East") # Get direction
|
east_dir = wall.direction("East") # Get direction
|
||||||
@@ -339,15 +339,16 @@ class Cube(metaclass=CubeMeta):
|
|||||||
if cls._wall_accessor is None:
|
if cls._wall_accessor is None:
|
||||||
cls._wall_accessor = WallAccessor()
|
cls._wall_accessor = WallAccessor()
|
||||||
return cls._wall_accessor
|
return cls._wall_accessor
|
||||||
|
|
||||||
# Use a descriptor to make wall work like a property on the class
|
# Use a descriptor to make wall work like a property on the class
|
||||||
class WallProperty:
|
class WallProperty:
|
||||||
"""Descriptor that returns wall accessor when accessed."""
|
"""Descriptor that returns wall accessor when accessed."""
|
||||||
|
|
||||||
def __get__(self, obj: Any, objtype: Optional[type] = None) -> "WallAccessor":
|
def __get__(self, obj: Any, objtype: Optional[type] = None) -> "WallAccessor":
|
||||||
if objtype is None:
|
if objtype is None:
|
||||||
objtype = type(obj)
|
objtype = type(obj)
|
||||||
return objtype._get_wall_accessor()
|
return objtype._get_wall_accessor()
|
||||||
|
|
||||||
wall = WallProperty()
|
wall = WallProperty()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -5,31 +5,31 @@ Provides access to Sephiroth, Paths, and Tree of Life correspondences.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot.tree import Tree
|
from tarot.tree import Tree
|
||||||
|
|
||||||
sephera = Tree.sephera(1) # Get Sephira 1 (Kether)
|
sephera = Tree.sephera(1) # Get Sephira 1 (Kether)
|
||||||
path = Tree.path(11) # Get Path 11
|
path = Tree.path(11) # Get Path 11
|
||||||
all_sepheras = Tree.sephera() # Get all Sephiroth
|
all_sepheras = Tree.sephera() # Get all Sephiroth
|
||||||
|
|
||||||
print(Tree()) # Display Tree structure
|
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:
|
if TYPE_CHECKING:
|
||||||
from tarot.attributes import Sephera, Path
|
from tarot.attributes import Path, Sephera
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
from utils.query import QueryResult, Query
|
from utils.query import Query
|
||||||
|
|
||||||
|
|
||||||
class TreeMeta(type):
|
class TreeMeta(type):
|
||||||
"""Metaclass to add __str__ to Tree class itself."""
|
"""Metaclass to add __str__ to Tree class itself."""
|
||||||
|
|
||||||
def __str__(cls) -> str:
|
def __str__(cls) -> str:
|
||||||
"""Return readable representation when Tree is converted to string."""
|
"""Return readable representation when Tree is converted to string."""
|
||||||
# Access Tree class attributes through type.__getattribute__
|
# Access Tree class attributes through type.__getattribute__
|
||||||
Tree._ensure_initialized()
|
Tree._ensure_initialized()
|
||||||
sepheras = type.__getattribute__(cls, '_sepheras')
|
sepheras = type.__getattribute__(cls, "_sepheras")
|
||||||
paths = type.__getattribute__(cls, '_paths')
|
paths = type.__getattribute__(cls, "_paths")
|
||||||
lines = [
|
lines = [
|
||||||
"Tree of Life",
|
"Tree of Life",
|
||||||
"=" * 60,
|
"=" * 60,
|
||||||
@@ -38,99 +38,99 @@ class TreeMeta(type):
|
|||||||
"",
|
"",
|
||||||
"Structure:",
|
"Structure:",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Show Sephira hierarchy
|
# Show Sephira hierarchy
|
||||||
for num in sorted(sepheras.keys()):
|
for num in sorted(sepheras.keys()):
|
||||||
seph = sepheras[num]
|
seph = sepheras[num]
|
||||||
lines.append(f" {num}. {seph.name} ({seph.hebrew_name})")
|
lines.append(f" {num}. {seph.name} ({seph.hebrew_name})")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __repr__(cls) -> str:
|
def __repr__(cls) -> str:
|
||||||
"""Return object representation."""
|
"""Return object representation."""
|
||||||
Tree._ensure_initialized()
|
Tree._ensure_initialized()
|
||||||
sepheras = type.__getattribute__(cls, '_sepheras')
|
sepheras = type.__getattribute__(cls, "_sepheras")
|
||||||
paths = type.__getattribute__(cls, '_paths')
|
paths = type.__getattribute__(cls, "_paths")
|
||||||
return f"Tree(sepheras={len(sepheras)}, paths={len(paths)})"
|
return f"Tree(sepheras={len(sepheras)}, paths={len(paths)})"
|
||||||
|
|
||||||
|
|
||||||
class Tree(metaclass=TreeMeta):
|
class Tree(metaclass=TreeMeta):
|
||||||
"""
|
"""
|
||||||
Unified accessor for Tree of Life correspondences.
|
Unified accessor for Tree of Life correspondences.
|
||||||
|
|
||||||
All methods are class methods, so Tree is accessed as a static namespace:
|
All methods are class methods, so Tree is accessed as a static namespace:
|
||||||
|
|
||||||
sephera = Tree.sephera(1)
|
sephera = Tree.sephera(1)
|
||||||
path = Tree.path(11)
|
path = Tree.path(11)
|
||||||
print(Tree()) # Displays tree structure
|
print(Tree()) # Displays tree structure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_sepheras: Dict[int, 'Sephera'] = {} # type: ignore
|
_sepheras: Dict[int, "Sephera"] = {} # type: ignore
|
||||||
_paths: Dict[int, 'Path'] = {} # type: ignore
|
_paths: Dict[int, "Path"] = {} # type: ignore
|
||||||
_initialized: bool = False
|
_initialized: bool = False
|
||||||
_loader: Optional['CardDataLoader'] = None # type: ignore
|
_loader: Optional["CardDataLoader"] = None # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _ensure_initialized(cls) -> None:
|
def _ensure_initialized(cls) -> None:
|
||||||
"""Lazy-load data from CardDataLoader on first access."""
|
"""Lazy-load data from CardDataLoader on first access."""
|
||||||
if cls._initialized:
|
if cls._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
|
|
||||||
cls._loader = CardDataLoader()
|
cls._loader = CardDataLoader()
|
||||||
cls._sepheras = cls._loader._sephera
|
cls._sepheras = cls._loader._sephera
|
||||||
cls._paths = cls._loader._paths
|
cls._paths = cls._loader._paths
|
||||||
cls._initialized = True
|
cls._initialized = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def sephera(cls, number: int) -> Optional['Sephera']:
|
def sephera(cls, number: int) -> Optional["Sephera"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def sephera(cls, number: None = ...) -> Dict[int, 'Sephera']:
|
def sephera(cls, number: None = ...) -> Dict[int, "Sephera"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return a Sephira or all Sephiroth."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
if number is None:
|
if number is None:
|
||||||
return cls._sepheras.copy()
|
return cls._sepheras.copy()
|
||||||
return cls._sepheras.get(number)
|
return cls._sepheras.get(number)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def path(cls, number: int) -> Optional['Path']:
|
def path(cls, number: int) -> Optional["Path"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def path(cls, number: None = ...) -> Dict[int, 'Path']:
|
def path(cls, number: None = ...) -> Dict[int, "Path"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return a Path or all Paths."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
if number is None:
|
if number is None:
|
||||||
return cls._paths.copy()
|
return cls._paths.copy()
|
||||||
return cls._paths.get(number)
|
return cls._paths.get(number)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter(cls, expression: str) -> 'Query':
|
def filter(cls, expression: str) -> "Query":
|
||||||
"""
|
"""
|
||||||
Filter Sephiroth by attribute:value expression.
|
Filter Sephiroth by attribute:value expression.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
Tree.filter('name:Kether').first()
|
Tree.filter('name:Kether').first()
|
||||||
Tree.filter('number:1').first()
|
Tree.filter('number:1').first()
|
||||||
Tree.filter('sphere:1').all()
|
Tree.filter('sphere:1').all()
|
||||||
|
|
||||||
Returns a Query object for chaining.
|
Returns a Query object for chaining.
|
||||||
"""
|
"""
|
||||||
from tarot.query import Query
|
from tarot.query import Query
|
||||||
|
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
# Create a query from all Sephiroth
|
# Create a query from all Sephiroth
|
||||||
return Query(cls._sepheras).filter(expression)
|
return Query(cls._sepheras).filter(expression)
|
||||||
|
|||||||
@@ -11,17 +11,16 @@ Provides fluent query interface for:
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import letter
|
from tarot import letter
|
||||||
|
|
||||||
letter.alphabet('english')
|
letter.alphabet('english')
|
||||||
letter.words.word('MAGICK').cipher('english_simple')
|
letter.words.word('MAGICK').cipher('english_simple')
|
||||||
letter.iching.hexagram(1)
|
letter.iching.hexagram(1)
|
||||||
letter.paths('aleph') # Get Hebrew letter with Tarot correspondences
|
letter.paths('aleph') # Get Hebrew letter with Tarot correspondences
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from .iChing import hexagram, trigram
|
||||||
from .letter import letter
|
from .letter import letter
|
||||||
from .iChing import trigram, hexagram
|
|
||||||
from .words import word
|
|
||||||
from .paths import letters
|
from .paths import letters
|
||||||
|
from .words import word
|
||||||
|
|
||||||
__all__ = ["letter", "trigram", "hexagram", "word", "letters"]
|
__all__ = ["letter", "trigram", "hexagram", "word", "letters"]
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ including Alphabets, Enochian letters, and Double Letter Trumps.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
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 (
|
from utils.attributes import (
|
||||||
Element,
|
|
||||||
ElementType,
|
ElementType,
|
||||||
Planet,
|
|
||||||
Meaning,
|
Meaning,
|
||||||
|
Planet,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Letter:
|
class Letter:
|
||||||
"""Represents a letter with its attributes."""
|
"""Represents a letter with its attributes."""
|
||||||
|
|
||||||
character: str
|
character: str
|
||||||
position: int
|
position: int
|
||||||
name: str
|
name: str
|
||||||
@@ -27,10 +27,11 @@ class Letter:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class EnglishAlphabet:
|
class EnglishAlphabet:
|
||||||
"""English alphabet with Tarot/Kabbalistic correspondence."""
|
"""English alphabet with Tarot/Kabbalistic correspondence."""
|
||||||
|
|
||||||
letter: str
|
letter: str
|
||||||
position: int
|
position: int
|
||||||
sound: str
|
sound: str
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not (1 <= self.position <= 26):
|
if not (1 <= self.position <= 26):
|
||||||
raise ValueError(f"Position must be between 1 and 26, got {self.position}")
|
raise ValueError(f"Position must be between 1 and 26, got {self.position}")
|
||||||
@@ -41,10 +42,11 @@ class EnglishAlphabet:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class GreekAlphabet:
|
class GreekAlphabet:
|
||||||
"""Greek alphabet with Tarot/Kabbalistic correspondence."""
|
"""Greek alphabet with Tarot/Kabbalistic correspondence."""
|
||||||
|
|
||||||
letter: str
|
letter: str
|
||||||
position: int
|
position: int
|
||||||
transliteration: str
|
transliteration: str
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not (1 <= self.position <= 24):
|
if not (1 <= self.position <= 24):
|
||||||
raise ValueError(f"Position must be between 1 and 24, got {self.position}")
|
raise ValueError(f"Position must be between 1 and 24, got {self.position}")
|
||||||
@@ -53,11 +55,12 @@ class GreekAlphabet:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class HebrewAlphabet:
|
class HebrewAlphabet:
|
||||||
"""Hebrew alphabet with Tarot/Kabbalistic correspondence."""
|
"""Hebrew alphabet with Tarot/Kabbalistic correspondence."""
|
||||||
|
|
||||||
letter: str
|
letter: str
|
||||||
position: int
|
position: int
|
||||||
transliteration: str
|
transliteration: str
|
||||||
meaning: str
|
meaning: str
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not (1 <= self.position <= 22):
|
if not (1 <= self.position <= 22):
|
||||||
raise ValueError(f"Position must be between 1 and 22, got {self.position}")
|
raise ValueError(f"Position must be between 1 and 22, got {self.position}")
|
||||||
@@ -66,19 +69,20 @@ class HebrewAlphabet:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class DoublLetterTrump:
|
class DoublLetterTrump:
|
||||||
"""Represents a Double Letter Trump (Yodh through Tau, 3-21 of Major Arcana)."""
|
"""Represents a Double Letter Trump (Yodh through Tau, 3-21 of Major Arcana)."""
|
||||||
|
|
||||||
number: int # 3-21 (19 double letter trumps)
|
number: int # 3-21 (19 double letter trumps)
|
||||||
name: str # Full name (e.g., "The Empress")
|
name: str # Full name (e.g., "The Empress")
|
||||||
hebrew_letter_1: str # First Hebrew letter (e.g., "Gimel")
|
hebrew_letter_1: str # First Hebrew letter (e.g., "Gimel")
|
||||||
hebrew_letter_2: Optional[str] = None # Second Hebrew letter if applicable
|
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"
|
tarot_trump: Optional[str] = None # e.g., "III - The Empress"
|
||||||
astrological_sign: Optional[str] = None # Zodiac sign if any
|
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
|
number_value: Optional[int] = None # Numerological value
|
||||||
keywords: List[str] = field(default_factory=list)
|
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 = ""
|
description: str = ""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 3 <= self.number <= 21:
|
if not 3 <= self.number <= 21:
|
||||||
raise ValueError(f"Double Letter Trump number must be 3-21, got {self.number}")
|
raise ValueError(f"Double Letter Trump number must be 3-21, got {self.number}")
|
||||||
@@ -87,6 +91,7 @@ class DoublLetterTrump:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class EnochianLetter:
|
class EnochianLetter:
|
||||||
"""Represents an Enochian letter with its properties."""
|
"""Represents an Enochian letter with its properties."""
|
||||||
|
|
||||||
name: str # Enochian letter name
|
name: str # Enochian letter name
|
||||||
letter: str # The letter itself
|
letter: str # The letter itself
|
||||||
hebrew_equivalent: Optional[str] = None
|
hebrew_equivalent: Optional[str] = None
|
||||||
@@ -100,6 +105,7 @@ class EnochianLetter:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class EnochianSpirit:
|
class EnochianSpirit:
|
||||||
"""Represents an Enochian spirit or intelligence."""
|
"""Represents an Enochian spirit or intelligence."""
|
||||||
|
|
||||||
name: str # Spirit name
|
name: str # Spirit name
|
||||||
rank: str # e.g., "King", "Prince", "Duke", "Intelligence"
|
rank: str # e.g., "King", "Prince", "Duke", "Intelligence"
|
||||||
element: Optional[str] = None
|
element: Optional[str] = None
|
||||||
@@ -113,30 +119,33 @@ class EnochianSpirit:
|
|||||||
class EnochianArchetype:
|
class EnochianArchetype:
|
||||||
"""
|
"""
|
||||||
Archetypal form of an Enochian Tablet.
|
Archetypal form of an Enochian Tablet.
|
||||||
|
|
||||||
Provides a 4x4 grid with positions that can be filled with different
|
Provides a 4x4 grid with positions that can be filled with different
|
||||||
visual representations (colors, images, symbols, etc.).
|
visual representations (colors, images, symbols, etc.).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str # e.g., "Tablet of Air Archetype"
|
name: str # e.g., "Tablet of Air Archetype"
|
||||||
tablet_name: str # Reference to parent tablet
|
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)
|
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)
|
keywords: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
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)."""
|
"""Get the grid position at (row, col)."""
|
||||||
if not 0 <= row < 4 or not 0 <= col < 4:
|
if not 0 <= row < 4 or not 0 <= col < 4:
|
||||||
return None
|
return None
|
||||||
return self.grid.get((row, col))
|
return self.grid.get((row, col))
|
||||||
|
|
||||||
def get_row_correspondence(self, row: int) -> Optional[Dict[str, Any]]:
|
def get_row_correspondence(self, row: int) -> Optional[Dict[str, Any]]:
|
||||||
"""Get the meaning/correspondence for a row."""
|
"""Get the meaning/correspondence for a row."""
|
||||||
if 0 <= row < len(self.row_correspondences):
|
if 0 <= row < len(self.row_correspondences):
|
||||||
return self.row_correspondences[row]
|
return self.row_correspondences[row]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_col_correspondence(self, col: int) -> Optional[Dict[str, Any]]:
|
def get_col_correspondence(self, col: int) -> Optional[Dict[str, Any]]:
|
||||||
"""Get the meaning/correspondence for a column."""
|
"""Get the meaning/correspondence for a column."""
|
||||||
if 0 <= col < len(self.col_correspondences):
|
if 0 <= col < len(self.col_correspondences):
|
||||||
@@ -148,12 +157,13 @@ class EnochianArchetype:
|
|||||||
class EnochianGridPosition:
|
class EnochianGridPosition:
|
||||||
"""
|
"""
|
||||||
Represents a single position in an Enochian Tablet grid.
|
Represents a single position in an Enochian Tablet grid.
|
||||||
|
|
||||||
A 4x4 grid cell with:
|
A 4x4 grid cell with:
|
||||||
- Center letter
|
- Center letter
|
||||||
- Directional letters (N, S, E, W)
|
- Directional letters (N, S, E, W)
|
||||||
- Archetypal correspondences (Tarot, element, etc.)
|
- Archetypal correspondences (Tarot, element, etc.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
row: int # Grid row (0-3)
|
row: int # Grid row (0-3)
|
||||||
col: int # Grid column (0-3)
|
col: int # Grid column (0-3)
|
||||||
center_letter: str # The main letter at this position
|
center_letter: str # The main letter at this position
|
||||||
@@ -169,7 +179,7 @@ class EnochianGridPosition:
|
|||||||
planetary_hour: Optional[str] = None # Associated hour
|
planetary_hour: Optional[str] = None # Associated hour
|
||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
meanings: List[str] = field(default_factory=list)
|
meanings: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
def get_all_letters(self) -> Dict[str, str]:
|
def get_all_letters(self) -> Dict[str, str]:
|
||||||
"""Get all letters in this position: center and directional."""
|
"""Get all letters in this position: center and directional."""
|
||||||
letters = {"center": self.center_letter}
|
letters = {"center": self.center_letter}
|
||||||
@@ -188,24 +198,27 @@ class EnochianGridPosition:
|
|||||||
class EnochianTablet:
|
class EnochianTablet:
|
||||||
"""
|
"""
|
||||||
Represents an Enochian Tablet.
|
Represents an Enochian Tablet.
|
||||||
|
|
||||||
The Enochian system contains:
|
The Enochian system contains:
|
||||||
- 4 elemental tablets (Earth, Water, Air, Fire)
|
- 4 elemental tablets (Earth, Water, Air, Fire)
|
||||||
- 1 tablet of union (Aethyr)
|
- 1 tablet of union (Aethyr)
|
||||||
- Each tablet is 12x12 (144 squares) containing Enochian letters
|
- Each tablet is 12x12 (144 squares) containing Enochian letters
|
||||||
- Archetypal form with 4x4 grid for user customization
|
- Archetypal form with 4x4 grid for user customization
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str # e.g., "Tablet of Earth", "Tablet of Air", etc.
|
name: str # e.g., "Tablet of Earth", "Tablet of Air", etc.
|
||||||
number: int # Tablet identifier (1-5)
|
number: int # Tablet identifier (1-5)
|
||||||
element: Optional[str] = None # Earth, Water, Air, Fire, or Aethyr/Union
|
element: Optional[str] = None # Earth, Water, Air, Fire, or Aethyr/Union
|
||||||
rulers: List[str] = field(default_factory=list) # Names of spirits ruling this tablet
|
rulers: List[str] = field(default_factory=list) # Names of spirits ruling this tablet
|
||||||
archangels: List[str] = field(default_factory=list) # Associated archangels
|
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
|
planetary_hours: List[str] = field(default_factory=list) # Associated hours
|
||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
archetype: Optional[EnochianArchetype] = None # Archetypal form for visualization
|
archetype: Optional[EnochianArchetype] = None # Archetypal form for visualization
|
||||||
|
|
||||||
# Valid tablets
|
# Valid tablets
|
||||||
VALID_TABLETS = {
|
VALID_TABLETS = {
|
||||||
"Tablet of Union", # Aethyr
|
"Tablet of Union", # Aethyr
|
||||||
@@ -214,12 +227,11 @@ class EnochianTablet:
|
|||||||
"Tablet of Air",
|
"Tablet of Air",
|
||||||
"Tablet of Fire",
|
"Tablet of Fire",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.name not in self.VALID_TABLETS:
|
if self.name not in self.VALID_TABLETS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid tablet '{self.name}'. "
|
f"Invalid tablet '{self.name}'. " f"Valid tablets: {', '.join(self.VALID_TABLETS)}"
|
||||||
f"Valid tablets: {', '.join(self.VALID_TABLETS)}"
|
|
||||||
)
|
)
|
||||||
# Tablet of Union uses 0, elemental tablets use 1-5
|
# Tablet of Union uses 0, elemental tablets use 1-5
|
||||||
valid_range = (0, 0) if "Union" in self.name else (1, 5)
|
valid_range = (0, 0) if "Union" in self.name else (1, 5)
|
||||||
@@ -227,23 +239,23 @@ class EnochianTablet:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Tablet number must be {valid_range[0]}-{valid_range[1]}, got {self.number}"
|
f"Tablet number must be {valid_range[0]}-{valid_range[1]}, got {self.number}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_elemental(self) -> bool:
|
def is_elemental(self) -> bool:
|
||||||
"""Check if this is an elemental tablet (not union)."""
|
"""Check if this is an elemental tablet (not union)."""
|
||||||
return self.element in {"Earth", "Water", "Air", "Fire"}
|
return self.element in {"Earth", "Water", "Air", "Fire"}
|
||||||
|
|
||||||
def is_union(self) -> bool:
|
def is_union(self) -> bool:
|
||||||
"""Check if this is the Tablet of Union (Aethyr)."""
|
"""Check if this is the Tablet of Union (Aethyr)."""
|
||||||
return self.element == "Aethyr" or "Union" in self.name
|
return self.element == "Aethyr" or "Union" in self.name
|
||||||
|
|
||||||
def get_letter(self, row: int, col: int) -> Optional[str]:
|
def get_letter(self, row: int, col: int) -> Optional[str]:
|
||||||
"""Get letter at specific grid position."""
|
"""Get letter at specific grid position."""
|
||||||
return self.letters.get((row, col))
|
return self.letters.get((row, col))
|
||||||
|
|
||||||
def get_row(self, row: int) -> List[Optional[str]]:
|
def get_row(self, row: int) -> List[Optional[str]]:
|
||||||
"""Get all letters in a row."""
|
"""Get all letters in a row."""
|
||||||
return [self.letters.get((row, col)) for col in range(12)]
|
return [self.letters.get((row, col)) for col in range(12)]
|
||||||
|
|
||||||
def get_column(self, col: int) -> List[Optional[str]]:
|
def get_column(self, col: int) -> List[Optional[str]]:
|
||||||
"""Get all letters in a column."""
|
"""Get all letters in a column."""
|
||||||
return [self.letters.get((row, col)) for row in range(12)]
|
return [self.letters.get((row, col)) for row in range(12)]
|
||||||
|
|||||||
@@ -5,18 +5,17 @@ including Tarot correspondences and binary representations.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from letter.iChing import trigram, hexagram
|
from letter.iChing import trigram, hexagram
|
||||||
|
|
||||||
qian = trigram.trigram('Qian')
|
qian = trigram.trigram('Qian')
|
||||||
creative = hexagram.hexagram(1)
|
creative = hexagram.hexagram(1)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Dict, Optional
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
from utils.query import CollectionAccessor
|
from utils.query import CollectionAccessor
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.attributes import Hexagram, Trigram
|
||||||
from tarot.attributes import Trigram, Hexagram
|
|
||||||
|
|
||||||
|
|
||||||
def _line_diagram_from_binary(binary: str) -> str:
|
def _line_diagram_from_binary(binary: str) -> str:
|
||||||
@@ -36,14 +35,14 @@ class _Trigram:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._initialized: bool = False
|
self._initialized: bool = False
|
||||||
self._trigrams: Dict[str, 'Trigram'] = {}
|
self._trigrams: Dict[str, "Trigram"] = {}
|
||||||
self.trigram = CollectionAccessor(self._get_trigrams)
|
self.trigram = CollectionAccessor(self._get_trigrams)
|
||||||
|
|
||||||
def _ensure_initialized(self) -> None:
|
def _ensure_initialized(self) -> None:
|
||||||
"""Load trigrams on first access."""
|
"""Load trigrams on first access."""
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._load_trigrams()
|
self._load_trigrams()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
@@ -54,16 +53,80 @@ class _Trigram:
|
|||||||
def _load_trigrams(self) -> None:
|
def _load_trigrams(self) -> None:
|
||||||
"""Load the eight I Ching trigrams."""
|
"""Load the eight I Ching trigrams."""
|
||||||
from tarot.attributes import Trigram
|
from tarot.attributes import Trigram
|
||||||
|
|
||||||
trigram_specs = [
|
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": "Qian",
|
||||||
{"name": "Li", "chinese": "离", "pinyin": "Lí", "element": "Fire", "attribute": "Clinging", "binary": "101", "description": "Radiant clarity that adheres to insight."},
|
"chinese": "乾",
|
||||||
{"name": "Zhen", "chinese": "震", "pinyin": "Zhèn", "element": "Thunder", "attribute": "Arousing", "binary": "001", "description": "Sudden awakening that shakes stagnation."},
|
"pinyin": "Qián",
|
||||||
{"name": "Xun", "chinese": "巽", "pinyin": "Xùn", "element": "Wind", "attribute": "Gentle", "binary": "110", "description": "Penetrating influence that persuades subtly."},
|
"element": "Heaven",
|
||||||
{"name": "Kan", "chinese": "坎", "pinyin": "Kǎn", "element": "Water", "attribute": "Abysmal", "binary": "010", "description": "Depth, risk, and sincere feeling."},
|
"attribute": "Creative",
|
||||||
{"name": "Gen", "chinese": "艮", "pinyin": "Gèn", "element": "Mountain", "attribute": "Stillness", "binary": "100", "description": "Grounded rest that establishes boundaries."},
|
"binary": "111",
|
||||||
{"name": "Kun", "chinese": "坤", "pinyin": "Kūn", "element": "Earth", "attribute": "Receptive", "binary": "000", "description": "Vast receptivity that nurtures form."},
|
"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 = {}
|
self._trigrams = {}
|
||||||
for spec in trigram_specs:
|
for spec in trigram_specs:
|
||||||
@@ -88,14 +151,14 @@ class _Hexagram:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._initialized: bool = False
|
self._initialized: bool = False
|
||||||
self._hexagrams: Dict[int, 'Hexagram'] = {}
|
self._hexagrams: Dict[int, "Hexagram"] = {}
|
||||||
self.hexagram = CollectionAccessor(self._get_hexagrams)
|
self.hexagram = CollectionAccessor(self._get_hexagrams)
|
||||||
|
|
||||||
def _ensure_initialized(self) -> None:
|
def _ensure_initialized(self) -> None:
|
||||||
"""Load hexagrams on first access."""
|
"""Load hexagrams on first access."""
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._load_hexagrams()
|
self._load_hexagrams()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
@@ -107,78 +170,718 @@ class _Hexagram:
|
|||||||
"""Load all 64 I Ching hexagrams."""
|
"""Load all 64 I Ching hexagrams."""
|
||||||
from tarot.attributes import Hexagram
|
from tarot.attributes import Hexagram
|
||||||
from tarot.card.data import CardDataLoader, calculate_digital_root
|
from tarot.card.data import CardDataLoader, calculate_digital_root
|
||||||
|
|
||||||
# Ensure trigrams are loaded first
|
# Ensure trigrams are loaded first
|
||||||
trigram._ensure_initialized()
|
trigram._ensure_initialized()
|
||||||
|
|
||||||
# Load planets for hexagram correspondences
|
# Load planets for hexagram correspondences
|
||||||
loader = CardDataLoader()
|
loader = CardDataLoader()
|
||||||
|
|
||||||
hex_specs = [
|
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": 1,
|
||||||
{"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"},
|
"name": "Creative Force",
|
||||||
{"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"},
|
"chinese": "乾",
|
||||||
{"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"},
|
"pinyin": "Qián",
|
||||||
{"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"},
|
"judgement": "Initiative succeeds when anchored in integrity.",
|
||||||
{"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"},
|
"image": "Heaven above and below mirrors unstoppable drive.",
|
||||||
{"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"},
|
"upper": "Qian",
|
||||||
{"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"},
|
"lower": "Qian",
|
||||||
{"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"},
|
"keywords": "Leadership|Momentum|Clarity",
|
||||||
{"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": 2,
|
||||||
{"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"},
|
"name": "Receptive Field",
|
||||||
{"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"},
|
"chinese": "坤",
|
||||||
{"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"},
|
"pinyin": "Kūn",
|
||||||
{"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"},
|
"judgement": "Grounded support flourishes through patience.",
|
||||||
{"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"},
|
"image": "Earth layered upon earth offers fertile space.",
|
||||||
{"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"},
|
"upper": "Kun",
|
||||||
{"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"},
|
"lower": "Kun",
|
||||||
{"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"},
|
"keywords": "Nurture|Support|Yielding",
|
||||||
{"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": 3,
|
||||||
{"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"},
|
"name": "Sprouting",
|
||||||
{"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"},
|
"chinese": "屯",
|
||||||
{"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"},
|
"pinyin": "Zhūn",
|
||||||
{"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"},
|
"judgement": "Challenges at the start need perseverance.",
|
||||||
{"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"},
|
"image": "Water over thunder shows storms that germinate seeds.",
|
||||||
{"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"},
|
"upper": "Kan",
|
||||||
{"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"},
|
"lower": "Zhen",
|
||||||
{"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"},
|
"keywords": "Beginnings|Struggle|Resolve",
|
||||||
{"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": 4,
|
||||||
{"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"},
|
"name": "Youthful Insight",
|
||||||
{"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"},
|
"chinese": "蒙",
|
||||||
{"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"},
|
"pinyin": "Méng",
|
||||||
{"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"},
|
"judgement": "Ignorance yields to steady guidance.",
|
||||||
{"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"},
|
"image": "Mountain above water signals learning via restraint.",
|
||||||
{"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"},
|
"upper": "Gen",
|
||||||
{"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"},
|
"lower": "Kan",
|
||||||
{"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"},
|
"keywords": "Study|Mentorship|Humility",
|
||||||
{"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": 5,
|
||||||
{"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"},
|
"name": "Waiting",
|
||||||
{"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"},
|
"chinese": "需",
|
||||||
{"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"},
|
"pinyin": "Xū",
|
||||||
{"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"},
|
"judgement": "Hold position until nourishment arrives.",
|
||||||
{"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"},
|
"image": "Water above heaven depicts clouds gathering provision.",
|
||||||
{"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"},
|
"upper": "Kan",
|
||||||
{"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"},
|
"lower": "Qian",
|
||||||
{"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"},
|
"keywords": "Patience|Faith|Preparation",
|
||||||
{"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": 6,
|
||||||
{"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"},
|
"name": "Conflict",
|
||||||
{"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"},
|
"chinese": "訟",
|
||||||
{"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"},
|
"pinyin": "Sòng",
|
||||||
{"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"},
|
"judgement": "Clarity and fairness prevent escalation.",
|
||||||
{"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"},
|
"image": "Heaven above water shows tension seeking balance.",
|
||||||
{"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"},
|
"upper": "Qian",
|
||||||
{"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"},
|
"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"]
|
planet_cycle = ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Earth"]
|
||||||
self._hexagrams = {}
|
self._hexagrams = {}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from utils.attributes import Number, Planet
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Trigram:
|
class Trigram:
|
||||||
"""Represents one of the eight I Ching trigrams."""
|
"""Represents one of the eight I Ching trigrams."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
chinese_name: str
|
chinese_name: str
|
||||||
pinyin: str
|
pinyin: str
|
||||||
@@ -27,6 +28,7 @@ class Trigram:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Hexagram:
|
class Hexagram:
|
||||||
"""Represents an I Ching hexagram with Tarot correspondence."""
|
"""Represents an I Ching hexagram with Tarot correspondence."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
chinese_name: str
|
chinese_name: str
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class Letter:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._initialized: bool = False
|
self._initialized: bool = False
|
||||||
self._loader: 'CardDataLoader | None' = None
|
self._loader: "CardDataLoader | None" = None
|
||||||
self.alphabet = CollectionAccessor(self._get_alphabets)
|
self.alphabet = CollectionAccessor(self._get_alphabets)
|
||||||
self.cipher = CollectionAccessor(self._get_ciphers)
|
self.cipher = CollectionAccessor(self._get_ciphers)
|
||||||
self.letter = CollectionAccessor(self._get_letters)
|
self.letter = CollectionAccessor(self._get_letters)
|
||||||
@@ -26,10 +26,11 @@ class Letter:
|
|||||||
return
|
return
|
||||||
|
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
|
|
||||||
self._loader = CardDataLoader()
|
self._loader = CardDataLoader()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def _require_loader(self) -> 'CardDataLoader':
|
def _require_loader(self) -> "CardDataLoader":
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
assert self._loader is not None, "Loader not initialized"
|
assert self._loader is not None, "Loader not initialized"
|
||||||
return self._loader
|
return self._loader
|
||||||
@@ -54,10 +55,10 @@ class Letter:
|
|||||||
loader = self._require_loader()
|
loader = self._require_loader()
|
||||||
return loader._periodic_table.copy()
|
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.
|
Start a fluent cipher request for the given text.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
letter.word('MAGICK').cipher('english_simple')
|
letter.word('MAGICK').cipher('english_simple')
|
||||||
letter.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
|
letter.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
|
||||||
|
|||||||
@@ -17,66 +17,68 @@ Each letter has attributes like:
|
|||||||
- Musical Note
|
- Musical Note
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Optional, Dict, Union, TYPE_CHECKING
|
from dataclasses import dataclass
|
||||||
from dataclasses import dataclass, field
|
from typing import TYPE_CHECKING, Dict, List, Optional, Union
|
||||||
from utils.filter import universal_filter, get_filterable_fields, format_results
|
|
||||||
|
from utils.filter import format_results, get_filterable_fields, universal_filter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from utils.query import CollectionAccessor
|
|
||||||
from tarot.attributes import Path
|
from tarot.attributes import Path
|
||||||
|
from utils.query import CollectionAccessor
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TarotLetter:
|
class TarotLetter:
|
||||||
"""
|
"""
|
||||||
Represents a Hebrew letter with full Tarot correspondences.
|
Represents a Hebrew letter with full Tarot correspondences.
|
||||||
|
|
||||||
Wraps Path objects from CardDataLoader to provide a letter-focused interface
|
Wraps Path objects from CardDataLoader to provide a letter-focused interface
|
||||||
while maintaining a single source of truth.
|
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)
|
letter_type: str # "Mother", "Double", or "Simple" (derived from path)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""Validate that path is set."""
|
"""Validate that path is set."""
|
||||||
if not self.path:
|
if not self.path:
|
||||||
raise ValueError("TarotLetter requires a valid Path object")
|
raise ValueError("TarotLetter requires a valid Path object")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hebrew_letter(self) -> str:
|
def hebrew_letter(self) -> str:
|
||||||
"""Get Hebrew letter character."""
|
"""Get Hebrew letter character."""
|
||||||
return self.path.hebrew_letter or ""
|
return self.path.hebrew_letter or ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def transliteration(self) -> str:
|
def transliteration(self) -> str:
|
||||||
"""Get transliterated name."""
|
"""Get transliterated name."""
|
||||||
return self.path.transliteration or ""
|
return self.path.transliteration or ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> int:
|
def position(self) -> int:
|
||||||
"""Get position (1-22 for paths)."""
|
"""Get position (1-22 for paths)."""
|
||||||
return self.path.number
|
return self.path.number
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trump(self) -> Optional[str]:
|
def trump(self) -> Optional[str]:
|
||||||
"""Get Tarot trump designation."""
|
"""Get Tarot trump designation."""
|
||||||
return self.path.tarot_trump
|
return self.path.tarot_trump
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def element(self) -> Optional[str]:
|
def element(self) -> Optional[str]:
|
||||||
"""Get element name if applicable."""
|
"""Get element name if applicable."""
|
||||||
return self.path.element.name if self.path.element else None
|
return self.path.element.name if self.path.element else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def planet(self) -> Optional[str]:
|
def planet(self) -> Optional[str]:
|
||||||
"""Get planet name if applicable."""
|
"""Get planet name if applicable."""
|
||||||
return self.path.planet.name if self.path.planet else None
|
return self.path.planet.name if self.path.planet else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def zodiac(self) -> Optional[str]:
|
def zodiac(self) -> Optional[str]:
|
||||||
"""Get zodiac sign if applicable."""
|
"""Get zodiac sign if applicable."""
|
||||||
return self.path.zodiac_sign
|
return self.path.zodiac_sign
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def intelligence(self) -> Optional[str]:
|
def intelligence(self) -> Optional[str]:
|
||||||
"""Get archangel/intelligence name from associated gods."""
|
"""Get archangel/intelligence name from associated gods."""
|
||||||
@@ -85,17 +87,17 @@ class TarotLetter:
|
|||||||
if all_gods:
|
if all_gods:
|
||||||
return all_gods[0].name
|
return all_gods[0].name
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def meaning(self) -> Optional[str]:
|
def meaning(self) -> Optional[str]:
|
||||||
"""Get path meaning/description."""
|
"""Get path meaning/description."""
|
||||||
return self.path.description
|
return self.path.description
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords(self) -> List[str]:
|
def keywords(self) -> List[str]:
|
||||||
"""Get keywords associated with path."""
|
"""Get keywords associated with path."""
|
||||||
return self.path.keywords or []
|
return self.path.keywords or []
|
||||||
|
|
||||||
def display(self) -> str:
|
def display(self) -> str:
|
||||||
"""Format letter for display."""
|
"""Format letter for display."""
|
||||||
lines = [
|
lines = [
|
||||||
@@ -104,7 +106,7 @@ class TarotLetter:
|
|||||||
f"Type: {self.letter_type}",
|
f"Type: {self.letter_type}",
|
||||||
f"Position: {self.position}",
|
f"Position: {self.position}",
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.trump:
|
if self.trump:
|
||||||
lines.append(f"Trump: {self.trump}")
|
lines.append(f"Trump: {self.trump}")
|
||||||
if self.zodiac:
|
if self.zodiac:
|
||||||
@@ -119,20 +121,20 @@ class TarotLetter:
|
|||||||
lines.append(f"Meaning: {self.meaning}")
|
lines.append(f"Meaning: {self.meaning}")
|
||||||
if self.keywords:
|
if self.keywords:
|
||||||
lines.append(f"Keywords: {', '.join(self.keywords)}")
|
lines.append(f"Keywords: {', '.join(self.keywords)}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
class LetterAccessor:
|
class LetterAccessor:
|
||||||
"""Fluent accessor for Tarot letters."""
|
"""Fluent accessor for Tarot letters."""
|
||||||
|
|
||||||
def __init__(self, letters_dict: Dict[str, TarotLetter]) -> None:
|
def __init__(self, letters_dict: Dict[str, TarotLetter]) -> None:
|
||||||
self._letters = letters_dict
|
self._letters = letters_dict
|
||||||
|
|
||||||
def __call__(self, transliteration: str) -> Optional[TarotLetter]:
|
def __call__(self, transliteration: str) -> Optional[TarotLetter]:
|
||||||
"""Get a letter by transliteration (e.g., 'aleph', 'beth', 'gimel')."""
|
"""Get a letter by transliteration (e.g., 'aleph', 'beth', 'gimel')."""
|
||||||
return self._letters.get(transliteration.lower())
|
return self._letters.get(transliteration.lower())
|
||||||
|
|
||||||
def __getitem__(self, key: Union[str, int]) -> Optional[TarotLetter]:
|
def __getitem__(self, key: Union[str, int]) -> Optional[TarotLetter]:
|
||||||
"""Get letter by name or position."""
|
"""Get letter by name or position."""
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
@@ -142,55 +144,59 @@ class LetterAccessor:
|
|||||||
return letter
|
return letter
|
||||||
return None
|
return None
|
||||||
return self(key)
|
return self(key)
|
||||||
|
|
||||||
def all(self) -> List[TarotLetter]:
|
def all(self) -> List[TarotLetter]:
|
||||||
"""Get all letters."""
|
"""Get all letters."""
|
||||||
return sorted(self._letters.values(), key=lambda x: x.position)
|
return sorted(self._letters.values(), key=lambda x: x.position)
|
||||||
|
|
||||||
def by_type(self, letter_type: str) -> List[TarotLetter]:
|
def by_type(self, letter_type: str) -> List[TarotLetter]:
|
||||||
"""Filter by type: 'Mother', 'Double', or 'Simple'."""
|
"""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]:
|
def by_zodiac(self, zodiac: str) -> Optional[TarotLetter]:
|
||||||
"""Get letter by zodiac sign."""
|
"""Get letter by zodiac sign."""
|
||||||
for letter in self._letters.values():
|
for letter in self._letters.values():
|
||||||
if letter.zodiac and zodiac.lower() in letter.zodiac.lower():
|
if letter.zodiac and zodiac.lower() in letter.zodiac.lower():
|
||||||
return letter
|
return letter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def by_planet(self, planet: str) -> List[TarotLetter]:
|
def by_planet(self, planet: str) -> List[TarotLetter]:
|
||||||
"""Get letters by planet."""
|
"""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]:
|
def by_trump(self, trump: str) -> Optional[TarotLetter]:
|
||||||
"""Get letter by tarot trump."""
|
"""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]:
|
def get_filterable_fields(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Dynamically get all filterable fields from TarotLetter.
|
Dynamically get all filterable fields from TarotLetter.
|
||||||
|
|
||||||
Returns the same fields as the universal filter utility.
|
Returns the same fields as the universal filter utility.
|
||||||
Useful for introspection and validation.
|
Useful for introspection and validation.
|
||||||
"""
|
"""
|
||||||
return get_filterable_fields(TarotLetter)
|
return get_filterable_fields(TarotLetter)
|
||||||
|
|
||||||
def filter(self, **kwargs) -> List[TarotLetter]:
|
def filter(self, **kwargs) -> List[TarotLetter]:
|
||||||
"""
|
"""
|
||||||
Filter letters by any TarotLetter attribute.
|
Filter letters by any TarotLetter attribute.
|
||||||
|
|
||||||
Uses the universal filter from utils.filter for consistency
|
Uses the universal filter from utils.filter for consistency
|
||||||
across the entire project.
|
across the entire project.
|
||||||
|
|
||||||
The filter automatically handles all fields from the TarotLetter dataclass:
|
The filter automatically handles all fields from the TarotLetter dataclass:
|
||||||
- letter_type, element, trump, zodiac, planet
|
- letter_type, element, trump, zodiac, planet
|
||||||
- king, queen, prince, princess
|
- king, queen, prince, princess
|
||||||
- cube, intelligence, note, meaning, hebrew_letter, transliteration, position
|
- cube, intelligence, note, meaning, hebrew_letter, transliteration, position
|
||||||
- keywords (list matching)
|
- keywords (list matching)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Any TarotLetter attribute with its value
|
**kwargs: Any TarotLetter attribute with its value
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
Tarot.letters.filter(letter_type="Simple")
|
Tarot.letters.filter(letter_type="Simple")
|
||||||
Tarot.letters.filter(element="Fire")
|
Tarot.letters.filter(element="Fire")
|
||||||
@@ -198,30 +204,30 @@ class LetterAccessor:
|
|||||||
Tarot.letters.filter(element="Air", letter_type="Mother")
|
Tarot.letters.filter(element="Air", letter_type="Mother")
|
||||||
Tarot.letters.filter(intelligence="Metatron")
|
Tarot.letters.filter(intelligence="Metatron")
|
||||||
Tarot.letters.filter(position=1)
|
Tarot.letters.filter(position=1)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of TarotLetter objects matching all filters
|
List of TarotLetter objects matching all filters
|
||||||
"""
|
"""
|
||||||
return universal_filter(self.all(), **kwargs)
|
return universal_filter(self.all(), **kwargs)
|
||||||
|
|
||||||
def display_filter(self, **kwargs) -> str:
|
def display_filter(self, **kwargs) -> str:
|
||||||
"""
|
"""
|
||||||
Filter letters and display results nicely formatted.
|
Filter letters and display results nicely formatted.
|
||||||
|
|
||||||
Combines filtering and formatting in one call.
|
Combines filtering and formatting in one call.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Any TarotLetter attribute with its value
|
**kwargs: Any TarotLetter attribute with its value
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string with filtered letters
|
Formatted string with filtered letters
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
print(Tarot.letters.display_filter(element="Fire"))
|
print(Tarot.letters.display_filter(element="Fire"))
|
||||||
"""
|
"""
|
||||||
results = self.filter(**kwargs)
|
results = self.filter(**kwargs)
|
||||||
return format_results(results)
|
return format_results(results)
|
||||||
|
|
||||||
def display_all(self) -> str:
|
def display_all(self) -> str:
|
||||||
"""Display all letters formatted."""
|
"""Display all letters formatted."""
|
||||||
lines = []
|
lines = []
|
||||||
@@ -229,20 +235,20 @@ class LetterAccessor:
|
|||||||
lines.append(letter.display())
|
lines.append(letter.display())
|
||||||
lines.append("-" * 50)
|
lines.append("-" * 50)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def display_by_type(self, letter_type: str) -> str:
|
def display_by_type(self, letter_type: str) -> str:
|
||||||
"""Display all letters of a specific type."""
|
"""Display all letters of a specific type."""
|
||||||
letters = self.by_type(letter_type)
|
letters = self.by_type(letter_type)
|
||||||
if not letters:
|
if not letters:
|
||||||
return f"No letters found with type: {letter_type}"
|
return f"No letters found with type: {letter_type}"
|
||||||
|
|
||||||
lines = [f"\n{letter_type.upper()} LETTERS ({len(letters)} total)"]
|
lines = [f"\n{letter_type.upper()} LETTERS ({len(letters)} total)"]
|
||||||
lines.append("=" * 50)
|
lines.append("=" * 50)
|
||||||
for letter in letters:
|
for letter in letters:
|
||||||
lines.append(letter.display())
|
lines.append(letter.display())
|
||||||
lines.append("-" * 50)
|
lines.append("-" * 50)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def iChing(self):
|
def iChing(self):
|
||||||
"""Access I Ching trigrams and hexagrams."""
|
"""Access I Ching trigrams and hexagrams."""
|
||||||
@@ -251,33 +257,34 @@ class LetterAccessor:
|
|||||||
|
|
||||||
class IChing:
|
class IChing:
|
||||||
"""Namespace for I Ching trigrams and hexagrams access.
|
"""Namespace for I Ching trigrams and hexagrams access.
|
||||||
|
|
||||||
Provides fluent query interface for accessing I Ching trigrams and hexagrams
|
Provides fluent query interface for accessing I Ching trigrams and hexagrams
|
||||||
with Tarot correspondences.
|
with Tarot correspondences.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
trigrams = Tarot.letters.iChing.trigram
|
trigrams = Tarot.letters.iChing.trigram
|
||||||
qian = trigrams.name('Qian')
|
qian = trigrams.name('Qian')
|
||||||
all_trigrams = trigrams.all()
|
all_trigrams = trigrams.all()
|
||||||
|
|
||||||
hexagrams = Tarot.letters.iChing.hexagram
|
hexagrams = Tarot.letters.iChing.hexagram
|
||||||
hex1 = hexagrams.all()[1]
|
hex1 = hexagrams.all()[1]
|
||||||
all_hex = hexagrams.list()
|
all_hex = hexagrams.list()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
trigram: 'CollectionAccessor'
|
trigram: "CollectionAccessor"
|
||||||
hexagram: 'CollectionAccessor'
|
hexagram: "CollectionAccessor"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize iChing accessor with trigram and hexagram collections."""
|
"""Initialize iChing accessor with trigram and hexagram collections."""
|
||||||
from tarot.letter import iChing as iching_module
|
from tarot.letter import iChing as iching_module
|
||||||
|
|
||||||
self.trigram = iching_module.trigram.trigram
|
self.trigram = iching_module.trigram.trigram
|
||||||
self.hexagram = iching_module.hexagram.hexagram
|
self.hexagram = iching_module.hexagram.hexagram
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Clean representation of iChing namespace."""
|
"""Clean representation of iChing namespace."""
|
||||||
return "IChing(trigram, hexagram)"
|
return "IChing(trigram, hexagram)"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""String representation of iChing namespace."""
|
"""String representation of iChing namespace."""
|
||||||
return "I Ching (trigrams and hexagrams)"
|
return "I Ching (trigrams and hexagrams)"
|
||||||
@@ -285,50 +292,50 @@ class IChing:
|
|||||||
|
|
||||||
class LettersRegistry:
|
class LettersRegistry:
|
||||||
"""Registry and accessor for all Hebrew letters with Tarot correspondences."""
|
"""Registry and accessor for all Hebrew letters with Tarot correspondences."""
|
||||||
|
|
||||||
_instance: Optional['LettersRegistry'] = None
|
_instance: Optional["LettersRegistry"] = None
|
||||||
_letters: Dict[str, TarotLetter] = {}
|
_letters: Dict[str, TarotLetter] = {}
|
||||||
_initialized: bool = False
|
_initialized: bool = False
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._initialize_letters()
|
self._initialize_letters()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def _initialize_letters(self) -> None:
|
def _initialize_letters(self) -> None:
|
||||||
"""Initialize all 22 Hebrew letters by wrapping Path objects from CardDataLoader."""
|
"""Initialize all 22 Hebrew letters by wrapping Path objects from CardDataLoader."""
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
|
|
||||||
loader = CardDataLoader()
|
loader = CardDataLoader()
|
||||||
paths = loader.path() # Get all 22 paths
|
paths = loader.path() # Get all 22 paths
|
||||||
|
|
||||||
self._letters = {}
|
self._letters = {}
|
||||||
|
|
||||||
# Map each path (11-32) to a TarotLetter with appropriate type
|
# Map each path (11-32) to a TarotLetter with appropriate type
|
||||||
for path_number, path in paths.items():
|
for path_number, path in paths.items():
|
||||||
# Determine letter type based on path number
|
# Determine letter type based on path number
|
||||||
# Mother letters: 11 (Aleph), 23 (Mem), 31 (Shin)
|
# Mother letters: 11 (Aleph), 23 (Mem), 31 (Shin)
|
||||||
# Double letters: 12, 13, 14, 15, 18, 21, 22
|
# Double letters: 12, 13, 14, 15, 18, 21, 22
|
||||||
# Simple (Zodiacal/Planetary): 16, 17, 19, 20, 24, 25, 26, 27, 28, 29, 30, 32
|
# Simple (Zodiacal/Planetary): 16, 17, 19, 20, 24, 25, 26, 27, 28, 29, 30, 32
|
||||||
|
|
||||||
if path_number in {11, 23, 31}:
|
if path_number in {11, 23, 31}:
|
||||||
letter_type = "Mother"
|
letter_type = "Mother"
|
||||||
elif path_number in {12, 13, 14, 15, 18, 21, 22}:
|
elif path_number in {12, 13, 14, 15, 18, 21, 22}:
|
||||||
letter_type = "Double"
|
letter_type = "Double"
|
||||||
else:
|
else:
|
||||||
letter_type = "Simple"
|
letter_type = "Simple"
|
||||||
|
|
||||||
# Create TarotLetter wrapping the path
|
# Create TarotLetter wrapping the path
|
||||||
letter_key = path.transliteration.lower()
|
letter_key = path.transliteration.lower()
|
||||||
self._letters[letter_key] = TarotLetter(path=path, letter_type=letter_type)
|
self._letters[letter_key] = TarotLetter(path=path, letter_type=letter_type)
|
||||||
|
|
||||||
def accessor(self) -> LetterAccessor:
|
def accessor(self) -> LetterAccessor:
|
||||||
"""Get the letter accessor."""
|
"""Get the letter accessor."""
|
||||||
return LetterAccessor(self._letters)
|
return LetterAccessor(self._letters)
|
||||||
|
|||||||
@@ -8,25 +8,26 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class _Word:
|
class _Word:
|
||||||
"""Fluent accessor for word analysis and cipher operations."""
|
"""Fluent accessor for word analysis and cipher operations."""
|
||||||
|
|
||||||
_loader: 'CardDataLoader | None' = None
|
_loader: "CardDataLoader | None" = None
|
||||||
_initialized: bool = False
|
_initialized: bool = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _ensure_initialized(cls) -> None:
|
def _ensure_initialized(cls) -> None:
|
||||||
"""Lazy-load CardDataLoader on first access."""
|
"""Lazy-load CardDataLoader on first access."""
|
||||||
if cls._initialized:
|
if cls._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
|
|
||||||
cls._loader = CardDataLoader()
|
cls._loader = CardDataLoader()
|
||||||
cls._initialized = True
|
cls._initialized = True
|
||||||
|
|
||||||
@classmethod
|
@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.
|
Start a fluent cipher request for the given text.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
word.word('MAGICK').cipher('english_simple')
|
word.word('MAGICK').cipher('english_simple')
|
||||||
word.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
|
word.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ Provides fluent query interface for:
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import number
|
from tarot import number
|
||||||
|
|
||||||
num = number.number(5)
|
num = number.number(5)
|
||||||
root = number.digital_root(256)
|
root = number.digital_root(256)
|
||||||
colors = number.color()
|
colors = number.color()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .number import number, calculate_digital_root
|
from .number import calculate_digital_root, number
|
||||||
|
|
||||||
__all__ = ["number", "calculate_digital_root"]
|
__all__ = ["number", "calculate_digital_root"]
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
"""Numbers loader - access to numerology and number correspondences."""
|
"""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
|
from utils.filter import universal_filter
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from utils.attributes import Color, Number
|
||||||
|
|
||||||
|
|
||||||
def calculate_digital_root(value: int) -> int:
|
def calculate_digital_root(value: int) -> int:
|
||||||
"""
|
"""
|
||||||
Calculate the digital root of a number by repeatedly summing its digits.
|
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
|
Digital root reduces any number to a single digit (1-9) by repeatedly
|
||||||
summing its digits until a single digit remains.
|
summing its digits until a single digit remains.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The number to reduce to digital root
|
value: The number to reduce to digital root
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The digital root (1-9)
|
The digital root (1-9)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> calculate_digital_root(14) # 1+4 = 5
|
>>> calculate_digital_root(14) # 1+4 = 5
|
||||||
5
|
5
|
||||||
@@ -27,172 +31,173 @@ def calculate_digital_root(value: int) -> int:
|
|||||||
"""
|
"""
|
||||||
if value < 1:
|
if value < 1:
|
||||||
raise ValueError(f"Value must be positive, got {value}")
|
raise ValueError(f"Value must be positive, got {value}")
|
||||||
|
|
||||||
while value >= 10:
|
while value >= 10:
|
||||||
value = sum(int(digit) for digit in str(value))
|
value = sum(int(digit) for digit in str(value))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class Numbers:
|
class Numbers:
|
||||||
"""
|
"""
|
||||||
Unified accessor for numerology, numbers, and color correspondences.
|
Unified accessor for numerology, numbers, and color correspondences.
|
||||||
|
|
||||||
All methods are class methods, so Numbers is accessed as a static namespace:
|
All methods are class methods, so Numbers is accessed as a static namespace:
|
||||||
|
|
||||||
num = Numbers.number(5)
|
num = Numbers.number(5)
|
||||||
root = Numbers.digital_root(256)
|
root = Numbers.digital_root(256)
|
||||||
color = Numbers.color_by_number(root)
|
color = Numbers.color_by_number(root)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# These are populated on first access from CardDataLoader
|
# These are populated on first access from CardDataLoader
|
||||||
_numbers: Dict[int, 'Number'] = {} # type: ignore
|
_numbers: Dict[int, "Number"] = {} # type: ignore
|
||||||
_colors: Dict[int, 'Color'] = {} # type: ignore
|
_colors: Dict[int, "Color"] = {} # type: ignore
|
||||||
_initialized: bool = False
|
_initialized: bool = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _ensure_initialized(cls) -> None:
|
def _ensure_initialized(cls) -> None:
|
||||||
"""Lazy-load data from CardDataLoader on first access."""
|
"""Lazy-load data from CardDataLoader on first access."""
|
||||||
if cls._initialized:
|
if cls._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
|
|
||||||
loader = CardDataLoader()
|
loader = CardDataLoader()
|
||||||
cls._numbers = loader.number()
|
cls._numbers = loader.number()
|
||||||
cls._colors = loader.color()
|
cls._colors = loader.color()
|
||||||
cls._initialized = True
|
cls._initialized = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def number(cls, value: int) -> Optional['Number']:
|
def number(cls, value: int) -> Optional["Number"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def number(cls, value: None = ...) -> Dict[int, 'Number']:
|
def number(cls, value: None = ...) -> Dict[int, "Number"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return an individual Number or the full numerology table."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
if value is None:
|
if value is None:
|
||||||
return cls._numbers.copy()
|
return cls._numbers.copy()
|
||||||
return cls._numbers.get(value)
|
return cls._numbers.get(value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def color(cls, sephera_number: int) -> Optional['Color']:
|
def color(cls, sephera_number: int) -> Optional["Color"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def color(cls, sephera_number: None = ...) -> Dict[int, 'Color']:
|
def color(cls, sephera_number: None = ...) -> Dict[int, "Color"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return a single color correspondence or the entire map."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
if sephera_number is None:
|
if sephera_number is None:
|
||||||
return cls._colors.copy()
|
return cls._colors.copy()
|
||||||
return cls._colors.get(sephera_number)
|
return cls._colors.get(sephera_number)
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Get a Color by mapping a number through digital root."""
|
||||||
root = calculate_digital_root(number)
|
root = calculate_digital_root(number)
|
||||||
return cls.color(root)
|
return cls.color(root)
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Get a Number object using digital root calculation."""
|
||||||
root = calculate_digital_root(value)
|
root = calculate_digital_root(value)
|
||||||
return cls.number(root)
|
return cls.number(root)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def digital_root(cls, value: int) -> int:
|
def digital_root(cls, value: int) -> int:
|
||||||
"""Get the digital root of a value."""
|
"""Get the digital root of a value."""
|
||||||
return calculate_digital_root(value)
|
return calculate_digital_root(value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_numbers(cls, **kwargs) -> list:
|
def filter_numbers(cls, **kwargs) -> list:
|
||||||
"""
|
"""
|
||||||
Filter numbers by any Number attribute.
|
Filter numbers by any Number attribute.
|
||||||
|
|
||||||
Uses the universal filter from utils.filter for consistency
|
Uses the universal filter from utils.filter for consistency
|
||||||
across the entire project.
|
across the entire project.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Any Number attribute with its value
|
**kwargs: Any Number attribute with its value
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
Numbers.filter_numbers(element="Fire")
|
Numbers.filter_numbers(element="Fire")
|
||||||
Numbers.filter_numbers(sephera_number=5)
|
Numbers.filter_numbers(sephera_number=5)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of Number objects matching all filters
|
List of Number objects matching all filters
|
||||||
"""
|
"""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
return universal_filter(list(cls._numbers.values()), **kwargs)
|
return universal_filter(list(cls._numbers.values()), **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def display_filter_numbers(cls, **kwargs) -> str:
|
def display_filter_numbers(cls, **kwargs) -> str:
|
||||||
"""
|
"""
|
||||||
Filter numbers and display results nicely formatted.
|
Filter numbers and display results nicely formatted.
|
||||||
|
|
||||||
Combines filtering and formatting in one call.
|
Combines filtering and formatting in one call.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Any Number attribute with its value
|
**kwargs: Any Number attribute with its value
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string with filtered numbers
|
Formatted string with filtered numbers
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
print(Numbers.display_filter_numbers(element="Fire"))
|
print(Numbers.display_filter_numbers(element="Fire"))
|
||||||
"""
|
"""
|
||||||
from utils.filter import format_results
|
from utils.filter import format_results
|
||||||
|
|
||||||
results = cls.filter_numbers(**kwargs)
|
results = cls.filter_numbers(**kwargs)
|
||||||
return format_results(results)
|
return format_results(results)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_colors(cls, **kwargs) -> list:
|
def filter_colors(cls, **kwargs) -> list:
|
||||||
"""
|
"""
|
||||||
Filter colors by any Color attribute.
|
Filter colors by any Color attribute.
|
||||||
|
|
||||||
Uses the universal filter from utils.filter for consistency
|
Uses the universal filter from utils.filter for consistency
|
||||||
across the entire project.
|
across the entire project.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Any Color attribute with its value
|
**kwargs: Any Color attribute with its value
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
Numbers.filter_colors(element="Water")
|
Numbers.filter_colors(element="Water")
|
||||||
Numbers.filter_colors(sephera_number=3)
|
Numbers.filter_colors(sephera_number=3)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of Color objects matching all filters
|
List of Color objects matching all filters
|
||||||
"""
|
"""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
return universal_filter(list(cls._colors.values()), **kwargs)
|
return universal_filter(list(cls._colors.values()), **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def display_filter_colors(cls, **kwargs) -> str:
|
def display_filter_colors(cls, **kwargs) -> str:
|
||||||
"""
|
"""
|
||||||
Filter colors and display results nicely formatted.
|
Filter colors and display results nicely formatted.
|
||||||
|
|
||||||
Combines filtering and formatting in one call.
|
Combines filtering and formatting in one call.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs: Any Color attribute with its value
|
**kwargs: Any Color attribute with its value
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string with filtered colors
|
Formatted string with filtered colors
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
print(Numbers.display_filter_colors(element="Water"))
|
print(Numbers.display_filter_colors(element="Water"))
|
||||||
"""
|
"""
|
||||||
from utils.filter import format_results
|
from utils.filter import format_results
|
||||||
|
|
||||||
results = cls.filter_colors(**kwargs)
|
results = cls.filter_colors(**kwargs)
|
||||||
return format_results(results)
|
return format_results(results)
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ if TYPE_CHECKING:
|
|||||||
def calculate_digital_root(value: int) -> int:
|
def calculate_digital_root(value: int) -> int:
|
||||||
"""
|
"""
|
||||||
Calculate the digital root of a number by repeatedly summing its digits.
|
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
|
Digital root reduces any number to a single digit (1-9) by repeatedly
|
||||||
summing its digits until a single digit remains.
|
summing its digits until a single digit remains.
|
||||||
"""
|
"""
|
||||||
if value < 1:
|
if value < 1:
|
||||||
raise ValueError(f"Value must be positive, got {value}")
|
raise ValueError(f"Value must be positive, got {value}")
|
||||||
|
|
||||||
while value >= 10:
|
while value >= 10:
|
||||||
value = sum(int(digit) for digit in str(value))
|
value = sum(int(digit) for digit in str(value))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ class _Number:
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._initialized: bool = False
|
self._initialized: bool = False
|
||||||
self._loader: 'CardDataLoader | None' = None
|
self._loader: "CardDataLoader | None" = None
|
||||||
self.number = CollectionAccessor(self._get_numbers)
|
self.number = CollectionAccessor(self._get_numbers)
|
||||||
self.color = CollectionAccessor(self._get_colors)
|
self.color = CollectionAccessor(self._get_colors)
|
||||||
self.cipher = CollectionAccessor(self._get_ciphers)
|
self.cipher = CollectionAccessor(self._get_ciphers)
|
||||||
@@ -40,10 +40,11 @@ class _Number:
|
|||||||
return
|
return
|
||||||
|
|
||||||
from tarot.card.data import CardDataLoader
|
from tarot.card.data import CardDataLoader
|
||||||
|
|
||||||
self._loader = CardDataLoader()
|
self._loader = CardDataLoader()
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def _require_loader(self) -> 'CardDataLoader':
|
def _require_loader(self) -> "CardDataLoader":
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
assert self._loader is not None, "Loader not initialized"
|
assert self._loader is not None, "Loader not initialized"
|
||||||
return self._loader
|
return self._loader
|
||||||
|
|||||||
@@ -17,66 +17,99 @@ Unified Namespaces (singular names):
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import number, letter, words, Tarot
|
from tarot import number, letter, words, Tarot
|
||||||
|
|
||||||
num = number.number(5)
|
num = number.number(5)
|
||||||
result = letter.words.word('MAGICK').cipher('english_simple')
|
result = letter.words.word('MAGICK').cipher('english_simple')
|
||||||
card = Tarot.deck.card(3)
|
card = Tarot.deck.card(3)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .deck import Deck, Card, MajorCard, MinorCard, DLT
|
import kaballah
|
||||||
from .attributes import (
|
from kaballah import Cube, Tree
|
||||||
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter,
|
from kaballah.cube.attributes import CubeOfSpace, Wall, WallDirection
|
||||||
Sephera, PeriodicTable, Degree, AstrologicalInfluence,
|
|
||||||
TreeOfLife, Correspondences, CardImage, DoublLetterTrump,
|
# Import from namespace folders
|
||||||
EnglishAlphabet, GreekAlphabet, HebrewAlphabet,
|
from letter import hexagram, letter, trigram
|
||||||
Trigram, Hexagram,
|
from number import calculate_digital_root, number
|
||||||
EnochianTablet, EnochianGridPosition, EnochianArchetype, Path,
|
from temporal import PlanetPosition, ThalemaClock
|
||||||
)
|
from temporal import Zodiac as AstrologyZodiac
|
||||||
|
|
||||||
# Import shared attributes from utils
|
# Import shared attributes from utils
|
||||||
from utils.attributes import (
|
from utils.attributes import (
|
||||||
Note, Element, ElementType, Number, Color, Colorscale,
|
Cipher,
|
||||||
Planet, God, Cipher, CipherResult, Perfume,
|
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)
|
# Import from card module (includes details, loader, and image_loader)
|
||||||
from .card import (
|
from .card import (
|
||||||
CardAccessor,
|
CardAccessor,
|
||||||
CardDetailsRegistry,
|
CardDetailsRegistry,
|
||||||
|
ImageDeckLoader,
|
||||||
|
filter_cards_by_keywords,
|
||||||
|
get_card_info,
|
||||||
|
get_cards_by_suit,
|
||||||
load_card_details,
|
load_card_details,
|
||||||
load_deck_details,
|
load_deck_details,
|
||||||
get_cards_by_suit,
|
|
||||||
filter_cards_by_keywords,
|
|
||||||
print_card_details,
|
|
||||||
get_card_info,
|
|
||||||
ImageDeckLoader,
|
|
||||||
load_deck_images,
|
load_deck_images,
|
||||||
|
print_card_details,
|
||||||
)
|
)
|
||||||
|
from .card.data import CardDataLoader
|
||||||
# Import from namespace folders
|
from .deck import DLT, Card, Deck, MajorCard, MinorCard
|
||||||
from letter import letter, trigram, hexagram
|
from .tarot_api import Tarot
|
||||||
from number import number, calculate_digital_root
|
|
||||||
import kaballah
|
|
||||||
from kaballah import Tree, Cube
|
|
||||||
from temporal import ThalemaClock, Zodiac as AstrologyZodiac, PlanetPosition
|
|
||||||
|
|
||||||
|
|
||||||
def display(obj):
|
def display(obj):
|
||||||
"""
|
"""
|
||||||
Pretty print any tarot object by showing all its attributes.
|
Pretty print any tarot object by showing all its attributes.
|
||||||
|
|
||||||
Automatically detects dataclass objects and displays their fields
|
Automatically detects dataclass objects and displays their fields
|
||||||
with values in a readable format.
|
with values in a readable format.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import display, number
|
from tarot import display, number
|
||||||
num = number.number(5)
|
num = number.number(5)
|
||||||
display(num) # Shows all attributes nicely formatted
|
display(num) # Shows all attributes nicely formatted
|
||||||
"""
|
"""
|
||||||
from dataclasses import fields
|
from dataclasses import fields
|
||||||
if hasattr(obj, '__dataclass_fields__'):
|
|
||||||
|
if hasattr(obj, "__dataclass_fields__"):
|
||||||
# It's a dataclass - show all fields
|
# It's a dataclass - show all fields
|
||||||
print(f"{obj.__class__.__name__}:")
|
print(f"{obj.__class__.__name__}:")
|
||||||
for field in fields(obj):
|
for field in fields(obj):
|
||||||
@@ -96,12 +129,10 @@ __all__ = [
|
|||||||
"Tarot",
|
"Tarot",
|
||||||
"trigram",
|
"trigram",
|
||||||
"hexagram",
|
"hexagram",
|
||||||
|
|
||||||
# Temporal and astrological
|
# Temporal and astrological
|
||||||
"ThalemaClock",
|
"ThalemaClock",
|
||||||
"AstrologyZodiac",
|
"AstrologyZodiac",
|
||||||
"PlanetPosition",
|
"PlanetPosition",
|
||||||
|
|
||||||
# Card details and loading
|
# Card details and loading
|
||||||
"CardDetailsRegistry",
|
"CardDetailsRegistry",
|
||||||
"load_card_details",
|
"load_card_details",
|
||||||
@@ -110,24 +141,20 @@ __all__ = [
|
|||||||
"filter_cards_by_keywords",
|
"filter_cards_by_keywords",
|
||||||
"print_card_details",
|
"print_card_details",
|
||||||
"get_card_info",
|
"get_card_info",
|
||||||
|
|
||||||
# Image loading
|
# Image loading
|
||||||
"ImageDeckLoader",
|
"ImageDeckLoader",
|
||||||
"load_deck_images",
|
"load_deck_images",
|
||||||
|
|
||||||
# Utilities
|
# Utilities
|
||||||
"display",
|
"display",
|
||||||
"CardAccessor",
|
"CardAccessor",
|
||||||
"Tree",
|
"Tree",
|
||||||
"Cube",
|
"Cube",
|
||||||
|
|
||||||
# Deck classes
|
# Deck classes
|
||||||
"Deck",
|
"Deck",
|
||||||
"Card",
|
"Card",
|
||||||
"MajorCard",
|
"MajorCard",
|
||||||
"MinorCard",
|
"MinorCard",
|
||||||
"DLT",
|
"DLT",
|
||||||
|
|
||||||
# Calendar/attribute classes
|
# Calendar/attribute classes
|
||||||
"Month",
|
"Month",
|
||||||
"Day",
|
"Day",
|
||||||
@@ -142,7 +169,6 @@ __all__ = [
|
|||||||
"CubeOfSpace",
|
"CubeOfSpace",
|
||||||
"WallDirection",
|
"WallDirection",
|
||||||
"Wall",
|
"Wall",
|
||||||
|
|
||||||
# Sepheric classes
|
# Sepheric classes
|
||||||
"Sephera",
|
"Sephera",
|
||||||
"PeriodicTable",
|
"PeriodicTable",
|
||||||
@@ -157,12 +183,10 @@ __all__ = [
|
|||||||
"EnochianTablet",
|
"EnochianTablet",
|
||||||
"EnochianGridPosition",
|
"EnochianGridPosition",
|
||||||
"EnochianArchetype",
|
"EnochianArchetype",
|
||||||
|
|
||||||
# Alphabet classes
|
# Alphabet classes
|
||||||
"EnglishAlphabet",
|
"EnglishAlphabet",
|
||||||
"GreekAlphabet",
|
"GreekAlphabet",
|
||||||
"HebrewAlphabet",
|
"HebrewAlphabet",
|
||||||
|
|
||||||
# Number and color classes
|
# Number and color classes
|
||||||
"Number",
|
"Number",
|
||||||
"Color",
|
"Color",
|
||||||
@@ -172,7 +196,6 @@ __all__ = [
|
|||||||
"Hexagram",
|
"Hexagram",
|
||||||
"Cipher",
|
"Cipher",
|
||||||
"CipherResult",
|
"CipherResult",
|
||||||
|
|
||||||
# Data loader and functions
|
# Data loader and functions
|
||||||
"CardDataLoader",
|
"CardDataLoader",
|
||||||
"calculate_digital_root",
|
"calculate_digital_root",
|
||||||
|
|||||||
@@ -8,54 +8,54 @@ attribute classes for cards.
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
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
|
# Re-export attributes from other modules for convenience/backward compatibility
|
||||||
from kaballah.attributes import (
|
from kaballah.attributes import (
|
||||||
Sephera,
|
|
||||||
PeriodicTable,
|
|
||||||
TreeOfLife,
|
|
||||||
Correspondences,
|
Correspondences,
|
||||||
Path,
|
Path,
|
||||||
|
PeriodicTable,
|
||||||
|
Sephera,
|
||||||
|
TreeOfLife,
|
||||||
)
|
)
|
||||||
from letter.attributes import (
|
from letter.attributes import (
|
||||||
Letter,
|
|
||||||
EnglishAlphabet,
|
|
||||||
GreekAlphabet,
|
|
||||||
HebrewAlphabet,
|
|
||||||
DoublLetterTrump,
|
DoublLetterTrump,
|
||||||
|
EnglishAlphabet,
|
||||||
|
EnochianArchetype,
|
||||||
|
EnochianGridPosition,
|
||||||
EnochianLetter,
|
EnochianLetter,
|
||||||
EnochianSpirit,
|
EnochianSpirit,
|
||||||
EnochianTablet,
|
EnochianTablet,
|
||||||
EnochianGridPosition,
|
GreekAlphabet,
|
||||||
EnochianArchetype,
|
HebrewAlphabet,
|
||||||
|
Letter,
|
||||||
)
|
)
|
||||||
from letter.iChing_attributes import (
|
from letter.iChing_attributes import (
|
||||||
Trigram,
|
|
||||||
Hexagram,
|
Hexagram,
|
||||||
|
Trigram,
|
||||||
)
|
)
|
||||||
from temporal.attributes import (
|
from temporal.attributes import (
|
||||||
|
AstrologicalInfluence,
|
||||||
|
ClockHour,
|
||||||
|
Degree,
|
||||||
|
Hour,
|
||||||
Month,
|
Month,
|
||||||
Weekday,
|
Weekday,
|
||||||
Hour,
|
|
||||||
ClockHour,
|
|
||||||
Zodiac,
|
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)
|
# Alias Day to Weekday for backward compatibility (Day in this context was Day of Week)
|
||||||
@@ -113,8 +113,9 @@ __all__ = [
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Suit:
|
class Suit:
|
||||||
"""Represents a tarot suit."""
|
"""Represents a tarot suit."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
element: 'ElementType'
|
element: "ElementType"
|
||||||
tarot_correspondence: str
|
tarot_correspondence: str
|
||||||
number: int
|
number: int
|
||||||
|
|
||||||
@@ -122,8 +123,8 @@ class Suit:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class CardImage:
|
class CardImage:
|
||||||
"""Represents an image associated with a card."""
|
"""Represents an image associated with a card."""
|
||||||
|
|
||||||
filename: str
|
filename: str
|
||||||
artist: str
|
artist: str
|
||||||
deck_name: str
|
deck_name: str
|
||||||
url: Optional[str] = None
|
url: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
from .card import CardAccessor
|
from .card import CardAccessor
|
||||||
from .details import CardDetailsRegistry
|
from .details import CardDetailsRegistry
|
||||||
|
from .image_loader import ImageDeckLoader, load_deck_images
|
||||||
from .loader import (
|
from .loader import (
|
||||||
|
filter_cards_by_keywords,
|
||||||
|
get_card_info,
|
||||||
|
get_cards_by_suit,
|
||||||
load_card_details,
|
load_card_details,
|
||||||
load_deck_details,
|
load_deck_details,
|
||||||
get_cards_by_suit,
|
|
||||||
filter_cards_by_keywords,
|
|
||||||
print_card_details,
|
print_card_details,
|
||||||
get_card_info,
|
|
||||||
)
|
)
|
||||||
from .image_loader import ImageDeckLoader, load_deck_images
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CardAccessor",
|
"CardAccessor",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Provides fluent access to Tarot cards through Tarot.deck namespace.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot.card import Deck, Card
|
from tarot.card import Deck, Card
|
||||||
|
|
||||||
card = Deck.card(3) # Get card 3
|
card = Deck.card(3) # Get card 3
|
||||||
cards = Deck.card.filter(arcana="Major") # Get all Major Arcana
|
cards = Deck.card.filter(arcana="Major") # Get all Major Arcana
|
||||||
cards = Deck.card.filter(arcana="Minor") # Get all Minor 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
|
cards = Deck.card.filter(arcana="Minor", suit="Wands", pip=5) # 5 of Wands
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
from utils.filter import universal_filter, format_results
|
|
||||||
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
|
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):
|
class CardList(list):
|
||||||
"""Custom list class for cards that formats nicely when printed."""
|
"""Custom list class for cards that formats nicely when printed."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Format card list for display."""
|
"""Format card list for display."""
|
||||||
if not self:
|
if not self:
|
||||||
return "(no cards)"
|
return "(no cards)"
|
||||||
return _format_cards(self)
|
return _format_cards(self)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return string representation."""
|
"""Return string representation."""
|
||||||
return self.__str__()
|
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.
|
Format a list of cards for user-friendly display.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cards: List of Card objects to format
|
cards: List of Card objects to format
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string with each card separated by blank lines
|
Formatted string with each card separated by blank lines
|
||||||
"""
|
"""
|
||||||
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
|
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for card in cards:
|
for card in cards:
|
||||||
card_num = getattr(card, 'number', '?')
|
card_num = getattr(card, "number", "?")
|
||||||
card_name = getattr(card, 'name', 'Unknown')
|
card_name = getattr(card, "name", "Unknown")
|
||||||
lines.append(f"--- {card_num}: {card_name} ---")
|
lines.append(f"--- {card_num}: {card_name} ---")
|
||||||
|
|
||||||
# Format all attributes with proper nesting
|
# Format all attributes with proper nesting
|
||||||
for attr_name, attr_value in get_object_attributes(card):
|
for attr_name, attr_value in get_object_attributes(card):
|
||||||
if is_nested_object(attr_value):
|
if is_nested_object(attr_value):
|
||||||
@@ -59,16 +61,16 @@ def _format_cards(cards: List['Card']) -> str:
|
|||||||
lines.append(nested)
|
lines.append(nested)
|
||||||
else:
|
else:
|
||||||
lines.append(f" {attr_name}: {attr_value}")
|
lines.append(f" {attr_name}: {attr_value}")
|
||||||
|
|
||||||
lines.append("") # Blank line between items
|
lines.append("") # Blank line between items
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
class CardAccessor:
|
class CardAccessor:
|
||||||
"""
|
"""
|
||||||
Fluent accessor for Tarot cards in the deck.
|
Fluent accessor for Tarot cards in the deck.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
Tarot.deck.card(3) # Get card 3
|
Tarot.deck.card(3) # Get card 3
|
||||||
Tarot.deck.card.filter(arcana="Major") # Get all Major Arcana
|
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.filter(arcana="Minor", suit="Wands") # Get all Wand cards
|
||||||
Tarot.deck.card.display_filter(arcana="Major") # Display Major Arcana
|
Tarot.deck.card.display_filter(arcana="Major") # Display Major Arcana
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_deck: Optional['Deck'] = None
|
_deck: Optional["Deck"] = None
|
||||||
_initialized: bool = False
|
_initialized: bool = False
|
||||||
|
|
||||||
def _ensure_initialized(self) -> None:
|
def _ensure_initialized(self) -> None:
|
||||||
"""Lazy-load the Deck on first access."""
|
"""Lazy-load the Deck on first access."""
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
from tarot.deck import Deck as DeckClass
|
from tarot.deck import Deck as DeckClass
|
||||||
|
|
||||||
CardAccessor._deck = DeckClass()
|
CardAccessor._deck = DeckClass()
|
||||||
CardAccessor._initialized = True
|
CardAccessor._initialized = True
|
||||||
|
|
||||||
def __call__(self, number: int) -> Optional['Card']:
|
def __call__(self, number: int) -> Optional["Card"]:
|
||||||
"""Get a card by number."""
|
"""Get a card by number."""
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
if self._deck is None:
|
if self._deck is None:
|
||||||
@@ -97,11 +100,11 @@ class CardAccessor:
|
|||||||
if card.number == number:
|
if card.number == number:
|
||||||
return card
|
return card
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def filter(self, **kwargs) -> CardList:
|
def filter(self, **kwargs) -> CardList:
|
||||||
"""
|
"""
|
||||||
Filter cards by any Card attribute.
|
Filter cards by any Card attribute.
|
||||||
|
|
||||||
Uses the universal filter from utils.filter for consistency
|
Uses the universal filter from utils.filter for consistency
|
||||||
across the entire project.
|
across the entire project.
|
||||||
|
|
||||||
@@ -122,11 +125,11 @@ class CardAccessor:
|
|||||||
if self._deck is None:
|
if self._deck is None:
|
||||||
return CardList()
|
return CardList()
|
||||||
return CardList(universal_filter(self._deck.cards, **kwargs))
|
return CardList(universal_filter(self._deck.cards, **kwargs))
|
||||||
|
|
||||||
def display_filter(self, **kwargs) -> str:
|
def display_filter(self, **kwargs) -> str:
|
||||||
"""
|
"""
|
||||||
Filter cards and display results nicely formatted.
|
Filter cards and display results nicely formatted.
|
||||||
|
|
||||||
Combines filtering and formatting in one call.
|
Combines filtering and formatting in one call.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -140,11 +143,11 @@ class CardAccessor:
|
|||||||
"""
|
"""
|
||||||
results = self.filter(**kwargs)
|
results = self.filter(**kwargs)
|
||||||
return format_results(results)
|
return format_results(results)
|
||||||
|
|
||||||
def display(self) -> str:
|
def display(self) -> str:
|
||||||
"""
|
"""
|
||||||
Format all cards in the deck for user-friendly display.
|
Format all cards in the deck for user-friendly display.
|
||||||
|
|
||||||
Returns a formatted string with each card separated by blank lines.
|
Returns a formatted string with each card separated by blank lines.
|
||||||
Nested objects are indented and separated with their own sections.
|
Nested objects are indented and separated with their own sections.
|
||||||
"""
|
"""
|
||||||
@@ -152,13 +155,13 @@ class CardAccessor:
|
|||||||
if self._deck is None:
|
if self._deck is None:
|
||||||
return "(deck not initialized)"
|
return "(deck not initialized)"
|
||||||
return _format_cards(self._deck.cards)
|
return _format_cards(self._deck.cards)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return the complete Tarot deck structure built from actual cards."""
|
"""Return the complete Tarot deck structure built from actual cards."""
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
if self._deck is None:
|
if self._deck is None:
|
||||||
return "CardAccessor (deck not initialized)"
|
return "CardAccessor (deck not initialized)"
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"Tarot Deck Structure",
|
"Tarot Deck Structure",
|
||||||
"=" * 60,
|
"=" * 60,
|
||||||
@@ -166,166 +169,168 @@ class CardAccessor:
|
|||||||
"The 78-card Tarot deck organized by structure and correspondence:",
|
"The 78-card Tarot deck organized by structure and correspondence:",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Build structure from actual cards
|
# Build structure from actual cards
|
||||||
major_arcana = [c for c in self._deck.cards if c.arcana == "Major"]
|
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"]
|
minor_arcana = [c for c in self._deck.cards if c.arcana == "Minor"]
|
||||||
|
|
||||||
# Major Arcana
|
# Major Arcana
|
||||||
if major_arcana:
|
if major_arcana:
|
||||||
lines.append(f"MAJOR ARCANA ({len(major_arcana)} cards):")
|
lines.append(f"MAJOR ARCANA ({len(major_arcana)} cards):")
|
||||||
fool = next((c for c in major_arcana if c.number == 0), None)
|
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)
|
world = next((c for c in major_arcana if c.number == 21), None)
|
||||||
if fool and world:
|
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]
|
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(f" Double Letter Trumps ({len(double_letter_trumps)} cards): Cards 3-21")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Minor Arcana
|
# Minor Arcana
|
||||||
if minor_arcana:
|
if minor_arcana:
|
||||||
lines.append(f"MINOR ARCANA ({len(minor_arcana)} cards - 4 suits × 14 ranks):")
|
lines.append(f"MINOR ARCANA ({len(minor_arcana)} cards - 4 suits × 14 ranks):")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Aces
|
# 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:
|
if aces:
|
||||||
lines.append(f" ACES ({len(aces)} cards - The Root Powers):")
|
lines.append(f" ACES ({len(aces)} cards - The Root Powers):")
|
||||||
for ace in aces:
|
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(f" Ace of {suit_name}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Pips (2-10)
|
# 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:
|
if pips:
|
||||||
lines.append(f" PIPS ({len(pips)} cards - 2-10 of each suit):")
|
lines.append(f" PIPS ({len(pips)} cards - 2-10 of each suit):")
|
||||||
# Group by suit
|
# Group by suit
|
||||||
suits_dict = {}
|
suits_dict = {}
|
||||||
for pip in pips:
|
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:
|
if suit_name not in suits_dict:
|
||||||
suits_dict[suit_name] = []
|
suits_dict[suit_name] = []
|
||||||
suits_dict[suit_name].append(pip)
|
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:
|
if suit_name in suits_dict:
|
||||||
pip_nums = sorted([p.pip for p in suits_dict[suit_name]])
|
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(f" {suit_name}: {', '.join(str(n) for n in pip_nums)}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Court Cards
|
# 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:
|
if courts:
|
||||||
lines.append(f" COURT CARDS ({len(courts)} cards - 4 ranks × 4 suits):")
|
lines.append(f" COURT CARDS ({len(courts)} cards - 4 ranks × 4 suits):")
|
||||||
# Get unique ranks and their order
|
# Get unique ranks and their order
|
||||||
rank_order = {"Knight": 0, "Prince": 1, "Princess": 2, "Queen": 3}
|
rank_order = {"Knight": 0, "Prince": 1, "Princess": 2, "Queen": 3}
|
||||||
lines.append(" Rank order per suit: Knight, Prince, Princess, Queen")
|
lines.append(" Rank order per suit: Knight, Prince, Princess, Queen")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Group by suit
|
# Group by suit
|
||||||
suits_dict = {}
|
suits_dict = {}
|
||||||
for court in courts:
|
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:
|
if suit_name not in suits_dict:
|
||||||
suits_dict[suit_name] = []
|
suits_dict[suit_name] = []
|
||||||
suits_dict[suit_name].append(court)
|
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:
|
if suit_name in suits_dict:
|
||||||
suit_courts = sorted(suits_dict[suit_name],
|
suit_courts = sorted(
|
||||||
key=lambda c: rank_order.get(c.court_rank, 99))
|
suits_dict[suit_name], key=lambda c: rank_order.get(c.court_rank, 99)
|
||||||
|
)
|
||||||
court_names = [c.court_rank for c in suit_courts]
|
court_names = [c.court_rank for c in suit_courts]
|
||||||
lines.append(f" {suit_name}: {', '.join(court_names)}")
|
lines.append(f" {suit_name}: {', '.join(court_names)}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Element correspondences
|
# Element correspondences
|
||||||
lines.append("SUIT CORRESPONDENCES:")
|
lines.append("SUIT CORRESPONDENCES:")
|
||||||
suits_info = {}
|
suits_info = {}
|
||||||
for card in minor_arcana:
|
for card in minor_arcana:
|
||||||
if hasattr(card, 'suit') and card.suit:
|
if hasattr(card, "suit") and card.suit:
|
||||||
suit_name = card.suit.name if hasattr(card.suit, 'name') else str(card.suit)
|
suit_name = card.suit.name if hasattr(card.suit, "name") else str(card.suit)
|
||||||
if suit_name not in suits_info:
|
if suit_name not in suits_info:
|
||||||
# Extract element info
|
# Extract element info
|
||||||
element_name = "Unknown"
|
element_name = "Unknown"
|
||||||
if hasattr(card.suit, 'element') and card.suit.element:
|
if hasattr(card.suit, "element") and card.suit.element:
|
||||||
if hasattr(card.suit.element, 'name'):
|
if hasattr(card.suit.element, "name"):
|
||||||
element_name = card.suit.element.name
|
element_name = card.suit.element.name
|
||||||
else:
|
else:
|
||||||
element_name = str(card.suit.element)
|
element_name = str(card.suit.element)
|
||||||
|
|
||||||
# Extract zodiac signs
|
# Extract zodiac signs
|
||||||
zodiac_signs = []
|
zodiac_signs = []
|
||||||
if hasattr(card.suit, 'element') and card.suit.element:
|
if hasattr(card.suit, "element") and card.suit.element:
|
||||||
if hasattr(card.suit.element, 'zodiac_signs'):
|
if hasattr(card.suit.element, "zodiac_signs"):
|
||||||
zodiac_signs = card.suit.element.zodiac_signs
|
zodiac_signs = card.suit.element.zodiac_signs
|
||||||
|
|
||||||
# Extract keywords
|
# Extract keywords
|
||||||
keywords = []
|
keywords = []
|
||||||
if hasattr(card.suit, 'element') and card.suit.element:
|
if hasattr(card.suit, "element") and card.suit.element:
|
||||||
if hasattr(card.suit.element, 'keywords'):
|
if hasattr(card.suit.element, "keywords"):
|
||||||
keywords = card.suit.element.keywords
|
keywords = card.suit.element.keywords
|
||||||
|
|
||||||
suits_info[suit_name] = {
|
suits_info[suit_name] = {
|
||||||
'element': element_name,
|
"element": element_name,
|
||||||
'zodiac': zodiac_signs,
|
"zodiac": zodiac_signs,
|
||||||
'keywords': keywords
|
"keywords": keywords,
|
||||||
}
|
}
|
||||||
|
|
||||||
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
|
for suit_name in ["Cups", "Pentacles", "Swords", "Wands"]:
|
||||||
if suit_name in suits_info:
|
if suit_name in suits_info:
|
||||||
info = suits_info[suit_name]
|
info = suits_info[suit_name]
|
||||||
lines.append(f" {suit_name} ({info['element']}):")
|
lines.append(f" {suit_name} ({info['element']}):")
|
||||||
if info['zodiac']:
|
if info["zodiac"]:
|
||||||
lines.append(f" Zodiac: {', '.join(info['zodiac'])}")
|
lines.append(f" Zodiac: {', '.join(info['zodiac'])}")
|
||||||
if info['keywords']:
|
if info["keywords"]:
|
||||||
lines.append(f" Keywords: {', '.join(info['keywords'])}")
|
lines.append(f" Keywords: {', '.join(info['keywords'])}")
|
||||||
|
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"Total: {len(self._deck.cards)} cards")
|
lines.append(f"Total: {len(self._deck.cards)} cards")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return a nice representation of the deck accessor."""
|
"""Return a nice representation of the deck accessor."""
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
def spread(self, spread_name: str):
|
def spread(self, spread_name: str):
|
||||||
"""
|
"""
|
||||||
Draw a Tarot card reading for a spread.
|
Draw a Tarot card reading for a spread.
|
||||||
|
|
||||||
Automatically draws random cards for each position in the spread,
|
Automatically draws random cards for each position in the spread,
|
||||||
with random reversals. Returns formatted reading with card details.
|
with random reversals. Returns formatted reading with card details.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spread_name: Name of the spread (case-insensitive, underscores or spaces)
|
spread_name: Name of the spread (case-insensitive, underscores or spaces)
|
||||||
Examples: 'Celtic Cross', 'golden dawn', 'three_card', 'tree of life'
|
Examples: 'Celtic Cross', 'golden dawn', 'three_card', 'tree of life'
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SpreadReading object containing the spread and drawn cards
|
SpreadReading object containing the spread and drawn cards
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If spread name not found
|
ValueError: If spread name not found
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
print(Tarot.deck.card.spread('Celtic Cross'))
|
print(Tarot.deck.card.spread('Celtic Cross'))
|
||||||
print(Tarot.deck.card.spread('golden dawn'))
|
print(Tarot.deck.card.spread('golden dawn'))
|
||||||
print(Tarot.deck.card.spread('three card'))
|
print(Tarot.deck.card.spread('three card'))
|
||||||
print(Tarot.deck.card.spread('tree of life'))
|
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
|
# Initialize deck if needed
|
||||||
self._ensure_initialized()
|
self._ensure_initialized()
|
||||||
|
|
||||||
# Create spread object
|
# Create spread object
|
||||||
spread = Spread(spread_name)
|
spread = Spread(spread_name)
|
||||||
|
|
||||||
# Draw cards for the spread
|
# Draw cards for the spread
|
||||||
drawn_cards = draw_spread(spread, self._deck.cards if self._deck else None)
|
drawn_cards = draw_spread(spread, self._deck.cards if self._deck else None)
|
||||||
|
|
||||||
# Create and return reading
|
# Create and return reading
|
||||||
reading = SpreadReading(spread, drawn_cards)
|
reading = SpreadReading(spread, drawn_cards)
|
||||||
return reading
|
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:
|
Usage:
|
||||||
from tarot.card.image_loader import load_deck_images
|
from tarot.card.image_loader import load_deck_images
|
||||||
|
|
||||||
deck = Deck()
|
deck = Deck()
|
||||||
count = load_deck_images(deck, "/path/to/deck/folder")
|
count = load_deck_images(deck, "/path/to/deck/folder")
|
||||||
print(f"Loaded {count} card images")
|
print(f"Loaded {count} card images")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
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:
|
if TYPE_CHECKING:
|
||||||
from tarot.deck import Card, Deck
|
from tarot.deck import Card, Deck
|
||||||
@@ -25,58 +24,62 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class ImageDeckLoader:
|
class ImageDeckLoader:
|
||||||
"""Loader for matching Tarot card images to deck cards."""
|
"""Loader for matching Tarot card images to deck cards."""
|
||||||
|
|
||||||
# Supported image extensions
|
# Supported image extensions
|
||||||
SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
|
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
|
||||||
|
|
||||||
# Regex patterns for file matching
|
# 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:
|
def __init__(self, deck_folder: str) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the image deck loader.
|
Initialize the image deck loader.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
deck_folder: Path to the folder containing card images
|
deck_folder: Path to the folder containing card images
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If folder doesn't exist or is not a directory
|
ValueError: If folder doesn't exist or is not a directory
|
||||||
"""
|
"""
|
||||||
self.deck_folder = Path(deck_folder)
|
self.deck_folder = Path(deck_folder)
|
||||||
|
|
||||||
if not self.deck_folder.exists():
|
if not self.deck_folder.exists():
|
||||||
raise ValueError(f"Deck folder does not exist: {deck_folder}")
|
raise ValueError(f"Deck folder does not exist: {deck_folder}")
|
||||||
|
|
||||||
if not self.deck_folder.is_dir():
|
if not self.deck_folder.is_dir():
|
||||||
raise ValueError(f"Deck path is not a directory: {deck_folder}")
|
raise ValueError(f"Deck path is not a directory: {deck_folder}")
|
||||||
|
|
||||||
self.image_files = self._scan_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()
|
self._build_mapping()
|
||||||
|
|
||||||
def _scan_folder(self) -> List[Path]:
|
def _scan_folder(self) -> List[Path]:
|
||||||
"""Scan folder for image files."""
|
"""Scan folder for image files."""
|
||||||
images = []
|
images = []
|
||||||
for ext in self.SUPPORTED_EXTENSIONS:
|
for ext in self.SUPPORTED_EXTENSIONS:
|
||||||
images.extend(self.deck_folder.glob(f'*{ext}'))
|
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.upper()}"))
|
||||||
|
|
||||||
# Sort by filename for consistent ordering
|
# Sort by filename for consistent ordering
|
||||||
return sorted(images)
|
return sorted(images)
|
||||||
|
|
||||||
def _parse_filename(self, filename: str) -> Tuple[Optional[int], Optional[str], bool]:
|
def _parse_filename(self, filename: str) -> Tuple[Optional[int], Optional[str], bool]:
|
||||||
"""
|
"""
|
||||||
Parse image filename to extract card number and optional custom name.
|
Parse image filename to extract card number and optional custom name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: The filename (without path)
|
filename: The filename (without path)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (card_number, custom_name, has_custom_name)
|
Tuple of (card_number, custom_name, has_custom_name)
|
||||||
- card_number: Parsed number if found, else None
|
- card_number: Parsed number if found, else None
|
||||||
- custom_name: Custom name if present (e.g., "foolish" from "00_foolish.jpg")
|
- custom_name: Custom name if present (e.g., "foolish" from "00_foolish.jpg")
|
||||||
- has_custom_name: True if custom name was found
|
- has_custom_name: True if custom name was found
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
"0.jpg" -> (0, None, False)
|
"0.jpg" -> (0, None, False)
|
||||||
"00_foolish.jpg" -> (0, "foolish", True)
|
"00_foolish.jpg" -> (0, "foolish", True)
|
||||||
@@ -84,37 +87,37 @@ class ImageDeckLoader:
|
|||||||
"invalid.jpg" -> (None, None, False)
|
"invalid.jpg" -> (None, None, False)
|
||||||
"""
|
"""
|
||||||
match = self.NUMBERED_PATTERN.match(filename)
|
match = self.NUMBERED_PATTERN.match(filename)
|
||||||
|
|
||||||
if not match:
|
if not match:
|
||||||
return None, None, False
|
return None, None, False
|
||||||
|
|
||||||
card_number = int(match.group(1))
|
card_number = int(match.group(1))
|
||||||
custom_name = match.group(2)
|
custom_name = match.group(2)
|
||||||
has_custom_name = custom_name is not None
|
has_custom_name = custom_name is not None
|
||||||
|
|
||||||
return card_number, custom_name, has_custom_name
|
return card_number, custom_name, has_custom_name
|
||||||
|
|
||||||
def _build_mapping(self) -> None:
|
def _build_mapping(self) -> None:
|
||||||
"""Build mapping from card numbers to image file paths."""
|
"""Build mapping from card numbers to image file paths."""
|
||||||
for image_path in self.image_files:
|
for image_path in self.image_files:
|
||||||
card_num, custom_name, has_custom_name = self._parse_filename(image_path.name)
|
card_num, custom_name, has_custom_name = self._parse_filename(image_path.name)
|
||||||
|
|
||||||
if card_num is not None:
|
if card_num is not None:
|
||||||
# Store path and whether it has a custom name
|
# Store path and whether it has a custom name
|
||||||
self.card_mapping[card_num] = (str(image_path), has_custom_name)
|
self.card_mapping[card_num] = (str(image_path), has_custom_name)
|
||||||
|
|
||||||
def _normalize_card_name(self, name: str) -> str:
|
def _normalize_card_name(self, name: str) -> str:
|
||||||
"""
|
"""
|
||||||
Normalize card name for matching.
|
Normalize card name for matching.
|
||||||
|
|
||||||
Converts to lowercase, removes special characters, collapses whitespace.
|
Converts to lowercase, removes special characters, collapses whitespace.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Original card name
|
name: Original card name
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Normalized name
|
Normalized name
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
"The Fool" -> "the fool"
|
"The Fool" -> "the fool"
|
||||||
"Princess of Swords" -> "princess of swords"
|
"Princess of Swords" -> "princess of swords"
|
||||||
@@ -122,69 +125,69 @@ class ImageDeckLoader:
|
|||||||
"""
|
"""
|
||||||
# Convert to lowercase
|
# Convert to lowercase
|
||||||
normalized = name.lower()
|
normalized = name.lower()
|
||||||
|
|
||||||
# Replace special characters with spaces
|
# Replace special characters with spaces
|
||||||
normalized = re.sub(r'[^\w\s]', ' ', normalized)
|
normalized = re.sub(r"[^\w\s]", " ", normalized)
|
||||||
|
|
||||||
# Collapse multiple spaces
|
# Collapse multiple spaces
|
||||||
normalized = re.sub(r'\s+', ' ', normalized).strip()
|
normalized = re.sub(r"\s+", " ", normalized).strip()
|
||||||
|
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
def _find_fuzzy_match(self, card_name_normalized: str) -> Optional[int]:
|
def _find_fuzzy_match(self, card_name_normalized: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
Find matching card number using fuzzy name matching.
|
Find matching card number using fuzzy name matching.
|
||||||
|
|
||||||
This is a fallback when card names don't parse as numbers.
|
This is a fallback when card names don't parse as numbers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card_name_normalized: Normalized card name
|
card_name_normalized: Normalized card name
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Card number if a match is found, else None
|
Card number if a match is found, else None
|
||||||
"""
|
"""
|
||||||
best_match = None
|
best_match = None
|
||||||
best_score = 0
|
best_score = 0
|
||||||
threshold = 0.6
|
threshold = 0.6
|
||||||
|
|
||||||
# Check all parsed custom names
|
# Check all parsed custom names
|
||||||
for card_num, (_, has_custom_name) in self.card_mapping.items():
|
for card_num, (_, has_custom_name) in self.card_mapping.items():
|
||||||
if not has_custom_name:
|
if not has_custom_name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get the actual filename to extract custom name
|
# Get the actual filename to extract custom name
|
||||||
for image_path in self.image_files:
|
for image_path in self.image_files:
|
||||||
parsed_num, custom_name, _ = self._parse_filename(image_path.name)
|
parsed_num, custom_name, _ = self._parse_filename(image_path.name)
|
||||||
|
|
||||||
if parsed_num == card_num and custom_name:
|
if parsed_num == card_num and custom_name:
|
||||||
normalized_custom = self._normalize_card_name(custom_name)
|
normalized_custom = self._normalize_card_name(custom_name)
|
||||||
|
|
||||||
# Simple similarity score: words that match
|
# Simple similarity score: words that match
|
||||||
query_words = set(card_name_normalized.split())
|
query_words = set(card_name_normalized.split())
|
||||||
custom_words = set(normalized_custom.split())
|
custom_words = set(normalized_custom.split())
|
||||||
|
|
||||||
if query_words and custom_words:
|
if query_words and custom_words:
|
||||||
intersection = len(query_words & custom_words)
|
intersection = len(query_words & custom_words)
|
||||||
union = len(query_words | custom_words)
|
union = len(query_words | custom_words)
|
||||||
score = intersection / union if union > 0 else 0
|
score = intersection / union if union > 0 else 0
|
||||||
|
|
||||||
if score > best_score and score >= threshold:
|
if score > best_score and score >= threshold:
|
||||||
best_score = score
|
best_score = score
|
||||||
best_match = card_num
|
best_match = card_num
|
||||||
|
|
||||||
return best_match
|
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.
|
Get the image path for a specific card.
|
||||||
|
|
||||||
Matches cards by:
|
Matches cards by:
|
||||||
1. Card number (primary method)
|
1. Card number (primary method)
|
||||||
2. Fuzzy matching on card name (fallback)
|
2. Fuzzy matching on card name (fallback)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card: The Card object to find an image for
|
card: The Card object to find an image for
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Full path to image file, or None if not found
|
Full path to image file, or None if not found
|
||||||
"""
|
"""
|
||||||
@@ -192,80 +195,80 @@ class ImageDeckLoader:
|
|||||||
if card.number in self.card_mapping:
|
if card.number in self.card_mapping:
|
||||||
path, _ = self.card_mapping[card.number]
|
path, _ = self.card_mapping[card.number]
|
||||||
return path
|
return path
|
||||||
|
|
||||||
# Try fuzzy match on name as fallback
|
# Try fuzzy match on name as fallback
|
||||||
normalized_name = self._normalize_card_name(card.name)
|
normalized_name = self._normalize_card_name(card.name)
|
||||||
fuzzy_match = self._find_fuzzy_match(normalized_name)
|
fuzzy_match = self._find_fuzzy_match(normalized_name)
|
||||||
|
|
||||||
if fuzzy_match is not None and fuzzy_match in self.card_mapping:
|
if fuzzy_match is not None and fuzzy_match in self.card_mapping:
|
||||||
path, _ = self.card_mapping[fuzzy_match]
|
path, _ = self.card_mapping[fuzzy_match]
|
||||||
return path
|
return path
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def should_override_name(self, card_number: int) -> bool:
|
def should_override_name(self, card_number: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if card name should be overridden from filename.
|
Check if card name should be overridden from filename.
|
||||||
|
|
||||||
Returns True only if:
|
Returns True only if:
|
||||||
- Image file has a custom name component (##_name.jpg format)
|
- Image file has a custom name component (##_name.jpg format)
|
||||||
- Not just a plain number (##.jpg format)
|
- Not just a plain number (##.jpg format)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card_number: The card's number
|
card_number: The card's number
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if name should be overridden from filename, False otherwise
|
True if name should be overridden from filename, False otherwise
|
||||||
"""
|
"""
|
||||||
if card_number not in self.card_mapping:
|
if card_number not in self.card_mapping:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_, has_custom_name = self.card_mapping[card_number]
|
_, has_custom_name = self.card_mapping[card_number]
|
||||||
return has_custom_name
|
return has_custom_name
|
||||||
|
|
||||||
def get_custom_name(self, card_number: int) -> Optional[str]:
|
def get_custom_name(self, card_number: int) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Get the custom card name from the filename.
|
Get the custom card name from the filename.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card_number: The card's number
|
card_number: The card's number
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Custom name if present, None otherwise
|
Custom name if present, None otherwise
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
If filename is "00_the_foolish.jpg", returns "the_foolish"
|
If filename is "00_the_foolish.jpg", returns "the_foolish"
|
||||||
If filename is "00.jpg", returns None
|
If filename is "00.jpg", returns None
|
||||||
"""
|
"""
|
||||||
if card_number not in self.card_mapping:
|
if card_number not in self.card_mapping:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Find the image file for this card number
|
# Find the image file for this card number
|
||||||
for image_path in self.image_files:
|
for image_path in self.image_files:
|
||||||
_, custom_name, _ = self._parse_filename(image_path.name)
|
_, custom_name, _ = self._parse_filename(image_path.name)
|
||||||
|
|
||||||
parsed_num, _, _ = self._parse_filename(image_path.name)
|
parsed_num, _, _ = self._parse_filename(image_path.name)
|
||||||
if parsed_num == card_number and custom_name:
|
if parsed_num == card_number and custom_name:
|
||||||
# Convert underscore-separated name to title case
|
# Convert underscore-separated name to title case
|
||||||
name_words = custom_name.split('_')
|
name_words = custom_name.split("_")
|
||||||
return ' '.join(word.capitalize() for word in name_words)
|
return " ".join(word.capitalize() for word in name_words)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def load_into_deck(self, deck: 'Deck',
|
def load_into_deck(
|
||||||
override_names: bool = True,
|
self, deck: "Deck", override_names: bool = True, verbose: bool = False
|
||||||
verbose: bool = False) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Load image paths into all cards in a deck.
|
Load image paths into all cards in a deck.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
deck: The Deck to load images into
|
deck: The Deck to load images into
|
||||||
override_names: If True, use custom names from filenames when available
|
override_names: If True, use custom names from filenames when available
|
||||||
verbose: If True, print progress information
|
verbose: If True, print progress information
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of cards that had images loaded
|
Number of cards that had images loaded
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> loader = ImageDeckLoader("/path/to/deck")
|
>>> loader = ImageDeckLoader("/path/to/deck")
|
||||||
>>> deck = Deck()
|
>>> deck = Deck()
|
||||||
@@ -273,14 +276,14 @@ class ImageDeckLoader:
|
|||||||
>>> print(f"Loaded {count} card images")
|
>>> print(f"Loaded {count} card images")
|
||||||
"""
|
"""
|
||||||
loaded_count = 0
|
loaded_count = 0
|
||||||
|
|
||||||
for card in deck.cards:
|
for card in deck.cards:
|
||||||
image_path = self.get_image_path(card)
|
image_path = self.get_image_path(card)
|
||||||
|
|
||||||
if image_path:
|
if image_path:
|
||||||
card.image_path = image_path
|
card.image_path = image_path
|
||||||
loaded_count += 1
|
loaded_count += 1
|
||||||
|
|
||||||
# Override name if appropriate
|
# Override name if appropriate
|
||||||
if override_names and self.should_override_name(card.number):
|
if override_names and self.should_override_name(card.number):
|
||||||
custom_name = self.get_custom_name(card.number)
|
custom_name = self.get_custom_name(card.number)
|
||||||
@@ -290,54 +293,53 @@ class ImageDeckLoader:
|
|||||||
card.name = custom_name
|
card.name = custom_name
|
||||||
elif verbose:
|
elif verbose:
|
||||||
print(f" ✓ {card.number}: {card.name}")
|
print(f" ✓ {card.number}: {card.name}")
|
||||||
|
|
||||||
return loaded_count
|
return loaded_count
|
||||||
|
|
||||||
def get_summary(self) -> Dict[str, any]:
|
def get_summary(self) -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
Get a summary of loaded images and statistics.
|
Get a summary of loaded images and statistics.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with loader statistics
|
Dictionary with loader statistics
|
||||||
"""
|
"""
|
||||||
total_images = len(self.image_files)
|
total_images = len(self.image_files)
|
||||||
mapped_cards = len(self.card_mapping)
|
mapped_cards = len(self.card_mapping)
|
||||||
custom_named = sum(1 for _, has_custom in self.card_mapping.values() if has_custom)
|
custom_named = sum(1 for _, has_custom in self.card_mapping.values() if has_custom)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'deck_folder': str(self.deck_folder),
|
"deck_folder": str(self.deck_folder),
|
||||||
'total_image_files': total_images,
|
"total_image_files": total_images,
|
||||||
'total_image_filenames': len(set(f.name for f in self.image_files)),
|
"total_image_filenames": len(set(f.name for f in self.image_files)),
|
||||||
'mapped_card_numbers': mapped_cards,
|
"mapped_card_numbers": mapped_cards,
|
||||||
'cards_with_custom_names': custom_named,
|
"cards_with_custom_names": custom_named,
|
||||||
'cards_with_generic_numbers': mapped_cards - custom_named,
|
"cards_with_generic_numbers": mapped_cards - custom_named,
|
||||||
'image_extensions_found': list(set(f.suffix.lower() for f in self.image_files)),
|
"image_extensions_found": list(set(f.suffix.lower() for f in self.image_files)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def load_deck_images(deck: 'Deck',
|
def load_deck_images(
|
||||||
deck_folder: str,
|
deck: "Deck", deck_folder: str, override_names: bool = True, verbose: bool = False
|
||||||
override_names: bool = True,
|
) -> int:
|
||||||
verbose: bool = False) -> int:
|
|
||||||
"""
|
"""
|
||||||
Convenience function to load deck images.
|
Convenience function to load deck images.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
deck: The Deck object to load images into
|
deck: The Deck object to load images into
|
||||||
deck_folder: Path to folder containing card images
|
deck_folder: Path to folder containing card images
|
||||||
override_names: If True, use custom names from filenames when available
|
override_names: If True, use custom names from filenames when available
|
||||||
verbose: If True, print progress information
|
verbose: If True, print progress information
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of cards that had images loaded
|
Number of cards that had images loaded
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If deck_folder doesn't exist or is invalid
|
ValueError: If deck_folder doesn't exist or is invalid
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Deck
|
>>> from tarot import Deck
|
||||||
>>> from tarot.card.image_loader import load_deck_images
|
>>> from tarot.card.image_loader import load_deck_images
|
||||||
>>>
|
>>>
|
||||||
>>> deck = Deck()
|
>>> deck = Deck()
|
||||||
>>> count = load_deck_images(deck, "/path/to/deck/images")
|
>>> count = load_deck_images(deck, "/path/to/deck/images")
|
||||||
>>> print(f"Loaded {count} card images")
|
>>> print(f"Loaded {count} card images")
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ into Card objects, supporting both individual cards and full decks.
|
|||||||
Usage:
|
Usage:
|
||||||
from tarot.card.loader import load_card_details, load_deck_details
|
from tarot.card.loader import load_card_details, load_deck_details
|
||||||
from tarot.card.details import CardDetailsRegistry
|
from tarot.card.details import CardDetailsRegistry
|
||||||
|
|
||||||
# Load single card
|
# Load single card
|
||||||
loader = CardDetailsRegistry()
|
loader = CardDetailsRegistry()
|
||||||
card = my_deck.minor.swords(11)
|
card = my_deck.minor.swords(11)
|
||||||
load_card_details(card, loader)
|
load_card_details(card, loader)
|
||||||
|
|
||||||
# Load entire deck
|
# Load entire deck
|
||||||
load_deck_details(my_deck, loader)
|
load_deck_details(my_deck, loader)
|
||||||
"""
|
"""
|
||||||
@@ -24,20 +24,17 @@ if TYPE_CHECKING:
|
|||||||
from tarot.deck import Deck
|
from tarot.deck import Deck
|
||||||
|
|
||||||
|
|
||||||
def load_card_details(
|
def load_card_details(card: "Card", registry: Optional["CardDetailsRegistry"] = None) -> bool:
|
||||||
card: 'Card',
|
|
||||||
registry: Optional['CardDetailsRegistry'] = None
|
|
||||||
) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Load details for a single card from the registry.
|
Load details for a single card from the registry.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card: The Card object to populate with details
|
card: The Card object to populate with details
|
||||||
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if details were found and loaded, False otherwise
|
True if details were found and loaded, False otherwise
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Deck
|
>>> from tarot import Deck
|
||||||
>>> deck = Deck()
|
>>> deck = Deck()
|
||||||
@@ -49,27 +46,26 @@ def load_card_details(
|
|||||||
"""
|
"""
|
||||||
if registry is None:
|
if registry is None:
|
||||||
from tarot.card.details import CardDetailsRegistry
|
from tarot.card.details import CardDetailsRegistry
|
||||||
|
|
||||||
registry = CardDetailsRegistry()
|
registry = CardDetailsRegistry()
|
||||||
|
|
||||||
return registry.load_into_card(card)
|
return registry.load_into_card(card)
|
||||||
|
|
||||||
|
|
||||||
def load_deck_details(
|
def load_deck_details(
|
||||||
deck: 'Deck',
|
deck: "Deck", registry: Optional["CardDetailsRegistry"] = None, verbose: bool = False
|
||||||
registry: Optional['CardDetailsRegistry'] = None,
|
|
||||||
verbose: bool = False
|
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Load details for all cards in a deck.
|
Load details for all cards in a deck.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
deck: The Deck object containing cards to populate
|
deck: The Deck object containing cards to populate
|
||||||
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
||||||
verbose: If True, prints information about each card loaded
|
verbose: If True, prints information about each card loaded
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of cards successfully loaded with details
|
Number of cards successfully loaded with details
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Deck
|
>>> from tarot import Deck
|
||||||
>>> deck = Deck()
|
>>> deck = Deck()
|
||||||
@@ -78,11 +74,12 @@ def load_deck_details(
|
|||||||
"""
|
"""
|
||||||
if registry is None:
|
if registry is None:
|
||||||
from tarot.card.details import CardDetailsRegistry
|
from tarot.card.details import CardDetailsRegistry
|
||||||
|
|
||||||
registry = CardDetailsRegistry()
|
registry = CardDetailsRegistry()
|
||||||
|
|
||||||
loaded_count = 0
|
loaded_count = 0
|
||||||
failed_cards = []
|
failed_cards = []
|
||||||
|
|
||||||
# Load all cards from the deck
|
# Load all cards from the deck
|
||||||
for card in deck.cards:
|
for card in deck.cards:
|
||||||
if load_card_details(card, registry):
|
if load_card_details(card, registry):
|
||||||
@@ -93,29 +90,26 @@ def load_deck_details(
|
|||||||
failed_cards.append(card.name)
|
failed_cards.append(card.name)
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"✗ Failed: {card.name}")
|
print(f"✗ Failed: {card.name}")
|
||||||
|
|
||||||
if verbose and failed_cards:
|
if verbose and failed_cards:
|
||||||
print(f"\n{len(failed_cards)} cards failed to load:")
|
print(f"\n{len(failed_cards)} cards failed to load:")
|
||||||
for name in failed_cards:
|
for name in failed_cards:
|
||||||
print(f" - {name}")
|
print(f" - {name}")
|
||||||
|
|
||||||
return loaded_count
|
return loaded_count
|
||||||
|
|
||||||
|
|
||||||
def get_cards_by_suit(
|
def get_cards_by_suit(deck: "Deck", suit_name: str) -> List["Card"]:
|
||||||
deck: 'Deck',
|
|
||||||
suit_name: str
|
|
||||||
) -> List['Card']:
|
|
||||||
"""
|
"""
|
||||||
Get all cards from a specific suit in the deck.
|
Get all cards from a specific suit in the deck.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
deck: The Deck object
|
deck: The Deck object
|
||||||
suit_name: The suit name ("Cups", "Pentacles", "Swords", "Wands")
|
suit_name: The suit name ("Cups", "Pentacles", "Swords", "Wands")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of Card objects from that suit
|
List of Card objects from that suit
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Deck
|
>>> from tarot import Deck
|
||||||
>>> from tarot.card.loader import get_cards_by_suit
|
>>> from tarot.card.loader import get_cards_by_suit
|
||||||
@@ -124,29 +118,29 @@ def get_cards_by_suit(
|
|||||||
>>> print(len(swords)) # Should be 14
|
>>> print(len(swords)) # Should be 14
|
||||||
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
|
# Deck has a suit method, use it
|
||||||
return deck.suit(suit_name)
|
return deck.suit(suit_name)
|
||||||
|
|
||||||
# Fallback: filter cards manually
|
# Fallback: filter cards manually
|
||||||
return [card for card in deck.cards if hasattr(card, 'suit') and
|
return [
|
||||||
card.suit and card.suit.name == suit_name]
|
card
|
||||||
|
for card in deck.cards
|
||||||
|
if hasattr(card, "suit") and card.suit and card.suit.name == suit_name
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def filter_cards_by_keywords(
|
def filter_cards_by_keywords(cards: List["Card"], keyword: str) -> List["Card"]:
|
||||||
cards: List['Card'],
|
|
||||||
keyword: str
|
|
||||||
) -> List['Card']:
|
|
||||||
"""
|
"""
|
||||||
Filter a list of cards by keyword.
|
Filter a list of cards by keyword.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cards: List of Card objects to filter
|
cards: List of Card objects to filter
|
||||||
keyword: The keyword to search for (case-insensitive)
|
keyword: The keyword to search for (case-insensitive)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of cards that have the keyword
|
List of cards that have the keyword
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Deck
|
>>> from tarot import Deck
|
||||||
>>> deck = Deck()
|
>>> deck = Deck()
|
||||||
@@ -155,20 +149,22 @@ def filter_cards_by_keywords(
|
|||||||
"""
|
"""
|
||||||
keyword_lower = keyword.lower()
|
keyword_lower = keyword.lower()
|
||||||
return [
|
return [
|
||||||
card for card in cards
|
card
|
||||||
if hasattr(card, 'keywords') and card.keywords and
|
for card in cards
|
||||||
any(keyword_lower in kw.lower() for kw in card.keywords)
|
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.
|
Pretty print card details to console.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card: The Card object to print
|
card: The Card object to print
|
||||||
include_reversed: If True, also print reversed keywords and interpretation
|
include_reversed: If True, also print reversed keywords and interpretation
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Deck
|
>>> from tarot import Deck
|
||||||
>>> deck = Deck()
|
>>> deck = Deck()
|
||||||
@@ -178,35 +174,35 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
|
|||||||
print(f"\n{'=' * 60}")
|
print(f"\n{'=' * 60}")
|
||||||
print(f" {card.name}")
|
print(f" {card.name}")
|
||||||
print(f"{'=' * 60}")
|
print(f"{'=' * 60}")
|
||||||
|
|
||||||
# Define attributes to print with their formatting
|
# Define attributes to print with their formatting
|
||||||
attributes = {
|
attributes = {
|
||||||
'explanation': ('Explanation', False),
|
"explanation": ("Explanation", False),
|
||||||
'interpretation': ('Interpretation', False),
|
"interpretation": ("Interpretation", False),
|
||||||
'guidance': ('Guidance', False),
|
"guidance": ("Guidance", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add reversed attributes only if requested
|
# Add reversed attributes only if requested
|
||||||
if include_reversed:
|
if include_reversed:
|
||||||
attributes['reversed_interpretation'] = ('Reversed Interpretation', False)
|
attributes["reversed_interpretation"] = ("Reversed Interpretation", False)
|
||||||
|
|
||||||
# List attributes (joined with commas)
|
# List attributes (joined with commas)
|
||||||
list_attributes = {
|
list_attributes = {
|
||||||
'keywords': 'Keywords',
|
"keywords": "Keywords",
|
||||||
'reversed_keywords': ('Reversed Keywords', include_reversed),
|
"reversed_keywords": ("Reversed Keywords", include_reversed),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Numeric attributes
|
# Numeric attributes
|
||||||
numeric_attributes = {
|
numeric_attributes = {
|
||||||
'numerology': 'Numerology',
|
"numerology": "Numerology",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Print text attributes
|
# Print text attributes
|
||||||
for attr_name, (display_name, _) in attributes.items():
|
for attr_name, (display_name, _) in attributes.items():
|
||||||
if hasattr(card, attr_name):
|
if hasattr(card, attr_name):
|
||||||
value = getattr(card, attr_name)
|
value = getattr(card, attr_name)
|
||||||
if value:
|
if value:
|
||||||
if attr_name == 'explanation' and isinstance(value, dict):
|
if attr_name == "explanation" and isinstance(value, dict):
|
||||||
print(f"\n{display_name}:")
|
print(f"\n{display_name}:")
|
||||||
if "summary" in value:
|
if "summary" in value:
|
||||||
print(f"Summary: {value['summary']}")
|
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}")
|
print(f"{k.capitalize()}: {v}")
|
||||||
else:
|
else:
|
||||||
print(f"\n{display_name}:\n{value}")
|
print(f"\n{display_name}:\n{value}")
|
||||||
|
|
||||||
# Print list attributes
|
# Print list attributes
|
||||||
for attr_name, display_info in list_attributes.items():
|
for attr_name, display_info in list_attributes.items():
|
||||||
if isinstance(display_info, tuple):
|
if isinstance(display_info, tuple):
|
||||||
@@ -227,36 +223,35 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
display_name = display_info
|
display_name = display_info
|
||||||
|
|
||||||
if hasattr(card, attr_name):
|
if hasattr(card, attr_name):
|
||||||
value = getattr(card, attr_name)
|
value = getattr(card, attr_name)
|
||||||
if value:
|
if value:
|
||||||
print(f"\n{display_name}: {', '.join(value)}")
|
print(f"\n{display_name}: {', '.join(value)}")
|
||||||
|
|
||||||
# Print numeric attributes
|
# Print numeric attributes
|
||||||
for attr_name, display_name in numeric_attributes.items():
|
for attr_name, display_name in numeric_attributes.items():
|
||||||
if hasattr(card, attr_name):
|
if hasattr(card, attr_name):
|
||||||
value = getattr(card, attr_name)
|
value = getattr(card, attr_name)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
print(f"\n{display_name}: {value}")
|
print(f"\n{display_name}: {value}")
|
||||||
|
|
||||||
print(f"\n{'=' * 60}\n")
|
print(f"\n{'=' * 60}\n")
|
||||||
|
|
||||||
|
|
||||||
def get_card_info(
|
def get_card_info(
|
||||||
card_name: str,
|
card_name: str, registry: Optional["CardDetailsRegistry"] = None
|
||||||
registry: Optional['CardDetailsRegistry'] = None
|
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Get card information by card name.
|
Get card information by card name.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
card_name: The name of the card (e.g., "Princess of Swords")
|
card_name: The name of the card (e.g., "Princess of Swords")
|
||||||
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing card details, or None if not found
|
Dictionary containing card details, or None if not found
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot.card.loader import get_card_info
|
>>> from tarot.card.loader import get_card_info
|
||||||
>>> info = get_card_info("Princess of Swords")
|
>>> info = get_card_info("Princess of Swords")
|
||||||
@@ -265,6 +260,7 @@ def get_card_info(
|
|||||||
"""
|
"""
|
||||||
if registry is None:
|
if registry is None:
|
||||||
from tarot.card.details import CardDetailsRegistry
|
from tarot.card.details import CardDetailsRegistry
|
||||||
|
|
||||||
registry = CardDetailsRegistry()
|
registry = CardDetailsRegistry()
|
||||||
|
|
||||||
return registry.get(card_name)
|
return registry.get(card_name)
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ with position meanings and automatic card drawing.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import Tarot
|
from tarot import Tarot
|
||||||
|
|
||||||
# Draw cards for a spread
|
# Draw cards for a spread
|
||||||
reading = Tarot.deck.card.spread('Celtic Cross')
|
reading = Tarot.deck.card.spread('Celtic Cross')
|
||||||
print(reading)
|
print(reading)
|
||||||
|
|
||||||
# Can also access spread with/without cards
|
# Can also access spread with/without cards
|
||||||
from tarot.card.spread import Spread, draw_spread
|
from tarot.card.spread import Spread, draw_spread
|
||||||
|
|
||||||
spread = Spread('Celtic Cross')
|
spread = Spread('Celtic Cross')
|
||||||
reading = draw_spread(spread) # Returns list of (position, card) tuples
|
reading = draw_spread(spread) # Returns list of (position, card) tuples
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import random
|
import random
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tarot.card import Card
|
from tarot.card import Card
|
||||||
@@ -29,11 +29,12 @@ if TYPE_CHECKING:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SpreadPosition:
|
class SpreadPosition:
|
||||||
"""Represents a position in a Tarot spread."""
|
"""Represents a position in a Tarot spread."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
meaning: str
|
meaning: str
|
||||||
reversed_meaning: Optional[str] = None
|
reversed_meaning: Optional[str] = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
result = f"{self.number}. {self.name}: {self.meaning}"
|
result = f"{self.number}. {self.name}: {self.meaning}"
|
||||||
if self.reversed_meaning:
|
if self.reversed_meaning:
|
||||||
@@ -44,192 +45,194 @@ class SpreadPosition:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class DrawnCard:
|
class DrawnCard:
|
||||||
"""Represents a card drawn for a spread position."""
|
"""Represents a card drawn for a spread position."""
|
||||||
|
|
||||||
position: SpreadPosition
|
position: SpreadPosition
|
||||||
card: 'Card'
|
card: "Card"
|
||||||
is_reversed: bool
|
is_reversed: bool
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Format the drawn card with position and interpretation."""
|
"""Format the drawn card with position and interpretation."""
|
||||||
card_name = self.card.name
|
card_name = self.card.name
|
||||||
if self.is_reversed:
|
if self.is_reversed:
|
||||||
card_name += " (Reversed)"
|
card_name += " (Reversed)"
|
||||||
|
|
||||||
return f"{self.position.number}. {self.position.name}\n" \
|
return (
|
||||||
f" └─ {card_name}\n" \
|
f"{self.position.number}. {self.position.name}\n"
|
||||||
f" └─ Position: {self.position.meaning}"
|
f" └─ {card_name}\n"
|
||||||
|
f" └─ Position: {self.position.meaning}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Spread:
|
class Spread:
|
||||||
"""Represents a Tarot spread with positions and meanings."""
|
"""Represents a Tarot spread with positions and meanings."""
|
||||||
|
|
||||||
# Define all available spreads
|
# Define all available spreads
|
||||||
SPREADS: Dict[str, Dict] = {
|
SPREADS: Dict[str, Dict] = {
|
||||||
'three card': {
|
"three card": {
|
||||||
'name': '3-Card Spread',
|
"name": "3-Card Spread",
|
||||||
'description': 'Simple 3-card spread for past, present, future or situation, action, outcome',
|
"description": (
|
||||||
'positions': [
|
"Simple 3-card spread for past, present, future "
|
||||||
SpreadPosition(1, 'First Position', 'Past, Foundation, or Situation'),
|
"or situation, action, outcome"
|
||||||
SpreadPosition(2, 'Second Position', 'Present, Action, or Influence'),
|
),
|
||||||
SpreadPosition(3, 'Third Position', 'Future, Outcome, or Advice'),
|
"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': {
|
"golden dawn": {
|
||||||
'name': 'Golden Dawn 3-Card',
|
"name": "Golden Dawn 3-Card",
|
||||||
'description': 'Three card spread used in Golden Dawn tradition',
|
"description": "Three card spread used in Golden Dawn tradition",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'Supernal Triangle', 'Spiritual/Divine aspect'),
|
SpreadPosition(1, "Supernal Triangle", "Spiritual/Divine aspect"),
|
||||||
SpreadPosition(2, 'Pillar of Severity', 'Challenging/Active force'),
|
SpreadPosition(2, "Pillar of Severity", "Challenging/Active force"),
|
||||||
SpreadPosition(3, 'Pillar of Mercy', 'Supportive/Passive force'),
|
SpreadPosition(3, "Pillar of Mercy", "Supportive/Passive force"),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'celtic cross': {
|
"celtic cross": {
|
||||||
'name': 'Celtic Cross',
|
"name": "Celtic Cross",
|
||||||
'description': 'Classic 10-card spread for in-depth reading',
|
"description": "Classic 10-card spread for in-depth reading",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'The Significator', 'The main situation or person'),
|
SpreadPosition(1, "The Significator", "The main situation or person"),
|
||||||
SpreadPosition(2, 'The Cross', 'The challenge or heart of the matter'),
|
SpreadPosition(2, "The Cross", "The challenge or heart of the matter"),
|
||||||
SpreadPosition(3, 'Crowning Influence', 'Conscious hopes/ideals'),
|
SpreadPosition(3, "Crowning Influence", "Conscious hopes/ideals"),
|
||||||
SpreadPosition(4, 'Beneath the Cross', 'Unconscious or hidden aspects'),
|
SpreadPosition(4, "Beneath the Cross", "Unconscious or hidden aspects"),
|
||||||
SpreadPosition(5, 'Behind', 'Past influences'),
|
SpreadPosition(5, "Behind", "Past influences"),
|
||||||
SpreadPosition(6, 'Before', 'Future influences'),
|
SpreadPosition(6, "Before", "Future influences"),
|
||||||
SpreadPosition(7, 'Self/Attitude', 'How the querent sees themselves'),
|
SpreadPosition(7, "Self/Attitude", "How the querent sees themselves"),
|
||||||
SpreadPosition(8, 'Others/Environment', 'External factors/opinions'),
|
SpreadPosition(8, "Others/Environment", "External factors/opinions"),
|
||||||
SpreadPosition(9, 'Hopes and Fears', 'What the querent hopes for or fears'),
|
SpreadPosition(9, "Hopes and Fears", "What the querent hopes for or fears"),
|
||||||
SpreadPosition(10, 'Outcome', 'Final outcome or resolution'),
|
SpreadPosition(10, "Outcome", "Final outcome or resolution"),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'horseshoe': {
|
"horseshoe": {
|
||||||
'name': 'Horseshoe',
|
"name": "Horseshoe",
|
||||||
'description': '7-card spread in horseshoe formation for past, present, future insight',
|
"description": "7-card spread in horseshoe formation for past, present, future insight",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'Distant Past', 'Ancient influences and foundations'),
|
SpreadPosition(1, "Distant Past", "Ancient influences and foundations"),
|
||||||
SpreadPosition(2, 'Recent Past', 'Recent events and circumstances'),
|
SpreadPosition(2, "Recent Past", "Recent events and circumstances"),
|
||||||
SpreadPosition(3, 'Present Situation', 'Current state of affairs'),
|
SpreadPosition(3, "Present Situation", "Current state of affairs"),
|
||||||
SpreadPosition(4, 'Immediate Future', 'Near-term developments'),
|
SpreadPosition(4, "Immediate Future", "Near-term developments"),
|
||||||
SpreadPosition(5, 'Distant Future', 'Long-term outcome'),
|
SpreadPosition(5, "Distant Future", "Long-term outcome"),
|
||||||
SpreadPosition(6, 'Inner Influence', 'Self/thoughts/emotions'),
|
SpreadPosition(6, "Inner Influence", "Self/thoughts/emotions"),
|
||||||
SpreadPosition(7, 'Outer Influence', 'External forces and environment'),
|
SpreadPosition(7, "Outer Influence", "External forces and environment"),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'pentagram': {
|
"pentagram": {
|
||||||
'name': 'Pentagram',
|
"name": "Pentagram",
|
||||||
'description': '5-card spread based on Earth element pentagram',
|
"description": "5-card spread based on Earth element pentagram",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'Spirit', 'Core essence or spiritual truth'),
|
SpreadPosition(1, "Spirit", "Core essence or spiritual truth"),
|
||||||
SpreadPosition(2, 'Fire', 'Action and willpower'),
|
SpreadPosition(2, "Fire", "Action and willpower"),
|
||||||
SpreadPosition(3, 'Water', 'Emotions and intuition'),
|
SpreadPosition(3, "Water", "Emotions and intuition"),
|
||||||
SpreadPosition(4, 'Air', 'Intellect and communication'),
|
SpreadPosition(4, "Air", "Intellect and communication"),
|
||||||
SpreadPosition(5, 'Earth', 'Physical manifestation and grounding'),
|
SpreadPosition(5, "Earth", "Physical manifestation and grounding"),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'tree of life': {
|
"tree of life": {
|
||||||
'name': 'Tree of Life',
|
"name": "Tree of Life",
|
||||||
'description': '10-card spread mapping Sephiroth on the Tree of Life',
|
"description": "10-card spread mapping Sephiroth on the Tree of Life",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'Kether (Crown)', 'Divine will and unity'),
|
SpreadPosition(1, "Kether (Crown)", "Divine will and unity"),
|
||||||
SpreadPosition(2, 'Chokmah (Wisdom)', 'Creative force and impulse'),
|
SpreadPosition(2, "Chokmah (Wisdom)", "Creative force and impulse"),
|
||||||
SpreadPosition(3, 'Binah (Understanding)', 'Form and structure'),
|
SpreadPosition(3, "Binah (Understanding)", "Form and structure"),
|
||||||
SpreadPosition(4, 'Chesed (Mercy)', 'Expansion and abundance'),
|
SpreadPosition(4, "Chesed (Mercy)", "Expansion and abundance"),
|
||||||
SpreadPosition(5, 'Gevurah (Severity)', 'Reduction and discipline'),
|
SpreadPosition(5, "Gevurah (Severity)", "Reduction and discipline"),
|
||||||
SpreadPosition(6, 'Tiphareth (Beauty)', 'Core self and integration'),
|
SpreadPosition(6, "Tiphareth (Beauty)", "Core self and integration"),
|
||||||
SpreadPosition(7, 'Netzach (Victory)', 'Desire and passion'),
|
SpreadPosition(7, "Netzach (Victory)", "Desire and passion"),
|
||||||
SpreadPosition(8, 'Hod (Splendor)', 'Intellect and communication'),
|
SpreadPosition(8, "Hod (Splendor)", "Intellect and communication"),
|
||||||
SpreadPosition(9, 'Yesod (Foundation)', 'Subconscious and dreams'),
|
SpreadPosition(9, "Yesod (Foundation)", "Subconscious and dreams"),
|
||||||
SpreadPosition(10, 'Malkuth (Kingdom)', 'Manifestation and physical reality'),
|
SpreadPosition(10, "Malkuth (Kingdom)", "Manifestation and physical reality"),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'relationship': {
|
"relationship": {
|
||||||
'name': 'Relationship',
|
"name": "Relationship",
|
||||||
'description': '5-card spread for relationship insight',
|
"description": "5-card spread for relationship insight",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'You', 'Your position, feelings, or role'),
|
SpreadPosition(1, "You", "Your position, feelings, or role"),
|
||||||
SpreadPosition(2, 'Them', 'Their position, feelings, or perspective'),
|
SpreadPosition(2, "Them", "Their position, feelings, or perspective"),
|
||||||
SpreadPosition(3, 'The Relationship', 'The dynamic and connection'),
|
SpreadPosition(3, "The Relationship", "The dynamic and connection"),
|
||||||
SpreadPosition(4, 'Challenge', 'Current challenge or friction point'),
|
SpreadPosition(4, "Challenge", "Current challenge or friction point"),
|
||||||
SpreadPosition(5, 'Outcome', 'Where the relationship is heading'),
|
SpreadPosition(5, "Outcome", "Where the relationship is heading"),
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
'yes or no': {
|
"yes or no": {
|
||||||
'name': 'Yes or No',
|
"name": "Yes or No",
|
||||||
'description': '1-card spread for simple yes/no answers',
|
"description": "1-card spread for simple yes/no answers",
|
||||||
'positions': [
|
"positions": [
|
||||||
SpreadPosition(1, 'Answer', 'Major Arcana = Yes, Minor Arcana = No, Court Cards = Maybe'),
|
SpreadPosition(
|
||||||
]
|
1, "Answer", "Major Arcana = Yes, Minor Arcana = No, Court Cards = Maybe"
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, spread_name: str) -> None:
|
def __init__(self, spread_name: str) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a spread by name (case-insensitive).
|
Initialize a spread by name (case-insensitive).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spread_name: Name of the spread to use
|
spread_name: Name of the spread to use
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If spread name not found
|
ValueError: If spread name not found
|
||||||
"""
|
"""
|
||||||
# Normalize name (case-insensitive, allow underscores or spaces)
|
# Normalize name (case-insensitive, allow underscores or spaces)
|
||||||
normalized_name = spread_name.lower().replace('_', ' ')
|
normalized_name = spread_name.lower().replace("_", " ")
|
||||||
|
|
||||||
# Find matching spread
|
# Find matching spread
|
||||||
spread_data = None
|
spread_data = None
|
||||||
for key, data in self.SPREADS.items():
|
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
|
spread_data = data
|
||||||
break
|
break
|
||||||
|
|
||||||
if not spread_data:
|
if not spread_data:
|
||||||
available = ', '.join(f"'{k}'" for k in self.SPREADS.keys())
|
available = ", ".join(f"'{k}'" for k in self.SPREADS.keys())
|
||||||
raise ValueError(
|
raise ValueError(f"Spread '{spread_name}' not found. Available spreads: {available}")
|
||||||
f"Spread '{spread_name}' not found. Available spreads: {available}"
|
|
||||||
)
|
self.name = spread_data["name"]
|
||||||
|
self.description = spread_data["description"]
|
||||||
self.name = spread_data['name']
|
self.positions: List[SpreadPosition] = spread_data["positions"]
|
||||||
self.description = spread_data['description']
|
|
||||||
self.positions: List[SpreadPosition] = spread_data['positions']
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return formatted spread information."""
|
"""Return formatted spread information."""
|
||||||
lines = [
|
lines = [
|
||||||
f"═══════════════════════════════════════════",
|
"═══════════════════════════════════════════",
|
||||||
f" {self.name}",
|
f" {self.name}",
|
||||||
f"═══════════════════════════════════════════",
|
"═══════════════════════════════════════════",
|
||||||
f"",
|
"",
|
||||||
f"{self.description}",
|
f"{self.description}",
|
||||||
f"",
|
"",
|
||||||
f"Positions ({len(self.positions)} cards):",
|
f"Positions ({len(self.positions)} cards):",
|
||||||
f"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
for pos in self.positions:
|
for pos in self.positions:
|
||||||
lines.append(f" {pos}")
|
lines.append(f" {pos}")
|
||||||
|
|
||||||
lines.append(f"")
|
lines.append("")
|
||||||
lines.append(f"═══════════════════════════════════════════")
|
lines.append("═══════════════════════════════════════════")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Spread('{self.name}')"
|
return f"Spread('{self.name}')"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def available_spreads(cls) -> str:
|
def available_spreads(cls) -> str:
|
||||||
"""Return list of all available spreads."""
|
"""Return list of all available spreads."""
|
||||||
lines = [
|
lines = ["Available Tarot Spreads:", "═" * 50, ""]
|
||||||
"Available Tarot Spreads:",
|
|
||||||
"═" * 50,
|
|
||||||
""
|
|
||||||
]
|
|
||||||
|
|
||||||
for key, data in cls.SPREADS.items():
|
for key, data in cls.SPREADS.items():
|
||||||
lines.append(f" • {data['name']}")
|
lines.append(f" • {data['name']}")
|
||||||
lines.append(f" Name for API: '{key}'")
|
lines.append(f" Name for API: '{key}'")
|
||||||
lines.append(f" Positions: {len(data['positions'])}")
|
lines.append(f" Positions: {len(data['positions'])}")
|
||||||
lines.append(f" {data['description']}")
|
lines.append(f" {data['description']}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def get_position(self, position_number: int) -> Optional[SpreadPosition]:
|
def get_position(self, position_number: int) -> Optional[SpreadPosition]:
|
||||||
"""Get a specific position by number."""
|
"""Get a specific position by number."""
|
||||||
for pos in self.positions:
|
for pos in self.positions:
|
||||||
@@ -241,96 +244,94 @@ class Spread:
|
|||||||
def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
|
def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
|
||||||
"""
|
"""
|
||||||
Draw cards for all positions in a spread.
|
Draw cards for all positions in a spread.
|
||||||
|
|
||||||
Ensures all drawn cards are unique (no duplicates in a single spread).
|
Ensures all drawn cards are unique (no duplicates in a single spread).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spread: The Spread object with positions defined
|
spread: The Spread object with positions defined
|
||||||
deck: Optional list of Card objects. If None, uses Tarot.deck.cards
|
deck: Optional list of Card objects. If None, uses Tarot.deck.cards
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of DrawnCard objects (one per position) with random cards and reversals
|
List of DrawnCard objects (one per position) with random cards and reversals
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If spread has more positions than cards in the deck
|
ValueError: If spread has more positions than cards in the deck
|
||||||
"""
|
"""
|
||||||
import random
|
|
||||||
|
|
||||||
# Load deck if not provided
|
# Load deck if not provided
|
||||||
if deck is None:
|
if deck is None:
|
||||||
from tarot.deck import Deck
|
from tarot.deck import Deck
|
||||||
|
|
||||||
deck_instance = Deck()
|
deck_instance = Deck()
|
||||||
deck = deck_instance.cards
|
deck = deck_instance.cards
|
||||||
|
|
||||||
# Validate that we have enough cards to draw from without duplicates
|
# Validate that we have enough cards to draw from without duplicates
|
||||||
num_positions = len(spread.positions)
|
num_positions = len(spread.positions)
|
||||||
if num_positions > len(deck):
|
if num_positions > len(deck):
|
||||||
raise ValueError(
|
raise ValueError(f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards")
|
||||||
f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Draw unique cards using random.sample (no replacements)
|
# Draw unique cards using random.sample (no replacements)
|
||||||
drawn_deck = random.sample(deck, num_positions)
|
drawn_deck = random.sample(deck, num_positions)
|
||||||
|
|
||||||
drawn_cards = []
|
drawn_cards = []
|
||||||
for position, card in zip(spread.positions, drawn_deck):
|
for position, card in zip(spread.positions, drawn_deck):
|
||||||
# Random reversal (50% chance)
|
# Random reversal (50% chance)
|
||||||
is_reversed = random.choice([True, False])
|
is_reversed = random.choice([True, False])
|
||||||
drawn_cards.append(DrawnCard(position, card, is_reversed))
|
drawn_cards.append(DrawnCard(position, card, is_reversed))
|
||||||
|
|
||||||
return drawn_cards
|
return drawn_cards
|
||||||
|
|
||||||
|
|
||||||
class SpreadReading:
|
class SpreadReading:
|
||||||
"""Represents a complete tarot reading with cards drawn for a spread."""
|
"""Represents a complete tarot reading with cards drawn for a spread."""
|
||||||
|
|
||||||
def __init__(self, spread: Spread, drawn_cards: List[DrawnCard]) -> None:
|
def __init__(self, spread: Spread, drawn_cards: List[DrawnCard]) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a reading with a spread and drawn cards.
|
Initialize a reading with a spread and drawn cards.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
spread: The Spread object
|
spread: The Spread object
|
||||||
drawn_cards: List of DrawnCard objects
|
drawn_cards: List of DrawnCard objects
|
||||||
"""
|
"""
|
||||||
self.spread = spread
|
self.spread = spread
|
||||||
self.drawn_cards = drawn_cards
|
self.drawn_cards = drawn_cards
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return formatted reading with all cards and interpretations."""
|
"""Return formatted reading with all cards and interpretations."""
|
||||||
lines = [
|
lines = [
|
||||||
f"╔═══════════════════════════════════════════╗",
|
"╔═══════════════════════════════════════════╗",
|
||||||
f"║ {self.spread.name:40}║",
|
f"║ {self.spread.name:40}║",
|
||||||
f"╚═══════════════════════════════════════════╝",
|
"╚═══════════════════════════════════════════╝",
|
||||||
f"",
|
"",
|
||||||
f"{self.spread.description}",
|
f"{self.spread.description}",
|
||||||
f"",
|
"",
|
||||||
f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
||||||
f"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
for drawn in self.drawn_cards:
|
for drawn in self.drawn_cards:
|
||||||
card = drawn.card
|
card = drawn.card
|
||||||
card_name = card.name
|
card_name = card.name
|
||||||
if drawn.is_reversed:
|
if drawn.is_reversed:
|
||||||
card_name += " ◄ REVERSED"
|
card_name += " ◄ REVERSED"
|
||||||
|
|
||||||
lines.append(f"Position {drawn.position.number}: {drawn.position.name}")
|
lines.append(f"Position {drawn.position.number}: {drawn.position.name}")
|
||||||
lines.append(f" Card: {card_name}")
|
lines.append(f" Card: {card_name}")
|
||||||
lines.append(f" Meaning: {drawn.position.meaning}")
|
lines.append(f" Meaning: {drawn.position.meaning}")
|
||||||
|
|
||||||
# Add card details if available
|
# Add card details if available
|
||||||
if hasattr(card, 'number'):
|
if hasattr(card, "number"):
|
||||||
lines.append(f" Card #: {card.number}")
|
lines.append(f" Card #: {card.number}")
|
||||||
if hasattr(card, 'arcana'):
|
if hasattr(card, "arcana"):
|
||||||
lines.append(f" Arcana: {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(f" Suit: {card.suit.name}")
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
lines.append(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"SpreadReading({self.spread.name}, {len(self.drawn_cards)} cards)"
|
return f"SpreadReading({self.spread.name}, {len(self.drawn_cards)} cards)"
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
"""
|
"""
|
||||||
Tarot deck module - Core card and deck classes.
|
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.
|
MinorCard, and related classes for representing individual cards.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .deck import (
|
from .deck import (
|
||||||
|
DLT,
|
||||||
|
AceCard,
|
||||||
Card,
|
Card,
|
||||||
|
CardQuery,
|
||||||
|
CourtCard,
|
||||||
|
Deck,
|
||||||
MajorCard,
|
MajorCard,
|
||||||
MinorCard,
|
MinorCard,
|
||||||
PipCard,
|
PipCard,
|
||||||
AceCard,
|
|
||||||
CourtCard,
|
|
||||||
CardQuery,
|
|
||||||
TemporalQuery,
|
TemporalQuery,
|
||||||
DLT,
|
|
||||||
Deck,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__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.
|
MajorCard, and MinorCard classes for representing individual cards.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import List, Optional, Tuple, TYPE_CHECKING, Dict
|
|
||||||
import random
|
import random
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from ..attributes import (
|
from ..attributes import (
|
||||||
Meaning, CardImage, Suit, Zodiac, Element, Path,
|
CardImage,
|
||||||
Planet, Sephera, Color, PeriodicTable, ElementType, DoublLetterTrump
|
Color,
|
||||||
|
Element,
|
||||||
|
ElementType,
|
||||||
|
Meaning,
|
||||||
|
Path,
|
||||||
|
PeriodicTable,
|
||||||
|
Planet,
|
||||||
|
Sephera,
|
||||||
|
Suit,
|
||||||
)
|
)
|
||||||
from ..constants import (
|
from ..constants import (
|
||||||
COURT_RANKS,
|
COURT_RANKS,
|
||||||
MAJOR_ARCANA_NAMES,
|
MAJOR_ARCANA_NAMES,
|
||||||
PIP_INDEX_TO_NUMBER,
|
|
||||||
MINOR_RANK_NAMES,
|
MINOR_RANK_NAMES,
|
||||||
|
PIP_INDEX_TO_NUMBER,
|
||||||
PIP_ORDER,
|
PIP_ORDER,
|
||||||
SUITS_FIRST,
|
SUITS_FIRST,
|
||||||
SUITS_LAST,
|
SUITS_LAST,
|
||||||
@@ -35,6 +43,7 @@ def _get_card_data():
|
|||||||
global _card_data
|
global _card_data
|
||||||
if _card_data is None:
|
if _card_data is None:
|
||||||
from ..card.data import CardDataLoader
|
from ..card.data import CardDataLoader
|
||||||
|
|
||||||
_card_data = CardDataLoader()
|
_card_data = CardDataLoader()
|
||||||
return _card_data
|
return _card_data
|
||||||
|
|
||||||
@@ -42,16 +51,17 @@ def _get_card_data():
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Card:
|
class Card:
|
||||||
"""Base class representing a Tarot card."""
|
"""Base class representing a Tarot card."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
meaning: Meaning
|
meaning: Meaning
|
||||||
arcana: str # "Major" or "Minor"
|
arcana: str # "Major" or "Minor"
|
||||||
image: Optional[CardImage] = None
|
image: Optional[CardImage] = None
|
||||||
|
|
||||||
# These are overridden in subclasses but declared here for MinorCard compatibility
|
# These are overridden in subclasses but declared here for MinorCard compatibility
|
||||||
suit: Optional[Suit] = None
|
suit: Optional[Suit] = None
|
||||||
pip: int = 0
|
pip: int = 0
|
||||||
|
|
||||||
# Card-specific details
|
# Card-specific details
|
||||||
explanation: Dict[str, str] = field(default_factory=dict)
|
explanation: Dict[str, str] = field(default_factory=dict)
|
||||||
interpretation: str = ""
|
interpretation: str = ""
|
||||||
@@ -59,41 +69,48 @@ class Card:
|
|||||||
reversed_keywords: List[str] = field(default_factory=list)
|
reversed_keywords: List[str] = field(default_factory=list)
|
||||||
guidance: str = ""
|
guidance: str = ""
|
||||||
numerology: Optional[int] = None
|
numerology: Optional[int] = None
|
||||||
|
|
||||||
# Image path for custom deck images
|
# Image path for custom deck images
|
||||||
image_path: Optional[str] = None
|
image_path: Optional[str] = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.number}. {self.name}"
|
return f"{self.number}. {self.name}"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Card({self.number}, '{self.name}')"
|
return f"Card({self.number}, '{self.name}')"
|
||||||
|
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
"""
|
"""
|
||||||
Get the card's key as a Roman numeral representation.
|
Get the card's key as a Roman numeral representation.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Roman numeral string (e.g., "I", "XXI") for Major Arcana,
|
Roman numeral string (e.g., "I", "XXI") for Major Arcana,
|
||||||
or the pip number as string for Minor Arcana.
|
or the pip number as string for Minor Arcana.
|
||||||
"""
|
"""
|
||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
from ..card.details import CardDetailsRegistry
|
from ..card.details import CardDetailsRegistry
|
||||||
|
|
||||||
# For Major Arcana cards, convert the key to Roman numerals
|
# For Major Arcana cards, convert the key to Roman numerals
|
||||||
if self.arcana == "Major":
|
if self.arcana == "Major":
|
||||||
return CardDetailsRegistry.key_to_roman(self.number)
|
return CardDetailsRegistry.key_to_roman(self.number)
|
||||||
|
|
||||||
# For Minor Arcana, return the pip number as a formatted string
|
# 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 = {
|
pip_names = {
|
||||||
2: "Two", 3: "Three", 4: "Four", 5: "Five",
|
2: "Two",
|
||||||
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten"
|
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 pip_names.get(self.pip, str(self.pip))
|
||||||
|
|
||||||
return str(self.number)
|
return str(self.number)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""Get the specific card type (Major, Pip, Ace, Court)."""
|
"""Get the specific card type (Major, Pip, Ace, Court)."""
|
||||||
@@ -111,24 +128,30 @@ class Card:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class MajorCard(Card):
|
class MajorCard(Card):
|
||||||
"""Represents a Major Arcana card."""
|
"""Represents a Major Arcana card."""
|
||||||
|
|
||||||
kabbalistic_number: Optional[int] = None
|
kabbalistic_number: Optional[int] = None
|
||||||
tarot_letter: Optional[str] = None
|
tarot_letter: Optional[str] = None
|
||||||
tree_of_life_path: Optional[int] = None
|
tree_of_life_path: Optional[int] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
# Kabbalistic number should be 0-21, but deck position can be anywhere
|
# 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):
|
if self.kabbalistic_number is not None and (
|
||||||
raise ValueError(f"Major Arcana kabbalistic number must be 0-21, got {self.kabbalistic_number}")
|
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"
|
self.arcana = "Major"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MinorCard(Card):
|
class MinorCard(Card):
|
||||||
"""Represents a Minor Arcana card - either Pip or Court card."""
|
"""Represents a Minor Arcana card - either Pip or Court card."""
|
||||||
|
|
||||||
suit: Suit = None # type: ignore
|
suit: Suit = None # type: ignore
|
||||||
astrological_influence: Optional[str] = None
|
astrological_influence: Optional[str] = None
|
||||||
element: Optional[Element] = None
|
element: Optional[Element] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.suit is None:
|
if self.suit is None:
|
||||||
raise ValueError("suit must be provided for MinorCard")
|
raise ValueError("suit must be provided for MinorCard")
|
||||||
@@ -138,12 +161,13 @@ class MinorCard(Card):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class PipCard(MinorCard):
|
class PipCard(MinorCard):
|
||||||
"""Represents a Pip card (2 through 10) - has a pip number.
|
"""Represents a Pip card (2 through 10) - has a pip number.
|
||||||
|
|
||||||
Pip cards represent numbered forces in their suit, from Two
|
Pip cards represent numbered forces in their suit, from Two
|
||||||
through its full development (10).
|
through its full development (10).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pip: int = 0
|
pip: int = 0
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not (2 <= self.pip <= 10):
|
if not (2 <= self.pip <= 10):
|
||||||
raise ValueError(f"Pip card number must be 2-10, got {self.pip}")
|
raise ValueError(f"Pip card number must be 2-10, got {self.pip}")
|
||||||
@@ -153,13 +177,14 @@ class PipCard(MinorCard):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AceCard(MinorCard):
|
class AceCard(MinorCard):
|
||||||
"""Represents an Ace card - the root/foundation of the suit.
|
"""Represents an Ace card - the root/foundation of the suit.
|
||||||
|
|
||||||
The Ace is the initial force of the suit and contains the potential
|
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
|
for all other cards within that suit. Aces have pip=1 but are not
|
||||||
technically pip cards.
|
technically pip cards.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pip: int = 1
|
pip: int = 1
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.pip != 1:
|
if self.pip != 1:
|
||||||
raise ValueError(f"AceCard must have pip 1, got {self.pip}")
|
raise ValueError(f"AceCard must have pip 1, got {self.pip}")
|
||||||
@@ -169,22 +194,22 @@ class AceCard(MinorCard):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class CourtCard(MinorCard):
|
class CourtCard(MinorCard):
|
||||||
"""Represents a Court Card - Knight, Prince, Princess, or Queen.
|
"""Represents a Court Card - Knight, Prince, Princess, or Queen.
|
||||||
|
|
||||||
Court cards represent people/personalities and are the highest rank
|
Court cards represent people/personalities and are the highest rank
|
||||||
in the minor arcana. They do NOT have pips - they are archetypes.
|
in the minor arcana. They do NOT have pips - they are archetypes.
|
||||||
|
|
||||||
Each court card is associated with an element and Hebrew letter (Path):
|
Each court card is associated with an element and Hebrew letter (Path):
|
||||||
- Knight: Fire + Yod (path 20)
|
- Knight: Fire + Yod (path 20)
|
||||||
- Prince: Air + Vav (path 16)
|
- Prince: Air + Vav (path 16)
|
||||||
- Princess: Earth + Heh (path 15)
|
- Princess: Earth + Heh (path 15)
|
||||||
- Queen: Water + Heh (path 15)
|
- Queen: Water + Heh (path 15)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
COURT_RANKS = {"Knight": 12, "Prince": 11, "Princess": 13, "Queen": 14}
|
COURT_RANKS = {"Knight": 12, "Prince": 11, "Princess": 13, "Queen": 14}
|
||||||
court_rank: str = ""
|
court_rank: str = ""
|
||||||
associated_element: Optional[ElementType] = None
|
associated_element: Optional[ElementType] = None
|
||||||
hebrew_letter_path: Optional['Path'] = None
|
hebrew_letter_path: Optional["Path"] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.court_rank not in self.COURT_RANKS:
|
if self.court_rank not in self.COURT_RANKS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -194,75 +219,90 @@ class CourtCard(MinorCard):
|
|||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CardQuery:
|
class CardQuery:
|
||||||
"""Helper class for fluent card queries: deck.number(3).minor.wands"""
|
"""Helper class for fluent card queries: deck.number(3).minor.wands"""
|
||||||
|
|
||||||
def __init__(self, deck: 'Deck', number: Optional[int] = None,
|
def __init__(
|
||||||
arcana: Optional[str] = None) -> None:
|
self, deck: "Deck", number: Optional[int] = None, arcana: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
self.deck = deck
|
self.deck = deck
|
||||||
self.number = number
|
self.number = number
|
||||||
self.arcana = arcana
|
self.arcana = arcana
|
||||||
|
|
||||||
def _filter_cards(self) -> List[Card]:
|
def _filter_cards(self) -> List[Card]:
|
||||||
"""Get filtered cards based on current query state."""
|
"""Get filtered cards based on current query state."""
|
||||||
cards = self.deck.cards
|
cards = self.deck.cards
|
||||||
|
|
||||||
if self.number is not None:
|
if self.number is not None:
|
||||||
cards = [c for c in cards if c.number == self.number or
|
cards = [
|
||||||
(hasattr(c, 'pip') and c.pip == self.number)]
|
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:
|
if self.arcana is not None:
|
||||||
cards = [c for c in cards if c.arcana == self.arcana]
|
cards = [c for c in cards if c.arcana == self.arcana]
|
||||||
|
|
||||||
return cards
|
return cards
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def major(self) -> List[Card]:
|
def major(self) -> List[Card]:
|
||||||
"""Filter to Major Arcana only."""
|
"""Filter to Major Arcana only."""
|
||||||
return [c for c in self._filter_cards() if c.arcana == "Major"]
|
return [c for c in self._filter_cards() if c.arcana == "Major"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def minor(self) -> 'CardQuery':
|
def minor(self) -> "CardQuery":
|
||||||
"""Filter to Minor Arcana, return new CardQuery for suit chaining."""
|
"""Filter to Minor Arcana, return new CardQuery for suit chaining."""
|
||||||
return CardQuery(self.deck, self.number, "Minor")
|
return CardQuery(self.deck, self.number, "Minor")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cups(self) -> List[Card]:
|
def cups(self) -> List[Card]:
|
||||||
"""Get cards in Cups suit."""
|
"""Get cards in Cups suit."""
|
||||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
return [
|
||||||
c.suit and c.suit.name == "Cups"]
|
c
|
||||||
|
for c in self._filter_cards()
|
||||||
|
if hasattr(c, "suit") and c.suit and c.suit.name == "Cups"
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swords(self) -> List[Card]:
|
def swords(self) -> List[Card]:
|
||||||
"""Get cards in Swords suit."""
|
"""Get cards in Swords suit."""
|
||||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
return [
|
||||||
c.suit and c.suit.name == "Swords"]
|
c
|
||||||
|
for c in self._filter_cards()
|
||||||
|
if hasattr(c, "suit") and c.suit and c.suit.name == "Swords"
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def wands(self) -> List[Card]:
|
def wands(self) -> List[Card]:
|
||||||
"""Get cards in Wands suit."""
|
"""Get cards in Wands suit."""
|
||||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
return [
|
||||||
c.suit and c.suit.name == "Wands"]
|
c
|
||||||
|
for c in self._filter_cards()
|
||||||
|
if hasattr(c, "suit") and c.suit and c.suit.name == "Wands"
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pentacles(self) -> List[Card]:
|
def pentacles(self) -> List[Card]:
|
||||||
"""Get cards in Pentacles suit."""
|
"""Get cards in Pentacles suit."""
|
||||||
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
|
return [
|
||||||
c.suit and c.suit.name == "Pentacles"]
|
c
|
||||||
|
for c in self._filter_cards()
|
||||||
|
if hasattr(c, "suit") and c.suit and c.suit.name == "Pentacles"
|
||||||
|
]
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Allow iteration over filtered cards."""
|
"""Allow iteration over filtered cards."""
|
||||||
return iter(self._filter_cards())
|
return iter(self._filter_cards())
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""Return count of filtered cards."""
|
"""Return count of filtered cards."""
|
||||||
return len(self._filter_cards())
|
return len(self._filter_cards())
|
||||||
|
|
||||||
def __getitem__(self, index: int) -> Card:
|
def __getitem__(self, index: int) -> Card:
|
||||||
"""Get card by index from filtered results."""
|
"""Get card by index from filtered results."""
|
||||||
return self._filter_cards()[index]
|
return self._filter_cards()[index]
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
cards = self._filter_cards()
|
cards = self._filter_cards()
|
||||||
names = [c.name for c in cards]
|
names = [c.name for c in cards]
|
||||||
@@ -271,12 +311,17 @@ class CardQuery:
|
|||||||
|
|
||||||
class TemporalQuery:
|
class TemporalQuery:
|
||||||
"""Helper class for fluent temporal queries: loader.month(5).day(23).hour(15)"""
|
"""Helper class for fluent temporal queries: loader.month(5).day(23).hour(15)"""
|
||||||
|
|
||||||
def __init__(self, loader: 'CardDataLoader', month_num: Optional[int] = None,
|
def __init__(
|
||||||
day_num: Optional[int] = None, hour_num: Optional[int] = None) -> None:
|
self,
|
||||||
|
loader: "CardDataLoader",
|
||||||
|
month_num: Optional[int] = None,
|
||||||
|
day_num: Optional[int] = None,
|
||||||
|
hour_num: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize temporal query builder.
|
Initialize temporal query builder.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
loader: CardDataLoader instance for fetching temporal data
|
loader: CardDataLoader instance for fetching temporal data
|
||||||
month_num: Month number (1-12)
|
month_num: Month number (1-12)
|
||||||
@@ -287,71 +332,74 @@ class TemporalQuery:
|
|||||||
self.month_num = month_num
|
self.month_num = month_num
|
||||||
self.day_num = day_num
|
self.day_num = day_num
|
||||||
self.hour_num = hour_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."""
|
"""Set month (1-12) and return new query for chaining."""
|
||||||
return TemporalQuery(self.loader, month_num=num,
|
return TemporalQuery(
|
||||||
day_num=self.day_num, hour_num=self.hour_num)
|
self.loader, month_num=num, day_num=self.day_num, hour_num=self.hour_num
|
||||||
|
)
|
||||||
def day(self, num: int) -> 'TemporalQuery':
|
|
||||||
|
def day(self, num: int) -> "TemporalQuery":
|
||||||
"""Set day (1-31) and return new query for chaining."""
|
"""Set day (1-31) and return new query for chaining."""
|
||||||
if self.month_num is None:
|
if self.month_num is None:
|
||||||
raise ValueError("Must set month before day")
|
raise ValueError("Must set month before day")
|
||||||
return TemporalQuery(self.loader, month_num=self.month_num,
|
return TemporalQuery(
|
||||||
day_num=num, hour_num=self.hour_num)
|
self.loader, month_num=self.month_num, day_num=num, hour_num=self.hour_num
|
||||||
|
)
|
||||||
def hour(self, num: int) -> 'TemporalQuery':
|
|
||||||
|
def hour(self, num: int) -> "TemporalQuery":
|
||||||
"""Set hour (0-23) and return new query for chaining."""
|
"""Set hour (0-23) and return new query for chaining."""
|
||||||
if self.month_num is None or self.day_num is None:
|
if self.month_num is None or self.day_num is None:
|
||||||
raise ValueError("Must set month and day before hour")
|
raise ValueError("Must set month and day before hour")
|
||||||
return TemporalQuery(self.loader, month_num=self.month_num,
|
return TemporalQuery(
|
||||||
day_num=self.day_num, hour_num=num)
|
self.loader, month_num=self.month_num, day_num=self.day_num, hour_num=num
|
||||||
|
)
|
||||||
|
|
||||||
def weekday(self) -> Optional[str]:
|
def weekday(self) -> Optional[str]:
|
||||||
"""Get weekday name for current month/day combination using Zeller's congruence."""
|
"""Get weekday name for current month/day combination using Zeller's congruence."""
|
||||||
if self.month_num is None or self.day_num is None:
|
if self.month_num is None or self.day_num is None:
|
||||||
raise ValueError("Must set month and day to get weekday")
|
raise ValueError("Must set month and day to get weekday")
|
||||||
|
|
||||||
# Zeller's congruence (adjusted for current calendar)
|
# Zeller's congruence (adjusted for current calendar)
|
||||||
month = self.month_num
|
month = self.month_num
|
||||||
day = self.day_num
|
day = self.day_num
|
||||||
year = 2024 # Use current year as reference
|
year = 2024 # Use current year as reference
|
||||||
|
|
||||||
# Adjust month and year for March-based calculation
|
# Adjust month and year for March-based calculation
|
||||||
if month < 3:
|
if month < 3:
|
||||||
month += 12
|
month += 12
|
||||||
year -= 1
|
year -= 1
|
||||||
|
|
||||||
# Zeller's formula
|
# Zeller's formula
|
||||||
q = day
|
q = day
|
||||||
m = month
|
m = month
|
||||||
k = year % 100
|
k = year % 100
|
||||||
j = year // 100
|
j = year // 100
|
||||||
|
|
||||||
h = (q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) % 7
|
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)
|
# Convert to weekday name (0=Saturday, 1=Sunday, 2=Monday, ..., 6=Friday)
|
||||||
day_names = ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
day_names = ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||||
return day_names[h]
|
return day_names[h]
|
||||||
|
|
||||||
def month_info(self):
|
def month_info(self):
|
||||||
"""Return month metadata for the configured query."""
|
"""Return month metadata for the configured query."""
|
||||||
if self.month_num is None:
|
if self.month_num is None:
|
||||||
return None
|
return None
|
||||||
return self.loader.month_info(self.month_num)
|
return self.loader.month_info(self.month_num)
|
||||||
|
|
||||||
def day_info(self):
|
def day_info(self):
|
||||||
"""Return day metadata for the configured query."""
|
"""Return day metadata for the configured query."""
|
||||||
if self.day_num is None:
|
if self.day_num is None:
|
||||||
return None
|
return None
|
||||||
return self.loader.day_info(self.day_num)
|
return self.loader.day_info(self.day_num)
|
||||||
|
|
||||||
def hour_info(self):
|
def hour_info(self):
|
||||||
"""Return the planetary hour metadata for the configured query."""
|
"""Return the planetary hour metadata for the configured query."""
|
||||||
if self.hour_num is None:
|
if self.hour_num is None:
|
||||||
return None
|
return None
|
||||||
return self.loader.clock_hour(self.hour_num)
|
return self.loader.clock_hour(self.hour_num)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
parts = []
|
parts = []
|
||||||
if self.month_num:
|
if self.month_num:
|
||||||
@@ -366,47 +414,48 @@ class TemporalQuery:
|
|||||||
class DLT:
|
class DLT:
|
||||||
"""
|
"""
|
||||||
Double Letter Trump (DLT) accessor.
|
Double Letter Trump (DLT) accessor.
|
||||||
|
|
||||||
Double Letter Trumps are Major Arcana cards 3-21 (19 cards total),
|
Double Letter Trumps are Major Arcana cards 3-21 (19 cards total),
|
||||||
each associated with a Hebrew letter and planetary/astrological force.
|
each associated with a Hebrew letter and planetary/astrological force.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
dlt = DLT(3) # Get the 3rd Double Letter Trump (The Empress)
|
dlt = DLT(3) # Get the 3rd Double Letter Trump (The Empress)
|
||||||
dlt = DLT(7) # Get the 7th Double Letter Trump (The Chariot)
|
dlt = DLT(7) # Get the 7th Double Letter Trump (The Chariot)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, trump_number: int) -> None:
|
def __init__(self, trump_number: int) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize a Double Letter Trump query.
|
Initialize a Double Letter Trump query.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
trump_number: Position in DLT sequence (3-21)
|
trump_number: Position in DLT sequence (3-21)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If trump_number is not 3-21
|
ValueError: If trump_number is not 3-21
|
||||||
"""
|
"""
|
||||||
if not 3 <= trump_number <= 21:
|
if not 3 <= trump_number <= 21:
|
||||||
raise ValueError(f"DLT number must be 3-21, got {trump_number}")
|
raise ValueError(f"DLT number must be 3-21, got {trump_number}")
|
||||||
|
|
||||||
self.trump_number = trump_number
|
self.trump_number = trump_number
|
||||||
self._loader: Optional['CardDataLoader'] = None
|
self._loader: Optional["CardDataLoader"] = None
|
||||||
self._deck: Optional[Deck] = None
|
self._deck: Optional[Deck] = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def loader(self) -> 'CardDataLoader':
|
def loader(self) -> "CardDataLoader":
|
||||||
"""Lazy-load CardDataLoader on first access."""
|
"""Lazy-load CardDataLoader on first access."""
|
||||||
if self._loader is None:
|
if self._loader is None:
|
||||||
from ..card.data import CardDataLoader
|
from ..card.data import CardDataLoader
|
||||||
|
|
||||||
self._loader = CardDataLoader()
|
self._loader = CardDataLoader()
|
||||||
return self._loader
|
return self._loader
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def deck(self) -> 'Deck':
|
def deck(self) -> "Deck":
|
||||||
"""Lazy-load Deck on first access."""
|
"""Lazy-load Deck on first access."""
|
||||||
if self._deck is None:
|
if self._deck is None:
|
||||||
self._deck = Deck()
|
self._deck = Deck()
|
||||||
return self._deck
|
return self._deck
|
||||||
|
|
||||||
def card(self) -> Optional[Card]:
|
def card(self) -> Optional[Card]:
|
||||||
"""Get the Tarot card for this DLT."""
|
"""Get the Tarot card for this DLT."""
|
||||||
# Major Arcana cards are numbered 0-21, so DLT(3) = Major card 3
|
# 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:
|
if card.arcana == "Major" and card.number == self.trump_number:
|
||||||
return card
|
return card
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def periodic_entry(self) -> Optional[PeriodicTable]:
|
def periodic_entry(self) -> Optional[PeriodicTable]:
|
||||||
"""Get the periodic table entry with cross-correspondences."""
|
"""Get the periodic table entry with cross-correspondences."""
|
||||||
return self.loader.periodic_entry(self.trump_number)
|
return self.loader.periodic_entry(self.trump_number)
|
||||||
|
|
||||||
def sephera(self) -> Optional[Sephera]:
|
def sephera(self) -> Optional[Sephera]:
|
||||||
"""Get the Sephira associated with this DLT."""
|
"""Get the Sephira associated with this DLT."""
|
||||||
return self.loader.sephera(self.trump_number)
|
return self.loader.sephera(self.trump_number)
|
||||||
|
|
||||||
def planet(self) -> Optional[Planet]:
|
def planet(self) -> Optional[Planet]:
|
||||||
"""Get the planetary ruler for this DLT."""
|
"""Get the planetary ruler for this DLT."""
|
||||||
periodic = self.periodic_entry()
|
periodic = self.periodic_entry()
|
||||||
return periodic.planet if periodic else None
|
return periodic.planet if periodic else None
|
||||||
|
|
||||||
def element(self) -> Optional[ElementType]:
|
def element(self) -> Optional[ElementType]:
|
||||||
"""Get the element associated with this DLT."""
|
"""Get the element associated with this DLT."""
|
||||||
periodic = self.periodic_entry()
|
periodic = self.periodic_entry()
|
||||||
return periodic.element if periodic else None
|
return periodic.element if periodic else None
|
||||||
|
|
||||||
def hebrew_letter(self) -> Optional[str]:
|
def hebrew_letter(self) -> Optional[str]:
|
||||||
"""Get the Hebrew letter associated with this DLT."""
|
"""Get the Hebrew letter associated with this DLT."""
|
||||||
periodic = self.periodic_entry()
|
periodic = self.periodic_entry()
|
||||||
return periodic.hebrew_letter if periodic else None
|
return periodic.hebrew_letter if periodic else None
|
||||||
|
|
||||||
def color(self) -> Optional[Color]:
|
def color(self) -> Optional[Color]:
|
||||||
"""Get the color associated with this DLT."""
|
"""Get the color associated with this DLT."""
|
||||||
periodic = self.periodic_entry()
|
periodic = self.periodic_entry()
|
||||||
return periodic.color if periodic else None
|
return periodic.color if periodic else None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
card = self.card()
|
card = self.card()
|
||||||
card_name = card.name if card else "Unknown"
|
card_name = card.name if card else "Unknown"
|
||||||
return f"DLT({self.trump_number}) - {card_name}"
|
return f"DLT({self.trump_number}) - {card_name}"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
class Deck:
|
class Deck:
|
||||||
"""Represents a standard 78-card Tarot deck."""
|
"""Represents a standard 78-card Tarot deck."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize a standard Tarot deck with all 78 cards."""
|
"""Initialize a standard Tarot deck with all 78 cards."""
|
||||||
self.cards: List[Card] = []
|
self.cards: List[Card] = []
|
||||||
self.discard_pile: List[Card] = []
|
self.discard_pile: List[Card] = []
|
||||||
self._initialize_deck()
|
self._initialize_deck()
|
||||||
|
|
||||||
def _initialize_deck(self) -> None:
|
def _initialize_deck(self) -> None:
|
||||||
"""Initialize the deck with all 78 Tarot cards.
|
"""Initialize the deck with all 78 Tarot cards.
|
||||||
|
|
||||||
Order: Cups (1-14), Pentacles/Disks (15-28), Swords (29-42),
|
Order: Cups (1-14), Pentacles/Disks (15-28), Swords (29-42),
|
||||||
Major Arcana (43-64), Wands (65-78)
|
Major Arcana (43-64), Wands (65-78)
|
||||||
|
|
||||||
Minor suit sequencing (per suit): Ace, 2-10, Prince, Knight, Princess, Queen.
|
Minor suit sequencing (per suit): Ace, 2-10, Prince, Knight, Princess, Queen.
|
||||||
|
|
||||||
This puts Queen of Wands as card #78, the final card.
|
This puts Queen of Wands as card #78, the final card.
|
||||||
"""
|
"""
|
||||||
# Minor Arcana - First three suits (Cups, Pentacles, Swords)
|
# Minor Arcana - First three suits (Cups, Pentacles, Swords)
|
||||||
@@ -479,18 +528,18 @@ class Deck:
|
|||||||
earth_element = card_data.element("Earth")
|
earth_element = card_data.element("Earth")
|
||||||
air_element = card_data.element("Air")
|
air_element = card_data.element("Air")
|
||||||
fire_element = card_data.element("Fire")
|
fire_element = card_data.element("Fire")
|
||||||
|
|
||||||
if not water_element or not earth_element or not air_element or not fire_element:
|
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")
|
raise RuntimeError("Failed to load element data from CardDataLoader")
|
||||||
|
|
||||||
# Get Hebrew letters (Paths) for court cards
|
# Get Hebrew letters (Paths) for court cards
|
||||||
yod_path = card_data.path(20) # Yod
|
yod_path = card_data.path(20) # Yod
|
||||||
vav_path = card_data.path(16) # Vav
|
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:
|
if not yod_path or not vav_path or not he_path:
|
||||||
raise RuntimeError("Failed to load Hebrew letter/path data from CardDataLoader")
|
raise RuntimeError("Failed to load Hebrew letter/path data from CardDataLoader")
|
||||||
|
|
||||||
# Map court ranks to their associated elements and Hebrew letter paths
|
# Map court ranks to their associated elements and Hebrew letter paths
|
||||||
# Knight -> Fire + Yod, Prince -> Air + Vav, Princess -> Earth + Heh, Queen -> Water + Heh
|
# Knight -> Fire + Yod, Prince -> Air + Vav, Princess -> Earth + Heh, Queen -> Water + Heh
|
||||||
court_rank_mappings = {
|
court_rank_mappings = {
|
||||||
@@ -507,12 +556,16 @@ class Deck:
|
|||||||
"Fire": fire_element,
|
"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]] = []
|
specs: List[Tuple[str, ElementType, int]] = []
|
||||||
for suit_name, element_key, suit_num in suit_defs:
|
for suit_name, element_key, suit_num in suit_defs:
|
||||||
element_obj = element_lookup.get(element_key)
|
element_obj = element_lookup.get(element_key)
|
||||||
if element_obj is None:
|
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))
|
specs.append((suit_name, element_obj, suit_num))
|
||||||
return specs
|
return specs
|
||||||
|
|
||||||
@@ -530,7 +583,7 @@ class Deck:
|
|||||||
card_number,
|
card_number,
|
||||||
court_rank_mappings,
|
court_rank_mappings,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Major Arcana (43-64)
|
# Major Arcana (43-64)
|
||||||
# Names match filenames in src/tarot/deck/default/
|
# Names match filenames in src/tarot/deck/default/
|
||||||
for i, name in enumerate(MAJOR_ARCANA_NAMES):
|
for i, name in enumerate(MAJOR_ARCANA_NAMES):
|
||||||
@@ -538,15 +591,14 @@ class Deck:
|
|||||||
number=card_number,
|
number=card_number,
|
||||||
name=name,
|
name=name,
|
||||||
meaning=Meaning(
|
meaning=Meaning(
|
||||||
upright=f"{name} upright meaning",
|
upright=f"{name} upright meaning", reversed=f"{name} reversed meaning"
|
||||||
reversed=f"{name} reversed meaning"
|
|
||||||
),
|
),
|
||||||
arcana="Major",
|
arcana="Major",
|
||||||
kabbalistic_number=i
|
kabbalistic_number=i,
|
||||||
)
|
)
|
||||||
self.cards.append(card)
|
self.cards.append(card)
|
||||||
card_number += 1
|
card_number += 1
|
||||||
|
|
||||||
# Minor Arcana - Last suit (Wands, 65-78)
|
# Minor Arcana - Last suit (Wands, 65-78)
|
||||||
# Organized logically: Ace, 2-10, then court cards Prince, Knight, Princess, Queen
|
# Organized logically: Ace, 2-10, then court cards Prince, Knight, Princess, Queen
|
||||||
for suit_name, element_obj, suit_num in suits_data_last:
|
for suit_name, element_obj, suit_num in suits_data_last:
|
||||||
@@ -557,16 +609,16 @@ class Deck:
|
|||||||
card_number,
|
card_number,
|
||||||
court_rank_mappings,
|
court_rank_mappings,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load detailed explanations and keywords from registry
|
# Load detailed explanations and keywords from registry
|
||||||
try:
|
try:
|
||||||
from ..card.loader import load_deck_details
|
from ..card.loader import load_deck_details
|
||||||
|
|
||||||
load_deck_details(self)
|
load_deck_details(self)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Handle case where loader might not be available or circular import issues
|
# Handle case where loader might not be available or circular import issues
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _add_minor_cards_for_suit(
|
def _add_minor_cards_for_suit(
|
||||||
self,
|
self,
|
||||||
suit_name: str,
|
suit_name: str,
|
||||||
@@ -625,77 +677,77 @@ class Deck:
|
|||||||
|
|
||||||
return card_number
|
return card_number
|
||||||
|
|
||||||
|
|
||||||
def shuffle(self) -> None:
|
def shuffle(self) -> None:
|
||||||
"""Shuffle the deck."""
|
"""Shuffle the deck."""
|
||||||
random.shuffle(self.cards)
|
random.shuffle(self.cards)
|
||||||
|
|
||||||
def draw(self, num_cards: int = 1) -> List[Card]:
|
def draw(self, num_cards: int = 1) -> List[Card]:
|
||||||
"""
|
"""
|
||||||
Draw cards from the deck.
|
Draw cards from the deck.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
num_cards: Number of cards to draw (default: 1)
|
num_cards: Number of cards to draw (default: 1)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of drawn cards
|
List of drawn cards
|
||||||
"""
|
"""
|
||||||
if num_cards < 1:
|
if num_cards < 1:
|
||||||
raise ValueError("Must draw at least 1 card")
|
raise ValueError("Must draw at least 1 card")
|
||||||
|
|
||||||
if num_cards > len(self.cards):
|
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 = []
|
drawn = []
|
||||||
for _ in range(num_cards):
|
for _ in range(num_cards):
|
||||||
drawn.append(self.cards.pop(0))
|
drawn.append(self.cards.pop(0))
|
||||||
|
|
||||||
return drawn
|
return drawn
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the deck to its initial state."""
|
"""Reset the deck to its initial state."""
|
||||||
self.cards.clear()
|
self.cards.clear()
|
||||||
self.discard_pile.clear()
|
self.discard_pile.clear()
|
||||||
self._initialize_deck()
|
self._initialize_deck()
|
||||||
|
|
||||||
def remaining(self) -> int:
|
def remaining(self) -> int:
|
||||||
"""Return the number of cards remaining in the deck."""
|
"""Return the number of cards remaining in the deck."""
|
||||||
return len(self.cards)
|
return len(self.cards)
|
||||||
|
|
||||||
def number(self, pip_value: int) -> CardQuery:
|
def number(self, pip_value: int) -> CardQuery:
|
||||||
"""
|
"""
|
||||||
Query cards by number (pip value).
|
Query cards by number (pip value).
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
deck.number(3) # All cards with 3
|
deck.number(3) # All cards with 3
|
||||||
deck.number(3).minor # All minor 3s
|
deck.number(3).minor # All minor 3s
|
||||||
deck.number(3).minor.wands # 3 of Wands
|
deck.number(3).minor.wands # 3 of Wands
|
||||||
"""
|
"""
|
||||||
return CardQuery(self, pip_value)
|
return CardQuery(self, pip_value)
|
||||||
|
|
||||||
def suit(self, suit_name: str) -> List[Card]:
|
def suit(self, suit_name: str) -> List[Card]:
|
||||||
"""
|
"""
|
||||||
Get all cards from a specific suit.
|
Get all cards from a specific suit.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
deck.suit("Wands")
|
deck.suit("Wands")
|
||||||
"""
|
"""
|
||||||
return [c for c in self.cards if hasattr(c, 'suit') and
|
return [c for c in self.cards if hasattr(c, "suit") and c.suit and c.suit.name == suit_name]
|
||||||
c.suit and c.suit.name == suit_name]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def major(self) -> List[Card]:
|
def major(self) -> List[Card]:
|
||||||
"""Get all Major Arcana cards."""
|
"""Get all Major Arcana cards."""
|
||||||
return [c for c in self.cards if c.arcana == "Major"]
|
return [c for c in self.cards if c.arcana == "Major"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def minor(self) -> List[Card]:
|
def minor(self) -> List[Card]:
|
||||||
"""Get all Minor Arcana cards."""
|
"""Get all Minor Arcana cards."""
|
||||||
return [c for c in self.cards if c.arcana == "Minor"]
|
return [c for c in self.cards if c.arcana == "Minor"]
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
"""Return the number of cards in the deck."""
|
"""Return the number of cards in the deck."""
|
||||||
return len(self.cards)
|
return len(self.cards)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Deck({len(self.cards)} cards remaining)"
|
return f"Deck({len(self.cards)} cards remaining)"
|
||||||
|
|||||||
@@ -5,115 +5,121 @@ Unified accessor for Tarot-related data and operations.
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from tarot import Tarot
|
from tarot import Tarot
|
||||||
|
|
||||||
# Deck and cards
|
# Deck and cards
|
||||||
card = Tarot.deck.card(3)
|
card = Tarot.deck.card(3)
|
||||||
major5 = Tarot.deck.card.major(5)
|
major5 = Tarot.deck.card.major(5)
|
||||||
cups2 = Tarot.deck.card.minor.cups(2)
|
cups2 = Tarot.deck.card.minor.cups(2)
|
||||||
|
|
||||||
# Letters (Hebrew with correspondences)
|
# Letters (Hebrew with correspondences)
|
||||||
letter = Tarot.letters('aleph')
|
letter = Tarot.letters('aleph')
|
||||||
simple_letters = Tarot.letters.filter(letter_type="Simple")
|
simple_letters = Tarot.letters.filter(letter_type="Simple")
|
||||||
all_letters = Tarot.letters.display_all()
|
all_letters = Tarot.letters.display_all()
|
||||||
|
|
||||||
# Tree of Life
|
# Tree of Life
|
||||||
sephera = Tarot.tree.sephera(1)
|
sephera = Tarot.tree.sephera(1)
|
||||||
path = Tarot.tree.path(11)
|
path = Tarot.tree.path(11)
|
||||||
|
|
||||||
# Cube of Space
|
# Cube of Space
|
||||||
wall = Tarot.cube.wall('North')
|
wall = Tarot.cube.wall('North')
|
||||||
area = Tarot.cube.area('North', 'center')
|
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 Cube, Tree
|
||||||
from kaballah import Tree, Cube
|
|
||||||
from letter import letters
|
from letter import letters
|
||||||
|
|
||||||
|
from .card import CardAccessor
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from utils.attributes import Planet, God
|
from utils.attributes import God, Planet
|
||||||
|
|
||||||
from .attributes import Hexagram
|
from .attributes import Hexagram
|
||||||
|
from .card.data import CardDataLoader
|
||||||
|
|
||||||
|
|
||||||
class DeckAccessor:
|
class DeckAccessor:
|
||||||
"""Accessor for deck and card operations."""
|
"""Accessor for deck and card operations."""
|
||||||
|
|
||||||
# Card accessor (Tarot.deck.card, Tarot.deck.card.major, etc.)
|
# Card accessor (Tarot.deck.card, Tarot.deck.card.major, etc.)
|
||||||
card = CardAccessor()
|
card = CardAccessor()
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return a nice summary of the deck accessor."""
|
"""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:
|
def __repr__(self) -> str:
|
||||||
"""Return a nice representation of the deck accessor."""
|
"""Return a nice representation of the deck accessor."""
|
||||||
return self.__str__()
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Tarot:
|
class Tarot:
|
||||||
"""
|
"""
|
||||||
Unified accessor for Tarot correspondences and data.
|
Unified accessor for Tarot correspondences and data.
|
||||||
|
|
||||||
Provides access to cards, letters, tree of life, and cube of space.
|
Provides access to cards, letters, tree of life, and cube of space.
|
||||||
|
|
||||||
Temporal and astrological functions are available through the temporal module:
|
Temporal and astrological functions are available through the temporal module:
|
||||||
from temporal import ThalemaClock, Zodiac
|
from temporal import ThalemaClock, Zodiac
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
deck: CardAccessor for card operations
|
deck: CardAccessor for card operations
|
||||||
letters: Hebrew letter accessor
|
letters: Hebrew letter accessor
|
||||||
tree: Tree of Life accessor
|
tree: Tree of Life accessor
|
||||||
cube: Cube of Space accessor
|
cube: Cube of Space accessor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
deck = DeckAccessor()
|
deck = DeckAccessor()
|
||||||
letters = letters()
|
letters = letters()
|
||||||
tree = Tree
|
tree = Tree
|
||||||
cube = Cube
|
cube = Cube
|
||||||
|
|
||||||
_loader: Optional['CardDataLoader'] = None # type: ignore
|
_loader: Optional["CardDataLoader"] = None # type: ignore
|
||||||
_initialized: bool = False
|
_initialized: bool = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _ensure_initialized(cls) -> None:
|
def _ensure_initialized(cls) -> None:
|
||||||
"""Lazy-load CardDataLoader on first access."""
|
"""Lazy-load CardDataLoader on first access."""
|
||||||
if cls._initialized:
|
if cls._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
from .card.data import CardDataLoader
|
from .card.data import CardDataLoader
|
||||||
|
|
||||||
cls._loader = CardDataLoader()
|
cls._loader = CardDataLoader()
|
||||||
cls._initialized = True
|
cls._initialized = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def planet(cls, name: str) -> Optional['Planet']:
|
def planet(cls, name: str) -> Optional["Planet"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@overload
|
@overload
|
||||||
def planet(cls, name: None = ...) -> Dict[str, 'Planet']:
|
def planet(cls, name: None = ...) -> Dict[str, "Planet"]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return a planet entry or all planets."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
return cls._loader.planet(name) # type: ignore
|
return cls._loader.planet(name) # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return a god entry or all gods."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
return cls._loader.god(name) # type: ignore
|
return cls._loader.god(name) # type: ignore
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Return a hexagram or all hexagrams."""
|
||||||
cls._ensure_initialized()
|
cls._ensure_initialized()
|
||||||
return cls._loader.hexagram(number) # type: ignore
|
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 Year, Month, Day, Hour, Week
|
||||||
from temporal import Calendar, TimeUtil, TemporalCoordinates, Season
|
from temporal import Calendar, TimeUtil, TemporalCoordinates, Season
|
||||||
from temporal import ThalemaClock, Zodiac, PlanetPosition
|
from temporal import ThalemaClock, Zodiac, PlanetPosition
|
||||||
|
|
||||||
# Basic usage
|
# Basic usage
|
||||||
year = Year(2025)
|
year = Year(2025)
|
||||||
month = Month(11) # November
|
month = Month(11) # November
|
||||||
day = Day(18)
|
day = Day(18)
|
||||||
hour = Hour(14)
|
hour = Hour(14)
|
||||||
|
|
||||||
# Calendar operations
|
# Calendar operations
|
||||||
current = Calendar.now()
|
current = Calendar.now()
|
||||||
is_leap = Calendar.is_leap_year(2025)
|
is_leap = Calendar.is_leap_year(2025)
|
||||||
|
|
||||||
# Temporal coordinates
|
# Temporal coordinates
|
||||||
season = TemporalCoordinates.get_season(11, 18)
|
season = TemporalCoordinates.get_season(11, 18)
|
||||||
days_until_winter = TemporalCoordinates.days_until_event(11, 18, Season.WINTER)
|
days_until_winter = TemporalCoordinates.days_until_event(11, 18, Season.WINTER)
|
||||||
|
|
||||||
# Astrological positions
|
# Astrological positions
|
||||||
clock = ThalemaClock()
|
clock = ThalemaClock()
|
||||||
print(clock) # Shows planetary positions
|
print(clock) # Shows planetary positions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .temporal import Year, Month, Day, Hour, Week
|
from .astrology import PlanetPosition, ThalemaClock, Zodiac
|
||||||
from .calendar import Calendar
|
from .calendar import Calendar
|
||||||
|
from .coordinates import Season, SolarEvent, TemporalCoordinates
|
||||||
|
from .temporal import Day, Hour, Month, Week, Year
|
||||||
from .time import TimeUtil
|
from .time import TimeUtil
|
||||||
from .coordinates import TemporalCoordinates, Season, SolarEvent
|
|
||||||
from .astrology import ThalemaClock, Zodiac, PlanetPosition
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Temporal classes
|
# Temporal classes
|
||||||
'Year',
|
"Year",
|
||||||
'Month',
|
"Month",
|
||||||
'Day',
|
"Day",
|
||||||
'Hour',
|
"Hour",
|
||||||
'Week',
|
"Week",
|
||||||
'Calendar',
|
"Calendar",
|
||||||
'TimeUtil',
|
"TimeUtil",
|
||||||
'TemporalCoordinates',
|
"TemporalCoordinates",
|
||||||
'Season',
|
"Season",
|
||||||
'SolarEvent',
|
"SolarEvent",
|
||||||
# Astrological classes
|
# Astrological classes
|
||||||
'ThalemaClock',
|
"ThalemaClock",
|
||||||
'Zodiac',
|
"Zodiac",
|
||||||
'PlanetPosition',
|
"PlanetPosition",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,27 +5,28 @@ Calculates current planetary positions with zodiac degrees and symbols.
|
|||||||
Usage:
|
Usage:
|
||||||
from clock import ThalemaClock
|
from clock import ThalemaClock
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
clock = ThalemaClock(now)
|
clock = ThalemaClock(now)
|
||||||
print(clock) # Display formatted with degrees and symbols
|
print(clock) # Display formatted with degrees and symbols
|
||||||
|
|
||||||
# Get individual planet info
|
# Get individual planet info
|
||||||
sun_info = clock.get_planet('Sun')
|
sun_info = clock.get_planet('Sun')
|
||||||
print(sun_info) # "☉ 25°♏︎"
|
print(sun_info) # "☉ 25°♏︎"
|
||||||
|
|
||||||
# Custom planet order
|
# Custom planet order
|
||||||
clock.display_format(['Moon', 'Mercury', 'Mars', 'Venus', 'Sun'])
|
clock.display_format(['Moon', 'Mercury', 'Mars', 'Venus', 'Sun'])
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional, List
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
class Zodiac(Enum):
|
class Zodiac(Enum):
|
||||||
"""Zodiac signs with degree ranges (0-360°)."""
|
"""Zodiac signs with degree ranges (0-360°)."""
|
||||||
|
|
||||||
ARIES = ("♈", 0, 30)
|
ARIES = ("♈", 0, 30)
|
||||||
TAURUS = ("♉", 30, 60)
|
TAURUS = ("♉", 30, 60)
|
||||||
GEMINI = ("♊", 60, 90)
|
GEMINI = ("♊", 60, 90)
|
||||||
@@ -63,6 +64,7 @@ class Zodiac(Enum):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class PlanetPosition:
|
class PlanetPosition:
|
||||||
"""Represents a planet's position with degree and zodiac."""
|
"""Represents a planet's position with degree and zodiac."""
|
||||||
|
|
||||||
planet_name: str
|
planet_name: str
|
||||||
planet_symbol: str
|
planet_symbol: str
|
||||||
zodiac: Zodiac
|
zodiac: Zodiac
|
||||||
@@ -219,7 +221,10 @@ class ThalemaClock:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def display_compact(self) -> str:
|
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()
|
return self.display_format()
|
||||||
|
|
||||||
def display_verbose(self) -> str:
|
def display_verbose(self) -> str:
|
||||||
@@ -228,7 +233,9 @@ class ThalemaClock:
|
|||||||
for planet_name in ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn"]:
|
for planet_name in ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn"]:
|
||||||
if planet_name in self.positions:
|
if planet_name in self.positions:
|
||||||
pos = self.positions[planet_name]
|
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)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ including Zodiac, Time cycles, and Astrological influences.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Optional
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Month:
|
class Month:
|
||||||
"""Represents a calendar month."""
|
"""Represents a calendar month."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
zodiac_start: str
|
zodiac_start: str
|
||||||
@@ -21,6 +22,7 @@ class Month:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Weekday:
|
class Weekday:
|
||||||
"""Represents weekday/weekend archetypes with planetary ties."""
|
"""Represents weekday/weekend archetypes with planetary ties."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
planetary_correspondence: str
|
planetary_correspondence: str
|
||||||
@@ -35,6 +37,7 @@ class Weekday:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Hour:
|
class Hour:
|
||||||
"""Represents an hour with planetary correspondence."""
|
"""Represents an hour with planetary correspondence."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
name: str
|
name: str
|
||||||
planetary_hours: List[str] = field(default_factory=list)
|
planetary_hours: List[str] = field(default_factory=list)
|
||||||
@@ -43,6 +46,7 @@ class Hour:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ClockHour:
|
class ClockHour:
|
||||||
"""Represents a clock hour with both 24-hour and 12-hour phases."""
|
"""Represents a clock hour with both 24-hour and 12-hour phases."""
|
||||||
|
|
||||||
hour_24: int
|
hour_24: int
|
||||||
hour_12: int
|
hour_12: int
|
||||||
period: str # AM or PM
|
period: str # AM or PM
|
||||||
@@ -63,6 +67,7 @@ class ClockHour:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Zodiac:
|
class Zodiac:
|
||||||
"""Represents a zodiac sign."""
|
"""Represents a zodiac sign."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
symbol: str
|
symbol: str
|
||||||
element: str
|
element: str
|
||||||
@@ -73,6 +78,7 @@ class Zodiac:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Degree:
|
class Degree:
|
||||||
"""Represents an astrological degree."""
|
"""Represents an astrological degree."""
|
||||||
|
|
||||||
number: int
|
number: int
|
||||||
constellation: str
|
constellation: str
|
||||||
ruling_planet: str
|
ruling_planet: str
|
||||||
@@ -82,6 +88,7 @@ class Degree:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AstrologicalInfluence:
|
class AstrologicalInfluence:
|
||||||
"""Represents astrological influences."""
|
"""Represents astrological influences."""
|
||||||
|
|
||||||
planet: str
|
planet: str
|
||||||
sign: str
|
sign: str
|
||||||
house: str
|
house: str
|
||||||
|
|||||||
@@ -3,55 +3,56 @@
|
|||||||
This module provides calendar-related operations and utilities.
|
This module provides calendar-related operations and utilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, date
|
from datetime import date, datetime
|
||||||
from typing import Optional, Dict, Any
|
from typing import Any, Dict
|
||||||
from .temporal import Year, Month, Day, Week, Hour
|
|
||||||
|
from .temporal import Day, Hour, Month, Week, Year
|
||||||
|
|
||||||
|
|
||||||
class Calendar:
|
class Calendar:
|
||||||
"""Calendar utilities for temporal calculations."""
|
"""Calendar utilities for temporal calculations."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def now() -> Dict[str, Any]:
|
def now() -> Dict[str, Any]:
|
||||||
"""Get current date and time components.
|
"""Get current date and time components.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with year, month, day, hour, minute, second
|
Dictionary with year, month, day, hour, minute, second
|
||||||
"""
|
"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
return {
|
return {
|
||||||
'year': Year(now.year),
|
"year": Year(now.year),
|
||||||
'month': Month(now.month),
|
"month": Month(now.month),
|
||||||
'day': Day(now.day),
|
"day": Day(now.day),
|
||||||
'hour': Hour(now.hour, now.minute, now.second),
|
"hour": Hour(now.hour, now.minute, now.second),
|
||||||
'week': Calendar.get_week(now.year, now.month, now.day),
|
"week": Calendar.get_week(now.year, now.month, now.day),
|
||||||
'datetime': now,
|
"datetime": now,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_week(year: int, month: int, day: int) -> Week:
|
def get_week(year: int, month: int, day: int) -> Week:
|
||||||
"""Get the week number for a given date.
|
"""Get the week number for a given date.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
year: The year
|
year: The year
|
||||||
month: The month (1-12)
|
month: The month (1-12)
|
||||||
day: The day of month (1-31)
|
day: The day of month (1-31)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Week object containing week number and year
|
Week object containing week number and year
|
||||||
"""
|
"""
|
||||||
d = date(year, month, day)
|
d = date(year, month, day)
|
||||||
week_num = d.isocalendar()[1]
|
week_num = d.isocalendar()[1]
|
||||||
return Week(week_num, year)
|
return Week(week_num, year)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def days_in_month(year: int, month: int) -> int:
|
def days_in_month(year: int, month: int) -> int:
|
||||||
"""Get the number of days in a month.
|
"""Get the number of days in a month.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
year: The year
|
year: The year
|
||||||
month: The month (1-12)
|
month: The month (1-12)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of days in that month
|
Number of days in that month
|
||||||
"""
|
"""
|
||||||
@@ -59,14 +60,14 @@ class Calendar:
|
|||||||
# February - check for leap year
|
# February - check for leap year
|
||||||
return 29 if (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)) else 28
|
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]
|
return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_leap_year(year: int) -> bool:
|
def is_leap_year(year: int) -> bool:
|
||||||
"""Check if a year is a leap year.
|
"""Check if a year is a leap year.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
year: The year to check
|
year: The year to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if leap year, False otherwise
|
True if leap year, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -6,37 +6,39 @@ and other astronomical/calendrical coordinates.
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class Season(Enum):
|
class Season(Enum):
|
||||||
"""The four seasons of the year."""
|
"""The four seasons of the year."""
|
||||||
SPRING = "Spring" # Vernal Equinox (Mar 20/21)
|
|
||||||
SUMMER = "Summer" # Summer Solstice (Jun 20/21)
|
SPRING = "Spring" # Vernal Equinox (Mar 20/21)
|
||||||
AUTUMN = "Autumn" # Autumnal Equinox (Sep 22/23)
|
SUMMER = "Summer" # Summer Solstice (Jun 20/21)
|
||||||
WINTER = "Winter" # Winter Solstice (Dec 21/22)
|
AUTUMN = "Autumn" # Autumnal Equinox (Sep 22/23)
|
||||||
|
WINTER = "Winter" # Winter Solstice (Dec 21/22)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SolarEvent:
|
class SolarEvent:
|
||||||
"""Represents a solar event (solstice or equinox).
|
"""Represents a solar event (solstice or equinox).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
event_type: The type of solar event (solstice or equinox)
|
event_type: The type of solar event (solstice or equinox)
|
||||||
date: The approximate date of the event
|
date: The approximate date of the event
|
||||||
season: The associated season
|
season: The associated season
|
||||||
"""
|
"""
|
||||||
|
|
||||||
event_type: str # "solstice" or "equinox"
|
event_type: str # "solstice" or "equinox"
|
||||||
date: tuple # (month, day)
|
date: tuple # (month, day)
|
||||||
season: Season
|
season: Season
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.season.value} {self.event_type.title()} ({self.date[0]}/{self.date[1]})"
|
return f"{self.season.value} {self.event_type.title()} ({self.date[0]}/{self.date[1]})"
|
||||||
|
|
||||||
|
|
||||||
class TemporalCoordinates:
|
class TemporalCoordinates:
|
||||||
"""Temporal positioning and astronomical calculations."""
|
"""Temporal positioning and astronomical calculations."""
|
||||||
|
|
||||||
# Approximate dates for solar events (can vary by ±1-2 days yearly)
|
# Approximate dates for solar events (can vary by ±1-2 days yearly)
|
||||||
SOLAR_EVENTS = {
|
SOLAR_EVENTS = {
|
||||||
Season.SPRING: SolarEvent("equinox", (3, 20), Season.SPRING),
|
Season.SPRING: SolarEvent("equinox", (3, 20), Season.SPRING),
|
||||||
@@ -44,15 +46,15 @@ class TemporalCoordinates:
|
|||||||
Season.AUTUMN: SolarEvent("equinox", (9, 22), Season.AUTUMN),
|
Season.AUTUMN: SolarEvent("equinox", (9, 22), Season.AUTUMN),
|
||||||
Season.WINTER: SolarEvent("solstice", (12, 21), Season.WINTER),
|
Season.WINTER: SolarEvent("solstice", (12, 21), Season.WINTER),
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_season(month: int, day: int) -> Season:
|
def get_season(month: int, day: int) -> Season:
|
||||||
"""Get the season for a given month and day.
|
"""Get the season for a given month and day.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
month: Month number (1-12)
|
month: Month number (1-12)
|
||||||
day: Day of month (1-31)
|
day: Day of month (1-31)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The Season enum value
|
The Season enum value
|
||||||
"""
|
"""
|
||||||
@@ -65,28 +67,28 @@ class TemporalCoordinates:
|
|||||||
return Season.AUTUMN
|
return Season.AUTUMN
|
||||||
else: # Winter
|
else: # Winter
|
||||||
return Season.WINTER
|
return Season.WINTER
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_solar_event(season: Season) -> Optional[SolarEvent]:
|
def get_solar_event(season: Season) -> Optional[SolarEvent]:
|
||||||
"""Get the solar event for a given season.
|
"""Get the solar event for a given season.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
season: The Season enum value
|
season: The Season enum value
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SolarEvent object for that season
|
SolarEvent object for that season
|
||||||
"""
|
"""
|
||||||
return TemporalCoordinates.SOLAR_EVENTS.get(season)
|
return TemporalCoordinates.SOLAR_EVENTS.get(season)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def days_until_event(month: int, day: int, target_season: Season) -> int:
|
def days_until_event(month: int, day: int, target_season: Season) -> int:
|
||||||
"""Calculate days until a solar event.
|
"""Calculate days until a solar event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
month: Current month (1-12)
|
month: Current month (1-12)
|
||||||
day: Current day (1-31)
|
day: Current day (1-31)
|
||||||
target_season: Target season for calculation
|
target_season: Target season for calculation
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Number of days until the target seasonal event
|
Number of days until the target seasonal event
|
||||||
"""
|
"""
|
||||||
@@ -94,28 +96,28 @@ class TemporalCoordinates:
|
|||||||
event = TemporalCoordinates.get_solar_event(target_season)
|
event = TemporalCoordinates.get_solar_event(target_season)
|
||||||
if not event:
|
if not event:
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
event_month, event_day = event.date
|
event_month, event_day = event.date
|
||||||
|
|
||||||
# Simple day-of-year calculation
|
# Simple day-of-year calculation
|
||||||
current_doy = TemporalCoordinates._day_of_year(month, day)
|
current_doy = TemporalCoordinates._day_of_year(month, day)
|
||||||
event_doy = TemporalCoordinates._day_of_year(event_month, event_day)
|
event_doy = TemporalCoordinates._day_of_year(event_month, event_day)
|
||||||
|
|
||||||
if event_doy >= current_doy:
|
if event_doy >= current_doy:
|
||||||
return event_doy - current_doy
|
return event_doy - current_doy
|
||||||
else:
|
else:
|
||||||
return (365 - current_doy) + event_doy
|
return (365 - current_doy) + event_doy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _day_of_year(month: int, day: int) -> int:
|
def _day_of_year(month: int, day: int) -> int:
|
||||||
"""Get the day of year (1-365/366).
|
"""Get the day of year (1-365/366).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
month: Month (1-12)
|
month: Month (1-12)
|
||||||
day: Day (1-31)
|
day: Day (1-31)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Day of year
|
Day of year
|
||||||
"""
|
"""
|
||||||
days_in_months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
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 dataclasses import dataclass
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Year:
|
class Year:
|
||||||
"""Represents a year in the Gregorian calendar."""
|
"""Represents a year in the Gregorian calendar."""
|
||||||
|
|
||||||
value: int
|
value: int
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Year({self.value})"
|
return f"Year({self.value})"
|
||||||
|
|
||||||
@@ -22,24 +22,35 @@ class Year:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Month:
|
class Month:
|
||||||
"""Represents a month (1-12)."""
|
"""Represents a month (1-12)."""
|
||||||
|
|
||||||
value: int
|
value: int
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 1 <= self.value <= 12:
|
if not 1 <= self.value <= 12:
|
||||||
raise ValueError(f"Month must be 1-12, got {self.value}")
|
raise ValueError(f"Month must be 1-12, got {self.value}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Get the month name."""
|
"""Get the month name."""
|
||||||
names = [
|
names = [
|
||||||
"January", "February", "March", "April", "May", "June",
|
"January",
|
||||||
"July", "August", "September", "October", "November", "December"
|
"February",
|
||||||
|
"March",
|
||||||
|
"April",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
]
|
]
|
||||||
return names[self.value - 1]
|
return names[self.value - 1]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Month({self.value})"
|
return f"Month({self.value})"
|
||||||
|
|
||||||
@@ -47,16 +58,17 @@ class Month:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Week:
|
class Week:
|
||||||
"""Represents a week in the calendar."""
|
"""Represents a week in the calendar."""
|
||||||
|
|
||||||
number: int # 1-53
|
number: int # 1-53
|
||||||
year: int
|
year: int
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 1 <= self.number <= 53:
|
if not 1 <= self.number <= 53:
|
||||||
raise ValueError(f"Week must be 1-53, got {self.number}")
|
raise ValueError(f"Week must be 1-53, got {self.number}")
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Week {self.number} of {self.year}"
|
return f"Week {self.number} of {self.year}"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Week({self.number}, {self.year})"
|
return f"Week({self.number}, {self.year})"
|
||||||
|
|
||||||
@@ -64,20 +76,21 @@ class Week:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Day:
|
class Day:
|
||||||
"""Represents a day of the month (1-31)."""
|
"""Represents a day of the month (1-31)."""
|
||||||
|
|
||||||
value: int
|
value: int
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 1 <= self.value <= 31:
|
if not 1 <= self.value <= 31:
|
||||||
raise ValueError(f"Day must be 1-31, got {self.value}")
|
raise ValueError(f"Day must be 1-31, got {self.value}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def day_of_week(self) -> str:
|
def day_of_week(self) -> str:
|
||||||
"""Get day of week name - requires full date context."""
|
"""Get day of week name - requires full date context."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.value)
|
return str(self.value)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Day({self.value})"
|
return f"Day({self.value})"
|
||||||
|
|
||||||
@@ -85,10 +98,11 @@ class Day:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Hour:
|
class Hour:
|
||||||
"""Represents an hour in 24-hour format (0-23)."""
|
"""Represents an hour in 24-hour format (0-23)."""
|
||||||
|
|
||||||
value: int
|
value: int
|
||||||
minute: int = 0
|
minute: int = 0
|
||||||
second: int = 0
|
second: int = 0
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 0 <= self.value <= 23:
|
if not 0 <= self.value <= 23:
|
||||||
raise ValueError(f"Hour must be 0-23, got {self.value}")
|
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}")
|
raise ValueError(f"Minute must be 0-59, got {self.minute}")
|
||||||
if not 0 <= self.second <= 59:
|
if not 0 <= self.second <= 59:
|
||||||
raise ValueError(f"Second must be 0-59, got {self.second}")
|
raise ValueError(f"Second must be 0-59, got {self.second}")
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.value:02d}:{self.minute:02d}:{self.second:02d}"
|
return f"{self.value:02d}:{self.minute:02d}:{self.second:02d}"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Hour({self.value}, {self.minute}, {self.second})"
|
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 datetime import datetime, time
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class TimeUtil:
|
class TimeUtil:
|
||||||
"""Utilities for time operations."""
|
"""Utilities for time operations."""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def current_time() -> time:
|
def current_time() -> time:
|
||||||
"""Get current time.
|
"""Get current time.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Current time object
|
Current time object
|
||||||
"""
|
"""
|
||||||
return datetime.now().time()
|
return datetime.now().time()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def seconds_to_hms(seconds: int) -> Dict[str, int]:
|
def seconds_to_hms(seconds: int) -> Dict[str, int]:
|
||||||
"""Convert seconds to hours, minutes, seconds.
|
"""Convert seconds to hours, minutes, seconds.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
seconds: Total seconds
|
seconds: Total seconds
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with 'hours', 'minutes', 'seconds' keys
|
Dictionary with 'hours', 'minutes', 'seconds' keys
|
||||||
"""
|
"""
|
||||||
@@ -33,34 +33,34 @@ class TimeUtil:
|
|||||||
remaining = seconds % 3600
|
remaining = seconds % 3600
|
||||||
minutes = remaining // 60
|
minutes = remaining // 60
|
||||||
secs = remaining % 60
|
secs = remaining % 60
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'hours': hours,
|
"hours": hours,
|
||||||
'minutes': minutes,
|
"minutes": minutes,
|
||||||
'seconds': secs,
|
"seconds": secs,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hms_to_seconds(hours: int, minutes: int = 0, seconds: int = 0) -> int:
|
def hms_to_seconds(hours: int, minutes: int = 0, seconds: int = 0) -> int:
|
||||||
"""Convert hours, minutes, seconds to total seconds.
|
"""Convert hours, minutes, seconds to total seconds.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
hours: Hours component
|
hours: Hours component
|
||||||
minutes: Minutes component (default 0)
|
minutes: Minutes component (default 0)
|
||||||
seconds: Seconds component (default 0)
|
seconds: Seconds component (default 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Total seconds
|
Total seconds
|
||||||
"""
|
"""
|
||||||
return hours * 3600 + minutes * 60 + seconds
|
return hours * 3600 + minutes * 60 + seconds
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_24_hour_format(hour: int) -> bool:
|
def is_24_hour_format(hour: int) -> bool:
|
||||||
"""Check if hour is valid in 24-hour format.
|
"""Check if hour is valid in 24-hour format.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
hour: The hour value to check
|
hour: The hour value to check
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if 0-23, False otherwise
|
True if 0-23, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
"""Utility modules for the Tarot project."""
|
"""Utility modules for the Tarot project."""
|
||||||
|
|
||||||
|
from .attributes import (
|
||||||
|
Cipher,
|
||||||
|
CipherResult,
|
||||||
|
Color,
|
||||||
|
Colorscale,
|
||||||
|
Element,
|
||||||
|
ElementType,
|
||||||
|
God,
|
||||||
|
Note,
|
||||||
|
Number,
|
||||||
|
Perfume,
|
||||||
|
Planet,
|
||||||
|
)
|
||||||
from .filter import (
|
from .filter import (
|
||||||
universal_filter,
|
describe_filter_fields,
|
||||||
get_filterable_fields,
|
|
||||||
filter_by,
|
filter_by,
|
||||||
format_results,
|
format_results,
|
||||||
get_filter_autocomplete,
|
get_filter_autocomplete,
|
||||||
describe_filter_fields,
|
get_filterable_fields,
|
||||||
)
|
universal_filter,
|
||||||
from .attributes import (
|
|
||||||
Note,
|
|
||||||
Element,
|
|
||||||
ElementType,
|
|
||||||
Number,
|
|
||||||
Color,
|
|
||||||
Colorscale,
|
|
||||||
Planet,
|
|
||||||
God,
|
|
||||||
Perfume,
|
|
||||||
Cipher,
|
|
||||||
CipherResult,
|
|
||||||
)
|
)
|
||||||
from .misc import (
|
from .misc import (
|
||||||
Personality,
|
|
||||||
MBTIType,
|
MBTIType,
|
||||||
|
Personality,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ exclusively to any single namespace.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
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
|
@dataclass
|
||||||
class Meaning:
|
class Meaning:
|
||||||
"""Represents the meaning of a card."""
|
"""Represents the meaning of a card."""
|
||||||
|
|
||||||
upright: str
|
upright: str
|
||||||
reversed: str
|
reversed: str
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ class Meaning:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Note:
|
class Note:
|
||||||
"""Represents a musical note with its properties."""
|
"""Represents a musical note with its properties."""
|
||||||
|
|
||||||
name: str # e.g., "C", "D", "E", "F#", "G", "A", "B"
|
name: str # e.g., "C", "D", "E", "F#", "G", "A", "B"
|
||||||
frequency: float # Frequency in Hz (A4 = 440 Hz)
|
frequency: float # Frequency in Hz (A4 = 440 Hz)
|
||||||
semitone: int # Position in chromatic scale (0-11)
|
semitone: int # Position in chromatic scale (0-11)
|
||||||
@@ -29,7 +31,7 @@ class Note:
|
|||||||
chakra: Optional[str] = None # Associated chakra if any
|
chakra: Optional[str] = None # Associated chakra if any
|
||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not 0 <= self.semitone <= 11:
|
if not 0 <= self.semitone <= 11:
|
||||||
raise ValueError(f"Semitone must be 0-11, got {self.semitone}")
|
raise ValueError(f"Semitone must be 0-11, got {self.semitone}")
|
||||||
@@ -40,6 +42,7 @@ class Note:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Element:
|
class Element:
|
||||||
"""Represents one of the four elements."""
|
"""Represents one of the four elements."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
symbol: str
|
symbol: str
|
||||||
color: str
|
color: str
|
||||||
@@ -50,6 +53,7 @@ class Element:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ElementType:
|
class ElementType:
|
||||||
"""Represents an elemental force (Fire, Water, Air, Earth, Spirit)."""
|
"""Represents an elemental force (Fire, Water, Air, Earth, Spirit)."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
symbol: str
|
symbol: str
|
||||||
direction: str
|
direction: str
|
||||||
@@ -68,16 +72,17 @@ class ElementType:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Number:
|
class Number:
|
||||||
"""Represents a number (1-9) with Kabbalistic attributes."""
|
"""Represents a number (1-9) with Kabbalistic attributes."""
|
||||||
|
|
||||||
value: int
|
value: int
|
||||||
sephera: str
|
sephera: str
|
||||||
element: str
|
element: str
|
||||||
compliment: int
|
compliment: int
|
||||||
color: Optional['Color'] = None
|
color: Optional["Color"] = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if not (1 <= self.value <= 9):
|
if not (1 <= self.value <= 9):
|
||||||
raise ValueError(f"Number value must be between 1 and 9, got {self.value}")
|
raise ValueError(f"Number value must be between 1 and 9, got {self.value}")
|
||||||
|
|
||||||
# Auto-calculate compliment: numbers complement to sum to 9
|
# Auto-calculate compliment: numbers complement to sum to 9
|
||||||
# 1↔8, 2↔7, 3↔6, 4↔5, 9↔9
|
# 1↔8, 2↔7, 3↔6, 4↔5, 9↔9
|
||||||
self.compliment = 9 - self.value if self.value != 9 else 9
|
self.compliment = 9 - self.value if self.value != 9 else 9
|
||||||
@@ -87,13 +92,14 @@ class Number:
|
|||||||
class Colorscale:
|
class Colorscale:
|
||||||
"""
|
"""
|
||||||
Represents Golden Dawn color scales (King, Queen, Emperor, Empress).
|
Represents Golden Dawn color scales (King, Queen, Emperor, Empress).
|
||||||
|
|
||||||
The four scales correspond to the four worlds/letters of Tetragrammaton:
|
The four scales correspond to the four worlds/letters of Tetragrammaton:
|
||||||
- King Scale (Yod): Father, originating impulse, pure archetype
|
- King Scale (Yod): Father, originating impulse, pure archetype
|
||||||
- Queen Scale (He): Mother, receptive, earthy counterpart
|
- Queen Scale (He): Mother, receptive, earthy counterpart
|
||||||
- Emperor Scale (Vau): Son/Form, active expression, concrete manifestation
|
- Emperor Scale (Vau): Son/Form, active expression, concrete manifestation
|
||||||
- Empress Scale (He final): Daughter, physical manifestation, receptivity in Assiah
|
- Empress Scale (He final): Daughter, physical manifestation, receptivity in Assiah
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str # Sephira/Path name (e.g., "Kether", "Path of Aleph")
|
name: str # Sephira/Path name (e.g., "Kether", "Path of Aleph")
|
||||||
number: int # 1-10 for Sephiroth, 11-32 for Paths
|
number: int # 1-10 for Sephiroth, 11-32 for Paths
|
||||||
king_scale: str # Yod - Father principle
|
king_scale: str # Yod - Father principle
|
||||||
@@ -109,6 +115,7 @@ class Colorscale:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Color:
|
class Color:
|
||||||
"""Represents a color with Kabbalistic correspondences."""
|
"""Represents a color with Kabbalistic correspondences."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
hex_value: str
|
hex_value: str
|
||||||
rgb: Tuple[int, int, int]
|
rgb: Tuple[int, int, int]
|
||||||
@@ -119,12 +126,12 @@ class Color:
|
|||||||
meaning: str
|
meaning: str
|
||||||
tarot_associations: List[str] = field(default_factory=list)
|
tarot_associations: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
# Validate hex color
|
# Validate hex color
|
||||||
if not self.hex_value.startswith("#") or len(self.hex_value) != 7:
|
if not self.hex_value.startswith("#") or len(self.hex_value) != 7:
|
||||||
raise ValueError(f"Invalid hex color: {self.hex_value}")
|
raise ValueError(f"Invalid hex color: {self.hex_value}")
|
||||||
|
|
||||||
# Validate RGB values
|
# Validate RGB values
|
||||||
if not all(0 <= c <= 255 for c in self.rgb):
|
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}")
|
raise ValueError(f"RGB values must be between 0 and 255, got {self.rgb}")
|
||||||
@@ -133,6 +140,7 @@ class Color:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Planet:
|
class Planet:
|
||||||
"""Represents a planetary correspondence entry."""
|
"""Represents a planetary correspondence entry."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
symbol: str
|
symbol: str
|
||||||
element: str
|
element: str
|
||||||
@@ -142,34 +150,35 @@ class Planet:
|
|||||||
keywords: List[str] = field(default_factory=list)
|
keywords: List[str] = field(default_factory=list)
|
||||||
color: Optional[Color] = None
|
color: Optional[Color] = None
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return nicely formatted string representation of the Planet."""
|
"""Return nicely formatted string representation of the Planet."""
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"{self.name} ({self.symbol})")
|
lines.append(f"{self.name} ({self.symbol})")
|
||||||
lines.append(f" element: {self.element}")
|
lines.append(f" element: {self.element}")
|
||||||
|
|
||||||
if self.ruling_zodiac:
|
if self.ruling_zodiac:
|
||||||
lines.append(f" ruling_zodiac: {', '.join(self.ruling_zodiac)}")
|
lines.append(f" ruling_zodiac: {', '.join(self.ruling_zodiac)}")
|
||||||
|
|
||||||
if self.associated_letters:
|
if self.associated_letters:
|
||||||
lines.append(f" associated_letters: {', '.join(self.associated_letters)}")
|
lines.append(f" associated_letters: {', '.join(self.associated_letters)}")
|
||||||
|
|
||||||
if self.keywords:
|
if self.keywords:
|
||||||
lines.append(f" keywords: {', '.join(self.keywords)}")
|
lines.append(f" keywords: {', '.join(self.keywords)}")
|
||||||
|
|
||||||
if self.color:
|
if self.color:
|
||||||
lines.append(f" color: {self.color.name}")
|
lines.append(f" color: {self.color.name}")
|
||||||
|
|
||||||
if self.description:
|
if self.description:
|
||||||
lines.append(f" description: {self.description}")
|
lines.append(f" description: {self.description}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class God:
|
class God:
|
||||||
"""Unified deity representation that synchronizes multiple pantheons."""
|
"""Unified deity representation that synchronizes multiple pantheons."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
culture: str
|
culture: str
|
||||||
pantheon: str
|
pantheon: str
|
||||||
@@ -195,60 +204,65 @@ class God:
|
|||||||
def primary_number(self) -> Optional[Number]:
|
def primary_number(self) -> Optional[Number]:
|
||||||
"""Return the first associated number if one is available."""
|
"""Return the first associated number if one is available."""
|
||||||
return self.associated_numbers[0] if self.associated_numbers else None
|
return self.associated_numbers[0] if self.associated_numbers else None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return nicely formatted string representation of the God."""
|
"""Return nicely formatted string representation of the God."""
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"{self.name}")
|
lines.append(f"{self.name}")
|
||||||
lines.append(f" culture: {self.culture}")
|
lines.append(f" culture: {self.culture}")
|
||||||
lines.append(f" pantheon: {self.pantheon}")
|
lines.append(f" pantheon: {self.pantheon}")
|
||||||
|
|
||||||
if self.domains:
|
if self.domains:
|
||||||
lines.append(f" domains: {', '.join(self.domains)}")
|
lines.append(f" domains: {', '.join(self.domains)}")
|
||||||
|
|
||||||
if self.epithets:
|
if self.epithets:
|
||||||
lines.append(f" epithets: {', '.join(self.epithets)}")
|
lines.append(f" epithets: {', '.join(self.epithets)}")
|
||||||
|
|
||||||
if self.mythology:
|
if self.mythology:
|
||||||
lines.append(f" mythology: {self.mythology}")
|
lines.append(f" mythology: {self.mythology}")
|
||||||
|
|
||||||
if self.sephera_numbers:
|
if self.sephera_numbers:
|
||||||
lines.append(f" sephera_numbers: {', '.join(str(n) for n in self.sephera_numbers)}")
|
lines.append(f" sephera_numbers: {', '.join(str(n) for n in self.sephera_numbers)}")
|
||||||
|
|
||||||
if self.path_numbers:
|
if self.path_numbers:
|
||||||
lines.append(f" path_numbers: {', '.join(str(n) for n in self.path_numbers)}")
|
lines.append(f" path_numbers: {', '.join(str(n) for n in self.path_numbers)}")
|
||||||
|
|
||||||
if self.planets:
|
if self.planets:
|
||||||
lines.append(f" planets: {', '.join(self.planets)}")
|
lines.append(f" planets: {', '.join(self.planets)}")
|
||||||
|
|
||||||
if self.elements:
|
if self.elements:
|
||||||
lines.append(f" elements: {', '.join(self.elements)}")
|
lines.append(f" elements: {', '.join(self.elements)}")
|
||||||
|
|
||||||
if self.zodiac_signs:
|
if self.zodiac_signs:
|
||||||
lines.append(f" zodiac_signs: {', '.join(self.zodiac_signs)}")
|
lines.append(f" zodiac_signs: {', '.join(self.zodiac_signs)}")
|
||||||
|
|
||||||
if self.associated_planet:
|
if self.associated_planet:
|
||||||
lines.append(f" associated_planet: {self.associated_planet.name}")
|
lines.append(f" associated_planet: {self.associated_planet.name}")
|
||||||
|
|
||||||
if self.associated_element:
|
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}")
|
lines.append(f" associated_element: {elem_name}")
|
||||||
|
|
||||||
if self.tarot_trumps:
|
if self.tarot_trumps:
|
||||||
lines.append(f" tarot_trumps: {', '.join(self.tarot_trumps)}")
|
lines.append(f" tarot_trumps: {', '.join(self.tarot_trumps)}")
|
||||||
|
|
||||||
if self.keywords:
|
if self.keywords:
|
||||||
lines.append(f" keywords: {', '.join(self.keywords)}")
|
lines.append(f" keywords: {', '.join(self.keywords)}")
|
||||||
|
|
||||||
if self.description:
|
if self.description:
|
||||||
lines.append(f" description: {self.description}")
|
lines.append(f" description: {self.description}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Perfume:
|
class Perfume:
|
||||||
"""Represents a perfume/incense correspondence in Kabbalah."""
|
"""Represents a perfume/incense correspondence in Kabbalah."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
alternative_names: List[str] = field(default_factory=list)
|
alternative_names: List[str] = field(default_factory=list)
|
||||||
scent_profile: str = "" # e.g., "Resinous", "Floral", "Spicy", "Earthy"
|
scent_profile: str = "" # e.g., "Resinous", "Floral", "Spicy", "Earthy"
|
||||||
@@ -263,49 +277,49 @@ class Perfume:
|
|||||||
magical_uses: List[str] = field(default_factory=list)
|
magical_uses: List[str] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return nicely formatted string representation of the Perfume."""
|
"""Return nicely formatted string representation of the Perfume."""
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"{self.name}")
|
lines.append(f"{self.name}")
|
||||||
|
|
||||||
if self.alternative_names:
|
if self.alternative_names:
|
||||||
lines.append(f" alternative_names: {', '.join(self.alternative_names)}")
|
lines.append(f" alternative_names: {', '.join(self.alternative_names)}")
|
||||||
|
|
||||||
if self.scent_profile:
|
if self.scent_profile:
|
||||||
lines.append(f" scent_profile: {self.scent_profile}")
|
lines.append(f" scent_profile: {self.scent_profile}")
|
||||||
|
|
||||||
# Correspondences
|
# Correspondences
|
||||||
if self.sephera_number is not None:
|
if self.sephera_number is not None:
|
||||||
lines.append(f" sephera_number: {self.sephera_number}")
|
lines.append(f" sephera_number: {self.sephera_number}")
|
||||||
|
|
||||||
if self.path_number is not None:
|
if self.path_number is not None:
|
||||||
lines.append(f" path_number: {self.path_number}")
|
lines.append(f" path_number: {self.path_number}")
|
||||||
|
|
||||||
if self.element:
|
if self.element:
|
||||||
lines.append(f" element: {self.element}")
|
lines.append(f" element: {self.element}")
|
||||||
|
|
||||||
if self.planet:
|
if self.planet:
|
||||||
lines.append(f" planet: {self.planet}")
|
lines.append(f" planet: {self.planet}")
|
||||||
|
|
||||||
if self.zodiac_sign:
|
if self.zodiac_sign:
|
||||||
lines.append(f" zodiac_sign: {self.zodiac_sign}")
|
lines.append(f" zodiac_sign: {self.zodiac_sign}")
|
||||||
|
|
||||||
if self.astrological_quality:
|
if self.astrological_quality:
|
||||||
lines.append(f" astrological_quality: {self.astrological_quality}")
|
lines.append(f" astrological_quality: {self.astrological_quality}")
|
||||||
|
|
||||||
if self.keywords:
|
if self.keywords:
|
||||||
lines.append(f" keywords: {', '.join(self.keywords)}")
|
lines.append(f" keywords: {', '.join(self.keywords)}")
|
||||||
|
|
||||||
if self.magical_uses:
|
if self.magical_uses:
|
||||||
lines.append(f" magical_uses: {', '.join(self.magical_uses)}")
|
lines.append(f" magical_uses: {', '.join(self.magical_uses)}")
|
||||||
|
|
||||||
if self.description:
|
if self.description:
|
||||||
lines.append(f" description: {self.description}")
|
lines.append(f" description: {self.description}")
|
||||||
|
|
||||||
if self.notes:
|
if self.notes:
|
||||||
lines.append(f" notes: {self.notes}")
|
lines.append(f" notes: {self.notes}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -362,9 +376,7 @@ class Cipher:
|
|||||||
expanded.append(self.pattern[idx % len(self.pattern)])
|
expanded.append(self.pattern[idx % len(self.pattern)])
|
||||||
idx += 1
|
idx += 1
|
||||||
return expanded
|
return expanded
|
||||||
raise ValueError(
|
raise ValueError("Cipher pattern length does not match alphabet and cycling is disabled")
|
||||||
"Cipher pattern length does not match alphabet and cycling is disabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -10,51 +10,57 @@ Provides a single, reusable filter mechanism that works across all modules:
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
from utils.filter import universal_filter, get_filterable_fields
|
from utils.filter import universal_filter, get_filterable_fields
|
||||||
|
|
||||||
# For TarotLetter
|
# For TarotLetter
|
||||||
results = universal_filter(Tarot.letters.all(), letter_type="Mother")
|
results = universal_filter(Tarot.letters.all(), letter_type="Mother")
|
||||||
|
|
||||||
# For Card
|
# For Card
|
||||||
results = universal_filter(Tarot.deck.cards, arcana="Major")
|
results = universal_filter(Tarot.deck.cards, arcana="Major")
|
||||||
|
|
||||||
# Get available fields for introspection
|
# Get available fields for introspection
|
||||||
fields = get_filterable_fields(TarotLetter)
|
fields = get_filterable_fields(TarotLetter)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Any, TypeVar, Union, Dict
|
from dataclasses import fields, is_dataclass
|
||||||
from dataclasses import is_dataclass, fields
|
from typing import Any, Dict, List, TypeVar
|
||||||
from utils.object_formatting import get_item_label, is_nested_object, get_object_attributes, format_value
|
|
||||||
|
|
||||||
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]:
|
def get_filterable_fields(dataclass_type) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Dynamically get all filterable fields from a dataclass.
|
Dynamically get all filterable fields from a dataclass.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dataclass_type: A dataclass type (e.g., TarotLetter, Card)
|
dataclass_type: A dataclass type (e.g., TarotLetter, Card)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of field names available for filtering
|
List of field names available for filtering
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
TypeError: If the type is not a dataclass
|
TypeError: If the type is not a dataclass
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
fields = get_filterable_fields(TarotLetter)
|
fields = get_filterable_fields(TarotLetter)
|
||||||
# ['hebrew_letter', 'transliteration', 'letter_type', ...]
|
# ['hebrew_letter', 'transliteration', 'letter_type', ...]
|
||||||
"""
|
"""
|
||||||
if not is_dataclass(dataclass_type):
|
if not is_dataclass(dataclass_type):
|
||||||
raise TypeError(f"{dataclass_type} is not a dataclass")
|
raise TypeError(f"{dataclass_type} is not a dataclass")
|
||||||
|
|
||||||
return [f.name for f in fields(dataclass_type)]
|
return [f.name for f in fields(dataclass_type)]
|
||||||
|
|
||||||
|
|
||||||
def _matches_filter(obj: Any, key: str, value: Any) -> bool:
|
def _matches_filter(obj: Any, key: str, value: Any) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if an object matches a filter criterion.
|
Check if an object matches a filter criterion.
|
||||||
|
|
||||||
Handles:
|
Handles:
|
||||||
- String matching (case-insensitive)
|
- String matching (case-insensitive)
|
||||||
- Numeric matching (exact)
|
- Numeric matching (exact)
|
||||||
@@ -63,25 +69,25 @@ def _matches_filter(obj: Any, key: str, value: Any) -> bool:
|
|||||||
- None/null matching
|
- None/null matching
|
||||||
- Nested object attribute matching (e.g., suit="Cups" matches Suit(name="Cups"))
|
- Nested object attribute matching (e.g., suit="Cups" matches Suit(name="Cups"))
|
||||||
- Multiple values (comma-separated strings or lists for OR logic)
|
- Multiple values (comma-separated strings or lists for OR logic)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The object to check
|
obj: The object to check
|
||||||
key: The attribute name
|
key: The attribute name
|
||||||
value: The value to match against (string, int, list, or comma-separated string)
|
value: The value to match against (string, int, list, or comma-separated string)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the object matches the filter, False otherwise
|
True if the object matches the filter, False otherwise
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
_matches_filter(card, "number", 3) # Single number
|
_matches_filter(card, "number", 3) # Single number
|
||||||
_matches_filter(card, "number", [3, 5, 6]) # Multiple numbers (OR)
|
_matches_filter(card, "number", [3, 5, 6]) # Multiple numbers (OR)
|
||||||
_matches_filter(card, "number", "3,5,6") # Comma-separated (OR)
|
_matches_filter(card, "number", "3,5,6") # Comma-separated (OR)
|
||||||
"""
|
"""
|
||||||
attr_value = getattr(obj, key, None)
|
attr_value = getattr(obj, key, None)
|
||||||
|
|
||||||
if attr_value is None:
|
if attr_value is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Parse multiple values (comma-separated string or list)
|
# Parse multiple values (comma-separated string or list)
|
||||||
values_to_check = []
|
values_to_check = []
|
||||||
if isinstance(value, str) and "," in value:
|
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)
|
values_to_check = list(value)
|
||||||
else:
|
else:
|
||||||
values_to_check = [value]
|
values_to_check = [value]
|
||||||
|
|
||||||
# Check if attribute matches ANY of the provided values (OR logic)
|
# Check if attribute matches ANY of the provided values (OR logic)
|
||||||
for check_value in values_to_check:
|
for check_value in values_to_check:
|
||||||
# Handle list attributes (like keywords, colors, etc.)
|
# Handle list attributes (like keywords, colors, etc.)
|
||||||
if isinstance(attr_value, list):
|
if isinstance(attr_value, list):
|
||||||
if any(
|
if any(str(check_value).lower() == str(item).lower() for item in attr_value):
|
||||||
str(check_value).lower() == str(item).lower()
|
|
||||||
for item in attr_value
|
|
||||||
):
|
|
||||||
return True
|
return True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle numeric comparisons
|
# Handle numeric comparisons
|
||||||
if isinstance(check_value, int) and isinstance(attr_value, int):
|
if isinstance(check_value, int) and isinstance(attr_value, int):
|
||||||
if attr_value == check_value:
|
if attr_value == check_value:
|
||||||
return True
|
return True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle boolean comparisons
|
# Handle boolean comparisons
|
||||||
if isinstance(check_value, bool) and isinstance(attr_value, bool):
|
if isinstance(check_value, bool) and isinstance(attr_value, bool):
|
||||||
if attr_value == check_value:
|
if attr_value == check_value:
|
||||||
return True
|
return True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle nested object comparison: if attr_value has a 'name' attribute,
|
# 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"))
|
# try matching the value against that (e.g., suit="Cups" vs Suit(name="Cups"))
|
||||||
if hasattr(attr_value, 'name'):
|
if hasattr(attr_value, "name"):
|
||||||
nested_name = getattr(attr_value, 'name', None)
|
nested_name = getattr(attr_value, "name", None)
|
||||||
if nested_name is not None:
|
if nested_name is not None:
|
||||||
if str(nested_name).lower() == str(check_value).lower():
|
if str(nested_name).lower() == str(check_value).lower():
|
||||||
return True
|
return True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle string comparisons
|
# Handle string comparisons
|
||||||
if str(attr_value).lower() == str(check_value).lower():
|
if str(attr_value).lower() == str(check_value).lower():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def universal_filter(items: List[T], **kwargs) -> List[T]:
|
def universal_filter(items: List[T], **kwargs) -> List[T]:
|
||||||
"""
|
"""
|
||||||
Universal filter function that works on any list of dataclass objects.
|
Universal filter function that works on any list of dataclass objects.
|
||||||
|
|
||||||
Dynamically filters a list of objects by any combination of their attributes.
|
Dynamically filters a list of objects by any combination of their attributes.
|
||||||
Works with any dataclass-based type throughout the project.
|
Works with any dataclass-based type throughout the project.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
items: List of objects to filter (typically all objects from a collection)
|
items: List of objects to filter (typically all objects from a collection)
|
||||||
**kwargs: Attribute filters (field_name=value or field_name=[value1, value2])
|
**kwargs: Attribute filters (field_name=value or field_name=[value1, value2])
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Filtered list containing only items matching ALL criteria
|
Filtered list containing only items matching ALL criteria
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Case-insensitive string matching
|
- Case-insensitive string matching
|
||||||
- Numeric exact 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
|
- Multiple values per field (comma-separated string or list) for OR matching
|
||||||
- Works with any dataclass object
|
- Works with any dataclass object
|
||||||
- Field aliases (e.g., 'type' -> 'arcana' for Cards)
|
- Field aliases (e.g., 'type' -> 'arcana' for Cards)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# Single values
|
# Single values
|
||||||
results = universal_filter(Tarot.deck.cards, arcana="Major")
|
results = universal_filter(Tarot.deck.cards, arcana="Major")
|
||||||
|
|
||||||
# Multiple values (OR logic) - comma-separated
|
# Multiple values (OR logic) - comma-separated
|
||||||
results = universal_filter(Tarot.deck.cards, number="3,5,6")
|
results = universal_filter(Tarot.deck.cards, number="3,5,6")
|
||||||
|
|
||||||
# Multiple values (OR logic) - list
|
# Multiple values (OR logic) - list
|
||||||
results = universal_filter(Tarot.deck.cards, number=[3, 5, 6])
|
results = universal_filter(Tarot.deck.cards, number=[3, 5, 6])
|
||||||
|
|
||||||
# Combine multiple fields (AND logic)
|
# Combine multiple fields (AND logic)
|
||||||
results = universal_filter(Tarot.deck.cards, suit="Cups", type="Court")
|
results = universal_filter(Tarot.deck.cards, suit="Cups", type="Court")
|
||||||
|
|
||||||
# Multiple values in one field + other filters
|
# Multiple values in one field + other filters
|
||||||
results = universal_filter(Tarot.deck.cards, number="3,5,6", suit="Wands")
|
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 = {
|
field_aliases = {
|
||||||
# No aliases - use direct property/field names
|
# No aliases - use direct property/field names
|
||||||
}
|
}
|
||||||
|
|
||||||
results = items
|
results = items
|
||||||
|
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Apply field alias if it exists
|
# Apply field alias if it exists
|
||||||
actual_key = field_aliases.get(key, key)
|
actual_key = field_aliases.get(key, key)
|
||||||
|
|
||||||
results = [
|
results = [obj for obj in results if _matches_filter(obj, actual_key, value)]
|
||||||
obj for obj in results
|
|
||||||
if _matches_filter(obj, actual_key, value)
|
|
||||||
]
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
def format_results(items: List[Any]) -> str:
|
def format_results(items: List[Any]) -> str:
|
||||||
"""
|
"""
|
||||||
Format a list of objects for user-friendly display.
|
Format a list of objects for user-friendly display.
|
||||||
|
|
||||||
Works with any object type and recursively formats nested structures.
|
Works with any object type and recursively formats nested structures.
|
||||||
Each object is separated by a blank line.
|
Each object is separated by a blank line.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
items: List of objects to format
|
items: List of objects to format
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string with proper indentation and hierarchy
|
Formatted string with proper indentation and hierarchy
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
results = universal_filter(Tarot.letters.all(), element="Fire")
|
results = universal_filter(Tarot.letters.all(), element="Fire")
|
||||||
print(format_results(results))
|
print(format_results(results))
|
||||||
"""
|
"""
|
||||||
if not items:
|
if not items:
|
||||||
return "(no items)"
|
return "(no items)"
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for item in items:
|
for item in items:
|
||||||
# Get label for item (handles name, transliteration, or str())
|
# Get label for item (handles name, transliteration, or str())
|
||||||
label = get_item_label(item, fallback=str(item))
|
label = get_item_label(item, fallback=str(item))
|
||||||
lines.append(f"--- {label} ---")
|
lines.append(f"--- {label} ---")
|
||||||
|
|
||||||
# Format all attributes with proper nesting
|
# Format all attributes with proper nesting
|
||||||
for attr_name, attr_value in get_object_attributes(item):
|
for attr_name, attr_value in get_object_attributes(item):
|
||||||
if is_nested_object(attr_value):
|
if is_nested_object(attr_value):
|
||||||
@@ -227,9 +227,9 @@ def format_results(items: List[Any]) -> str:
|
|||||||
lines.append(nested)
|
lines.append(nested)
|
||||||
else:
|
else:
|
||||||
lines.append(f" {attr_name}: {attr_value}")
|
lines.append(f" {attr_name}: {attr_value}")
|
||||||
|
|
||||||
lines.append("") # Blank line between items
|
lines.append("") # Blank line between items
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -240,16 +240,16 @@ filter_by = universal_filter
|
|||||||
def get_filter_autocomplete(dataclass_type) -> Dict[str, List[str]]:
|
def get_filter_autocomplete(dataclass_type) -> Dict[str, List[str]]:
|
||||||
"""
|
"""
|
||||||
Get autocomplete suggestions for filtering a dataclass type.
|
Get autocomplete suggestions for filtering a dataclass type.
|
||||||
|
|
||||||
Returns a dictionary mapping field names to example values found in the dataclass.
|
Returns a dictionary mapping field names to example values found in the dataclass.
|
||||||
Useful for IDE autocomplete and CLI help.
|
Useful for IDE autocomplete and CLI help.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dataclass_type: A dataclass type (e.g., TarotLetter, Card, Wall)
|
dataclass_type: A dataclass type (e.g., TarotLetter, Card, Wall)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with field names as keys and list of example values
|
Dictionary with field names as keys and list of example values
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
autocomplete = get_filter_autocomplete(TarotLetter)
|
autocomplete = get_filter_autocomplete(TarotLetter)
|
||||||
# Returns:
|
# Returns:
|
||||||
@@ -262,41 +262,41 @@ def get_filter_autocomplete(dataclass_type) -> Dict[str, List[str]]:
|
|||||||
"""
|
"""
|
||||||
if not is_dataclass(dataclass_type):
|
if not is_dataclass(dataclass_type):
|
||||||
raise TypeError(f"{dataclass_type} is not a dataclass")
|
raise TypeError(f"{dataclass_type} is not a dataclass")
|
||||||
|
|
||||||
autocomplete = {}
|
autocomplete = {}
|
||||||
field_names = [f.name for f in fields(dataclass_type)]
|
field_names = [f.name for f in fields(dataclass_type)]
|
||||||
|
|
||||||
for field_name in field_names:
|
for field_name in field_names:
|
||||||
autocomplete[field_name] = f"<value for {field_name}>"
|
autocomplete[field_name] = f"<value for {field_name}>"
|
||||||
|
|
||||||
return autocomplete
|
return autocomplete
|
||||||
|
|
||||||
|
|
||||||
def describe_filter_fields(dataclass_type) -> str:
|
def describe_filter_fields(dataclass_type) -> str:
|
||||||
"""
|
"""
|
||||||
Get a human-readable description of all filterable fields.
|
Get a human-readable description of all filterable fields.
|
||||||
|
|
||||||
Useful for help text and documentation.
|
Useful for help text and documentation.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dataclass_type: A dataclass type
|
dataclass_type: A dataclass type
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string with field descriptions
|
Formatted string with field descriptions
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
print(describe_filter_fields(TarotLetter))
|
print(describe_filter_fields(TarotLetter))
|
||||||
"""
|
"""
|
||||||
if not is_dataclass(dataclass_type):
|
if not is_dataclass(dataclass_type):
|
||||||
raise TypeError(f"{dataclass_type} is not a dataclass")
|
raise TypeError(f"{dataclass_type} is not a dataclass")
|
||||||
|
|
||||||
field_list = get_filterable_fields(dataclass_type)
|
field_list = get_filterable_fields(dataclass_type)
|
||||||
lines = [
|
lines = [
|
||||||
f"Filterable fields for {dataclass_type}:",
|
f"Filterable fields for {dataclass_type}:",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
for field_name in field_list:
|
for field_name in field_list:
|
||||||
lines.append(f" • {field_name}")
|
lines.append(f" • {field_name}")
|
||||||
|
|
||||||
return "\n".join(lines)
|
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 dataclasses import dataclass
|
||||||
from typing import Optional, TYPE_CHECKING
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tarot.deck.deck import CourtCard
|
from tarot.deck.deck import CourtCard
|
||||||
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class MBTIType(Enum):
|
class MBTIType(Enum):
|
||||||
"""16 MBTI personality types."""
|
"""16 MBTI personality types."""
|
||||||
|
|
||||||
ISTJ = "ISTJ"
|
ISTJ = "ISTJ"
|
||||||
ISFJ = "ISFJ"
|
ISFJ = "ISFJ"
|
||||||
INFJ = "INFJ"
|
INFJ = "INFJ"
|
||||||
@@ -36,79 +37,76 @@ class MBTIType(Enum):
|
|||||||
class Personality:
|
class Personality:
|
||||||
"""
|
"""
|
||||||
MBTI Personality Type mapped to a specific Tarot Court Card.
|
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
|
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
|
personality types and their corresponding Tarot court cards. Based on the
|
||||||
comprehensive system developed by Dante DiMatteo at 78 Revelations Per Minute:
|
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/
|
https://78revelationsaminute.wordpress.com/2015/07/08/personality-types-the-tarot-court-cards-and-the-myers-briggs-type-indicator/
|
||||||
|
|
||||||
The mapping is based on:
|
The mapping is based on:
|
||||||
- SUITS correspond to Jung's 4 cognitive functions:
|
- SUITS correspond to Jung's 4 cognitive functions:
|
||||||
* Wands: Intuition (N)
|
* Wands: Intuition (N)
|
||||||
* Cups: Feeling (F)
|
* Cups: Feeling (F)
|
||||||
* Swords: Thinking (T)
|
* Swords: Thinking (T)
|
||||||
* Pentacles: Sensation (S)
|
* Pentacles: Sensation (S)
|
||||||
|
|
||||||
- RANKS correspond to MBTI traits:
|
- RANKS correspond to MBTI traits:
|
||||||
* Kings (E + J): Extraverted Judgers
|
* Kings (E + J): Extraverted Judgers
|
||||||
* Queens (I + J): Introverted Judgers
|
* Queens (I + J): Introverted Judgers
|
||||||
* Princes (E + P): Extraverted Perceivers
|
* Princes (E + P): Extraverted Perceivers
|
||||||
* Princesses (I + P): Introverted Perceivers
|
* Princesses (I + P): Introverted Perceivers
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
mbti_type: The MBTI personality type (e.g., ENFP)
|
mbti_type: The MBTI personality type (e.g., ENFP)
|
||||||
court_card: The single CourtCard object representing this personality
|
court_card: The single CourtCard object representing this personality
|
||||||
description: Brief description of the personality archetype
|
description: Brief description of the personality archetype
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mbti_type: MBTIType
|
mbti_type: MBTIType
|
||||||
court_card: Optional['CourtCard'] = None
|
court_card: Optional["CourtCard"] = None
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
# Direct MBTI-to-CourtCard mapping (1-to-1 relationship)
|
# Direct MBTI-to-CourtCard mapping (1-to-1 relationship)
|
||||||
# Format: MBTI_TYPE -> (Rank, Suit)
|
# Format: MBTI_TYPE -> (Rank, Suit)
|
||||||
_MBTI_TO_CARD_MAPPING = {
|
_MBTI_TO_CARD_MAPPING = {
|
||||||
# KINGS (E + J) - Extraverted Judgers
|
# KINGS (E + J) - Extraverted Judgers
|
||||||
"ENTJ": ("Knight", "Wands"), # Fiery, forceful leadership
|
"ENTJ": ("Knight", "Wands"), # Fiery, forceful leadership
|
||||||
"ENFJ": ("Knight", "Cups"), # Sensitive, mission-driven
|
"ENFJ": ("Knight", "Cups"), # Sensitive, mission-driven
|
||||||
"ESTJ": ("Knight", "Swords"), # Practical, pragmatic
|
"ESTJ": ("Knight", "Swords"), # Practical, pragmatic
|
||||||
"ESFJ": ("Knight", "Pentacles"), # Sociable, consensus-seeking
|
"ESFJ": ("Knight", "Pentacles"), # Sociable, consensus-seeking
|
||||||
|
|
||||||
# QUEENS (I + J) - Introverted Judgers
|
# QUEENS (I + J) - Introverted Judgers
|
||||||
"INTJ": ("Queen", "Wands"), # Analytical, self-motivated
|
"INTJ": ("Queen", "Wands"), # Analytical, self-motivated
|
||||||
"INFJ": ("Queen", "Cups"), # Sensitive, interconnected
|
"INFJ": ("Queen", "Cups"), # Sensitive, interconnected
|
||||||
"ISTJ": ("Queen", "Swords"), # Pragmatic, duty-fulfiller
|
"ISTJ": ("Queen", "Swords"), # Pragmatic, duty-fulfiller
|
||||||
"ISFJ": ("Queen", "Pentacles"), # Caring, earth-mother type
|
"ISFJ": ("Queen", "Pentacles"), # Caring, earth-mother type
|
||||||
|
|
||||||
# PRINCES (E + P) - Extraverted Perceivers
|
# PRINCES (E + P) - Extraverted Perceivers
|
||||||
"ENTP": ("Prince", "Wands"), # Visionary, quick-study
|
"ENTP": ("Prince", "Wands"), # Visionary, quick-study
|
||||||
"ENFP": ("Prince", "Cups"), # Inspiring, intuitive
|
"ENFP": ("Prince", "Cups"), # Inspiring, intuitive
|
||||||
"ESTP": ("Prince", "Swords"), # Action-oriented, risk-taker
|
"ESTP": ("Prince", "Swords"), # Action-oriented, risk-taker
|
||||||
"ESFP": ("Prince", "Pentacles"), # Aesthete, sensualist
|
"ESFP": ("Prince", "Pentacles"), # Aesthete, sensualist
|
||||||
|
|
||||||
# PRINCESSES (I + P) - Introverted Perceivers
|
# PRINCESSES (I + P) - Introverted Perceivers
|
||||||
"INTP": ("Princess", "Wands"), # Thinker par excellence
|
"INTP": ("Princess", "Wands"), # Thinker par excellence
|
||||||
"INFP": ("Princess", "Cups"), # Idealistic, devoted
|
"INFP": ("Princess", "Cups"), # Idealistic, devoted
|
||||||
"ISTP": ("Princess", "Swords"), # Observer, mechanic
|
"ISTP": ("Princess", "Swords"), # Observer, mechanic
|
||||||
"ISFP": ("Princess", "Pentacles"), # Aesthete, free spirit
|
"ISFP": ("Princess", "Pentacles"), # Aesthete, free spirit
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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.
|
Create a Personality from an MBTI type string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
mbti_type: MBTI type as string (e.g., "ENFP", "ISTJ")
|
mbti_type: MBTI type as string (e.g., "ENFP", "ISTJ")
|
||||||
deck: Optional Tarot Deck to fetch the court card from. If not provided,
|
deck: Optional Tarot Deck to fetch the court card from. If not provided,
|
||||||
court card will be fetched dynamically when accessed.
|
court card will be fetched dynamically when accessed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Personality object with associated court card
|
Personality object with associated court card
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If mbti_type is not a valid MBTI type
|
ValueError: If mbti_type is not a valid MBTI type
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> from tarot import Tarot
|
>>> from tarot import Tarot
|
||||||
>>> personality = Personality.from_mbti("ENFP", Tarot.deck)
|
>>> personality = Personality.from_mbti("ENFP", Tarot.deck)
|
||||||
@@ -118,7 +116,7 @@ class Personality:
|
|||||||
Prince of Cups
|
Prince of Cups
|
||||||
"""
|
"""
|
||||||
mbti_type = mbti_type.upper()
|
mbti_type = mbti_type.upper()
|
||||||
|
|
||||||
# Validate MBTI type
|
# Validate MBTI type
|
||||||
try:
|
try:
|
||||||
mbti_enum = MBTIType[mbti_type]
|
mbti_enum = MBTIType[mbti_type]
|
||||||
@@ -127,63 +125,58 @@ class Personality:
|
|||||||
f"Invalid MBTI type: {mbti_type}. Must be one of: "
|
f"Invalid MBTI type: {mbti_type}. Must be one of: "
|
||||||
f"{', '.join([t.value for t in MBTIType])}"
|
f"{', '.join([t.value for t in MBTIType])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the rank and suit for this MBTI type
|
# Get the rank and suit for this MBTI type
|
||||||
rank, suit = cls._MBTI_TO_CARD_MAPPING.get(mbti_type, (None, None))
|
rank, suit = cls._MBTI_TO_CARD_MAPPING.get(mbti_type, (None, None))
|
||||||
if not rank or not suit:
|
if not rank or not suit:
|
||||||
raise ValueError(f"No court card mapping found for MBTI type {mbti_type}")
|
raise ValueError(f"No court card mapping found for MBTI type {mbti_type}")
|
||||||
|
|
||||||
# Get court card from deck if provided
|
# Get court card from deck if provided
|
||||||
court_card = None
|
court_card = None
|
||||||
if deck is not None:
|
if deck is not None:
|
||||||
# Import here to avoid circular imports
|
# Import here to avoid circular imports
|
||||||
from tarot import Tarot
|
from tarot import Tarot
|
||||||
|
|
||||||
# Use provided deck or default to 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)
|
cards = d.card.filter(type="Court", court_rank=rank, suit=suit)
|
||||||
if cards:
|
if cards:
|
||||||
court_card = cards[0]
|
court_card = cards[0]
|
||||||
|
|
||||||
# Get description
|
# Get description
|
||||||
descriptions = {
|
descriptions = {
|
||||||
"ENTJ": "The Commander - Strategic, ambitious, leader of Wands",
|
"ENTJ": "The Commander - Strategic, ambitious, leader of Wands",
|
||||||
"ENFJ": "The Protagonist - Inspiring, empathetic, leader of Cups",
|
"ENFJ": "The Protagonist - Inspiring, empathetic, leader of Cups",
|
||||||
"ESTJ": "The Supervisor - Practical, decisive, leader of Swords",
|
"ESTJ": "The Supervisor - Practical, decisive, leader of Swords",
|
||||||
"ESFJ": "The Consul - Sociable, cooperative, leader of Pentacles",
|
"ESFJ": "The Consul - Sociable, cooperative, leader of Pentacles",
|
||||||
|
|
||||||
"INTJ": "The Architect - Strategic, logical, sage of Wands",
|
"INTJ": "The Architect - Strategic, logical, sage of Wands",
|
||||||
"INFJ": "The Advocate - Insightful, idealistic, sage of Cups",
|
"INFJ": "The Advocate - Insightful, idealistic, sage of Cups",
|
||||||
"ISTJ": "The Logistician - Practical, reliable, sage of Swords",
|
"ISTJ": "The Logistician - Practical, reliable, sage of Swords",
|
||||||
"ISFJ": "The Defender - Caring, conscientious, sage of Pentacles",
|
"ISFJ": "The Defender - Caring, conscientious, sage of Pentacles",
|
||||||
|
|
||||||
"ENTP": "The Debater - Innovative, quick-witted, explorer of Wands",
|
"ENTP": "The Debater - Innovative, quick-witted, explorer of Wands",
|
||||||
"ENFP": "The Campaigner - Enthusiastic, social, explorer of Cups",
|
"ENFP": "The Campaigner - Enthusiastic, social, explorer of Cups",
|
||||||
"ESTP": "The Entrepreneur - Energetic, bold, explorer of Swords",
|
"ESTP": "The Entrepreneur - Energetic, bold, explorer of Swords",
|
||||||
"ESFP": "The Entertainer - Spontaneous, outgoing, explorer of Pentacles",
|
"ESFP": "The Entertainer - Spontaneous, outgoing, explorer of Pentacles",
|
||||||
|
|
||||||
"INTP": "The Logician - Analytical, curious, seeker of Wands",
|
"INTP": "The Logician - Analytical, curious, seeker of Wands",
|
||||||
"INFP": "The Mediator - Idealistic, authentic, seeker of Cups",
|
"INFP": "The Mediator - Idealistic, authentic, seeker of Cups",
|
||||||
"ISTP": "The Virtuoso - Practical, observant, seeker of Swords",
|
"ISTP": "The Virtuoso - Practical, observant, seeker of Swords",
|
||||||
"ISFP": "The Adventurer - Sensitive, spontaneous, seeker of Pentacles",
|
"ISFP": "The Adventurer - Sensitive, spontaneous, seeker of Pentacles",
|
||||||
}
|
}
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
mbti_type=mbti_enum,
|
mbti_type=mbti_enum, court_card=court_card, description=descriptions.get(mbti_type, "")
|
||||||
court_card=court_card,
|
|
||||||
description=descriptions.get(mbti_type, "")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
"""Return string representation of personality with court card."""
|
"""Return string representation of personality with court card."""
|
||||||
if self.court_card:
|
if self.court_card:
|
||||||
card_str = f"{self.court_card.court_rank} of {self.court_card.suit.name}"
|
card_str = f"{self.court_card.court_rank} of {self.court_card.suit.name}"
|
||||||
else:
|
else:
|
||||||
card_str = "No court card loaded"
|
card_str = "No court card loaded"
|
||||||
|
|
||||||
return f"{self.mbti_type.value} - {self.description}\n Court Card: {card_str}"
|
return f"{self.mbti_type.value} - {self.description}\n Court Card: {card_str}"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Return detailed representation."""
|
"""Return detailed representation."""
|
||||||
card_name = (
|
card_name = (
|
||||||
|
|||||||
@@ -16,27 +16,26 @@ Usage:
|
|||||||
|
|
||||||
from typing import Any, List, Tuple
|
from typing import Any, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
# Type checking predicates
|
# Type checking predicates
|
||||||
SCALAR_TYPES = (str, int, float, bool, list, dict, type(None))
|
SCALAR_TYPES = (str, int, float, bool, list, dict, type(None))
|
||||||
|
|
||||||
|
|
||||||
def is_dataclass(obj: Any) -> bool:
|
def is_dataclass(obj: Any) -> bool:
|
||||||
"""Check if object is a dataclass."""
|
"""Check if object is a dataclass."""
|
||||||
return hasattr(obj, '__dataclass_fields__')
|
return hasattr(obj, "__dataclass_fields__")
|
||||||
|
|
||||||
|
|
||||||
def is_nested_object(obj: Any) -> bool:
|
def is_nested_object(obj: Any) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if object is a nested/complex object (not a scalar type).
|
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.
|
Returns True for dataclasses, dicts, and objects with __dict__ that aren't scalars.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return True
|
return True
|
||||||
if is_dataclass(obj):
|
if is_dataclass(obj):
|
||||||
return True
|
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:
|
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:
|
def get_item_label(item: Any, fallback: str = "item") -> str:
|
||||||
"""
|
"""
|
||||||
Extract a display label for an item using priority order.
|
Extract a display label for an item using priority order.
|
||||||
|
|
||||||
Priority:
|
Priority:
|
||||||
1. item.name (most common in Tarot data)
|
1. item.name (most common in Tarot data)
|
||||||
2. item.transliteration (used in letters/numbers)
|
2. item.transliteration (used in letters/numbers)
|
||||||
3. str(item) (fallback)
|
3. str(item) (fallback)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
item: The object to get a label for
|
item: The object to get a label for
|
||||||
fallback: Value to use if no attributes found (rarely used)
|
fallback: Value to use if no attributes found (rarely used)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A string suitable for display as an item label
|
A string suitable for display as an item label
|
||||||
"""
|
"""
|
||||||
if hasattr(item, 'name'):
|
if hasattr(item, "name"):
|
||||||
return str(getattr(item, 'name', fallback))
|
return str(getattr(item, "name", fallback))
|
||||||
elif hasattr(item, 'transliteration'):
|
elif hasattr(item, "transliteration"):
|
||||||
return str(getattr(item, 'transliteration', fallback))
|
return str(getattr(item, "transliteration", fallback))
|
||||||
return str(item) if item is not None else fallback
|
return str(item) if item is not None else fallback
|
||||||
|
|
||||||
|
|
||||||
def get_dataclass_fields(obj: Any) -> List[str]:
|
def get_dataclass_fields(obj: Any) -> List[str]:
|
||||||
"""Get list of field names from a dataclass."""
|
"""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 list(obj.__dataclass_fields__.keys())
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -77,64 +76,69 @@ def get_dataclass_fields(obj: Any) -> List[str]:
|
|||||||
def get_object_attributes(obj: Any) -> List[Tuple[str, Any]]:
|
def get_object_attributes(obj: Any) -> List[Tuple[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Extract all public attributes from an object.
|
Extract all public attributes from an object.
|
||||||
|
|
||||||
Returns list of (name, value) tuples, skipping private attributes (starting with '_').
|
Returns list of (name, value) tuples, skipping private attributes (starting with '_').
|
||||||
Works with dataclasses, dicts, and regular objects with __dict__.
|
Works with dataclasses, dicts, and regular objects with __dict__.
|
||||||
"""
|
"""
|
||||||
attributes = []
|
attributes = []
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return list(obj.items())
|
return list(obj.items())
|
||||||
|
|
||||||
if is_dataclass(obj):
|
if is_dataclass(obj):
|
||||||
for field_name in obj.__dataclass_fields__:
|
for field_name in obj.__dataclass_fields__:
|
||||||
value = getattr(obj, field_name, None)
|
value = getattr(obj, field_name, None)
|
||||||
attributes.append((field_name, value))
|
attributes.append((field_name, value))
|
||||||
elif hasattr(obj, '__dict__'):
|
elif hasattr(obj, "__dict__"):
|
||||||
for field_name, value in obj.__dict__.items():
|
for field_name, value in obj.__dict__.items():
|
||||||
if not field_name.startswith('_'):
|
if not field_name.startswith("_"):
|
||||||
attributes.append((field_name, value))
|
attributes.append((field_name, value))
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
def format_value(value: Any, indent: int = 2) -> str:
|
def format_value(value: Any, indent: int = 2) -> str:
|
||||||
"""
|
"""
|
||||||
Format a value for display, recursively handling nested objects.
|
Format a value for display, recursively handling nested objects.
|
||||||
|
|
||||||
Handles:
|
Handles:
|
||||||
- Nested dataclasses and objects with custom __str__ methods
|
- Nested dataclasses and objects with custom __str__ methods
|
||||||
- Lists and dicts
|
- Lists and dicts
|
||||||
- Scalar values
|
- Scalar values
|
||||||
- Proper indentation for nested structures
|
- Proper indentation for nested structures
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
value: The value to format
|
value: The value to format
|
||||||
indent: Number of spaces for indentation (increases for nested objects)
|
indent: Number of spaces for indentation (increases for nested objects)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string representation of the value
|
Formatted string representation of the value
|
||||||
"""
|
"""
|
||||||
indent_str = " " * indent
|
indent_str = " " * indent
|
||||||
|
|
||||||
# Check if object has a custom __str__ method (not the default object repr)
|
# Check if object has a custom __str__ method (not the default object repr)
|
||||||
if is_nested_object(value):
|
if is_nested_object(value):
|
||||||
# Classes that have custom __str__ implementations should use them
|
# Classes that have custom __str__ implementations should use them
|
||||||
obj_class = type(value).__name__
|
obj_class = type(value).__name__
|
||||||
has_custom_str = (
|
has_custom_str = hasattr(value, "__str__") and type(value).__str__ is not object.__str__
|
||||||
hasattr(value, '__str__') and
|
|
||||||
type(value).__str__ is not object.__str__
|
if has_custom_str and obj_class in [
|
||||||
)
|
"Path",
|
||||||
|
"Planet",
|
||||||
if has_custom_str and obj_class in ['Path', 'Planet', 'Perfume', 'God', 'Colorscale', 'Sephera', 'ElementType']:
|
"Perfume",
|
||||||
|
"God",
|
||||||
|
"Colorscale",
|
||||||
|
"Sephera",
|
||||||
|
"ElementType",
|
||||||
|
]:
|
||||||
# Use the custom __str__ method and indent each line
|
# Use the custom __str__ method and indent each line
|
||||||
custom_output = str(value)
|
custom_output = str(value)
|
||||||
lines = []
|
lines = []
|
||||||
for line in custom_output.split('\n'):
|
for line in custom_output.split("\n"):
|
||||||
if line.strip(): # Skip empty lines
|
if line.strip(): # Skip empty lines
|
||||||
lines.append(f"{indent_str}{line}")
|
lines.append(f"{indent_str}{line}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
# Default behavior: iterate through attributes
|
# Default behavior: iterate through attributes
|
||||||
lines = []
|
lines = []
|
||||||
for attr_name, attr_value in get_object_attributes(value):
|
for attr_name, attr_value in get_object_attributes(value):
|
||||||
@@ -146,7 +150,7 @@ def format_value(value: Any, indent: int = 2) -> str:
|
|||||||
else:
|
else:
|
||||||
lines.append(f"{indent_str}{attr_name}: {attr_value}")
|
lines.append(f"{indent_str}{attr_name}: {attr_value}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
# Scalar values
|
# Scalar values
|
||||||
return str(value)
|
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]:
|
def format_object_attributes(obj: Any, indent: int = 2) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Format all attributes of an object as a list of formatted lines.
|
Format all attributes of an object as a list of formatted lines.
|
||||||
|
|
||||||
Handles nested objects with proper indentation and section headers.
|
Handles nested objects with proper indentation and section headers.
|
||||||
Used by display methods to format individual items consistently.
|
Used by display methods to format individual items consistently.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj: The object to format
|
obj: The object to format
|
||||||
indent: Base indentation level in spaces
|
indent: Base indentation level in spaces
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of formatted lines ready to join with newlines
|
List of formatted lines ready to join with newlines
|
||||||
"""
|
"""
|
||||||
lines = []
|
lines = []
|
||||||
indent_str = " " * indent
|
indent_str = " " * indent
|
||||||
|
|
||||||
for attr_name, attr_value in get_object_attributes(obj):
|
for attr_name, attr_value in get_object_attributes(obj):
|
||||||
if is_nested_object(attr_value):
|
if is_nested_object(attr_value):
|
||||||
# Nested object - add section header and format recursively
|
# Nested object - add section header and format recursively
|
||||||
@@ -178,5 +182,5 @@ def format_object_attributes(obj: Any, indent: int = 2) -> List[str]:
|
|||||||
else:
|
else:
|
||||||
# Scalar value - just print
|
# Scalar value - just print
|
||||||
lines.append(f"{indent_str}{attr_name}: {attr_value}")
|
lines.append(f"{indent_str}{attr_name}: {attr_value}")
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -8,42 +8,43 @@ Usage:
|
|||||||
# By name
|
# By name
|
||||||
result = letter.iching().name('peace')
|
result = letter.iching().name('peace')
|
||||||
result = letter.alphabet().name('english')
|
result = letter.alphabet().name('english')
|
||||||
|
|
||||||
# By filter expressions
|
# By filter expressions
|
||||||
result = letter.iching().filter('number:1')
|
result = letter.iching().filter('number:1')
|
||||||
result = letter.alphabet().filter('name:hebrew')
|
result = letter.alphabet().filter('name:hebrew')
|
||||||
result = number.number().filter('value:5')
|
result = number.number().filter('value:5')
|
||||||
|
|
||||||
# Get all results
|
# Get all results
|
||||||
results = letter.iching().all() # Dict[int, Hexagram]
|
results = letter.iching().all() # Dict[int, Hexagram]
|
||||||
results = letter.iching().list() # List[Hexagram]
|
results = letter.iching().list() # List[Hexagram]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
|
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
|
||||||
|
|
||||||
from utils.object_formatting import format_value, get_object_attributes, is_nested_object
|
from utils.object_formatting import format_value, get_object_attributes, is_nested_object
|
||||||
|
|
||||||
T = TypeVar('T')
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class QueryResult:
|
class QueryResult:
|
||||||
"""Single result from a query."""
|
"""Single result from a query."""
|
||||||
|
|
||||||
def __init__(self, data: Any) -> None:
|
def __init__(self, data: Any) -> None:
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if hasattr(self.data, '__repr__'):
|
if hasattr(self.data, "__repr__"):
|
||||||
return repr(self.data)
|
return repr(self.data)
|
||||||
return f"{self.__class__.__name__}({self.data})"
|
return f"{self.__class__.__name__}({self.data})"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
if hasattr(self.data, '__str__'):
|
if hasattr(self.data, "__str__"):
|
||||||
return str(self.data)
|
return str(self.data)
|
||||||
return repr(self)
|
return repr(self)
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
"""Pass through attribute access to the wrapped data."""
|
"""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}'")
|
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||||
return getattr(self.data, name)
|
return getattr(self.data, name)
|
||||||
|
|
||||||
@@ -51,40 +52,40 @@ class QueryResult:
|
|||||||
class Query:
|
class Query:
|
||||||
"""
|
"""
|
||||||
Fluent query builder for accessing and filtering tarot data.
|
Fluent query builder for accessing and filtering tarot data.
|
||||||
|
|
||||||
Supports chaining: .filter() → .name() → .get()
|
Supports chaining: .filter() → .name() → .get()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data: Union[Dict[Any, T], List[T]]) -> None:
|
def __init__(self, data: Union[Dict[Any, T], List[T]]) -> None:
|
||||||
"""Initialize with data source (dict or list)."""
|
"""Initialize with data source (dict or list)."""
|
||||||
self._original_data = data
|
self._original_data = data
|
||||||
self._data = data if isinstance(data, list) else list(data.values())
|
self._data = data if isinstance(data, list) else list(data.values())
|
||||||
self._filters: List[Callable[[T], bool]] = []
|
self._filters: List[Callable[[T], bool]] = []
|
||||||
|
|
||||||
def filter(self, expression: str) -> 'Query':
|
def filter(self, expression: str) -> "Query":
|
||||||
"""
|
"""
|
||||||
Filter by key:value expression.
|
Filter by key:value expression.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
.filter('name:peace')
|
.filter('name:peace')
|
||||||
.filter('number:1')
|
.filter('number:1')
|
||||||
.filter('sephera:gevurah')
|
.filter('sephera:gevurah')
|
||||||
.filter('value:5')
|
.filter('value:5')
|
||||||
|
|
||||||
Supports multiple filters by chaining:
|
Supports multiple filters by chaining:
|
||||||
.filter('number:1').filter('name:creative')
|
.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:
|
def filter_func(item: T) -> bool:
|
||||||
# Special handling for 'name' key
|
# Special handling for 'name' key
|
||||||
if key == 'name':
|
if key == "name":
|
||||||
if hasattr(item, 'name'):
|
if hasattr(item, "name"):
|
||||||
value_lower = value.lower()
|
value_lower = value.lower()
|
||||||
item_name = str(item.name).lower()
|
item_name = str(item.name).lower()
|
||||||
return value_lower == item_name or value_lower in item_name
|
return value_lower == item_name or value_lower in item_name
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not hasattr(item, key):
|
if not hasattr(item, key):
|
||||||
return False
|
return False
|
||||||
item_value = getattr(item, key)
|
item_value = getattr(item, key)
|
||||||
@@ -93,34 +94,34 @@ class Query:
|
|||||||
return value.lower() in str(item_value).lower()
|
return value.lower() in str(item_value).lower()
|
||||||
else:
|
else:
|
||||||
return str(value) in str(item_value)
|
return str(value) in str(item_value)
|
||||||
|
|
||||||
self._filters.append(filter_func)
|
self._filters.append(filter_func)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def name(self, value: str) -> Optional['QueryResult']:
|
def name(self, value: str) -> Optional["QueryResult"]:
|
||||||
"""
|
"""
|
||||||
Deprecated: Use .filter('name:value') instead.
|
Deprecated: Use .filter('name:value') instead.
|
||||||
|
|
||||||
Find item by name (exact or partial match, case-insensitive).
|
Find item by name (exact or partial match, case-insensitive).
|
||||||
Returns QueryResult wrapping the found item, or None if not found.
|
Returns QueryResult wrapping the found item, or None if not found.
|
||||||
"""
|
"""
|
||||||
return self.filter(f'name:{value}').first()
|
return self.filter(f"name:{value}").first()
|
||||||
|
|
||||||
def get(self) -> Optional['QueryResult']:
|
def get(self) -> Optional["QueryResult"]:
|
||||||
"""
|
"""
|
||||||
Get first result matching all applied filters.
|
Get first result matching all applied filters.
|
||||||
|
|
||||||
Returns QueryResult or None if no match.
|
Returns QueryResult or None if no match.
|
||||||
"""
|
"""
|
||||||
for item in self._data:
|
for item in self._data:
|
||||||
if all(f(item) for f in self._filters):
|
if all(f(item) for f in self._filters):
|
||||||
return QueryResult(item)
|
return QueryResult(item)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def all(self) -> Dict[Any, T]:
|
def all(self) -> Dict[Any, T]:
|
||||||
"""
|
"""
|
||||||
Get all results matching filters as dict.
|
Get all results matching filters as dict.
|
||||||
|
|
||||||
Returns original dict structure (if input was dict) with filtered values.
|
Returns original dict structure (if input was dict) with filtered values.
|
||||||
"""
|
"""
|
||||||
filtered = {}
|
filtered = {}
|
||||||
@@ -133,26 +134,26 @@ class Query:
|
|||||||
if all(f(item) for f in self._filters):
|
if all(f(item) for f in self._filters):
|
||||||
filtered[i] = item
|
filtered[i] = item
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
def list(self) -> List[T]:
|
def list(self) -> List[T]:
|
||||||
"""
|
"""
|
||||||
Get all results matching filters as list.
|
Get all results matching filters as list.
|
||||||
|
|
||||||
Returns list of filtered items.
|
Returns list of filtered items.
|
||||||
"""
|
"""
|
||||||
return [item for item in self._data if all(f(item) for f in self._filters)]
|
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."""
|
"""Alias for get() - returns first matching item."""
|
||||||
return self.get()
|
return self.get()
|
||||||
|
|
||||||
def count(self) -> int:
|
def count(self) -> int:
|
||||||
"""Count items matching all filters."""
|
"""Count items matching all filters."""
|
||||||
return len(self.list())
|
return len(self.list())
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"Query({self.count()} items)"
|
return f"Query({self.count()} items)"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
items = self.list()
|
items = self.list()
|
||||||
if not items:
|
if not items:
|
||||||
@@ -201,20 +202,18 @@ class CollectionAccessor(Generic[T]):
|
|||||||
def display(self) -> str:
|
def display(self) -> str:
|
||||||
"""
|
"""
|
||||||
Format all entries for user-friendly display with proper indentation.
|
Format all entries for user-friendly display with proper indentation.
|
||||||
|
|
||||||
Returns a formatted string with each item separated by blank lines.
|
Returns a formatted string with each item separated by blank lines.
|
||||||
Nested objects are indented and separated with their own sections.
|
Nested objects are indented and separated with their own sections.
|
||||||
"""
|
"""
|
||||||
from utils.object_formatting import is_nested_object, get_object_attributes
|
|
||||||
|
|
||||||
data = self.all()
|
data = self.all()
|
||||||
if not data:
|
if not data:
|
||||||
return "(empty collection)"
|
return "(empty collection)"
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for key, item in data.items():
|
for key, item in data.items():
|
||||||
lines.append(f"--- {key} ---")
|
lines.append(f"--- {key} ---")
|
||||||
|
|
||||||
# Format all attributes with proper nesting
|
# Format all attributes with proper nesting
|
||||||
for attr_name, attr_value in get_object_attributes(item):
|
for attr_name, attr_value in get_object_attributes(item):
|
||||||
if is_nested_object(attr_value):
|
if is_nested_object(attr_value):
|
||||||
@@ -224,9 +223,9 @@ class CollectionAccessor(Generic[T]):
|
|||||||
lines.append(nested)
|
lines.append(nested)
|
||||||
else:
|
else:
|
||||||
lines.append(f" {attr_name}: {attr_value}")
|
lines.append(f" {attr_name}: {attr_value}")
|
||||||
|
|
||||||
lines.append("") # Blank line between items
|
lines.append("") # Blank line between items
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
@@ -240,34 +239,32 @@ class CollectionAccessor(Generic[T]):
|
|||||||
|
|
||||||
class FilterableDict(dict):
|
class FilterableDict(dict):
|
||||||
"""Dict subclass that provides .filter() method for dynamic querying."""
|
"""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.
|
Filter dict values by attribute:value expression.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
data.filter('name:peace')
|
data.filter('name:peace')
|
||||||
data.filter('number:1')
|
data.filter('number:1')
|
||||||
data.filter('') # Returns query of all items
|
data.filter('') # Returns query of all items
|
||||||
"""
|
"""
|
||||||
return Query(self).filter(expression) if expression else Query(self)
|
return Query(self).filter(expression) if expression else Query(self)
|
||||||
|
|
||||||
def display(self) -> str:
|
def display(self) -> str:
|
||||||
"""
|
"""
|
||||||
Format all items in the dict for user-friendly display.
|
Format all items in the dict for user-friendly display.
|
||||||
|
|
||||||
Returns a formatted string with each item separated by blank lines.
|
Returns a formatted string with each item separated by blank lines.
|
||||||
Nested objects are indented and separated with their own sections.
|
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:
|
if not self:
|
||||||
return "(empty collection)"
|
return "(empty collection)"
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for key, item in self.items():
|
for key, item in self.items():
|
||||||
lines.append(f"--- {key} ---")
|
lines.append(f"--- {key} ---")
|
||||||
|
|
||||||
# Format all attributes with proper nesting
|
# Format all attributes with proper nesting
|
||||||
for attr_name, attr_value in get_object_attributes(item):
|
for attr_name, attr_value in get_object_attributes(item):
|
||||||
if is_nested_object(attr_value):
|
if is_nested_object(attr_value):
|
||||||
@@ -277,16 +274,16 @@ class FilterableDict(dict):
|
|||||||
lines.append(nested)
|
lines.append(nested)
|
||||||
else:
|
else:
|
||||||
lines.append(f" {attr_name}: {attr_value}")
|
lines.append(f" {attr_name}: {attr_value}")
|
||||||
|
|
||||||
lines.append("") # Blank line between items
|
lines.append("") # Blank line between items
|
||||||
|
|
||||||
return "\n".join(lines)
|
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.
|
Convert dict or list to a filterable object with .filter() support.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
walls = make_filterable(Cube.wall())
|
walls = make_filterable(Cube.wall())
|
||||||
peace = walls.filter('name:North').first()
|
peace = walls.filter('name:North').first()
|
||||||
@@ -297,4 +294,4 @@ def make_filterable(data: Union[Dict[Any, T], List[T]]) -> Union['FilterableDict
|
|||||||
return filterable
|
return filterable
|
||||||
else:
|
else:
|
||||||
# For lists, wrap in a Query
|
# 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
|
from datetime import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.tarot.attributes import (
|
from src.tarot.attributes import (
|
||||||
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter, Sephera, Degree, Element,
|
AstrologicalInfluence,
|
||||||
AstrologicalInfluence, TreeOfLife, Correspondences, CardImage,
|
CardImage,
|
||||||
EnglishAlphabet, GreekAlphabet, HebrewAlphabet, Number, Color, Planet, God,
|
Cipher,
|
||||||
Cipher, CipherResult,
|
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
|
from src.tarot.card.data import CardDataLoader, calculate_digital_root
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Basic Attribute Tests
|
# Basic Attribute Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestMonth:
|
class TestMonth:
|
||||||
def test_month_creation(self):
|
def test_month_creation(self):
|
||||||
month = Month(1, "January", "Capricorn", "Aquarius")
|
month = Month(1, "January", "Capricorn", "Aquarius")
|
||||||
@@ -24,10 +46,7 @@ class TestMonth:
|
|||||||
assert month.zodiac_start == "Capricorn"
|
assert month.zodiac_start == "Capricorn"
|
||||||
|
|
||||||
def test_month_all_months(self):
|
def test_month_all_months(self):
|
||||||
months = [
|
months = [Month(i, f"Month_{i}", "Sign_1", "Sign_2") for i in range(1, 13)]
|
||||||
Month(i, f"Month_{i}", "Sign_1", "Sign_2")
|
|
||||||
for i in range(1, 13)
|
|
||||||
]
|
|
||||||
assert len(months) == 12
|
assert len(months) == 12
|
||||||
assert months[0].number == 1
|
assert months[0].number == 1
|
||||||
assert months[11].number == 12
|
assert months[11].number == 12
|
||||||
@@ -41,10 +60,7 @@ class TestDay:
|
|||||||
assert day.planetary_correspondence == "Sun"
|
assert day.planetary_correspondence == "Sun"
|
||||||
|
|
||||||
def test_all_weekdays(self):
|
def test_all_weekdays(self):
|
||||||
days = [
|
days = [Day(i, f"Day_{i}", f"Planet_{i}") for i in range(1, 8)]
|
||||||
Day(i, f"Day_{i}", f"Planet_{i}")
|
|
||||||
for i in range(1, 8)
|
|
||||||
]
|
|
||||||
assert len(days) == 7
|
assert len(days) == 7
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +115,7 @@ class TestMeaning:
|
|||||||
# Sepheric Tests
|
# Sepheric Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestSephera:
|
class TestSephera:
|
||||||
def test_sephera_creation(self):
|
def test_sephera_creation(self):
|
||||||
sephera = Sephera(1, "Kether", "כתר", "Crown", "Metatron", "Chaioth", "Primum")
|
sephera = Sephera(1, "Kether", "כתר", "Crown", "Metatron", "Chaioth", "Primum")
|
||||||
@@ -118,6 +135,7 @@ class TestSephera:
|
|||||||
# Alphabet Tests
|
# Alphabet Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestEnglishAlphabet:
|
class TestEnglishAlphabet:
|
||||||
def test_english_letter_creation(self):
|
def test_english_letter_creation(self):
|
||||||
letter = EnglishAlphabet("A", 1, "ay")
|
letter = EnglishAlphabet("A", 1, "ay")
|
||||||
@@ -189,6 +207,7 @@ class TestHebrewAlphabet:
|
|||||||
# Number Tests
|
# Number Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestNumber:
|
class TestNumber:
|
||||||
def test_number_creation(self):
|
def test_number_creation(self):
|
||||||
num = Number(1, "Kether", "Spirit", 0) # compliment is auto-calculated
|
num = Number(1, "Kether", "Spirit", 0) # compliment is auto-calculated
|
||||||
@@ -220,6 +239,7 @@ class TestNumber:
|
|||||||
# Color Tests
|
# Color Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestColor:
|
class TestColor:
|
||||||
def test_color_creation(self):
|
def test_color_creation(self):
|
||||||
color = Color("Red", "#FF0000", (255, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power")
|
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 r in [0, 128, 255]:
|
||||||
for g in [0, 128, 255]:
|
for g in [0, 128, 255]:
|
||||||
for b 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)
|
assert color.rgb == (r, g, b)
|
||||||
|
|
||||||
|
|
||||||
@@ -260,7 +282,9 @@ class TestColor:
|
|||||||
class TestPlanet:
|
class TestPlanet:
|
||||||
def test_planet_creation(self):
|
def test_planet_creation(self):
|
||||||
number = Number(6, "Tiphareth", "Fire", 0)
|
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(
|
planet = Planet(
|
||||||
name="Sun",
|
name="Sun",
|
||||||
symbol="☉",
|
symbol="☉",
|
||||||
@@ -342,6 +366,7 @@ class TestGod:
|
|||||||
# Cipher Tests
|
# Cipher Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestCipher:
|
class TestCipher:
|
||||||
def test_cipher_mapping_basic(self):
|
def test_cipher_mapping_basic(self):
|
||||||
cipher = Cipher("Test", "test", [1, 2, 3])
|
cipher = Cipher("Test", "test", [1, 2, 3])
|
||||||
@@ -374,6 +399,7 @@ class TestCipherResult:
|
|||||||
# Digital Root Tests
|
# Digital Root Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestDigitalRoot:
|
class TestDigitalRoot:
|
||||||
def test_digital_root_single_digit(self):
|
def test_digital_root_single_digit(self):
|
||||||
"""Single digits should return themselves."""
|
"""Single digits should return themselves."""
|
||||||
@@ -389,7 +415,7 @@ class TestDigitalRoot:
|
|||||||
|
|
||||||
def test_digital_root_large_numbers(self):
|
def test_digital_root_large_numbers(self):
|
||||||
"""Test large numbers."""
|
"""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(100) == 1 # 1+0+0 = 1
|
||||||
assert calculate_digital_root(123) == 6 # 1+2+3 = 6
|
assert calculate_digital_root(123) == 6 # 1+2+3 = 6
|
||||||
|
|
||||||
@@ -398,13 +424,13 @@ class TestDigitalRoot:
|
|||||||
# Major Arcana cards 0-21
|
# Major Arcana cards 0-21
|
||||||
assert calculate_digital_root(14) == 5 # Card 14 (Temperance) -> 5
|
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(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):
|
def test_digital_root_invalid_input(self):
|
||||||
"""Test that invalid inputs raise errors."""
|
"""Test that invalid inputs raise errors."""
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
calculate_digital_root(0)
|
calculate_digital_root(0)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
calculate_digital_root(-5)
|
calculate_digital_root(-5)
|
||||||
|
|
||||||
@@ -413,6 +439,7 @@ class TestDigitalRoot:
|
|||||||
# CardDataLoader Tests
|
# CardDataLoader Tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
class TestCardDataLoader:
|
class TestCardDataLoader:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def loader(self):
|
def loader(self):
|
||||||
@@ -443,33 +470,35 @@ class TestCardDataLoader:
|
|||||||
sephera = loader.sephera(i)
|
sephera = loader.sephera(i)
|
||||||
assert sephera is not None
|
assert sephera is not None
|
||||||
assert sephera.number == i
|
assert sephera.number == i
|
||||||
|
|
||||||
def test_load_ciphers(self, loader):
|
def test_load_ciphers(self, loader):
|
||||||
"""Ensure cipher catalog is populated."""
|
"""Ensure cipher catalog is populated."""
|
||||||
ciphers = loader.cipher()
|
ciphers = loader.cipher()
|
||||||
assert "english_simple" in ciphers
|
assert "english_simple" in ciphers
|
||||||
assert ciphers["english_simple"].default_alphabet == "english"
|
assert ciphers["english_simple"].default_alphabet == "english"
|
||||||
|
|
||||||
def test_word_cipher_request(self, loader):
|
def test_word_cipher_request(self, loader):
|
||||||
"""word().cipher() should return meaningful totals."""
|
"""word().cipher() should return meaningful totals."""
|
||||||
result = loader.word("tarot").cipher("english_simple")
|
result = loader.word("tarot").cipher("english_simple")
|
||||||
assert isinstance(result, CipherResult)
|
assert isinstance(result, CipherResult)
|
||||||
assert result.total == 74
|
assert result.total == 74
|
||||||
assert result.alphabet_name == "english"
|
assert result.alphabet_name == "english"
|
||||||
|
|
||||||
def test_word_cipher_custom_alphabet(self, loader):
|
def test_word_cipher_custom_alphabet(self, loader):
|
||||||
result = loader.word("אמש").cipher("kabbalah_three_mother")
|
result = loader.word("אמש").cipher("kabbalah_three_mother")
|
||||||
assert result.values == (1, 40, 300)
|
assert result.values == (1, 40, 300)
|
||||||
|
|
||||||
def test_trigram_line_diagram(self, loader):
|
def test_trigram_line_diagram(self, loader):
|
||||||
from letter import trigram
|
from letter import trigram
|
||||||
|
|
||||||
tri = trigram.trigram.name("Zhen") # Thunder
|
tri = trigram.trigram.name("Zhen") # Thunder
|
||||||
assert tri is not None
|
assert tri is not None
|
||||||
assert tri.data.line_diagram == "|::"
|
assert tri.data.line_diagram == "|::"
|
||||||
|
|
||||||
def test_hexagram_line_diagram(self, loader):
|
def test_hexagram_line_diagram(self, loader):
|
||||||
from letter import hexagram
|
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 is not None
|
||||||
assert hex_result.data.line_diagram == "||||||"
|
assert hex_result.data.line_diagram == "||||||"
|
||||||
|
|
||||||
@@ -575,7 +604,7 @@ class TestCardDataLoader:
|
|||||||
assert "english" in alphabets
|
assert "english" in alphabets
|
||||||
assert "greek" in alphabets
|
assert "greek" in alphabets
|
||||||
assert "hebrew" in alphabets
|
assert "hebrew" in alphabets
|
||||||
|
|
||||||
assert len(alphabets["english"]) == 26
|
assert len(alphabets["english"]) == 26
|
||||||
assert len(alphabets["greek"]) == 24
|
assert len(alphabets["greek"]) == 24
|
||||||
assert len(alphabets["hebrew"]) == 22
|
assert len(alphabets["hebrew"]) == 22
|
||||||
@@ -592,16 +621,16 @@ class TestCardDataLoader:
|
|||||||
|
|
||||||
class TestDigitalRootIntegration:
|
class TestDigitalRootIntegration:
|
||||||
"""Integration tests for digital root with Tarot cards."""
|
"""Integration tests for digital root with Tarot cards."""
|
||||||
|
|
||||||
def test_all_major_arcana_digital_roots(self):
|
def test_all_major_arcana_digital_roots(self):
|
||||||
"""Test digital root for all Major Arcana cards (0-21)."""
|
"""Test digital root for all Major Arcana cards (0-21)."""
|
||||||
loader = CardDataLoader()
|
loader = CardDataLoader()
|
||||||
|
|
||||||
# All Major Arcana cards should map to colors 1-9
|
# All Major Arcana cards should map to colors 1-9
|
||||||
for card_num in range(22):
|
for card_num in range(22):
|
||||||
if card_num == 0:
|
if card_num == 0:
|
||||||
continue # Skip The Fool (0)
|
continue # Skip The Fool (0)
|
||||||
|
|
||||||
color = loader.color_by_number(card_num)
|
color = loader.color_by_number(card_num)
|
||||||
assert color is not None
|
assert color is not None
|
||||||
assert 1 <= color.number <= 9
|
assert 1 <= color.number <= 9
|
||||||
@@ -609,7 +638,7 @@ class TestDigitalRootIntegration:
|
|||||||
def test_color_consistency(self):
|
def test_color_consistency(self):
|
||||||
"""Test that equivalent numbers map to same color."""
|
"""Test that equivalent numbers map to same color."""
|
||||||
loader = CardDataLoader()
|
loader = CardDataLoader()
|
||||||
|
|
||||||
# 5 and 14 should map to same color (both have digital root 5)
|
# 5 and 14 should map to same color (both have digital root 5)
|
||||||
color_5 = loader.color_by_number(5)
|
color_5 = loader.color_by_number(5)
|
||||||
color_14 = loader.color_by_number(14)
|
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
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tarot.deck import Card
|
||||||
|
from tarot.ui import CardDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_card_display_delegation():
|
def test_card_display_delegation():
|
||||||
"""Test that CardDisplay delegates to SpreadDisplay correctly."""
|
"""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
|
# 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()
|
display = CardDisplay()
|
||||||
|
|
||||||
# Create dummy card
|
# Create dummy card
|
||||||
card = MagicMock(spec=Card)
|
card = MagicMock(spec=Card)
|
||||||
card.name = "The Fool"
|
card.name = "The Fool"
|
||||||
card.image_path = "fool.jpg"
|
card.image_path = "fool.jpg"
|
||||||
cards = [card]
|
cards = [card]
|
||||||
|
|
||||||
display.show_cards(cards, title="Test Spread")
|
display.show_cards(cards, title="Test Spread")
|
||||||
|
|
||||||
# Verify SpreadDisplay was instantiated
|
# Verify SpreadDisplay was instantiated
|
||||||
assert MockSpreadDisplay.call_count == 1
|
assert MockSpreadDisplay.call_count == 1
|
||||||
|
|
||||||
# Verify run was called
|
# Verify run was called
|
||||||
MockSpreadDisplay.return_value.run.assert_called_once()
|
MockSpreadDisplay.return_value.run.assert_called_once()
|
||||||
|
|
||||||
# Verify arguments passed to SpreadDisplay
|
# Verify arguments passed to SpreadDisplay
|
||||||
args, _ = MockSpreadDisplay.call_args
|
args, _ = MockSpreadDisplay.call_args
|
||||||
reading = args[0]
|
reading = args[0]
|
||||||
deck_name = args[1]
|
deck_name = args[1]
|
||||||
|
|
||||||
assert deck_name == "default"
|
assert deck_name == "default"
|
||||||
assert reading.spread.name == "Card List"
|
assert reading.spread.name == "Card List"
|
||||||
assert reading.spread.description == "Test Spread"
|
assert reading.spread.description == "Test Spread"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from tarot.ui import CubeDisplay
|
|
||||||
from tarot.tarot_api import Tarot
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_cube_display_init():
|
def test_cube_display_init():
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
@@ -8,38 +10,40 @@ def test_cube_display_init():
|
|||||||
assert display.current_wall_name == "North"
|
assert display.current_wall_name == "North"
|
||||||
assert display.deck_name == "default"
|
assert display.deck_name == "default"
|
||||||
|
|
||||||
|
|
||||||
def test_cube_navigation():
|
def test_cube_navigation():
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
|
|
||||||
# North -> Right -> East
|
# North -> Right -> East
|
||||||
display._navigate("Right")
|
display._navigate("Right")
|
||||||
assert display.current_wall_name == "East"
|
assert display.current_wall_name == "East"
|
||||||
|
|
||||||
# East -> Up -> Above
|
# East -> Up -> Above
|
||||||
display._navigate("Up")
|
display._navigate("Up")
|
||||||
assert display.current_wall_name == "Above"
|
assert display.current_wall_name == "Above"
|
||||||
|
|
||||||
# Above -> Down -> North
|
# Above -> Down -> North
|
||||||
display._navigate("Down")
|
display._navigate("Down")
|
||||||
assert display.current_wall_name == "North"
|
assert display.current_wall_name == "North"
|
||||||
|
|
||||||
# North -> Left -> West
|
# North -> Left -> West
|
||||||
display._navigate("Left")
|
display._navigate("Left")
|
||||||
assert display.current_wall_name == "West"
|
assert display.current_wall_name == "West"
|
||||||
|
|
||||||
|
|
||||||
def test_find_card_for_direction():
|
def test_find_card_for_direction():
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
|
|
||||||
# North Wall, Center Direction -> Aleph -> The Fool
|
# North Wall, Center Direction -> Aleph -> The Fool
|
||||||
wall = cube.wall("North")
|
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.
|
# Wait, let's check what Center of North is.
|
||||||
# Actually, let's just mock a direction
|
# Actually, let's just mock a direction
|
||||||
|
|
||||||
from kaballah.cube.attributes import WallDirection
|
from kaballah.cube.attributes import WallDirection
|
||||||
|
|
||||||
# Aleph -> The Fool
|
# Aleph -> The Fool
|
||||||
d = WallDirection("Center", "Aleph")
|
d = WallDirection("Center", "Aleph")
|
||||||
card = display._find_card_for_direction(d)
|
card = display._find_card_for_direction(d)
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from tarot.ui import CubeDisplay
|
|
||||||
from tarot.tarot_api import Tarot
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_cube_zoom():
|
def test_cube_zoom():
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
assert display.zoom_level == 1.0
|
assert display.zoom_level == 1.0
|
||||||
|
|
||||||
display._zoom(1.1)
|
display._zoom(1.1)
|
||||||
assert display.zoom_level > 1.0
|
assert display.zoom_level > 1.0
|
||||||
|
|
||||||
display._zoom(0.5)
|
display._zoom(0.5)
|
||||||
assert display.zoom_level < 1.0
|
assert display.zoom_level < 1.0
|
||||||
|
|
||||||
|
|
||||||
def test_cube_zoom_limits():
|
def test_cube_zoom_limits():
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
|
|
||||||
# Test upper limit
|
# Test upper limit
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
display._zoom(1.5)
|
display._zoom(1.5)
|
||||||
assert display.zoom_level <= 3.0
|
assert display.zoom_level <= 3.0
|
||||||
|
|
||||||
# Test lower limit
|
# Test lower limit
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
display._zoom(0.5)
|
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
|
import tkinter as tk
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_zoom_limits():
|
def test_zoom_limits():
|
||||||
# Mock Tk root
|
# Mock Tk root
|
||||||
class MockRoot:
|
class MockRoot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
self.images = []
|
self.images = []
|
||||||
def bind(self, key, callback): pass
|
|
||||||
def title(self, _): pass
|
def bind(self, key, callback):
|
||||||
def update_idletasks(self): pass
|
pass
|
||||||
def winfo_reqwidth(self): return 800
|
|
||||||
def winfo_reqheight(self): return 600
|
def title(self, _):
|
||||||
def winfo_screenwidth(self): return 1920
|
pass
|
||||||
def winfo_screenheight(self): return 1080
|
|
||||||
def geometry(self, _): pass
|
def update_idletasks(self):
|
||||||
def mainloop(self): pass
|
pass
|
||||||
def focus_force(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
|
# Mock Frame
|
||||||
class MockFrame:
|
class MockFrame:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.children = []
|
self.children = []
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def place(self, **kwargs): pass
|
def pack(self, **kwargs):
|
||||||
def grid(self, **kwargs): pass
|
pass
|
||||||
def grid_propagate(self, flag): pass
|
|
||||||
def winfo_children(self): return self.children
|
def place(self, **kwargs):
|
||||||
def destroy(self): pass
|
pass
|
||||||
def update_idletasks(self): pass
|
|
||||||
def winfo_reqwidth(self): return 100
|
def grid(self, **kwargs):
|
||||||
def winfo_reqheight(self): return 100
|
pass
|
||||||
def bind(self, event, callback): 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
|
# Mock Canvas
|
||||||
class MockCanvas:
|
class MockCanvas:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def bind(self, event, callback): pass
|
def pack(self, **kwargs):
|
||||||
def create_window(self, coords, **kwargs): return 1
|
pass
|
||||||
def config(self, **kwargs): pass
|
|
||||||
def bbox(self, tag): return (0,0,100,100)
|
def bind(self, event, callback):
|
||||||
def winfo_width(self): return 800
|
pass
|
||||||
def winfo_height(self): return 600
|
|
||||||
def coords(self, item, x, y): pass
|
def create_window(self, coords, **kwargs):
|
||||||
def scan_mark(self, x, y): pass
|
return 1
|
||||||
def scan_dragto(self, x, y, gain=1): pass
|
|
||||||
def canvasx(self, x): return x
|
def config(self, **kwargs):
|
||||||
def canvasy(self, y): return y
|
pass
|
||||||
def xview_moveto(self, fraction): pass
|
|
||||||
def yview_moveto(self, fraction): 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
|
# Monkey patch tk
|
||||||
original_tk = tk.Tk
|
original_tk = tk.Tk
|
||||||
@@ -62,60 +133,68 @@ def test_zoom_limits():
|
|||||||
original_canvas = tk.Canvas
|
original_canvas = tk.Canvas
|
||||||
original_label = tk.ttk.Label
|
original_label = tk.ttk.Label
|
||||||
original_button = tk.ttk.Button
|
original_button = tk.ttk.Button
|
||||||
|
|
||||||
# Mock Label and Button
|
# Mock Label and Button
|
||||||
class MockWidget:
|
class MockWidget:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def place(self, **kwargs): pass
|
def pack(self, **kwargs):
|
||||||
def grid(self, **kwargs): pass
|
pass
|
||||||
def grid_propagate(self, flag): pass
|
|
||||||
|
def place(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def grid(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def grid_propagate(self, flag):
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tk.Tk = MockRoot
|
tk.Tk = MockRoot
|
||||||
tk.ttk.Frame = MockFrame
|
tk.ttk.Frame = MockFrame
|
||||||
tk.Canvas = MockCanvas
|
tk.Canvas = MockCanvas
|
||||||
tk.ttk.Label = MockWidget
|
tk.ttk.Label = MockWidget
|
||||||
tk.ttk.Button = MockWidget
|
tk.ttk.Button = MockWidget
|
||||||
|
|
||||||
# Mock Image to avoid memory issues
|
# 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 = MagicMock()
|
||||||
mock_img.size = (100, 100)
|
mock_img.size = (100, 100)
|
||||||
mock_img.resize.return_value = mock_img
|
mock_img.resize.return_value = mock_img
|
||||||
mock_open.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
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
display.root = MockRoot()
|
display.root = MockRoot()
|
||||||
display.canvas = MockCanvas()
|
display.canvas = MockCanvas()
|
||||||
display.content_frame = MockFrame()
|
display.content_frame = MockFrame()
|
||||||
display.canvas_window = 1 # Mock window ID
|
display.canvas_window = 1 # Mock window ID
|
||||||
|
|
||||||
# Test initial zoom
|
# Test initial zoom
|
||||||
assert display.zoom_level == 1.0
|
assert display.zoom_level == 1.0
|
||||||
|
|
||||||
# Test zoom in
|
# Test zoom in
|
||||||
display._zoom(1.22)
|
display._zoom(1.22)
|
||||||
assert display.zoom_level == 1.22
|
assert display.zoom_level == 1.22
|
||||||
|
|
||||||
# Test max limit (should be 50.0)
|
# Test max limit (should be 50.0)
|
||||||
# Zoom way in
|
# Zoom way in
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
display._zoom(1.22)
|
display._zoom(1.22)
|
||||||
|
|
||||||
assert display.zoom_level == 50.0
|
assert display.zoom_level == 50.0
|
||||||
|
|
||||||
# Test min limit (should be 0.1)
|
# Test min limit (should be 0.1)
|
||||||
# Zoom way out
|
# Zoom way out
|
||||||
for _ in range(200):
|
for _ in range(200):
|
||||||
display._zoom(0.5)
|
display._zoom(0.5)
|
||||||
|
|
||||||
assert display.zoom_level == 0.1
|
assert display.zoom_level == 0.1
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
tk.Tk = original_tk
|
tk.Tk = original_tk
|
||||||
tk.ttk.Frame = original_frame
|
tk.ttk.Frame = original_frame
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ Tests for Tarot deck and card classes.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
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:
|
class TestCard:
|
||||||
@@ -30,7 +31,7 @@ class TestMajorCard:
|
|||||||
name="The Magician",
|
name="The Magician",
|
||||||
meaning=Meaning("Upright", "Reversed"),
|
meaning=Meaning("Upright", "Reversed"),
|
||||||
arcana="Major",
|
arcana="Major",
|
||||||
kabbalistic_number=1
|
kabbalistic_number=1,
|
||||||
)
|
)
|
||||||
assert card.number == 1
|
assert card.number == 1
|
||||||
assert card.arcana == "Major"
|
assert card.arcana == "Major"
|
||||||
@@ -42,7 +43,7 @@ class TestMajorCard:
|
|||||||
name="Test",
|
name="Test",
|
||||||
meaning=Meaning("Up", "Rev"),
|
meaning=Meaning("Up", "Rev"),
|
||||||
arcana="Major",
|
arcana="Major",
|
||||||
kabbalistic_number=-1
|
kabbalistic_number=-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_major_card_invalid_high(self):
|
def test_major_card_invalid_high(self):
|
||||||
@@ -52,16 +53,13 @@ class TestMajorCard:
|
|||||||
name="Test",
|
name="Test",
|
||||||
meaning=Meaning("Up", "Rev"),
|
meaning=Meaning("Up", "Rev"),
|
||||||
arcana="Major",
|
arcana="Major",
|
||||||
kabbalistic_number=22
|
kabbalistic_number=22,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_major_card_valid_range(self):
|
def test_major_card_valid_range(self):
|
||||||
for i in range(22):
|
for i in range(22):
|
||||||
card = MajorCard(
|
card = MajorCard(
|
||||||
number=i,
|
number=i, name=f"Card {i}", meaning=Meaning("Up", "Rev"), arcana="Major"
|
||||||
name=f"Card {i}",
|
|
||||||
meaning=Meaning("Up", "Rev"),
|
|
||||||
arcana="Major"
|
|
||||||
)
|
)
|
||||||
assert card.number == i
|
assert card.number == i
|
||||||
|
|
||||||
@@ -75,7 +73,7 @@ class TestMinorCard:
|
|||||||
meaning=Meaning("Upright", "Reversed"),
|
meaning=Meaning("Upright", "Reversed"),
|
||||||
arcana="Minor",
|
arcana="Minor",
|
||||||
suit=suit,
|
suit=suit,
|
||||||
pip=1
|
pip=1,
|
||||||
)
|
)
|
||||||
assert card.number == 1
|
assert card.number == 1
|
||||||
assert card.suit.name == "Cups"
|
assert card.suit.name == "Cups"
|
||||||
@@ -90,7 +88,7 @@ class TestMinorCard:
|
|||||||
meaning=Meaning("Up", "Rev"),
|
meaning=Meaning("Up", "Rev"),
|
||||||
arcana="Minor",
|
arcana="Minor",
|
||||||
suit=suit,
|
suit=suit,
|
||||||
pip=0
|
pip=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_minor_card_invalid_pip_high(self):
|
def test_minor_card_invalid_pip_high(self):
|
||||||
@@ -102,7 +100,7 @@ class TestMinorCard:
|
|||||||
meaning=Meaning("Up", "Rev"),
|
meaning=Meaning("Up", "Rev"),
|
||||||
arcana="Minor",
|
arcana="Minor",
|
||||||
suit=suit,
|
suit=suit,
|
||||||
pip=15
|
pip=15,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_minor_card_valid_pips(self):
|
def test_minor_card_valid_pips(self):
|
||||||
@@ -114,7 +112,7 @@ class TestMinorCard:
|
|||||||
meaning=Meaning("Up", "Rev"),
|
meaning=Meaning("Up", "Rev"),
|
||||||
arcana="Minor",
|
arcana="Minor",
|
||||||
suit=suit,
|
suit=suit,
|
||||||
pip=i
|
pip=i,
|
||||||
)
|
)
|
||||||
assert card.pip == i
|
assert card.pip == i
|
||||||
|
|
||||||
@@ -137,10 +135,10 @@ class TestDeck:
|
|||||||
def test_deck_shuffle(self):
|
def test_deck_shuffle(self):
|
||||||
deck1 = Deck()
|
deck1 = Deck()
|
||||||
cards_before = [c.name for c in deck1.cards]
|
cards_before = [c.name for c in deck1.cards]
|
||||||
|
|
||||||
deck1.shuffle()
|
deck1.shuffle()
|
||||||
cards_after = [c.name for c in deck1.cards]
|
cards_after = [c.name for c in deck1.cards]
|
||||||
|
|
||||||
# After shuffle, order should change (with high probability)
|
# After shuffle, order should change (with high probability)
|
||||||
# We don't assert they're different since shuffle could randomly give same order
|
# We don't assert they're different since shuffle could randomly give same order
|
||||||
assert len(cards_after) == 78
|
assert len(cards_after) == 78
|
||||||
@@ -148,18 +146,18 @@ class TestDeck:
|
|||||||
def test_deck_draw_single(self):
|
def test_deck_draw_single(self):
|
||||||
deck = Deck()
|
deck = Deck()
|
||||||
initial_count = len(deck.cards)
|
initial_count = len(deck.cards)
|
||||||
|
|
||||||
drawn = deck.draw(1)
|
drawn = deck.draw(1)
|
||||||
|
|
||||||
assert len(drawn) == 1
|
assert len(drawn) == 1
|
||||||
assert len(deck.cards) == initial_count - 1
|
assert len(deck.cards) == initial_count - 1
|
||||||
|
|
||||||
def test_deck_draw_multiple(self):
|
def test_deck_draw_multiple(self):
|
||||||
deck = Deck()
|
deck = Deck()
|
||||||
initial_count = len(deck.cards)
|
initial_count = len(deck.cards)
|
||||||
|
|
||||||
drawn = deck.draw(5)
|
drawn = deck.draw(5)
|
||||||
|
|
||||||
assert len(drawn) == 5
|
assert len(drawn) == 5
|
||||||
assert len(deck.cards) == initial_count - 5
|
assert len(deck.cards) == initial_count - 5
|
||||||
|
|
||||||
@@ -177,28 +175,28 @@ class TestDeck:
|
|||||||
deck = Deck()
|
deck = Deck()
|
||||||
deck.draw(5)
|
deck.draw(5)
|
||||||
assert len(deck.cards) < 78
|
assert len(deck.cards) < 78
|
||||||
|
|
||||||
deck.reset()
|
deck.reset()
|
||||||
assert len(deck.cards) == 78
|
assert len(deck.cards) == 78
|
||||||
|
|
||||||
def test_deck_remaining(self):
|
def test_deck_remaining(self):
|
||||||
deck = Deck()
|
deck = Deck()
|
||||||
assert deck.remaining() == 78
|
assert deck.remaining() == 78
|
||||||
|
|
||||||
deck.draw(1)
|
deck.draw(1)
|
||||||
assert deck.remaining() == 77
|
assert deck.remaining() == 77
|
||||||
|
|
||||||
def test_deck_len(self):
|
def test_deck_len(self):
|
||||||
deck = Deck()
|
deck = Deck()
|
||||||
assert len(deck) == 78
|
assert len(deck) == 78
|
||||||
|
|
||||||
deck.draw(1)
|
deck.draw(1)
|
||||||
assert len(deck) == 77
|
assert len(deck) == 77
|
||||||
|
|
||||||
def test_deck_repr(self):
|
def test_deck_repr(self):
|
||||||
deck = Deck()
|
deck = Deck()
|
||||||
assert "78 cards" in repr(deck)
|
assert "78 cards" in repr(deck)
|
||||||
|
|
||||||
deck.draw(1)
|
deck.draw(1)
|
||||||
assert "77 cards" in repr(deck)
|
assert "77 cards" in repr(deck)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import pytest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from tarot.ui import CardDisplay
|
from tarot.ui import CardDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_card_display_init():
|
def test_card_display_init():
|
||||||
display = CardDisplay("default")
|
display = CardDisplay("default")
|
||||||
assert display.deck_name == "default"
|
assert display.deck_name == "default"
|
||||||
# Check if path resolves correctly relative to src/tarot/ui.py
|
# Check if path resolves correctly relative to src/tarot/ui.py
|
||||||
# src/tarot/ui.py -> src/tarot -> src/tarot/deck/default
|
# src/tarot/ui.py -> src/tarot -> src/tarot/deck/default
|
||||||
expected_suffix = os.path.join("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():
|
def test_card_display_resolve_path():
|
||||||
display = CardDisplay("thoth")
|
display = CardDisplay("thoth")
|
||||||
assert display.deck_name == "thoth"
|
assert display.deck_name == "thoth"
|
||||||
assert str(display.deck_path).endswith("thoth")
|
assert str(display.deck_path).endswith("thoth")
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -1,60 +1,63 @@
|
|||||||
import pytest
|
|
||||||
from tarot.ui import CubeDisplay
|
|
||||||
from tarot.tarot_api import Tarot
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_recursive_binding():
|
def test_recursive_binding():
|
||||||
# Mock Tk root and widgets
|
# Mock Tk root and widgets
|
||||||
class MockWidget:
|
class MockWidget:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.children = []
|
self.children = []
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
|
|
||||||
def bind(self, key, callback):
|
def bind(self, key, callback):
|
||||||
self.bindings[key] = callback
|
self.bindings[key] = callback
|
||||||
|
|
||||||
def winfo_children(self):
|
def winfo_children(self):
|
||||||
return self.children
|
return self.children
|
||||||
|
|
||||||
def add_child(self, child):
|
def add_child(self, child):
|
||||||
self.children.append(child)
|
self.children.append(child)
|
||||||
|
|
||||||
# Monkey patch tk
|
# Monkey patch tk
|
||||||
original_tk = tk.Tk
|
original_tk = tk.Tk
|
||||||
original_frame = tk.ttk.Frame
|
original_frame = tk.ttk.Frame
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# We don't need to mock everything, just enough to test _bind_recursive
|
# We don't need to mock everything, just enough to test _bind_recursive
|
||||||
|
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
# We can instantiate CubeDisplay without showing it
|
# We can instantiate CubeDisplay without showing it
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
|
|
||||||
# Create a mock widget tree
|
# Create a mock widget tree
|
||||||
parent = MockWidget()
|
parent = MockWidget()
|
||||||
child1 = MockWidget()
|
child1 = MockWidget()
|
||||||
child2 = MockWidget()
|
child2 = MockWidget()
|
||||||
grandchild = MockWidget()
|
grandchild = MockWidget()
|
||||||
|
|
||||||
parent.add_child(child1)
|
parent.add_child(child1)
|
||||||
parent.add_child(child2)
|
parent.add_child(child2)
|
||||||
child1.add_child(grandchild)
|
child1.add_child(grandchild)
|
||||||
|
|
||||||
# Run recursive binding
|
# Run recursive binding
|
||||||
display._bind_recursive(parent)
|
display._bind_recursive(parent)
|
||||||
|
|
||||||
# Verify bindings
|
# Verify bindings
|
||||||
assert "<ButtonPress-1>" in parent.bindings
|
assert "<ButtonPress-1>" in parent.bindings
|
||||||
assert "<B1-Motion>" in parent.bindings
|
assert "<B1-Motion>" in parent.bindings
|
||||||
|
|
||||||
assert "<ButtonPress-1>" in child1.bindings
|
assert "<ButtonPress-1>" in child1.bindings
|
||||||
assert "<B1-Motion>" in child1.bindings
|
assert "<B1-Motion>" in child1.bindings
|
||||||
|
|
||||||
assert "<ButtonPress-1>" in child2.bindings
|
assert "<ButtonPress-1>" in child2.bindings
|
||||||
assert "<B1-Motion>" in child2.bindings
|
assert "<B1-Motion>" in child2.bindings
|
||||||
|
|
||||||
assert "<ButtonPress-1>" in grandchild.bindings
|
assert "<ButtonPress-1>" in grandchild.bindings
|
||||||
assert "<B1-Motion>" in grandchild.bindings
|
assert "<B1-Motion>" in grandchild.bindings
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,72 +1,99 @@
|
|||||||
import pytest
|
|
||||||
from tarot.ui import CubeDisplay
|
|
||||||
from tarot.tarot_api import Tarot
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_zoom_key_bindings():
|
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.
|
# but cannot easily simulate key presses in headless environment.
|
||||||
# We check if the bind method was called with correct keys.
|
# We check if the bind method was called with correct keys.
|
||||||
|
|
||||||
# Mock Tk root
|
# Mock Tk root
|
||||||
class MockRoot:
|
class MockRoot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
|
|
||||||
def bind(self, key, callback):
|
def bind(self, key, callback):
|
||||||
self.bindings[key] = callback
|
self.bindings[key] = callback
|
||||||
|
|
||||||
def title(self, _): pass
|
def title(self, _):
|
||||||
def update_idletasks(self): pass
|
pass
|
||||||
def winfo_reqwidth(self): return 800
|
|
||||||
def winfo_reqheight(self): return 600
|
def update_idletasks(self):
|
||||||
def winfo_screenwidth(self): return 1920
|
pass
|
||||||
def winfo_screenheight(self): return 1080
|
|
||||||
def geometry(self, _): pass
|
def winfo_reqwidth(self):
|
||||||
def mainloop(self): pass
|
return 800
|
||||||
def focus_force(self): pass
|
|
||||||
|
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
|
# Mock Frame
|
||||||
class MockFrame:
|
class MockFrame:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.children = []
|
self.children = []
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def winfo_children(self): return self.children
|
def pack(self, **kwargs):
|
||||||
def destroy(self): pass
|
pass
|
||||||
|
|
||||||
|
def winfo_children(self):
|
||||||
|
return self.children
|
||||||
|
|
||||||
|
def destroy(self):
|
||||||
|
pass
|
||||||
|
|
||||||
# Monkey patch tk
|
# Monkey patch tk
|
||||||
original_tk = tk.Tk
|
original_tk = tk.Tk
|
||||||
original_frame = tk.ttk.Frame
|
original_frame = tk.ttk.Frame
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tk.Tk = MockRoot
|
tk.Tk = MockRoot
|
||||||
tk.ttk.Frame = MockFrame
|
tk.ttk.Frame = MockFrame
|
||||||
|
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
|
|
||||||
# We need to call show() to trigger bindings, but avoid mainloop
|
# 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.
|
# 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.
|
# 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
|
# 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.
|
# a display, we'll rely on the fact that we added the bindings in the code.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
tk.Tk = original_tk
|
tk.Tk = original_tk
|
||||||
tk.ttk.Frame = original_frame
|
tk.ttk.Frame = original_frame
|
||||||
|
|
||||||
|
|
||||||
def test_zoom_logic_direct():
|
def test_zoom_logic_direct():
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
display.zoom_level = 1.0
|
display.zoom_level = 1.0
|
||||||
|
|
||||||
# Simulate + key press effect
|
# Simulate + key press effect
|
||||||
display._zoom(1.1)
|
display._zoom(1.1)
|
||||||
assert display.zoom_level > 1.0
|
assert display.zoom_level > 1.0
|
||||||
|
|
||||||
# Simulate - key press effect
|
# Simulate - key press effect
|
||||||
display._zoom(0.9)
|
display._zoom(0.9)
|
||||||
assert display.zoom_level < 1.1
|
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 tkinter as tk
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_canvas_structure():
|
def test_canvas_structure():
|
||||||
# Mock Tk root
|
# Mock Tk root
|
||||||
class MockRoot:
|
class MockRoot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
def bind(self, key, callback): pass
|
|
||||||
def title(self, _): pass
|
def bind(self, key, callback):
|
||||||
def update_idletasks(self): pass
|
pass
|
||||||
def winfo_reqwidth(self): return 800
|
|
||||||
def winfo_reqheight(self): return 600
|
def title(self, _):
|
||||||
def winfo_screenwidth(self): return 1920
|
pass
|
||||||
def winfo_screenheight(self): return 1080
|
|
||||||
def geometry(self, _): pass
|
def update_idletasks(self):
|
||||||
def mainloop(self): pass
|
pass
|
||||||
def focus_force(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
|
# Mock Frame
|
||||||
class MockFrame:
|
class MockFrame:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.children = []
|
self.children = []
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def place(self, **kwargs): pass
|
def pack(self, **kwargs):
|
||||||
def winfo_children(self): return self.children
|
pass
|
||||||
def destroy(self): pass
|
|
||||||
def update_idletasks(self): pass
|
def place(self, **kwargs):
|
||||||
def winfo_reqwidth(self): return 100
|
pass
|
||||||
def winfo_reqheight(self): return 100
|
|
||||||
|
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
|
# Mock Canvas
|
||||||
class MockCanvas:
|
class MockCanvas:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def bind(self, event, callback): pass
|
def pack(self, **kwargs):
|
||||||
def create_window(self, coords, **kwargs): return 1
|
pass
|
||||||
def config(self, **kwargs): pass
|
|
||||||
def bbox(self, tag): return (0,0,100,100)
|
def bind(self, event, callback):
|
||||||
def winfo_width(self): return 800
|
pass
|
||||||
def winfo_height(self): return 600
|
|
||||||
def coords(self, item, x, y): pass
|
def create_window(self, coords, **kwargs):
|
||||||
def scan_mark(self, x, y): pass
|
return 1
|
||||||
def scan_dragto(self, x, y, gain=1): pass
|
|
||||||
|
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
|
# Monkey patch tk
|
||||||
original_tk = tk.Tk
|
original_tk = tk.Tk
|
||||||
original_frame = tk.ttk.Frame
|
original_frame = tk.ttk.Frame
|
||||||
original_canvas = tk.Canvas
|
original_canvas = tk.Canvas
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tk.Tk = MockRoot
|
tk.Tk = MockRoot
|
||||||
tk.ttk.Frame = MockFrame
|
tk.ttk.Frame = MockFrame
|
||||||
tk.Canvas = MockCanvas
|
tk.Canvas = MockCanvas
|
||||||
|
|
||||||
cube = Tarot.cube
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
|
|
||||||
# Trigger show to build UI
|
# Trigger show to build UI
|
||||||
# We can't fully run show() because of mainloop, but we can instantiate parts
|
# We can't fully run show() because of mainloop, but we can instantiate parts
|
||||||
# Actually, show() creates the root.
|
# Actually, show() creates the root.
|
||||||
# Let's just verify the structure by inspecting the code or trusting the manual test.
|
# Let's just verify the structure by inspecting the code or trusting the manual test.
|
||||||
# But we can test the pan methods directly.
|
# But we can test the pan methods directly.
|
||||||
|
|
||||||
display.canvas = MockCanvas()
|
display.canvas = MockCanvas()
|
||||||
|
|
||||||
# Test pan methods
|
# Test pan methods
|
||||||
class MockEvent:
|
class MockEvent:
|
||||||
x = 10
|
x = 10
|
||||||
y = 20
|
y = 20
|
||||||
x_root = 110
|
x_root = 110
|
||||||
y_root = 120
|
y_root = 120
|
||||||
|
|
||||||
display._start_pan(MockEvent())
|
display._start_pan(MockEvent())
|
||||||
display._pan(MockEvent())
|
display._pan(MockEvent())
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
tk.Tk = original_tk
|
tk.Tk = original_tk
|
||||||
tk.ttk.Frame = original_frame
|
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
|
import tkinter as tk
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tarot.tarot_api import Tarot
|
||||||
|
from tarot.ui import CubeDisplay
|
||||||
|
|
||||||
|
|
||||||
def test_wasd_panning():
|
def test_wasd_panning():
|
||||||
# Mock Tk root
|
# Mock Tk root
|
||||||
class MockRoot:
|
class MockRoot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.bindings = {}
|
self.bindings = {}
|
||||||
self.images = []
|
self.images = []
|
||||||
def bind(self, key, callback):
|
|
||||||
|
def bind(self, key, callback):
|
||||||
self.bindings[key] = callback
|
self.bindings[key] = callback
|
||||||
def title(self, _): pass
|
|
||||||
def update_idletasks(self): pass
|
def title(self, _):
|
||||||
def winfo_reqwidth(self): return 800
|
pass
|
||||||
def winfo_reqheight(self): return 600
|
|
||||||
def winfo_screenwidth(self): return 1920
|
def update_idletasks(self):
|
||||||
def winfo_screenheight(self): return 1080
|
pass
|
||||||
def geometry(self, _): pass
|
|
||||||
def mainloop(self): pass
|
def winfo_reqwidth(self):
|
||||||
def focus_force(self): pass
|
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
|
# Mock Frame
|
||||||
class MockFrame:
|
class MockFrame:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.children = []
|
self.children = []
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def place(self, **kwargs): pass
|
def pack(self, **kwargs):
|
||||||
def grid(self, **kwargs): pass
|
pass
|
||||||
def grid_propagate(self, flag): pass
|
|
||||||
def winfo_children(self): return self.children
|
def place(self, **kwargs):
|
||||||
def destroy(self): pass
|
pass
|
||||||
def update_idletasks(self): pass
|
|
||||||
def winfo_reqwidth(self): return 100
|
def grid(self, **kwargs):
|
||||||
def winfo_reqheight(self): return 100
|
pass
|
||||||
def bind(self, event, callback): 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
|
# Mock Canvas
|
||||||
class MockCanvas:
|
class MockCanvas:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.x_scrolls = []
|
self.x_scrolls = []
|
||||||
self.y_scrolls = []
|
self.y_scrolls = []
|
||||||
|
|
||||||
def pack(self, **kwargs): pass
|
def pack(self, **kwargs):
|
||||||
def bind(self, event, callback): pass
|
pass
|
||||||
def create_window(self, coords, **kwargs): return 1
|
|
||||||
def config(self, **kwargs): pass
|
def bind(self, event, callback):
|
||||||
def bbox(self, tag): return (0,0,100,100)
|
pass
|
||||||
def winfo_width(self): return 800
|
|
||||||
def winfo_height(self): return 600
|
def create_window(self, coords, **kwargs):
|
||||||
def coords(self, item, x, y): pass
|
return 1
|
||||||
def scan_mark(self, x, y): pass
|
|
||||||
def scan_dragto(self, x, y, gain=1): pass
|
def config(self, **kwargs):
|
||||||
def canvasx(self, x): return x
|
pass
|
||||||
def canvasy(self, y): return y
|
|
||||||
def xview_moveto(self, fraction): pass
|
def bbox(self, tag):
|
||||||
def yview_moveto(self, fraction): pass
|
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):
|
def xview_scroll(self, number, what):
|
||||||
self.x_scrolls.append((number, what))
|
self.x_scrolls.append((number, what))
|
||||||
|
|
||||||
def yview_scroll(self, number, what):
|
def yview_scroll(self, number, what):
|
||||||
self.y_scrolls.append((number, what))
|
self.y_scrolls.append((number, what))
|
||||||
|
|
||||||
@@ -72,54 +141,62 @@ def test_wasd_panning():
|
|||||||
original_canvas = tk.Canvas
|
original_canvas = tk.Canvas
|
||||||
original_label = tk.ttk.Label
|
original_label = tk.ttk.Label
|
||||||
original_button = tk.ttk.Button
|
original_button = tk.ttk.Button
|
||||||
|
|
||||||
# Mock Label and Button
|
# Mock Label and Button
|
||||||
class MockWidget:
|
class MockWidget:
|
||||||
def __init__(self, master=None, **kwargs):
|
def __init__(self, master=None, **kwargs):
|
||||||
self.master = master
|
self.master = master
|
||||||
def pack(self, **kwargs): pass
|
|
||||||
def place(self, **kwargs): pass
|
def pack(self, **kwargs):
|
||||||
def grid(self, **kwargs): pass
|
pass
|
||||||
def grid_propagate(self, flag): pass
|
|
||||||
|
def place(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def grid(self, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def grid_propagate(self, flag):
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tk.Tk = MockRoot
|
tk.Tk = MockRoot
|
||||||
tk.ttk.Frame = MockFrame
|
tk.ttk.Frame = MockFrame
|
||||||
tk.Canvas = MockCanvas
|
tk.Canvas = MockCanvas
|
||||||
tk.ttk.Label = MockWidget
|
tk.ttk.Label = MockWidget
|
||||||
tk.ttk.Button = MockWidget
|
tk.ttk.Button = MockWidget
|
||||||
|
|
||||||
# Mock Image to avoid memory issues
|
# 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 = MagicMock()
|
||||||
mock_img.size = (100, 100)
|
mock_img.size = (100, 100)
|
||||||
mock_img.resize.return_value = mock_img
|
mock_img.resize.return_value = mock_img
|
||||||
mock_open.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
|
cube = Tarot.cube
|
||||||
display = CubeDisplay(cube)
|
display = CubeDisplay(cube)
|
||||||
display.root = MockRoot()
|
display.root = MockRoot()
|
||||||
display.canvas = MockCanvas()
|
display.canvas = MockCanvas()
|
||||||
display.content_frame = MockFrame()
|
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)
|
# 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
|
# But we can call _pan_key directly to test logic
|
||||||
|
|
||||||
display._pan_key("up")
|
display._pan_key("up")
|
||||||
assert display.canvas.y_scrolls[-1] == (-1, "units")
|
assert display.canvas.y_scrolls[-1] == (-1, "units")
|
||||||
|
|
||||||
display._pan_key("down")
|
display._pan_key("down")
|
||||||
assert display.canvas.y_scrolls[-1] == (1, "units")
|
assert display.canvas.y_scrolls[-1] == (1, "units")
|
||||||
|
|
||||||
display._pan_key("left")
|
display._pan_key("left")
|
||||||
assert display.canvas.x_scrolls[-1] == (-1, "units")
|
assert display.canvas.x_scrolls[-1] == (-1, "units")
|
||||||
|
|
||||||
display._pan_key("right")
|
display._pan_key("right")
|
||||||
assert display.canvas.x_scrolls[-1] == (1, "units")
|
assert display.canvas.x_scrolls[-1] == (1, "units")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
tk.Tk = original_tk
|
tk.Tk = original_tk
|
||||||
tk.ttk.Frame = original_frame
|
tk.ttk.Frame = original_frame
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ References:
|
|||||||
- Weekday planetary rulers
|
- Weekday planetary rulers
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime, date, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
# Planetary symbols for weekdays (Sun=0, Mon=1, ..., Sat=6)
|
# Planetary symbols for weekdays (Sun=0, Mon=1, ..., Sat=6)
|
||||||
|
|||||||
Reference in New Issue
Block a user