This commit is contained in:
2026-02-11 00:02:35 -08:00
parent be37eb9d5b
commit ccadea0576
59 changed files with 4569 additions and 2510 deletions

View File

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

View File

@@ -15,8 +15,8 @@ Usage:
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"]

View File

@@ -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,18 +79,19 @@ 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 = ""
@@ -108,23 +112,23 @@ class Path:
"""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
@@ -148,7 +152,7 @@ class Path:
# 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

View File

@@ -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"]

View File

@@ -17,6 +17,7 @@ class WallDirection:
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
@@ -51,6 +52,7 @@ class Wall:
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)
@@ -82,9 +84,7 @@ class Wall:
# 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)
@@ -177,6 +177,7 @@ class CubeOfSpace:
- 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
@@ -392,9 +393,7 @@ class CubeOfSpace:
"""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":
@@ -416,7 +415,7 @@ 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():

View File

@@ -17,7 +17,10 @@ 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):
@@ -29,7 +32,7 @@ class CubeMeta(type):
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,
@@ -42,8 +45,8 @@ class CubeMeta(type):
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)
@@ -53,7 +56,7 @@ class CubeMeta(type):
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)
@@ -343,6 +343,7 @@ class Cube(metaclass=CubeMeta):
# 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)

View File

@@ -13,12 +13,12 @@ Usage:
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):
@@ -28,8 +28,8 @@ class TreeMeta(type):
"""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,
@@ -49,8 +49,8 @@ class TreeMeta(type):
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)})"
@@ -65,10 +65,10 @@ class Tree(metaclass=TreeMeta):
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:
@@ -77,6 +77,7 @@ class Tree(metaclass=TreeMeta):
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
@@ -84,16 +85,16 @@ class Tree(metaclass=TreeMeta):
@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:
@@ -102,16 +103,14 @@ class Tree(metaclass=TreeMeta):
@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:
@@ -119,7 +118,7 @@ class Tree(metaclass=TreeMeta):
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.
@@ -131,6 +130,7 @@ class Tree(metaclass=TreeMeta):
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)

View File

@@ -18,10 +18,9 @@ Usage:
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"]

View File

@@ -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,6 +27,7 @@ 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
@@ -41,6 +42,7 @@ 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
@@ -53,6 +55,7 @@ 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
@@ -66,17 +69,18 @@ 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:
@@ -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
@@ -117,15 +123,18 @@ class EnochianArchetype:
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
@@ -154,6 +163,7 @@ class EnochianGridPosition:
- 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
@@ -195,12 +205,15 @@ class EnochianTablet:
- 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 = ""
@@ -218,8 +231,7 @@ class EnochianTablet:
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)

View File

@@ -10,13 +10,12 @@ Usage:
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,7 +35,7 @@ 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:
@@ -56,14 +55,78 @@ class _Trigram:
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": "", "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": "",
"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,7 +151,7 @@ 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:
@@ -115,70 +178,710 @@ class _Hexagram:
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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "", "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": "",
{"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": "", "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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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": "",
"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 = {}

View File

@@ -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

View File

@@ -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,7 +55,7 @@ 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.

View File

@@ -17,13 +17,14 @@ 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
@@ -34,7 +35,8 @@ class TarotLetter:
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:
@@ -149,7 +151,7 @@ class LetterAccessor:
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."""
@@ -160,11 +162,15 @@ class LetterAccessor:
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]:
""" """
@@ -265,12 +271,13 @@ class IChing:
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
@@ -286,7 +293,7 @@ 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

View File

@@ -9,7 +9,7 @@ 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
@@ -19,11 +19,12 @@ class _Word:
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.

View File

@@ -14,6 +14,6 @@ Usage:
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"]

View File

@@ -1,8 +1,12 @@
"""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:
""" """
@@ -46,8 +50,8 @@ class Numbers:
""" """
# 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
@@ -57,6 +61,7 @@ class Numbers:
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()
@@ -64,16 +69,14 @@ class Numbers:
@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:
@@ -82,16 +85,16 @@ class Numbers:
@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:
@@ -99,13 +102,13 @@ class Numbers:
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)
@@ -153,6 +156,7 @@ class Numbers:
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)
@@ -194,5 +198,6 @@ class Numbers:
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)

View File

@@ -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

View File

@@ -23,44 +23,76 @@ Usage:
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):
@@ -76,7 +108,8 @@ def display(obj):
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",

View File

@@ -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

View File

@@ -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",

View File

@@ -13,9 +13,13 @@ 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):
@@ -32,7 +36,7 @@ class CardList(list):
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.
@@ -42,12 +46,10 @@ def _format_cards(cards: List['Card']) -> str:
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
@@ -78,17 +80,18 @@ class CardAccessor:
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:
@@ -177,7 +180,9 @@ class CardAccessor:
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")
@@ -189,34 +194,34 @@ class CardAccessor:
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
@@ -227,15 +232,16 @@ class CardAccessor:
# 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("")
@@ -244,45 +250,44 @@ class CardAccessor:
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")
@@ -315,7 +320,7 @@ class CardAccessor:
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()

File diff suppressed because it is too large Load Diff

View File

@@ -64,19 +64,9 @@ class CardDetailsRegistry:
if key == 0: if key == 0:
return "o" return "o"
val = [ val = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
1000, 900, 500, 400, syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
100, 90, 50, 40, roman_num = ""
10, 9, 5, 4,
1
]
syms = [
"M", "CM", "D", "CD",
"C", "XC", "L", "XL",
"X", "IX", "V", "IV",
"I"
]
roman_num = ''
i = 0 i = 0
while key > 0: while key > 0:
for _ in range(key // val[i]): for _ in range(key // val[i]):
@@ -120,7 +110,7 @@ class CardDetailsRegistry:
"o": { "o": {
"explanation": { "explanation": {
"summary": "The Fool represents new beginnings, innocence, and spontaneity. This card signifies a fresh start or embarking on a new journey with optimism and faith.", "summary": "The Fool represents new beginnings, innocence, and spontaneity. This card signifies a fresh start or embarking on a new journey with optimism and faith.",
"waite": "The Fool, Mate, or Unwise Man. Court de Gebelin places it at the head of the whole series as the zero or negative which is presupposed by numeration, and as this is a simpler so also it is a better arrangement. It has been abandoned because in later times the cards have been attributed to the letters of the Hebrew alphabet, and there has been apparently some difficulty about allocating the zero symbol satisfactorily in a sequence of letters all of which signify numbers. In the present reference of the card to the letter Shin, which corresponds to 200, the difficulty or the unreason remains. The truth is that the real arrangement of the cards has never transpired. The Fool carries a wallet; he is looking over his shoulder and does not know that he is on the brink of a precipice; but a dog or other animal--some call it a tiger--is attacking him from behind, and he is hurried to his destruction unawares." "waite": "The Fool, Mate, or Unwise Man. Court de Gebelin places it at the head of the whole series as the zero or negative which is presupposed by numeration, and as this is a simpler so also it is a better arrangement. It has been abandoned because in later times the cards have been attributed to the letters of the Hebrew alphabet, and there has been apparently some difficulty about allocating the zero symbol satisfactorily in a sequence of letters all of which signify numbers. In the present reference of the card to the letter Shin, which corresponds to 200, the difficulty or the unreason remains. The truth is that the real arrangement of the cards has never transpired. The Fool carries a wallet; he is looking over his shoulder and does not know that he is on the brink of a precipice; but a dog or other animal--some call it a tiger--is attacking him from behind, and he is hurried to his destruction unawares.",
}, },
"interpretation": "Beginning of the Great Work, innocence; a fool for love; divine madness. Reason is transcended. Take the leap. Gain or loss through foolish actions.", "interpretation": "Beginning of the Great Work, innocence; a fool for love; divine madness. Reason is transcended. Take the leap. Gain or loss through foolish actions.",
"keywords": ["new beginnings", "innocence", "faith", "spontaneity", "potential"], "keywords": ["new beginnings", "innocence", "faith", "spontaneity", "potential"],
@@ -130,137 +120,261 @@ class CardDetailsRegistry:
"I": { "I": {
"explanation": { "explanation": {
"summary": "The Magician embodies manifestation, resourcefulness, and personal power. This card shows mastery of skills and the ability to turn ideas into reality.", "summary": "The Magician embodies manifestation, resourcefulness, and personal power. This card shows mastery of skills and the ability to turn ideas into reality.",
"waite": "The Magus, Magician, or juggler, the caster of the dice and mountebank, in the world of vulgar trickery. This is the colportage interpretation, and it has the same correspondence with the real symbolical meaning that the use of the Tarot in fortune-telling has with its mystic construction according to the secret science of symbolism. I should add that many independent students of the subject, following their own lights, have produced individual sequences of meaning in respect of the Trumps Major, and their lights are sometimes suggestive, but they are not the true lights." "waite": "The Magus, Magician, or juggler, the caster of the dice and mountebank, in the world of vulgar trickery. This is the colportage interpretation, and it has the same correspondence with the real symbolical meaning that the use of the Tarot in fortune-telling has with its mystic construction according to the secret science of symbolism. I should add that many independent students of the subject, following their own lights, have produced individual sequences of meaning in respect of the Trumps Major, and their lights are sometimes suggestive, but they are not the true lights.",
}, },
"interpretation": "Communication; Conscious Will; the process of continuous creation; ambiguity; deceptionl Things may not be as they appear. Concentration, meditation; mind used to direct the Will. Manipulation; crafty maneuverings.", "interpretation": "Communication; Conscious Will; the process of continuous creation; ambiguity; deceptionl Things may not be as they appear. Concentration, meditation; mind used to direct the Will. Manipulation; crafty maneuverings.",
"keywords": ["manifestation", "resourcefulness", "power", "inspired action", "concentration"], "keywords": [
"reversed_keywords": ["manipulation", "poor planning", "untapped talents", "lack of direction"], "manifestation",
"resourcefulness",
"power",
"inspired action",
"concentration",
],
"reversed_keywords": [
"manipulation",
"poor planning",
"untapped talents",
"lack of direction",
],
"guidance": "Focus your energy and intention on what you want to manifest. You have the tools and talents you need.", "guidance": "Focus your energy and intention on what you want to manifest. You have the tools and talents you need.",
}, },
"II": { "II": {
"explanation": { "explanation": {
"summary": "The High Priestess represents intuition, sacred knowledge, and the subconscious mind. She embodies mystery and inner wisdom.", "summary": "The High Priestess represents intuition, sacred knowledge, and the subconscious mind. She embodies mystery and inner wisdom.",
"waite": "The High Priestess, the Pope Joan, or Female Pontiff; early expositors have sought to term this card the Mother, or Pope's Wife, which is opposed to the symbolism. It is sometimes held to represent the Divine Law and the Gnosis, in which case the Priestess corresponds to the idea of the Shekinah. She is the Secret Tradition and the higher sense of the instituted Mysteries." "waite": "The High Priestess, the Pope Joan, or Female Pontiff; early expositors have sought to term this card the Mother, or Pope's Wife, which is opposed to the symbolism. It is sometimes held to represent the Divine Law and the Gnosis, in which case the Priestess corresponds to the idea of the Shekinah. She is the Secret Tradition and the higher sense of the instituted Mysteries.",
}, },
"interpretation": "Symbol of highest initiation; link between the archetypal and Formative Worlds. An initiatrixl Wooing by enchantment. possibility. The Idea behind the Form. Fluctuationl Time may not be right for a decision concerning mundane matters.", "interpretation": "Symbol of highest initiation; link between the archetypal and Formative Worlds. An initiatrixl Wooing by enchantment. possibility. The Idea behind the Form. Fluctuationl Time may not be right for a decision concerning mundane matters.",
"keywords": ["intuition", "sacred knowledge", "divine feminine", "the subconscious", "mystery"], "keywords": [
"reversed_keywords": ["hidden information", "silence", "disconnection from intuition", "superficiality"], "intuition",
"sacred knowledge",
"divine feminine",
"the subconscious",
"mystery",
],
"reversed_keywords": [
"hidden information",
"silence",
"disconnection from intuition",
"superficiality",
],
"guidance": "Listen to your inner voice. The answers you seek lie within. Trust the wisdom of your intuition.", "guidance": "Listen to your inner voice. The answers you seek lie within. Trust the wisdom of your intuition.",
}, },
"III": { "III": {
"explanation": { "explanation": {
"summary": "The Empress symbolizes abundance, fertility, and nurturing energy. She represents creativity, sensuality, and the power of manifestation through nurturing.", "summary": "The Empress symbolizes abundance, fertility, and nurturing energy. She represents creativity, sensuality, and the power of manifestation through nurturing.",
"waite": "The Empress, who is sometimes represented with full face, while her correspondence, the Emperor, is in profile. As there has been some tendency to ascribe a symbolical significance to this distinction, it seems desirable to say that it carries no inner meaning. The Empress has been connected with the ideas of universal fecundity and in a general sense with activity." "waite": "The Empress, who is sometimes represented with full face, while her correspondence, the Emperor, is in profile. As there has been some tendency to ascribe a symbolical significance to this distinction, it seems desirable to say that it carries no inner meaning. The Empress has been connected with the ideas of universal fecundity and in a general sense with activity.",
}, },
"interpretation": "The Holy Grail. love unites the Will. Love; beauty; friendship; success; passive balance. The feminine point of view. The door is open. Disregard the details and concentrate on the big picture.", "interpretation": "The Holy Grail. love unites the Will. Love; beauty; friendship; success; passive balance. The feminine point of view. The door is open. Disregard the details and concentrate on the big picture.",
"keywords": ["abundance", "fertility", "femininity", "beauty", "nature", "creativity"], "keywords": [
"reversed_keywords": ["dependency", "creative block", "neediness", "underdevelopment"], "abundance",
"fertility",
"femininity",
"beauty",
"nature",
"creativity",
],
"reversed_keywords": [
"dependency",
"creative block",
"neediness",
"underdevelopment",
],
"guidance": "Nurture yourself and others. Allow yourself to enjoy the fruits of your labor and appreciate beauty.", "guidance": "Nurture yourself and others. Allow yourself to enjoy the fruits of your labor and appreciate beauty.",
}, },
"IV": { "IV": {
"explanation": { "explanation": {
"summary": "The Emperor represents authority, leadership, and established power. He embodies structure, discipline, and protection through strength and control.", "summary": "The Emperor represents authority, leadership, and established power. He embodies structure, discipline, and protection through strength and control.",
"waite": "The Emperor, by imputation the spouse of the former. He is occasionally represented as wearing, in addition to his personal insignia, the stars or ribbons of some order of chivalry. I mention this to shew that the cards are a medley of old and new emblems." "waite": "The Emperor, by imputation the spouse of the former. He is occasionally represented as wearing, in addition to his personal insignia, the stars or ribbons of some order of chivalry. I mention this to shew that the cards are a medley of old and new emblems.",
}, },
"interpretation": "Creative wisdom radiating upon the organized man and woman. Domination after conquest; quarrelsomeness; paternal love; ambition. Thought ruled by creative, masculine, fiery energy. Stubbornness; war; authority; energy in its most temporal form. Swift immpermaent action over confidence.", "interpretation": "Creative wisdom radiating upon the organized man and woman. Domination after conquest; quarrelsomeness; paternal love; ambition. Thought ruled by creative, masculine, fiery energy. Stubbornness; war; authority; energy in its most temporal form. Swift immpermaent action over confidence.",
"keywords": ["authority", "leadership", "power", "structure", "protection", "discipline"], "keywords": [
"reversed_keywords": ["weakness", "ineffectual leadership", "lack of discipline", "tyranny"], "authority",
"leadership",
"power",
"structure",
"protection",
"discipline",
],
"reversed_keywords": [
"weakness",
"ineffectual leadership",
"lack of discipline",
"tyranny",
],
"guidance": "Step into your power with confidence. Establish clear boundaries and structure. Lead by example.", "guidance": "Step into your power with confidence. Establish clear boundaries and structure. Lead by example.",
}, },
"V": { "V": {
"explanation": { "explanation": {
"summary": "The Hierophant represents tradition, conventional wisdom, and spiritual authority. This card embodies education, ceremony, and moral values.", "summary": "The Hierophant represents tradition, conventional wisdom, and spiritual authority. This card embodies education, ceremony, and moral values.",
"waite": "The High Priest or Hierophant, called also Spiritual Father, and more commonly and obviously the Pope. It seems even to have been named the Abbot, and then its correspondence, the High Priestess, was the Abbess or Mother of the Convent. Both are arbitrary names. The insignia of the figures are papal, and in such case the High Priestess is and can be only the Church, to whom Pope and priests are married by the spiritual rite of ordination." "waite": "The High Priest or Hierophant, called also Spiritual Father, and more commonly and obviously the Pope. It seems even to have been named the Abbot, and then its correspondence, the High Priestess, was the Abbess or Mother of the Convent. Both are arbitrary names. The insignia of the figures are papal, and in such case the High Priestess is and can be only the Church, to whom Pope and priests are married by the spiritual rite of ordination.",
}, },
"interpretation": "The Holy Guardian Angel. The uniting of t hat which is above with that which is below. Love is indicated, but the nature of that love is not yet to be revealed. Inspiration; teaching; organization; discipline; strength; endurance; toil; help from superiors.", "interpretation": "The Holy Guardian Angel. The uniting of t hat which is above with that which is below. Love is indicated, but the nature of that love is not yet to be revealed. Inspiration; teaching; organization; discipline; strength; endurance; toil; help from superiors.",
"keywords": ["tradition", "spirituality", "wisdom", "ritual", "morality", "ethics"], "keywords": ["tradition", "spirituality", "wisdom", "ritual", "morality", "ethics"],
"reversed_keywords": ["rebellion", "unconventionality", "questioning authority", "dogmatism"], "reversed_keywords": [
"rebellion",
"unconventionality",
"questioning authority",
"dogmatism",
],
"guidance": "Seek guidance from established wisdom. Respect traditions while finding your own spiritual path.", "guidance": "Seek guidance from established wisdom. Respect traditions while finding your own spiritual path.",
}, },
"VI": { "VI": {
"explanation": { "explanation": {
"summary": "The Lovers represents relationships, values alignment, and the union of opposites. It signifies choice, intimacy, and deep connection.", "summary": "The Lovers represents relationships, values alignment, and the union of opposites. It signifies choice, intimacy, and deep connection.",
"waite": "The Lovers or Marriage. This symbol has undergone many variations, as might be expected from its subject. In the eighteenth century form, by which it first became known to the world of archæological research, it is really a card of married life, shewing father and mother, with their child placed between them; and the pagan Cupid above, in the act of flying his shaft, is, of course, a misapplied emblem." "waite": "The Lovers or Marriage. This symbol has undergone many variations, as might be expected from its subject. In the eighteenth century form, by which it first became known to the world of archæological research, it is really a card of married life, shewing father and mother, with their child placed between them; and the pagan Cupid above, in the act of flying his shaft, is, of course, a misapplied emblem.",
}, },
"interpretation": "Intuition. Be open to your own inner voice. A well-intended, arranged marriage. An artificial union. The need to make a choice with awareness of consequences union; analysis followed by synthesis; indecision; instability; superficiality.", "interpretation": "Intuition. Be open to your own inner voice. A well-intended, arranged marriage. An artificial union. The need to make a choice with awareness of consequences union; analysis followed by synthesis; indecision; instability; superficiality.",
"keywords": ["relationships", "love", "union", "values", "choice", "alignment"], "keywords": ["relationships", "love", "union", "values", "choice", "alignment"],
"reversed_keywords": ["disharmony", "misalignment", "conflict", "communication breakdown"], "reversed_keywords": [
"disharmony",
"misalignment",
"conflict",
"communication breakdown",
],
"guidance": "Choose with your heart aligned with your values. Deep connection requires vulnerability and honesty.", "guidance": "Choose with your heart aligned with your values. Deep connection requires vulnerability and honesty.",
}, },
"VII": { "VII": {
"explanation": { "explanation": {
"summary": "The Chariot embodies willpower, determination, and control through focused intention. It represents triumph through discipline and forward momentum.", "summary": "The Chariot embodies willpower, determination, and control through focused intention. It represents triumph through discipline and forward momentum.",
"waite": "The Chariot. This is represented in some extant codices as being drawn by two sphinxes, and the device is in consonance with the symbolism, but it must not be supposed that such was its original form; the variation was invented to support a particular historical hypothesis. In the eighteenth century white horses were yoked to the car. As regards its usual name, the lesser stands for the greater; it is really the King in his triumph." "waite": "The Chariot. This is represented in some extant codices as being drawn by two sphinxes, and the device is in consonance with the symbolism, but it must not be supposed that such was its original form; the variation was invented to support a particular historical hypothesis. In the eighteenth century white horses were yoked to the car. As regards its usual name, the lesser stands for the greater; it is really the King in his triumph.",
}, },
"interpretation": "Light in the darkness. The burden you carry may be the Holy Grail. Faithfulness; hope; obedience; a protective relationship; firm, even violent adherance to dogma or tradition. Glory; riches; englightened civilization; victory; triumph; chain of command.", "interpretation": "Light in the darkness. The burden you carry may be the Holy Grail. Faithfulness; hope; obedience; a protective relationship; firm, even violent adherance to dogma or tradition. Glory; riches; englightened civilization; victory; triumph; chain of command.",
"keywords": ["determination", "willpower", "control", "momentum", "victory", "focus"], "keywords": [
"determination",
"willpower",
"control",
"momentum",
"victory",
"focus",
],
"reversed_keywords": ["lack of control", "haste", "resistance", "moving backward"], "reversed_keywords": ["lack of control", "haste", "resistance", "moving backward"],
"guidance": "Take the reins of your life. Move forward with determination and clear direction. You have the power.", "guidance": "Take the reins of your life. Move forward with determination and clear direction. You have the power.",
}, },
"VIII": { "VIII": {
"explanation": { "explanation": {
"summary": "Strength represents inner power, courage, and compassion. It shows mastery through gentleness and the ability to face challenges with calm confidence.", "summary": "Strength represents inner power, courage, and compassion. It shows mastery through gentleness and the ability to face challenges with calm confidence.",
"waite": "Fortitude. This is one of the cardinal virtues, of which I shall speak later. The female figure is usually represented as closing the mouth of a lion. In the earlier form which is printed by Court de Gebelin, she is obviously opening it. The first alternative is better symbolically, but either is an instance of strength in its conventional understanding, and conveys the idea of mastery." "waite": "Fortitude. This is one of the cardinal virtues, of which I shall speak later. The female figure is usually represented as closing the mouth of a lion. In the earlier form which is printed by Court de Gebelin, she is obviously opening it. The first alternative is better symbolically, but either is an instance of strength in its conventional understanding, and conveys the idea of mastery.",
}, },
"interpretation": "Equilibrium; karmic law; the dance of life; all possibilities. The woman satisfied. Balance; weigh each thought against its opposite. Lawsuits; treaties. Pause and look before you leap.", "interpretation": "Equilibrium; karmic law; the dance of life; all possibilities. The woman satisfied. Balance; weigh each thought against its opposite. Lawsuits; treaties. Pause and look before you leap.",
"keywords": ["strength", "courage", "patience", "compassion", "control", "confidence"], "keywords": [
"reversed_keywords": ["weakness", "self-doubt", "lack of composure", "poor control"], "strength",
"courage",
"patience",
"compassion",
"control",
"confidence",
],
"reversed_keywords": [
"weakness",
"self-doubt",
"lack of composure",
"poor control",
],
"guidance": "True strength comes from within. Face challenges with courage and compassion for yourself and others.", "guidance": "True strength comes from within. Face challenges with courage and compassion for yourself and others.",
}, },
"IX": { "IX": {
"explanation": { "explanation": {
"summary": "The Hermit represents introspection, spiritual seeking, and inner guidance. This card embodies solitude, wisdom gained through reflection, and self-discovery.", "summary": "The Hermit represents introspection, spiritual seeking, and inner guidance. This card embodies solitude, wisdom gained through reflection, and self-discovery.",
"waite": "The Hermit, as he is termed in common parlance, stands next on the list; he is also the Capuchin, and in more philosophical language the Sage. He is said to be in search of that Truth which is located far off in the sequence, and of justice which has preceded him on the way. But this is a card of attainment, as we shall see later, rather than a card of quest." "waite": "The Hermit, as he is termed in common parlance, stands next on the list; he is also the Capuchin, and in more philosophical language the Sage. He is said to be in search of that Truth which is located far off in the sequence, and of justice which has preceded him on the way. But this is a card of attainment, as we shall see later, rather than a card of quest.",
}, },
"interpretation": "Divine seed of all things. By silence comes inspiration and wisdom. Wandering alone; temporary solitude; creative contemplation; a virgin. Retirement from involvement in current events.", "interpretation": "Divine seed of all things. By silence comes inspiration and wisdom. Wandering alone; temporary solitude; creative contemplation; a virgin. Retirement from involvement in current events.",
"keywords": ["introspection", "spiritual seeking", "inner light", "wisdom", "solitude", "truth"], "keywords": [
"reversed_keywords": ["loneliness", "isolation", "lost", "paranoia", "disconnection"], "introspection",
"spiritual seeking",
"inner light",
"wisdom",
"solitude",
"truth",
],
"reversed_keywords": [
"loneliness",
"isolation",
"lost",
"paranoia",
"disconnection",
],
"guidance": "Take time for introspection and self-discovery. Your inner light guides your path. Seek solitude for wisdom.", "guidance": "Take time for introspection and self-discovery. Your inner light guides your path. Seek solitude for wisdom.",
}, },
"X": { "X": {
"explanation": { "explanation": {
"summary": "The Wheel of Fortune represents cycles, destiny, and the turning points of life. It embodies luck, karma, and the natural ebb and flow of existence.", "summary": "The Wheel of Fortune represents cycles, destiny, and the turning points of life. It embodies luck, karma, and the natural ebb and flow of existence.",
"waite": "The Wheel of Fortune. There is a current Manual of Cartomancy which has obtained a considerable vogue in England, and amidst a great scattermeal of curious things to no purpose has intersected a few serious subjects. In its last and largest edition it treats in one section of the Tarot; which--if I interpret the author rightly--it regards from beginning to end as the Wheel of Fortune." "waite": "The Wheel of Fortune. There is a current Manual of Cartomancy which has obtained a considerable vogue in England, and amidst a great scattermeal of curious things to no purpose has intersected a few serious subjects. In its last and largest edition it treats in one section of the Tarot; which--if I interpret the author rightly--it regards from beginning to end as the Wheel of Fortune.",
}, },
"interpretation": "Continual change. In the midst of revolving phenomena, reaach joyously the motionless center. Carefree love; wanton pleasure; amusement; fun; change of fortune, usually good.", "interpretation": "Continual change. In the midst of revolving phenomena, reaach joyously the motionless center. Carefree love; wanton pleasure; amusement; fun; change of fortune, usually good.",
"keywords": ["fate", "destiny", "cycles", "fortune", "karma", "turning point"], "keywords": ["fate", "destiny", "cycles", "fortune", "karma", "turning point"],
"reversed_keywords": ["bad luck", "resistance to change", "broken cycles", "misfortune"], "reversed_keywords": [
"bad luck",
"resistance to change",
"broken cycles",
"misfortune",
],
"guidance": "Trust in the cycles of life. What goes up must come down. Embrace change as part of your journey.", "guidance": "Trust in the cycles of life. What goes up must come down. Embrace change as part of your journey.",
}, },
"XI": { "XI": {
"explanation": { "explanation": {
"summary": "Justice represents fairness, truth, and balance. It embodies accountability, clear judgment, and the consequences of actions both past and present.", "summary": "Justice represents fairness, truth, and balance. It embodies accountability, clear judgment, and the consequences of actions both past and present.",
"waite": "Justice. That the Tarot, though it is of all reasonable antiquity, is not of time immemorial, is shewn by this card, which could have been presented in a much more archaic manner. Those, however, who have gifts of discernment in matters of this kind will not need to be told that age is in no sense of the essence of the consideration." "waite": "Justice. That the Tarot, though it is of all reasonable antiquity, is not of time immemorial, is shewn by this card, which could have been presented in a much more archaic manner. Those, however, who have gifts of discernment in matters of this kind will not need to be told that age is in no sense of the essence of the consideration.",
}, },
"interpretation": "Understanding; the Will of New Aeon; passion; sense smitten with ecstasy. let love devour all. Energy independent of reason. Strength; courage; utilization of magical power.", "interpretation": "Understanding; the Will of New Aeon; passion; sense smitten with ecstasy. let love devour all. Energy independent of reason. Strength; courage; utilization of magical power.",
"keywords": ["justice", "fairness", "truth", "cause and effect", "balance", "accountability"], "keywords": [
"justice",
"fairness",
"truth",
"cause and effect",
"balance",
"accountability",
],
"reversed_keywords": ["injustice", "bias", "lack of accountability", "dishonesty"], "reversed_keywords": ["injustice", "bias", "lack of accountability", "dishonesty"],
"guidance": "Seek the truth and act with fairness. Take responsibility for your actions. Balance is key.", "guidance": "Seek the truth and act with fairness. Take responsibility for your actions. Balance is key.",
}, },
"XII": { "XII": {
"explanation": { "explanation": {
"summary": "The Hanged Man represents suspension, letting go, and seeing things from a new perspective. It embodies surrender, pause, and gaining wisdom through sacrifice.", "summary": "The Hanged Man represents suspension, letting go, and seeing things from a new perspective. It embodies surrender, pause, and gaining wisdom through sacrifice.",
"waite": "The Hanged Man. This is the symbol which is supposed to represent Prudence, and Éliphas Lévi says, in his most shallow and plausible manner, that it is the adept bound by his engagements. The figure of a man is suspended head-downwards from a gibbet, to which he is attached by a rope about one of his ankles." "waite": "The Hanged Man. This is the symbol which is supposed to represent Prudence, and Éliphas Lévi says, in his most shallow and plausible manner, that it is the adept bound by his engagements. The figure of a man is suspended head-downwards from a gibbet, to which he is attached by a rope about one of his ankles.",
}, },
"interpretation": "Redemption, sacrifice, annihilation in the beloved; martyrdom; loss; torment; suspension; death; suffering.", "interpretation": "Redemption, sacrifice, annihilation in the beloved; martyrdom; loss; torment; suspension; death; suffering.",
"keywords": ["suspension", "restriction", "letting go", "new perspective", "surrender", "pause"], "keywords": [
"reversed_keywords": ["resistance", "stalling", "unwillingness to change", "impatience"], "suspension",
"restriction",
"letting go",
"new perspective",
"surrender",
"pause",
],
"reversed_keywords": [
"resistance",
"stalling",
"unwillingness to change",
"impatience",
],
"guidance": "Pause and reflect. What are you holding onto? Surrender control and trust the process.", "guidance": "Pause and reflect. What are you holding onto? Surrender control and trust the process.",
}, },
"XIII": { "XIII": {
"explanation": { "explanation": {
"summary": "Death represents transformation, endings, and new beginnings. This card embodies major life transitions, the release of the old, and inevitable change.", "summary": "Death represents transformation, endings, and new beginnings. This card embodies major life transitions, the release of the old, and inevitable change.",
"waite": "Death. The method of presentation is almost invariable, and embodies a bourgeois form of symbolism. The scene is the field of life, and amidst ordinary rank vegetation there are living arms and heads protruding from the ground. One of the heads is crowned, and a skeleton with a great scythe is in the act of mowing it." "waite": "Death. The method of presentation is almost invariable, and embodies a bourgeois form of symbolism. The scene is the field of life, and amidst ordinary rank vegetation there are living arms and heads protruding from the ground. One of the heads is crowned, and a skeleton with a great scythe is in the act of mowing it.",
}, },
"interpretation": "End of cycle; transformation; raw sexuality. Sex is death. Stress becomes intolerable. Any change is welcome. Time; age; unexpected change; death.", "interpretation": "End of cycle; transformation; raw sexuality. Sex is death. Stress becomes intolerable. Any change is welcome. Time; age; unexpected change; death.",
"keywords": ["transformation", "transition", "endings", "beginnings", "change", "acceptance"], "keywords": [
"reversed_keywords": ["resistance to change", "stagnation", "missed opportunity", "delay"], "transformation",
"transition",
"endings",
"beginnings",
"change",
"acceptance",
],
"reversed_keywords": [
"resistance to change",
"stagnation",
"missed opportunity",
"delay",
],
"guidance": "Release what no longer serves you. Transformation is inevitable. Trust in the cycle of death and rebirth.", "guidance": "Release what no longer serves you. Transformation is inevitable. Trust in the cycle of death and rebirth.",
}, },
"XIV": { "XIV": {
"explanation": { "explanation": {
"summary": "Temperance represents balance, moderation, and harmony. It embodies blending of opposites, inner peace through balance, and finding your rhythm.", "summary": "Temperance represents balance, moderation, and harmony. It embodies blending of opposites, inner peace through balance, and finding your rhythm.",
"waite": "Temperance. The winged figure of a female--who, in opposition to all doctrine concerning the hierarchy of angels, is usually allocated to this order of ministering spirits--is pouring liquid from one pitcher to another. In his last work on the Tarot, Dr. Papus abandons the traditional form and depicts a woman wearing an Egyptian head-dress." "waite": "Temperance. The winged figure of a female--who, in opposition to all doctrine concerning the hierarchy of angels, is usually allocated to this order of ministering spirits--is pouring liquid from one pitcher to another. In his last work on the Tarot, Dr. Papus abandons the traditional form and depicts a woman wearing an Egyptian head-dress.",
}, },
"interpretation": "Transmutation through union of opposites. A perfect marriage exalts and transforms each partner. The scientific method. Success follows complex maneuvers.", "interpretation": "Transmutation through union of opposites. A perfect marriage exalts and transforms each partner. The scientific method. Success follows complex maneuvers.",
"keywords": ["balance", "moderation", "harmony", "patience", "timing", "peace"], "keywords": ["balance", "moderation", "harmony", "patience", "timing", "peace"],
@@ -270,47 +384,84 @@ class CardDetailsRegistry:
"XV": { "XV": {
"explanation": { "explanation": {
"summary": "The Devil represents bondage, materialism, and shadow aspects of self. It embodies addictions, illusions, and the consequences of giving away personal power.", "summary": "The Devil represents bondage, materialism, and shadow aspects of self. It embodies addictions, illusions, and the consequences of giving away personal power.",
"waite": "The Devil. In the eighteenth century this card seems to have been rather a symbol of merely animal impudicity. Except for a fantastic head-dress, the chief figure is entirely naked; it has bat-like wings, and the hands and feet are represented by the claws of a bird." "waite": "The Devil. In the eighteenth century this card seems to have been rather a symbol of merely animal impudicity. Except for a fantastic head-dress, the chief figure is entirely naked; it has bat-like wings, and the hands and feet are represented by the claws of a bird.",
}, },
"interpretation": "Thou hast no right but to do thy will. Obession; temptation; ecstasy found in every phenomenon; creative action, yet sublimely careless of result; unscrupulous ambition; strength.", "interpretation": "Thou hast no right but to do thy will. Obession; temptation; ecstasy found in every phenomenon; creative action, yet sublimely careless of result; unscrupulous ambition; strength.",
"keywords": ["bondage", "materialism", "playfulness", "shadow self", "sexuality", "excess"], "keywords": [
"bondage",
"materialism",
"playfulness",
"shadow self",
"sexuality",
"excess",
],
"reversed_keywords": ["freedom", "detachment", "reclaiming power", "breaking free"], "reversed_keywords": ["freedom", "detachment", "reclaiming power", "breaking free"],
"guidance": "Examine what binds you. Acknowledge your shadow. You hold the key to your own freedom.", "guidance": "Examine what binds you. Acknowledge your shadow. You hold the key to your own freedom.",
}, },
"XVI": { "XVI": {
"explanation": { "explanation": {
"summary": "The Tower represents sudden disruption, revelation, and breakthrough through crisis. It embodies sudden change, truth revealed, and necessary destruction.", "summary": "The Tower represents sudden disruption, revelation, and breakthrough through crisis. It embodies sudden change, truth revealed, and necessary destruction.",
"waite": "The Tower struck by Lightning. Its alternative titles are: Castle of Plutus, God's House and the Tower of Babel. In the last case, the figures falling therefrom are held to be Nimrod and his minister. It is assuredly a card of confusion, and the design corresponds, broadly speaking, to any of the designations except Maison Dieu." "waite": "The Tower struck by Lightning. Its alternative titles are: Castle of Plutus, God's House and the Tower of Babel. In the last case, the figures falling therefrom are held to be Nimrod and his minister. It is assuredly a card of confusion, and the design corresponds, broadly speaking, to any of the designations except Maison Dieu.",
}, },
"interpretation": "Escape from the prison of organized life; renunciation of love; quarreling. Plans are destroyed. War; danger; sudden death.", "interpretation": "Escape from the prison of organized life; renunciation of love; quarreling. Plans are destroyed. War; danger; sudden death.",
"keywords": ["sudden change", "upheaval", "revelation", "breakdown", "breakthrough", "chaos"], "keywords": [
"reversed_keywords": ["resistance to change", "averted crisis", "delay", "stagnation"], "sudden change",
"upheaval",
"revelation",
"breakdown",
"breakthrough",
"chaos",
],
"reversed_keywords": [
"resistance to change",
"averted crisis",
"delay",
"stagnation",
],
"guidance": "Crisis brings clarity. Though change is sudden and jarring, it clears away the false and brings truth.", "guidance": "Crisis brings clarity. Though change is sudden and jarring, it clears away the false and brings truth.",
}, },
"XVII": { "XVII": {
"explanation": { "explanation": {
"summary": "The Star represents hope, guidance, and inspiration. It embodies clarity of purpose, spiritual insight, and the light that guides your path forward.", "summary": "The Star represents hope, guidance, and inspiration. It embodies clarity of purpose, spiritual insight, and the light that guides your path forward.",
"waite": "The Star, Dog-Star, or Sirius, also called fantastically the Star of the Magi. Grouped about it are seven minor luminaries, and beneath it is a naked female figure, with her left knee upon the earth and her right foot upon the water. She is in the act of pouring fluids from two vessels." "waite": "The Star, Dog-Star, or Sirius, also called fantastically the Star of the Magi. Grouped about it are seven minor luminaries, and beneath it is a naked female figure, with her left knee upon the earth and her right foot upon the water. She is in the act of pouring fluids from two vessels.",
}, },
"interpretation": "Clairvoyance; visions; drams; hope; love; yearning; realization of inexhaustible possibilities; dreaminess; unexpected help; renewal.", "interpretation": "Clairvoyance; visions; drams; hope; love; yearning; realization of inexhaustible possibilities; dreaminess; unexpected help; renewal.",
"keywords": ["hope", "faith", "inspiration", "vision", "guidance", "spirituality"], "keywords": ["hope", "faith", "inspiration", "vision", "guidance", "spirituality"],
"reversed_keywords": ["hopelessness", "despair", "lack of direction", "lost", "obscured"], "reversed_keywords": [
"hopelessness",
"despair",
"lack of direction",
"lost",
"obscured",
],
"guidance": "Let your inner light shine. Trust in your vision. Hope and guidance light your path forward.", "guidance": "Let your inner light shine. Trust in your vision. Hope and guidance light your path forward.",
}, },
"XVIII": { "XVIII": {
"explanation": { "explanation": {
"summary": "The Moon represents illusion, intuition, and the subconscious mind. It embodies mystery, dreams, and navigating by inner knowing rather than sight.", "summary": "The Moon represents illusion, intuition, and the subconscious mind. It embodies mystery, dreams, and navigating by inner knowing rather than sight.",
"waite": "The Moon. Some eighteenth-century cards shew the luminary on its waning side; in the debased edition of Etteilla, it is the moon at night in her plenitude, set in a heaven of stars; of recent years the moon is shewn on the side of her increase. In nearly all presentations she is shining brightly and shedding the moisture of fertilizing dew in great drops." "waite": "The Moon. Some eighteenth-century cards shew the luminary on its waning side; in the debased edition of Etteilla, it is the moon at night in her plenitude, set in a heaven of stars; of recent years the moon is shewn on the side of her increase. In nearly all presentations she is shining brightly and shedding the moisture of fertilizing dew in great drops.",
}, },
"interpretation": "The Dark night of the soul; deception; falsehood; illusion; madness; the threshold of significant change.", "interpretation": "The Dark night of the soul; deception; falsehood; illusion; madness; the threshold of significant change.",
"keywords": ["illusion", "intuition", "uncertainty", "subconscious", "dreams", "mystery"], "keywords": [
"reversed_keywords": ["clarity", "truth revealed", "release from illusion", "awakening"], "illusion",
"intuition",
"uncertainty",
"subconscious",
"dreams",
"mystery",
],
"reversed_keywords": [
"clarity",
"truth revealed",
"release from illusion",
"awakening",
],
"guidance": "Trust your intuition to navigate mystery. What appears illusory contains deeper truths worth exploring.", "guidance": "Trust your intuition to navigate mystery. What appears illusory contains deeper truths worth exploring.",
}, },
"XIX": { "XIX": {
"explanation": { "explanation": {
"summary": "The Sun represents joy, clarity, and vitality. It embodies success, positive energy, and the radiance of authentic self-expression.", "summary": "The Sun represents joy, clarity, and vitality. It embodies success, positive energy, and the radiance of authentic self-expression.",
"waite": "The Sun. The luminary is distinguished in older cards by chief rays that are waved and salient alternately and by secondary salient rays. It appears to shed its influence on earth not only by light and heat, but--like the moon--by drops of dew." "waite": "The Sun. The luminary is distinguished in older cards by chief rays that are waved and salient alternately and by secondary salient rays. It appears to shed its influence on earth not only by light and heat, but--like the moon--by drops of dew.",
}, },
"interpretation": "Lord of the New Aeon. Spiritual emancipation. Pleasure; shamelessness; vanity; frankness. Freedom brings sanity. Glory; riches; enlightened civilization.", "interpretation": "Lord of the New Aeon. Spiritual emancipation. Pleasure; shamelessness; vanity; frankness. Freedom brings sanity. Glory; riches; enlightened civilization.",
"keywords": ["success", "joy", "clarity", "vitality", "warmth", "authenticity"], "keywords": ["success", "joy", "clarity", "vitality", "warmth", "authenticity"],
@@ -320,29 +471,47 @@ class CardDetailsRegistry:
"XX": { "XX": {
"explanation": { "explanation": {
"summary": "Judgement represents awakening, calling, and significant decisions. It embodies reckoning, rebirth, and responding to a higher calling.", "summary": "Judgement represents awakening, calling, and significant decisions. It embodies reckoning, rebirth, and responding to a higher calling.",
"waite": "The Last judgment. I have spoken of this symbol already, the form of which is essentially invariable, even in the Etteilla set. An angel sounds his trumpet per sepulchra regionum, and the dead arise. It matters little that Etteilla omits the angel, or that Dr. Papus substitutes a ridiculous figure." "waite": "The Last judgment. I have spoken of this symbol already, the form of which is essentially invariable, even in the Etteilla set. An angel sounds his trumpet per sepulchra regionum, and the dead arise. It matters little that Etteilla omits the angel, or that Dr. Papus substitutes a ridiculous figure.",
}, },
"interpretation": "Let every act be an act of Worship; let every act be an act of Love. Final decision; judgement. Learn from the past. Prepare for the future.", "interpretation": "Let every act be an act of Worship; let every act be an act of Love. Final decision; judgement. Learn from the past. Prepare for the future.",
"keywords": ["awakening", "calling", "judgment", "rebirth", "evaluation", "absolution"], "keywords": [
"reversed_keywords": ["doubt", "self-doubt", "harsh judgment", "reluctance to change"], "awakening",
"calling",
"judgment",
"rebirth",
"evaluation",
"absolution",
],
"reversed_keywords": [
"doubt",
"self-doubt",
"harsh judgment",
"reluctance to change",
],
"guidance": "Answer your higher calling. Evaluate with compassion. A significant awakening or decision awaits.", "guidance": "Answer your higher calling. Evaluate with compassion. A significant awakening or decision awaits.",
}, },
"XXI": { "XXI": {
"explanation": { "explanation": {
"summary": "The World represents completion, wholeness, and fulfillment. It embodies the end of a cycle, achievement of goals, and a sense of unity.", "summary": "The World represents completion, wholeness, and fulfillment. It embodies the end of a cycle, achievement of goals, and a sense of unity.",
"waite": "The World, the Universe, or Time. The four living creatures of the Apocalypse and Ezekiel's vision, attributed to the evangelists in Christian symbolism, are grouped about an elliptic garland, as if it were a chain of flowers intended to symbolize all sensible things; within this garland there is the figure of a woman, whom the wind has girt about the loins with a light scarf, and this is all her vesture." "waite": "The World, the Universe, or Time. The four living creatures of the Apocalypse and Ezekiel's vision, attributed to the evangelists in Christian symbolism, are grouped about an elliptic garland, as if it were a chain of flowers intended to symbolize all sensible things; within this garland there is the figure of a woman, whom the wind has girt about the loins with a light scarf, and this is all her vesture.",
}, },
"interpretation": "Completion of the Greatk Work; patience; perseverance; stubbornness; serious meditation. Work accomplished.", "interpretation": "Completion of the Greatk Work; patience; perseverance; stubbornness; serious meditation. Work accomplished.",
"keywords": ["completion", "fulfillment", "wholeness", "travel", "unity", "achievement"], "keywords": [
"completion",
"fulfillment",
"wholeness",
"travel",
"unity",
"achievement",
],
"reversed_keywords": ["incomplete", "blocked", "separation", "seeking closure"], "reversed_keywords": ["incomplete", "blocked", "separation", "seeking closure"],
"guidance": "A significant cycle completes. You have achieved wholeness. Yet every ending is a new beginning.", "guidance": "A significant cycle completes. You have achieved wholeness. Yet every ending is a new beginning.",
}, },
# Minor Arcana - Swords # Minor Arcana - Swords
"Ace of Swords": { "Ace of Swords": {
"explanation": { "explanation": {
"summary": "A hand issues from a cloud, grasping as word, the point of which is encircled by a crown.", "summary": "A hand issues from a cloud, grasping as word, the point of which is encircled by a crown.",
"waite": "A hand issues from a cloud, grasping as word, the point of which is encircled by a crown. Divinatory Meanings: Triumph, the excessive degree in everything, conquest, triumph of force. It is a card of great force, in love as well as in hatred. The crown may carry a much higher significance than comes usually within the sphere of fortune-telling. Reversed: The same, but the results are disastrous; another account says--conception, childbirth, augmentation, multiplicity." "waite": "A hand issues from a cloud, grasping as word, the point of which is encircled by a crown. Divinatory Meanings: Triumph, the excessive degree in everything, conquest, triumph of force. It is a card of great force, in love as well as in hatred. The crown may carry a much higher significance than comes usually within the sphere of fortune-telling. Reversed: The same, but the results are disastrous; another account says--conception, childbirth, augmentation, multiplicity.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -352,7 +521,7 @@ class CardDetailsRegistry:
"Two of Swords": { "Two of Swords": {
"explanation": { "explanation": {
"summary": "A hoodwinked female figure balances two swords upon her shoulders.", "summary": "A hoodwinked female figure balances two swords upon her shoulders.",
"waite": "A hoodwinked female figure balances two swords upon her shoulders. Divinatory Meanings: Conformity and the equipoise which it suggests, courage, friendship, concord in a state of arms; another reading gives tenderness, affection, intimacy. The suggestion of harmony and other favourable readings must be considered in a qualified manner, as Swords generally are not symbolical of beneficent forces in human affairs. Reversed: Imposture, falsehood, duplicity, disloyalty." "waite": "A hoodwinked female figure balances two swords upon her shoulders. Divinatory Meanings: Conformity and the equipoise which it suggests, courage, friendship, concord in a state of arms; another reading gives tenderness, affection, intimacy. The suggestion of harmony and other favourable readings must be considered in a qualified manner, as Swords generally are not symbolical of beneficent forces in human affairs. Reversed: Imposture, falsehood, duplicity, disloyalty.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -362,7 +531,7 @@ class CardDetailsRegistry:
"Three of Swords": { "Three of Swords": {
"explanation": { "explanation": {
"summary": "Three swords piercing a heart; cloud and rain behind.", "summary": "Three swords piercing a heart; cloud and rain behind.",
"waite": "Three swords piercing a heart; cloud and rain behind. Divinatory Meanings: Removal, absence, delay, division, rupture, dispersion, and all that the design signifies naturally, being too simple and obvious to call for specific enumeration. Reversed: Mental alienation, error, loss, distraction, disorder, confusion." "waite": "Three swords piercing a heart; cloud and rain behind. Divinatory Meanings: Removal, absence, delay, division, rupture, dispersion, and all that the design signifies naturally, being too simple and obvious to call for specific enumeration. Reversed: Mental alienation, error, loss, distraction, disorder, confusion.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -372,7 +541,7 @@ class CardDetailsRegistry:
"Four of Swords": { "Four of Swords": {
"explanation": { "explanation": {
"summary": "The effigy of a knight in the attitude of prayer, at full length upon his tomb.", "summary": "The effigy of a knight in the attitude of prayer, at full length upon his tomb.",
"waite": "The effigy of a knight in the attitude of prayer, at full length upon his tomb. Divinatory Meanings: Vigilance, retreat, solitude, hermit's repose, exile, tomb and coffin. It is these last that have suggested the design. Reversed: Wise administration, circumspection, economy, avarice, precaution, testament." "waite": "The effigy of a knight in the attitude of prayer, at full length upon his tomb. Divinatory Meanings: Vigilance, retreat, solitude, hermit's repose, exile, tomb and coffin. It is these last that have suggested the design. Reversed: Wise administration, circumspection, economy, avarice, precaution, testament.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -382,7 +551,7 @@ class CardDetailsRegistry:
"Five of Swords": { "Five of Swords": {
"explanation": { "explanation": {
"summary": "A disdainful man looks after two retreating and dejected figures.", "summary": "A disdainful man looks after two retreating and dejected figures.",
"waite": "A disdainful man looks after two retreating and dejected figures. Their swords lie upon the ground. He carries two others on his left shoulder, and a third sword is in his right hand, point to earth. He is the master in possession of the field. Divinatory Meanings: Degradation, destruction, revocation, infamy, dishonour, loss, with the variants and analogues of these. Reversed: The same; burial and obsequies." "waite": "A disdainful man looks after two retreating and dejected figures. Their swords lie upon the ground. He carries two others on his left shoulder, and a third sword is in his right hand, point to earth. He is the master in possession of the field. Divinatory Meanings: Degradation, destruction, revocation, infamy, dishonour, loss, with the variants and analogues of these. Reversed: The same; burial and obsequies.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -392,7 +561,7 @@ class CardDetailsRegistry:
"Six of Swords": { "Six of Swords": {
"explanation": { "explanation": {
"summary": "A ferryman carrying passengers in his punt to the further shore.", "summary": "A ferryman carrying passengers in his punt to the further shore.",
"waite": "A ferryman carrying passengers in his punt to the further shore. The course is smooth, and seeing that the freight is light, it may be noted that the work is not beyond his strength. Divinatory Meanings: journey by water, route, way, envoy, commissionary, expedient. Reversed: Declaration, confession, publicity; one account says that it is a proposal of love." "waite": "A ferryman carrying passengers in his punt to the further shore. The course is smooth, and seeing that the freight is light, it may be noted that the work is not beyond his strength. Divinatory Meanings: journey by water, route, way, envoy, commissionary, expedient. Reversed: Declaration, confession, publicity; one account says that it is a proposal of love.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -402,7 +571,7 @@ class CardDetailsRegistry:
"Seven of Swords": { "Seven of Swords": {
"explanation": { "explanation": {
"summary": "A man in the act of carrying away five swords rapidly; the two others of the card remain stuck in the ground.", "summary": "A man in the act of carrying away five swords rapidly; the two others of the card remain stuck in the ground.",
"waite": "A man in the act of carrying away five swords rapidly; the two others of the card remain stuck in the ground. A camp is close at hand. Divinatory Meanings: Design, attempt, wish, hope, confidence; also quarrelling, a plan that may fail, annoyance. The design is uncertain in its import, because the significations are widely at variance with each other. Reversed: Good advice, counsel, instruction, slander, babbling." "waite": "A man in the act of carrying away five swords rapidly; the two others of the card remain stuck in the ground. A camp is close at hand. Divinatory Meanings: Design, attempt, wish, hope, confidence; also quarrelling, a plan that may fail, annoyance. The design is uncertain in its import, because the significations are widely at variance with each other. Reversed: Good advice, counsel, instruction, slander, babbling.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -412,7 +581,7 @@ class CardDetailsRegistry:
"Eight of Swords": { "Eight of Swords": {
"explanation": { "explanation": {
"summary": "A woman, bound and hoodwinked, with the swords of the card about her.", "summary": "A woman, bound and hoodwinked, with the swords of the card about her.",
"waite": "A woman, bound and hoodwinked, with the swords of the card about her. Yet it is rather a card of temporary durance than of irretrievable bondage. Divinatory Meanings: Bad news, violent chagrin, crisis, censure, power in trammels, conflict, calumny; also sickness. Reversed: Disquiet, difficulty, opposition, accident, treachery; what is unforeseen; fatality." "waite": "A woman, bound and hoodwinked, with the swords of the card about her. Yet it is rather a card of temporary durance than of irretrievable bondage. Divinatory Meanings: Bad news, violent chagrin, crisis, censure, power in trammels, conflict, calumny; also sickness. Reversed: Disquiet, difficulty, opposition, accident, treachery; what is unforeseen; fatality.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -422,7 +591,7 @@ class CardDetailsRegistry:
"Nine of Swords": { "Nine of Swords": {
"explanation": { "explanation": {
"summary": "One seated on her couch in lamentation, with the swords over her.", "summary": "One seated on her couch in lamentation, with the swords over her.",
"waite": "One seated on her couch in lamentation, with the swords over her. She is as one who knows no sorrow which is like unto hers. It is a card of utter desolation. Divinatory Meanings: Death, failure, miscarriage, delay, deception, disappointment, despair. Reversed: Imprisonment, suspicion, doubt, reasonable fear, shame." "waite": "One seated on her couch in lamentation, with the swords over her. She is as one who knows no sorrow which is like unto hers. It is a card of utter desolation. Divinatory Meanings: Death, failure, miscarriage, delay, deception, disappointment, despair. Reversed: Imprisonment, suspicion, doubt, reasonable fear, shame.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -432,7 +601,7 @@ class CardDetailsRegistry:
"Ten of Swords": { "Ten of Swords": {
"explanation": { "explanation": {
"summary": "A prostrate figure, pierced by all the swords belonging to the card.", "summary": "A prostrate figure, pierced by all the swords belonging to the card.",
"waite": "A prostrate figure, pierced by all the swords belonging to the card. Divinatory Meanings: Whatsoever is intimated by the design; also pain, affliction, tears, sadness, desolation. It is not especially a card of violent death. Reversed: Advantage, profit, success, favour, but none of these are permanent; also power and authority." "waite": "A prostrate figure, pierced by all the swords belonging to the card. Divinatory Meanings: Whatsoever is intimated by the design; also pain, affliction, tears, sadness, desolation. It is not especially a card of violent death. Reversed: Advantage, profit, success, favour, but none of these are permanent; also power and authority.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -442,7 +611,7 @@ class CardDetailsRegistry:
"Page of Swords": { "Page of Swords": {
"explanation": { "explanation": {
"summary": "A lithe, active figure holds a sword upright in both hands, while in the act of swift walking.", "summary": "A lithe, active figure holds a sword upright in both hands, while in the act of swift walking.",
"waite": "A lithe, active figure holds a sword upright in both hands, while in the act of swift walking. He is passing over rugged land, and about his way the clouds are collocated wildly. He is alert and lithe, looking this way and that, as if an expected enemy might appear at any moment. Divinatory Meanings: Authority, overseeing, secret service, vigilance, spying, examination, and the qualities thereto belonging. Reversed: More evil side of these qualities; what is unforeseen, unprepared state; sickness is also intimated." "waite": "A lithe, active figure holds a sword upright in both hands, while in the act of swift walking. He is passing over rugged land, and about his way the clouds are collocated wildly. He is alert and lithe, looking this way and that, as if an expected enemy might appear at any moment. Divinatory Meanings: Authority, overseeing, secret service, vigilance, spying, examination, and the qualities thereto belonging. Reversed: More evil side of these qualities; what is unforeseen, unprepared state; sickness is also intimated.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -452,7 +621,7 @@ class CardDetailsRegistry:
"Knight of Swords": { "Knight of Swords": {
"explanation": { "explanation": {
"summary": "He is riding in full course, as if scattering his enemies.", "summary": "He is riding in full course, as if scattering his enemies.",
"waite": "He is riding in full course, as if scattering his enemies. In the design he is really a prototypical hero of romantic chivalry. He might almost be Galahad, whose sword is swift and sure because he is clean of heart. Divinatory Meanings: Skill, bravery, capacity, defence, address, enmity, wrath, war, destruction, opposition, resistance, ruin. There is therefore a sense in which the card signifies death, but it carries this meaning only in its proximity to other cards of fatality. Reversed: Imprudence, incapacity, extravagance." "waite": "He is riding in full course, as if scattering his enemies. In the design he is really a prototypical hero of romantic chivalry. He might almost be Galahad, whose sword is swift and sure because he is clean of heart. Divinatory Meanings: Skill, bravery, capacity, defence, address, enmity, wrath, war, destruction, opposition, resistance, ruin. There is therefore a sense in which the card signifies death, but it carries this meaning only in its proximity to other cards of fatality. Reversed: Imprudence, incapacity, extravagance.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -462,7 +631,7 @@ class CardDetailsRegistry:
"Queen of Swords": { "Queen of Swords": {
"explanation": { "explanation": {
"summary": "Her right hand raises the weapon vertically and the hilt rests on an arm of her royal chair the left hand is extended, the arm raised her countenance is severe but chastened; it suggests familiarity with sorrow.", "summary": "Her right hand raises the weapon vertically and the hilt rests on an arm of her royal chair the left hand is extended, the arm raised her countenance is severe but chastened; it suggests familiarity with sorrow.",
"waite": "Her right hand raises the weapon vertically and the hilt rests on an arm of her royal chair the left hand is extended, the arm raised her countenance is severe but chastened; it suggests familiarity with sorrow. It does not represent mercy, and, her sword notwithstanding, she is scarcely a symbol of power. Divinatory Meanings: Widowhood, female sadness and embarrassment, absence, sterility, mourning, privation, separation. Reversed: Malice, bigotry, artifice, prudery, bale, deceit." "waite": "Her right hand raises the weapon vertically and the hilt rests on an arm of her royal chair the left hand is extended, the arm raised her countenance is severe but chastened; it suggests familiarity with sorrow. It does not represent mercy, and, her sword notwithstanding, she is scarcely a symbol of power. Divinatory Meanings: Widowhood, female sadness and embarrassment, absence, sterility, mourning, privation, separation. Reversed: Malice, bigotry, artifice, prudery, bale, deceit.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -472,7 +641,7 @@ class CardDetailsRegistry:
"King of Swords": { "King of Swords": {
"explanation": { "explanation": {
"summary": "Whatsoever arises out of the idea of judgment and all its connexions-power, command, authority, militant intelligence, law, offices of the crown, and so forth.", "summary": "Whatsoever arises out of the idea of judgment and all its connexions-power, command, authority, militant intelligence, law, offices of the crown, and so forth.",
"waite": "Whatsoever arises out of the idea of judgment and all its connexions-power, command, authority, militant intelligence, law, offices of the crown, and so forth." "waite": "Whatsoever arises out of the idea of judgment and all its connexions-power, command, authority, militant intelligence, law, offices of the crown, and so forth.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -483,7 +652,7 @@ class CardDetailsRegistry:
"Ace of Cups": { "Ace of Cups": {
"explanation": { "explanation": {
"summary": "The waters are beneath, and thereon are water-lilies; the hand issues from the cloud, holding in its palm the cup, from which four streams are pouring; a dove, bearing in its bill a cross-marked Host, descends to place the Wafer in the Cup; the dew of water is falling on all sides.", "summary": "The waters are beneath, and thereon are water-lilies; the hand issues from the cloud, holding in its palm the cup, from which four streams are pouring; a dove, bearing in its bill a cross-marked Host, descends to place the Wafer in the Cup; the dew of water is falling on all sides.",
"waite": "The waters are beneath, and thereon are water-lilies; the hand issues from the cloud, holding in its palm the cup, from which four streams are pouring; a dove, bearing in its bill a cross-marked Host, descends to place the Wafer in the Cup; the dew of water is falling on all sides. It is an intimation of that which may lie behind the Lesser Arcana. Divinatory Meanings: House of the true heart, joy, content, abode, nourishment, abundance, fertility; Holy Table, felicity hereof. Reversed: House of the false heart, mutation, instability, revolution." "waite": "The waters are beneath, and thereon are water-lilies; the hand issues from the cloud, holding in its palm the cup, from which four streams are pouring; a dove, bearing in its bill a cross-marked Host, descends to place the Wafer in the Cup; the dew of water is falling on all sides. It is an intimation of that which may lie behind the Lesser Arcana. Divinatory Meanings: House of the true heart, joy, content, abode, nourishment, abundance, fertility; Holy Table, felicity hereof. Reversed: House of the false heart, mutation, instability, revolution.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -493,7 +662,7 @@ class CardDetailsRegistry:
"Two of Cups": { "Two of Cups": {
"explanation": { "explanation": {
"summary": "A youth and maiden are pledging one another, and above their cups rises the Caduceus of Hermes, between the great wings of which there appears a lion's head.", "summary": "A youth and maiden are pledging one another, and above their cups rises the Caduceus of Hermes, between the great wings of which there appears a lion's head.",
"waite": "A youth and maiden are pledging one another, and above their cups rises the Caduceus of Hermes, between the great wings of which there appears a lion's head. It is a variant of a sign which is found in a few old examples of this card. Some curious emblematical meanings are attached to it, but they do not concern us in this place. Divinatory Meanings: Love, passion, friendship, affinity, union, concord, sympathy, the interrelation of the sexes, and--as a suggestion apart from all offices of divination--that desire which is not in Nature, but by which Nature is sanctified." "waite": "A youth and maiden are pledging one another, and above their cups rises the Caduceus of Hermes, between the great wings of which there appears a lion's head. It is a variant of a sign which is found in a few old examples of this card. Some curious emblematical meanings are attached to it, but they do not concern us in this place. Divinatory Meanings: Love, passion, friendship, affinity, union, concord, sympathy, the interrelation of the sexes, and--as a suggestion apart from all offices of divination--that desire which is not in Nature, but by which Nature is sanctified.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -503,7 +672,7 @@ class CardDetailsRegistry:
"Three of Cups": { "Three of Cups": {
"explanation": { "explanation": {
"summary": "Maidens in a garden-ground with cups uplifted, as if pledging one another.", "summary": "Maidens in a garden-ground with cups uplifted, as if pledging one another.",
"waite": "Maidens in a garden-ground with cups uplifted, as if pledging one another. Divinatory Meanings: The conclusion of any matter in plenty, perfection and merriment; happy issue, victory, fulfilment, solace, healing, Reversed: Expedition, dispatch, achievement, end. It signifies also the side of excess in physical enjoyment, and the pleasures of the senses." "waite": "Maidens in a garden-ground with cups uplifted, as if pledging one another. Divinatory Meanings: The conclusion of any matter in plenty, perfection and merriment; happy issue, victory, fulfilment, solace, healing, Reversed: Expedition, dispatch, achievement, end. It signifies also the side of excess in physical enjoyment, and the pleasures of the senses.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -513,7 +682,7 @@ class CardDetailsRegistry:
"Four of Cups": { "Four of Cups": {
"explanation": { "explanation": {
"summary": "A young man is seated under a tree and contemplates three cups set on the grass before him; an arm issuing from a cloud offers him another cup.", "summary": "A young man is seated under a tree and contemplates three cups set on the grass before him; an arm issuing from a cloud offers him another cup.",
"waite": "A young man is seated under a tree and contemplates three cups set on the grass before him; an arm issuing from a cloud offers him another cup. His expression notwithstanding is one of discontent with his environment. Divinatory Meanings: Weariness, disgust, aversion, imaginary vexations, as if the wine of this world had caused satiety only; another wine, as if a fairy gift, is now offered the wastrel, but he sees no consolation therein. This is also a card of blended pleasure. Reversed: Novelty, presage, new instruction, new relations." "waite": "A young man is seated under a tree and contemplates three cups set on the grass before him; an arm issuing from a cloud offers him another cup. His expression notwithstanding is one of discontent with his environment. Divinatory Meanings: Weariness, disgust, aversion, imaginary vexations, as if the wine of this world had caused satiety only; another wine, as if a fairy gift, is now offered the wastrel, but he sees no consolation therein. This is also a card of blended pleasure. Reversed: Novelty, presage, new instruction, new relations.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -523,7 +692,7 @@ class CardDetailsRegistry:
"Five of Cups": { "Five of Cups": {
"explanation": { "explanation": {
"summary": "A dark, cloaked figure, looking sideways at three prone cups two others stand upright behind him; a bridge is in the background, leading to a small keep or holding.", "summary": "A dark, cloaked figure, looking sideways at three prone cups two others stand upright behind him; a bridge is in the background, leading to a small keep or holding.",
"waite": "A dark, cloaked figure, looking sideways at three prone cups two others stand upright behind him; a bridge is in the background, leading to a small keep or holding. Divinatory Meanings: It is a card of loss, but something remains over; three have been taken, but two are left; it is a card of inheritance, patrimony, transmission, but not corresponding to expectations; with some interpreters it is a card of marriage, but not without bitterness or frustration. Reversed: News, alliances, affinity, consanguinity, ancestry, return, false projects." "waite": "A dark, cloaked figure, looking sideways at three prone cups two others stand upright behind him; a bridge is in the background, leading to a small keep or holding. Divinatory Meanings: It is a card of loss, but something remains over; three have been taken, but two are left; it is a card of inheritance, patrimony, transmission, but not corresponding to expectations; with some interpreters it is a card of marriage, but not without bitterness or frustration. Reversed: News, alliances, affinity, consanguinity, ancestry, return, false projects.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -533,7 +702,7 @@ class CardDetailsRegistry:
"Six of Cups": { "Six of Cups": {
"explanation": { "explanation": {
"summary": "Children in an old garden, their cups filled with flowers.", "summary": "Children in an old garden, their cups filled with flowers.",
"waite": "Children in an old garden, their cups filled with flowers. Divinatory Meanings: A card of the past and of memories, looking back, as--for example--on childhood; happiness, enjoyment, but coming rather from the past; things that have vanished. Another reading reverses this, giving new relations, new knowledge, new environment, and then the children are disporting in an unfamiliar precinct. Reversed: The future, renewal, that which will come to pass presently." "waite": "Children in an old garden, their cups filled with flowers. Divinatory Meanings: A card of the past and of memories, looking back, as--for example--on childhood; happiness, enjoyment, but coming rather from the past; things that have vanished. Another reading reverses this, giving new relations, new knowledge, new environment, and then the children are disporting in an unfamiliar precinct. Reversed: The future, renewal, that which will come to pass presently.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -543,7 +712,7 @@ class CardDetailsRegistry:
"Seven of Cups": { "Seven of Cups": {
"explanation": { "explanation": {
"summary": "Strange chalices of vision, but the images are more especially those of the fantastic spirit.", "summary": "Strange chalices of vision, but the images are more especially those of the fantastic spirit.",
"waite": "Strange chalices of vision, but the images are more especially those of the fantastic spirit. Divinatory Meanings: Fairy favours, images of reflection, sentiment, imagination, things seen in the glass of contemplation; some attainment in these degrees, but nothing permanent or substantial is suggested. Reversed: Desire, will, determination, project." "waite": "Strange chalices of vision, but the images are more especially those of the fantastic spirit. Divinatory Meanings: Fairy favours, images of reflection, sentiment, imagination, things seen in the glass of contemplation; some attainment in these degrees, but nothing permanent or substantial is suggested. Reversed: Desire, will, determination, project.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -553,7 +722,7 @@ class CardDetailsRegistry:
"Eight of Cups": { "Eight of Cups": {
"explanation": { "explanation": {
"summary": "A man of dejected aspect is deserting the cups of his felicity, enterprise, undertaking or previous concern.", "summary": "A man of dejected aspect is deserting the cups of his felicity, enterprise, undertaking or previous concern.",
"waite": "A man of dejected aspect is deserting the cups of his felicity, enterprise, undertaking or previous concern. Divinatory Meanings: The card speaks for itself on the surface, but other readings are entirely antithetical--giving joy, mildness, timidity, honour, modesty. In practice, it is usually found that the card shews the decline of a matter, or that a matter which has been thought to be important is really of slight consequence--either for good or evil. Reversed: Great joy, happiness, feasting." "waite": "A man of dejected aspect is deserting the cups of his felicity, enterprise, undertaking or previous concern. Divinatory Meanings: The card speaks for itself on the surface, but other readings are entirely antithetical--giving joy, mildness, timidity, honour, modesty. In practice, it is usually found that the card shews the decline of a matter, or that a matter which has been thought to be important is really of slight consequence--either for good or evil. Reversed: Great joy, happiness, feasting.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -563,7 +732,7 @@ class CardDetailsRegistry:
"Nine of Cups": { "Nine of Cups": {
"explanation": { "explanation": {
"summary": "A goodly personage has feasted to his heart's content, and abundant refreshment of wine is on the arched counter behind him, seeming to indicate that the future is also assured.", "summary": "A goodly personage has feasted to his heart's content, and abundant refreshment of wine is on the arched counter behind him, seeming to indicate that the future is also assured.",
"waite": "A goodly personage has feasted to his heart's content, and abundant refreshment of wine is on the arched counter behind him, seeming to indicate that the future is also assured. The picture offers the material side only, but there are other aspects. Divinatory Meanings: Concord, contentment, physical bien-être; also victory, success, advantage; satisfaction for the Querent or person for whom the consultation is made. Reversed: Truth, loyalty, liberty; but the readings vary and include mistakes, imperfections, etc." "waite": "A goodly personage has feasted to his heart's content, and abundant refreshment of wine is on the arched counter behind him, seeming to indicate that the future is also assured. The picture offers the material side only, but there are other aspects. Divinatory Meanings: Concord, contentment, physical bien-être; also victory, success, advantage; satisfaction for the Querent or person for whom the consultation is made. Reversed: Truth, loyalty, liberty; but the readings vary and include mistakes, imperfections, etc.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -573,7 +742,7 @@ class CardDetailsRegistry:
"Ten of Cups": { "Ten of Cups": {
"explanation": { "explanation": {
"summary": "Appearance of Cups in a rainbow; it is contemplated in wonder and ecstacy by a man and woman below, evidently husband and wife.", "summary": "Appearance of Cups in a rainbow; it is contemplated in wonder and ecstacy by a man and woman below, evidently husband and wife.",
"waite": "Appearance of Cups in a rainbow; it is contemplated in wonder and ecstacy by a man and woman below, evidently husband and wife. His right arm is about her; his left is raised upward; she raises her right arm. The two children dancing near them have not observed the prodigy but are happy after their own manner. There is a home-scene beyond. Divinatory Meanings: Contentment, repose of the entire heart; the perfection of that state; also perfection of human love and friendship; if with several picture-cards, a person who is taking charge of the Querent's interests; also the town, village or country inhabited by the Querent. Reversed: Repose of the false heart, indignation, violence." "waite": "Appearance of Cups in a rainbow; it is contemplated in wonder and ecstacy by a man and woman below, evidently husband and wife. His right arm is about her; his left is raised upward; she raises her right arm. The two children dancing near them have not observed the prodigy but are happy after their own manner. There is a home-scene beyond. Divinatory Meanings: Contentment, repose of the entire heart; the perfection of that state; also perfection of human love and friendship; if with several picture-cards, a person who is taking charge of the Querent's interests; also the town, village or country inhabited by the Querent. Reversed: Repose of the false heart, indignation, violence.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -583,7 +752,7 @@ class CardDetailsRegistry:
"Page of Cups": { "Page of Cups": {
"explanation": { "explanation": {
"summary": "A fair, pleasing, somewhat effeminate page, of studious and intent aspect, contemplates a fish rising from a cup to look at him.", "summary": "A fair, pleasing, somewhat effeminate page, of studious and intent aspect, contemplates a fish rising from a cup to look at him.",
"waite": "A fair, pleasing, somewhat effeminate page, of studious and intent aspect, contemplates a fish rising from a cup to look at him. It is the pictures of the mind taking form. Divinatory Meanings: Fair young man, one impelled to render service and with whom the Querent will be connected; a studious youth; news, message; application, reflection, meditation; also these things directed to business. Reversed: Taste, inclination, attachment, seduction, deception, artifice." "waite": "A fair, pleasing, somewhat effeminate page, of studious and intent aspect, contemplates a fish rising from a cup to look at him. It is the pictures of the mind taking form. Divinatory Meanings: Fair young man, one impelled to render service and with whom the Querent will be connected; a studious youth; news, message; application, reflection, meditation; also these things directed to business. Reversed: Taste, inclination, attachment, seduction, deception, artifice.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -593,7 +762,7 @@ class CardDetailsRegistry:
"Knight of Cups": { "Knight of Cups": {
"explanation": { "explanation": {
"summary": "Graceful, but not warlike; riding quietly, wearing a winged helmet, referring to those higher graces of the imagination which sometimes characterize this card.", "summary": "Graceful, but not warlike; riding quietly, wearing a winged helmet, referring to those higher graces of the imagination which sometimes characterize this card.",
"waite": "Graceful, but not warlike; riding quietly, wearing a winged helmet, referring to those higher graces of the imagination which sometimes characterize this card. He too is a dreamer, but the images of the side of sense haunt him in his vision. Divinatory Meanings: Arrival, approach--sometimes that of a messenger; advances, proposition, demeanour, invitation, incitement. Reversed: Trickery, artifice, subtlety, swindling, duplicity, fraud." "waite": "Graceful, but not warlike; riding quietly, wearing a winged helmet, referring to those higher graces of the imagination which sometimes characterize this card. He too is a dreamer, but the images of the side of sense haunt him in his vision. Divinatory Meanings: Arrival, approach--sometimes that of a messenger; advances, proposition, demeanour, invitation, incitement. Reversed: Trickery, artifice, subtlety, swindling, duplicity, fraud.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -603,7 +772,7 @@ class CardDetailsRegistry:
"Queen of Cups": { "Queen of Cups": {
"explanation": { "explanation": {
"summary": "Beautiful, fair, dreamy--as one who sees visions in a cup.", "summary": "Beautiful, fair, dreamy--as one who sees visions in a cup.",
"waite": "Beautiful, fair, dreamy--as one who sees visions in a cup. This is, however, only one of her aspects; she sees, but she also acts, and her activity feeds her dream. Divinatory Meanings: Good, fair woman; honest, devoted woman, who will do service to the Querent; loving intelligence, and hence the gift of vision; success, happiness, pleasure; also wisdom, virtue; a perfect spouse and a good mother. Reversed: The accounts vary; good woman; otherwise, distinguished woman but one not to be trusted; perverse woman; vice, dishonour, depravity." "waite": "Beautiful, fair, dreamy--as one who sees visions in a cup. This is, however, only one of her aspects; she sees, but she also acts, and her activity feeds her dream. Divinatory Meanings: Good, fair woman; honest, devoted woman, who will do service to the Querent; loving intelligence, and hence the gift of vision; success, happiness, pleasure; also wisdom, virtue; a perfect spouse and a good mother. Reversed: The accounts vary; good woman; otherwise, distinguished woman but one not to be trusted; perverse woman; vice, dishonour, depravity.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -613,7 +782,7 @@ class CardDetailsRegistry:
"King of Cups": { "King of Cups": {
"explanation": { "explanation": {
"summary": "Fair man, man of business, law, or divinity; responsible, disposed to oblige the Querent; also equity, art and science, including those who profess science, law and art; creative intelligence.", "summary": "Fair man, man of business, law, or divinity; responsible, disposed to oblige the Querent; also equity, art and science, including those who profess science, law and art; creative intelligence.",
"waite": "Fair man, man of business, law, or divinity; responsible, disposed to oblige the Querent; also equity, art and science, including those who profess science, law and art; creative intelligence." "waite": "Fair man, man of business, law, or divinity; responsible, disposed to oblige the Querent; also equity, art and science, including those who profess science, law and art; creative intelligence.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -624,7 +793,7 @@ class CardDetailsRegistry:
"Ace of Pentacles": { "Ace of Pentacles": {
"explanation": { "explanation": {
"summary": "A hand--issuing, as usual, from a cloud--holds up a pentacle.", "summary": "A hand--issuing, as usual, from a cloud--holds up a pentacle.",
"waite": "A hand--issuing, as usual, from a cloud--holds up a pentacle. Divinatory Meanings: Perfect contentment, felicity, ecstasy; also speedy intelligence; gold. Reversed: The evil side of wealth, bad intelligence; also great riches. In any case it shews prosperity, comfortable material conditions, but whether these are of advantage to the possessor will depend on whether the card is reversed or not." "waite": "A hand--issuing, as usual, from a cloud--holds up a pentacle. Divinatory Meanings: Perfect contentment, felicity, ecstasy; also speedy intelligence; gold. Reversed: The evil side of wealth, bad intelligence; also great riches. In any case it shews prosperity, comfortable material conditions, but whether these are of advantage to the possessor will depend on whether the card is reversed or not.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -634,7 +803,7 @@ class CardDetailsRegistry:
"Two of Pentacles": { "Two of Pentacles": {
"explanation": { "explanation": {
"summary": "A young man, in the act of dancing, has a pentacle in either hand, and they are joined by that endless cord which is like the number 8 reversed.", "summary": "A young man, in the act of dancing, has a pentacle in either hand, and they are joined by that endless cord which is like the number 8 reversed.",
"waite": "A young man, in the act of dancing, has a pentacle in either hand, and they are joined by that endless cord which is like the number 8 reversed. Divinatory Meanings: On the one hand it is represented as a card of gaiety, recreation and its connexions, which is the subject of the design; but it is read also as news and messages in writing, as obstacles, agitation, trouble, embroilment. Reversed: Enforced gaiety, simulated enjoyment, literal sense, handwriting, composition, letters of exchange." "waite": "A young man, in the act of dancing, has a pentacle in either hand, and they are joined by that endless cord which is like the number 8 reversed. Divinatory Meanings: On the one hand it is represented as a card of gaiety, recreation and its connexions, which is the subject of the design; but it is read also as news and messages in writing, as obstacles, agitation, trouble, embroilment. Reversed: Enforced gaiety, simulated enjoyment, literal sense, handwriting, composition, letters of exchange.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -644,7 +813,7 @@ class CardDetailsRegistry:
"Three of Pentacles": { "Three of Pentacles": {
"explanation": { "explanation": {
"summary": "A sculptor at his work in a monastery.", "summary": "A sculptor at his work in a monastery.",
"waite": "A sculptor at his work in a monastery. Compare the design which illustrates the Eight of Pentacles. The apprentice or amateur therein has received his reward and is now at work in earnest. Divinatory Meanings: Métier, trade, skilled labour; usually, however, regarded as a card of nobility, aristocracy, renown, glory. Reversed: Mediocrity, in work and otherwise, puerility, pettiness, weakness." "waite": "A sculptor at his work in a monastery. Compare the design which illustrates the Eight of Pentacles. The apprentice or amateur therein has received his reward and is now at work in earnest. Divinatory Meanings: Métier, trade, skilled labour; usually, however, regarded as a card of nobility, aristocracy, renown, glory. Reversed: Mediocrity, in work and otherwise, puerility, pettiness, weakness.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -654,7 +823,7 @@ class CardDetailsRegistry:
"Four of Pentacles": { "Four of Pentacles": {
"explanation": { "explanation": {
"summary": "A crowned figure, having a pentacle over his crown, clasps another with hands and arms; two pentacles are under his feet.", "summary": "A crowned figure, having a pentacle over his crown, clasps another with hands and arms; two pentacles are under his feet.",
"waite": "A crowned figure, having a pentacle over his crown, clasps another with hands and arms; two pentacles are under his feet. He holds to that which he has. Divinatory Meanings: The surety of possessions, cleaving to that which one has, gift, legacy, inheritance. Reversed: Suspense, delay, opposition." "waite": "A crowned figure, having a pentacle over his crown, clasps another with hands and arms; two pentacles are under his feet. He holds to that which he has. Divinatory Meanings: The surety of possessions, cleaving to that which one has, gift, legacy, inheritance. Reversed: Suspense, delay, opposition.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -664,7 +833,7 @@ class CardDetailsRegistry:
"Five of Pentacles": { "Five of Pentacles": {
"explanation": { "explanation": {
"summary": "Two mendicants in a snow-storm pass a lighted casement.", "summary": "Two mendicants in a snow-storm pass a lighted casement.",
"waite": "Two mendicants in a snow-storm pass a lighted casement. Divinatory Meanings: The card foretells material trouble above all, whether in the form illustrated--that is, destitution--or otherwise. For some cartomancists, it is a card of love and lovers-wife, husband, friend, mistress; also concordance, affinities. These alternatives cannot be harmonized. Reversed: Disorder, chaos, ruin, discord, profligacy." "waite": "Two mendicants in a snow-storm pass a lighted casement. Divinatory Meanings: The card foretells material trouble above all, whether in the form illustrated--that is, destitution--or otherwise. For some cartomancists, it is a card of love and lovers-wife, husband, friend, mistress; also concordance, affinities. These alternatives cannot be harmonized. Reversed: Disorder, chaos, ruin, discord, profligacy.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -674,7 +843,7 @@ class CardDetailsRegistry:
"Six of Pentacles": { "Six of Pentacles": {
"explanation": { "explanation": {
"summary": "A person in the guise of a merchant weighs money in a pair of scales and distributes it to the needy and distressed.", "summary": "A person in the guise of a merchant weighs money in a pair of scales and distributes it to the needy and distressed.",
"waite": "A person in the guise of a merchant weighs money in a pair of scales and distributes it to the needy and distressed. It is a testimony to his own success in life, as well as to his goodness of heart. Divinatory Meanings: Presents, gifts, gratification another account says attention, vigilance now is the accepted time, present prosperity, etc. Reversed: Desire, cupidity, envy, jealousy, illusion." "waite": "A person in the guise of a merchant weighs money in a pair of scales and distributes it to the needy and distressed. It is a testimony to his own success in life, as well as to his goodness of heart. Divinatory Meanings: Presents, gifts, gratification another account says attention, vigilance now is the accepted time, present prosperity, etc. Reversed: Desire, cupidity, envy, jealousy, illusion.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -684,7 +853,7 @@ class CardDetailsRegistry:
"Seven of Pentacles": { "Seven of Pentacles": {
"explanation": { "explanation": {
"summary": "A young man, leaning on his staff, looks intently at seven pentacles attached to a clump of greenery on his right; one would say that these were his treasures and that his heart was there.", "summary": "A young man, leaning on his staff, looks intently at seven pentacles attached to a clump of greenery on his right; one would say that these were his treasures and that his heart was there.",
"waite": "A young man, leaning on his staff, looks intently at seven pentacles attached to a clump of greenery on his right; one would say that these were his treasures and that his heart was there. Divinatory Meanings: These are exceedingly contradictory; in the main, it is a card of money, business, barter; but one reading gives altercation, quarrels--and another innocence, ingenuity, purgation. Reversed: Cause for anxiety regarding money which it may be proposed to lend." "waite": "A young man, leaning on his staff, looks intently at seven pentacles attached to a clump of greenery on his right; one would say that these were his treasures and that his heart was there. Divinatory Meanings: These are exceedingly contradictory; in the main, it is a card of money, business, barter; but one reading gives altercation, quarrels--and another innocence, ingenuity, purgation. Reversed: Cause for anxiety regarding money which it may be proposed to lend.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -694,7 +863,7 @@ class CardDetailsRegistry:
"Eight of Pentacles": { "Eight of Pentacles": {
"explanation": { "explanation": {
"summary": "An artist in stone at his work, which he exhibits in the form of trophies.", "summary": "An artist in stone at his work, which he exhibits in the form of trophies.",
"waite": "An artist in stone at his work, which he exhibits in the form of trophies. Divinatory Meanings: Work, employment, commission, craftsmanship, skill in craft and business, perhaps in the preparatory stage. Reversed: Voided ambition, vanity, cupidity, exaction, usury. It may also signify the possession of skill, in the sense of the ingenious mind turned to cunning and intrigue." "waite": "An artist in stone at his work, which he exhibits in the form of trophies. Divinatory Meanings: Work, employment, commission, craftsmanship, skill in craft and business, perhaps in the preparatory stage. Reversed: Voided ambition, vanity, cupidity, exaction, usury. It may also signify the possession of skill, in the sense of the ingenious mind turned to cunning and intrigue.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -704,7 +873,7 @@ class CardDetailsRegistry:
"Nine of Pentacles": { "Nine of Pentacles": {
"explanation": { "explanation": {
"summary": "A woman, with a bird upon her wrist, stands amidst a great abundance of grapevines in the garden of a manorial house.", "summary": "A woman, with a bird upon her wrist, stands amidst a great abundance of grapevines in the garden of a manorial house.",
"waite": "A woman, with a bird upon her wrist, stands amidst a great abundance of grapevines in the garden of a manorial house. It is a wide domain, suggesting plenty in all things. Possibly it is her own possession and testifies to material well-being. Divinatory Meanings: Prudence, safety, success, accomplishment, certitude, discernment. Reversed: Roguery, deception, voided project, bad faith." "waite": "A woman, with a bird upon her wrist, stands amidst a great abundance of grapevines in the garden of a manorial house. It is a wide domain, suggesting plenty in all things. Possibly it is her own possession and testifies to material well-being. Divinatory Meanings: Prudence, safety, success, accomplishment, certitude, discernment. Reversed: Roguery, deception, voided project, bad faith.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -714,7 +883,7 @@ class CardDetailsRegistry:
"Ten of Pentacles": { "Ten of Pentacles": {
"explanation": { "explanation": {
"summary": "A man and woman beneath an archway which gives entrance to a house and domain.", "summary": "A man and woman beneath an archway which gives entrance to a house and domain.",
"waite": "A man and woman beneath an archway which gives entrance to a house and domain. They are accompanied by a child, who looks curiously at two dogs accosting an ancient personage seated in the foreground. The child's hand is on one of them. Divinatory Meanings: Gain, riches; family matters, archives, extraction, the abode of a family. Reversed: Chance, fatality, loss, robbery, games of hazard; sometimes gift, dowry, pension." "waite": "A man and woman beneath an archway which gives entrance to a house and domain. They are accompanied by a child, who looks curiously at two dogs accosting an ancient personage seated in the foreground. The child's hand is on one of them. Divinatory Meanings: Gain, riches; family matters, archives, extraction, the abode of a family. Reversed: Chance, fatality, loss, robbery, games of hazard; sometimes gift, dowry, pension.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -724,7 +893,7 @@ class CardDetailsRegistry:
"Page of Pentacles": { "Page of Pentacles": {
"explanation": { "explanation": {
"summary": "A youthful figure, looking intently at the pentacle which hovers over his raised hands.", "summary": "A youthful figure, looking intently at the pentacle which hovers over his raised hands.",
"waite": "A youthful figure, looking intently at the pentacle which hovers over his raised hands. He moves slowly, insensible of that which is about him. Divinatory Meanings: Application, study, scholarship, reflection another reading says news, messages and the bringer thereof; also rule, management. Reversed: Prodigality, dissipation, liberality, luxury; unfavourable news." "waite": "A youthful figure, looking intently at the pentacle which hovers over his raised hands. He moves slowly, insensible of that which is about him. Divinatory Meanings: Application, study, scholarship, reflection another reading says news, messages and the bringer thereof; also rule, management. Reversed: Prodigality, dissipation, liberality, luxury; unfavourable news.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -734,7 +903,7 @@ class CardDetailsRegistry:
"Knight of Pentacles": { "Knight of Pentacles": {
"explanation": { "explanation": {
"summary": "He rides a slow, enduring, heavy horse, to which his own aspect corresponds.", "summary": "He rides a slow, enduring, heavy horse, to which his own aspect corresponds.",
"waite": "He rides a slow, enduring, heavy horse, to which his own aspect corresponds. He exhibits his symbol, but does not look therein. Divinatory Meanings: Utility, serviceableness, interest, responsibility, rectitude-all on the normal and external plane. Reversed: inertia, idleness, repose of that kind, stagnation; also placidity, discouragement, carelessness." "waite": "He rides a slow, enduring, heavy horse, to which his own aspect corresponds. He exhibits his symbol, but does not look therein. Divinatory Meanings: Utility, serviceableness, interest, responsibility, rectitude-all on the normal and external plane. Reversed: inertia, idleness, repose of that kind, stagnation; also placidity, discouragement, carelessness.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -744,7 +913,7 @@ class CardDetailsRegistry:
"Queen of Pentacles": { "Queen of Pentacles": {
"explanation": { "explanation": {
"summary": "The face suggests that of a dark woman, whose qualities might be summed up in the idea of greatness of soul; she has also the serious cast of intelligence; she contemplates her symbol and may see worlds therein.", "summary": "The face suggests that of a dark woman, whose qualities might be summed up in the idea of greatness of soul; she has also the serious cast of intelligence; she contemplates her symbol and may see worlds therein.",
"waite": "The face suggests that of a dark woman, whose qualities might be summed up in the idea of greatness of soul; she has also the serious cast of intelligence; she contemplates her symbol and may see worlds therein. Divinatory Meanings: Opulence, generosity, magnificence, security, liberty. Reversed: Evil, suspicion, suspense, fear, mistrust." "waite": "The face suggests that of a dark woman, whose qualities might be summed up in the idea of greatness of soul; she has also the serious cast of intelligence; she contemplates her symbol and may see worlds therein. Divinatory Meanings: Opulence, generosity, magnificence, security, liberty. Reversed: Evil, suspicion, suspense, fear, mistrust.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -754,7 +923,7 @@ class CardDetailsRegistry:
"King of Pentacles": { "King of Pentacles": {
"explanation": { "explanation": {
"summary": "Valour, realizing intelligence, business and normal intellectual aptitude, sometimes mathematical gifts and attainments of this kind; success in these paths.", "summary": "Valour, realizing intelligence, business and normal intellectual aptitude, sometimes mathematical gifts and attainments of this kind; success in these paths.",
"waite": "Valour, realizing intelligence, business and normal intellectual aptitude, sometimes mathematical gifts and attainments of this kind; success in these paths." "waite": "Valour, realizing intelligence, business and normal intellectual aptitude, sometimes mathematical gifts and attainments of this kind; success in these paths.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -765,7 +934,7 @@ class CardDetailsRegistry:
"Ace of Wands": { "Ace of Wands": {
"explanation": { "explanation": {
"summary": "Creation, invention, enterprise, the powers which result in these; principle, beginning, source; birth, family, origin, and in a sense the virility which is behind them; the starting point of enterprises; according to another account, money, fortune, inheritance.", "summary": "Creation, invention, enterprise, the powers which result in these; principle, beginning, source; birth, family, origin, and in a sense the virility which is behind them; the starting point of enterprises; according to another account, money, fortune, inheritance.",
"waite": "Creation, invention, enterprise, the powers which result in these; principle, beginning, source; birth, family, origin, and in a sense the virility which is behind them; the starting point of enterprises; according to another account, money, fortune, inheritance." "waite": "Creation, invention, enterprise, the powers which result in these; principle, beginning, source; birth, family, origin, and in a sense the virility which is behind them; the starting point of enterprises; according to another account, money, fortune, inheritance.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -775,7 +944,7 @@ class CardDetailsRegistry:
"Two of Wands": { "Two of Wands": {
"explanation": { "explanation": {
"summary": "Between the alternative readings there is no marriage possible; on the one hand, riches, fortune, magnificence; on the other, physical suffering, disease, chagrin, sadness, mortification.", "summary": "Between the alternative readings there is no marriage possible; on the one hand, riches, fortune, magnificence; on the other, physical suffering, disease, chagrin, sadness, mortification.",
"waite": "Between the alternative readings there is no marriage possible; on the one hand, riches, fortune, magnificence; on the other, physical suffering, disease, chagrin, sadness, mortification. The design gives one suggestion; here is a lord overlooking his dominion and alternately contemplating a globe; it looks like the malady, the mortification, the sadness of Alexander amidst the grandeur of this world's wealth." "waite": "Between the alternative readings there is no marriage possible; on the one hand, riches, fortune, magnificence; on the other, physical suffering, disease, chagrin, sadness, mortification. The design gives one suggestion; here is a lord overlooking his dominion and alternately contemplating a globe; it looks like the malady, the mortification, the sadness of Alexander amidst the grandeur of this world's wealth.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -785,7 +954,7 @@ class CardDetailsRegistry:
"Three of Wands": { "Three of Wands": {
"explanation": { "explanation": {
"summary": "He symbolizes established strength, enterprise, effort, trade, commerce, discovery; those are his ships, bearing his merchandise, which are sailing over the sea.", "summary": "He symbolizes established strength, enterprise, effort, trade, commerce, discovery; those are his ships, bearing his merchandise, which are sailing over the sea.",
"waite": "He symbolizes established strength, enterprise, effort, trade, commerce, discovery; those are his ships, bearing his merchandise, which are sailing over the sea. The card also signifies able co-operation in business, as if the successful merchant prince were looking from his side towards yours with a view to help you." "waite": "He symbolizes established strength, enterprise, effort, trade, commerce, discovery; those are his ships, bearing his merchandise, which are sailing over the sea. The card also signifies able co-operation in business, as if the successful merchant prince were looking from his side towards yours with a view to help you.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -795,7 +964,7 @@ class CardDetailsRegistry:
"Four of Wands": { "Four of Wands": {
"explanation": { "explanation": {
"summary": "They are for once almost on the surface--country life, haven of refuge, a species of domestic harvest-home, repose, concord, harmony, prosperity, peace, and the perfected work of these.", "summary": "They are for once almost on the surface--country life, haven of refuge, a species of domestic harvest-home, repose, concord, harmony, prosperity, peace, and the perfected work of these.",
"waite": "They are for once almost on the surface--country life, haven of refuge, a species of domestic harvest-home, repose, concord, harmony, prosperity, peace, and the perfected work of these." "waite": "They are for once almost on the surface--country life, haven of refuge, a species of domestic harvest-home, repose, concord, harmony, prosperity, peace, and the perfected work of these.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -805,7 +974,7 @@ class CardDetailsRegistry:
"Five of Wands": { "Five of Wands": {
"explanation": { "explanation": {
"summary": "Imitation, as, for example, sham fight, but also the strenuous competition and struggle of the search after riches and fortune.", "summary": "Imitation, as, for example, sham fight, but also the strenuous competition and struggle of the search after riches and fortune.",
"waite": "Imitation, as, for example, sham fight, but also the strenuous competition and struggle of the search after riches and fortune. In this sense it connects with the battle of life. Hence some attributions say that it is a card of gold, gain, opulence." "waite": "Imitation, as, for example, sham fight, but also the strenuous competition and struggle of the search after riches and fortune. In this sense it connects with the battle of life. Hence some attributions say that it is a card of gold, gain, opulence.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -815,7 +984,7 @@ class CardDetailsRegistry:
"Six of Wands": { "Six of Wands": {
"explanation": { "explanation": {
"summary": "The card has been so designed that it can cover several significations; on the surface, it is a victor triumphing, but it is also great news, such as might be carried in state by the King's courier; it is expectation crowned with its own desire, the crown of hope, and so forth.", "summary": "The card has been so designed that it can cover several significations; on the surface, it is a victor triumphing, but it is also great news, such as might be carried in state by the King's courier; it is expectation crowned with its own desire, the crown of hope, and so forth.",
"waite": "The card has been so designed that it can cover several significations; on the surface, it is a victor triumphing, but it is also great news, such as might be carried in state by the King's courier; it is expectation crowned with its own desire, the crown of hope, and so forth." "waite": "The card has been so designed that it can cover several significations; on the surface, it is a victor triumphing, but it is also great news, such as might be carried in state by the King's courier; it is expectation crowned with its own desire, the crown of hope, and so forth.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -825,7 +994,7 @@ class CardDetailsRegistry:
"Seven of Wands": { "Seven of Wands": {
"explanation": { "explanation": {
"summary": "It is a card of valour, for, on the surface, six are attacking one, who has, however, the vantage position.", "summary": "It is a card of valour, for, on the surface, six are attacking one, who has, however, the vantage position.",
"waite": "It is a card of valour, for, on the surface, six are attacking one, who has, however, the vantage position. On the intellectual plane, it signifies discussion, wordy strife; in business--negotiations, war of trade, barter, competition. It is further a card of success, for the combatant is on the top and his enemies may be unable to reach him." "waite": "It is a card of valour, for, on the surface, six are attacking one, who has, however, the vantage position. On the intellectual plane, it signifies discussion, wordy strife; in business--negotiations, war of trade, barter, competition. It is further a card of success, for the combatant is on the top and his enemies may be unable to reach him.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -835,7 +1004,7 @@ class CardDetailsRegistry:
"Eight of Wands": { "Eight of Wands": {
"explanation": { "explanation": {
"summary": "Activity in undertakings, the path of such activity, swiftness, as that of an express messenger; great haste, great hope, speed towards an end which promises assured felicity; generally, that which is on the move; also the arrows of love.", "summary": "Activity in undertakings, the path of such activity, swiftness, as that of an express messenger; great haste, great hope, speed towards an end which promises assured felicity; generally, that which is on the move; also the arrows of love.",
"waite": "Activity in undertakings, the path of such activity, swiftness, as that of an express messenger; great haste, great hope, speed towards an end which promises assured felicity; generally, that which is on the move; also the arrows of love." "waite": "Activity in undertakings, the path of such activity, swiftness, as that of an express messenger; great haste, great hope, speed towards an end which promises assured felicity; generally, that which is on the move; also the arrows of love.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -845,7 +1014,7 @@ class CardDetailsRegistry:
"Nine of Wands": { "Nine of Wands": {
"explanation": { "explanation": {
"summary": "The card signifies strength in opposition.", "summary": "The card signifies strength in opposition.",
"waite": "The card signifies strength in opposition. If attacked, the person will meet an onslaught boldly; and his build shews, that he may prove a formidable antagonist. With this main significance there are all its possible adjuncts--delay, suspension, adjournment." "waite": "The card signifies strength in opposition. If attacked, the person will meet an onslaught boldly; and his build shews, that he may prove a formidable antagonist. With this main significance there are all its possible adjuncts--delay, suspension, adjournment.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -855,7 +1024,7 @@ class CardDetailsRegistry:
"Ten of Wands": { "Ten of Wands": {
"explanation": { "explanation": {
"summary": "A card of many significances, and some of the readings cannot be harmonized.", "summary": "A card of many significances, and some of the readings cannot be harmonized.",
"waite": "A card of many significances, and some of the readings cannot be harmonized. I set aside that which connects it with honour and good faith. The chief meaning is oppression simply, but it is also fortune, gain, any kind of success, and then it is the oppression of these things. It is also a card of false-seeming, disguise, perfidy. The place which the figure is approaching may suffer from the rods that he carries. Success is stultified if the Nine of Swords follows, and if it is a question of a lawsuit, there will be certain loss." "waite": "A card of many significances, and some of the readings cannot be harmonized. I set aside that which connects it with honour and good faith. The chief meaning is oppression simply, but it is also fortune, gain, any kind of success, and then it is the oppression of these things. It is also a card of false-seeming, disguise, perfidy. The place which the figure is approaching may suffer from the rods that he carries. Success is stultified if the Nine of Swords follows, and if it is a question of a lawsuit, there will be certain loss.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -865,7 +1034,7 @@ class CardDetailsRegistry:
"Page of Wands": { "Page of Wands": {
"explanation": { "explanation": {
"summary": "Dark young man, faithful, a lover, an envoy, a postman.", "summary": "Dark young man, faithful, a lover, an envoy, a postman.",
"waite": "Dark young man, faithful, a lover, an envoy, a postman. Beside a man, he will bear favourable testimony concerning him. A dangerous rival, if followed by the Page of Cups. Has the chief qualities of his suit. He may signify family intelligence." "waite": "Dark young man, faithful, a lover, an envoy, a postman. Beside a man, he will bear favourable testimony concerning him. A dangerous rival, if followed by the Page of Cups. Has the chief qualities of his suit. He may signify family intelligence.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -875,7 +1044,7 @@ class CardDetailsRegistry:
"Knight of Wands": { "Knight of Wands": {
"explanation": { "explanation": {
"summary": "Departure, absence, flight, emigration.", "summary": "Departure, absence, flight, emigration.",
"waite": "Departure, absence, flight, emigration. A dark young man, friendly. Change of residence." "waite": "Departure, absence, flight, emigration. A dark young man, friendly. Change of residence.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -885,7 +1054,7 @@ class CardDetailsRegistry:
"Queen of Wands": { "Queen of Wands": {
"explanation": { "explanation": {
"summary": "A dark woman, countrywoman, friendly, chaste, loving, honourable.", "summary": "A dark woman, countrywoman, friendly, chaste, loving, honourable.",
"waite": "A dark woman, countrywoman, friendly, chaste, loving, honourable. If the card beside her signifies a man, she is well disposed towards him; if a woman, she is interested in the Querent. Also, love of money, or a certain success in business." "waite": "A dark woman, countrywoman, friendly, chaste, loving, honourable. If the card beside her signifies a man, she is well disposed towards him; if a woman, she is interested in the Querent. Also, love of money, or a certain success in business.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -895,7 +1064,7 @@ class CardDetailsRegistry:
"King of Wands": { "King of Wands": {
"explanation": { "explanation": {
"summary": "Dark man, friendly, countryman, generally married, honest and conscientious.", "summary": "Dark man, friendly, countryman, generally married, honest and conscientious.",
"waite": "Dark man, friendly, countryman, generally married, honest and conscientious. The card always signifies honesty, and may mean news concerning an unexpected heritage to fall in before very long." "waite": "Dark man, friendly, countryman, generally married, honest and conscientious. The card always signifies honesty, and may mean news concerning an unexpected heritage to fall in before very long.",
}, },
"interpretation": "", "interpretation": "",
"keywords": [], "keywords": [],
@@ -942,11 +1111,12 @@ class CardDetailsRegistry:
Dictionary of card details for that suit Dictionary of card details for that suit
""" """
return { return {
name: details for name, details in self._details.items() name: details
for name, details in self._details.items()
if suit_name.lower() in name.lower() if suit_name.lower() in name.lower()
} }
def _get_registry_key_for_card(self, card: 'Card') -> Optional[str]: def _get_registry_key_for_card(self, card: "Card") -> Optional[str]:
""" """
Get the registry key for a card based on deck position (1-78). Get the registry key for a card based on deck position (1-78).
@@ -961,7 +1131,7 @@ class CardDetailsRegistry:
""" """
return self._position_map.get(card.number) return self._position_map.get(card.number)
def load_into_card(self, card: 'Card') -> bool: def load_into_card(self, card: "Card") -> bool:
""" """
Load details from registry into a Card object using its position. Load details from registry into a Card object using its position.

View File

@@ -14,10 +14,9 @@ Usage:
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
@@ -27,10 +26,12 @@ 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:
""" """
@@ -51,15 +52,17 @@ class ImageDeckLoader:
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)
@@ -124,10 +127,10 @@ class ImageDeckLoader:
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
@@ -174,7 +177,7 @@ class ImageDeckLoader:
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.
@@ -247,14 +250,14 @@ class ImageDeckLoader:
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.
@@ -305,20 +308,19 @@ class ImageDeckLoader:
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.

View File

@@ -24,10 +24,7 @@ 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.
@@ -49,15 +46,14 @@ 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.
@@ -78,6 +74,7 @@ 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
@@ -102,10 +99,7 @@ def load_deck_details(
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.
@@ -124,19 +118,19 @@ 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.
@@ -155,13 +149,15 @@ 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.
@@ -181,24 +177,24 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
# 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
@@ -206,7 +202,7 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
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']}")
@@ -244,8 +240,7 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
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.
@@ -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)

View File

@@ -18,9 +18,9 @@ Usage:
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,6 +29,7 @@ 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
@@ -44,8 +45,9 @@ 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:
@@ -54,9 +56,11 @@ class DrawnCard:
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:
@@ -64,97 +68,102 @@ class Spread:
# 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"
),
],
}, },
} }
@@ -169,43 +178,41 @@ class Spread:
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.name = spread_data["name"]
self.description = spread_data['description'] self.description = spread_data["description"]
self.positions: List[SpreadPosition] = spread_data['positions'] 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)
@@ -215,11 +222,7 @@ class Spread:
@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']}")
@@ -254,20 +257,18 @@ def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
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)
@@ -298,14 +299,14 @@ class SpreadReading:
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:
@@ -319,16 +320,16 @@ class SpreadReading:
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)

View File

@@ -6,16 +6,16 @@ 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__ = [

View File

@@ -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,6 +51,7 @@ 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
@@ -85,10 +95,17 @@ class Card:
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))
@@ -111,20 +128,26 @@ 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
@@ -142,6 +165,7 @@ class PipCard(MinorCard):
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:
@@ -158,6 +182,7 @@ class AceCard(MinorCard):
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:
@@ -183,7 +208,7 @@ class CourtCard(MinorCard):
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:
@@ -194,12 +219,12 @@ 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
@@ -209,8 +234,11 @@ class CardQuery:
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]
@@ -223,33 +251,45 @@ class CardQuery:
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."""
@@ -272,8 +312,13 @@ 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.
@@ -288,24 +333,27 @@ class TemporalQuery:
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."""
@@ -389,19 +437,20 @@ class DLT:
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()
@@ -486,7 +535,7 @@ class Deck:
# 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")
@@ -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
@@ -538,11 +591,10 @@ 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
@@ -561,12 +613,12 @@ class Deck:
# 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,7 +677,6 @@ 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)
@@ -644,7 +695,9 @@ class Deck:
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):
@@ -680,8 +733,7 @@ class Deck:
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]:

View File

@@ -25,15 +25,18 @@ Usage:
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:
@@ -44,16 +47,20 @@ class DeckAccessor:
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.
@@ -75,7 +82,7 @@ class Tarot:
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
@@ -85,35 +92,34 @@ class Tarot:
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

View File

@@ -6,20 +6,20 @@ supporting multiple decks and automatic image resolution.
""" """
import os import os
import io
import tkinter as tk import tkinter as tk
from tkinter import ttk, filedialog
from pathlib import Path from pathlib import Path
from tkinter import filedialog, ttk
from typing import List, Optional from typing import List, Optional
try: try:
from PIL import Image, ImageTk, ImageGrab, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont, ImageTk
HAS_PILLOW = True HAS_PILLOW = True
except ImportError: except ImportError:
HAS_PILLOW = False HAS_PILLOW = False
from tarot.deck import Card
from tarot.card.image_loader import ImageDeckLoader from tarot.card.image_loader import ImageDeckLoader
from tarot.deck import Card
class CardDisplay: class CardDisplay:
@@ -66,7 +66,7 @@ class CardDisplay:
return return
# Import spread classes here to avoid circular imports if any # Import spread classes here to avoid circular imports if any
from tarot.card.spread import SpreadPosition, DrawnCard, SpreadReading from tarot.card.spread import DrawnCard, SpreadPosition, SpreadReading
# Create a dummy spread class since Spread requires a valid name # Create a dummy spread class since Spread requires a valid name
class SimpleSpread: class SimpleSpread:
@@ -81,19 +81,11 @@ class CardDisplay:
for i, card in enumerate(cards, 1): for i, card in enumerate(cards, 1):
# Create a generic position # Create a generic position
pos = SpreadPosition( pos = SpreadPosition(number=i, name=f"Card {i}", meaning="Display Card")
number=i,
name=f"Card {i}",
meaning="Display Card"
)
positions.append(pos) positions.append(pos)
# Create drawn card # Create drawn card
drawn = DrawnCard( drawn = DrawnCard(position=pos, card=card, is_reversed=False)
position=pos,
card=card,
is_reversed=False
)
drawn_cards.append(drawn) drawn_cards.append(drawn)
# Create a synthetic spread # Create a synthetic spread
@@ -101,7 +93,7 @@ class CardDisplay:
spread.positions = positions spread.positions = positions
# Create reading # Create reading
reading = SpreadReading(spread, drawn_cards) # type: ignore reading = SpreadReading(spread, drawn_cards) # type: ignore
# Use SpreadDisplay # Use SpreadDisplay
display = SpreadDisplay(reading, self.deck_name) display = SpreadDisplay(reading, self.deck_name)
@@ -109,8 +101,6 @@ class CardDisplay:
display.run() display.run()
class CubeDisplay: class CubeDisplay:
""" """
Displays the Cube of Space with navigation. Displays the Cube of Space with navigation.
@@ -124,6 +114,12 @@ class CubeDisplay:
"Above": {"Right": "East", "Left": "West", "Up": "South", "Down": "North"}, "Above": {"Right": "East", "Left": "West", "Up": "South", "Down": "North"},
"Below": {"Right": "East", "Left": "West", "Up": "North", "Down": "South"}, "Below": {"Right": "East", "Left": "West", "Up": "North", "Down": "South"},
} }
# Zoom bounds used when UI is not initialized (headless tests)
MIN_ZOOM = 0.5
MAX_ZOOM = 3.0
# Zoom bounds for the live UI (when canvas/root exist)
UI_MIN_ZOOM = 0.1
UI_MAX_ZOOM = 50.0
def __init__(self, cube, deck_name: str = "default"): def __init__(self, cube, deck_name: str = "default"):
self.cube = cube self.cube = cube
@@ -153,9 +149,9 @@ class CubeDisplay:
self.root.bind("<plus>", lambda e: self._zoom(1.1)) self.root.bind("<plus>", lambda e: self._zoom(1.1))
self.root.bind("<equal>", lambda e: self._zoom(1.1)) # Often same key as plus self.root.bind("<equal>", lambda e: self._zoom(1.1)) # Often same key as plus
self.root.bind("<minus>", lambda e: self._zoom(0.9)) self.root.bind("<minus>", lambda e: self._zoom(0.9))
self.root.bind("<underscore>", lambda e: self._zoom(0.9)) # Shift+minus self.root.bind("<underscore>", lambda e: self._zoom(0.9)) # Shift+minus
self.root.bind("<KP_Add>", lambda e: self._zoom(1.1)) # Numpad + self.root.bind("<KP_Add>", lambda e: self._zoom(1.1)) # Numpad +
self.root.bind("<KP_Subtract>", lambda e: self._zoom(0.9)) # Numpad - self.root.bind("<KP_Subtract>", lambda e: self._zoom(0.9)) # Numpad -
# Bind WASD for panning # Bind WASD for panning
self.root.bind("w", lambda e: self._pan_key("up")) self.root.bind("w", lambda e: self._pan_key("up"))
@@ -181,7 +177,9 @@ class CubeDisplay:
# Content Frame (inside canvas) # Content Frame (inside canvas)
self.content_frame = ttk.Frame(self.canvas) self.content_frame = ttk.Frame(self.canvas)
self.canvas_window = self.canvas.create_window((0, 0), window=self.content_frame, anchor="center") self.canvas_window = self.canvas.create_window(
(0, 0), window=self.content_frame, anchor="center"
)
# Overlay Controls # Overlay Controls
# Navigation Frame (Bottom Center) # Navigation Frame (Bottom Center)
@@ -194,8 +192,12 @@ class CubeDisplay:
# Populate Zoom Frame # Populate Zoom Frame
ttk.Label(zoom_frame, text="Zoom:").pack(side=tk.LEFT, padx=5) ttk.Label(zoom_frame, text="Zoom:").pack(side=tk.LEFT, padx=5)
ttk.Button(zoom_frame, text="+", width=3, command=lambda: self._zoom(1.22)).pack(side=tk.LEFT) ttk.Button(zoom_frame, text="+", width=3, command=lambda: self._zoom(1.22)).pack(
ttk.Button(zoom_frame, text="-", width=3, command=lambda: self._zoom(0.82)).pack(side=tk.LEFT) side=tk.LEFT
)
ttk.Button(zoom_frame, text="-", width=3, command=lambda: self._zoom(0.82)).pack(
side=tk.LEFT
)
# Populate Navigation Frame # Populate Navigation Frame
dir_frame = ttk.Frame(nav_frame) dir_frame = ttk.Frame(nav_frame)
@@ -205,8 +207,12 @@ class CubeDisplay:
mid_nav = ttk.Frame(dir_frame) mid_nav = ttk.Frame(dir_frame)
mid_nav.pack(side=tk.TOP) mid_nav.pack(side=tk.TOP)
ttk.Button(mid_nav, text="Left", command=lambda: self._navigate("Left")).pack(side=tk.LEFT, padx=5) ttk.Button(mid_nav, text="Left", command=lambda: self._navigate("Left")).pack(
ttk.Button(mid_nav, text="Right", command=lambda: self._navigate("Right")).pack(side=tk.LEFT, padx=5) side=tk.LEFT, padx=5
)
ttk.Button(mid_nav, text="Right", command=lambda: self._navigate("Right")).pack(
side=tk.LEFT, padx=5
)
ttk.Button(dir_frame, text="Down", command=lambda: self._navigate("Down")).pack(side=tk.TOP) ttk.Button(dir_frame, text="Down", command=lambda: self._navigate("Down")).pack(side=tk.TOP)
@@ -221,7 +227,7 @@ class CubeDisplay:
screen_height = self.root.winfo_screenheight() screen_height = self.root.winfo_screenheight()
x = (screen_width // 2) - (width // 2) x = (screen_width // 2) - (width // 2)
y = (screen_height // 2) - (height // 2) y = (screen_height // 2) - (height // 2)
self.root.geometry(f'{width}x{height}+{x}+{y}') self.root.geometry(f"{width}x{height}+{x}+{y}")
# Ensure window has focus for keyboard events # Ensure window has focus for keyboard events
self.root.focus_force() self.root.focus_force()
@@ -238,17 +244,27 @@ class CubeDisplay:
def _pan_key(self, direction): def _pan_key(self, direction):
"""Pan the canvas using keys.""" """Pan the canvas using keys."""
if direction == 'up': if direction == "up":
self.canvas.yview_scroll(-1, "units") self.canvas.yview_scroll(-1, "units")
elif direction == 'down': elif direction == "down":
self.canvas.yview_scroll(1, "units") self.canvas.yview_scroll(1, "units")
elif direction == 'left': elif direction == "left":
self.canvas.xview_scroll(-1, "units") self.canvas.xview_scroll(-1, "units")
elif direction == 'right': elif direction == "right":
self.canvas.xview_scroll(1, "units") self.canvas.xview_scroll(1, "units")
def _zoom(self, factor): def _zoom(self, factor):
"""Adjust zoom level and redraw, keeping the view centered.""" """Adjust zoom level and redraw, keeping the view centered."""
# If UI not initialized (no canvas), just update zoom level and return.
if not getattr(self, "canvas", None):
old_zoom = self.zoom_level
self.zoom_level *= factor
# Clamp zoom level to configured bounds
self.zoom_level = max(self.MIN_ZOOM, min(self.zoom_level, self.MAX_ZOOM))
# _update_display is safe and will no-op if content isn't initialized
self._update_display()
return
# 1. Capture current state # 1. Capture current state
canvas_width = self.canvas.winfo_width() canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height() canvas_height = self.canvas.winfo_height()
@@ -262,7 +278,8 @@ class CubeDisplay:
if not bbox: if not bbox:
# Should not happen if initialized # Should not happen if initialized
self.zoom_level *= factor self.zoom_level *= factor
self.zoom_level = max(0.1, min(self.zoom_level, 50.0)) # Clamp to UI bounds when canvas exists
self.zoom_level = max(self.UI_MIN_ZOOM, min(self.zoom_level, self.UI_MAX_ZOOM))
self._update_display() self._update_display()
return return
@@ -276,8 +293,12 @@ class CubeDisplay:
# 2. Update zoom level # 2. Update zoom level
old_zoom = self.zoom_level old_zoom = self.zoom_level
self.zoom_level *= factor self.zoom_level *= factor
# Clamp zoom level # Clamp zoom level (UI uses broader bounds when initialized)
self.zoom_level = max(0.1, min(self.zoom_level, 50.0)) if getattr(self, "canvas", None):
min_z, max_z = self.UI_MIN_ZOOM, self.UI_MAX_ZOOM
else:
min_z, max_z = self.MIN_ZOOM, self.MAX_ZOOM
self.zoom_level = max(min_z, min(self.zoom_level, max_z))
# Calculate effective factor in case of clamping # Calculate effective factor in case of clamping
effective_factor = self.zoom_level / old_zoom if old_zoom > 0 else factor effective_factor = self.zoom_level / old_zoom if old_zoom > 0 else factor
@@ -335,8 +356,11 @@ class CubeDisplay:
widget.destroy() widget.destroy()
# Title # Title
ttk.Label(self.content_frame, text=f"Wall: {self.current_wall_name}", ttk.Label(
font=("Helvetica", 16, "bold")).pack(pady=(0, 20)) self.content_frame,
text=f"Wall: {self.current_wall_name}",
font=("Helvetica", 16, "bold"),
).pack(pady=(0, 20))
# Grid for directions # Grid for directions
grid_frame = ttk.Frame(self.content_frame) grid_frame = ttk.Frame(self.content_frame)
@@ -351,7 +375,7 @@ class CubeDisplay:
"West": (1, 0), "West": (1, 0),
"Center": (1, 1), "Center": (1, 1),
"East": (1, 2), "East": (1, 2),
"South": (2, 1) "South": (2, 1),
} }
# Calculate sizes based on zoom # Calculate sizes based on zoom
@@ -365,7 +389,9 @@ class CubeDisplay:
for dir_name, (row, col) in layout.items(): for dir_name, (row, col) in layout.items():
direction = wall.direction(dir_name) direction = wall.direction(dir_name)
cell_frame = ttk.Frame(grid_frame, borderwidth=1, relief="solid", width=cell_width, height=cell_height) cell_frame = ttk.Frame(
grid_frame, borderwidth=1, relief="solid", width=cell_width, height=cell_height
)
cell_frame.grid(row=row, column=col, padx=5, pady=5) cell_frame.grid(row=row, column=col, padx=5, pady=5)
cell_frame.grid_propagate(False) cell_frame.grid_propagate(False)
@@ -385,9 +411,11 @@ class CubeDisplay:
pil_img = Image.open(img_path) pil_img = Image.open(img_path)
# Resize for grid # Resize for grid
base_height = img_height base_height = img_height
h_percent = (base_height / float(pil_img.size[1])) h_percent = base_height / float(pil_img.size[1])
w_size = int((float(pil_img.size[0]) * float(h_percent))) w_size = int((float(pil_img.size[0]) * float(h_percent)))
pil_img = pil_img.resize((w_size, base_height), Image.Resampling.LANCZOS) pil_img = pil_img.resize(
(w_size, base_height), Image.Resampling.LANCZOS
)
tk_img = ImageTk.PhotoImage(pil_img) tk_img = ImageTk.PhotoImage(pil_img)
self.root.images.append(tk_img) self.root.images.append(tk_img)
@@ -414,10 +442,10 @@ class CubeDisplay:
content_height = self.content_frame.winfo_reqheight() content_height = self.content_frame.winfo_reqheight()
if canvas_width > content_width and canvas_height > content_height: if canvas_width > content_width and canvas_height > content_height:
self.canvas.coords(self.canvas_window, canvas_width/2, canvas_height/2) self.canvas.coords(self.canvas_window, canvas_width / 2, canvas_height / 2)
else: else:
# Reset to top-left or center of scroll region # Reset to top-left or center of scroll region
self.canvas.coords(self.canvas_window, content_width/2, content_height/2) self.canvas.coords(self.canvas_window, content_width / 2, content_height / 2)
# Bind panning events to all content widgets # Bind panning events to all content widgets
self._bind_recursive(self.content_frame) self._bind_recursive(self.content_frame)
@@ -491,6 +519,7 @@ def display_cube(cube=None, deck_name: str = "default"):
""" """
if cube is None: if cube is None:
from tarot.tarot_api import Tarot from tarot.tarot_api import Tarot
cube = Tarot.cube cube = Tarot.cube
display = CubeDisplay(cube, deck_name) display = CubeDisplay(cube, deck_name)
@@ -506,54 +535,48 @@ class SpreadDisplay:
# Coordinates are relative grid units (approx card width/height) # Coordinates are relative grid units (approx card width/height)
# Using 1.02 spacing for tight layout # Using 1.02 spacing for tight layout
LAYOUTS = { LAYOUTS = {
'Celtic Cross': { "Celtic Cross": {
1: {'pos': (0, 0)}, 1: {"pos": (0, 0)},
2: {'pos': (0, 0), 'rotate': 90, 'z': 10}, # Top layer 2: {"pos": (0, 0), "rotate": 90, "z": 10}, # Top layer
3: {'pos': (0, -1.02)}, 3: {"pos": (0, -1.02)},
4: {'pos': (0, 1.02)}, 4: {"pos": (0, 1.02)},
5: {'pos': (-1.02, 0)}, 5: {"pos": (-1.02, 0)},
6: {'pos': (1.02, 0)}, 6: {"pos": (1.02, 0)},
7: {'pos': (2.1, 1.53)}, # 1.5 * 1.02 7: {"pos": (2.1, 1.53)}, # 1.5 * 1.02
8: {'pos': (2.1, 0.51)}, # 0.5 * 1.02 8: {"pos": (2.1, 0.51)}, # 0.5 * 1.02
9: {'pos': (2.1, -0.51)}, 9: {"pos": (2.1, -0.51)},
10: {'pos': (2.1, -1.53)} 10: {"pos": (2.1, -1.53)},
}, },
'3-Card Spread': { "3-Card Spread": {1: {"pos": (-1.02, 0)}, 2: {"pos": (0, 0)}, 3: {"pos": (1.02, 0)}},
1: {'pos': (-1.02, 0)}, "Golden Dawn 3-Card": {
2: {'pos': (0, 0)}, 1: {"pos": (0, -1.02)},
3: {'pos': (1.02, 0)} 2: {"pos": (-1.02, 0.8)},
3: {"pos": (1.02, 0.8)},
}, },
'Golden Dawn 3-Card': { "Horseshoe": {
1: {'pos': (0, -1.02)}, 1: {"pos": (-3.06, 1.02)},
2: {'pos': (-1.02, 0.8)}, 2: {"pos": (-2.04, 0)},
3: {'pos': (1.02, 0.8)} 3: {"pos": (-1.02, -0.51)},
4: {"pos": (0, -1.02)},
5: {"pos": (1.02, -0.51)},
6: {"pos": (2.04, 0)},
7: {"pos": (3.06, 1.02)},
}, },
'Horseshoe': { "Pentagram": {
1: {'pos': (-3.06, 1.02)}, 1: {"pos": (0, -1.5)}, # Spirit (Top)
2: {'pos': (-2.04, 0)}, 2: {"pos": (1.5, -0.4)}, # Fire (Right Top)
3: {'pos': (-1.02, -0.51)}, 3: {"pos": (1.0, 1.5)}, # Water (Right Bottom)
4: {'pos': (0, -1.02)}, 4: {"pos": (-1.0, 1.5)}, # Air (Left Bottom)
5: {'pos': (1.02, -0.51)}, 5: {"pos": (-1.5, -0.4)}, # Earth (Left Top)
6: {'pos': (2.04, 0)},
7: {'pos': (3.06, 1.02)}
}, },
'Pentagram': { "Relationship": {
1: {'pos': (0, -1.5)}, # Spirit (Top) 1: {"pos": (-1.5, 0)}, # You
2: {'pos': (1.5, -0.4)}, # Fire (Right Top) 2: {"pos": (1.5, 0)}, # Them
3: {'pos': (1.0, 1.5)}, # Water (Right Bottom) 3: {"pos": (0, -1.02)}, # Relationship (Center Top)
4: {'pos': (-1.0, 1.5)}, # Air (Left Bottom) 4: {"pos": (0, 0.5)}, # Challenge (Center Bottom)
5: {'pos': (-1.5, -0.4)} # Earth (Left Top) 5: {"pos": (0, 2.0)}, # Outcome (Bottom)
}, },
'Relationship': { "Yes or No": {1: {"pos": (0, 0)}},
1: {'pos': (-1.5, 0)}, # You
2: {'pos': (1.5, 0)}, # Them
3: {'pos': (0, -1.02)}, # Relationship (Center Top)
4: {'pos': (0, 0.5)}, # Challenge (Center Bottom)
5: {'pos': (0, 2.0)} # Outcome (Bottom)
},
'Yes or No': {
1: {'pos': (0, 0)}
}
} }
def __init__(self, reading, deck_name="default"): def __init__(self, reading, deck_name="default"):
@@ -566,7 +589,7 @@ class SpreadDisplay:
self.show_text = True self.show_text = True
self.show_top_card = True self.show_top_card = True
self.drag_data = {"x": 0, "y": 0} self.drag_data = {"x": 0, "y": 0}
self._tk_images = [] # Keep references self._tk_images = [] # Keep references
# Setup UI # Setup UI
self._setup_ui() self._setup_ui()
@@ -593,15 +616,25 @@ class SpreadDisplay:
toolbar = ttk.Frame(self.root) toolbar = ttk.Frame(self.root)
toolbar.pack(side=tk.TOP, fill=tk.X) toolbar.pack(side=tk.TOP, fill=tk.X)
ttk.Button(toolbar, text="Zoom In (+)", command=lambda: self._zoom(1.2)).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="Zoom In (+)", command=lambda: self._zoom(1.2)).pack(
ttk.Button(toolbar, text="Zoom Out (-)", command=lambda: self._zoom(0.8)).pack(side=tk.LEFT, padx=2) side=tk.LEFT, padx=2
)
ttk.Button(toolbar, text="Zoom Out (-)", command=lambda: self._zoom(0.8)).pack(
side=tk.LEFT, padx=2
)
ttk.Button(toolbar, text="Reset View", command=self._reset_view).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="Reset View", command=self._reset_view).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar, text="Toggle Text", command=self._toggle_text).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="Toggle Text", command=self._toggle_text).pack(
ttk.Button(toolbar, text="Export PNG", command=self._export_image).pack(side=tk.LEFT, padx=2) side=tk.LEFT, padx=2
)
ttk.Button(toolbar, text="Export PNG", command=self._export_image).pack(
side=tk.LEFT, padx=2
)
# Only show toggle top card if relevant # Only show toggle top card if relevant
if self.reading.spread.name == 'Celtic Cross': if self.reading.spread.name == "Celtic Cross":
ttk.Button(toolbar, text="Toggle Cross", command=self._toggle_top_card).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="Toggle Cross", command=self._toggle_top_card).pack(
side=tk.LEFT, padx=2
)
# Canvas # Canvas
self.canvas = tk.Canvas(self.root, bg="#2c3e50") self.canvas = tk.Canvas(self.root, bg="#2c3e50")
@@ -636,8 +669,8 @@ class SpreadDisplay:
# Center of virtual space (arbitrary large number to allow scrolling) # Center of virtual space (arbitrary large number to allow scrolling)
cx, cy = 2000, 2000 cx, cy = 2000, 2000
min_x, min_y = float('inf'), float('inf') min_x, min_y = float("inf"), float("inf")
max_x, max_y = float('-inf'), float('-inf') max_x, max_y = float("-inf"), float("-inf")
# Sort cards by z-index (default 0) # Sort cards by z-index (default 0)
cards_to_draw = [] cards_to_draw = []
@@ -646,7 +679,7 @@ class SpreadDisplay:
if not pos_data: if not pos_data:
continue continue
z_index = pos_data.get('z', 0) z_index = pos_data.get("z", 0)
# Skip if top card is hidden # Skip if top card is hidden
if z_index > 0 and not self.show_top_card: if z_index > 0 and not self.show_top_card:
@@ -658,8 +691,8 @@ class SpreadDisplay:
cards_to_draw.sort(key=lambda x: x[2]) cards_to_draw.sort(key=lambda x: x[2])
for drawn, pos_data, _ in cards_to_draw: for drawn, pos_data, _ in cards_to_draw:
rel_x, rel_y = pos_data['pos'] rel_x, rel_y = pos_data["pos"]
rotation = pos_data.get('rotate', 0) rotation = pos_data.get("rotate", 0)
# Calculate position # Calculate position
x = cx + (rel_x * unit_x) x = cx + (rel_x * unit_x)
@@ -672,7 +705,7 @@ class SpreadDisplay:
# If rotated 90 deg, width and height swap for bounding box # If rotated 90 deg, width and height swap for bounding box
if abs(rotation % 180) == 90: if abs(rotation % 180) == 90:
half_w, half_h = half_h, half_w half_w, half_h = half_h, half_w
min_x = min(min_x, x - half_w) min_x = min(min_x, x - half_w)
max_x = max(max_x, x + half_w) max_x = max(max_x, x + half_w)
@@ -684,7 +717,9 @@ class SpreadDisplay:
# Set scroll region # Set scroll region
padding = 50 * self.zoom_level padding = 50 * self.zoom_level
self.canvas.configure(scrollregion=(min_x - padding, min_y - padding, max_x + padding, max_y + padding)) self.canvas.configure(
scrollregion=(min_x - padding, min_y - padding, max_x + padding, max_y + padding)
)
def _draw_card(self, drawn, x, y, w, h, layout_rotation): def _draw_card(self, drawn, x, y, w, h, layout_rotation):
card_name = drawn.card.name card_name = drawn.card.name
@@ -701,8 +736,10 @@ class SpreadDisplay:
if not pil_image: if not pil_image:
# Draw placeholder # Draw placeholder
self.canvas.create_rectangle(x-w/2, y-h/2, x+w/2, y+h/2, fill="white", outline="black") self.canvas.create_rectangle(
self.canvas.create_text(x, y, text=card_name, width=w-10) x - w / 2, y - h / 2, x + w / 2, y + h / 2, fill="white", outline="black"
)
self.canvas.create_text(x, y, text=card_name, width=w - 10)
else: else:
# Total rotation # Total rotation
rotation = layout_rotation rotation = layout_rotation
@@ -728,7 +765,7 @@ class SpreadDisplay:
# Text Overlay # Text Overlay
# Calculate visual dimensions # Calculate visual dimensions
is_vertical = (layout_rotation % 180 == 0) is_vertical = layout_rotation % 180 == 0
vis_w = w if is_vertical else h vis_w = w if is_vertical else h
vis_h = h if is_vertical else w vis_h = h if is_vertical else w
@@ -739,35 +776,36 @@ class SpreadDisplay:
# Height needed: approx 3 lines of text # Height needed: approx 3 lines of text
text_h = font_size * 3.5 text_h = font_size * 3.5
bg_x1 = x - vis_w/2 bg_x1 = x - vis_w / 2
bg_y1 = y + vis_h/2 - text_h bg_y1 = y + vis_h / 2 - text_h
bg_x2 = x + vis_w/2 bg_x2 = x + vis_w / 2
bg_y2 = y + vis_h/2 bg_y2 = y + vis_h / 2
# Draw semi-transparent-ish background (stipple works on some platforms, otherwise solid) # Draw semi-transparent-ish background (stipple works on some platforms, otherwise solid)
self.canvas.create_rectangle( self.canvas.create_rectangle(
bg_x1, bg_y1, bg_x2, bg_y2, bg_x1, bg_y1, bg_x2, bg_y2, fill="#000000", stipple="gray75", outline=""
fill="#000000", stipple="gray75", outline=""
) )
# Position Name # Position Name
self.canvas.create_text( self.canvas.create_text(
x, bg_y1 + font_size, x,
bg_y1 + font_size,
text=f"{drawn.position.number}. {drawn.position.name}", text=f"{drawn.position.number}. {drawn.position.name}",
fill="white", fill="white",
font=("Arial", font_size, "bold"), font=("Arial", font_size, "bold"),
width=vis_w - 4, width=vis_w - 4,
justify="center" justify="center",
) )
# Meaning (shortened) # Meaning (shortened)
self.canvas.create_text( self.canvas.create_text(
x, bg_y1 + font_size * 2.2, x,
bg_y1 + font_size * 2.2,
text=drawn.position.meaning, text=drawn.position.meaning,
fill="#ecf0f1", fill="#ecf0f1",
font=("Arial", int(font_size * 0.8)), font=("Arial", int(font_size * 0.8)),
width=vis_w - 4, width=vis_w - 4,
justify="center" justify="center",
) )
def _draw_grid_fallback(self): def _draw_grid_fallback(self):
@@ -777,15 +815,15 @@ class SpreadDisplay:
card_height = 150 * self.zoom_level card_height = 150 * self.zoom_level
padding = 20 * self.zoom_level padding = 20 * self.zoom_level
min_x, min_y = float('inf'), float('inf') min_x, min_y = float("inf"), float("inf")
max_x, max_y = float('-inf'), float('-inf') max_x, max_y = float("-inf"), float("-inf")
cols = 5 cols = 5
for i, drawn in enumerate(self.reading.drawn_cards): for i, drawn in enumerate(self.reading.drawn_cards):
row = i // cols row = i // cols
col = i % cols col = i % cols
x = cx + (col - cols/2) * (card_width + padding) x = cx + (col - cols / 2) * (card_width + padding)
y = cy + (row * 1.5) * (card_height + padding) y = cy + (row * 1.5) * (card_height + padding)
self._draw_card(drawn, x, y, card_width, card_height, 0) self._draw_card(drawn, x, y, card_width, card_height, 0)
@@ -799,16 +837,18 @@ class SpreadDisplay:
min_y = min(min_y, y - half_h) min_y = min(min_y, y - half_h)
max_y = max(max_y, y + half_h) max_y = max(max_y, y + half_h)
if min_x == float('inf'): # No cards if min_x == float("inf"): # No cards
min_x, min_y, max_x, max_y = cx, cy, cx, cy min_x, min_y, max_x, max_y = cx, cy, cx, cy
scroll_padding = 50 * self.zoom_level scroll_padding = 50 * self.zoom_level
self.canvas.configure(scrollregion=( self.canvas.configure(
min_x - scroll_padding, scrollregion=(
min_y - scroll_padding, min_x - scroll_padding,
max_x + scroll_padding, min_y - scroll_padding,
max_y + scroll_padding max_x + scroll_padding,
)) max_y + scroll_padding,
)
)
def _zoom(self, factor): def _zoom(self, factor):
self.zoom_level *= factor self.zoom_level *= factor
@@ -962,7 +1002,10 @@ class SpreadDisplay:
if pil_image.mode in ("RGBA", "PA"): if pil_image.mode in ("RGBA", "PA"):
# Create white background for transparency # Create white background for transparency
background = Image.new("RGB", pil_image.size, "white") background = Image.new("RGB", pil_image.size, "white")
background.paste(pil_image, mask=pil_image.split()[-1] if pil_image.mode == "RGBA" else None) background.paste(
pil_image,
mask=pil_image.split()[-1] if pil_image.mode == "RGBA" else None,
)
pil_image = background pil_image = background
elif pil_image.mode != "RGB": elif pil_image.mode != "RGB":
pil_image = pil_image.convert("RGB") pil_image = pil_image.convert("RGB")
@@ -994,7 +1037,9 @@ class SpreadDisplay:
except Exception as e: except Exception as e:
print(f" Debug: Paste failed for {drawn.card.name} at ({x}, {y}): {e}") print(f" Debug: Paste failed for {drawn.card.name} at ({x}, {y}): {e}")
print(f" Image mode: {pil_image.mode}, size: {pil_image.size}") print(f" Image mode: {pil_image.mode}, size: {pil_image.size}")
print(f" Canvas size: {img.size}, position: ({int(x - pw / 2)}, {int(y - ph / 2)})") print(
f" Canvas size: {img.size}, position: ({int(x - pw / 2)}, {int(y - ph / 2)})"
)
if self.show_text: if self.show_text:
text_font = font text_font = font
@@ -1008,7 +1053,9 @@ class SpreadDisplay:
by2 = int(y + card_height / 2) by2 = int(y + card_height / 2)
draw.rectangle([bx1, by1, bx2, by2], fill=(0, 0, 0, 180)) draw.rectangle([bx1, by1, bx2, by2], fill=(0, 0, 0, 180))
draw.text((bx1 + 4, by1 + 4), label, fill="white", font=text_font) draw.text((bx1 + 4, by1 + 4), label, fill="white", font=text_font)
draw.text((bx1 + 4, by1 + 4 + text_h * 1.4), meaning, fill="#ecf0f1", font=text_font) draw.text(
(bx1 + 4, by1 + 4 + text_h * 1.4), meaning, fill="#ecf0f1", font=text_font
)
return img.convert("RGB") return img.convert("RGB")

View File

@@ -28,26 +28,26 @@ Access Patterns:
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",
] ]

View File

@@ -20,12 +20,13 @@ Usage:
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:

View File

@@ -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

View File

@@ -3,9 +3,10 @@
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:
@@ -20,12 +21,12 @@ class Calendar:
""" """
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

View File

@@ -6,15 +6,16 @@ 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
@@ -26,8 +27,9 @@ class SolarEvent:
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:
@@ -118,4 +120,4 @@ class TemporalCoordinates:
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

View File

@@ -4,12 +4,12 @@ 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:
@@ -22,6 +22,7 @@ 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:
@@ -32,8 +33,18 @@ class Month:
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]
@@ -47,6 +58,7 @@ 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
@@ -64,6 +76,7 @@ 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:
@@ -85,6 +98,7 @@ 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

View File

@@ -4,7 +4,7 @@ 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:
@@ -35,9 +35,9 @@ class TimeUtil:
secs = remaining % 60 secs = remaining % 60
return { return {
'hours': hours, "hours": hours,
'minutes': minutes, "minutes": minutes,
'seconds': secs, "seconds": secs,
} }
@staticmethod @staticmethod

View File

@@ -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__ = [

View File

@@ -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)
@@ -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,11 +72,12 @@ 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):
@@ -94,6 +99,7 @@ class Colorscale:
- 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]
@@ -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
@@ -170,6 +178,7 @@ class Planet:
@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
@@ -231,7 +240,11 @@ class God:
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:
@@ -249,6 +262,7 @@ class God:
@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"
@@ -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)

View File

@@ -21,11 +21,17 @@ Usage:
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]:
@@ -96,10 +102,7 @@ def _matches_filter(obj: Any, key: str, value: Any) -> bool:
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
@@ -117,8 +120,8 @@ def _matches_filter(obj: Any, key: str, value: Any) -> bool:
# 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
@@ -184,10 +187,7 @@ def universal_filter(items: List[T], **kwargs) -> List[T]:
# 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

View File

@@ -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"
@@ -62,39 +63,36 @@ class Personality:
""" """
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.
@@ -140,7 +138,7 @@ class Personality:
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:
@@ -152,17 +150,14 @@ class Personality:
"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",
@@ -170,9 +165,7 @@ class Personality:
} }
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:

View File

@@ -16,14 +16,13 @@ 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:
@@ -36,7 +35,7 @@ def is_nested_object(obj: Any) -> bool:
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:
@@ -60,16 +59,16 @@ def get_item_label(item: Any, fallback: str = "item") -> str:
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 []
@@ -90,9 +89,9 @@ def get_object_attributes(obj: Any) -> List[Tuple[str, Any]]:
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
@@ -121,16 +120,21 @@ def format_value(value: Any, indent: int = 2) -> str:
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', 'Perfume', 'God', 'Colorscale', 'Sephera', 'ElementType']: if has_custom_str and obj_class in [
"Path",
"Planet",
"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)

View File

@@ -20,9 +20,10 @@ Usage:
""" """
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:
@@ -32,18 +33,18 @@ class QueryResult:
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)
@@ -61,7 +62,7 @@ class Query:
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.
@@ -74,12 +75,12 @@ class Query:
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
@@ -97,16 +98,16 @@ class Query:
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.
@@ -142,7 +143,7 @@ class Query:
""" """
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()
@@ -205,8 +206,6 @@ class CollectionAccessor(Generic[T]):
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)"
@@ -241,7 +240,7 @@ 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.
@@ -259,8 +258,6 @@ class FilterableDict(dict):
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)"
@@ -283,7 +280,7 @@ class FilterableDict(dict):
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.

View File

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

View File

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

View File

@@ -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,7 +424,7 @@ 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."""
@@ -413,6 +439,7 @@ class TestDigitalRoot:
# CardDataLoader Tests # CardDataLoader Tests
# ============================================================================ # ============================================================================
class TestCardDataLoader: class TestCardDataLoader:
@pytest.fixture @pytest.fixture
def loader(self): def loader(self):
@@ -463,13 +490,15 @@ class TestCardDataLoader:
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 == "||||||"

View File

@@ -1,13 +1,16 @@
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

View File

@@ -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,6 +10,7 @@ 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)
@@ -28,13 +31,14 @@ def test_cube_navigation():
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

View File

@@ -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_zoom(): def test_cube_zoom():
cube = Tarot.cube cube = Tarot.cube
@@ -13,6 +15,7 @@ def test_cube_zoom():
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)

View File

@@ -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
@@ -67,10 +138,18 @@ def test_zoom_limits():
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
@@ -80,20 +159,20 @@ def test_zoom_limits():
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

View File

@@ -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

View File

@@ -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

View File

@@ -1,8 +1,11 @@
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:

View File

@@ -1,8 +1,11 @@
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.
@@ -16,23 +19,46 @@ def test_zoom_key_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
@@ -58,6 +84,7 @@ def test_zoom_key_bindings():
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)

View File

@@ -1,51 +1,108 @@
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

View File

@@ -1,42 +1,84 @@
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:
@@ -45,20 +87,47 @@ def test_wasd_panning():
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))
@@ -77,10 +146,18 @@ def test_wasd_panning():
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
@@ -90,13 +167,13 @@ def test_wasd_panning():
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)

View File

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