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")
"""
from .tree import Tree
from .cube import Cube
from .tree import Tree
# Export classes for fluent access
__all__ = ["Tree", "Cube"]

View File

@@ -6,22 +6,22 @@ including Sephira, Paths, and Tree of Life structures.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any
from typing import Dict, List, Optional, Tuple
from utils.attributes import (
Element,
ElementType,
Planet,
Color,
Colorscale,
Perfume,
ElementType,
God,
Perfume,
Planet,
)
@dataclass
class Sephera:
"""Represents a Sephira on the Tree of Life."""
number: int
name: str
hebrew_name: str
@@ -29,21 +29,22 @@ class Sephera:
archangel: str
order_of_angels: str
mundane_chakra: str
element: Optional['ElementType'] = None
element: Optional["ElementType"] = None
planetary_ruler: Optional[str] = None
tarot_trump: Optional[str] = None
colorscale: Optional['Colorscale'] = None
colorscale: Optional["Colorscale"] = None
@dataclass
class PeriodicTable:
"""Represents a Sephirothic position in Kabbalah with cross-correspondences."""
number: int
name: str
sephera: Optional[Sephera]
element: Optional['ElementType'] = None
planet: Optional['Planet'] = None
color: Optional['Color'] = None
element: Optional["ElementType"] = None
planet: Optional["Planet"] = None
color: Optional["Color"] = None
tarot_trump: Optional[str] = None
hebrew_letter: Optional[str] = None
divine_name: Optional[str] = None
@@ -55,6 +56,7 @@ class PeriodicTable:
@dataclass
class TreeOfLife:
"""Represents the Tree of Life structure."""
sephiroth: Dict[int, str]
paths: Dict[Tuple[int, int], str]
@@ -62,6 +64,7 @@ class TreeOfLife:
@dataclass
class Correspondences:
"""Represents Kabbalistic correspondences."""
number: int
sephira: str
element: Optional[str]
@@ -76,18 +79,19 @@ class Correspondences:
@dataclass
class Path:
"""Represents one of the 22 Paths on the Tree of Life with full correspondences."""
number: int # 11-32
hebrew_letter: str # Hebrew letter name (Aleph through Tau)
transliteration: str # English transliteration
tarot_trump: str # Major Arcana card (0-XXI)
sephera_from: Optional['Sephera'] = None # Lower Sephira
sephera_to: Optional['Sephera'] = None # Upper Sephira
element: Optional['ElementType'] = None # Element (Air, Fire, Water, Earth)
planet: Optional['Planet'] = None # Planetary ruler
sephera_from: Optional["Sephera"] = None # Lower Sephira
sephera_to: Optional["Sephera"] = None # Upper Sephira
element: Optional["ElementType"] = None # Element (Air, Fire, Water, Earth)
planet: Optional["Planet"] = None # Planetary ruler
zodiac_sign: Optional[str] = None # Zodiac sign (12 paths only)
colorscale: Optional['Colorscale'] = None # Golden Dawn color scales
perfumes: List['Perfume'] = field(default_factory=list)
gods: Dict[str, List['God']] = field(default_factory=dict)
colorscale: Optional["Colorscale"] = None # Golden Dawn color scales
perfumes: List["Perfume"] = field(default_factory=list)
gods: Dict[str, List["God"]] = field(default_factory=dict)
keywords: List[str] = field(default_factory=list)
description: str = ""
@@ -108,23 +112,23 @@ class Path:
"""Check if this path has zodiac correspondence."""
return self.zodiac_sign is not None
def add_god(self, god: 'God') -> None:
def add_god(self, god: "God") -> None:
"""Attach a god to this path grouped by culture."""
culture_key = god.culture_key()
culture_bucket = self.gods.setdefault(culture_key, [])
if god not in culture_bucket:
culture_bucket.append(god)
def add_perfume(self, perfume: 'Perfume') -> None:
def add_perfume(self, perfume: "Perfume") -> None:
"""Attach a perfume correspondence if it is not already present."""
if perfume not in self.perfumes:
self.perfumes.append(perfume)
def get_gods(self, culture: Optional[str] = None) -> List['God']:
def get_gods(self, culture: Optional[str] = None) -> List["God"]:
"""Return all gods for this path, optionally filtered by culture."""
if culture:
return list(self.gods.get(culture.lower(), []))
merged: List['God'] = []
merged: List["God"] = []
for values in self.gods.values():
merged.extend(values)
return merged
@@ -148,7 +152,7 @@ class Path:
# Element
if self.element:
element_name = self.element.name if hasattr(self.element, 'name') else str(self.element)
element_name = self.element.name if hasattr(self.element, "name") else str(self.element)
lines.append(f"element: {element_name}")
# Planet

View File

@@ -1,6 +1,6 @@
"""Cube namespace - access Cube of Space walls and areas."""
from .cube import Cube
from .attributes import CubeOfSpace, Wall, WallDirection
from .cube import Cube
__all__ = ["Cube", "CubeOfSpace", "Wall", "WallDirection"]

View File

@@ -17,6 +17,7 @@ class WallDirection:
Each wall has 5 directions: North, South, East, West, Center.
Each direction has a Hebrew letter and zodiac correspondence.
"""
name: str # "North", "South", "East", "West", "Center"
letter: str # Hebrew letter (e.g., "Aleph", "Bet", etc.)
zodiac: Optional[str] = None # Zodiac sign if applicable
@@ -51,6 +52,7 @@ class Wall:
Opposite walls: North↔South, East↔West, Above↔Below.
Each direction has a Hebrew letter and zodiac correspondence.
"""
name: str # "North", "South", "East", "West", "Above", "Below"
side: str # Alias for name, used for filtering (e.g., "north", "south")
opposite: str # Opposite wall name (e.g., "South" for North wall)
@@ -82,9 +84,7 @@ class Wall:
# Validate side matches name (case-insensitive)
if self.side.capitalize() != self.name:
raise ValueError(
f"Wall side '{self.side}' must match name '{self.name}'"
)
raise ValueError(f"Wall side '{self.side}' must match name '{self.name}'")
# Validate opposite wall
expected_opposite = self.OPPOSITE_WALLS.get(self.name)
@@ -177,6 +177,7 @@ class CubeOfSpace:
- Opposite walls: North↔South, East↔West, Above↔Below
- Total: 30 positions plus central core
"""
walls: Dict[str, Wall] = field(default_factory=dict)
center: Optional[WallDirection] = None # Central core position
@@ -392,9 +393,7 @@ class CubeOfSpace:
"""Validate that all 6 walls are present."""
required_walls = {"North", "South", "East", "West", "Above", "Below"}
if set(self.walls.keys()) != required_walls:
raise ValueError(
f"CubeOfSpace must have all 6 walls, got: {set(self.walls.keys())}"
)
raise ValueError(f"CubeOfSpace must have all 6 walls, got: {set(self.walls.keys())}")
@classmethod
def create_default(cls) -> "CubeOfSpace":
@@ -416,7 +415,7 @@ class CubeOfSpace:
"above": {"name": "North", "letter": "Bet", "zodiac": None},
"below": {"name": "South", "letter": "Gimel", "zodiac": None},
"east": {"name": "East", "letter": "Daleth", "zodiac": "Aries"},
"west": {"name": "West", "letter": "He", "zodiac": "Pisces"}
"west": {"name": "West", "letter": "He", "zodiac": "Pisces"},
}
for wall_name, wall_data in cls._WALL_DEFINITIONS.items():

View File

@@ -17,7 +17,10 @@ Usage:
wall.direction("East") # Get specific direction
"""
from typing import Optional, Any
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from kaballah.cube.attributes import CubeOfSpace
class CubeMeta(type):
@@ -29,7 +32,7 @@ class CubeMeta(type):
if cls._cube is None:
return "Cube of Space (not initialized)"
walls = cls._cube.walls if hasattr(cls._cube, 'walls') else {}
walls = cls._cube.walls if hasattr(cls._cube, "walls") else {}
lines = [
"Cube of Space",
"=" * 60,
@@ -42,8 +45,8 @@ class CubeMeta(type):
for wall_name in ["North", "South", "East", "West", "Above", "Below"]:
wall = walls.get(wall_name)
if wall:
element = f" [{wall.element}]" if hasattr(wall, 'element') else ""
areas = len(wall.directions) if hasattr(wall, 'directions') else 0
element = f" [{wall.element}]" if hasattr(wall, "element") else ""
areas = len(wall.directions) if hasattr(wall, "directions") else 0
lines.append(f" {wall_name}{element}: {areas} areas")
return "\n".join(lines)
@@ -53,7 +56,7 @@ class CubeMeta(type):
cls._ensure_initialized()
if cls._cube is None:
return "Cube(not initialized)"
walls = cls._cube.walls if hasattr(cls._cube, 'walls') else {}
walls = cls._cube.walls if hasattr(cls._cube, "walls") else {}
return f"Cube(walls={len(walls)})"
@@ -68,7 +71,7 @@ class DirectionAccessor:
def all(self) -> list:
"""Get all directions in this wall."""
if self._wall is None or not hasattr(self._wall, 'directions'):
if self._wall is None or not hasattr(self._wall, "directions"):
return []
return list(self._wall.directions.values())
@@ -89,10 +92,7 @@ class DirectionAccessor:
# Filter by direction name if provided
if direction_name:
all_dirs = [
d for d in all_dirs
if d.name.lower() == direction_name.lower()
]
all_dirs = [d for d in all_dirs if d.name.lower() == direction_name.lower()]
# Apply other filters
if kwargs:
@@ -126,7 +126,7 @@ class DirectionAccessor:
"""Get specific direction by name."""
if direction_name is None:
return self.all()
if self._wall is None or not hasattr(self._wall, 'directions'):
if self._wall is None or not hasattr(self._wall, "directions"):
return None
return self._wall.directions.get(direction_name.capitalize())
@@ -152,7 +152,7 @@ class WallWrapper:
def __getattr__(self, name: str) -> Any:
"""Delegate attribute access to the wrapped wall."""
if name in ('_wall', '_direction_accessor'):
if name in ("_wall", "_direction_accessor"):
return object.__getattribute__(self, name)
return getattr(self._wall, name)
@@ -343,6 +343,7 @@ class Cube(metaclass=CubeMeta):
# Use a descriptor to make wall work like a property on the class
class WallProperty:
"""Descriptor that returns wall accessor when accessed."""
def __get__(self, obj: Any, objtype: Optional[type] = None) -> "WallAccessor":
if objtype is None:
objtype = type(obj)

View File

@@ -13,12 +13,12 @@ Usage:
print(Tree()) # Display Tree structure
"""
from typing import TYPE_CHECKING, Dict, List, Optional, Union, overload
from typing import TYPE_CHECKING, Dict, Optional, Union, overload
if TYPE_CHECKING:
from tarot.attributes import Sephera, Path
from tarot.attributes import Path, Sephera
from tarot.card.data import CardDataLoader
from utils.query import QueryResult, Query
from utils.query import Query
class TreeMeta(type):
@@ -28,8 +28,8 @@ class TreeMeta(type):
"""Return readable representation when Tree is converted to string."""
# Access Tree class attributes through type.__getattribute__
Tree._ensure_initialized()
sepheras = type.__getattribute__(cls, '_sepheras')
paths = type.__getattribute__(cls, '_paths')
sepheras = type.__getattribute__(cls, "_sepheras")
paths = type.__getattribute__(cls, "_paths")
lines = [
"Tree of Life",
"=" * 60,
@@ -49,8 +49,8 @@ class TreeMeta(type):
def __repr__(cls) -> str:
"""Return object representation."""
Tree._ensure_initialized()
sepheras = type.__getattribute__(cls, '_sepheras')
paths = type.__getattribute__(cls, '_paths')
sepheras = type.__getattribute__(cls, "_sepheras")
paths = type.__getattribute__(cls, "_paths")
return f"Tree(sepheras={len(sepheras)}, paths={len(paths)})"
@@ -65,10 +65,10 @@ class Tree(metaclass=TreeMeta):
print(Tree()) # Displays tree structure
"""
_sepheras: Dict[int, 'Sephera'] = {} # type: ignore
_paths: Dict[int, 'Path'] = {} # type: ignore
_sepheras: Dict[int, "Sephera"] = {} # type: ignore
_paths: Dict[int, "Path"] = {} # type: ignore
_initialized: bool = False
_loader: Optional['CardDataLoader'] = None # type: ignore
_loader: Optional["CardDataLoader"] = None # type: ignore
@classmethod
def _ensure_initialized(cls) -> None:
@@ -77,6 +77,7 @@ class Tree(metaclass=TreeMeta):
return
from tarot.card.data import CardDataLoader
cls._loader = CardDataLoader()
cls._sepheras = cls._loader._sephera
cls._paths = cls._loader._paths
@@ -84,16 +85,16 @@ class Tree(metaclass=TreeMeta):
@classmethod
@overload
def sephera(cls, number: int) -> Optional['Sephera']:
...
def sephera(cls, number: int) -> Optional["Sephera"]: ...
@classmethod
@overload
def sephera(cls, number: None = ...) -> Dict[int, 'Sephera']:
...
def sephera(cls, number: None = ...) -> Dict[int, "Sephera"]: ...
@classmethod
def sephera(cls, number: Optional[int] = None) -> Union[Optional['Sephera'], Dict[int, 'Sephera']]:
def sephera(
cls, number: Optional[int] = None
) -> Union[Optional["Sephera"], Dict[int, "Sephera"]]:
"""Return a Sephira or all Sephiroth."""
cls._ensure_initialized()
if number is None:
@@ -102,16 +103,14 @@ class Tree(metaclass=TreeMeta):
@classmethod
@overload
def path(cls, number: int) -> Optional['Path']:
...
def path(cls, number: int) -> Optional["Path"]: ...
@classmethod
@overload
def path(cls, number: None = ...) -> Dict[int, 'Path']:
...
def path(cls, number: None = ...) -> Dict[int, "Path"]: ...
@classmethod
def path(cls, number: Optional[int] = None) -> Union[Optional['Path'], Dict[int, 'Path']]:
def path(cls, number: Optional[int] = None) -> Union[Optional["Path"], Dict[int, "Path"]]:
"""Return a Path or all Paths."""
cls._ensure_initialized()
if number is None:
@@ -119,7 +118,7 @@ class Tree(metaclass=TreeMeta):
return cls._paths.get(number)
@classmethod
def filter(cls, expression: str) -> 'Query':
def filter(cls, expression: str) -> "Query":
"""
Filter Sephiroth by attribute:value expression.
@@ -131,6 +130,7 @@ class Tree(metaclass=TreeMeta):
Returns a Query object for chaining.
"""
from tarot.query import Query
cls._ensure_initialized()
# Create a query from all Sephiroth
return Query(cls._sepheras).filter(expression)

View File

@@ -18,10 +18,9 @@ Usage:
letter.paths('aleph') # Get Hebrew letter with Tarot correspondences
"""
from .iChing import hexagram, trigram
from .letter import letter
from .iChing import trigram, hexagram
from .words import word
from .paths import letters
from .words import word
__all__ = ["letter", "trigram", "hexagram", "word", "letters"]

View File

@@ -6,19 +6,19 @@ including Alphabets, Enochian letters, and Double Letter Trumps.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any
from typing import Any, Dict, List, Optional, Tuple
from utils.attributes import (
Element,
ElementType,
Planet,
Meaning,
Planet,
)
@dataclass
class Letter:
"""Represents a letter with its attributes."""
character: str
position: int
name: str
@@ -27,6 +27,7 @@ class Letter:
@dataclass
class EnglishAlphabet:
"""English alphabet with Tarot/Kabbalistic correspondence."""
letter: str
position: int
sound: str
@@ -41,6 +42,7 @@ class EnglishAlphabet:
@dataclass
class GreekAlphabet:
"""Greek alphabet with Tarot/Kabbalistic correspondence."""
letter: str
position: int
transliteration: str
@@ -53,6 +55,7 @@ class GreekAlphabet:
@dataclass
class HebrewAlphabet:
"""Hebrew alphabet with Tarot/Kabbalistic correspondence."""
letter: str
position: int
transliteration: str
@@ -66,17 +69,18 @@ class HebrewAlphabet:
@dataclass
class DoublLetterTrump:
"""Represents a Double Letter Trump (Yodh through Tau, 3-21 of Major Arcana)."""
number: int # 3-21 (19 double letter trumps)
name: str # Full name (e.g., "The Empress")
hebrew_letter_1: str # First Hebrew letter (e.g., "Gimel")
hebrew_letter_2: Optional[str] = None # Second Hebrew letter if applicable
planet: Optional['Planet'] = None # Associated planet
planet: Optional["Planet"] = None # Associated planet
tarot_trump: Optional[str] = None # e.g., "III - The Empress"
astrological_sign: Optional[str] = None # Zodiac sign if any
element: Optional['ElementType'] = None # Associated element
element: Optional["ElementType"] = None # Associated element
number_value: Optional[int] = None # Numerological value
keywords: List[str] = field(default_factory=list)
meaning: Optional['Meaning'] = None # Upright and reversed meanings
meaning: Optional["Meaning"] = None # Upright and reversed meanings
description: str = ""
def __post_init__(self) -> None:
@@ -87,6 +91,7 @@ class DoublLetterTrump:
@dataclass(frozen=True)
class EnochianLetter:
"""Represents an Enochian letter with its properties."""
name: str # Enochian letter name
letter: str # The letter itself
hebrew_equivalent: Optional[str] = None
@@ -100,6 +105,7 @@ class EnochianLetter:
@dataclass(frozen=True)
class EnochianSpirit:
"""Represents an Enochian spirit or intelligence."""
name: str # Spirit name
rank: str # e.g., "King", "Prince", "Duke", "Intelligence"
element: Optional[str] = None
@@ -117,15 +123,18 @@ class EnochianArchetype:
Provides a 4x4 grid with positions that can be filled with different
visual representations (colors, images, symbols, etc.).
"""
name: str # e.g., "Tablet of Air Archetype"
tablet_name: str # Reference to parent tablet
grid: Dict[Tuple[int, int], 'EnochianGridPosition'] = field(default_factory=dict) # 4x4 grid
grid: Dict[Tuple[int, int], "EnochianGridPosition"] = field(default_factory=dict) # 4x4 grid
row_correspondences: List[Dict[str, Any]] = field(default_factory=list) # Row meanings (4 rows)
col_correspondences: List[Dict[str, Any]] = field(default_factory=list) # Column meanings (4 cols)
col_correspondences: List[Dict[str, Any]] = field(
default_factory=list
) # Column meanings (4 cols)
keywords: List[str] = field(default_factory=list)
description: str = ""
def get_position(self, row: int, col: int) -> Optional['EnochianGridPosition']:
def get_position(self, row: int, col: int) -> Optional["EnochianGridPosition"]:
"""Get the grid position at (row, col)."""
if not 0 <= row < 4 or not 0 <= col < 4:
return None
@@ -154,6 +163,7 @@ class EnochianGridPosition:
- Directional letters (N, S, E, W)
- Archetypal correspondences (Tarot, element, etc.)
"""
row: int # Grid row (0-3)
col: int # Grid column (0-3)
center_letter: str # The main letter at this position
@@ -195,12 +205,15 @@ class EnochianTablet:
- Each tablet is 12x12 (144 squares) containing Enochian letters
- Archetypal form with 4x4 grid for user customization
"""
name: str # e.g., "Tablet of Earth", "Tablet of Air", etc.
number: int # Tablet identifier (1-5)
element: Optional[str] = None # Earth, Water, Air, Fire, or Aethyr/Union
rulers: List[str] = field(default_factory=list) # Names of spirits ruling this tablet
archangels: List[str] = field(default_factory=list) # Associated archangels
letters: Dict[Tuple[int, int], str] = field(default_factory=dict) # Grid of letters (row, col) -> letter
letters: Dict[Tuple[int, int], str] = field(
default_factory=dict
) # Grid of letters (row, col) -> letter
planetary_hours: List[str] = field(default_factory=list) # Associated hours
keywords: List[str] = field(default_factory=list)
description: str = ""
@@ -218,8 +231,7 @@ class EnochianTablet:
def __post_init__(self) -> None:
if self.name not in self.VALID_TABLETS:
raise ValueError(
f"Invalid tablet '{self.name}'. "
f"Valid tablets: {', '.join(self.VALID_TABLETS)}"
f"Invalid tablet '{self.name}'. " f"Valid tablets: {', '.join(self.VALID_TABLETS)}"
)
# Tablet of Union uses 0, elemental tablets use 1-5
valid_range = (0, 0) if "Union" in self.name else (1, 5)

View File

@@ -10,13 +10,12 @@ Usage:
creative = hexagram.hexagram(1)
"""
from typing import TYPE_CHECKING, Dict, Optional
from typing import TYPE_CHECKING, Dict
from utils.query import CollectionAccessor
if TYPE_CHECKING:
from tarot.card.data import CardDataLoader
from tarot.attributes import Trigram, Hexagram
from tarot.attributes import Hexagram, Trigram
def _line_diagram_from_binary(binary: str) -> str:
@@ -36,7 +35,7 @@ class _Trigram:
def __init__(self) -> None:
self._initialized: bool = False
self._trigrams: Dict[str, 'Trigram'] = {}
self._trigrams: Dict[str, "Trigram"] = {}
self.trigram = CollectionAccessor(self._get_trigrams)
def _ensure_initialized(self) -> None:
@@ -56,14 +55,78 @@ class _Trigram:
from tarot.attributes import Trigram
trigram_specs = [
{"name": "Qian", "chinese": "", "pinyin": "Qián", "element": "Heaven", "attribute": "Creative", "binary": "111", "description": "Pure yang drive that initiates action."},
{"name": "Dui", "chinese": "", "pinyin": "Duì", "element": "Lake", "attribute": "Joyous", "binary": "011", "description": "Open delight that invites community."},
{"name": "Li", "chinese": "", "pinyin": "", "element": "Fire", "attribute": "Clinging", "binary": "101", "description": "Radiant clarity that adheres to insight."},
{"name": "Zhen", "chinese": "", "pinyin": "Zhèn", "element": "Thunder", "attribute": "Arousing", "binary": "001", "description": "Sudden awakening that shakes stagnation."},
{"name": "Xun", "chinese": "", "pinyin": "Xùn", "element": "Wind", "attribute": "Gentle", "binary": "110", "description": "Penetrating influence that persuades subtly."},
{"name": "Kan", "chinese": "", "pinyin": "Kǎn", "element": "Water", "attribute": "Abysmal", "binary": "010", "description": "Depth, risk, and sincere feeling."},
{"name": "Gen", "chinese": "", "pinyin": "Gèn", "element": "Mountain", "attribute": "Stillness", "binary": "100", "description": "Grounded rest that establishes boundaries."},
{"name": "Kun", "chinese": "", "pinyin": "Kūn", "element": "Earth", "attribute": "Receptive", "binary": "000", "description": "Vast receptivity that nurtures form."},
{
"name": "Qian",
"chinese": "",
"pinyin": "Qián",
"element": "Heaven",
"attribute": "Creative",
"binary": "111",
"description": "Pure yang drive that initiates action.",
},
{
"name": "Dui",
"chinese": "",
"pinyin": "Duì",
"element": "Lake",
"attribute": "Joyous",
"binary": "011",
"description": "Open delight that invites community.",
},
{
"name": "Li",
"chinese": "",
"pinyin": "",
"element": "Fire",
"attribute": "Clinging",
"binary": "101",
"description": "Radiant clarity that adheres to insight.",
},
{
"name": "Zhen",
"chinese": "",
"pinyin": "Zhèn",
"element": "Thunder",
"attribute": "Arousing",
"binary": "001",
"description": "Sudden awakening that shakes stagnation.",
},
{
"name": "Xun",
"chinese": "",
"pinyin": "Xùn",
"element": "Wind",
"attribute": "Gentle",
"binary": "110",
"description": "Penetrating influence that persuades subtly.",
},
{
"name": "Kan",
"chinese": "",
"pinyin": "Kǎn",
"element": "Water",
"attribute": "Abysmal",
"binary": "010",
"description": "Depth, risk, and sincere feeling.",
},
{
"name": "Gen",
"chinese": "",
"pinyin": "Gèn",
"element": "Mountain",
"attribute": "Stillness",
"binary": "100",
"description": "Grounded rest that establishes boundaries.",
},
{
"name": "Kun",
"chinese": "",
"pinyin": "Kūn",
"element": "Earth",
"attribute": "Receptive",
"binary": "000",
"description": "Vast receptivity that nurtures form.",
},
]
self._trigrams = {}
for spec in trigram_specs:
@@ -88,7 +151,7 @@ class _Hexagram:
def __init__(self) -> None:
self._initialized: bool = False
self._hexagrams: Dict[int, 'Hexagram'] = {}
self._hexagrams: Dict[int, "Hexagram"] = {}
self.hexagram = CollectionAccessor(self._get_hexagrams)
def _ensure_initialized(self) -> None:
@@ -115,70 +178,710 @@ class _Hexagram:
loader = CardDataLoader()
hex_specs = [
{"number": 1, "name": "Creative Force", "chinese": "", "pinyin": "Qián", "judgement": "Initiative succeeds when anchored in integrity.", "image": "Heaven above and below mirrors unstoppable drive.", "upper": "Qian", "lower": "Qian", "keywords": "Leadership|Momentum|Clarity"},
{"number": 2, "name": "Receptive Field", "chinese": "", "pinyin": "Kūn", "judgement": "Grounded support flourishes through patience.", "image": "Earth layered upon earth offers fertile space.", "upper": "Kun", "lower": "Kun", "keywords": "Nurture|Support|Yielding"},
{"number": 3, "name": "Sprouting", "chinese": "", "pinyin": "Zhūn", "judgement": "Challenges at the start need perseverance.", "image": "Water over thunder shows storms that germinate seeds.", "upper": "Kan", "lower": "Zhen", "keywords": "Beginnings|Struggle|Resolve"},
{"number": 4, "name": "Youthful Insight", "chinese": "", "pinyin": "Méng", "judgement": "Ignorance yields to steady guidance.", "image": "Mountain above water signals learning via restraint.", "upper": "Gen", "lower": "Kan", "keywords": "Study|Mentorship|Humility"},
{"number": 5, "name": "Waiting", "chinese": "", "pinyin": "", "judgement": "Hold position until nourishment arrives.", "image": "Water above heaven depicts clouds gathering provision.", "upper": "Kan", "lower": "Qian", "keywords": "Patience|Faith|Preparation"},
{"number": 6, "name": "Conflict", "chinese": "", "pinyin": "Sòng", "judgement": "Clarity and fairness prevent escalation.", "image": "Heaven above water shows tension seeking balance.", "upper": "Qian", "lower": "Kan", "keywords": "Debate|Justice|Boundaries"},
{"number": 7, "name": "Collective Force", "chinese": "", "pinyin": "Shī", "judgement": "Coordinated effort requires disciplined leadership.", "image": "Earth over water mirrors troops marshaling supplies.", "upper": "Kun", "lower": "Kan", "keywords": "Discipline|Leadership|Community"},
{"number": 8, "name": "Union", "chinese": "", "pinyin": "", "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"},
{
"number": 1,
"name": "Creative Force",
"chinese": "",
"pinyin": "Qián",
"judgement": "Initiative succeeds when anchored in integrity.",
"image": "Heaven above and below mirrors unstoppable drive.",
"upper": "Qian",
"lower": "Qian",
"keywords": "Leadership|Momentum|Clarity",
},
{
"number": 2,
"name": "Receptive Field",
"chinese": "",
"pinyin": "Kūn",
"judgement": "Grounded support flourishes through patience.",
"image": "Earth layered upon earth offers fertile space.",
"upper": "Kun",
"lower": "Kun",
"keywords": "Nurture|Support|Yielding",
},
{
"number": 3,
"name": "Sprouting",
"chinese": "",
"pinyin": "Zhūn",
"judgement": "Challenges at the start need perseverance.",
"image": "Water over thunder shows storms that germinate seeds.",
"upper": "Kan",
"lower": "Zhen",
"keywords": "Beginnings|Struggle|Resolve",
},
{
"number": 4,
"name": "Youthful Insight",
"chinese": "",
"pinyin": "Méng",
"judgement": "Ignorance yields to steady guidance.",
"image": "Mountain above water signals learning via restraint.",
"upper": "Gen",
"lower": "Kan",
"keywords": "Study|Mentorship|Humility",
},
{
"number": 5,
"name": "Waiting",
"chinese": "",
"pinyin": "",
"judgement": "Hold position until nourishment arrives.",
"image": "Water above heaven depicts clouds gathering provision.",
"upper": "Kan",
"lower": "Qian",
"keywords": "Patience|Faith|Preparation",
},
{
"number": 6,
"name": "Conflict",
"chinese": "",
"pinyin": "Sòng",
"judgement": "Clarity and fairness prevent escalation.",
"image": "Heaven above water shows tension seeking balance.",
"upper": "Qian",
"lower": "Kan",
"keywords": "Debate|Justice|Boundaries",
},
{
"number": 7,
"name": "Collective Force",
"chinese": "",
"pinyin": "Shī",
"judgement": "Coordinated effort requires disciplined leadership.",
"image": "Earth over water mirrors troops marshaling supplies.",
"upper": "Kun",
"lower": "Kan",
"keywords": "Discipline|Leadership|Community",
},
{
"number": 8,
"name": "Union",
"chinese": "",
"pinyin": "",
"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"]
self._hexagrams = {}

View File

@@ -14,6 +14,7 @@ from utils.attributes import Number, Planet
@dataclass
class Trigram:
"""Represents one of the eight I Ching trigrams."""
name: str
chinese_name: str
pinyin: str
@@ -27,6 +28,7 @@ class Trigram:
@dataclass
class Hexagram:
"""Represents an I Ching hexagram with Tarot correspondence."""
number: int
name: str
chinese_name: str

View File

@@ -13,7 +13,7 @@ class Letter:
def __init__(self) -> None:
self._initialized: bool = False
self._loader: 'CardDataLoader | None' = None
self._loader: "CardDataLoader | None" = None
self.alphabet = CollectionAccessor(self._get_alphabets)
self.cipher = CollectionAccessor(self._get_ciphers)
self.letter = CollectionAccessor(self._get_letters)
@@ -26,10 +26,11 @@ class Letter:
return
from tarot.card.data import CardDataLoader
self._loader = CardDataLoader()
self._initialized = True
def _require_loader(self) -> 'CardDataLoader':
def _require_loader(self) -> "CardDataLoader":
self._ensure_initialized()
assert self._loader is not None, "Loader not initialized"
return self._loader
@@ -54,7 +55,7 @@ class Letter:
loader = self._require_loader()
return loader._periodic_table.copy()
def word(self, text: str, *, alphabet: str = 'english'):
def word(self, text: str, *, alphabet: str = "english"):
"""
Start a fluent cipher request for the given text.

View File

@@ -17,13 +17,14 @@ Each letter has attributes like:
- Musical Note
"""
from typing import List, Optional, Dict, Union, TYPE_CHECKING
from dataclasses import dataclass, field
from utils.filter import universal_filter, get_filterable_fields, format_results
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from utils.filter import format_results, get_filterable_fields, universal_filter
if TYPE_CHECKING:
from utils.query import CollectionAccessor
from tarot.attributes import Path
from utils.query import CollectionAccessor
@dataclass
@@ -34,7 +35,8 @@ class TarotLetter:
Wraps Path objects from CardDataLoader to provide a letter-focused interface
while maintaining a single source of truth.
"""
path: 'Path' # Reference to the actual Path object from CardDataLoader
path: "Path" # Reference to the actual Path object from CardDataLoader
letter_type: str # "Mother", "Double", or "Simple" (derived from path)
def __post_init__(self) -> None:
@@ -149,7 +151,7 @@ class LetterAccessor:
def by_type(self, letter_type: str) -> List[TarotLetter]:
"""Filter by type: 'Mother', 'Double', or 'Simple'."""
return [l for l in self._letters.values() if l.letter_type == letter_type]
return [letter for letter in self._letters.values() if letter.letter_type == letter_type]
def by_zodiac(self, zodiac: str) -> Optional[TarotLetter]:
"""Get letter by zodiac sign."""
@@ -160,11 +162,15 @@ class LetterAccessor:
def by_planet(self, planet: str) -> List[TarotLetter]:
"""Get letters by planet."""
return [l for l in self._letters.values() if l.planet and planet.lower() in l.planet.lower()]
return [
letter
for letter in self._letters.values()
if letter.planet and planet.lower() in letter.planet.lower()
]
def by_trump(self, trump: str) -> Optional[TarotLetter]:
"""Get letter by tarot trump."""
return next((l for l in self._letters.values() if l.trump == trump), None)
return next((letter for letter in self._letters.values() if letter.trump == trump), None)
def get_filterable_fields(self) -> List[str]:
"""
@@ -265,12 +271,13 @@ class IChing:
all_hex = hexagrams.list()
"""
trigram: 'CollectionAccessor'
hexagram: 'CollectionAccessor'
trigram: "CollectionAccessor"
hexagram: "CollectionAccessor"
def __init__(self) -> None:
"""Initialize iChing accessor with trigram and hexagram collections."""
from tarot.letter import iChing as iching_module
self.trigram = iching_module.trigram.trigram
self.hexagram = iching_module.hexagram.hexagram
@@ -286,7 +293,7 @@ class IChing:
class LettersRegistry:
"""Registry and accessor for all Hebrew letters with Tarot correspondences."""
_instance: Optional['LettersRegistry'] = None
_instance: Optional["LettersRegistry"] = None
_letters: Dict[str, TarotLetter] = {}
_initialized: bool = False

View File

@@ -9,7 +9,7 @@ if TYPE_CHECKING:
class _Word:
"""Fluent accessor for word analysis and cipher operations."""
_loader: 'CardDataLoader | None' = None
_loader: "CardDataLoader | None" = None
_initialized: bool = False
@classmethod
@@ -19,11 +19,12 @@ class _Word:
return
from tarot.card.data import CardDataLoader
cls._loader = CardDataLoader()
cls._initialized = True
@classmethod
def word(cls, text: str, *, alphabet: str = 'english'):
def word(cls, text: str, *, alphabet: str = "english"):
"""
Start a fluent cipher request for the given text.

View File

@@ -14,6 +14,6 @@ Usage:
colors = number.color()
"""
from .number import number, calculate_digital_root
from .number import calculate_digital_root, number
__all__ = ["number", "calculate_digital_root"]

View File

@@ -1,8 +1,12 @@
"""Numbers loader - access to numerology and number correspondences."""
from typing import Dict, Optional, Union, overload
from typing import TYPE_CHECKING, Dict, Optional, Union, overload
from utils.filter import universal_filter
if TYPE_CHECKING:
from utils.attributes import Color, Number
def calculate_digital_root(value: int) -> int:
"""
@@ -46,8 +50,8 @@ class Numbers:
"""
# These are populated on first access from CardDataLoader
_numbers: Dict[int, 'Number'] = {} # type: ignore
_colors: Dict[int, 'Color'] = {} # type: ignore
_numbers: Dict[int, "Number"] = {} # type: ignore
_colors: Dict[int, "Color"] = {} # type: ignore
_initialized: bool = False
@classmethod
@@ -57,6 +61,7 @@ class Numbers:
return
from tarot.card.data import CardDataLoader
loader = CardDataLoader()
cls._numbers = loader.number()
cls._colors = loader.color()
@@ -64,16 +69,14 @@ class Numbers:
@classmethod
@overload
def number(cls, value: int) -> Optional['Number']:
...
def number(cls, value: int) -> Optional["Number"]: ...
@classmethod
@overload
def number(cls, value: None = ...) -> Dict[int, 'Number']:
...
def number(cls, value: None = ...) -> Dict[int, "Number"]: ...
@classmethod
def number(cls, value: Optional[int] = None) -> Union[Optional['Number'], Dict[int, 'Number']]:
def number(cls, value: Optional[int] = None) -> Union[Optional["Number"], Dict[int, "Number"]]:
"""Return an individual Number or the full numerology table."""
cls._ensure_initialized()
if value is None:
@@ -82,16 +85,16 @@ class Numbers:
@classmethod
@overload
def color(cls, sephera_number: int) -> Optional['Color']:
...
def color(cls, sephera_number: int) -> Optional["Color"]: ...
@classmethod
@overload
def color(cls, sephera_number: None = ...) -> Dict[int, 'Color']:
...
def color(cls, sephera_number: None = ...) -> Dict[int, "Color"]: ...
@classmethod
def color(cls, sephera_number: Optional[int] = None) -> Union[Optional['Color'], Dict[int, 'Color']]:
def color(
cls, sephera_number: Optional[int] = None
) -> Union[Optional["Color"], Dict[int, "Color"]]:
"""Return a single color correspondence or the entire map."""
cls._ensure_initialized()
if sephera_number is None:
@@ -99,13 +102,13 @@ class Numbers:
return cls._colors.get(sephera_number)
@classmethod
def color_by_number(cls, number: int) -> Optional['Color']:
def color_by_number(cls, number: int) -> Optional["Color"]:
"""Get a Color by mapping a number through digital root."""
root = calculate_digital_root(number)
return cls.color(root)
@classmethod
def number_by_digital_root(cls, value: int) -> Optional['Number']:
def number_by_digital_root(cls, value: int) -> Optional["Number"]:
"""Get a Number object using digital root calculation."""
root = calculate_digital_root(value)
return cls.number(root)
@@ -153,6 +156,7 @@ class Numbers:
print(Numbers.display_filter_numbers(element="Fire"))
"""
from utils.filter import format_results
results = cls.filter_numbers(**kwargs)
return format_results(results)
@@ -194,5 +198,6 @@ class Numbers:
print(Numbers.display_filter_colors(element="Water"))
"""
from utils.filter import format_results
results = cls.filter_colors(**kwargs)
return format_results(results)

View File

@@ -29,7 +29,7 @@ class _Number:
def __init__(self) -> None:
self._initialized: bool = False
self._loader: 'CardDataLoader | None' = None
self._loader: "CardDataLoader | None" = None
self.number = CollectionAccessor(self._get_numbers)
self.color = CollectionAccessor(self._get_colors)
self.cipher = CollectionAccessor(self._get_ciphers)
@@ -40,10 +40,11 @@ class _Number:
return
from tarot.card.data import CardDataLoader
self._loader = CardDataLoader()
self._initialized = True
def _require_loader(self) -> 'CardDataLoader':
def _require_loader(self) -> "CardDataLoader":
self._ensure_initialized()
assert self._loader is not None, "Loader not initialized"
return self._loader

View File

@@ -23,44 +23,76 @@ Usage:
card = Tarot.deck.card(3)
"""
from .deck import Deck, Card, MajorCard, MinorCard, DLT
from .attributes import (
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter,
Sephera, PeriodicTable, Degree, AstrologicalInfluence,
TreeOfLife, Correspondences, CardImage, DoublLetterTrump,
EnglishAlphabet, GreekAlphabet, HebrewAlphabet,
Trigram, Hexagram,
EnochianTablet, EnochianGridPosition, EnochianArchetype, Path,
)
import kaballah
from kaballah import Cube, Tree
from kaballah.cube.attributes import CubeOfSpace, Wall, WallDirection
# Import from namespace folders
from letter import hexagram, letter, trigram
from number import calculate_digital_root, number
from temporal import PlanetPosition, ThalemaClock
from temporal import Zodiac as AstrologyZodiac
# Import shared attributes from utils
from utils.attributes import (
Note, Element, ElementType, Number, Color, Colorscale,
Planet, God, Cipher, CipherResult, Perfume,
Cipher,
CipherResult,
Color,
Colorscale,
Element,
ElementType,
God,
Note,
Number,
Perfume,
Planet,
)
from .attributes import (
AstrologicalInfluence,
CardImage,
ClockHour,
Correspondences,
Day,
Degree,
DoublLetterTrump,
EnglishAlphabet,
EnochianArchetype,
EnochianGridPosition,
EnochianTablet,
GreekAlphabet,
HebrewAlphabet,
Hexagram,
Hour,
Letter,
Meaning,
Month,
Path,
PeriodicTable,
Sephera,
Suit,
TreeOfLife,
Trigram,
Weekday,
Zodiac,
)
from kaballah.cube.attributes import CubeOfSpace, WallDirection, Wall
from .card.data import CardDataLoader, calculate_digital_root
from .tarot_api import Tarot
# Import from card module (includes details, loader, and image_loader)
from .card import (
CardAccessor,
CardDetailsRegistry,
ImageDeckLoader,
filter_cards_by_keywords,
get_card_info,
get_cards_by_suit,
load_card_details,
load_deck_details,
get_cards_by_suit,
filter_cards_by_keywords,
print_card_details,
get_card_info,
ImageDeckLoader,
load_deck_images,
print_card_details,
)
# Import from namespace folders
from letter import letter, trigram, hexagram
from number import number, calculate_digital_root
import kaballah
from kaballah import Tree, Cube
from temporal import ThalemaClock, Zodiac as AstrologyZodiac, PlanetPosition
from .card.data import CardDataLoader
from .deck import DLT, Card, Deck, MajorCard, MinorCard
from .tarot_api import Tarot
def display(obj):
@@ -76,7 +108,8 @@ def display(obj):
display(num) # Shows all attributes nicely formatted
"""
from dataclasses import fields
if hasattr(obj, '__dataclass_fields__'):
if hasattr(obj, "__dataclass_fields__"):
# It's a dataclass - show all fields
print(f"{obj.__class__.__name__}:")
for field in fields(obj):
@@ -96,12 +129,10 @@ __all__ = [
"Tarot",
"trigram",
"hexagram",
# Temporal and astrological
"ThalemaClock",
"AstrologyZodiac",
"PlanetPosition",
# Card details and loading
"CardDetailsRegistry",
"load_card_details",
@@ -110,24 +141,20 @@ __all__ = [
"filter_cards_by_keywords",
"print_card_details",
"get_card_info",
# Image loading
"ImageDeckLoader",
"load_deck_images",
# Utilities
"display",
"CardAccessor",
"Tree",
"Cube",
# Deck classes
"Deck",
"Card",
"MajorCard",
"MinorCard",
"DLT",
# Calendar/attribute classes
"Month",
"Day",
@@ -142,7 +169,6 @@ __all__ = [
"CubeOfSpace",
"WallDirection",
"Wall",
# Sepheric classes
"Sephera",
"PeriodicTable",
@@ -157,12 +183,10 @@ __all__ = [
"EnochianTablet",
"EnochianGridPosition",
"EnochianArchetype",
# Alphabet classes
"EnglishAlphabet",
"GreekAlphabet",
"HebrewAlphabet",
# Number and color classes
"Number",
"Color",
@@ -172,7 +196,6 @@ __all__ = [
"Hexagram",
"Cipher",
"CipherResult",
# Data loader and functions
"CardDataLoader",
"calculate_digital_root",

View File

@@ -8,54 +8,54 @@ attribute classes for cards.
from dataclasses import dataclass
from typing import Optional
# Re-export shared attributes from utils
from utils.attributes import (
Element,
ElementType,
Number,
Color,
Colorscale,
Planet,
God,
Cipher,
CipherResult,
Perfume,
Note,
Meaning,
)
# Re-export attributes from other modules for convenience/backward compatibility
from kaballah.attributes import (
Sephera,
PeriodicTable,
TreeOfLife,
Correspondences,
Path,
PeriodicTable,
Sephera,
TreeOfLife,
)
from letter.attributes import (
Letter,
EnglishAlphabet,
GreekAlphabet,
HebrewAlphabet,
DoublLetterTrump,
EnglishAlphabet,
EnochianArchetype,
EnochianGridPosition,
EnochianLetter,
EnochianSpirit,
EnochianTablet,
EnochianGridPosition,
EnochianArchetype,
GreekAlphabet,
HebrewAlphabet,
Letter,
)
from letter.iChing_attributes import (
Trigram,
Hexagram,
Trigram,
)
from temporal.attributes import (
AstrologicalInfluence,
ClockHour,
Degree,
Hour,
Month,
Weekday,
Hour,
ClockHour,
Zodiac,
Degree,
AstrologicalInfluence,
)
# Re-export shared attributes from utils
from utils.attributes import (
Cipher,
CipherResult,
Color,
Colorscale,
Element,
ElementType,
God,
Meaning,
Note,
Number,
Perfume,
Planet,
)
# Alias Day to Weekday for backward compatibility (Day in this context was Day of Week)
@@ -113,8 +113,9 @@ __all__ = [
@dataclass
class Suit:
"""Represents a tarot suit."""
name: str
element: 'ElementType'
element: "ElementType"
tarot_correspondence: str
number: int
@@ -122,8 +123,8 @@ class Suit:
@dataclass
class CardImage:
"""Represents an image associated with a card."""
filename: str
artist: str
deck_name: str
url: Optional[str] = None

View File

@@ -2,15 +2,15 @@
from .card import CardAccessor
from .details import CardDetailsRegistry
from .image_loader import ImageDeckLoader, load_deck_images
from .loader import (
filter_cards_by_keywords,
get_card_info,
get_cards_by_suit,
load_card_details,
load_deck_details,
get_cards_by_suit,
filter_cards_by_keywords,
print_card_details,
get_card_info,
)
from .image_loader import ImageDeckLoader, load_deck_images
__all__ = [
"CardAccessor",

View File

@@ -13,9 +13,13 @@ Usage:
cards = Deck.card.filter(arcana="Minor", suit="Wands", pip=5) # 5 of Wands
"""
from typing import List, Optional
from utils.filter import universal_filter, format_results
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
from typing import TYPE_CHECKING, List, Optional
from utils.filter import format_results, universal_filter
from utils.object_formatting import format_value, get_object_attributes, is_nested_object
if TYPE_CHECKING:
from tarot.deck import Card, Deck
class CardList(list):
@@ -32,7 +36,7 @@ class CardList(list):
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.
@@ -42,12 +46,10 @@ def _format_cards(cards: List['Card']) -> str:
Returns:
Formatted string with each card separated by blank lines
"""
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
lines = []
for card in cards:
card_num = getattr(card, 'number', '?')
card_name = getattr(card, 'name', 'Unknown')
card_num = getattr(card, "number", "?")
card_name = getattr(card, "name", "Unknown")
lines.append(f"--- {card_num}: {card_name} ---")
# Format all attributes with proper nesting
@@ -78,17 +80,18 @@ class CardAccessor:
Tarot.deck.card.display_filter(arcana="Major") # Display Major Arcana
"""
_deck: Optional['Deck'] = None
_deck: Optional["Deck"] = None
_initialized: bool = False
def _ensure_initialized(self) -> None:
"""Lazy-load the Deck on first access."""
if not self._initialized:
from tarot.deck import Deck as DeckClass
CardAccessor._deck = DeckClass()
CardAccessor._initialized = True
def __call__(self, number: int) -> Optional['Card']:
def __call__(self, number: int) -> Optional["Card"]:
"""Get a card by number."""
self._ensure_initialized()
if self._deck is None:
@@ -177,7 +180,9 @@ class CardAccessor:
fool = next((c for c in major_arcana if c.number == 0), None)
world = next((c for c in major_arcana if c.number == 21), None)
if fool and world:
lines.append(f" Special Pair: {fool.name} ({fool.number}) - {world.name} ({world.number})")
lines.append(
f" Special Pair: {fool.name} ({fool.number}) - {world.name} ({world.number})"
)
double_letter_trumps = [c for c in major_arcana if 3 <= c.number <= 21]
lines.append(f" Double Letter Trumps ({len(double_letter_trumps)} cards): Cards 3-21")
@@ -189,34 +194,34 @@ class CardAccessor:
lines.append("")
# Aces
aces = [c for c in minor_arcana if hasattr(c, 'pip') and c.pip == 1]
aces = [c for c in minor_arcana if hasattr(c, "pip") and c.pip == 1]
if aces:
lines.append(f" ACES ({len(aces)} cards - The Root Powers):")
for ace in aces:
suit_name = ace.suit.name if hasattr(ace.suit, 'name') else str(ace.suit)
suit_name = ace.suit.name if hasattr(ace.suit, "name") else str(ace.suit)
lines.append(f" Ace of {suit_name}")
lines.append("")
# Pips (2-10)
pips = [c for c in minor_arcana if hasattr(c, 'pip') and 2 <= c.pip <= 10]
pips = [c for c in minor_arcana if hasattr(c, "pip") and 2 <= c.pip <= 10]
if pips:
lines.append(f" PIPS ({len(pips)} cards - 2-10 of each suit):")
# Group by suit
suits_dict = {}
for pip in pips:
suit_name = pip.suit.name if hasattr(pip.suit, 'name') else str(pip.suit)
suit_name = pip.suit.name if hasattr(pip.suit, "name") else str(pip.suit)
if suit_name not in suits_dict:
suits_dict[suit_name] = []
suits_dict[suit_name].append(pip)
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
for suit_name in ["Cups", "Pentacles", "Swords", "Wands"]:
if suit_name in suits_dict:
pip_nums = sorted([p.pip for p in suits_dict[suit_name]])
lines.append(f" {suit_name}: {', '.join(str(n) for n in pip_nums)}")
lines.append("")
# Court Cards
courts = [c for c in minor_arcana if hasattr(c, 'court_rank') and c.court_rank]
courts = [c for c in minor_arcana if hasattr(c, "court_rank") and c.court_rank]
if courts:
lines.append(f" COURT CARDS ({len(courts)} cards - 4 ranks × 4 suits):")
# Get unique ranks and their order
@@ -227,15 +232,16 @@ class CardAccessor:
# Group by suit
suits_dict = {}
for court in courts:
suit_name = court.suit.name if hasattr(court.suit, 'name') else str(court.suit)
suit_name = court.suit.name if hasattr(court.suit, "name") else str(court.suit)
if suit_name not in suits_dict:
suits_dict[suit_name] = []
suits_dict[suit_name].append(court)
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
for suit_name in ["Cups", "Pentacles", "Swords", "Wands"]:
if suit_name in suits_dict:
suit_courts = sorted(suits_dict[suit_name],
key=lambda c: rank_order.get(c.court_rank, 99))
suit_courts = sorted(
suits_dict[suit_name], key=lambda c: rank_order.get(c.court_rank, 99)
)
court_names = [c.court_rank for c in suit_courts]
lines.append(f" {suit_name}: {', '.join(court_names)}")
lines.append("")
@@ -244,45 +250,44 @@ class CardAccessor:
lines.append("SUIT CORRESPONDENCES:")
suits_info = {}
for card in minor_arcana:
if hasattr(card, 'suit') and card.suit:
suit_name = card.suit.name if hasattr(card.suit, 'name') else str(card.suit)
if hasattr(card, "suit") and card.suit:
suit_name = card.suit.name if hasattr(card.suit, "name") else str(card.suit)
if suit_name not in suits_info:
# Extract element info
element_name = "Unknown"
if hasattr(card.suit, 'element') and card.suit.element:
if hasattr(card.suit.element, 'name'):
if hasattr(card.suit, "element") and card.suit.element:
if hasattr(card.suit.element, "name"):
element_name = card.suit.element.name
else:
element_name = str(card.suit.element)
# Extract zodiac signs
zodiac_signs = []
if hasattr(card.suit, 'element') and card.suit.element:
if hasattr(card.suit.element, 'zodiac_signs'):
if hasattr(card.suit, "element") and card.suit.element:
if hasattr(card.suit.element, "zodiac_signs"):
zodiac_signs = card.suit.element.zodiac_signs
# Extract keywords
keywords = []
if hasattr(card.suit, 'element') and card.suit.element:
if hasattr(card.suit.element, 'keywords'):
if hasattr(card.suit, "element") and card.suit.element:
if hasattr(card.suit.element, "keywords"):
keywords = card.suit.element.keywords
suits_info[suit_name] = {
'element': element_name,
'zodiac': zodiac_signs,
'keywords': keywords
"element": element_name,
"zodiac": zodiac_signs,
"keywords": keywords,
}
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
for suit_name in ["Cups", "Pentacles", "Swords", "Wands"]:
if suit_name in suits_info:
info = suits_info[suit_name]
lines.append(f" {suit_name} ({info['element']}):")
if info['zodiac']:
if info["zodiac"]:
lines.append(f" Zodiac: {', '.join(info['zodiac'])}")
if info['keywords']:
if info["keywords"]:
lines.append(f" Keywords: {', '.join(info['keywords'])}")
lines.append("")
lines.append(f"Total: {len(self._deck.cards)} cards")
@@ -315,7 +320,7 @@ class CardAccessor:
print(Tarot.deck.card.spread('three card'))
print(Tarot.deck.card.spread('tree of life'))
"""
from tarot.card.spread import Spread, draw_spread, SpreadReading
from tarot.card.spread import Spread, SpreadReading, draw_spread
# Initialize deck if needed
self._ensure_initialized()

File diff suppressed because it is too large Load Diff

View File

@@ -64,19 +64,9 @@ class CardDetailsRegistry:
if key == 0:
return "o"
val = [
1000, 900, 500, 400,
100, 90, 50, 40,
10, 9, 5, 4,
1
]
syms = [
"M", "CM", "D", "CD",
"C", "XC", "L", "XL",
"X", "IX", "V", "IV",
"I"
]
roman_num = ''
val = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
roman_num = ""
i = 0
while key > 0:
for _ in range(key // val[i]):
@@ -120,7 +110,7 @@ class CardDetailsRegistry:
"o": {
"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.",
"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.",
"keywords": ["new beginnings", "innocence", "faith", "spontaneity", "potential"],
@@ -130,137 +120,261 @@ class CardDetailsRegistry:
"I": {
"explanation": {
"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.",
"keywords": ["manifestation", "resourcefulness", "power", "inspired action", "concentration"],
"reversed_keywords": ["manipulation", "poor planning", "untapped talents", "lack of direction"],
"keywords": [
"manifestation",
"resourcefulness",
"power",
"inspired action",
"concentration",
],
"reversed_keywords": [
"manipulation",
"poor planning",
"untapped talents",
"lack of direction",
],
"guidance": "Focus your energy and intention on what you want to manifest. You have the tools and talents you need.",
},
"II": {
"explanation": {
"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.",
"keywords": ["intuition", "sacred knowledge", "divine feminine", "the subconscious", "mystery"],
"reversed_keywords": ["hidden information", "silence", "disconnection from intuition", "superficiality"],
"keywords": [
"intuition",
"sacred knowledge",
"divine feminine",
"the subconscious",
"mystery",
],
"reversed_keywords": [
"hidden information",
"silence",
"disconnection from intuition",
"superficiality",
],
"guidance": "Listen to your inner voice. The answers you seek lie within. Trust the wisdom of your intuition.",
},
"III": {
"explanation": {
"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.",
"keywords": ["abundance", "fertility", "femininity", "beauty", "nature", "creativity"],
"reversed_keywords": ["dependency", "creative block", "neediness", "underdevelopment"],
"keywords": [
"abundance",
"fertility",
"femininity",
"beauty",
"nature",
"creativity",
],
"reversed_keywords": [
"dependency",
"creative block",
"neediness",
"underdevelopment",
],
"guidance": "Nurture yourself and others. Allow yourself to enjoy the fruits of your labor and appreciate beauty.",
},
"IV": {
"explanation": {
"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.",
"keywords": ["authority", "leadership", "power", "structure", "protection", "discipline"],
"reversed_keywords": ["weakness", "ineffectual leadership", "lack of discipline", "tyranny"],
"keywords": [
"authority",
"leadership",
"power",
"structure",
"protection",
"discipline",
],
"reversed_keywords": [
"weakness",
"ineffectual leadership",
"lack of discipline",
"tyranny",
],
"guidance": "Step into your power with confidence. Establish clear boundaries and structure. Lead by example.",
},
"V": {
"explanation": {
"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.",
"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.",
},
"VI": {
"explanation": {
"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.",
"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.",
},
"VII": {
"explanation": {
"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.",
"keywords": ["determination", "willpower", "control", "momentum", "victory", "focus"],
"keywords": [
"determination",
"willpower",
"control",
"momentum",
"victory",
"focus",
],
"reversed_keywords": ["lack of control", "haste", "resistance", "moving backward"],
"guidance": "Take the reins of your life. Move forward with determination and clear direction. You have the power.",
},
"VIII": {
"explanation": {
"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.",
"keywords": ["strength", "courage", "patience", "compassion", "control", "confidence"],
"reversed_keywords": ["weakness", "self-doubt", "lack of composure", "poor control"],
"keywords": [
"strength",
"courage",
"patience",
"compassion",
"control",
"confidence",
],
"reversed_keywords": [
"weakness",
"self-doubt",
"lack of composure",
"poor control",
],
"guidance": "True strength comes from within. Face challenges with courage and compassion for yourself and others.",
},
"IX": {
"explanation": {
"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.",
"keywords": ["introspection", "spiritual seeking", "inner light", "wisdom", "solitude", "truth"],
"reversed_keywords": ["loneliness", "isolation", "lost", "paranoia", "disconnection"],
"keywords": [
"introspection",
"spiritual seeking",
"inner light",
"wisdom",
"solitude",
"truth",
],
"reversed_keywords": [
"loneliness",
"isolation",
"lost",
"paranoia",
"disconnection",
],
"guidance": "Take time for introspection and self-discovery. Your inner light guides your path. Seek solitude for wisdom.",
},
"X": {
"explanation": {
"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.",
"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.",
},
"XI": {
"explanation": {
"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.",
"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"],
"guidance": "Seek the truth and act with fairness. Take responsibility for your actions. Balance is key.",
},
"XII": {
"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.",
"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.",
"keywords": ["suspension", "restriction", "letting go", "new perspective", "surrender", "pause"],
"reversed_keywords": ["resistance", "stalling", "unwillingness to change", "impatience"],
"keywords": [
"suspension",
"restriction",
"letting go",
"new perspective",
"surrender",
"pause",
],
"reversed_keywords": [
"resistance",
"stalling",
"unwillingness to change",
"impatience",
],
"guidance": "Pause and reflect. What are you holding onto? Surrender control and trust the process.",
},
"XIII": {
"explanation": {
"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.",
"keywords": ["transformation", "transition", "endings", "beginnings", "change", "acceptance"],
"reversed_keywords": ["resistance to change", "stagnation", "missed opportunity", "delay"],
"keywords": [
"transformation",
"transition",
"endings",
"beginnings",
"change",
"acceptance",
],
"reversed_keywords": [
"resistance to change",
"stagnation",
"missed opportunity",
"delay",
],
"guidance": "Release what no longer serves you. Transformation is inevitable. Trust in the cycle of death and rebirth.",
},
"XIV": {
"explanation": {
"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.",
"keywords": ["balance", "moderation", "harmony", "patience", "timing", "peace"],
@@ -270,47 +384,84 @@ class CardDetailsRegistry:
"XV": {
"explanation": {
"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.",
"keywords": ["bondage", "materialism", "playfulness", "shadow self", "sexuality", "excess"],
"keywords": [
"bondage",
"materialism",
"playfulness",
"shadow self",
"sexuality",
"excess",
],
"reversed_keywords": ["freedom", "detachment", "reclaiming power", "breaking free"],
"guidance": "Examine what binds you. Acknowledge your shadow. You hold the key to your own freedom.",
},
"XVI": {
"explanation": {
"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.",
"keywords": ["sudden change", "upheaval", "revelation", "breakdown", "breakthrough", "chaos"],
"reversed_keywords": ["resistance to change", "averted crisis", "delay", "stagnation"],
"keywords": [
"sudden change",
"upheaval",
"revelation",
"breakdown",
"breakthrough",
"chaos",
],
"reversed_keywords": [
"resistance to change",
"averted crisis",
"delay",
"stagnation",
],
"guidance": "Crisis brings clarity. Though change is sudden and jarring, it clears away the false and brings truth.",
},
"XVII": {
"explanation": {
"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.",
"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.",
},
"XVIII": {
"explanation": {
"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.",
"keywords": ["illusion", "intuition", "uncertainty", "subconscious", "dreams", "mystery"],
"reversed_keywords": ["clarity", "truth revealed", "release from illusion", "awakening"],
"keywords": [
"illusion",
"intuition",
"uncertainty",
"subconscious",
"dreams",
"mystery",
],
"reversed_keywords": [
"clarity",
"truth revealed",
"release from illusion",
"awakening",
],
"guidance": "Trust your intuition to navigate mystery. What appears illusory contains deeper truths worth exploring.",
},
"XIX": {
"explanation": {
"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.",
"keywords": ["success", "joy", "clarity", "vitality", "warmth", "authenticity"],
@@ -320,29 +471,47 @@ class CardDetailsRegistry:
"XX": {
"explanation": {
"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.",
"keywords": ["awakening", "calling", "judgment", "rebirth", "evaluation", "absolution"],
"reversed_keywords": ["doubt", "self-doubt", "harsh judgment", "reluctance to change"],
"keywords": [
"awakening",
"calling",
"judgment",
"rebirth",
"evaluation",
"absolution",
],
"reversed_keywords": [
"doubt",
"self-doubt",
"harsh judgment",
"reluctance to change",
],
"guidance": "Answer your higher calling. Evaluate with compassion. A significant awakening or decision awaits.",
},
"XXI": {
"explanation": {
"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.",
"keywords": ["completion", "fulfillment", "wholeness", "travel", "unity", "achievement"],
"keywords": [
"completion",
"fulfillment",
"wholeness",
"travel",
"unity",
"achievement",
],
"reversed_keywords": ["incomplete", "blocked", "separation", "seeking closure"],
"guidance": "A significant cycle completes. You have achieved wholeness. Yet every ending is a new beginning.",
},
# Minor Arcana - Swords
"Ace of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -352,7 +521,7 @@ class CardDetailsRegistry:
"Two of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -362,7 +531,7 @@ class CardDetailsRegistry:
"Three of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -372,7 +541,7 @@ class CardDetailsRegistry:
"Four of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -382,7 +551,7 @@ class CardDetailsRegistry:
"Five of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -392,7 +561,7 @@ class CardDetailsRegistry:
"Six of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -402,7 +571,7 @@ class CardDetailsRegistry:
"Seven of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -412,7 +581,7 @@ class CardDetailsRegistry:
"Eight of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -422,7 +591,7 @@ class CardDetailsRegistry:
"Nine of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -432,7 +601,7 @@ class CardDetailsRegistry:
"Ten of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -442,7 +611,7 @@ class CardDetailsRegistry:
"Page of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -452,7 +621,7 @@ class CardDetailsRegistry:
"Knight of Swords": {
"explanation": {
"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": "",
"keywords": [],
@@ -462,7 +631,7 @@ class CardDetailsRegistry:
"Queen of Swords": {
"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.",
"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": "",
"keywords": [],
@@ -472,7 +641,7 @@ class CardDetailsRegistry:
"King of Swords": {
"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.",
"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": "",
"keywords": [],
@@ -483,7 +652,7 @@ class CardDetailsRegistry:
"Ace of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -493,7 +662,7 @@ class CardDetailsRegistry:
"Two of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -503,7 +672,7 @@ class CardDetailsRegistry:
"Three of Cups": {
"explanation": {
"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": "",
"keywords": [],
@@ -513,7 +682,7 @@ class CardDetailsRegistry:
"Four of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -523,7 +692,7 @@ class CardDetailsRegistry:
"Five of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -533,7 +702,7 @@ class CardDetailsRegistry:
"Six of Cups": {
"explanation": {
"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": "",
"keywords": [],
@@ -543,7 +712,7 @@ class CardDetailsRegistry:
"Seven of Cups": {
"explanation": {
"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": "",
"keywords": [],
@@ -553,7 +722,7 @@ class CardDetailsRegistry:
"Eight of Cups": {
"explanation": {
"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": "",
"keywords": [],
@@ -563,7 +732,7 @@ class CardDetailsRegistry:
"Nine of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -573,7 +742,7 @@ class CardDetailsRegistry:
"Ten of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -583,7 +752,7 @@ class CardDetailsRegistry:
"Page of Cups": {
"explanation": {
"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": "",
"keywords": [],
@@ -593,7 +762,7 @@ class CardDetailsRegistry:
"Knight of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -603,7 +772,7 @@ class CardDetailsRegistry:
"Queen of Cups": {
"explanation": {
"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": "",
"keywords": [],
@@ -613,7 +782,7 @@ class CardDetailsRegistry:
"King of Cups": {
"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.",
"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": "",
"keywords": [],
@@ -624,7 +793,7 @@ class CardDetailsRegistry:
"Ace of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -634,7 +803,7 @@ class CardDetailsRegistry:
"Two of Pentacles": {
"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.",
"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": "",
"keywords": [],
@@ -644,7 +813,7 @@ class CardDetailsRegistry:
"Three of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -654,7 +823,7 @@ class CardDetailsRegistry:
"Four of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -664,7 +833,7 @@ class CardDetailsRegistry:
"Five of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -674,7 +843,7 @@ class CardDetailsRegistry:
"Six of Pentacles": {
"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.",
"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": "",
"keywords": [],
@@ -684,7 +853,7 @@ class CardDetailsRegistry:
"Seven of Pentacles": {
"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.",
"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": "",
"keywords": [],
@@ -694,7 +863,7 @@ class CardDetailsRegistry:
"Eight of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -704,7 +873,7 @@ class CardDetailsRegistry:
"Nine of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -714,7 +883,7 @@ class CardDetailsRegistry:
"Ten of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -724,7 +893,7 @@ class CardDetailsRegistry:
"Page of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -734,7 +903,7 @@ class CardDetailsRegistry:
"Knight of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -744,7 +913,7 @@ class CardDetailsRegistry:
"Queen of Pentacles": {
"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.",
"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": "",
"keywords": [],
@@ -754,7 +923,7 @@ class CardDetailsRegistry:
"King of Pentacles": {
"explanation": {
"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": "",
"keywords": [],
@@ -765,7 +934,7 @@ class CardDetailsRegistry:
"Ace of Wands": {
"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.",
"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": "",
"keywords": [],
@@ -775,7 +944,7 @@ class CardDetailsRegistry:
"Two of Wands": {
"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.",
"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": "",
"keywords": [],
@@ -785,7 +954,7 @@ class CardDetailsRegistry:
"Three of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -795,7 +964,7 @@ class CardDetailsRegistry:
"Four of Wands": {
"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.",
"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": "",
"keywords": [],
@@ -805,7 +974,7 @@ class CardDetailsRegistry:
"Five of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -815,7 +984,7 @@ class CardDetailsRegistry:
"Six of Wands": {
"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.",
"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": "",
"keywords": [],
@@ -825,7 +994,7 @@ class CardDetailsRegistry:
"Seven of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -835,7 +1004,7 @@ class CardDetailsRegistry:
"Eight of Wands": {
"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.",
"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": "",
"keywords": [],
@@ -845,7 +1014,7 @@ class CardDetailsRegistry:
"Nine of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -855,7 +1024,7 @@ class CardDetailsRegistry:
"Ten of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -865,7 +1034,7 @@ class CardDetailsRegistry:
"Page of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -875,7 +1044,7 @@ class CardDetailsRegistry:
"Knight of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -885,7 +1054,7 @@ class CardDetailsRegistry:
"Queen of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -895,7 +1064,7 @@ class CardDetailsRegistry:
"King of Wands": {
"explanation": {
"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": "",
"keywords": [],
@@ -942,11 +1111,12 @@ class CardDetailsRegistry:
Dictionary of card details for that suit
"""
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()
}
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).
@@ -961,7 +1131,7 @@ class CardDetailsRegistry:
"""
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.

View File

@@ -14,10 +14,9 @@ Usage:
print(f"Loaded {count} card images")
"""
import os
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
if TYPE_CHECKING:
from tarot.deck import Card, Deck
@@ -27,10 +26,12 @@ class ImageDeckLoader:
"""Loader for matching Tarot card images to deck cards."""
# Supported image extensions
SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
SUPPORTED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
# Regex patterns for file matching
NUMBERED_PATTERN = re.compile(r'^(\d+)(?:_(.+))?\.(?:jpg|jpeg|png|gif|bmp|webp)$', re.IGNORECASE)
NUMBERED_PATTERN = re.compile(
r"^(\d+)(?:_(.+))?\.(?:jpg|jpeg|png|gif|bmp|webp)$", re.IGNORECASE
)
def __init__(self, deck_folder: str) -> None:
"""
@@ -51,15 +52,17 @@ class ImageDeckLoader:
raise ValueError(f"Deck path is not a directory: {deck_folder}")
self.image_files = self._scan_folder()
self.card_mapping: Dict[int, Tuple[str, bool]] = {} # card_number -> (path, has_custom_name)
self.card_mapping: Dict[int, Tuple[str, bool]] = (
{}
) # card_number -> (path, has_custom_name)
self._build_mapping()
def _scan_folder(self) -> List[Path]:
"""Scan folder for image files."""
images = []
for ext in self.SUPPORTED_EXTENSIONS:
images.extend(self.deck_folder.glob(f'*{ext}'))
images.extend(self.deck_folder.glob(f'*{ext.upper()}'))
images.extend(self.deck_folder.glob(f"*{ext}"))
images.extend(self.deck_folder.glob(f"*{ext.upper()}"))
# Sort by filename for consistent ordering
return sorted(images)
@@ -124,10 +127,10 @@ class ImageDeckLoader:
normalized = name.lower()
# Replace special characters with spaces
normalized = re.sub(r'[^\w\s]', ' ', normalized)
normalized = re.sub(r"[^\w\s]", " ", normalized)
# Collapse multiple spaces
normalized = re.sub(r'\s+', ' ', normalized).strip()
normalized = re.sub(r"\s+", " ", normalized).strip()
return normalized
@@ -174,7 +177,7 @@ class ImageDeckLoader:
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.
@@ -247,14 +250,14 @@ class ImageDeckLoader:
parsed_num, _, _ = self._parse_filename(image_path.name)
if parsed_num == card_number and custom_name:
# Convert underscore-separated name to title case
name_words = custom_name.split('_')
return ' '.join(word.capitalize() for word in name_words)
name_words = custom_name.split("_")
return " ".join(word.capitalize() for word in name_words)
return None
def load_into_deck(self, deck: 'Deck',
override_names: bool = True,
verbose: bool = False) -> int:
def load_into_deck(
self, deck: "Deck", override_names: bool = True, verbose: bool = False
) -> int:
"""
Load image paths into all cards in a deck.
@@ -305,20 +308,19 @@ class ImageDeckLoader:
custom_named = sum(1 for _, has_custom in self.card_mapping.values() if has_custom)
return {
'deck_folder': str(self.deck_folder),
'total_image_files': total_images,
'total_image_filenames': len(set(f.name for f in self.image_files)),
'mapped_card_numbers': mapped_cards,
'cards_with_custom_names': custom_named,
'cards_with_generic_numbers': mapped_cards - custom_named,
'image_extensions_found': list(set(f.suffix.lower() for f in self.image_files)),
"deck_folder": str(self.deck_folder),
"total_image_files": total_images,
"total_image_filenames": len(set(f.name for f in self.image_files)),
"mapped_card_numbers": mapped_cards,
"cards_with_custom_names": custom_named,
"cards_with_generic_numbers": mapped_cards - custom_named,
"image_extensions_found": list(set(f.suffix.lower() for f in self.image_files)),
}
def load_deck_images(deck: 'Deck',
deck_folder: str,
override_names: bool = True,
verbose: bool = False) -> int:
def load_deck_images(
deck: "Deck", deck_folder: str, override_names: bool = True, verbose: bool = False
) -> int:
"""
Convenience function to load deck images.

View File

@@ -24,10 +24,7 @@ if TYPE_CHECKING:
from tarot.deck import Deck
def load_card_details(
card: 'Card',
registry: Optional['CardDetailsRegistry'] = None
) -> bool:
def load_card_details(card: "Card", registry: Optional["CardDetailsRegistry"] = None) -> bool:
"""
Load details for a single card from the registry.
@@ -49,15 +46,14 @@ def load_card_details(
"""
if registry is None:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
return registry.load_into_card(card)
def load_deck_details(
deck: 'Deck',
registry: Optional['CardDetailsRegistry'] = None,
verbose: bool = False
deck: "Deck", registry: Optional["CardDetailsRegistry"] = None, verbose: bool = False
) -> int:
"""
Load details for all cards in a deck.
@@ -78,6 +74,7 @@ def load_deck_details(
"""
if registry is None:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
loaded_count = 0
@@ -102,10 +99,7 @@ def load_deck_details(
return loaded_count
def get_cards_by_suit(
deck: 'Deck',
suit_name: str
) -> List['Card']:
def get_cards_by_suit(deck: "Deck", suit_name: str) -> List["Card"]:
"""
Get all cards from a specific suit in the deck.
@@ -124,19 +118,19 @@ def get_cards_by_suit(
>>> print(len(swords)) # Should be 14
14
"""
if hasattr(deck, 'suit') and callable(deck.suit):
if hasattr(deck, "suit") and callable(deck.suit):
# Deck has a suit method, use it
return deck.suit(suit_name)
# Fallback: filter cards manually
return [card for card in deck.cards if hasattr(card, 'suit') and
card.suit and card.suit.name == suit_name]
return [
card
for card in deck.cards
if hasattr(card, "suit") and card.suit and card.suit.name == suit_name
]
def filter_cards_by_keywords(
cards: List['Card'],
keyword: str
) -> List['Card']:
def filter_cards_by_keywords(cards: List["Card"], keyword: str) -> List["Card"]:
"""
Filter a list of cards by keyword.
@@ -155,13 +149,15 @@ def filter_cards_by_keywords(
"""
keyword_lower = keyword.lower()
return [
card for card in cards
if hasattr(card, 'keywords') and card.keywords and
any(keyword_lower in kw.lower() for kw in card.keywords)
card
for card in cards
if hasattr(card, "keywords")
and card.keywords
and any(keyword_lower in kw.lower() for kw in card.keywords)
]
def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
def print_card_details(card: "Card", include_reversed: bool = False) -> None:
"""
Pretty print card details to console.
@@ -181,24 +177,24 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
# Define attributes to print with their formatting
attributes = {
'explanation': ('Explanation', False),
'interpretation': ('Interpretation', False),
'guidance': ('Guidance', False),
"explanation": ("Explanation", False),
"interpretation": ("Interpretation", False),
"guidance": ("Guidance", False),
}
# Add reversed attributes only if requested
if include_reversed:
attributes['reversed_interpretation'] = ('Reversed Interpretation', False)
attributes["reversed_interpretation"] = ("Reversed Interpretation", False)
# List attributes (joined with commas)
list_attributes = {
'keywords': 'Keywords',
'reversed_keywords': ('Reversed Keywords', include_reversed),
"keywords": "Keywords",
"reversed_keywords": ("Reversed Keywords", include_reversed),
}
# Numeric attributes
numeric_attributes = {
'numerology': 'Numerology',
"numerology": "Numerology",
}
# Print text attributes
@@ -206,7 +202,7 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
if hasattr(card, attr_name):
value = getattr(card, attr_name)
if value:
if attr_name == 'explanation' and isinstance(value, dict):
if attr_name == "explanation" and isinstance(value, dict):
print(f"\n{display_name}:")
if "summary" in value:
print(f"Summary: {value['summary']}")
@@ -244,8 +240,7 @@ def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
def get_card_info(
card_name: str,
registry: Optional['CardDetailsRegistry'] = None
card_name: str, registry: Optional["CardDetailsRegistry"] = None
) -> Optional[dict]:
"""
Get card information by card name.
@@ -265,6 +260,7 @@ def get_card_info(
"""
if registry is None:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
return registry.get(card_name)

View File

@@ -18,9 +18,9 @@ Usage:
reading = draw_spread(spread) # Returns list of (position, card) tuples
"""
from typing import Dict, List, Optional, TYPE_CHECKING
from dataclasses import dataclass
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional
if TYPE_CHECKING:
from tarot.card import Card
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
@dataclass
class SpreadPosition:
"""Represents a position in a Tarot spread."""
number: int
name: str
meaning: str
@@ -44,8 +45,9 @@ class SpreadPosition:
@dataclass
class DrawnCard:
"""Represents a card drawn for a spread position."""
position: SpreadPosition
card: 'Card'
card: "Card"
is_reversed: bool
def __str__(self) -> str:
@@ -54,9 +56,11 @@ class DrawnCard:
if self.is_reversed:
card_name += " (Reversed)"
return f"{self.position.number}. {self.position.name}\n" \
f" └─ {card_name}\n" \
return (
f"{self.position.number}. {self.position.name}\n"
f" └─ {card_name}\n"
f" └─ Position: {self.position.meaning}"
)
class Spread:
@@ -64,97 +68,102 @@ class Spread:
# Define all available spreads
SPREADS: Dict[str, Dict] = {
'three card': {
'name': '3-Card Spread',
'description': 'Simple 3-card spread for past, present, future or situation, action, outcome',
'positions': [
SpreadPosition(1, 'First Position', 'Past, Foundation, or Situation'),
SpreadPosition(2, 'Second Position', 'Present, Action, or Influence'),
SpreadPosition(3, 'Third Position', 'Future, Outcome, or Advice'),
]
"three card": {
"name": "3-Card Spread",
"description": (
"Simple 3-card spread for past, present, future "
"or situation, action, outcome"
),
"positions": [
SpreadPosition(1, "First Position", "Past, Foundation, or Situation"),
SpreadPosition(2, "Second Position", "Present, Action, or Influence"),
SpreadPosition(3, "Third Position", "Future, Outcome, or Advice"),
],
},
'golden dawn': {
'name': 'Golden Dawn 3-Card',
'description': 'Three card spread used in Golden Dawn tradition',
'positions': [
SpreadPosition(1, 'Supernal Triangle', 'Spiritual/Divine aspect'),
SpreadPosition(2, 'Pillar of Severity', 'Challenging/Active force'),
SpreadPosition(3, 'Pillar of Mercy', 'Supportive/Passive force'),
]
"golden dawn": {
"name": "Golden Dawn 3-Card",
"description": "Three card spread used in Golden Dawn tradition",
"positions": [
SpreadPosition(1, "Supernal Triangle", "Spiritual/Divine aspect"),
SpreadPosition(2, "Pillar of Severity", "Challenging/Active force"),
SpreadPosition(3, "Pillar of Mercy", "Supportive/Passive force"),
],
},
'celtic cross': {
'name': 'Celtic Cross',
'description': 'Classic 10-card spread for in-depth reading',
'positions': [
SpreadPosition(1, 'The Significator', 'The main situation or person'),
SpreadPosition(2, 'The Cross', 'The challenge or heart of the matter'),
SpreadPosition(3, 'Crowning Influence', 'Conscious hopes/ideals'),
SpreadPosition(4, 'Beneath the Cross', 'Unconscious or hidden aspects'),
SpreadPosition(5, 'Behind', 'Past influences'),
SpreadPosition(6, 'Before', 'Future influences'),
SpreadPosition(7, 'Self/Attitude', 'How the querent sees themselves'),
SpreadPosition(8, 'Others/Environment', 'External factors/opinions'),
SpreadPosition(9, 'Hopes and Fears', 'What the querent hopes for or fears'),
SpreadPosition(10, 'Outcome', 'Final outcome or resolution'),
]
"celtic cross": {
"name": "Celtic Cross",
"description": "Classic 10-card spread for in-depth reading",
"positions": [
SpreadPosition(1, "The Significator", "The main situation or person"),
SpreadPosition(2, "The Cross", "The challenge or heart of the matter"),
SpreadPosition(3, "Crowning Influence", "Conscious hopes/ideals"),
SpreadPosition(4, "Beneath the Cross", "Unconscious or hidden aspects"),
SpreadPosition(5, "Behind", "Past influences"),
SpreadPosition(6, "Before", "Future influences"),
SpreadPosition(7, "Self/Attitude", "How the querent sees themselves"),
SpreadPosition(8, "Others/Environment", "External factors/opinions"),
SpreadPosition(9, "Hopes and Fears", "What the querent hopes for or fears"),
SpreadPosition(10, "Outcome", "Final outcome or resolution"),
],
},
'horseshoe': {
'name': 'Horseshoe',
'description': '7-card spread in horseshoe formation for past, present, future insight',
'positions': [
SpreadPosition(1, 'Distant Past', 'Ancient influences and foundations'),
SpreadPosition(2, 'Recent Past', 'Recent events and circumstances'),
SpreadPosition(3, 'Present Situation', 'Current state of affairs'),
SpreadPosition(4, 'Immediate Future', 'Near-term developments'),
SpreadPosition(5, 'Distant Future', 'Long-term outcome'),
SpreadPosition(6, 'Inner Influence', 'Self/thoughts/emotions'),
SpreadPosition(7, 'Outer Influence', 'External forces and environment'),
]
"horseshoe": {
"name": "Horseshoe",
"description": "7-card spread in horseshoe formation for past, present, future insight",
"positions": [
SpreadPosition(1, "Distant Past", "Ancient influences and foundations"),
SpreadPosition(2, "Recent Past", "Recent events and circumstances"),
SpreadPosition(3, "Present Situation", "Current state of affairs"),
SpreadPosition(4, "Immediate Future", "Near-term developments"),
SpreadPosition(5, "Distant Future", "Long-term outcome"),
SpreadPosition(6, "Inner Influence", "Self/thoughts/emotions"),
SpreadPosition(7, "Outer Influence", "External forces and environment"),
],
},
'pentagram': {
'name': 'Pentagram',
'description': '5-card spread based on Earth element pentagram',
'positions': [
SpreadPosition(1, 'Spirit', 'Core essence or spiritual truth'),
SpreadPosition(2, 'Fire', 'Action and willpower'),
SpreadPosition(3, 'Water', 'Emotions and intuition'),
SpreadPosition(4, 'Air', 'Intellect and communication'),
SpreadPosition(5, 'Earth', 'Physical manifestation and grounding'),
]
"pentagram": {
"name": "Pentagram",
"description": "5-card spread based on Earth element pentagram",
"positions": [
SpreadPosition(1, "Spirit", "Core essence or spiritual truth"),
SpreadPosition(2, "Fire", "Action and willpower"),
SpreadPosition(3, "Water", "Emotions and intuition"),
SpreadPosition(4, "Air", "Intellect and communication"),
SpreadPosition(5, "Earth", "Physical manifestation and grounding"),
],
},
'tree of life': {
'name': 'Tree of Life',
'description': '10-card spread mapping Sephiroth on the Tree of Life',
'positions': [
SpreadPosition(1, 'Kether (Crown)', 'Divine will and unity'),
SpreadPosition(2, 'Chokmah (Wisdom)', 'Creative force and impulse'),
SpreadPosition(3, 'Binah (Understanding)', 'Form and structure'),
SpreadPosition(4, 'Chesed (Mercy)', 'Expansion and abundance'),
SpreadPosition(5, 'Gevurah (Severity)', 'Reduction and discipline'),
SpreadPosition(6, 'Tiphareth (Beauty)', 'Core self and integration'),
SpreadPosition(7, 'Netzach (Victory)', 'Desire and passion'),
SpreadPosition(8, 'Hod (Splendor)', 'Intellect and communication'),
SpreadPosition(9, 'Yesod (Foundation)', 'Subconscious and dreams'),
SpreadPosition(10, 'Malkuth (Kingdom)', 'Manifestation and physical reality'),
]
"tree of life": {
"name": "Tree of Life",
"description": "10-card spread mapping Sephiroth on the Tree of Life",
"positions": [
SpreadPosition(1, "Kether (Crown)", "Divine will and unity"),
SpreadPosition(2, "Chokmah (Wisdom)", "Creative force and impulse"),
SpreadPosition(3, "Binah (Understanding)", "Form and structure"),
SpreadPosition(4, "Chesed (Mercy)", "Expansion and abundance"),
SpreadPosition(5, "Gevurah (Severity)", "Reduction and discipline"),
SpreadPosition(6, "Tiphareth (Beauty)", "Core self and integration"),
SpreadPosition(7, "Netzach (Victory)", "Desire and passion"),
SpreadPosition(8, "Hod (Splendor)", "Intellect and communication"),
SpreadPosition(9, "Yesod (Foundation)", "Subconscious and dreams"),
SpreadPosition(10, "Malkuth (Kingdom)", "Manifestation and physical reality"),
],
},
'relationship': {
'name': 'Relationship',
'description': '5-card spread for relationship insight',
'positions': [
SpreadPosition(1, 'You', 'Your position, feelings, or role'),
SpreadPosition(2, 'Them', 'Their position, feelings, or perspective'),
SpreadPosition(3, 'The Relationship', 'The dynamic and connection'),
SpreadPosition(4, 'Challenge', 'Current challenge or friction point'),
SpreadPosition(5, 'Outcome', 'Where the relationship is heading'),
]
"relationship": {
"name": "Relationship",
"description": "5-card spread for relationship insight",
"positions": [
SpreadPosition(1, "You", "Your position, feelings, or role"),
SpreadPosition(2, "Them", "Their position, feelings, or perspective"),
SpreadPosition(3, "The Relationship", "The dynamic and connection"),
SpreadPosition(4, "Challenge", "Current challenge or friction point"),
SpreadPosition(5, "Outcome", "Where the relationship is heading"),
],
},
'yes or no': {
'name': 'Yes or No',
'description': '1-card spread for simple yes/no answers',
'positions': [
SpreadPosition(1, 'Answer', 'Major Arcana = Yes, Minor Arcana = No, Court Cards = Maybe'),
]
"yes or no": {
"name": "Yes or No",
"description": "1-card spread for simple yes/no answers",
"positions": [
SpreadPosition(
1, "Answer", "Major Arcana = Yes, Minor Arcana = No, Court Cards = Maybe"
),
],
},
}
@@ -169,43 +178,41 @@ class Spread:
ValueError: If spread name not found
"""
# Normalize name (case-insensitive, allow underscores or spaces)
normalized_name = spread_name.lower().replace('_', ' ')
normalized_name = spread_name.lower().replace("_", " ")
# Find matching spread
spread_data = None
for key, data in self.SPREADS.items():
if key == normalized_name or data['name'].lower() == normalized_name:
if key == normalized_name or data["name"].lower() == normalized_name:
spread_data = data
break
if not spread_data:
available = ', '.join(f"'{k}'" for k in self.SPREADS.keys())
raise ValueError(
f"Spread '{spread_name}' not found. Available spreads: {available}"
)
available = ", ".join(f"'{k}'" for k in self.SPREADS.keys())
raise ValueError(f"Spread '{spread_name}' not found. Available spreads: {available}")
self.name = spread_data['name']
self.description = spread_data['description']
self.positions: List[SpreadPosition] = spread_data['positions']
self.name = spread_data["name"]
self.description = spread_data["description"]
self.positions: List[SpreadPosition] = spread_data["positions"]
def __str__(self) -> str:
"""Return formatted spread information."""
lines = [
f"═══════════════════════════════════════════",
"═══════════════════════════════════════════",
f" {self.name}",
f"═══════════════════════════════════════════",
f"",
"═══════════════════════════════════════════",
"",
f"{self.description}",
f"",
"",
f"Positions ({len(self.positions)} cards):",
f"",
"",
]
for pos in self.positions:
lines.append(f" {pos}")
lines.append(f"")
lines.append(f"═══════════════════════════════════════════")
lines.append("")
lines.append("═══════════════════════════════════════════")
return "\n".join(lines)
@@ -215,11 +222,7 @@ class Spread:
@classmethod
def available_spreads(cls) -> str:
"""Return list of all available spreads."""
lines = [
"Available Tarot Spreads:",
"" * 50,
""
]
lines = ["Available Tarot Spreads:", "" * 50, ""]
for key, data in cls.SPREADS.items():
lines.append(f"{data['name']}")
@@ -254,20 +257,18 @@ def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
Raises:
ValueError: If spread has more positions than cards in the deck
"""
import random
# Load deck if not provided
if deck is None:
from tarot.deck import Deck
deck_instance = Deck()
deck = deck_instance.cards
# Validate that we have enough cards to draw from without duplicates
num_positions = len(spread.positions)
if num_positions > len(deck):
raise ValueError(
f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards"
)
raise ValueError(f"Cannot draw {num_positions} unique cards from deck of {len(deck)} cards")
# Draw unique cards using random.sample (no replacements)
drawn_deck = random.sample(deck, num_positions)
@@ -298,14 +299,14 @@ class SpreadReading:
def __str__(self) -> str:
"""Return formatted reading with all cards and interpretations."""
lines = [
f"╔═══════════════════════════════════════════╗",
"╔═══════════════════════════════════════════╗",
f"{self.spread.name:40}",
f"╚═══════════════════════════════════════════╝",
f"",
"╚═══════════════════════════════════════════╝",
"",
f"{self.spread.description}",
f"",
f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
f"",
"",
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
"",
]
for drawn in self.drawn_cards:
@@ -319,16 +320,16 @@ class SpreadReading:
lines.append(f" Meaning: {drawn.position.meaning}")
# Add card details if available
if hasattr(card, 'number'):
if hasattr(card, "number"):
lines.append(f" Card #: {card.number}")
if hasattr(card, 'arcana'):
if hasattr(card, "arcana"):
lines.append(f" Arcana: {card.arcana}")
if hasattr(card, 'suit') and card.suit:
if hasattr(card, "suit") and card.suit:
lines.append(f" Suit: {card.suit.name}")
lines.append("")
lines.append(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
return "\n".join(lines)

View File

@@ -6,16 +6,16 @@ MinorCard, and related classes for representing individual cards.
"""
from .deck import (
DLT,
AceCard,
Card,
CardQuery,
CourtCard,
Deck,
MajorCard,
MinorCard,
PipCard,
AceCard,
CourtCard,
CardQuery,
TemporalQuery,
DLT,
Deck,
)
__all__ = [

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.
"""
from dataclasses import dataclass, field
from typing import List, Optional, Tuple, TYPE_CHECKING, Dict
import random
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
from ..attributes import (
Meaning, CardImage, Suit, Zodiac, Element, Path,
Planet, Sephera, Color, PeriodicTable, ElementType, DoublLetterTrump
CardImage,
Color,
Element,
ElementType,
Meaning,
Path,
PeriodicTable,
Planet,
Sephera,
Suit,
)
from ..constants import (
COURT_RANKS,
MAJOR_ARCANA_NAMES,
PIP_INDEX_TO_NUMBER,
MINOR_RANK_NAMES,
PIP_INDEX_TO_NUMBER,
PIP_ORDER,
SUITS_FIRST,
SUITS_LAST,
@@ -35,6 +43,7 @@ def _get_card_data():
global _card_data
if _card_data is None:
from ..card.data import CardDataLoader
_card_data = CardDataLoader()
return _card_data
@@ -42,6 +51,7 @@ def _get_card_data():
@dataclass
class Card:
"""Base class representing a Tarot card."""
number: int
name: str
meaning: Meaning
@@ -85,10 +95,17 @@ class Card:
return CardDetailsRegistry.key_to_roman(self.number)
# For Minor Arcana, return the pip number as a formatted string
if hasattr(self, 'pip') and self.pip > 0:
if hasattr(self, "pip") and self.pip > 0:
pip_names = {
2: "Two", 3: "Three", 4: "Four", 5: "Five",
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten"
2: "Two",
3: "Three",
4: "Four",
5: "Five",
6: "Six",
7: "Seven",
8: "Eight",
9: "Nine",
10: "Ten",
}
return pip_names.get(self.pip, str(self.pip))
@@ -111,20 +128,26 @@ class Card:
@dataclass
class MajorCard(Card):
"""Represents a Major Arcana card."""
kabbalistic_number: Optional[int] = None
tarot_letter: Optional[str] = None
tree_of_life_path: Optional[int] = None
def __post_init__(self) -> None:
# Kabbalistic number should be 0-21, but deck position can be anywhere
if self.kabbalistic_number is not None and (self.kabbalistic_number < 0 or self.kabbalistic_number > 21):
raise ValueError(f"Major Arcana kabbalistic number must be 0-21, got {self.kabbalistic_number}")
if self.kabbalistic_number is not None and (
self.kabbalistic_number < 0 or self.kabbalistic_number > 21
):
raise ValueError(
f"Major Arcana kabbalistic number must be 0-21, got {self.kabbalistic_number}"
)
self.arcana = "Major"
@dataclass
class MinorCard(Card):
"""Represents a Minor Arcana card - either Pip or Court card."""
suit: Suit = None # type: ignore
astrological_influence: Optional[str] = None
element: Optional[Element] = None
@@ -142,6 +165,7 @@ class PipCard(MinorCard):
Pip cards represent numbered forces in their suit, from Two
through its full development (10).
"""
pip: int = 0
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
technically pip cards.
"""
pip: int = 1
def __post_init__(self) -> None:
@@ -183,7 +208,7 @@ class CourtCard(MinorCard):
COURT_RANKS = {"Knight": 12, "Prince": 11, "Princess": 13, "Queen": 14}
court_rank: str = ""
associated_element: Optional[ElementType] = None
hebrew_letter_path: Optional['Path'] = None
hebrew_letter_path: Optional["Path"] = None
def __post_init__(self) -> None:
if self.court_rank not in self.COURT_RANKS:
@@ -194,12 +219,12 @@ class CourtCard(MinorCard):
super().__post_init__()
class CardQuery:
"""Helper class for fluent card queries: deck.number(3).minor.wands"""
def __init__(self, deck: 'Deck', number: Optional[int] = None,
arcana: Optional[str] = None) -> None:
def __init__(
self, deck: "Deck", number: Optional[int] = None, arcana: Optional[str] = None
) -> None:
self.deck = deck
self.number = number
self.arcana = arcana
@@ -209,8 +234,11 @@ class CardQuery:
cards = self.deck.cards
if self.number is not None:
cards = [c for c in cards if c.number == self.number or
(hasattr(c, 'pip') and c.pip == self.number)]
cards = [
c
for c in cards
if c.number == self.number or (hasattr(c, "pip") and c.pip == self.number)
]
if self.arcana is not None:
cards = [c for c in cards if c.arcana == self.arcana]
@@ -223,33 +251,45 @@ class CardQuery:
return [c for c in self._filter_cards() if c.arcana == "Major"]
@property
def minor(self) -> 'CardQuery':
def minor(self) -> "CardQuery":
"""Filter to Minor Arcana, return new CardQuery for suit chaining."""
return CardQuery(self.deck, self.number, "Minor")
@property
def cups(self) -> List[Card]:
"""Get cards in Cups suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Cups"]
return [
c
for c in self._filter_cards()
if hasattr(c, "suit") and c.suit and c.suit.name == "Cups"
]
@property
def swords(self) -> List[Card]:
"""Get cards in Swords suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Swords"]
return [
c
for c in self._filter_cards()
if hasattr(c, "suit") and c.suit and c.suit.name == "Swords"
]
@property
def wands(self) -> List[Card]:
"""Get cards in Wands suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Wands"]
return [
c
for c in self._filter_cards()
if hasattr(c, "suit") and c.suit and c.suit.name == "Wands"
]
@property
def pentacles(self) -> List[Card]:
"""Get cards in Pentacles suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Pentacles"]
return [
c
for c in self._filter_cards()
if hasattr(c, "suit") and c.suit and c.suit.name == "Pentacles"
]
def __iter__(self):
"""Allow iteration over filtered cards."""
@@ -272,8 +312,13 @@ class CardQuery:
class TemporalQuery:
"""Helper class for fluent temporal queries: loader.month(5).day(23).hour(15)"""
def __init__(self, loader: 'CardDataLoader', month_num: Optional[int] = None,
day_num: Optional[int] = None, hour_num: Optional[int] = None) -> None:
def __init__(
self,
loader: "CardDataLoader",
month_num: Optional[int] = None,
day_num: Optional[int] = None,
hour_num: Optional[int] = None,
) -> None:
"""
Initialize temporal query builder.
@@ -288,24 +333,27 @@ class TemporalQuery:
self.day_num = day_num
self.hour_num = hour_num
def month(self, num: int) -> 'TemporalQuery':
def month(self, num: int) -> "TemporalQuery":
"""Set month (1-12) and return new query for chaining."""
return TemporalQuery(self.loader, month_num=num,
day_num=self.day_num, hour_num=self.hour_num)
return TemporalQuery(
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."""
if self.month_num is None:
raise ValueError("Must set month before day")
return TemporalQuery(self.loader, month_num=self.month_num,
day_num=num, hour_num=self.hour_num)
return TemporalQuery(
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."""
if self.month_num is None or self.day_num is None:
raise ValueError("Must set month and day before hour")
return TemporalQuery(self.loader, month_num=self.month_num,
day_num=self.day_num, hour_num=num)
return TemporalQuery(
self.loader, month_num=self.month_num, day_num=self.day_num, hour_num=num
)
def weekday(self) -> Optional[str]:
"""Get weekday name for current month/day combination using Zeller's congruence."""
@@ -389,19 +437,20 @@ class DLT:
raise ValueError(f"DLT number must be 3-21, got {trump_number}")
self.trump_number = trump_number
self._loader: Optional['CardDataLoader'] = None
self._loader: Optional["CardDataLoader"] = None
self._deck: Optional[Deck] = None
@property
def loader(self) -> 'CardDataLoader':
def loader(self) -> "CardDataLoader":
"""Lazy-load CardDataLoader on first access."""
if self._loader is None:
from ..card.data import CardDataLoader
self._loader = CardDataLoader()
return self._loader
@property
def deck(self) -> 'Deck':
def deck(self) -> "Deck":
"""Lazy-load Deck on first access."""
if self._deck is None:
self._deck = Deck()
@@ -507,12 +556,16 @@ class Deck:
"Fire": fire_element,
}
def _suit_specs(suit_defs: List[Tuple[str, str, int]]) -> List[Tuple[str, ElementType, int]]:
def _suit_specs(
suit_defs: List[Tuple[str, str, int]],
) -> List[Tuple[str, ElementType, int]]:
specs: List[Tuple[str, ElementType, int]] = []
for suit_name, element_key, suit_num in suit_defs:
element_obj = element_lookup.get(element_key)
if element_obj is None:
raise RuntimeError(f"Failed to resolve element '{element_key}' for suit '{suit_name}'")
raise RuntimeError(
f"Failed to resolve element '{element_key}' for suit '{suit_name}'"
)
specs.append((suit_name, element_obj, suit_num))
return specs
@@ -538,11 +591,10 @@ class Deck:
number=card_number,
name=name,
meaning=Meaning(
upright=f"{name} upright meaning",
reversed=f"{name} reversed meaning"
upright=f"{name} upright meaning", reversed=f"{name} reversed meaning"
),
arcana="Major",
kabbalistic_number=i
kabbalistic_number=i,
)
self.cards.append(card)
card_number += 1
@@ -561,12 +613,12 @@ class Deck:
# Load detailed explanations and keywords from registry
try:
from ..card.loader import load_deck_details
load_deck_details(self)
except ImportError:
# Handle case where loader might not be available or circular import issues
pass
def _add_minor_cards_for_suit(
self,
suit_name: str,
@@ -625,7 +677,6 @@ class Deck:
return card_number
def shuffle(self) -> None:
"""Shuffle the deck."""
random.shuffle(self.cards)
@@ -644,7 +695,9 @@ class Deck:
raise ValueError("Must draw at least 1 card")
if num_cards > len(self.cards):
raise ValueError(f"Cannot draw {num_cards} cards from deck with {len(self.cards)} cards")
raise ValueError(
f"Cannot draw {num_cards} cards from deck with {len(self.cards)} cards"
)
drawn = []
for _ in range(num_cards):
@@ -680,8 +733,7 @@ class Deck:
Usage:
deck.suit("Wands")
"""
return [c for c in self.cards if hasattr(c, 'suit') and
c.suit and c.suit.name == suit_name]
return [c for c in self.cards if hasattr(c, "suit") and c.suit and c.suit.name == suit_name]
@property
def major(self) -> List[Card]:

View File

@@ -25,15 +25,18 @@ Usage:
area = Tarot.cube.area('North', 'center')
"""
from typing import Dict, Optional, Union, overload, TYPE_CHECKING
from typing import TYPE_CHECKING, Dict, Optional, Union, overload
from .card import CardAccessor
from kaballah import Tree, Cube
from kaballah import Cube, Tree
from letter import letters
from .card import CardAccessor
if TYPE_CHECKING:
from utils.attributes import Planet, God
from utils.attributes import God, Planet
from .attributes import Hexagram
from .card.data import CardDataLoader
class DeckAccessor:
@@ -44,16 +47,20 @@ class DeckAccessor:
def __str__(self) -> str:
"""Return a nice summary of the deck accessor."""
return "Tarot Deck Accessor\n\nAccess methods:\n Tarot.deck.card(3) - Get card by number\n Tarot.deck.card.filter(...) - Filter cards\n Tarot.deck.card.display() - Display all cards\n Tarot.deck.card.spread(...) - Draw a spread"
return (
"Tarot Deck Accessor\n\n"
"Access methods:\n"
" Tarot.deck.card(3) - Get card by number\n"
" Tarot.deck.card.filter(...) - Filter cards\n"
" Tarot.deck.card.display() - Display all cards\n"
" Tarot.deck.card.spread(...) - Draw a spread"
)
def __repr__(self) -> str:
"""Return a nice representation of the deck accessor."""
return self.__str__()
class Tarot:
"""
Unified accessor for Tarot correspondences and data.
@@ -75,7 +82,7 @@ class Tarot:
tree = Tree
cube = Cube
_loader: Optional['CardDataLoader'] = None # type: ignore
_loader: Optional["CardDataLoader"] = None # type: ignore
_initialized: bool = False
@classmethod
@@ -85,35 +92,34 @@ class Tarot:
return
from .card.data import CardDataLoader
cls._loader = CardDataLoader()
cls._initialized = True
@classmethod
@overload
def planet(cls, name: str) -> Optional['Planet']:
...
def planet(cls, name: str) -> Optional["Planet"]: ...
@classmethod
@overload
def planet(cls, name: None = ...) -> Dict[str, 'Planet']:
...
def planet(cls, name: None = ...) -> Dict[str, "Planet"]: ...
@classmethod
def planet(cls, name: Optional[str] = None) -> Union[Optional['Planet'], Dict[str, 'Planet']]:
def planet(cls, name: Optional[str] = None) -> Union[Optional["Planet"], Dict[str, "Planet"]]:
"""Return a planet entry or all planets."""
cls._ensure_initialized()
return cls._loader.planet(name) # type: ignore
@classmethod
def god(cls, name: Optional[str] = None) -> Union[Optional['God'], Dict[str, 'God']]:
def god(cls, name: Optional[str] = None) -> Union[Optional["God"], Dict[str, "God"]]:
"""Return a god entry or all gods."""
cls._ensure_initialized()
return cls._loader.god(name) # type: ignore
@classmethod
def hexagram(cls, number: Optional[int] = None) -> Union[Optional['Hexagram'], Dict[int, 'Hexagram']]:
def hexagram(
cls, number: Optional[int] = None
) -> Union[Optional["Hexagram"], Dict[int, "Hexagram"]]:
"""Return a hexagram or all hexagrams."""
cls._ensure_initialized()
return cls._loader.hexagram(number) # type: ignore

View File

@@ -6,20 +6,20 @@ supporting multiple decks and automatic image resolution.
"""
import os
import io
import tkinter as tk
from tkinter import ttk, filedialog
from pathlib import Path
from tkinter import filedialog, ttk
from typing import List, Optional
try:
from PIL import Image, ImageTk, ImageGrab, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont, ImageTk
HAS_PILLOW = True
except ImportError:
HAS_PILLOW = False
from tarot.deck import Card
from tarot.card.image_loader import ImageDeckLoader
from tarot.deck import Card
class CardDisplay:
@@ -66,7 +66,7 @@ class CardDisplay:
return
# 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
class SimpleSpread:
@@ -81,19 +81,11 @@ class CardDisplay:
for i, card in enumerate(cards, 1):
# Create a generic position
pos = SpreadPosition(
number=i,
name=f"Card {i}",
meaning="Display Card"
)
pos = SpreadPosition(number=i, name=f"Card {i}", meaning="Display Card")
positions.append(pos)
# Create drawn card
drawn = DrawnCard(
position=pos,
card=card,
is_reversed=False
)
drawn = DrawnCard(position=pos, card=card, is_reversed=False)
drawn_cards.append(drawn)
# Create a synthetic spread
@@ -109,8 +101,6 @@ class CardDisplay:
display.run()
class CubeDisplay:
"""
Displays the Cube of Space with navigation.
@@ -124,6 +114,12 @@ class CubeDisplay:
"Above": {"Right": "East", "Left": "West", "Up": "South", "Down": "North"},
"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"):
self.cube = cube
@@ -181,7 +177,9 @@ class CubeDisplay:
# Content Frame (inside 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
# Navigation Frame (Bottom Center)
@@ -194,8 +192,12 @@ class CubeDisplay:
# Populate Zoom Frame
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(0.82)).pack(side=tk.LEFT)
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(0.82)).pack(
side=tk.LEFT
)
# Populate Navigation Frame
dir_frame = ttk.Frame(nav_frame)
@@ -205,8 +207,12 @@ class CubeDisplay:
mid_nav = ttk.Frame(dir_frame)
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="Right", command=lambda: self._navigate("Right")).pack(side=tk.LEFT, padx=5)
ttk.Button(mid_nav, text="Left", command=lambda: self._navigate("Left")).pack(
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)
@@ -221,7 +227,7 @@ class CubeDisplay:
screen_height = self.root.winfo_screenheight()
x = (screen_width // 2) - (width // 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
self.root.focus_force()
@@ -238,17 +244,27 @@ class CubeDisplay:
def _pan_key(self, direction):
"""Pan the canvas using keys."""
if direction == 'up':
if direction == "up":
self.canvas.yview_scroll(-1, "units")
elif direction == 'down':
elif direction == "down":
self.canvas.yview_scroll(1, "units")
elif direction == 'left':
elif direction == "left":
self.canvas.xview_scroll(-1, "units")
elif direction == 'right':
elif direction == "right":
self.canvas.xview_scroll(1, "units")
def _zoom(self, factor):
"""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
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
@@ -262,7 +278,8 @@ class CubeDisplay:
if not bbox:
# Should not happen if initialized
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()
return
@@ -276,8 +293,12 @@ class CubeDisplay:
# 2. Update zoom level
old_zoom = self.zoom_level
self.zoom_level *= factor
# Clamp zoom level
self.zoom_level = max(0.1, min(self.zoom_level, 50.0))
# Clamp zoom level (UI uses broader bounds when initialized)
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
effective_factor = self.zoom_level / old_zoom if old_zoom > 0 else factor
@@ -335,8 +356,11 @@ class CubeDisplay:
widget.destroy()
# Title
ttk.Label(self.content_frame, text=f"Wall: {self.current_wall_name}",
font=("Helvetica", 16, "bold")).pack(pady=(0, 20))
ttk.Label(
self.content_frame,
text=f"Wall: {self.current_wall_name}",
font=("Helvetica", 16, "bold"),
).pack(pady=(0, 20))
# Grid for directions
grid_frame = ttk.Frame(self.content_frame)
@@ -351,7 +375,7 @@ class CubeDisplay:
"West": (1, 0),
"Center": (1, 1),
"East": (1, 2),
"South": (2, 1)
"South": (2, 1),
}
# Calculate sizes based on zoom
@@ -365,7 +389,9 @@ class CubeDisplay:
for dir_name, (row, col) in layout.items():
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_propagate(False)
@@ -385,9 +411,11 @@ class CubeDisplay:
pil_img = Image.open(img_path)
# Resize for grid
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)))
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)
self.root.images.append(tk_img)
@@ -491,6 +519,7 @@ def display_cube(cube=None, deck_name: str = "default"):
"""
if cube is None:
from tarot.tarot_api import Tarot
cube = Tarot.cube
display = CubeDisplay(cube, deck_name)
@@ -506,54 +535,48 @@ class SpreadDisplay:
# Coordinates are relative grid units (approx card width/height)
# Using 1.02 spacing for tight layout
LAYOUTS = {
'Celtic Cross': {
1: {'pos': (0, 0)},
2: {'pos': (0, 0), 'rotate': 90, 'z': 10}, # Top layer
3: {'pos': (0, -1.02)},
4: {'pos': (0, 1.02)},
5: {'pos': (-1.02, 0)},
6: {'pos': (1.02, 0)},
7: {'pos': (2.1, 1.53)}, # 1.5 * 1.02
8: {'pos': (2.1, 0.51)}, # 0.5 * 1.02
9: {'pos': (2.1, -0.51)},
10: {'pos': (2.1, -1.53)}
"Celtic Cross": {
1: {"pos": (0, 0)},
2: {"pos": (0, 0), "rotate": 90, "z": 10}, # Top layer
3: {"pos": (0, -1.02)},
4: {"pos": (0, 1.02)},
5: {"pos": (-1.02, 0)},
6: {"pos": (1.02, 0)},
7: {"pos": (2.1, 1.53)}, # 1.5 * 1.02
8: {"pos": (2.1, 0.51)}, # 0.5 * 1.02
9: {"pos": (2.1, -0.51)},
10: {"pos": (2.1, -1.53)},
},
'3-Card Spread': {
1: {'pos': (-1.02, 0)},
2: {'pos': (0, 0)},
3: {'pos': (1.02, 0)}
"3-Card Spread": {1: {"pos": (-1.02, 0)}, 2: {"pos": (0, 0)}, 3: {"pos": (1.02, 0)}},
"Golden Dawn 3-Card": {
1: {"pos": (0, -1.02)},
2: {"pos": (-1.02, 0.8)},
3: {"pos": (1.02, 0.8)},
},
'Golden Dawn 3-Card': {
1: {'pos': (0, -1.02)},
2: {'pos': (-1.02, 0.8)},
3: {'pos': (1.02, 0.8)}
"Horseshoe": {
1: {"pos": (-3.06, 1.02)},
2: {"pos": (-2.04, 0)},
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': {
1: {'pos': (-3.06, 1.02)},
2: {'pos': (-2.04, 0)},
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)}
"Pentagram": {
1: {"pos": (0, -1.5)}, # Spirit (Top)
2: {"pos": (1.5, -0.4)}, # Fire (Right Top)
3: {"pos": (1.0, 1.5)}, # Water (Right Bottom)
4: {"pos": (-1.0, 1.5)}, # Air (Left Bottom)
5: {"pos": (-1.5, -0.4)}, # Earth (Left Top)
},
'Pentagram': {
1: {'pos': (0, -1.5)}, # Spirit (Top)
2: {'pos': (1.5, -0.4)}, # Fire (Right Top)
3: {'pos': (1.0, 1.5)}, # Water (Right Bottom)
4: {'pos': (-1.0, 1.5)}, # Air (Left Bottom)
5: {'pos': (-1.5, -0.4)} # Earth (Left Top)
"Relationship": {
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)
},
'Relationship': {
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)}
}
"Yes or No": {1: {"pos": (0, 0)}},
}
def __init__(self, reading, deck_name="default"):
@@ -593,15 +616,25 @@ class SpreadDisplay:
toolbar = ttk.Frame(self.root)
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 Out (-)", command=lambda: self._zoom(0.8)).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar, text="Zoom In (+)", command=lambda: self._zoom(1.2)).pack(
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="Toggle Text", command=self._toggle_text).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar, text="Export PNG", command=self._export_image).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="Export PNG", command=self._export_image).pack(
side=tk.LEFT, padx=2
)
# Only show toggle top card if relevant
if self.reading.spread.name == 'Celtic Cross':
ttk.Button(toolbar, text="Toggle Cross", command=self._toggle_top_card).pack(side=tk.LEFT, padx=2)
if self.reading.spread.name == "Celtic Cross":
ttk.Button(toolbar, text="Toggle Cross", command=self._toggle_top_card).pack(
side=tk.LEFT, padx=2
)
# Canvas
self.canvas = tk.Canvas(self.root, bg="#2c3e50")
@@ -636,8 +669,8 @@ class SpreadDisplay:
# Center of virtual space (arbitrary large number to allow scrolling)
cx, cy = 2000, 2000
min_x, min_y = float('inf'), float('inf')
max_x, max_y = float('-inf'), float('-inf')
min_x, min_y = float("inf"), float("inf")
max_x, max_y = float("-inf"), float("-inf")
# Sort cards by z-index (default 0)
cards_to_draw = []
@@ -646,7 +679,7 @@ class SpreadDisplay:
if not pos_data:
continue
z_index = pos_data.get('z', 0)
z_index = pos_data.get("z", 0)
# Skip if top card is hidden
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])
for drawn, pos_data, _ in cards_to_draw:
rel_x, rel_y = pos_data['pos']
rotation = pos_data.get('rotate', 0)
rel_x, rel_y = pos_data["pos"]
rotation = pos_data.get("rotate", 0)
# Calculate position
x = cx + (rel_x * unit_x)
@@ -684,7 +717,9 @@ class SpreadDisplay:
# Set scroll region
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):
card_name = drawn.card.name
@@ -701,7 +736,9 @@ class SpreadDisplay:
if not pil_image:
# 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(
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:
# Total rotation
@@ -728,7 +765,7 @@ class SpreadDisplay:
# Text Overlay
# Calculate visual dimensions
is_vertical = (layout_rotation % 180 == 0)
is_vertical = layout_rotation % 180 == 0
vis_w = w if is_vertical else h
vis_h = h if is_vertical else w
@@ -746,28 +783,29 @@ class SpreadDisplay:
# Draw semi-transparent-ish background (stipple works on some platforms, otherwise solid)
self.canvas.create_rectangle(
bg_x1, bg_y1, bg_x2, bg_y2,
fill="#000000", stipple="gray75", outline=""
bg_x1, bg_y1, bg_x2, bg_y2, fill="#000000", stipple="gray75", outline=""
)
# Position Name
self.canvas.create_text(
x, bg_y1 + font_size,
x,
bg_y1 + font_size,
text=f"{drawn.position.number}. {drawn.position.name}",
fill="white",
font=("Arial", font_size, "bold"),
width=vis_w - 4,
justify="center"
justify="center",
)
# Meaning (shortened)
self.canvas.create_text(
x, bg_y1 + font_size * 2.2,
x,
bg_y1 + font_size * 2.2,
text=drawn.position.meaning,
fill="#ecf0f1",
font=("Arial", int(font_size * 0.8)),
width=vis_w - 4,
justify="center"
justify="center",
)
def _draw_grid_fallback(self):
@@ -777,8 +815,8 @@ class SpreadDisplay:
card_height = 150 * self.zoom_level
padding = 20 * self.zoom_level
min_x, min_y = float('inf'), float('inf')
max_x, max_y = float('-inf'), float('-inf')
min_x, min_y = float("inf"), float("inf")
max_x, max_y = float("-inf"), float("-inf")
cols = 5
for i, drawn in enumerate(self.reading.drawn_cards):
@@ -799,16 +837,18 @@ class SpreadDisplay:
min_y = min(min_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
scroll_padding = 50 * self.zoom_level
self.canvas.configure(scrollregion=(
self.canvas.configure(
scrollregion=(
min_x - scroll_padding,
min_y - scroll_padding,
max_x + scroll_padding,
max_y + scroll_padding
))
max_y + scroll_padding,
)
)
def _zoom(self, factor):
self.zoom_level *= factor
@@ -962,7 +1002,10 @@ class SpreadDisplay:
if pil_image.mode in ("RGBA", "PA"):
# Create white background for transparency
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
elif pil_image.mode != "RGB":
pil_image = pil_image.convert("RGB")
@@ -994,7 +1037,9 @@ class SpreadDisplay:
except Exception as 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" 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:
text_font = font
@@ -1008,7 +1053,9 @@ class SpreadDisplay:
by2 = int(y + card_height / 2)
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 + 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")

View File

@@ -28,26 +28,26 @@ Access Patterns:
print(clock) # Shows planetary positions
"""
from .temporal import Year, Month, Day, Hour, Week
from .astrology import PlanetPosition, ThalemaClock, Zodiac
from .calendar import Calendar
from .coordinates import Season, SolarEvent, TemporalCoordinates
from .temporal import Day, Hour, Month, Week, Year
from .time import TimeUtil
from .coordinates import TemporalCoordinates, Season, SolarEvent
from .astrology import ThalemaClock, Zodiac, PlanetPosition
__all__ = [
# Temporal classes
'Year',
'Month',
'Day',
'Hour',
'Week',
'Calendar',
'TimeUtil',
'TemporalCoordinates',
'Season',
'SolarEvent',
"Year",
"Month",
"Day",
"Hour",
"Week",
"Calendar",
"TimeUtil",
"TemporalCoordinates",
"Season",
"SolarEvent",
# Astrological classes
'ThalemaClock',
'Zodiac',
'PlanetPosition',
"ThalemaClock",
"Zodiac",
"PlanetPosition",
]

View File

@@ -20,12 +20,13 @@ Usage:
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Optional, List
from enum import Enum
from typing import Dict, List, Optional
class Zodiac(Enum):
"""Zodiac signs with degree ranges (0-360°)."""
ARIES = ("", 0, 30)
TAURUS = ("", 30, 60)
GEMINI = ("", 60, 90)
@@ -63,6 +64,7 @@ class Zodiac(Enum):
@dataclass
class PlanetPosition:
"""Represents a planet's position with degree and zodiac."""
planet_name: str
planet_symbol: str
zodiac: Zodiac
@@ -219,7 +221,10 @@ class ThalemaClock:
return result
def display_compact(self) -> str:
"""Display in compact format like: ☉︎ 25°Scorpio : ☾︎ 23°Libra : ☿︎ 2°Sagittarius : ♀︎ 13°Scorpio : ♂︎ 9°Sagittarius"""
"""Display in compact format.
Example: ☉︎ 25°Scorpio : ☾︎ 23°Libra : ☿︎ 2°Sagittarius : ♀︎ 13°Scorpio : ♂︎ 9°Sagittarius
"""
return self.display_format()
def display_verbose(self) -> str:
@@ -228,7 +233,9 @@ class ThalemaClock:
for planet_name in ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn"]:
if planet_name in self.positions:
pos = self.positions[planet_name]
lines.append(f"{planet_name:9} {pos.zodiac.name:11} {pos.degree_in_sign:5.1f}° {pos}")
lines.append(
f"{planet_name:9} {pos.zodiac.name:11} {pos.degree_in_sign:5.1f}° {pos}"
)
return "\n".join(lines)
def __str__(self) -> str:

View File

@@ -6,12 +6,13 @@ including Zodiac, Time cycles, and Astrological influences.
"""
from dataclasses import dataclass, field
from typing import List, Optional
from typing import List
@dataclass
class Month:
"""Represents a calendar month."""
number: int
name: str
zodiac_start: str
@@ -21,6 +22,7 @@ class Month:
@dataclass
class Weekday:
"""Represents weekday/weekend archetypes with planetary ties."""
number: int
name: str
planetary_correspondence: str
@@ -35,6 +37,7 @@ class Weekday:
@dataclass
class Hour:
"""Represents an hour with planetary correspondence."""
number: int
name: str
planetary_hours: List[str] = field(default_factory=list)
@@ -43,6 +46,7 @@ class Hour:
@dataclass
class ClockHour:
"""Represents a clock hour with both 24-hour and 12-hour phases."""
hour_24: int
hour_12: int
period: str # AM or PM
@@ -63,6 +67,7 @@ class ClockHour:
@dataclass
class Zodiac:
"""Represents a zodiac sign."""
name: str
symbol: str
element: str
@@ -73,6 +78,7 @@ class Zodiac:
@dataclass
class Degree:
"""Represents an astrological degree."""
number: int
constellation: str
ruling_planet: str
@@ -82,6 +88,7 @@ class Degree:
@dataclass
class AstrologicalInfluence:
"""Represents astrological influences."""
planet: str
sign: str
house: str

View File

@@ -3,9 +3,10 @@
This module provides calendar-related operations and utilities.
"""
from datetime import datetime, date
from typing import Optional, Dict, Any
from .temporal import Year, Month, Day, Week, Hour
from datetime import date, datetime
from typing import Any, Dict
from .temporal import Day, Hour, Month, Week, Year
class Calendar:
@@ -20,12 +21,12 @@ class Calendar:
"""
now = datetime.now()
return {
'year': Year(now.year),
'month': Month(now.month),
'day': Day(now.day),
'hour': Hour(now.hour, now.minute, now.second),
'week': Calendar.get_week(now.year, now.month, now.day),
'datetime': now,
"year": Year(now.year),
"month": Month(now.month),
"day": Day(now.day),
"hour": Hour(now.hour, now.minute, now.second),
"week": Calendar.get_week(now.year, now.month, now.day),
"datetime": now,
}
@staticmethod

View File

@@ -6,11 +6,12 @@ and other astronomical/calendrical coordinates.
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Dict, Any
from typing import Optional
class Season(Enum):
"""The four seasons of the year."""
SPRING = "Spring" # Vernal Equinox (Mar 20/21)
SUMMER = "Summer" # Summer Solstice (Jun 20/21)
AUTUMN = "Autumn" # Autumnal Equinox (Sep 22/23)
@@ -26,6 +27,7 @@ class SolarEvent:
date: The approximate date of the event
season: The associated season
"""
event_type: str # "solstice" or "equinox"
date: tuple # (month, day)
season: Season

View File

@@ -4,12 +4,12 @@ This module provides the core temporal domain classes used throughout the system
"""
from dataclasses import dataclass
from typing import Optional, List
@dataclass
class Year:
"""Represents a year in the Gregorian calendar."""
value: int
def __str__(self) -> str:
@@ -22,6 +22,7 @@ class Year:
@dataclass
class Month:
"""Represents a month (1-12)."""
value: int
def __post_init__(self) -> None:
@@ -32,8 +33,18 @@ class Month:
def name(self) -> str:
"""Get the month name."""
names = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
return names[self.value - 1]
@@ -47,6 +58,7 @@ class Month:
@dataclass
class Week:
"""Represents a week in the calendar."""
number: int # 1-53
year: int
@@ -64,6 +76,7 @@ class Week:
@dataclass
class Day:
"""Represents a day of the month (1-31)."""
value: int
def __post_init__(self) -> None:
@@ -85,6 +98,7 @@ class Day:
@dataclass
class Hour:
"""Represents an hour in 24-hour format (0-23)."""
value: int
minute: 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 typing import Dict, Any, Optional
from typing import Dict
class TimeUtil:
@@ -35,9 +35,9 @@ class TimeUtil:
secs = remaining % 60
return {
'hours': hours,
'minutes': minutes,
'seconds': secs,
"hours": hours,
"minutes": minutes,
"seconds": secs,
}
@staticmethod

View File

@@ -1,29 +1,29 @@
"""Utility modules for the Tarot project."""
from .attributes import (
Cipher,
CipherResult,
Color,
Colorscale,
Element,
ElementType,
God,
Note,
Number,
Perfume,
Planet,
)
from .filter import (
universal_filter,
get_filterable_fields,
describe_filter_fields,
filter_by,
format_results,
get_filter_autocomplete,
describe_filter_fields,
)
from .attributes import (
Note,
Element,
ElementType,
Number,
Color,
Colorscale,
Planet,
God,
Perfume,
Cipher,
CipherResult,
get_filterable_fields,
universal_filter,
)
from .misc import (
Personality,
MBTIType,
Personality,
)
__all__ = [

View File

@@ -7,12 +7,13 @@ exclusively to any single namespace.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple
from typing import Dict, List, Optional, Sequence, Set, Tuple
@dataclass
class Meaning:
"""Represents the meaning of a card."""
upright: str
reversed: str
@@ -20,6 +21,7 @@ class Meaning:
@dataclass(frozen=True)
class Note:
"""Represents a musical note with its properties."""
name: str # e.g., "C", "D", "E", "F#", "G", "A", "B"
frequency: float # Frequency in Hz (A4 = 440 Hz)
semitone: int # Position in chromatic scale (0-11)
@@ -40,6 +42,7 @@ class Note:
@dataclass
class Element:
"""Represents one of the four elements."""
name: str
symbol: str
color: str
@@ -50,6 +53,7 @@ class Element:
@dataclass
class ElementType:
"""Represents an elemental force (Fire, Water, Air, Earth, Spirit)."""
name: str
symbol: str
direction: str
@@ -68,11 +72,12 @@ class ElementType:
@dataclass
class Number:
"""Represents a number (1-9) with Kabbalistic attributes."""
value: int
sephera: str
element: str
compliment: int
color: Optional['Color'] = None
color: Optional["Color"] = None
def __post_init__(self) -> None:
if not (1 <= self.value <= 9):
@@ -94,6 +99,7 @@ class Colorscale:
- Emperor Scale (Vau): Son/Form, active expression, concrete manifestation
- Empress Scale (He final): Daughter, physical manifestation, receptivity in Assiah
"""
name: str # Sephira/Path name (e.g., "Kether", "Path of Aleph")
number: int # 1-10 for Sephiroth, 11-32 for Paths
king_scale: str # Yod - Father principle
@@ -109,6 +115,7 @@ class Colorscale:
@dataclass
class Color:
"""Represents a color with Kabbalistic correspondences."""
name: str
hex_value: str
rgb: Tuple[int, int, int]
@@ -133,6 +140,7 @@ class Color:
@dataclass
class Planet:
"""Represents a planetary correspondence entry."""
name: str
symbol: str
element: str
@@ -170,6 +178,7 @@ class Planet:
@dataclass
class God:
"""Unified deity representation that synchronizes multiple pantheons."""
name: str
culture: str
pantheon: str
@@ -231,7 +240,11 @@ class God:
lines.append(f" associated_planet: {self.associated_planet.name}")
if self.associated_element:
elem_name = self.associated_element.name if hasattr(self.associated_element, 'name') else str(self.associated_element)
elem_name = (
self.associated_element.name
if hasattr(self.associated_element, "name")
else str(self.associated_element)
)
lines.append(f" associated_element: {elem_name}")
if self.tarot_trumps:
@@ -249,6 +262,7 @@ class God:
@dataclass
class Perfume:
"""Represents a perfume/incense correspondence in Kabbalah."""
name: str
alternative_names: List[str] = field(default_factory=list)
scent_profile: str = "" # e.g., "Resinous", "Floral", "Spicy", "Earthy"
@@ -362,9 +376,7 @@ class Cipher:
expanded.append(self.pattern[idx % len(self.pattern)])
idx += 1
return expanded
raise ValueError(
"Cipher pattern length does not match alphabet and cycling is disabled"
)
raise ValueError("Cipher pattern length does not match alphabet and cycling is disabled")
@dataclass(frozen=True)

View File

@@ -21,11 +21,17 @@ Usage:
fields = get_filterable_fields(TarotLetter)
"""
from typing import List, Any, TypeVar, Union, Dict
from dataclasses import is_dataclass, fields
from utils.object_formatting import get_item_label, is_nested_object, get_object_attributes, format_value
from dataclasses import fields, is_dataclass
from typing import Any, Dict, List, TypeVar
T = TypeVar('T') # Generic type for any dataclass
from utils.object_formatting import (
format_value,
get_item_label,
get_object_attributes,
is_nested_object,
)
T = TypeVar("T") # Generic type for any dataclass
def get_filterable_fields(dataclass_type) -> List[str]:
@@ -96,10 +102,7 @@ def _matches_filter(obj: Any, key: str, value: Any) -> bool:
for check_value in values_to_check:
# Handle list attributes (like keywords, colors, etc.)
if isinstance(attr_value, list):
if any(
str(check_value).lower() == str(item).lower()
for item in attr_value
):
if any(str(check_value).lower() == str(item).lower() for item in attr_value):
return True
continue
@@ -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,
# try matching the value against that (e.g., suit="Cups" vs Suit(name="Cups"))
if hasattr(attr_value, 'name'):
nested_name = getattr(attr_value, 'name', None)
if hasattr(attr_value, "name"):
nested_name = getattr(attr_value, "name", None)
if nested_name is not None:
if str(nested_name).lower() == str(check_value).lower():
return True
@@ -184,10 +187,7 @@ def universal_filter(items: List[T], **kwargs) -> List[T]:
# Apply field alias if it exists
actual_key = field_aliases.get(key, key)
results = [
obj for obj in results
if _matches_filter(obj, actual_key, value)
]
results = [obj for obj in results if _matches_filter(obj, actual_key, value)]
return results

View File

@@ -5,8 +5,8 @@ This module contains specialized utilities that don't fit into other categories.
"""
from dataclasses import dataclass
from typing import Optional, TYPE_CHECKING
from enum import Enum
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from tarot.deck.deck import CourtCard
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
class MBTIType(Enum):
"""16 MBTI personality types."""
ISTJ = "ISTJ"
ISFJ = "ISFJ"
INFJ = "INFJ"
@@ -62,7 +63,7 @@ class Personality:
"""
mbti_type: MBTIType
court_card: Optional['CourtCard'] = None
court_card: Optional["CourtCard"] = None
description: str = ""
# Direct MBTI-to-CourtCard mapping (1-to-1 relationship)
@@ -73,19 +74,16 @@ class Personality:
"ENFJ": ("Knight", "Cups"), # Sensitive, mission-driven
"ESTJ": ("Knight", "Swords"), # Practical, pragmatic
"ESFJ": ("Knight", "Pentacles"), # Sociable, consensus-seeking
# QUEENS (I + J) - Introverted Judgers
"INTJ": ("Queen", "Wands"), # Analytical, self-motivated
"INFJ": ("Queen", "Cups"), # Sensitive, interconnected
"ISTJ": ("Queen", "Swords"), # Pragmatic, duty-fulfiller
"ISFJ": ("Queen", "Pentacles"), # Caring, earth-mother type
# PRINCES (E + P) - Extraverted Perceivers
"ENTP": ("Prince", "Wands"), # Visionary, quick-study
"ENFP": ("Prince", "Cups"), # Inspiring, intuitive
"ESTP": ("Prince", "Swords"), # Action-oriented, risk-taker
"ESFP": ("Prince", "Pentacles"), # Aesthete, sensualist
# PRINCESSES (I + P) - Introverted Perceivers
"INTP": ("Princess", "Wands"), # Thinker par excellence
"INFP": ("Princess", "Cups"), # Idealistic, devoted
@@ -94,7 +92,7 @@ class Personality:
}
@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.
@@ -140,7 +138,7 @@ class Personality:
from tarot import Tarot
# Use provided deck or default to Tarot
d = deck if hasattr(deck, 'card') else Tarot.deck
d = deck if hasattr(deck, "card") else Tarot.deck
cards = d.card.filter(type="Court", court_rank=rank, suit=suit)
if cards:
@@ -152,17 +150,14 @@ class Personality:
"ENFJ": "The Protagonist - Inspiring, empathetic, leader of Cups",
"ESTJ": "The Supervisor - Practical, decisive, leader of Swords",
"ESFJ": "The Consul - Sociable, cooperative, leader of Pentacles",
"INTJ": "The Architect - Strategic, logical, sage of Wands",
"INFJ": "The Advocate - Insightful, idealistic, sage of Cups",
"ISTJ": "The Logistician - Practical, reliable, sage of Swords",
"ISFJ": "The Defender - Caring, conscientious, sage of Pentacles",
"ENTP": "The Debater - Innovative, quick-witted, explorer of Wands",
"ENFP": "The Campaigner - Enthusiastic, social, explorer of Cups",
"ESTP": "The Entrepreneur - Energetic, bold, explorer of Swords",
"ESFP": "The Entertainer - Spontaneous, outgoing, explorer of Pentacles",
"INTP": "The Logician - Analytical, curious, seeker of Wands",
"INFP": "The Mediator - Idealistic, authentic, seeker of Cups",
"ISTP": "The Virtuoso - Practical, observant, seeker of Swords",
@@ -170,9 +165,7 @@ class Personality:
}
return cls(
mbti_type=mbti_enum,
court_card=court_card,
description=descriptions.get(mbti_type, "")
mbti_type=mbti_enum, court_card=court_card, description=descriptions.get(mbti_type, "")
)
def __str__(self) -> str:

View File

@@ -16,14 +16,13 @@ Usage:
from typing import Any, List, Tuple
# Type checking predicates
SCALAR_TYPES = (str, int, float, bool, list, dict, type(None))
def is_dataclass(obj: Any) -> bool:
"""Check if object is a dataclass."""
return hasattr(obj, '__dataclass_fields__')
return hasattr(obj, "__dataclass_fields__")
def is_nested_object(obj: Any) -> bool:
@@ -36,7 +35,7 @@ def is_nested_object(obj: Any) -> bool:
return True
if is_dataclass(obj):
return True
return hasattr(obj, '__dict__') and not isinstance(obj, SCALAR_TYPES)
return hasattr(obj, "__dict__") and not isinstance(obj, SCALAR_TYPES)
def is_scalar(obj: Any) -> bool:
@@ -60,16 +59,16 @@ def get_item_label(item: Any, fallback: str = "item") -> str:
Returns:
A string suitable for display as an item label
"""
if hasattr(item, 'name'):
return str(getattr(item, 'name', fallback))
elif hasattr(item, 'transliteration'):
return str(getattr(item, 'transliteration', fallback))
if hasattr(item, "name"):
return str(getattr(item, "name", fallback))
elif hasattr(item, "transliteration"):
return str(getattr(item, "transliteration", fallback))
return str(item) if item is not None else fallback
def get_dataclass_fields(obj: Any) -> List[str]:
"""Get list of field names from a dataclass."""
if hasattr(obj, '__dataclass_fields__'):
if hasattr(obj, "__dataclass_fields__"):
return list(obj.__dataclass_fields__.keys())
return []
@@ -90,9 +89,9 @@ def get_object_attributes(obj: Any) -> List[Tuple[str, Any]]:
for field_name in obj.__dataclass_fields__:
value = getattr(obj, field_name, None)
attributes.append((field_name, value))
elif hasattr(obj, '__dict__'):
elif hasattr(obj, "__dict__"):
for field_name, value in obj.__dict__.items():
if not field_name.startswith('_'):
if not field_name.startswith("_"):
attributes.append((field_name, value))
return attributes
@@ -121,16 +120,21 @@ def format_value(value: Any, indent: int = 2) -> str:
if is_nested_object(value):
# Classes that have custom __str__ implementations should use them
obj_class = type(value).__name__
has_custom_str = (
hasattr(value, '__str__') and
type(value).__str__ is not object.__str__
)
has_custom_str = hasattr(value, "__str__") and type(value).__str__ is not object.__str__
if has_custom_str and obj_class in ['Path', 'Planet', 'Perfume', 'God', 'Colorscale', 'Sephera', 'ElementType']:
if has_custom_str and obj_class in [
"Path",
"Planet",
"Perfume",
"God",
"Colorscale",
"Sephera",
"ElementType",
]:
# Use the custom __str__ method and indent each line
custom_output = str(value)
lines = []
for line in custom_output.split('\n'):
for line in custom_output.split("\n"):
if line.strip(): # Skip empty lines
lines.append(f"{indent_str}{line}")
return "\n".join(lines)

View File

@@ -20,9 +20,10 @@ Usage:
"""
from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
from utils.object_formatting import format_value, get_object_attributes, is_nested_object
T = TypeVar('T')
T = TypeVar("T")
class QueryResult:
@@ -32,18 +33,18 @@ class QueryResult:
self.data = data
def __repr__(self) -> str:
if hasattr(self.data, '__repr__'):
if hasattr(self.data, "__repr__"):
return repr(self.data)
return f"{self.__class__.__name__}({self.data})"
def __str__(self) -> str:
if hasattr(self.data, '__str__'):
if hasattr(self.data, "__str__"):
return str(self.data)
return repr(self)
def __getattr__(self, name: str) -> Any:
"""Pass through attribute access to the wrapped data."""
if name.startswith('_'):
if name.startswith("_"):
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
return getattr(self.data, name)
@@ -61,7 +62,7 @@ class Query:
self._data = data if isinstance(data, list) else list(data.values())
self._filters: List[Callable[[T], bool]] = []
def filter(self, expression: str) -> 'Query':
def filter(self, expression: str) -> "Query":
"""
Filter by key:value expression.
@@ -74,12 +75,12 @@ class Query:
Supports multiple filters by chaining:
.filter('number:1').filter('name:creative')
"""
key, value = expression.split(':', 1) if ':' in expression else (expression, '')
key, value = expression.split(":", 1) if ":" in expression else (expression, "")
def filter_func(item: T) -> bool:
# Special handling for 'name' key
if key == 'name':
if hasattr(item, 'name'):
if key == "name":
if hasattr(item, "name"):
value_lower = value.lower()
item_name = str(item.name).lower()
return value_lower == item_name or value_lower in item_name
@@ -97,16 +98,16 @@ class Query:
self._filters.append(filter_func)
return self
def name(self, value: str) -> Optional['QueryResult']:
def name(self, value: str) -> Optional["QueryResult"]:
"""
Deprecated: Use .filter('name:value') instead.
Find item by name (exact or partial match, case-insensitive).
Returns QueryResult wrapping the found item, or None if not found.
"""
return self.filter(f'name:{value}').first()
return self.filter(f"name:{value}").first()
def get(self) -> Optional['QueryResult']:
def get(self) -> Optional["QueryResult"]:
"""
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)]
def first(self) -> Optional['QueryResult']:
def first(self) -> Optional["QueryResult"]:
"""Alias for get() - returns first matching item."""
return self.get()
@@ -205,8 +206,6 @@ class CollectionAccessor(Generic[T]):
Returns a formatted string with each item separated by blank lines.
Nested objects are indented and separated with their own sections.
"""
from utils.object_formatting import is_nested_object, get_object_attributes
data = self.all()
if not data:
return "(empty collection)"
@@ -241,7 +240,7 @@ class CollectionAccessor(Generic[T]):
class FilterableDict(dict):
"""Dict subclass that provides .filter() method for dynamic querying."""
def filter(self, expression: str = '') -> Query:
def filter(self, expression: str = "") -> Query:
"""
Filter dict values by attribute:value expression.
@@ -259,8 +258,6 @@ class FilterableDict(dict):
Returns a formatted string with each item separated by blank lines.
Nested objects are indented and separated with their own sections.
"""
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
if not self:
return "(empty collection)"
@@ -283,7 +280,7 @@ class FilterableDict(dict):
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.

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
import pytest
from src.tarot.attributes import (
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter, Sephera, Degree, Element,
AstrologicalInfluence, TreeOfLife, Correspondences, CardImage,
EnglishAlphabet, GreekAlphabet, HebrewAlphabet, Number, Color, Planet, God,
Cipher, CipherResult,
AstrologicalInfluence,
CardImage,
Cipher,
CipherResult,
ClockHour,
Color,
Correspondences,
Day,
Degree,
Element,
EnglishAlphabet,
God,
GreekAlphabet,
HebrewAlphabet,
Hour,
Letter,
Meaning,
Month,
Number,
Planet,
Sephera,
Suit,
TreeOfLife,
Weekday,
Zodiac,
)
from src.tarot.card.data import CardDataLoader, calculate_digital_root
# ============================================================================
# Basic Attribute Tests
# ============================================================================
class TestMonth:
def test_month_creation(self):
month = Month(1, "January", "Capricorn", "Aquarius")
@@ -24,10 +46,7 @@ class TestMonth:
assert month.zodiac_start == "Capricorn"
def test_month_all_months(self):
months = [
Month(i, f"Month_{i}", "Sign_1", "Sign_2")
for i in range(1, 13)
]
months = [Month(i, f"Month_{i}", "Sign_1", "Sign_2") for i in range(1, 13)]
assert len(months) == 12
assert months[0].number == 1
assert months[11].number == 12
@@ -41,10 +60,7 @@ class TestDay:
assert day.planetary_correspondence == "Sun"
def test_all_weekdays(self):
days = [
Day(i, f"Day_{i}", f"Planet_{i}")
for i in range(1, 8)
]
days = [Day(i, f"Day_{i}", f"Planet_{i}") for i in range(1, 8)]
assert len(days) == 7
@@ -99,6 +115,7 @@ class TestMeaning:
# Sepheric Tests
# ============================================================================
class TestSephera:
def test_sephera_creation(self):
sephera = Sephera(1, "Kether", "כתר", "Crown", "Metatron", "Chaioth", "Primum")
@@ -118,6 +135,7 @@ class TestSephera:
# Alphabet Tests
# ============================================================================
class TestEnglishAlphabet:
def test_english_letter_creation(self):
letter = EnglishAlphabet("A", 1, "ay")
@@ -189,6 +207,7 @@ class TestHebrewAlphabet:
# Number Tests
# ============================================================================
class TestNumber:
def test_number_creation(self):
num = Number(1, "Kether", "Spirit", 0) # compliment is auto-calculated
@@ -220,6 +239,7 @@ class TestNumber:
# Color Tests
# ============================================================================
class TestColor:
def test_color_creation(self):
color = Color("Red", "#FF0000", (255, 0, 0), "Gevurah", 5, "Fire", "Briah", "Power")
@@ -248,7 +268,9 @@ class TestColor:
for r in [0, 128, 255]:
for g in [0, 128, 255]:
for b in [0, 128, 255]:
color = Color("Test", "#000000", (r, g, b), "Sephera", 1, "Element", "Scale", "Meaning")
color = Color(
"Test", "#000000", (r, g, b), "Sephera", 1, "Element", "Scale", "Meaning"
)
assert color.rgb == (r, g, b)
@@ -260,7 +282,9 @@ class TestColor:
class TestPlanet:
def test_planet_creation(self):
number = Number(6, "Tiphareth", "Fire", 0)
color = Color("Gold", "#FFD700", (255, 215, 0), "Tiphareth", 6, "Fire", "Yetzirah", "Beauty")
color = Color(
"Gold", "#FFD700", (255, 215, 0), "Tiphareth", 6, "Fire", "Yetzirah", "Beauty"
)
planet = Planet(
name="Sun",
symbol="",
@@ -342,6 +366,7 @@ class TestGod:
# Cipher Tests
# ============================================================================
class TestCipher:
def test_cipher_mapping_basic(self):
cipher = Cipher("Test", "test", [1, 2, 3])
@@ -374,6 +399,7 @@ class TestCipherResult:
# Digital Root Tests
# ============================================================================
class TestDigitalRoot:
def test_digital_root_single_digit(self):
"""Single digits should return themselves."""
@@ -413,6 +439,7 @@ class TestDigitalRoot:
# CardDataLoader Tests
# ============================================================================
class TestCardDataLoader:
@pytest.fixture
def loader(self):
@@ -463,13 +490,15 @@ class TestCardDataLoader:
def test_trigram_line_diagram(self, loader):
from letter import trigram
tri = trigram.trigram.name("Zhen") # Thunder
assert tri is not None
assert tri.data.line_diagram == "|::"
def test_hexagram_line_diagram(self, loader):
from letter import hexagram
hex_result = hexagram.hexagram.filter('number:1').first()
hex_result = hexagram.hexagram.filter("number:1").first()
assert hex_result is not None
assert hex_result.data.line_diagram == "||||||"

View File

@@ -1,13 +1,16 @@
import pytest
from tarot.ui import CardDisplay
from tarot.deck import Card
from unittest.mock import MagicMock, patch
import pytest
from tarot.deck import Card
from tarot.ui import CardDisplay
def test_card_display_delegation():
"""Test that CardDisplay delegates to SpreadDisplay correctly."""
with patch('tarot.ui.SpreadDisplay') as MockSpreadDisplay:
with patch("tarot.ui.SpreadDisplay") as MockSpreadDisplay:
# Mock HAS_PILLOW to True to ensure we proceed
with patch('tarot.ui.HAS_PILLOW', True):
with patch("tarot.ui.HAS_PILLOW", True):
display = CardDisplay()
# Create dummy card

View File

@@ -1,6 +1,8 @@
import pytest
from tarot.ui import CubeDisplay
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_cube_display_init():
cube = Tarot.cube
@@ -8,6 +10,7 @@ def test_cube_display_init():
assert display.current_wall_name == "North"
assert display.deck_name == "default"
def test_cube_navigation():
cube = Tarot.cube
display = CubeDisplay(cube)
@@ -28,6 +31,7 @@ def test_cube_navigation():
display._navigate("Left")
assert display.current_wall_name == "West"
def test_find_card_for_direction():
cube = Tarot.cube
display = CubeDisplay(cube)

View File

@@ -1,6 +1,8 @@
import pytest
from tarot.ui import CubeDisplay
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_cube_zoom():
cube = Tarot.cube
@@ -13,6 +15,7 @@ def test_cube_zoom():
display._zoom(0.5)
assert display.zoom_level < 1.0
def test_cube_zoom_limits():
cube = Tarot.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
from unittest.mock import MagicMock, patch
import pytest
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_zoom_limits():
# Mock Tk root
class MockRoot:
def __init__(self):
self.bindings = {}
self.images = []
def bind(self, key, callback): pass
def title(self, _): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 800
def winfo_reqheight(self): return 600
def winfo_screenwidth(self): return 1920
def winfo_screenheight(self): return 1080
def geometry(self, _): pass
def mainloop(self): pass
def focus_force(self): pass
def bind(self, key, callback):
pass
def title(self, _):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 800
def winfo_reqheight(self):
return 600
def winfo_screenwidth(self):
return 1920
def winfo_screenheight(self):
return 1080
def geometry(self, _):
pass
def mainloop(self):
pass
def focus_force(self):
pass
# Mock Frame
class MockFrame:
def __init__(self, master=None, **kwargs):
self.children = []
self.master = master
def pack(self, **kwargs): pass
def place(self, **kwargs): pass
def grid(self, **kwargs): pass
def grid_propagate(self, flag): pass
def winfo_children(self): return self.children
def destroy(self): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 100
def winfo_reqheight(self): return 100
def bind(self, event, callback): pass
def pack(self, **kwargs):
pass
def place(self, **kwargs):
pass
def grid(self, **kwargs):
pass
def grid_propagate(self, flag):
pass
def winfo_children(self):
return self.children
def destroy(self):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 100
def winfo_reqheight(self):
return 100
def bind(self, event, callback):
pass
# Mock Canvas
class MockCanvas:
def __init__(self, master=None, **kwargs):
self.master = master
def pack(self, **kwargs): pass
def bind(self, event, callback): pass
def create_window(self, coords, **kwargs): return 1
def config(self, **kwargs): pass
def bbox(self, tag): return (0,0,100,100)
def winfo_width(self): return 800
def winfo_height(self): return 600
def coords(self, item, x, y): pass
def scan_mark(self, x, y): pass
def scan_dragto(self, x, y, gain=1): pass
def canvasx(self, x): return x
def canvasy(self, y): return y
def xview_moveto(self, fraction): pass
def yview_moveto(self, fraction): pass
def pack(self, **kwargs):
pass
def bind(self, event, callback):
pass
def create_window(self, coords, **kwargs):
return 1
def config(self, **kwargs):
pass
def bbox(self, tag):
return (0, 0, 100, 100)
def winfo_width(self):
return 800
def winfo_height(self):
return 600
def coords(self, item, x, y):
pass
def scan_mark(self, x, y):
pass
def scan_dragto(self, x, y, gain=1):
pass
def canvasx(self, x):
return x
def canvasy(self, y):
return y
def xview_moveto(self, fraction):
pass
def yview_moveto(self, fraction):
pass
# Monkey patch tk
original_tk = tk.Tk
@@ -67,10 +138,18 @@ def test_zoom_limits():
class MockWidget:
def __init__(self, master=None, **kwargs):
self.master = master
def pack(self, **kwargs): pass
def place(self, **kwargs): pass
def grid(self, **kwargs): pass
def grid_propagate(self, flag): pass
def pack(self, **kwargs):
pass
def place(self, **kwargs):
pass
def grid(self, **kwargs):
pass
def grid_propagate(self, flag):
pass
try:
tk.Tk = MockRoot
@@ -80,13 +159,13 @@ def test_zoom_limits():
tk.ttk.Button = MockWidget
# Mock Image to avoid memory issues
with patch('PIL.Image.open') as mock_open:
with patch("PIL.Image.open") as mock_open:
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.resize.return_value = mock_img
mock_open.return_value = mock_img
with patch('PIL.ImageTk.PhotoImage') as mock_photo:
with patch("PIL.ImageTk.PhotoImage") as mock_photo:
cube = Tarot.cube
display = CubeDisplay(cube)

View File

@@ -3,8 +3,9 @@ Tests for Tarot deck and card classes.
"""
import pytest
from src.tarot.deck import Deck, Card, MajorCard, MinorCard, PipCard, AceCard, CourtCard
from src.tarot.attributes import Meaning, Suit, CardImage
from src.tarot.attributes import CardImage, Meaning, Suit
from src.tarot.deck import AceCard, Card, CourtCard, Deck, MajorCard, MinorCard, PipCard
class TestCard:
@@ -30,7 +31,7 @@ class TestMajorCard:
name="The Magician",
meaning=Meaning("Upright", "Reversed"),
arcana="Major",
kabbalistic_number=1
kabbalistic_number=1,
)
assert card.number == 1
assert card.arcana == "Major"
@@ -42,7 +43,7 @@ class TestMajorCard:
name="Test",
meaning=Meaning("Up", "Rev"),
arcana="Major",
kabbalistic_number=-1
kabbalistic_number=-1,
)
def test_major_card_invalid_high(self):
@@ -52,16 +53,13 @@ class TestMajorCard:
name="Test",
meaning=Meaning("Up", "Rev"),
arcana="Major",
kabbalistic_number=22
kabbalistic_number=22,
)
def test_major_card_valid_range(self):
for i in range(22):
card = MajorCard(
number=i,
name=f"Card {i}",
meaning=Meaning("Up", "Rev"),
arcana="Major"
number=i, name=f"Card {i}", meaning=Meaning("Up", "Rev"), arcana="Major"
)
assert card.number == i
@@ -75,7 +73,7 @@ class TestMinorCard:
meaning=Meaning("Upright", "Reversed"),
arcana="Minor",
suit=suit,
pip=1
pip=1,
)
assert card.number == 1
assert card.suit.name == "Cups"
@@ -90,7 +88,7 @@ class TestMinorCard:
meaning=Meaning("Up", "Rev"),
arcana="Minor",
suit=suit,
pip=0
pip=0,
)
def test_minor_card_invalid_pip_high(self):
@@ -102,7 +100,7 @@ class TestMinorCard:
meaning=Meaning("Up", "Rev"),
arcana="Minor",
suit=suit,
pip=15
pip=15,
)
def test_minor_card_valid_pips(self):
@@ -114,7 +112,7 @@ class TestMinorCard:
meaning=Meaning("Up", "Rev"),
arcana="Minor",
suit=suit,
pip=i
pip=i,
)
assert card.pip == i

View File

@@ -1,18 +1,25 @@
import pytest
from pathlib import Path
import pytest
from tarot.ui import CardDisplay
def test_card_display_init():
display = CardDisplay("default")
assert display.deck_name == "default"
# Check if path resolves correctly relative to src/tarot/ui.py
# src/tarot/ui.py -> src/tarot -> src/tarot/deck/default
expected_suffix = os.path.join("src", "tarot", "deck", "default")
assert str(display.deck_path).endswith(expected_suffix) or str(display.deck_path).endswith("default")
assert str(display.deck_path).endswith(expected_suffix) or str(display.deck_path).endswith(
"default"
)
def test_card_display_resolve_path():
display = CardDisplay("thoth")
assert display.deck_name == "thoth"
assert str(display.deck_path).endswith("thoth")
import os

View File

@@ -1,8 +1,11 @@
import pytest
from tarot.ui import CubeDisplay
from tarot.tarot_api import Tarot
import tkinter as tk
import pytest
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_recursive_binding():
# Mock Tk root and widgets
class MockWidget:

View File

@@ -1,8 +1,11 @@
import pytest
from tarot.ui import CubeDisplay
from tarot.tarot_api import Tarot
import tkinter as tk
import pytest
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_zoom_key_bindings():
# This test verifies that the bindings are set up,
# but cannot easily simulate key presses in headless environment.
@@ -16,23 +19,46 @@ def test_zoom_key_bindings():
def bind(self, key, callback):
self.bindings[key] = callback
def title(self, _): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 800
def winfo_reqheight(self): return 600
def winfo_screenwidth(self): return 1920
def winfo_screenheight(self): return 1080
def geometry(self, _): pass
def mainloop(self): pass
def focus_force(self): pass
def title(self, _):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 800
def winfo_reqheight(self):
return 600
def winfo_screenwidth(self):
return 1920
def winfo_screenheight(self):
return 1080
def geometry(self, _):
pass
def mainloop(self):
pass
def focus_force(self):
pass
# Mock Frame
class MockFrame:
def __init__(self, master=None, **kwargs):
self.children = []
def pack(self, **kwargs): pass
def winfo_children(self): return self.children
def destroy(self): pass
def pack(self, **kwargs):
pass
def winfo_children(self):
return self.children
def destroy(self):
pass
# Monkey patch tk
original_tk = tk.Tk
@@ -58,6 +84,7 @@ def test_zoom_key_bindings():
tk.Tk = original_tk
tk.ttk.Frame = original_frame
def test_zoom_logic_direct():
cube = Tarot.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 pytest
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_canvas_structure():
# Mock Tk root
class MockRoot:
def __init__(self):
self.bindings = {}
def bind(self, key, callback): pass
def title(self, _): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 800
def winfo_reqheight(self): return 600
def winfo_screenwidth(self): return 1920
def winfo_screenheight(self): return 1080
def geometry(self, _): pass
def mainloop(self): pass
def focus_force(self): pass
def bind(self, key, callback):
pass
def title(self, _):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 800
def winfo_reqheight(self):
return 600
def winfo_screenwidth(self):
return 1920
def winfo_screenheight(self):
return 1080
def geometry(self, _):
pass
def mainloop(self):
pass
def focus_force(self):
pass
# Mock Frame
class MockFrame:
def __init__(self, master=None, **kwargs):
self.children = []
self.master = master
def pack(self, **kwargs): pass
def place(self, **kwargs): pass
def winfo_children(self): return self.children
def destroy(self): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 100
def winfo_reqheight(self): return 100
def pack(self, **kwargs):
pass
def place(self, **kwargs):
pass
def winfo_children(self):
return self.children
def destroy(self):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 100
def winfo_reqheight(self):
return 100
# Mock Canvas
class MockCanvas:
def __init__(self, master=None, **kwargs):
self.master = master
def pack(self, **kwargs): pass
def bind(self, event, callback): pass
def create_window(self, coords, **kwargs): return 1
def config(self, **kwargs): pass
def bbox(self, tag): return (0,0,100,100)
def winfo_width(self): return 800
def winfo_height(self): return 600
def coords(self, item, x, y): pass
def scan_mark(self, x, y): pass
def scan_dragto(self, x, y, gain=1): pass
def pack(self, **kwargs):
pass
def bind(self, event, callback):
pass
def create_window(self, coords, **kwargs):
return 1
def config(self, **kwargs):
pass
def bbox(self, tag):
return (0, 0, 100, 100)
def winfo_width(self):
return 800
def winfo_height(self):
return 600
def coords(self, item, x, y):
pass
def scan_mark(self, x, y):
pass
def scan_dragto(self, x, y, gain=1):
pass
# Monkey patch tk
original_tk = tk.Tk

View File

@@ -1,42 +1,84 @@
import pytest
from tarot.ui import CubeDisplay
from tarot.tarot_api import Tarot
import tkinter as tk
from unittest.mock import MagicMock, patch
import pytest
from tarot.tarot_api import Tarot
from tarot.ui import CubeDisplay
def test_wasd_panning():
# Mock Tk root
class MockRoot:
def __init__(self):
self.bindings = {}
self.images = []
def bind(self, key, callback):
self.bindings[key] = callback
def title(self, _): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 800
def winfo_reqheight(self): return 600
def winfo_screenwidth(self): return 1920
def winfo_screenheight(self): return 1080
def geometry(self, _): pass
def mainloop(self): pass
def focus_force(self): pass
def title(self, _):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 800
def winfo_reqheight(self):
return 600
def winfo_screenwidth(self):
return 1920
def winfo_screenheight(self):
return 1080
def geometry(self, _):
pass
def mainloop(self):
pass
def focus_force(self):
pass
# Mock Frame
class MockFrame:
def __init__(self, master=None, **kwargs):
self.children = []
self.master = master
def pack(self, **kwargs): pass
def place(self, **kwargs): pass
def grid(self, **kwargs): pass
def grid_propagate(self, flag): pass
def winfo_children(self): return self.children
def destroy(self): pass
def update_idletasks(self): pass
def winfo_reqwidth(self): return 100
def winfo_reqheight(self): return 100
def bind(self, event, callback): pass
def pack(self, **kwargs):
pass
def place(self, **kwargs):
pass
def grid(self, **kwargs):
pass
def grid_propagate(self, flag):
pass
def winfo_children(self):
return self.children
def destroy(self):
pass
def update_idletasks(self):
pass
def winfo_reqwidth(self):
return 100
def winfo_reqheight(self):
return 100
def bind(self, event, callback):
pass
# Mock Canvas
class MockCanvas:
@@ -45,20 +87,47 @@ def test_wasd_panning():
self.x_scrolls = []
self.y_scrolls = []
def pack(self, **kwargs): pass
def bind(self, event, callback): pass
def create_window(self, coords, **kwargs): return 1
def config(self, **kwargs): pass
def bbox(self, tag): return (0,0,100,100)
def winfo_width(self): return 800
def winfo_height(self): return 600
def coords(self, item, x, y): pass
def scan_mark(self, x, y): pass
def scan_dragto(self, x, y, gain=1): pass
def canvasx(self, x): return x
def canvasy(self, y): return y
def xview_moveto(self, fraction): pass
def yview_moveto(self, fraction): pass
def pack(self, **kwargs):
pass
def bind(self, event, callback):
pass
def create_window(self, coords, **kwargs):
return 1
def config(self, **kwargs):
pass
def bbox(self, tag):
return (0, 0, 100, 100)
def winfo_width(self):
return 800
def winfo_height(self):
return 600
def coords(self, item, x, y):
pass
def scan_mark(self, x, y):
pass
def scan_dragto(self, x, y, gain=1):
pass
def canvasx(self, x):
return x
def canvasy(self, y):
return y
def xview_moveto(self, fraction):
pass
def yview_moveto(self, fraction):
pass
def xview_scroll(self, number, what):
self.x_scrolls.append((number, what))
@@ -77,10 +146,18 @@ def test_wasd_panning():
class MockWidget:
def __init__(self, master=None, **kwargs):
self.master = master
def pack(self, **kwargs): pass
def place(self, **kwargs): pass
def grid(self, **kwargs): pass
def grid_propagate(self, flag): pass
def pack(self, **kwargs):
pass
def place(self, **kwargs):
pass
def grid(self, **kwargs):
pass
def grid_propagate(self, flag):
pass
try:
tk.Tk = MockRoot
@@ -90,13 +167,13 @@ def test_wasd_panning():
tk.ttk.Button = MockWidget
# Mock Image to avoid memory issues
with patch('PIL.Image.open') as mock_open:
with patch("PIL.Image.open") as mock_open:
mock_img = MagicMock()
mock_img.size = (100, 100)
mock_img.resize.return_value = mock_img
mock_open.return_value = mock_img
with patch('PIL.ImageTk.PhotoImage') as mock_photo:
with patch("PIL.ImageTk.PhotoImage") as mock_photo:
cube = Tarot.cube
display = CubeDisplay(cube)

View File

@@ -8,8 +8,8 @@ References:
- Weekday planetary rulers
"""
from datetime import datetime, date, timedelta, timezone
from typing import Dict, List, Optional
from datetime import datetime, timedelta, timezone
from typing import Dict, Optional
# Planetary symbols for weekdays (Sun=0, Mon=1, ..., Sat=6)