This commit is contained in:
nose
2025-11-25 22:19:36 -08:00
commit 79d4f1a09e
132 changed files with 11052 additions and 0 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 PY-Tarot Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
include README.md
include LICENSE
include pyproject.toml
recursive-include docs *.md
recursive-include tests *.py

192
docs/FILTERING.md Normal file
View File

@@ -0,0 +1,192 @@
# Filtering Guide
Universal filter syntax for all queryable objects in PY-Tarot.
## Basic Filtering (AND Logic)
Multiple filters use AND logic - all conditions must match.
### Cards - Basic
```python
from tarot import Tarot
# Single filter
Tarot.deck.card.filter(suit="Cups")
# → All Cups cards (1-14)
# Multiple filters (AND)
Tarot.deck.card.filter(suit="Cups", type="Court")
# → Knight, Prince, Princess, Queen of Cups (4 cards)
# Filter by pip number
Tarot.deck.card.filter(pip=3)
# → 3 of Cups, Pentacles, Swords, Wands (4 cards)
# Filter Aces
Tarot.deck.card.filter(type="Ace")
# → Ace of Cups, Pentacles, Swords, Wands (4 cards)
# Filter specific court rank
Tarot.deck.card.filter(court_rank="Knight")
# → All 4 Knights (all suits)
# Combine: Knights of Cups
Tarot.deck.card.filter(court_rank="Knight", suit="Cups")
# → Knight of Cups (1 card)
```
### Cards - By Card Type
```python
# All Major Arcana
Tarot.deck.card.filter(type="Major")
# → The Fool through The World (22 cards)
# All Court Cards
Tarot.deck.card.filter(type="Court")
# → 16 cards (4 ranks × 4 suits)
# All Pips (2-10)
Tarot.deck.card.filter(type="Pip")
# → 36 cards
# All Aces
Tarot.deck.card.filter(type="Ace")
# → 4 cards
# Specific pip type + suit
Tarot.deck.card.filter(type="Pip", pip=5, suit="Wands")
# → 5 of Wands (1 card)
```
### Cards - Court Cards
```python
# All Knights
Tarot.deck.card.filter(type="Court", court_rank="Knight")
# → Knight of Cups, Pentacles, Swords, Wands (4 cards)
# All Queens
Tarot.deck.card.filter(type="Court", court_rank="Queen")
# → Queen of Cups, Pentacles, Swords, Wands (4 cards)
# Queen of specific element
Tarot.deck.card.filter(type="Court", court_rank="Queen", suit="Wands")
# → Queen of Wands (1 card)
# All Princes
Tarot.deck.card.filter(court_rank="Prince")
# → Prince of Cups, Pentacles, Swords, Wands (4 cards)
# All Princesses
Tarot.deck.card.filter(court_rank="Princess")
# → Princess of Cups, Pentacles, Swords, Wands (4 cards)
```
## Advanced Filtering
### Multiple Values (OR Logic)
Use `filter()` multiple times and combine results:
```python
# Aces OR Pips with value 3
aces = Tarot.deck.card.filter(type="Ace")
threes = Tarot.deck.card.filter(pip=3)
result = aces + threes
# → Ace of all suits + 3 of all suits (8 cards)
# Wands OR Pentacles
wands = Tarot.deck.card.filter(suit="Wands")
pentacles = Tarot.deck.card.filter(suit="Pentacles")
result = wands + pentacles
# → All Wands + all Pentacles (28 cards)
# Knights OR Queens
knights = Tarot.deck.card.filter(court_rank="Knight")
queens = Tarot.deck.card.filter(court_rank="Queen")
result = knights + queens
# → All Knights + all Queens (8 cards)
```
### Complex Queries
```python
# All Cups court cards
cups_court = Tarot.deck.card.filter(suit="Cups", type="Court")
# → Knight, Prince, Princess, Queen of Cups (4 cards)
# All water element (Cups and lower pips)
water_cards = Tarot.deck.card.filter(suit="Cups")
# → All 14 Cups cards
# Fire element court cards
fire_court = Tarot.deck.card.filter(suit="Wands", type="Court")
# → Knight, Prince, Princess, Queen of Wands (4 cards)
# All numbered cards from 2-10 (pips) in specific suits
fives_in_water_earth = (
Tarot.deck.card.filter(pip=5, suit="Cups") +
Tarot.deck.card.filter(pip=5, suit="Pentacles")
)
# → 5 of Cups + 5 of Pentacles (2 cards)
```
## Available Filter Fields
### All Cards
- `type` → "Major", "Pip", "Ace", "Court"
- `arcana` → "Major", "Minor"
- `number` → Card's position in deck (1-78)
- `name` → Full card name (case-insensitive)
### Minor Arcana Only
- `suit` → "Cups", "Pentacles", "Swords", "Wands"
- `pip` → 1-10 (1 for Ace, 2-10 for pips)
- `court_rank` → "Knight", "Prince", "Princess", "Queen"
### Court Cards Only
- `court_rank` → "Knight", "Prince", "Princess", "Queen"
- `associated_element` → Element object for the court rank
### Major Arcana Only
- `kabbalistic_number` → 0-21
## Display Results
```python
# Print as formatted list
cards = Tarot.deck.card.filter(suit="Cups")
print(cards)
# Print nicely formatted
cards_str = Tarot.deck.card.display_filter(suit="Cups")
print(cards_str)
# Access individual cards
cups = Tarot.deck.card.filter(suit="Cups")
first_cup = cups[0] # Ace of Cups
print(f"{first_cup.number}. {first_cup.name}")
```
## Case Sensitivity
All filters are case-insensitive:
```python
Tarot.deck.card.filter(suit="cups") # Works
Tarot.deck.card.filter(suit="CUPS") # Works
Tarot.deck.card.filter(suit="Cups") # Works
Tarot.deck.card.filter(type="ace") # Works
Tarot.deck.card.filter(type="ACE") # Works
Tarot.deck.card.filter(type="Ace") # Works
```
## Tips
- Use `type` to filter by card class (Major, Pip, Ace, Court)
- Use `suit` to filter by element (Cups/Water, Pentacles/Earth, Swords/Air, Wands/Fire)
- Multiple kwargs = AND logic
- For OR logic, call `filter()` separately and combine lists with `+`
- All string comparisons are case-insensitive

110
docs/REGISTRY_MAPPING.md Normal file
View File

@@ -0,0 +1,110 @@
## Registry System: Position-Based Card Details
### Overview
The registry uses a **position-based lookup system** that maps card positions (1-78) to interpretive data stored in `CardDetailsRegistry`. This system is independent of card names, allowing the same registry entries to work across different deck variants.
### Design Principle
**Card position is the permanent identifier**, not the card name. This means:
- Card #44 is always "Magus position" in the deck order
- If you rename card #44 to "Magus", "Magician", "Mage", or any other variant, it still maps to the same registry entry
- Different deck variants can have completely different names but use the same spiritual/interpretive data
### File Organization
Your deck files follow this numbering (which drives card position):
- **1-14**: Cups (Ace, Ten, 2-9, Knight, Prince, Princess, Queen)
- **15-28**: Pentacles/Disks (same structure)
- **29-42**: Swords (same structure)
- **43-64**: Major Arcana (43=Fool, 44=Magus, ..., 64=Universe)
- **65-78**: Wands (same structure)
Example: `44_Magus.webp` → Card at position 44 → Magus name → Registry position 44 → Details for position 44
### Position-Based Lookup Process
```
Card created with number (position)
card.number = 44
load_into_card(card)
get_by_position(44)
Position 44 maps to registry key "I" (1st trump after Fool)
registry.get("I") → Returns Magician/Magus details
Details loaded into card object (independent of card.name)
```
### Position Mapping
**Minor Arcana Positions:**
- 1-14: Cups - Maps to "Ace of Cups" through "Queen of Cups"
- 15-28: Pentacles - Maps to "Ace of Pentacles" through "Queen of Pentacles"
- 29-42: Swords - Maps to "Ace of Swords" through "Queen of Swords"
- 65-78: Wands - Maps to "Ace of Wands" through "Queen of Wands"
**Major Arcana Positions:**
- 43 → "o" (Roman for 0/Fool)
- 44 → "I" (Roman for 1/Magus)
- 45 → "II" (Roman for 2)
- ...continuing through...
- 64 → "XXI" (Roman for 21/Universe)
### Implementation
```python
def _build_position_map(self) -> Dict[int, str]:
"""
Maps card position (1-78) to registry key:
- Minor Arcana: position → card name ("Ace of Cups", etc.)
- Major Arcana: position → Roman numeral ("o", "I", "II", etc.)
"""
# Builds complete 1-78 mapping
return position_map
def get_by_position(self, position: int) -> Optional[Dict[str, Any]]:
"""Get details for a card by its position (1-78)."""
registry_key = self._position_map.get(position)
return self._details.get(registry_key)
def load_into_card(self, card: 'Card') -> bool:
"""Load card details using position-based lookup."""
details = self.get_by_position(card.number)
# Populate card with: explanation, interpretation, keywords, etc.
```
### Why Position-Based?
1. **Deck Variant Independence**: Different decks can use completely different names
2. **Stable Identity**: Card position never changes across variants
3. **Scalable**: Easily support new deck variants by just changing card names
4. **Future Proof**: New interpretations can be added keyed to positions, not names
### Example Flow
File: `44_Magus.webp`
Card object: number=44, name="Magus", arcana="Major"
`load_into_card(card)` called
`get_by_position(44)` returns registry key "I"
Registry lookup: `registry.get("I")`
Populates card with Magician/Magus interpretation:
- keywords: ["manifestation", "resourcefulness", ...]
- interpretation: "Communication; Conscious Will; ..."
- guidance: "Focus your energy and intention..."
### Key Point
Even if you rename card #44 to something completely different, it will still load the same interpretation because the lookup is based on **position (44)**, not **name ("Magus")**.
Registry lookup: `registry.get("o")`
Populates card with: explanation, interpretation, keywords, etc.

56
mytest.py Normal file
View File

@@ -0,0 +1,56 @@
from tarot import Tarot, letter, number, kaballah
from temporal import ThalemaClock
from datetime import datetime
from utils import Personality, MBTIType
# Tarot core functionality
card = Tarot.deck.card(3)
print(f"Card: {card}")
# Spreads - now under Tarot.deck.card.spread()
print("\n" + Tarot.deck.card.spread("Celtic Cross"))
# Temporal functionality (separate module)
clock = ThalemaClock(datetime.now())
print(f"\nClock: {clock}")
print(Tarot.deck.card.filter(suit="Cups"))
# Top-level namespaces with pretty printing
print("\n" + "=" * 60)
print("Letter Namespace:")
print(letter)
print("\n" + "=" * 60)
print("Number Namespace:")
print(number)
print("\nDigital root of 343:", number.digital_root(343))
print("\n" + "=" * 60)
print("Kaballah - Tree of Life:")
print(kaballah.Tree)
print("\n" + "=" * 60)
print("Kaballah - Cube of Space:")
print(kaballah.Cube.wall.display_filter(side="Below"))
# Filtering examples
print("\n" + "=" * 60)
print(Tarot.deck.card.filter(type='court'))
# MBTI Personality types mapped to Tarot court cards (1-to-1 direct mapping)
print("\n" + "=" * 60)
print("MBTI Personality Types & Tarot Court Cards")
print("=" * 60)
# Create personalities for all 16 MBTI types
mbti_types = ['ENFP', 'ISTJ', 'INTJ', 'INFJ', 'ENTJ', 'ESFJ', 'ESTP', 'ISFJ',
'ENTP', 'ISFP', 'INTP', 'INFP', 'ESTJ', 'ESFP', 'ISTP', 'INTJ']
for mbti in mbti_types:
personality = Personality.from_mbti(mbti, Tarot.deck)
print(f"\n{personality}")
#prints court cards
print(Tarot.deck.card.filter(suit="cups,wands"))

74
pyproject.toml Normal file
View File

@@ -0,0 +1,74 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "py-tarot"
version = "0.1.0"
description = "A Python library for Tarot card reading and interpretation"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
keywords = ["tarot", "divination", "cards", "spirituality"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"tomli>=1.2.0;python_version<'3.11'",
"tomli_w>=1.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=3.0",
"black>=22.0",
"isort>=5.10",
"flake8>=4.0",
"mypy>=0.950",
]
docs = [
"sphinx>=4.5",
"sphinx-rtd-theme>=1.0",
]
[project.urls]
Homepage = "https://github.com/yourusername/py-tarot"
Documentation = "https://py-tarot.readthedocs.io"
Repository = "https://github.com/yourusername/py-tarot.git"
Issues = "https://github.com/yourusername/py-tarot/issues"
[tool.setuptools]
packages = ["tarot"]
[tool.setuptools.package-dir]
"" = "src"
[tool.black]
line-length = 100
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
[tool.isort]
profile = "black"
line_length = 100
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
[tool.pytest.ini_options]
testpaths = ["tests"]

35
src/__init__.py Normal file
View File

@@ -0,0 +1,35 @@
"""
PY-Tarot: Comprehensive Tarot library with hierarchical namespaces.
Provides four root namespaces for different domains:
number - Numerology (digital root, colors, Sepheric attributes)
letter - Alphabets, ciphers (English, Hebrew, Greek), words, I Ching
kaballah - Tree of Life and Cube of Space
tarot - Tarot-specific (deck, cards, temporal)
Quick Start:
from tarot import number, letter, kaballah, Tarot
# Number
num = number.number(5)
root = number.digital_root(256)
# Letter
letter_obj = letter.letter('A')
result = letter.word('MAGICK').cipher('english_simple')
# Kaballah
sephera = kaballah.Tree.sephera(1)
wall = kaballah.Cube.wall('North')
# Tarot
card = Tarot.deck.card(3)
major5 = Tarot.deck.card.major(5)
cups2 = Tarot.deck.card.minor.cups(2)
"""
__version__ = "0.1.0"
__author__ = "PY-Tarot Contributors"
__all__ = []

22
src/kaballah/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
"""
Kaballah namespace - Tree of Life and Cube of Space.
Provides fluent query interface for:
- Tree of Life with Sephiroth and Paths
- Cube of Space with walls and areas
- Kabbalistic correspondences
Usage:
from tarot import kaballah
sephera = kaballah.Tree.sephera(1)
path = kaballah.Tree.path(11)
wall = kaballah.Cube.wall("North")
direction = kaballah.Cube.direction("North", "East")
"""
from .tree import Tree
from .cube import Cube
# Export classes for fluent access
__all__ = ["Tree", "Cube"]

215
src/kaballah/attributes.py Normal file
View File

@@ -0,0 +1,215 @@
"""
Kabbalistic attributes and data structures.
This module defines attributes specific to the Kabbalah module,
including Sephira, Paths, and Tree of Life structures.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any
from utils.attributes import (
Element,
ElementType,
Planet,
Color,
Colorscale,
Perfume,
God,
)
@dataclass
class Sephera:
"""Represents a Sephira on the Tree of Life."""
number: int
name: str
hebrew_name: str
meaning: str
archangel: str
order_of_angels: str
mundane_chakra: str
element: Optional['ElementType'] = None
planetary_ruler: Optional[str] = None
tarot_trump: Optional[str] = None
colorscale: Optional['Colorscale'] = None
@dataclass
class PeriodicTable:
"""Represents a Sephirothic position in Kabbalah with cross-correspondences."""
number: int
name: str
sephera: Optional[Sephera]
element: Optional['ElementType'] = None
planet: Optional['Planet'] = None
color: Optional['Color'] = None
tarot_trump: Optional[str] = None
hebrew_letter: Optional[str] = None
divine_name: Optional[str] = None
archangel: Optional[str] = None
order_of_angels: Optional[str] = None
keywords: List[str] = field(default_factory=list)
@dataclass
class TreeOfLife:
"""Represents the Tree of Life structure."""
sephiroth: Dict[int, str]
paths: Dict[Tuple[int, int], str]
@dataclass
class Correspondences:
"""Represents Kabbalistic correspondences."""
number: int
sephira: str
element: Optional[str]
planet: Optional[str]
zodiac: Optional[str]
tarot_trump: Optional[str]
archangel: Optional[str]
order_of_angels: Optional[str]
divine_name: Optional[str]
@dataclass
class Path:
"""Represents one of the 22 Paths on the Tree of Life with full correspondences."""
number: int # 11-32
hebrew_letter: str # Hebrew letter name (Aleph through Tau)
transliteration: str # English transliteration
tarot_trump: str # Major Arcana card (0-XXI)
sephera_from: Optional['Sephera'] = None # Lower Sephira
sephera_to: Optional['Sephera'] = None # Upper Sephira
element: Optional['ElementType'] = None # Element (Air, Fire, Water, Earth)
planet: Optional['Planet'] = None # Planetary ruler
zodiac_sign: Optional[str] = None # Zodiac sign (12 paths only)
colorscale: Optional['Colorscale'] = None # Golden Dawn color scales
perfumes: List['Perfume'] = field(default_factory=list)
gods: Dict[str, List['God']] = field(default_factory=dict)
keywords: List[str] = field(default_factory=list)
description: str = ""
def __post_init__(self) -> None:
if not 11 <= self.number <= 32:
raise ValueError(f"Path number must be between 11 and 32, got {self.number}")
def is_elemental_path(self) -> bool:
"""Check if this is one of the 4 elemental paths."""
elemental_numbers = {11, 23, 31, 32} # Aleph, Mem, Shin, 32-bis
return self.number in elemental_numbers
def is_planetary_path(self) -> bool:
"""Check if this path has planetary correspondence."""
return self.planet is not None
def is_zodiacal_path(self) -> bool:
"""Check if this path has zodiac correspondence."""
return self.zodiac_sign is not None
def add_god(self, god: 'God') -> None:
"""Attach a god to this path grouped by culture."""
culture_key = god.culture_key()
culture_bucket = self.gods.setdefault(culture_key, [])
if god not in culture_bucket:
culture_bucket.append(god)
def add_perfume(self, perfume: 'Perfume') -> None:
"""Attach a perfume correspondence if it is not already present."""
if perfume not in self.perfumes:
self.perfumes.append(perfume)
def get_gods(self, culture: Optional[str] = None) -> List['God']:
"""Return all gods for this path, optionally filtered by culture."""
if culture:
return list(self.gods.get(culture.lower(), []))
merged: List['God'] = []
for values in self.gods.values():
merged.extend(values)
return merged
def __str__(self) -> str:
"""Return nicely formatted string representation of the Path."""
lines = []
# Header with path number and letter
lines.append(f"--- Path {self.number}: {self.hebrew_letter} ({self.transliteration}) ---")
lines.append("")
# Basic correspondences
lines.append(f"tarot_trump: {self.tarot_trump}")
# Connections
if self.sephera_from or self.sephera_to:
seph_from = self.sephera_from.name if self.sephera_from else "Unknown"
seph_to = self.sephera_to.name if self.sephera_to else "Unknown"
lines.append(f"connects: {seph_from}{seph_to}")
# Element
if self.element:
element_name = self.element.name if hasattr(self.element, 'name') else str(self.element)
lines.append(f"element: {element_name}")
# Planet
if self.planet:
lines.append("")
lines.append("--- Planet ---")
for line in str(self.planet).split("\n"):
lines.append(f" {line}")
# Zodiac
if self.zodiac_sign:
lines.append(f"zodiac_sign: {self.zodiac_sign}")
# Colorscale
if self.colorscale:
lines.append("")
lines.append("--- Colorscale ---")
lines.append(f" name: {self.colorscale.name}")
lines.append(f" king_scale: {self.colorscale.king_scale}")
lines.append(f" queen_scale: {self.colorscale.queen_scale}")
lines.append(f" emperor_scale: {self.colorscale.emperor_scale}")
lines.append(f" empress_scale: {self.colorscale.empress_scale}")
if self.colorscale.sephirotic_color:
lines.append(f" sephirotic_color: {self.colorscale.sephirotic_color}")
lines.append(f" type: {self.colorscale.type}")
if self.colorscale.keywords:
lines.append(f" keywords: {', '.join(self.colorscale.keywords)}")
if self.colorscale.description:
lines.append(f" description: {self.colorscale.description}")
# Perfumes
if self.perfumes:
lines.append("")
lines.append("--- Perfumes ---")
for perfume in self.perfumes:
for line in str(perfume).split("\n"):
lines.append(f" {line}")
lines.append("")
# Gods
if self.gods:
lines.append("")
lines.append("--- Gods ---")
for culture, god_list in self.gods.items():
lines.append(f" {culture}:")
for god in god_list:
for line in str(god).split("\n"):
lines.append(f" {line}")
lines.append("")
# Keywords
if self.keywords:
lines.append("")
lines.append("--- Keywords ---")
lines.append(f" {', '.join(self.keywords)}")
# Description
if self.description:
lines.append("")
lines.append("--- Description ---")
lines.append(f" {self.description}")
lines.append("")
return "\n".join(lines)

View File

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

View File

@@ -0,0 +1,513 @@
"""
Cube of Space attributes and data structures.
Defines the CubeOfSpace, Wall, and WallDirection classes for the Cube of Space
Kabbalistic model with hierarchical wall and direction structure.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional
@dataclass(frozen=True, repr=False)
class WallDirection:
"""
Represents a single direction within a Wall of the Cube of Space.
Each wall has 5 directions: North, South, East, West, Center.
Each direction has a Hebrew letter and zodiac correspondence.
"""
name: str # "North", "South", "East", "West", "Center"
letter: str # Hebrew letter (e.g., "Aleph", "Bet", etc.)
zodiac: Optional[str] = None # Zodiac sign if applicable
element: Optional[str] = None # Associated element if any
planet: Optional[str] = None # Associated planet if any
keywords: List[str] = field(default_factory=list)
description: str = ""
VALID_DIRECTION_NAMES = {"North", "South", "East", "West", "Center"}
def __post_init__(self) -> None:
if self.name not in self.VALID_DIRECTION_NAMES:
raise ValueError(
f"Invalid direction name '{self.name}'. "
f"Valid names: {', '.join(sorted(self.VALID_DIRECTION_NAMES))}"
)
if not self.letter or not isinstance(self.letter, str):
raise ValueError(f"Direction must have a letter, got {self.letter}")
def __repr__(self) -> str:
"""Custom repr showing key attributes."""
return f"WallDirection({self.name}, {self.letter})"
@dataclass(frozen=True, repr=False)
class Wall:
"""
Represents one of the 6 walls of the Cube of Space.
Each wall has 5 directions: North, South, East, West, Center.
The 6 walls are: North, South, East, West, Above, Below.
Opposite walls: North↔South, East↔West, Above↔Below.
Each direction has a Hebrew letter and zodiac correspondence.
"""
name: str # "North", "South", "East", "West", "Above", "Below"
side: str # Alias for name, used for filtering (e.g., "north", "south")
opposite: str # Opposite wall name (e.g., "South" for North wall)
element: Optional[str] = None # Associated element
planet: Optional[str] = None # Associated planet
archangel: Optional[str] = None # Associated archangel
keywords: List[str] = field(default_factory=list)
description: str = ""
directions: Dict[str, "WallDirection"] = field(default_factory=dict)
VALID_WALL_NAMES = {"North", "South", "East", "West", "Above", "Below"}
# Opposite wall mappings
OPPOSITE_WALLS = {
"North": "South",
"South": "North",
"East": "West",
"West": "East",
"Above": "Below",
"Below": "Above",
}
def __post_init__(self) -> None:
if self.name not in self.VALID_WALL_NAMES:
raise ValueError(
f"Invalid wall name '{self.name}'. "
f"Valid walls: {', '.join(sorted(self.VALID_WALL_NAMES))}"
)
# Validate side matches name (case-insensitive)
if self.side.capitalize() != self.name:
raise ValueError(
f"Wall side '{self.side}' must match name '{self.name}'"
)
# Validate opposite wall
expected_opposite = self.OPPOSITE_WALLS.get(self.name)
if self.opposite != expected_opposite:
raise ValueError(
f"Wall '{self.name}' must have opposite '{expected_opposite}', got '{self.opposite}'"
)
# Ensure all 5 directions exist
if len(self.directions) != 5:
raise ValueError(
f"Wall '{self.name}' must have exactly 5 directions (North, South, East, West, Center), "
f"got {len(self.directions)}"
)
required_directions = {"North", "South", "East", "West", "Center"}
if set(self.directions.keys()) != required_directions:
raise ValueError(
f"Wall '{self.name}' must have directions: {required_directions}, "
f"got {set(self.directions.keys())}"
)
def __repr__(self) -> str:
"""Custom repr showing wall name and element."""
return f"Wall({self.name}, {self.element})"
def __str__(self) -> str:
"""Custom string representation for printing wall details with recursive direction details."""
keywords_str = ", ".join(self.keywords) if self.keywords else "None"
lines = [
f"Wall: {self.name}",
f" Side: {self.side}",
f" Opposite: {self.opposite}",
f" Element: {self.element}",
f" Planet: {self.planet}",
f" Archangel: {self.archangel}",
f" Keywords: {keywords_str}",
]
# Add directions with their details recursively
if self.directions:
lines.append(" Directions:")
# Order: Center, North, South, East, West
direction_order = ["Center", "North", "South", "East", "West"]
for dir_name in direction_order:
if dir_name in self.directions:
direction = self.directions[dir_name]
lines.append(f" --- {direction.name} ---")
lines.append(f" Letter: {direction.letter}")
if direction.zodiac:
lines.append(f" Zodiac: {direction.zodiac}")
if direction.element:
lines.append(f" Element: {direction.element}")
if direction.planet:
lines.append(f" Planet: {direction.planet}")
if direction.keywords:
keywords = ", ".join(direction.keywords)
lines.append(f" Keywords: {keywords}")
if direction.description:
lines.append(f" Description: {direction.description}")
return "\n".join(lines)
def direction(self, direction_name: str) -> Optional["WallDirection"]:
"""Get a specific direction by name. Usage: wall.direction("North")"""
return self.directions.get(direction_name.capitalize())
def all_directions(self) -> list:
"""Return all 5 directions as a list."""
return list(self.directions.values())
# Aliases for backward compatibility
def get_direction(self, direction_name: str) -> Optional["WallDirection"]:
"""Deprecated: use direction() instead."""
return self.direction(direction_name)
def get_opposite_wall_name(self) -> str:
"""Deprecated: use the opposite property instead."""
return self.opposite
@dataclass
class CubeOfSpace:
"""
Represents the Cube of Space with 6 walls.
The Cube of Space is a 3D sacred geometry model consisting of:
- 6 walls (North, South, East, West, Above, Below)
- Each wall contains 5 areas (center, above, below, east, west)
- Opposite walls: North↔South, East↔West, Above↔Below
- Total: 30 positions plus central core
"""
walls: Dict[str, Wall] = field(default_factory=dict)
center: Optional[WallDirection] = None # Central core position
# Built-in wall definitions with all correspondences
_WALL_DEFINITIONS = {
"North": {
"element": "Air",
"planet": "Mercury",
"archangel": "Raphael",
"keywords": ["Thought", "Communication", "Intellect"],
"description": "Northern Wall - Air element, Mercury correspondence",
"areas": {
"center": {
"element": "Spirit",
"keywords": ["Integration", "Balance", "Foundation"],
"description": "Center of North wall - synthesis of thought",
},
"above": {
"element": "Fire",
"keywords": ["Higher Mind", "Spiritual Thought", "Ascent"],
"description": "Above area - elevated intellectual consciousness",
},
"below": {
"element": "Earth",
"keywords": ["Practical Thought", "Grounded Mind", "Implementation"],
"description": "Below area - material manifestation of thought",
},
"east": {
"element": "Air",
"keywords": ["Clarity", "Awakening", "Dawn Thought"],
"description": "East area - morning mind, new perspectives",
},
"west": {
"element": "Water",
"keywords": ["Emotional Thought", "Reflection", "Dreams"],
"description": "West area - intuitive mind, introspection",
},
},
},
"South": {
"element": "Fire",
"planet": "Mars",
"archangel": "Samael",
"keywords": ["Will", "Action", "Passion"],
"description": "Southern Wall - Fire element, Mars correspondence",
"areas": {
"center": {
"element": "Spirit",
"keywords": ["Pure Will", "Center of Power", "Drive"],
"description": "Center of South wall - focal point of action",
},
"above": {
"element": "Fire",
"keywords": ["Divine Will", "Higher Purpose", "Spiritual Force"],
"description": "Above area - transcendent power and courage",
},
"below": {
"element": "Earth",
"keywords": ["Physical Action", "Embodied Will", "Manifestation"],
"description": "Below area - action in material world",
},
"east": {
"element": "Air",
"keywords": ["Active Mind", "Strategic Will", "Beginning Action"],
"description": "East area - dawn of new endeavors",
},
"west": {
"element": "Water",
"keywords": ["Passionate Emotion", "Emotional Drive", "Desire"],
"description": "West area - feeling-guided action",
},
},
},
"East": {
"element": "Air",
"planet": "Venus",
"archangel": "Haniel",
"keywords": ["Dawn", "Beginning", "Ascent"],
"description": "Eastern Wall - Air element, new beginnings",
"areas": {
"center": {
"element": "Spirit",
"keywords": ["New Potential", "Morning Star", "Awakening"],
"description": "Center of East wall - point of emergence",
},
"above": {
"element": "Fire",
"keywords": ["Spiritual Dawn", "Divine Light", "Inspiration"],
"description": "Above area - celestial promise",
},
"below": {
"element": "Earth",
"keywords": ["Material Growth", "Physical Sunrise", "Earthly Beginning"],
"description": "Below area - manifestation of new potential",
},
"east": {
"element": "Air",
"keywords": ["Pure Beginning", "First Breath", "Clarity"],
"description": "East area - absolute dawn principle",
},
"west": {
"element": "Water",
"keywords": ["Emotional Renewal", "Feelings Awakening", "Hope"],
"description": "West area - emotional opening toward new day",
},
},
},
"West": {
"element": "Water",
"planet": "Venus",
"archangel": "Uriel",
"keywords": ["Emotion", "Decline", "Closure"],
"description": "Western Wall - Water element, endings and emotions",
"areas": {
"center": {
"element": "Spirit",
"keywords": ["Emotional Core", "Sunset Synthesis", "Integration"],
"description": "Center of West wall - emotional balance point",
},
"above": {
"element": "Fire",
"keywords": ["Spiritual Emotion", "Divine Love", "Transcendence"],
"description": "Above area - love beyond form",
},
"below": {
"element": "Earth",
"keywords": ["Physical Emotion", "Embodied Feeling", "Sensuality"],
"description": "Below area - emotion in material form",
},
"east": {
"element": "Air",
"keywords": ["Mental Emotion", "Thoughts Felt", "Understanding Feeling"],
"description": "East area - intellectual understanding of emotion",
},
"west": {
"element": "Water",
"keywords": ["Pure Emotion", "Deep Feeling", "Subconscious"],
"description": "West area - pure emotional depths",
},
},
},
"Above": {
"element": "Fire",
"planet": "Sun",
"archangel": "Michael",
"keywords": ["Heaven", "Spirit", "Light"],
"description": "Upper Wall - Fire element, divine consciousness",
"areas": {
"center": {
"element": "Spirit",
"keywords": ["Divine Center", "Oneness", "Source"],
"description": "Center of Above wall - point of divine unity",
},
"above": {
"element": "Fire",
"keywords": ["Pure Spirit", "Infinite Light", "Transcendence"],
"description": "Above area - highest divine realm",
},
"below": {
"element": "Earth",
"keywords": ["Spirit in Matter", "Divine Manifestation", "Incarnation"],
"description": "Below area - spirit descending to form",
},
"east": {
"element": "Air",
"keywords": ["Divine Thought", "Holy Wisdom", "Inspiration"],
"description": "East area - divine intellect and inspiration",
},
"west": {
"element": "Water",
"keywords": ["Divine Love", "Compassion", "Mercy"],
"description": "West area - divine love and compassion",
},
},
},
"Below": {
"element": "Earth",
"planet": "Saturn",
"archangel": "Cassiel",
"keywords": ["Matter", "Foundation", "Manifestation"],
"description": "Lower Wall - Earth element, material foundation",
"areas": {
"center": {
"element": "Spirit",
"keywords": ["Material Foundation", "Grounding", "Embodiment"],
"description": "Center of Below wall - anchor point of manifestation",
},
"above": {
"element": "Fire",
"keywords": ["Active Force", "Will to Build", "Transformation"],
"description": "Above area - active principle in matter",
},
"below": {
"element": "Earth",
"keywords": ["Pure Earth", "Deep Matter", "Grounding Root"],
"description": "Below area - deepest material foundation",
},
"east": {
"element": "Air",
"keywords": ["Material Structure", "Physical Form", "Manifestation"],
"description": "East area - form emerging into manifestation",
},
"west": {
"element": "Water",
"keywords": ["Nurturing Matter", "Fertile Ground", "Sustenance"],
"description": "West area - material nourishment and growth",
},
},
},
}
def __post_init__(self) -> None:
"""Validate that all 6 walls are present."""
required_walls = {"North", "South", "East", "West", "Above", "Below"}
if set(self.walls.keys()) != required_walls:
raise ValueError(
f"CubeOfSpace must have all 6 walls, got: {set(self.walls.keys())}"
)
@classmethod
def create_default(cls) -> "CubeOfSpace":
"""
Create a CubeOfSpace with all 6 walls fully populated with built-in definitions.
Each wall has 5 directions (North, South, East, West, Center) positioned on that wall.
Each direction has a Hebrew letter and optional zodiac correspondence.
Returns:
CubeOfSpace: Fully initialized cube with all walls and directions
"""
walls = {}
# Direction name mapping - same 5 directions on every wall
# Maps old area names to consistent direction names
direction_map = {
"center": {"name": "Center", "letter": "Aleph", "zodiac": None},
"above": {"name": "North", "letter": "Bet", "zodiac": None},
"below": {"name": "South", "letter": "Gimel", "zodiac": None},
"east": {"name": "East", "letter": "Daleth", "zodiac": "Aries"},
"west": {"name": "West", "letter": "He", "zodiac": "Pisces"}
}
for wall_name, wall_data in cls._WALL_DEFINITIONS.items():
# Create directions for this wall
# Each wall has the same 5 directions: North, South, East, West, Center
directions = {}
for old_name, direction_config in direction_map.items():
if old_name in wall_data["areas"]:
direction_data = wall_data["areas"][old_name]
direction = WallDirection(
name=direction_config["name"],
letter=direction_config["letter"],
zodiac=direction_config.get("zodiac"),
element=direction_data.get("element"),
keywords=direction_data.get("keywords", []),
description=direction_data.get("description", ""),
)
# Use the direction name as key so every wall has North, South, East, West, Center
directions[direction_config["name"]] = direction
# Create the wall
wall = Wall(
name=wall_name,
side=wall_name.lower(),
opposite=Wall.OPPOSITE_WALLS[wall_name],
element=wall_data.get("element"),
planet=wall_data.get("planet"),
archangel=wall_data.get("archangel"),
keywords=wall_data.get("keywords", []),
description=wall_data.get("description", ""),
directions=directions,
)
walls[wall_name] = wall
# Create central core
central_core = WallDirection(
name="Center",
letter="Aleph",
element="Spirit",
keywords=["Unity", "Source", "All"],
description="Central core of the Cube of Space - synthesis of all forces",
)
return cls(walls=walls, center=central_core)
def wall(self, wall_name: str) -> Optional[Wall]:
"""Get a wall by name. Usage: cube.wall("north")"""
return self.walls.get(wall_name)
def opposite(self, wall_name: str) -> Optional[Wall]:
"""Get the opposite wall. Usage: cube.opposite("north")"""
opposite_name = Wall.OPPOSITE_WALLS.get(wall_name)
if not opposite_name:
return None
return self.walls.get(opposite_name)
def direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
"""Get a specific direction from a specific wall. Usage: cube.direction("north", "center")"""
wall = self.wall(wall_name)
if not wall:
return None
return wall.direction(direction_name)
def walls_all(self) -> List[Wall]:
"""Return all 6 walls as a list."""
return list(self.walls.values())
def directions(self, wall_name: str) -> list:
"""Return all 5 directions for a specific wall. Usage: cube.directions("north")"""
wall = self.wall(wall_name)
if not wall:
return []
return wall.all_directions()
# Aliases for backward compatibility
def get_wall(self, wall_name: str) -> Optional[Wall]:
"""Deprecated: use wall() instead."""
return self.wall(wall_name)
def get_direction(self, wall_name: str, direction_name: str) -> Optional[WallDirection]:
"""Deprecated: use direction() instead."""
return self.direction(wall_name, direction_name)
def get_opposite_wall(self, wall_name: str) -> Optional[Wall]:
"""Deprecated: use opposite() instead."""
return self.opposite(wall_name)
def all_walls(self) -> List[Wall]:
"""Deprecated: use walls_all() instead."""
return self.walls_all()
def all_directions_for_wall(self, wall_name: str) -> list:
"""Deprecated: use directions() instead."""
return self.directions(wall_name)

359
src/kaballah/cube/cube.py Normal file
View File

@@ -0,0 +1,359 @@
"""
Tarot Cube of Space module.
Provides hierarchical access to Cube > Wall > Direction structure.
Usage:
from tarot.cube import Cube
# Access walls
Tarot.cube.wall("North") # Get specific wall
Tarot.cube.wall().filter(element="Air") # Filter all walls
# Access directions (NEW - replaces old "area" concept)
wall = Tarot.cube.wall("North")
wall.filter("East") # Filter by direction
wall.filter(element="Fire") # Filter by attribute
wall.direction("East") # Get specific direction
"""
from typing import Optional, Any
class CubeMeta(type):
"""Metaclass to add __str__ to Cube class itself."""
def __str__(cls) -> str:
"""Return readable representation when Cube is converted to string."""
cls._ensure_initialized()
if cls._cube is None:
return "Cube of Space (not initialized)"
walls = cls._cube.walls if hasattr(cls._cube, 'walls') else {}
lines = [
"Cube of Space",
"=" * 60,
f"Walls: {len(walls)} (North, South, East, West, Above, Below)",
"",
"Structure:",
]
# Show walls with their elements and areas
for wall_name in ["North", "South", "East", "West", "Above", "Below"]:
wall = walls.get(wall_name)
if wall:
element = f" [{wall.element}]" if hasattr(wall, 'element') else ""
areas = len(wall.directions) if hasattr(wall, 'directions') else 0
lines.append(f" {wall_name}{element}: {areas} areas")
return "\n".join(lines)
def __repr__(cls) -> str:
"""Return object representation."""
cls._ensure_initialized()
if cls._cube is None:
return "Cube(not initialized)"
walls = cls._cube.walls if hasattr(cls._cube, 'walls') else {}
return f"Cube(walls={len(walls)})"
class DirectionAccessor:
"""Fluent accessor for filtering and accessing directions within a specific wall."""
_wall: Optional[Any] = None
def __init__(self, wall: Any):
"""Initialize with a Wall object."""
self._wall = wall
def all(self) -> list:
"""Get all directions in this wall."""
if self._wall is None or not hasattr(self._wall, 'directions'):
return []
return list(self._wall.directions.values())
def filter(self, direction_name: Optional[str] = None, **kwargs) -> list:
"""
Filter directions in this wall by name or any WallDirection attribute.
Args:
direction_name: Specific direction (North, South, East, West, Center)
**kwargs: Any WallDirection attribute
Returns:
List of WallDirection objects matching filters
"""
from utils.filter import universal_filter
all_dirs = self.all()
# Filter by direction name if provided
if direction_name:
all_dirs = [
d for d in all_dirs
if d.name.lower() == direction_name.lower()
]
# Apply other filters
if kwargs:
all_dirs = universal_filter(all_dirs, **kwargs)
return all_dirs
def display(self) -> str:
"""Display all directions in this wall formatted."""
from utils.filter import format_results
return format_results(self.all())
def display_filter(self, direction_name: Optional[str] = None, **kwargs) -> str:
"""
Filter directions and display results nicely formatted.
Args:
direction_name: Direction name to filter
**kwargs: Any WallDirection attribute
Example:
print(wall.display_filter("East"))
"""
from utils.filter import format_results
results = self.filter(direction_name, **kwargs)
return format_results(results)
def __call__(self, direction_name: Optional[str] = None) -> Optional[Any]:
"""Get specific direction by name."""
if direction_name is None:
return self.all()
if self._wall is None or not hasattr(self._wall, 'directions'):
return None
return self._wall.directions.get(direction_name.capitalize())
def __repr__(self) -> str:
"""Return friendly representation."""
directions = self.all()
dir_names = ", ".join([d.name for d in directions])
wall_name = self._wall.name if self._wall else "Unknown"
return f"DirectionAccessor({wall_name}: {dir_names})"
def __str__(self) -> str:
"""Return formatted string of all directions."""
return self.display()
class WallWrapper:
"""Wraps a Wall object to add DirectionAccessor for hierarchical access."""
def __init__(self, wall: Any):
"""Initialize with a Wall object."""
self._wall = wall
self._direction_accessor = DirectionAccessor(wall)
def __getattr__(self, name: str) -> Any:
"""Delegate attribute access to the wrapped wall."""
if name in ('_wall', '_direction_accessor'):
return object.__getattribute__(self, name)
return getattr(self._wall, name)
def filter(self, direction_name: Optional[str] = None, **kwargs) -> list:
"""
Filter directions in this wall.
Usage:
wall.filter("East") # Get East direction
wall.filter(element="Fire") # Get Fire directions
"""
return self._direction_accessor.filter(direction_name, **kwargs)
def display_filter(self, direction_name: Optional[str] = None, **kwargs) -> str:
"""
Filter directions and display results nicely formatted.
Usage:
wall.display_filter("East")
wall.display_filter(element="Fire")
"""
return self._direction_accessor.display_filter(direction_name, **kwargs)
def direction(self, direction_name: str) -> Optional[Any]:
"""Get a specific direction."""
return self._direction_accessor(direction_name)
def all_directions(self) -> list:
"""Get all directions in this wall."""
return self._direction_accessor.all()
def __repr__(self) -> str:
"""Return friendly representation."""
return f"Wall({self._wall.name}, {self._wall.element})"
def __str__(self) -> str:
"""Return formatted string of wall details."""
wall = self._wall
lines = [
f"--- {wall.name} ---",
f" name: {wall.name}",
f" side: {wall.side}",
f" element: {wall.element}",
f" planet: {wall.planet}",
f" opposite: {wall.opposite}",
f" archangel: {wall.archangel}",
f" keywords: {', '.join(wall.keywords) if wall.keywords else 'None'}",
f" description: {wall.description}",
f" directions: {', '.join(wall.directions.keys())}",
]
return "\n".join(lines)
class WallAccessor:
"""Fluent accessor for filtering and accessing Cube walls."""
_cube: Optional["CubeOfSpace"] = None # type: ignore
_initialized: bool = False
@classmethod
def _ensure_initialized(cls) -> None:
"""Lazy-initialize the Cube on first access."""
if cls._initialized:
return
from kaballah.cube.attributes import CubeOfSpace
WallAccessor._cube = CubeOfSpace.create_default()
WallAccessor._initialized = True
def all(self) -> list:
"""Get all walls."""
self._ensure_initialized()
if WallAccessor._cube is None:
return []
return WallAccessor._cube.all_walls()
def filter(self, **kwargs) -> list:
"""
Filter walls by any Wall attribute.
Uses the universal filter for consistency across the project.
Args:
**kwargs: Any Wall attribute with its value
Usage:
Cube.wall.filter(element="Air")
Cube.wall.filter(planet="Mercury")
Returns:
List of Wall objects matching all filters
"""
from utils.filter import universal_filter
return universal_filter(self.all(), **kwargs)
def display(self) -> str:
"""Display all walls formatted."""
from utils.filter import format_results
return format_results(self.all())
def display_filter(self, **kwargs) -> str:
"""
Filter walls and display results nicely formatted.
Args:
**kwargs: Any Wall attribute with its value
Returns:
Formatted string with filtered walls
Example:
print(Cube.wall.display_filter(element="Air"))
"""
results = self.filter(**kwargs)
# Use the custom __str__ method of Wall objects for proper line-by-line formatting
return "\n\n".join([str(wall) for wall in results])
def __call__(self, wall_name: Optional[str] = None) -> Optional[Any]:
"""Get a specific wall by name or return all walls.
Deprecated: Use filter(side="north") instead.
"""
self._ensure_initialized()
if wall_name is None:
return self.all()
if WallAccessor._cube is None:
return None
return WallAccessor._cube.wall(wall_name)
def __repr__(self) -> str:
"""Return friendly representation showing all walls."""
walls = self.all()
wall_names = ", ".join([w.name for w in walls])
return f"WallAccessor({wall_names})"
def __str__(self) -> str:
"""Return formatted string of all walls."""
return self.display()
class Cube(metaclass=CubeMeta):
"""
Unified accessor for Cube of Space correspondences.
Hierarchical structure: Cube > Wall > Direction
Usage:
# Filter walls by side
north = Cube.wall.filter(side="north")[0] # Get north wall
air_walls = Cube.wall.filter(element="Air") # Filter by element
# Access all walls
all_walls = Cube.wall.all() # Get all 6 walls
# Work with directions within a wall
wall = Cube.wall.filter(side="north")[0]
east_dir = wall.direction("East") # Get direction
fire_dirs = wall.filter(element="Fire") # Filter directions
"""
_cube: Optional["CubeOfSpace"] = None # type: ignore
_initialized: bool = False
_wall_accessor: Optional[WallAccessor] = None
@classmethod
def _ensure_initialized(cls) -> None:
"""Lazy-initialize the Cube of Space on first access."""
if cls._initialized:
return
from kaballah.cube.attributes import CubeOfSpace
cls._cube = CubeOfSpace.create_default()
cls._initialized = True
@classmethod
def _get_wall_accessor(cls) -> "WallAccessor":
"""Get or create the wall accessor."""
cls._ensure_initialized()
if cls._wall_accessor is None:
cls._wall_accessor = WallAccessor()
return cls._wall_accessor
# Use a descriptor to make wall work like a property on the class
class WallProperty:
"""Descriptor that returns wall accessor when accessed."""
def __get__(self, obj: Any, objtype: Optional[type] = None) -> "WallAccessor":
if objtype is None:
objtype = type(obj)
return objtype._get_wall_accessor()
wall = WallProperty()
@classmethod
def opposite_wall(cls, wall_name: str) -> Optional[object]:
"""Get the opposite wall."""
cls._ensure_initialized()
if cls._cube is None:
return None
return cls._cube.opposite_wall(wall_name)

View File

@@ -0,0 +1,5 @@
"""Tree namespace - access Tree of Life, Sephiroth, and Paths."""
from .tree import Tree
__all__ = ["Tree"]

136
src/kaballah/tree/tree.py Normal file
View File

@@ -0,0 +1,136 @@
"""
Tarot Tree of Life module.
Provides access to Sephiroth, Paths, and Tree of Life correspondences.
Usage:
from tarot.tree import Tree
sephera = Tree.sephera(1) # Get Sephira 1 (Kether)
path = Tree.path(11) # Get Path 11
all_sepheras = Tree.sephera() # Get all Sephiroth
print(Tree()) # Display Tree structure
"""
from typing import TYPE_CHECKING, Dict, List, Optional, Union, overload
if TYPE_CHECKING:
from tarot.attributes import Sephera, Path
from tarot.card.data import CardDataLoader
from utils.query import QueryResult, Query
class TreeMeta(type):
"""Metaclass to add __str__ to Tree class itself."""
def __str__(cls) -> str:
"""Return readable representation when Tree is converted to string."""
# Access Tree class attributes through type.__getattribute__
Tree._ensure_initialized()
sepheras = type.__getattribute__(cls, '_sepheras')
paths = type.__getattribute__(cls, '_paths')
lines = [
"Tree of Life",
"=" * 60,
f"Sephiroth: {len(sepheras)} nodes",
f"Paths: {len(paths)} connections",
"",
"Structure:",
]
# Show Sephira hierarchy
for num in sorted(sepheras.keys()):
seph = sepheras[num]
lines.append(f" {num}. {seph.name} ({seph.hebrew_name})")
return "\n".join(lines)
def __repr__(cls) -> str:
"""Return object representation."""
Tree._ensure_initialized()
sepheras = type.__getattribute__(cls, '_sepheras')
paths = type.__getattribute__(cls, '_paths')
return f"Tree(sepheras={len(sepheras)}, paths={len(paths)})"
class Tree(metaclass=TreeMeta):
"""
Unified accessor for Tree of Life correspondences.
All methods are class methods, so Tree is accessed as a static namespace:
sephera = Tree.sephera(1)
path = Tree.path(11)
print(Tree()) # Displays tree structure
"""
_sepheras: Dict[int, 'Sephera'] = {} # type: ignore
_paths: Dict[int, 'Path'] = {} # type: ignore
_initialized: bool = False
_loader: Optional['CardDataLoader'] = None # type: ignore
@classmethod
def _ensure_initialized(cls) -> None:
"""Lazy-load data from CardDataLoader on first access."""
if cls._initialized:
return
from tarot.card.data import CardDataLoader
cls._loader = CardDataLoader()
cls._sepheras = cls._loader._sephera
cls._paths = cls._loader._paths
cls._initialized = True
@classmethod
@overload
def sephera(cls, number: int) -> Optional['Sephera']:
...
@classmethod
@overload
def sephera(cls, number: None = ...) -> Dict[int, 'Sephera']:
...
@classmethod
def sephera(cls, number: Optional[int] = None) -> Union[Optional['Sephera'], Dict[int, 'Sephera']]:
"""Return a Sephira or all Sephiroth."""
cls._ensure_initialized()
if number is None:
return cls._sepheras.copy()
return cls._sepheras.get(number)
@classmethod
@overload
def path(cls, number: int) -> Optional['Path']:
...
@classmethod
@overload
def path(cls, number: None = ...) -> Dict[int, 'Path']:
...
@classmethod
def path(cls, number: Optional[int] = None) -> Union[Optional['Path'], Dict[int, 'Path']]:
"""Return a Path or all Paths."""
cls._ensure_initialized()
if number is None:
return cls._paths.copy()
return cls._paths.get(number)
@classmethod
def filter(cls, expression: str) -> 'Query':
"""
Filter Sephiroth by attribute:value expression.
Examples:
Tree.filter('name:Kether').first()
Tree.filter('number:1').first()
Tree.filter('sphere:1').all()
Returns a Query object for chaining.
"""
from tarot.query import Query
cls._ensure_initialized()
# Create a query from all Sephiroth
return Query(cls._sepheras).filter(expression)

27
src/letter/__init__.py Normal file
View File

@@ -0,0 +1,27 @@
"""
Letter namespace - Alphabets, Letters, Ciphers, I Ching, Periodic Table, Words.
Provides fluent query interface for:
- Alphabets (English, Hebrew, Greek)
- Ciphers and word encoding
- Hebrew letters with Tarot correspondences (paths)
- I Ching trigrams and hexagrams
- Periodic table with Sephiroth
- Word analysis and cipher operations
Usage:
from tarot import letter
letter.alphabet('english')
letter.words.word('MAGICK').cipher('english_simple')
letter.iching.hexagram(1)
letter.paths('aleph') # Get Hebrew letter with Tarot correspondences
"""
from .letter import letter
from .iChing import trigram, hexagram
from .words import word
from .paths import letters
__all__ = ["letter", "trigram", "hexagram", "word", "letters"]

249
src/letter/attributes.py Normal file
View File

@@ -0,0 +1,249 @@
"""
Letter attributes and data structures.
This module defines attributes specific to the Letter module,
including Alphabets, Enochian letters, and Double Letter Trumps.
"""
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple, Any
from utils.attributes import (
Element,
ElementType,
Planet,
Meaning,
)
@dataclass
class Letter:
"""Represents a letter with its attributes."""
character: str
position: int
name: str
@dataclass
class EnglishAlphabet:
"""English alphabet with Tarot/Kabbalistic correspondence."""
letter: str
position: int
sound: str
def __post_init__(self) -> None:
if not (1 <= self.position <= 26):
raise ValueError(f"Position must be between 1 and 26, got {self.position}")
if len(self.letter) != 1 or not self.letter.isalpha():
raise ValueError(f"Letter must be a single alphabetic character, got {self.letter}")
@dataclass
class GreekAlphabet:
"""Greek alphabet with Tarot/Kabbalistic correspondence."""
letter: str
position: int
transliteration: str
def __post_init__(self) -> None:
if not (1 <= self.position <= 24):
raise ValueError(f"Position must be between 1 and 24, got {self.position}")
@dataclass
class HebrewAlphabet:
"""Hebrew alphabet with Tarot/Kabbalistic correspondence."""
letter: str
position: int
transliteration: str
meaning: str
def __post_init__(self) -> None:
if not (1 <= self.position <= 22):
raise ValueError(f"Position must be between 1 and 22, got {self.position}")
@dataclass
class DoublLetterTrump:
"""Represents a Double Letter Trump (Yodh through Tau, 3-21 of Major Arcana)."""
number: int # 3-21 (19 double letter trumps)
name: str # Full name (e.g., "The Empress")
hebrew_letter_1: str # First Hebrew letter (e.g., "Gimel")
hebrew_letter_2: Optional[str] = None # Second Hebrew letter if applicable
planet: Optional['Planet'] = None # Associated planet
tarot_trump: Optional[str] = None # e.g., "III - The Empress"
astrological_sign: Optional[str] = None # Zodiac sign if any
element: Optional['ElementType'] = None # Associated element
number_value: Optional[int] = None # Numerological value
keywords: List[str] = field(default_factory=list)
meaning: Optional['Meaning'] = None # Upright and reversed meanings
description: str = ""
def __post_init__(self) -> None:
if not 3 <= self.number <= 21:
raise ValueError(f"Double Letter Trump number must be 3-21, got {self.number}")
@dataclass(frozen=True)
class EnochianLetter:
"""Represents an Enochian letter with its properties."""
name: str # Enochian letter name
letter: str # The letter itself
hebrew_equivalent: Optional[str] = None
tarot_correspondence: Optional[str] = None
planet: Optional[str] = None
element: Optional[str] = None
keywords: List[str] = field(default_factory=list)
description: str = ""
@dataclass(frozen=True)
class EnochianSpirit:
"""Represents an Enochian spirit or intelligence."""
name: str # Spirit name
rank: str # e.g., "King", "Prince", "Duke", "Intelligence"
element: Optional[str] = None
direction: Optional[str] = None # e.g., "East", "South", etc.
sigil: Optional[str] = None # ASCII representation or description
keywords: List[str] = field(default_factory=list)
description: str = ""
@dataclass(frozen=True)
class EnochianArchetype:
"""
Archetypal form of an Enochian Tablet.
Provides a 4x4 grid with positions that can be filled with different
visual representations (colors, images, symbols, etc.).
"""
name: str # e.g., "Tablet of Air Archetype"
tablet_name: str # Reference to parent tablet
grid: Dict[Tuple[int, int], 'EnochianGridPosition'] = field(default_factory=dict) # 4x4 grid
row_correspondences: List[Dict[str, Any]] = field(default_factory=list) # Row meanings (4 rows)
col_correspondences: List[Dict[str, Any]] = field(default_factory=list) # Column meanings (4 cols)
keywords: List[str] = field(default_factory=list)
description: str = ""
def get_position(self, row: int, col: int) -> Optional['EnochianGridPosition']:
"""Get the grid position at (row, col)."""
if not 0 <= row < 4 or not 0 <= col < 4:
return None
return self.grid.get((row, col))
def get_row_correspondence(self, row: int) -> Optional[Dict[str, Any]]:
"""Get the meaning/correspondence for a row."""
if 0 <= row < len(self.row_correspondences):
return self.row_correspondences[row]
return None
def get_col_correspondence(self, col: int) -> Optional[Dict[str, Any]]:
"""Get the meaning/correspondence for a column."""
if 0 <= col < len(self.col_correspondences):
return self.col_correspondences[col]
return None
@dataclass(frozen=True)
class EnochianGridPosition:
"""
Represents a single position in an Enochian Tablet grid.
A 4x4 grid cell with:
- Center letter
- Directional letters (N, S, E, W)
- Archetypal correspondences (Tarot, element, etc.)
"""
row: int # Grid row (0-3)
col: int # Grid column (0-3)
center_letter: str # The main letter at this position
north_letter: Optional[str] = None # Letter above
south_letter: Optional[str] = None # Letter below
east_letter: Optional[str] = None # Letter to the right
west_letter: Optional[str] = None # Letter to the left
tarot_card: Optional[str] = None # Associated Tarot card (e.g., "Ace of Swords")
tarot_suit: Optional[str] = None # Suit correspondence (Swords, Wands, Cups, Pentacles)
tarot_number: Optional[int] = None # Card number (0-13, 0=Ace)
element: Optional[str] = None # Element correspondence
zodiac_sign: Optional[str] = None # Zodiac correspondence
planetary_hour: Optional[str] = None # Associated hour
keywords: List[str] = field(default_factory=list)
meanings: List[str] = field(default_factory=list)
def get_all_letters(self) -> Dict[str, str]:
"""Get all letters in this position: center and directional."""
letters = {"center": self.center_letter}
if self.north_letter:
letters["north"] = self.north_letter
if self.south_letter:
letters["south"] = self.south_letter
if self.east_letter:
letters["east"] = self.east_letter
if self.west_letter:
letters["west"] = self.west_letter
return letters
@dataclass(frozen=True)
class EnochianTablet:
"""
Represents an Enochian Tablet.
The Enochian system contains:
- 4 elemental tablets (Earth, Water, Air, Fire)
- 1 tablet of union (Aethyr)
- Each tablet is 12x12 (144 squares) containing Enochian letters
- Archetypal form with 4x4 grid for user customization
"""
name: str # e.g., "Tablet of Earth", "Tablet of Air", etc.
number: int # Tablet identifier (1-5)
element: Optional[str] = None # Earth, Water, Air, Fire, or Aethyr/Union
rulers: List[str] = field(default_factory=list) # Names of spirits ruling this tablet
archangels: List[str] = field(default_factory=list) # Associated archangels
letters: Dict[Tuple[int, int], str] = field(default_factory=dict) # Grid of letters (row, col) -> letter
planetary_hours: List[str] = field(default_factory=list) # Associated hours
keywords: List[str] = field(default_factory=list)
description: str = ""
archetype: Optional[EnochianArchetype] = None # Archetypal form for visualization
# Valid tablets
VALID_TABLETS = {
"Tablet of Union", # Aethyr
"Tablet of Earth",
"Tablet of Water",
"Tablet of Air",
"Tablet of Fire",
}
def __post_init__(self) -> None:
if self.name not in self.VALID_TABLETS:
raise ValueError(
f"Invalid tablet '{self.name}'. "
f"Valid tablets: {', '.join(self.VALID_TABLETS)}"
)
# Tablet of Union uses 0, elemental tablets use 1-5
valid_range = (0, 0) if "Union" in self.name else (1, 5)
if not valid_range[0] <= self.number <= valid_range[1]:
raise ValueError(
f"Tablet number must be {valid_range[0]}-{valid_range[1]}, got {self.number}"
)
def is_elemental(self) -> bool:
"""Check if this is an elemental tablet (not union)."""
return self.element in {"Earth", "Water", "Air", "Fire"}
def is_union(self) -> bool:
"""Check if this is the Tablet of Union (Aethyr)."""
return self.element == "Aethyr" or "Union" in self.name
def get_letter(self, row: int, col: int) -> Optional[str]:
"""Get letter at specific grid position."""
return self.letters.get((row, col))
def get_row(self, row: int) -> List[Optional[str]]:
"""Get all letters in a row."""
return [self.letters.get((row, col)) for col in range(12)]
def get_column(self, col: int) -> List[Optional[str]]:
"""Get all letters in a column."""
return [self.letters.get((row, col)) for row in range(12)]

220
src/letter/iChing.py Normal file
View File

@@ -0,0 +1,220 @@
"""I Ching trigrams and hexagrams module.
Provides fluent query interface for I Ching trigrams and hexagrams,
including Tarot correspondences and binary representations.
Usage:
from letter.iChing import trigram, hexagram
qian = trigram.trigram('Qian')
creative = hexagram.hexagram(1)
"""
from typing import TYPE_CHECKING, Dict, Optional
from utils.query import CollectionAccessor
if TYPE_CHECKING:
from tarot.card.data import CardDataLoader
from tarot.attributes import Trigram, Hexagram
def _line_diagram_from_binary(binary: str) -> str:
"""Render a sideways ASCII diagram where top lines appear on the right."""
if not binary:
return ""
cleaned = [bit for bit in binary if bit in {"0", "1"}]
if not cleaned:
return ""
symbol_map = {"1": "|", "0": ":"}
# Reverse so the right-most character represents the top line.
return "".join(symbol_map[bit] for bit in reversed(cleaned))
class _Trigram:
"""Fluent query accessor for I Ching trigrams."""
def __init__(self) -> None:
self._initialized: bool = False
self._trigrams: Dict[str, 'Trigram'] = {}
self.trigram = CollectionAccessor(self._get_trigrams)
def _ensure_initialized(self) -> None:
"""Load trigrams on first access."""
if self._initialized:
return
self._load_trigrams()
self._initialized = True
def _get_trigrams(self):
self._ensure_initialized()
return self._trigrams.copy()
def _load_trigrams(self) -> None:
"""Load the eight I Ching trigrams."""
from tarot.attributes import Trigram
trigram_specs = [
{"name": "Qian", "chinese": "", "pinyin": "Qián", "element": "Heaven", "attribute": "Creative", "binary": "111", "description": "Pure yang drive that initiates action."},
{"name": "Dui", "chinese": "", "pinyin": "Duì", "element": "Lake", "attribute": "Joyous", "binary": "011", "description": "Open delight that invites community."},
{"name": "Li", "chinese": "", "pinyin": "", "element": "Fire", "attribute": "Clinging", "binary": "101", "description": "Radiant clarity that adheres to insight."},
{"name": "Zhen", "chinese": "", "pinyin": "Zhèn", "element": "Thunder", "attribute": "Arousing", "binary": "001", "description": "Sudden awakening that shakes stagnation."},
{"name": "Xun", "chinese": "", "pinyin": "Xùn", "element": "Wind", "attribute": "Gentle", "binary": "110", "description": "Penetrating influence that persuades subtly."},
{"name": "Kan", "chinese": "", "pinyin": "Kǎn", "element": "Water", "attribute": "Abysmal", "binary": "010", "description": "Depth, risk, and sincere feeling."},
{"name": "Gen", "chinese": "", "pinyin": "Gèn", "element": "Mountain", "attribute": "Stillness", "binary": "100", "description": "Grounded rest that establishes boundaries."},
{"name": "Kun", "chinese": "", "pinyin": "Kūn", "element": "Earth", "attribute": "Receptive", "binary": "000", "description": "Vast receptivity that nurtures form."},
]
self._trigrams = {}
for spec in trigram_specs:
name = spec.get("name")
if not name:
raise ValueError("Trigram spec missing 'name'")
binary = spec.get("binary", "")
self._trigrams[name.lower()] = Trigram(
name=name,
chinese_name=spec.get("chinese", ""),
pinyin=spec.get("pinyin", ""),
element=spec.get("element", ""),
attribute=spec.get("attribute", ""),
binary=binary,
description=spec.get("description", ""),
line_diagram=_line_diagram_from_binary(binary),
)
class _Hexagram:
"""Fluent query accessor for I Ching hexagrams."""
def __init__(self) -> None:
self._initialized: bool = False
self._hexagrams: Dict[int, 'Hexagram'] = {}
self.hexagram = CollectionAccessor(self._get_hexagrams)
def _ensure_initialized(self) -> None:
"""Load hexagrams on first access."""
if self._initialized:
return
self._load_hexagrams()
self._initialized = True
def _get_hexagrams(self):
self._ensure_initialized()
return self._hexagrams.copy()
def _load_hexagrams(self) -> None:
"""Load all 64 I Ching hexagrams."""
from tarot.attributes import Hexagram
from tarot.card.data import CardDataLoader, calculate_digital_root
# Ensure trigrams are loaded first
trigram._ensure_initialized()
# Load planets for hexagram correspondences
loader = CardDataLoader()
hex_specs = [
{"number": 1, "name": "Creative Force", "chinese": "", "pinyin": "Qián", "judgement": "Initiative succeeds when anchored in integrity.", "image": "Heaven above and below mirrors unstoppable drive.", "upper": "Qian", "lower": "Qian", "keywords": "Leadership|Momentum|Clarity"},
{"number": 2, "name": "Receptive Field", "chinese": "", "pinyin": "Kūn", "judgement": "Grounded support flourishes through patience.", "image": "Earth layered upon earth offers fertile space.", "upper": "Kun", "lower": "Kun", "keywords": "Nurture|Support|Yielding"},
{"number": 3, "name": "Sprouting", "chinese": "", "pinyin": "Zhūn", "judgement": "Challenges at the start need perseverance.", "image": "Water over thunder shows storms that germinate seeds.", "upper": "Kan", "lower": "Zhen", "keywords": "Beginnings|Struggle|Resolve"},
{"number": 4, "name": "Youthful Insight", "chinese": "", "pinyin": "Méng", "judgement": "Ignorance yields to steady guidance.", "image": "Mountain above water signals learning via restraint.", "upper": "Gen", "lower": "Kan", "keywords": "Study|Mentorship|Humility"},
{"number": 5, "name": "Waiting", "chinese": "", "pinyin": "", "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 = {}
for spec in hex_specs:
number = spec.get("number")
name = spec.get("name")
upper_name = spec.get("upper")
lower_name = spec.get("lower")
if number is None or not name or not upper_name or not lower_name:
continue
upper = trigram._trigrams.get(upper_name.lower())
lower = trigram._trigrams.get(lower_name.lower())
if upper is None or lower is None:
continue
assoc_number = loader._numbers.get(calculate_digital_root(number))
planet_name = spec.get("planet") or planet_cycle[(number - 1) % len(planet_cycle)]
planet = loader._planets.get(planet_name.lower()) if planet_name else None
keywords_field = spec.get("keywords")
keywords = keywords_field.split("|") if keywords_field else []
line_diagram = _line_diagram_from_binary(upper.binary + lower.binary)
self._hexagrams[number] = Hexagram(
number=number,
name=name,
chinese_name=spec.get("chinese", ""),
pinyin=spec.get("pinyin", ""),
judgement=spec.get("judgement", ""),
image=spec.get("image", ""),
upper_trigram=upper,
lower_trigram=lower,
keywords=keywords,
associated_number=assoc_number,
planetary_influence=planet,
line_diagram=line_diagram,
)
# Create singleton instances
trigram = _Trigram()
hexagram = _Hexagram()

View File

@@ -0,0 +1,42 @@
"""
I Ching attributes and data structures.
This module defines attributes specific to the I Ching system,
including Trigrams and Hexagrams.
"""
from dataclasses import dataclass, field
from typing import List, Optional
from utils.attributes import Number, Planet
@dataclass
class Trigram:
"""Represents one of the eight I Ching trigrams."""
name: str
chinese_name: str
pinyin: str
element: str
attribute: str
binary: str
description: str = ""
line_diagram: str = ""
@dataclass
class Hexagram:
"""Represents an I Ching hexagram with Tarot correspondence."""
number: int
name: str
chinese_name: str
pinyin: str
judgement: str
image: str
upper_trigram: Trigram
lower_trigram: Trigram
keywords: List[str] = field(default_factory=list)
associated_number: Optional[Number] = None
planetary_influence: Optional[Planet] = None
notes: str = ""
line_diagram: str = ""

87
src/letter/letter.py Normal file
View File

@@ -0,0 +1,87 @@
"""Tarot letter namespace - fluent query interface."""
from typing import TYPE_CHECKING
from utils.query import CollectionAccessor
if TYPE_CHECKING:
from tarot.card.data import CardDataLoader
class Letter:
"""Fluent query accessor for letters, alphabets, ciphers, and correspondences."""
def __init__(self) -> None:
self._initialized: bool = False
self._loader: 'CardDataLoader | None' = None
self.alphabet = CollectionAccessor(self._get_alphabets)
self.cipher = CollectionAccessor(self._get_ciphers)
self.letter = CollectionAccessor(self._get_letters)
self.iching = CollectionAccessor(self._get_hexagrams)
self.periodic = CollectionAccessor(self._get_periodic)
def _ensure_initialized(self) -> None:
"""Lazy-load data from CardDataLoader on first access."""
if self._initialized:
return
from tarot.card.data import CardDataLoader
self._loader = CardDataLoader()
self._initialized = True
def _require_loader(self) -> 'CardDataLoader':
self._ensure_initialized()
assert self._loader is not None, "Loader not initialized"
return self._loader
def _get_alphabets(self):
loader = self._require_loader()
return loader._alphabets.copy()
def _get_ciphers(self):
loader = self._require_loader()
return loader._ciphers.copy()
def _get_letters(self):
loader = self._require_loader()
return dict(loader.letter())
def _get_hexagrams(self):
loader = self._require_loader()
return loader._hexagrams.copy()
def _get_periodic(self):
loader = self._require_loader()
return loader._periodic_table.copy()
def word(self, text: str, *, alphabet: str = 'english'):
"""
Start a fluent cipher request for the given text.
Usage:
letter.word('MAGICK').cipher('english_simple')
letter.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
"""
loader = self._require_loader()
return loader.word(text, alphabet=alphabet)
def __str__(self) -> str:
"""Return a nice summary of the letter accessor."""
return (
"Letter Namespace - Alphabets, Letters, Ciphers, I Ching, Periodic Table, Words\n\n"
"Access methods:\n"
" letter.alphabet - English, Hebrew, Greek alphabets\n"
" letter.cipher - Cipher systems (English simple, Hebrew, etc.)\n"
" letter.letter - Hebrew letters (Aleph through Tau)\n"
" letter.word(text) - Encode text with cipher systems\n"
" letter.iching - I Ching trigrams and hexagrams\n"
" letter.periodic - Periodic table with Sephiroth"
)
def __repr__(self) -> str:
"""Return a nice representation of the letter accessor."""
return self.__str__()
# Create singleton instance
letter = Letter()

340
src/letter/paths.py Normal file
View File

@@ -0,0 +1,340 @@
"""
Tarot Letters namespace - Hebrew letters with full correspondences.
Provides fluent access to Hebrew letters (Paths on Tree of Life) organized by type:
- Mother Letters (3): Aleph, Mem, Shin
- Double Letters (7): Beth through Tzadi
- Simple Letters (12): Yodh through Tau
Data is sourced from CardDataLoader.paths() for a single source of truth.
Each letter has attributes like:
- Hebrew Letter
- Zodiac (for simple letters)
- Trump (Major Arcana card)
- Four Color System (King, Queen, Prince, Princess)
- Cube of Space correspondence
- Intelligence/Archangel
- Musical Note
"""
from typing import List, Optional, Dict, Union, TYPE_CHECKING
from dataclasses import dataclass, field
from utils.filter import universal_filter, get_filterable_fields, format_results
if TYPE_CHECKING:
from utils.query import CollectionAccessor
from tarot.attributes import Path
@dataclass
class TarotLetter:
"""
Represents a Hebrew letter with full Tarot correspondences.
Wraps Path objects from CardDataLoader to provide a letter-focused interface
while maintaining a single source of truth.
"""
path: 'Path' # Reference to the actual Path object from CardDataLoader
letter_type: str # "Mother", "Double", or "Simple" (derived from path)
def __post_init__(self) -> None:
"""Validate that path is set."""
if not self.path:
raise ValueError("TarotLetter requires a valid Path object")
@property
def hebrew_letter(self) -> str:
"""Get Hebrew letter character."""
return self.path.hebrew_letter or ""
@property
def transliteration(self) -> str:
"""Get transliterated name."""
return self.path.transliteration or ""
@property
def position(self) -> int:
"""Get position (1-22 for paths)."""
return self.path.number
@property
def trump(self) -> Optional[str]:
"""Get Tarot trump designation."""
return self.path.tarot_trump
@property
def element(self) -> Optional[str]:
"""Get element name if applicable."""
return self.path.element.name if self.path.element else None
@property
def planet(self) -> Optional[str]:
"""Get planet name if applicable."""
return self.path.planet.name if self.path.planet else None
@property
def zodiac(self) -> Optional[str]:
"""Get zodiac sign if applicable."""
return self.path.zodiac_sign
@property
def intelligence(self) -> Optional[str]:
"""Get archangel/intelligence name from associated gods."""
# Extract first god's name from the path's associated gods
all_gods = self.path.get_gods()
if all_gods:
return all_gods[0].name
return None
@property
def meaning(self) -> Optional[str]:
"""Get path meaning/description."""
return self.path.description
@property
def keywords(self) -> List[str]:
"""Get keywords associated with path."""
return self.path.keywords or []
def display(self) -> str:
"""Format letter for display."""
lines = [
f"Hebrew: {self.hebrew_letter}",
f"Name: {self.transliteration}",
f"Type: {self.letter_type}",
f"Position: {self.position}",
]
if self.trump:
lines.append(f"Trump: {self.trump}")
if self.zodiac:
lines.append(f"Zodiac: {self.zodiac}")
if self.planet:
lines.append(f"Planet: {self.planet}")
if self.element:
lines.append(f"Element: {self.element}")
if self.intelligence:
lines.append(f"Intelligence: {self.intelligence}")
if self.meaning:
lines.append(f"Meaning: {self.meaning}")
if self.keywords:
lines.append(f"Keywords: {', '.join(self.keywords)}")
return "\n".join(lines)
class LetterAccessor:
"""Fluent accessor for Tarot letters."""
def __init__(self, letters_dict: Dict[str, TarotLetter]) -> None:
self._letters = letters_dict
def __call__(self, transliteration: str) -> Optional[TarotLetter]:
"""Get a letter by transliteration (e.g., 'aleph', 'beth', 'gimel')."""
return self._letters.get(transliteration.lower())
def __getitem__(self, key: Union[str, int]) -> Optional[TarotLetter]:
"""Get letter by name or position."""
if isinstance(key, int):
# Get by position (1-22)
for letter in self._letters.values():
if letter.position == key:
return letter
return None
return self(key)
def all(self) -> List[TarotLetter]:
"""Get all letters."""
return sorted(self._letters.values(), key=lambda x: x.position)
def by_type(self, letter_type: str) -> List[TarotLetter]:
"""Filter by type: 'Mother', 'Double', or 'Simple'."""
return [l for l in self._letters.values() if l.letter_type == letter_type]
def by_zodiac(self, zodiac: str) -> Optional[TarotLetter]:
"""Get letter by zodiac sign."""
for letter in self._letters.values():
if letter.zodiac and zodiac.lower() in letter.zodiac.lower():
return letter
return None
def by_planet(self, planet: str) -> List[TarotLetter]:
"""Get letters by planet."""
return [l for l in self._letters.values() if l.planet and planet.lower() in l.planet.lower()]
def by_trump(self, trump: str) -> Optional[TarotLetter]:
"""Get letter by tarot trump."""
return next((l for l in self._letters.values() if l.trump == trump), None)
def get_filterable_fields(self) -> List[str]:
"""
Dynamically get all filterable fields from TarotLetter.
Returns the same fields as the universal filter utility.
Useful for introspection and validation.
"""
return get_filterable_fields(TarotLetter)
def filter(self, **kwargs) -> List[TarotLetter]:
"""
Filter letters by any TarotLetter attribute.
Uses the universal filter from utils.filter for consistency
across the entire project.
The filter automatically handles all fields from the TarotLetter dataclass:
- letter_type, element, trump, zodiac, planet
- king, queen, prince, princess
- cube, intelligence, note, meaning, hebrew_letter, transliteration, position
- keywords (list matching)
Args:
**kwargs: Any TarotLetter attribute with its value
Usage:
Tarot.letters.filter(letter_type="Simple")
Tarot.letters.filter(element="Fire")
Tarot.letters.filter(letter_type="Double", planet="Mars")
Tarot.letters.filter(element="Air", letter_type="Mother")
Tarot.letters.filter(intelligence="Metatron")
Tarot.letters.filter(position=1)
Returns:
List of TarotLetter objects matching all filters
"""
return universal_filter(self.all(), **kwargs)
def display_filter(self, **kwargs) -> str:
"""
Filter letters and display results nicely formatted.
Combines filtering and formatting in one call.
Args:
**kwargs: Any TarotLetter attribute with its value
Returns:
Formatted string with filtered letters
Example:
print(Tarot.letters.display_filter(element="Fire"))
"""
results = self.filter(**kwargs)
return format_results(results)
def display_all(self) -> str:
"""Display all letters formatted."""
lines = []
for letter in self.all():
lines.append(letter.display())
lines.append("-" * 50)
return "\n".join(lines)
def display_by_type(self, letter_type: str) -> str:
"""Display all letters of a specific type."""
letters = self.by_type(letter_type)
if not letters:
return f"No letters found with type: {letter_type}"
lines = [f"\n{letter_type.upper()} LETTERS ({len(letters)} total)"]
lines.append("=" * 50)
for letter in letters:
lines.append(letter.display())
lines.append("-" * 50)
return "\n".join(lines)
@property
def iChing(self):
"""Access I Ching trigrams and hexagrams."""
return IChing()
class IChing:
"""Namespace for I Ching trigrams and hexagrams access.
Provides fluent query interface for accessing I Ching trigrams and hexagrams
with Tarot correspondences.
Usage:
trigrams = Tarot.letters.iChing.trigram
qian = trigrams.name('Qian')
all_trigrams = trigrams.all()
hexagrams = Tarot.letters.iChing.hexagram
hex1 = hexagrams.all()[1]
all_hex = hexagrams.list()
"""
trigram: 'CollectionAccessor'
hexagram: 'CollectionAccessor'
def __init__(self) -> None:
"""Initialize iChing accessor with trigram and hexagram collections."""
from tarot.letter import iChing as iching_module
self.trigram = iching_module.trigram.trigram
self.hexagram = iching_module.hexagram.hexagram
def __repr__(self) -> str:
"""Clean representation of iChing namespace."""
return "IChing(trigram, hexagram)"
def __str__(self) -> str:
"""String representation of iChing namespace."""
return "I Ching (trigrams and hexagrams)"
class LettersRegistry:
"""Registry and accessor for all Hebrew letters with Tarot correspondences."""
_instance: Optional['LettersRegistry'] = None
_letters: Dict[str, TarotLetter] = {}
_initialized: bool = False
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
if self._initialized:
return
self._initialize_letters()
self._initialized = True
def _initialize_letters(self) -> None:
"""Initialize all 22 Hebrew letters by wrapping Path objects from CardDataLoader."""
from tarot.card.data import CardDataLoader
loader = CardDataLoader()
paths = loader.path() # Get all 22 paths
self._letters = {}
# Map each path (11-32) to a TarotLetter with appropriate type
for path_number, path in paths.items():
# Determine letter type based on path number
# Mother letters: 11 (Aleph), 23 (Mem), 31 (Shin)
# Double letters: 12, 13, 14, 15, 18, 21, 22
# Simple (Zodiacal/Planetary): 16, 17, 19, 20, 24, 25, 26, 27, 28, 29, 30, 32
if path_number in {11, 23, 31}:
letter_type = "Mother"
elif path_number in {12, 13, 14, 15, 18, 21, 22}:
letter_type = "Double"
else:
letter_type = "Simple"
# Create TarotLetter wrapping the path
letter_key = path.transliteration.lower()
self._letters[letter_key] = TarotLetter(path=path, letter_type=letter_type)
def accessor(self) -> LetterAccessor:
"""Get the letter accessor."""
return LetterAccessor(self._letters)
def letters() -> LetterAccessor:
"""Get the letters accessor for fluent queries."""
registry = LettersRegistry()
return registry.accessor()

View File

@@ -0,0 +1,5 @@
"""Words namespace - word cipher and gematria operations."""
from .word import word
__all__ = ["word"]

40
src/letter/words/word.py Normal file
View File

@@ -0,0 +1,40 @@
"""Tarot word namespace - fluent cipher operations."""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tarot.card.data import CardDataLoader
class _Word:
"""Fluent accessor for word analysis and cipher operations."""
_loader: 'CardDataLoader | None' = None
_initialized: bool = False
@classmethod
def _ensure_initialized(cls) -> None:
"""Lazy-load CardDataLoader on first access."""
if cls._initialized:
return
from tarot.card.data import CardDataLoader
cls._loader = CardDataLoader()
cls._initialized = True
@classmethod
def word(cls, text: str, *, alphabet: str = 'english'):
"""
Start a fluent cipher request for the given text.
Usage:
word.word('MAGICK').cipher('english_simple')
word.word('MAGICK', alphabet='hebrew').cipher('hebrew_standard')
"""
cls._ensure_initialized()
assert cls._loader is not None, "Loader not initialized"
return cls._loader.word(text, alphabet=alphabet)
# Create singleton instance
word = _Word()

19
src/number/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
"""
Number namespace - Numerology and number correspondences.
Provides fluent query interface for:
- Numbers 1-9 with Sepheric attributes
- Digital root calculation
- Colors and correspondences
Usage:
from tarot import number
num = number.number(5)
root = number.digital_root(256)
colors = number.color()
"""
from .number import number, calculate_digital_root
__all__ = ["number", "calculate_digital_root"]

198
src/number/loader.py Normal file
View File

@@ -0,0 +1,198 @@
"""Numbers loader - access to numerology and number correspondences."""
from typing import Dict, Optional, Union, overload
from utils.filter import universal_filter
def calculate_digital_root(value: int) -> int:
"""
Calculate the digital root of a number by repeatedly summing its digits.
Digital root reduces any number to a single digit (1-9) by repeatedly
summing its digits until a single digit remains.
Args:
value: The number to reduce to digital root
Returns:
The digital root (1-9)
Examples:
>>> calculate_digital_root(14) # 1+4 = 5
5
>>> calculate_digital_root(99) # 9+9 = 18, 1+8 = 9
9
>>> calculate_digital_root(5)
5
"""
if value < 1:
raise ValueError(f"Value must be positive, got {value}")
while value >= 10:
value = sum(int(digit) for digit in str(value))
return value
class Numbers:
"""
Unified accessor for numerology, numbers, and color correspondences.
All methods are class methods, so Numbers is accessed as a static namespace:
num = Numbers.number(5)
root = Numbers.digital_root(256)
color = Numbers.color_by_number(root)
"""
# These are populated on first access from CardDataLoader
_numbers: Dict[int, 'Number'] = {} # type: ignore
_colors: Dict[int, 'Color'] = {} # type: ignore
_initialized: bool = False
@classmethod
def _ensure_initialized(cls) -> None:
"""Lazy-load data from CardDataLoader on first access."""
if cls._initialized:
return
from tarot.card.data import CardDataLoader
loader = CardDataLoader()
cls._numbers = loader.number()
cls._colors = loader.color()
cls._initialized = True
@classmethod
@overload
def number(cls, value: int) -> Optional['Number']:
...
@classmethod
@overload
def number(cls, value: None = ...) -> Dict[int, 'Number']:
...
@classmethod
def number(cls, value: Optional[int] = None) -> Union[Optional['Number'], Dict[int, 'Number']]:
"""Return an individual Number or the full numerology table."""
cls._ensure_initialized()
if value is None:
return cls._numbers.copy()
return cls._numbers.get(value)
@classmethod
@overload
def color(cls, sephera_number: int) -> Optional['Color']:
...
@classmethod
@overload
def color(cls, sephera_number: None = ...) -> Dict[int, 'Color']:
...
@classmethod
def color(cls, sephera_number: Optional[int] = None) -> Union[Optional['Color'], Dict[int, 'Color']]:
"""Return a single color correspondence or the entire map."""
cls._ensure_initialized()
if sephera_number is None:
return cls._colors.copy()
return cls._colors.get(sephera_number)
@classmethod
def color_by_number(cls, number: int) -> Optional['Color']:
"""Get a Color by mapping a number through digital root."""
root = calculate_digital_root(number)
return cls.color(root)
@classmethod
def number_by_digital_root(cls, value: int) -> Optional['Number']:
"""Get a Number object using digital root calculation."""
root = calculate_digital_root(value)
return cls.number(root)
@classmethod
def digital_root(cls, value: int) -> int:
"""Get the digital root of a value."""
return calculate_digital_root(value)
@classmethod
def filter_numbers(cls, **kwargs) -> list:
"""
Filter numbers by any Number attribute.
Uses the universal filter from utils.filter for consistency
across the entire project.
Args:
**kwargs: Any Number attribute with its value
Usage:
Numbers.filter_numbers(element="Fire")
Numbers.filter_numbers(sephera_number=5)
Returns:
List of Number objects matching all filters
"""
cls._ensure_initialized()
return universal_filter(list(cls._numbers.values()), **kwargs)
@classmethod
def display_filter_numbers(cls, **kwargs) -> str:
"""
Filter numbers and display results nicely formatted.
Combines filtering and formatting in one call.
Args:
**kwargs: Any Number attribute with its value
Returns:
Formatted string with filtered numbers
Example:
print(Numbers.display_filter_numbers(element="Fire"))
"""
from utils.filter import format_results
results = cls.filter_numbers(**kwargs)
return format_results(results)
@classmethod
def filter_colors(cls, **kwargs) -> list:
"""
Filter colors by any Color attribute.
Uses the universal filter from utils.filter for consistency
across the entire project.
Args:
**kwargs: Any Color attribute with its value
Usage:
Numbers.filter_colors(element="Water")
Numbers.filter_colors(sephera_number=3)
Returns:
List of Color objects matching all filters
"""
cls._ensure_initialized()
return universal_filter(list(cls._colors.values()), **kwargs)
@classmethod
def display_filter_colors(cls, **kwargs) -> str:
"""
Filter colors and display results nicely formatted.
Combines filtering and formatting in one call.
Args:
**kwargs: Any Color attribute with its value
Returns:
Formatted string with filtered colors
Example:
print(Numbers.display_filter_colors(element="Water"))
"""
from utils.filter import format_results
results = cls.filter_colors(**kwargs)
return format_results(results)

84
src/number/number.py Normal file
View File

@@ -0,0 +1,84 @@
"""Tarot number namespace - fluent query interface for numerology."""
from typing import TYPE_CHECKING
from utils.query import CollectionAccessor
if TYPE_CHECKING:
from tarot.card.data import CardDataLoader
def calculate_digital_root(value: int) -> int:
"""
Calculate the digital root of a number by repeatedly summing its digits.
Digital root reduces any number to a single digit (1-9) by repeatedly
summing its digits until a single digit remains.
"""
if value < 1:
raise ValueError(f"Value must be positive, got {value}")
while value >= 10:
value = sum(int(digit) for digit in str(value))
return value
class _Number:
"""Fluent query accessor for numerology and number correspondences."""
def __init__(self) -> None:
self._initialized: bool = False
self._loader: 'CardDataLoader | None' = None
self.number = CollectionAccessor(self._get_numbers)
self.color = CollectionAccessor(self._get_colors)
self.cipher = CollectionAccessor(self._get_ciphers)
def _ensure_initialized(self) -> None:
"""Lazy-load data from CardDataLoader on first access."""
if self._initialized:
return
from tarot.card.data import CardDataLoader
self._loader = CardDataLoader()
self._initialized = True
def _require_loader(self) -> 'CardDataLoader':
self._ensure_initialized()
assert self._loader is not None, "Loader not initialized"
return self._loader
def _get_numbers(self):
loader = self._require_loader()
return loader.number().copy()
def _get_colors(self):
loader = self._require_loader()
return loader.color().copy()
def _get_ciphers(self):
loader = self._require_loader()
return loader._ciphers.copy()
def digital_root(self, value: int) -> int:
"""Get the digital root of a value."""
return calculate_digital_root(value)
def __str__(self) -> str:
"""Return a nice summary of the number accessor."""
return (
"Number Namespace - Numerology and Number Correspondences\n\n"
"Access methods:\n"
" number.number(n) - Get number 1-9 with correspondences\n"
" number.color() - Get color correspondences\n"
" number.cipher() - Get cipher systems\n"
" number.digital_root(n) - Calculate digital root of any number"
)
def __repr__(self) -> str:
"""Return a nice representation of the number accessor."""
return self.__str__()
# Create singleton instance
number = _Number()

179
src/tarot/__init__.py Normal file
View File

@@ -0,0 +1,179 @@
"""
PY-Tarot: A comprehensive Python library for Tarot card reading and interpretation.
This library provides:
- Full 78-card Tarot deck with Major and Minor Arcana
- Kabbalistic correspondences and Tree of Life data
- Multiple configurable cipher systems (Hebrew, English, Greek, Reduction)
- Tarot alphabets (English, Greek, Hebrew) with meanings
- Numbers 1-9 with Sepheric attributes and digital root calculation
- Crowley 777 color system with Sephiroth correspondences
- Full type hints for IDE support and type checking
Unified Namespaces (singular names):
number - Numerology and number correspondences
letter - Alphabets (English, Hebrew, Greek), ciphers, and word analysis
Tarot - Tarot-specific (deck, cards, tree, cube, temporal)
Usage:
from tarot import number, letter, words, Tarot
num = number.number(5)
result = letter.words.word('MAGICK').cipher('english_simple')
card = Tarot.deck.card(3)
"""
from .deck import Deck, Card, MajorCard, MinorCard, DLT
from .attributes import (
Month, Day, Weekday, Hour, ClockHour, Zodiac, Suit, Meaning, Letter,
Sephera, PeriodicTable, Degree, AstrologicalInfluence,
TreeOfLife, Correspondences, CardImage, DoublLetterTrump,
EnglishAlphabet, GreekAlphabet, HebrewAlphabet,
Trigram, Hexagram,
EnochianTablet, EnochianGridPosition, EnochianArchetype, Path,
)
# Import shared attributes from utils
from utils.attributes import (
Note, Element, ElementType, Number, Color, Colorscale,
Planet, God, Cipher, CipherResult, Perfume,
)
from kaballah.cube.attributes import CubeOfSpace, WallDirection, Wall
from .card.data import CardDataLoader, calculate_digital_root
from .tarot_api import Tarot
# Import from card module (includes details, loader, and image_loader)
from .card import (
CardAccessor,
CardDetailsRegistry,
load_card_details,
load_deck_details,
get_cards_by_suit,
filter_cards_by_keywords,
print_card_details,
get_card_info,
ImageDeckLoader,
load_deck_images,
)
# Import from namespace folders
from letter import letter, trigram, hexagram
from number import number, calculate_digital_root
import kaballah
from kaballah import Tree, Cube
from temporal import ThalemaClock, Zodiac as AstrologyZodiac, PlanetPosition
def display(obj):
"""
Pretty print any tarot object by showing all its attributes.
Automatically detects dataclass objects and displays their fields
with values in a readable format.
Usage:
from tarot import display, number
num = number.number(5)
display(num) # Shows all attributes nicely formatted
"""
from dataclasses import fields
if hasattr(obj, '__dataclass_fields__'):
# It's a dataclass - show all fields
print(f"{obj.__class__.__name__}:")
for field in fields(obj):
value = getattr(obj, field.name)
print(f" {field.name}: {value}")
else:
print(obj)
__version__ = "0.1.0"
__author__ = "PY-Tarot Contributors"
__all__ = [
# Namespaces (singular)
"number",
"letter",
"kaballah",
"Tarot",
"trigram",
"hexagram",
# Temporal and astrological
"ThalemaClock",
"AstrologyZodiac",
"PlanetPosition",
# Card details and loading
"CardDetailsRegistry",
"load_card_details",
"load_deck_details",
"get_cards_by_suit",
"filter_cards_by_keywords",
"print_card_details",
"get_card_info",
# Image loading
"ImageDeckLoader",
"load_deck_images",
# Utilities
"display",
"CardAccessor",
"Tree",
"Cube",
# Deck classes
"Deck",
"Card",
"MajorCard",
"MinorCard",
"DLT",
# Calendar/attribute classes
"Month",
"Day",
"Weekday",
"Hour",
"ClockHour",
"Zodiac",
"Suit",
"Meaning",
"Letter",
"Note",
"CubeOfSpace",
"WallDirection",
"Wall",
# Sepheric classes
"Sephera",
"PeriodicTable",
"Degree",
"Element",
"ElementType",
"AstrologicalInfluence",
"TreeOfLife",
"Correspondences",
"CardImage",
"DoublLetterTrump",
"EnochianTablet",
"EnochianGridPosition",
"EnochianArchetype",
# Alphabet classes
"EnglishAlphabet",
"GreekAlphabet",
"HebrewAlphabet",
# Number and color classes
"Number",
"Color",
"Planet",
"God",
"Trigram",
"Hexagram",
"Cipher",
"CipherResult",
# Data loader and functions
"CardDataLoader",
"calculate_digital_root",
]

129
src/tarot/attributes.py Normal file
View File

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

View File

@@ -0,0 +1,26 @@
"""Card namespace - access Tarot cards and deck information."""
from .card import CardAccessor
from .details import CardDetailsRegistry
from .loader import (
load_card_details,
load_deck_details,
get_cards_by_suit,
filter_cards_by_keywords,
print_card_details,
get_card_info,
)
from .image_loader import ImageDeckLoader, load_deck_images
__all__ = [
"CardAccessor",
"CardDetailsRegistry",
"load_card_details",
"load_deck_details",
"get_cards_by_suit",
"filter_cards_by_keywords",
"print_card_details",
"get_card_info",
"ImageDeckLoader",
"load_deck_images",
]

331
src/tarot/card/card.py Normal file
View File

@@ -0,0 +1,331 @@
"""
Tarot deck and card accessor module.
Provides fluent access to Tarot cards through Tarot.deck namespace.
Usage:
from tarot.card import Deck, Card
card = Deck.card(3) # Get card 3
cards = Deck.card.filter(arcana="Major") # Get all Major Arcana
cards = Deck.card.filter(arcana="Minor") # Get all Minor Arcana
cards = Deck.card.filter(suit="Cups") # Get all Cups
cards = Deck.card.filter(arcana="Minor", suit="Wands", pip=5) # 5 of Wands
"""
from typing import List, Optional
from utils.filter import universal_filter, format_results
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
class CardList(list):
"""Custom list class for cards that formats nicely when printed."""
def __str__(self) -> str:
"""Format card list for display."""
if not self:
return "(no cards)"
return _format_cards(self)
def __repr__(self) -> str:
"""Return string representation."""
return self.__str__()
def _format_cards(cards: List['Card']) -> str:
"""
Format a list of cards for user-friendly display.
Args:
cards: List of Card objects to format
Returns:
Formatted string with each card separated by blank lines
"""
from utils.object_formatting import is_nested_object, get_object_attributes, format_value
lines = []
for card in cards:
card_num = getattr(card, 'number', '?')
card_name = getattr(card, 'name', 'Unknown')
lines.append(f"--- {card_num}: {card_name} ---")
# Format all attributes with proper nesting
for attr_name, attr_value in get_object_attributes(card):
if is_nested_object(attr_value):
lines.append(f" {attr_name}:")
lines.append(f" --- {attr_name.replace('_', ' ').title()} ---")
nested = format_value(attr_value, indent=4)
lines.append(nested)
else:
lines.append(f" {attr_name}: {attr_value}")
lines.append("") # Blank line between items
return "\n".join(lines)
class CardAccessor:
"""
Fluent accessor for Tarot cards in the deck.
Usage:
Tarot.deck.card(3) # Get card 3
Tarot.deck.card.filter(arcana="Major") # Get all Major Arcana
Tarot.deck.card.filter(arcana="Minor") # Get all Minor Arcana
Tarot.deck.card.filter(suit="Cups") # Get all Cups
Tarot.deck.card.filter(arcana="Minor", suit="Wands") # Get all Wand cards
Tarot.deck.card.display_filter(arcana="Major") # Display Major Arcana
"""
_deck: Optional['Deck'] = None
_initialized: bool = False
def _ensure_initialized(self) -> None:
"""Lazy-load the Deck on first access."""
if not self._initialized:
from tarot.deck import Deck as DeckClass
CardAccessor._deck = DeckClass()
CardAccessor._initialized = True
def __call__(self, number: int) -> Optional['Card']:
"""Get a card by number."""
self._ensure_initialized()
if self._deck is None:
return None
for card in self._deck.cards:
if card.number == number:
return card
return None
def filter(self, **kwargs) -> CardList:
"""
Filter cards by any Card attribute.
Uses the universal filter from utils.filter for consistency
across the entire project.
Args:
**kwargs: Any Card attribute with its value
Usage:
Tarot.deck.card.filter(arcana="Major")
Tarot.deck.card.filter(arcana="Minor", suit="Cups")
Tarot.deck.card.filter(number=5)
Tarot.deck.card.filter(element="Fire")
Tarot.deck.card.filter(pip=3)
Returns:
CardList of Card objects matching all filters
"""
self._ensure_initialized()
if self._deck is None:
return CardList()
return CardList(universal_filter(self._deck.cards, **kwargs))
def display_filter(self, **kwargs) -> str:
"""
Filter cards and display results nicely formatted.
Combines filtering and formatting in one call.
Args:
**kwargs: Any Card attribute with its value
Returns:
Formatted string with filtered cards
Example:
print(Tarot.deck.card.display_filter(arcana="Major"))
"""
results = self.filter(**kwargs)
return format_results(results)
def display(self) -> str:
"""
Format all cards in the deck for user-friendly display.
Returns a formatted string with each card separated by blank lines.
Nested objects are indented and separated with their own sections.
"""
self._ensure_initialized()
if self._deck is None:
return "(deck not initialized)"
return _format_cards(self._deck.cards)
def __str__(self) -> str:
"""Return the complete Tarot deck structure built from actual cards."""
self._ensure_initialized()
if self._deck is None:
return "CardAccessor (deck not initialized)"
lines = [
"Tarot Deck Structure",
"=" * 60,
"",
"The 78-card Tarot deck organized by structure and correspondence:",
"",
]
# Build structure from actual cards
major_arcana = [c for c in self._deck.cards if c.arcana == "Major"]
minor_arcana = [c for c in self._deck.cards if c.arcana == "Minor"]
# Major Arcana
if major_arcana:
lines.append(f"MAJOR ARCANA ({len(major_arcana)} cards):")
fool = next((c for c in major_arcana if c.number == 0), None)
world = next((c for c in major_arcana if c.number == 21), None)
if fool and world:
lines.append(f" Special Pair: {fool.name} ({fool.number}) - {world.name} ({world.number})")
double_letter_trumps = [c for c in major_arcana if 3 <= c.number <= 21]
lines.append(f" Double Letter Trumps ({len(double_letter_trumps)} cards): Cards 3-21")
lines.append("")
# Minor Arcana
if minor_arcana:
lines.append(f"MINOR ARCANA ({len(minor_arcana)} cards - 4 suits × 14 ranks):")
lines.append("")
# Aces
aces = [c for c in minor_arcana if hasattr(c, 'pip') and c.pip == 1]
if aces:
lines.append(f" ACES ({len(aces)} cards - The Root Powers):")
for ace in aces:
suit_name = ace.suit.name if hasattr(ace.suit, 'name') else str(ace.suit)
lines.append(f" Ace of {suit_name}")
lines.append("")
# Pips (2-10)
pips = [c for c in minor_arcana if hasattr(c, 'pip') and 2 <= c.pip <= 10]
if pips:
lines.append(f" PIPS ({len(pips)} cards - 2-10 of each suit):")
# Group by suit
suits_dict = {}
for pip in pips:
suit_name = pip.suit.name if hasattr(pip.suit, 'name') else str(pip.suit)
if suit_name not in suits_dict:
suits_dict[suit_name] = []
suits_dict[suit_name].append(pip)
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
if suit_name in suits_dict:
pip_nums = sorted([p.pip for p in suits_dict[suit_name]])
lines.append(f" {suit_name}: {', '.join(str(n) for n in pip_nums)}")
lines.append("")
# Court Cards
courts = [c for c in minor_arcana if hasattr(c, 'court_rank') and c.court_rank]
if courts:
lines.append(f" COURT CARDS ({len(courts)} cards - 4 ranks × 4 suits):")
# Get unique ranks and their order
rank_order = {"Knight": 0, "Prince": 1, "Princess": 2, "Queen": 3}
lines.append(" Rank order per suit: Knight, Prince, Princess, Queen")
lines.append("")
# Group by suit
suits_dict = {}
for court in courts:
suit_name = court.suit.name if hasattr(court.suit, 'name') else str(court.suit)
if suit_name not in suits_dict:
suits_dict[suit_name] = []
suits_dict[suit_name].append(court)
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
if suit_name in suits_dict:
suit_courts = sorted(suits_dict[suit_name],
key=lambda c: rank_order.get(c.court_rank, 99))
court_names = [c.court_rank for c in suit_courts]
lines.append(f" {suit_name}: {', '.join(court_names)}")
lines.append("")
# Element correspondences
lines.append("SUIT CORRESPONDENCES:")
suits_info = {}
for card in minor_arcana:
if hasattr(card, 'suit') and card.suit:
suit_name = card.suit.name if hasattr(card.suit, 'name') else str(card.suit)
if suit_name not in suits_info:
# Extract element info
element_name = "Unknown"
if hasattr(card.suit, 'element') and card.suit.element:
if hasattr(card.suit.element, 'name'):
element_name = card.suit.element.name
else:
element_name = str(card.suit.element)
# Extract zodiac signs
zodiac_signs = []
if hasattr(card.suit, 'element') and card.suit.element:
if hasattr(card.suit.element, 'zodiac_signs'):
zodiac_signs = card.suit.element.zodiac_signs
# Extract keywords
keywords = []
if hasattr(card.suit, 'element') and card.suit.element:
if hasattr(card.suit.element, 'keywords'):
keywords = card.suit.element.keywords
suits_info[suit_name] = {
'element': element_name,
'zodiac': zodiac_signs,
'keywords': keywords
}
for suit_name in ['Cups', 'Pentacles', 'Swords', 'Wands']:
if suit_name in suits_info:
info = suits_info[suit_name]
lines.append(f" {suit_name} ({info['element']}):")
if info['zodiac']:
lines.append(f" Zodiac: {', '.join(info['zodiac'])}")
if info['keywords']:
lines.append(f" Keywords: {', '.join(info['keywords'])}")
lines.append("")
lines.append(f"Total: {len(self._deck.cards)} cards")
return "\n".join(lines)
def __repr__(self) -> str:
"""Return a nice representation of the deck accessor."""
return self.__str__()
def spread(self, spread_name: str) -> str:
"""
Draw a Tarot card reading for a spread.
Automatically draws random cards for each position in the spread,
with random reversals. Returns formatted reading with card details.
Args:
spread_name: Name of the spread (case-insensitive, underscores or spaces)
Examples: 'Celtic Cross', 'golden dawn', 'three_card', 'tree of life'
Returns:
Formatted string with spread positions, drawn cards, and interpretations
Raises:
ValueError: If spread name not found
Examples:
print(Tarot.deck.card.spread('Celtic Cross'))
print(Tarot.deck.card.spread('golden dawn'))
print(Tarot.deck.card.spread('three card'))
print(Tarot.deck.card.spread('tree of life'))
"""
from tarot.card.spread import Spread, draw_spread, SpreadReading
# Initialize deck if needed
self._ensure_initialized()
# Create spread object
spread = Spread(spread_name)
# Draw cards for the spread
drawn_cards = draw_spread(spread, self._deck.cards if self._deck else None)
# Create and return reading
reading = SpreadReading(spread, drawn_cards)
return str(reading)

1750
src/tarot/card/data.py Normal file

File diff suppressed because it is too large Load Diff

557
src/tarot/card/details.py Normal file
View File

@@ -0,0 +1,557 @@
"""Card details and interpretations for all 78 Tarot cards.
This module provides interpretive data (explanations, keywords, guidance) for cards.
Registry is keyed by card position (1-78), independent of deck-specific names.
Deck order: Cups (1-14), Pentacles (15-28), Swords (29-42),
Major Arcana (43-64), Wands (65-78)
Usage:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
details = registry.get_by_position(44) # Get details for card at position 44
# Or load into a card object:
from tarot.deck import Deck
deck = Deck()
card = deck.cards[43] # Card at position 44 (0-indexed)
registry.load_into_card(card)
"""
from typing import TYPE_CHECKING, Any, Dict, Optional
if TYPE_CHECKING:
from tarot.deck import Card
class CardDetailsRegistry:
"""Registry for storing interpretive data for all 78 Tarot cards.
Uses card position (1-78) as the unique identifier, independent of deck names.
This allows the same card details to apply across different deck variants.
Deck order:
- 1-14: Cups (Ace, Ten, 2-9, Knight, Prince, Princess, Queen)
- 15-28: Pentacles (same structure)
- 29-42: Swords (same structure)
- 43-64: Major Arcana (Fool through Universe)
- 65-78: Wands (same structure)
"""
def __init__(self) -> None:
"""Initialize the card details registry with interpretive data."""
self._details: Dict[str, Dict[str, Any]] = self._build_registry()
# Map card positions (1-78) to registry keys
self._position_map = self._build_position_map()
@staticmethod
def key_to_roman(key: int) -> str:
"""
Convert a numeric key to Roman numerals.
Args:
key: The numeric key (0-21 for major arcana)
Returns:
Roman numeral representation (e.g., 21 -> "XXI", 0 -> "o")
"""
# Special case: 0 -> "o" (letter O for The Fool)
if key == 0:
return "o"
val = [
1000, 900, 500, 400,
100, 90, 50, 40,
10, 9, 5, 4,
1
]
syms = [
"M", "CM", "D", "CD",
"C", "XC", "L", "XL",
"X", "IX", "V", "IV",
"I"
]
roman_num = ''
i = 0
while key > 0:
for _ in range(key // val[i]):
roman_num += syms[i]
key -= val[i]
i += 1
return roman_num if roman_num else "o"
def _build_position_map(self) -> Dict[int, str]:
"""
Build a mapping from card position (1-78) to registry key.
Returns:
Dictionary mapping position to registry key
"""
position_map = {}
# Positions 1-14: Cups (Ace, Ten, 2-9, Knight, Prince, Princess, Queen)
cups_names = ["Ace of Cups", "Ten of Cups", "Two of Cups", "Three of Cups",
"Four of Cups", "Five of Cups", "Six of Cups", "Seven of Cups",
"Eight of Cups", "Nine of Cups", "Knight of Cups", "Prince of Cups",
"Princess of Cups", "Queen of Cups"]
for pos, name in enumerate(cups_names, start=1):
position_map[pos] = name
# Positions 15-28: Pentacles (same structure)
pentacles_names = ["Ace of Pentacles", "Ten of Pentacles", "Two of Pentacles", "Three of Pentacles",
"Four of Pentacles", "Five of Pentacles", "Six of Pentacles", "Seven of Pentacles",
"Eight of Pentacles", "Nine of Pentacles", "Knight of Pentacles", "Prince of Pentacles",
"Princess of Pentacles", "Queen of Pentacles"]
for pos, name in enumerate(pentacles_names, start=15):
position_map[pos] = name
# Positions 29-42: Swords (same structure)
swords_names = ["Ace of Swords", "Ten of Swords", "Two of Swords", "Three of Swords",
"Four of Swords", "Five of Swords", "Six of Swords", "Seven of Swords",
"Eight of Swords", "Nine of Swords", "Knight of Swords", "Prince of Swords",
"Princess of Swords", "Queen of Swords"]
for pos, name in enumerate(swords_names, start=29):
position_map[pos] = name
# Positions 43-64: Major Arcana (mapped to Roman numerals)
major_arcana_keys = ["o", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX",
"X", "XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX",
"XX", "XXI"]
for pos, key in enumerate(major_arcana_keys, start=43):
position_map[pos] = key
# Positions 65-78: Wands (same structure)
wands_names = ["Ace of Wands", "Ten of Wands", "Two of Wands", "Three of Wands",
"Four of Wands", "Five of Wands", "Six of Wands", "Seven of Wands",
"Eight of Wands", "Nine of Wands", "Knight of Wands", "Prince of Wands",
"Princess of Wands", "Queen of Wands"]
for pos, name in enumerate(wands_names, start=65):
position_map[pos] = name
return position_map
def get_by_position(self, position: int) -> Optional[Dict[str, Any]]:
"""
Get details for a card by its position (1-78).
Args:
position: Card position (1-78)
Returns:
Dictionary containing card details, or None if not found
"""
registry_key = self._position_map.get(position)
if registry_key is None:
return None
return self._details.get(registry_key)
def _build_registry(self) -> Dict[str, Dict[str, Any]]:
"""Build the interpretive data registry (card structure comes from Deck).
Stores only unique interpretive data (explanation, keywords, guidance).
Card names and structure are sourced from Deck for DRY compliance.
"""
return {
# Major Arcana (0-21) - Interpretive data only
"o": {
"explanation": "The Fool represents new beginnings, innocence, and spontaneity. This card signifies a fresh start or embarking on a new journey with optimism and faith.",
"interpretation": "Beginning of the Great Work, innocence; a fool for love; divine madness. Reason is transcended. Take the leap. Gain or loss through foolish actions.",
"keywords": ["new beginnings", "innocence", "faith", "spontaneity", "potential"],
"reversed_keywords": ["recklessness", "naivety", "poor judgment", "folly"],
"guidance": "Trust in the unfolding of your path. Embrace new opportunities with awareness and openness.",
},
"I": {
"explanation": "The Magician embodies manifestation, resourcefulness, and personal power. This card shows mastery of skills and the ability to turn ideas into reality.",
"interpretation": "Communication; Conscious Will; the process of continuous creation; ambiguity; deceptionl Things may not be as they appear. Concentration, meditation; mind used to direct the Will. Manipulation; crafty maneuverings.",
"keywords": ["manifestation", "resourcefulness", "power", "inspired action", "concentration"],
"reversed_keywords": ["manipulation", "poor planning", "untapped talents", "lack of direction"],
"guidance": "Focus your energy and intention on what you want to manifest. You have the tools and talents you need.",
},
"II": {
"explanation": "The High Priestess represents intuition, sacred knowledge, and the subconscious mind. She embodies mystery and inner wisdom.",
"interpretation": "Symbol of highest initiation; link between the archetypal and Formative Worlds. An initiatrixl Wooing by enchantment. possibility. The Idea behind the Form. Fluctuationl Time may not be right for a decision concerning mundane matters.",
"keywords": ["intuition", "sacred knowledge", "divine feminine", "the subconscious", "mystery"],
"reversed_keywords": ["hidden information", "silence", "disconnection from intuition", "superficiality"],
"guidance": "Listen to your inner voice. The answers you seek lie within. Trust the wisdom of your intuition.",
},
"III": {
"explanation": "The Empress symbolizes abundance, fertility, and nurturing energy. She represents creativity, sensuality, and the power of manifestation through nurturing.",
"interpretation": "The Holy Grail. love unites the Will. Love; beauty; friendship; success; passive balance. The feminine point of view. The door is open. Disregard the details and concentrate on the big picture.",
"keywords": ["abundance", "fertility", "femininity", "beauty", "nature", "creativity"],
"reversed_keywords": ["dependency", "creative block", "neediness", "underdevelopment"],
"guidance": "Nurture yourself and others. Allow yourself to enjoy the fruits of your labor and appreciate beauty.",
},
"IV": {
"explanation": "The Emperor represents authority, leadership, and established power. He embodies structure, discipline, and protection through strength and control.",
"interpretation": "Creative wisdom radiating upon the organized man and woman. Domination after conquest; quarrelsomeness; paternal love; ambition. Thought ruled by creative, masculine, fiery energy. Stubbornness; war; authority; energy in its most temporal form. Swift immpermaent action over confidence.",
"keywords": ["authority", "leadership", "power", "structure", "protection", "discipline"],
"reversed_keywords": ["weakness", "ineffectual leadership", "lack of discipline", "tyranny"],
"guidance": "Step into your power with confidence. Establish clear boundaries and structure. Lead by example.",
},
"V": {
"explanation": "The Hierophant represents tradition, conventional wisdom, and spiritual authority. This card embodies education, ceremony, and moral values.",
"interpretation": "The Holy Guardian Angel. The uniting of t hat which is above with that which is below. Love is indicated, but the nature of that love is not yet to be revealed. Inspiration; teaching; organization; discipline; strength; endurance; toil; help from superiors.",
"keywords": ["tradition", "spirituality", "wisdom", "ritual", "morality", "ethics"],
"reversed_keywords": ["rebellion", "unconventionality", "questioning authority", "dogmatism"],
"guidance": "Seek guidance from established wisdom. Respect traditions while finding your own spiritual path.",
},
"VI": {
"explanation": "The Lovers represents relationships, values alignment, and the union of opposites. It signifies choice, intimacy, and deep connection.",
"interpretation": "Intuition. Be open to your own inner voice. A well-intended, arranged marriage. An artificial union. The need to make a choice with awareness of consequences union; analysis followed by synthesis; indecision; instability; superficiality.",
"keywords": ["relationships", "love", "union", "values", "choice", "alignment"],
"reversed_keywords": ["disharmony", "misalignment", "conflict", "communication breakdown"],
"guidance": "Choose with your heart aligned with your values. Deep connection requires vulnerability and honesty.",
},
"VII": {
"explanation": "The Chariot embodies willpower, determination, and control through focused intention. It represents triumph through discipline and forward momentum.",
"interpretation": "Light in the darkness. The burden you carry may be the Holy Grail. Faithfulness; hope; obedience; a protective relationship; firm, even violent adherance to dogma or tradition. Glory; riches; englightened civilization; victory; triumph; chain of command.",
"keywords": ["determination", "willpower", "control", "momentum", "victory", "focus"],
"reversed_keywords": ["lack of control", "haste", "resistance", "moving backward"],
"guidance": "Take the reins of your life. Move forward with determination and clear direction. You have the power.",
},
"VIII": {
"explanation": "Strength represents inner power, courage, and compassion. It shows mastery through gentleness and the ability to face challenges with calm confidence.",
"interpretation": "Equilibrium; karmic law; the dance of life; all possibilities. The woman satisfied. Balance; weigh each thought against its opposite. Lawsuits; treaties. Pause and look before you leap.",
"keywords": ["strength", "courage", "patience", "compassion", "control", "confidence"],
"reversed_keywords": ["weakness", "self-doubt", "lack of composure", "poor control"],
"guidance": "True strength comes from within. Face challenges with courage and compassion for yourself and others.",
},
"IX": {
"explanation": "The Hermit represents introspection, spiritual seeking, and inner guidance. This card embodies solitude, wisdom gained through reflection, and self-discovery.",
"interpretation": "Divine seed of all things. By silence comes inspiration and wisdom. Wandering alone; temporary solitude; creative contemplation; a virgin. Retirement from involvement in current events.",
"keywords": ["introspection", "spiritual seeking", "inner light", "wisdom", "solitude", "truth"],
"reversed_keywords": ["loneliness", "isolation", "lost", "paranoia", "disconnection"],
"guidance": "Take time for introspection and self-discovery. Your inner light guides your path. Seek solitude for wisdom.",
},
"X": {
"explanation": "The Wheel of Fortune represents cycles, destiny, and the turning points of life. It embodies luck, karma, and the natural ebb and flow of existence.",
"interpretation": "Continual change. In the midst of revolving phenomena, reaach joyously the motionless center. Carefree love; wanton pleasure; amusement; fun; change of fortune, usually good.",
"keywords": ["fate", "destiny", "cycles", "fortune", "karma", "turning point"],
"reversed_keywords": ["bad luck", "resistance to change", "broken cycles", "misfortune"],
"guidance": "Trust in the cycles of life. What goes up must come down. Embrace change as part of your journey.",
},
"XI": {
"explanation": "Justice represents fairness, truth, and balance. It embodies accountability, clear judgment, and the consequences of actions both past and present.",
"interpretation": "Understanding; the Will of New Aeon; passion; sense smitten with ecstasy. let love devour all. Energy independent of reason. Strength; courage; utilization of magical power.",
"keywords": ["justice", "fairness", "truth", "cause and effect", "balance", "accountability"],
"reversed_keywords": ["injustice", "bias", "lack of accountability", "dishonesty"],
"guidance": "Seek the truth and act with fairness. Take responsibility for your actions. Balance is key.",
},
"XII": {
"explanation": "The Hanged Man represents suspension, letting go, and seeing things from a new perspective. It embodies surrender, pause, and gaining wisdom through sacrifice.",
"interpretation": "Redemption, sacrifice, annihilation in the beloved; martyrdom; loss; torment; suspension; death; suffering.",
"keywords": ["suspension", "restriction", "letting go", "new perspective", "surrender", "pause"],
"reversed_keywords": ["resistance", "stalling", "unwillingness to change", "impatience"],
"guidance": "Pause and reflect. What are you holding onto? Surrender control and trust the process.",
},
"XIII": {
"explanation": "Death represents transformation, endings, and new beginnings. This card embodies major life transitions, the release of the old, and inevitable change.",
"interpretation": "End of cycle; transformation; raw sexuality. Sex is death. Stress becomes intolerable. Any change is welcome. Time; age; unexpected change; death.",
"keywords": ["transformation", "transition", "endings", "beginnings", "change", "acceptance"],
"reversed_keywords": ["resistance to change", "stagnation", "missed opportunity", "delay"],
"guidance": "Release what no longer serves you. Transformation is inevitable. Trust in the cycle of death and rebirth.",
},
"XIV": {
"explanation": "Temperance represents balance, moderation, and harmony. It embodies blending of opposites, inner peace through balance, and finding your rhythm.",
"interpretation": "Transmutation through union of opposites. A perfect marriage exalts and transforms each partner. The scientific method. Success follows complex maneuvers.",
"keywords": ["balance", "moderation", "harmony", "patience", "timing", "peace"],
"reversed_keywords": ["imbalance", "excess", "conflict", "intemperance", "discord"],
"guidance": "Seek balance in all things. Blend opposing forces. Find your rhythm through moderation and patience.",
},
"XV": {
"explanation": "The Devil represents bondage, materialism, and shadow aspects of self. It embodies addictions, illusions, and the consequences of giving away personal power.",
"interpretation": "Thou hast no right but to do thy will. Obession; temptation; ecstasy found in every phenomenon; creative action, yet sublimely careless of result; unscrupulous ambition; strength.",
"keywords": ["bondage", "materialism", "playfulness", "shadow self", "sexuality", "excess"],
"reversed_keywords": ["freedom", "detachment", "reclaiming power", "breaking free"],
"guidance": "Examine what binds you. Acknowledge your shadow. You hold the key to your own freedom.",
},
"XVI": {
"explanation": "The Tower represents sudden disruption, revelation, and breakthrough through crisis. It embodies sudden change, truth revealed, and necessary destruction.",
"interpretation": "Escape from the prison of organized life; renunciation of love; quarreling. Plans are destroyed. War; danger; sudden death.",
"keywords": ["sudden change", "upheaval", "revelation", "breakdown", "breakthrough", "chaos"],
"reversed_keywords": ["resistance to change", "averted crisis", "delay", "stagnation"],
"guidance": "Crisis brings clarity. Though change is sudden and jarring, it clears away the false and brings truth.",
},
"XVII": {
"explanation": "The Star represents hope, guidance, and inspiration. It embodies clarity of purpose, spiritual insight, and the light that guides your path forward.",
"interpretation": "Clairvoyance; visions; drams; hope; love; yearning; realization of inexhaustible possibilities; dreaminess; unexpected help; renewal.",
"keywords": ["hope", "faith", "inspiration", "vision", "guidance", "spirituality"],
"reversed_keywords": ["hopelessness", "despair", "lack of direction", "lost", "obscured"],
"guidance": "Let your inner light shine. Trust in your vision. Hope and guidance light your path forward.",
},
"XVIII": {
"explanation": "The Moon represents illusion, intuition, and the subconscious mind. It embodies mystery, dreams, and navigating by inner knowing rather than sight.",
"interpretation": "The Dark night of the soul; deception; falsehood; illusion; madness; the threshold of significant change.",
"keywords": ["illusion", "intuition", "uncertainty", "subconscious", "dreams", "mystery"],
"reversed_keywords": ["clarity", "truth revealed", "release from illusion", "awakening"],
"guidance": "Trust your intuition to navigate mystery. What appears illusory contains deeper truths worth exploring.",
},
"XIX": {
"explanation": "The Sun represents joy, clarity, and vitality. It embodies success, positive energy, and the radiance of authentic self-expression.",
"interpretation": "Lord of the New Aeon. Spiritual emancipation. Pleasure; shamelessness; vanity; frankness. Freedom brings sanity. Glory; riches; enlightened civilization.",
"keywords": ["success", "joy", "clarity", "vitality", "warmth", "authenticity"],
"reversed_keywords": ["temporary darkness", "lost vitality", "setback", "sadness"],
"guidance": "Celebrate your success. Let your authentic self shine. Joy and clarity light your way.",
},
"XX": {
"explanation": "Judgement represents awakening, calling, and significant decisions. It embodies reckoning, rebirth, and responding to a higher calling.",
"interpretation": "Let every act be an act of Worship; let every act be an act of Love. Final decision; judgement. Learn from the past. Prepare for the future.",
"keywords": ["awakening", "calling", "judgment", "rebirth", "evaluation", "absolution"],
"reversed_keywords": ["doubt", "self-doubt", "harsh judgment", "reluctance to change"],
"guidance": "Answer your higher calling. Evaluate with compassion. A significant awakening or decision awaits.",
},
"XXI": {
"explanation": "The World represents completion, wholeness, and fulfillment. It embodies the end of a cycle, achievement of goals, and a sense of unity.",
"interpretation": "Completion of the Greatk Work; patience; perseverance; stubbornness; serious meditation. Work accomplished.",
"keywords": ["completion", "fulfillment", "wholeness", "travel", "unity", "achievement"],
"reversed_keywords": ["incomplete", "blocked", "separation", "seeking closure"],
"guidance": "A significant cycle completes. You have achieved wholeness. Yet every ending is a new beginning.",
},
# Minor Arcana - Swords
"Ace of Swords": {
"explanation": "The Ace of Swords represents clarity, breakthrough, and new ideas. It embodies truth emerging, mental clarity, and the power of honest communication.",
"interpretation": "New idea or perspective, clarity and truth, breakthrough thinking, mental clarity",
"keywords": ["breakthrough", "clarity", "truth", "new ideas", "communication"],
"reversed_keywords": ["confusion", "unclear communication", "hidden truth", "mental fog"],
"guidance": "A breakthrough arrives. Speak your truth with clarity. Mental clarity reveals new possibilities.",
},
"Two of Swords": {
"explanation": "The Two of Swords represents stalemate, difficult choices, and mental struggle. It embodies indecision, conflicting information, and the need for perspective.",
"interpretation": "Stalemate and indecision, difficult choices ahead, conflicting perspectives, mental struggle",
"keywords": ["stalemate", "indecision", "confusion", "difficult choice", "standoff"],
"reversed_keywords": ["clarity emerging", "decision made", "moving forward", "resolution"],
"guidance": "Step back from the conflict. You need more information or perspective before deciding.",
},
"Three of Swords": {
"explanation": "The Three of Swords represents heartbreak, difficult truths, and mental anguish. It embodies challenging communication, painful revelations, and clarity that hurts.",
"interpretation": "Difficult truths and heartbreak, communication challenges, mental anguish, clarity through pain",
"keywords": ["heartbreak", "sorrow", "difficult truth", "mental anguish", "separation"],
"reversed_keywords": ["healing", "moving forward", "forgiveness", "reconciliation"],
"guidance": "Difficult truths are emerging. Allow yourself to feel the pain. Healing follows acknowledgment.",
},
"Four of Swords": {
"explanation": "The Four of Swords represents rest, recovery, and mental respite. It embodies the need for pause, recuperation, and gathering strength.",
"interpretation": "Rest and recovery, pause and contemplation, gathering strength, needed respite",
"keywords": ["rest", "pause", "recovery", "contemplation", "respite"],
"reversed_keywords": ["restlessness", "stress", "unwillingness to rest", "agitation"],
"guidance": "Take time to rest and recover. Your mind and spirit need respite. Gather your strength.",
},
"Five of Swords": {
"explanation": "The Five of Swords represents conflict, victory at a cost, and difficult truths after battle. It embodies competition with consequences and the emptiness of winning wrongly.",
"interpretation": "Conflict and competition, pyrrhic victory, harsh truths, aftermath of conflict",
"keywords": ["conflict", "defeat", "victory at cost", "awkwardness", "tension"],
"reversed_keywords": ["reconciliation", "resolution", "forgiveness", "peace"],
"guidance": "Sometimes victory costs more than it's worth. Seek reconciliation over conquest.",
},
"Six of Swords": {
"explanation": "The Six of Swords represents moving forward, healing journey, and leaving troubles behind. It embodies transition, mental resolution, and the path to better days.",
"interpretation": "Moving forward and transition, leaving trouble behind, journey and travel, mental resolution",
"keywords": ["transition", "healing journey", "moving forward", "travel", "freedom"],
"reversed_keywords": ["stuck", "resistance to change", "delays", "unresolved issues"],
"guidance": "A journey of healing begins. Move forward. Leave the past behind. Better days await.",
},
"Seven of Swords": {
"explanation": "The Seven of Swords represents deception, cunning, and strategic retreat. It embodies hidden agendas, betrayal, and escape from difficult situations.",
"interpretation": "Deception and cunning, hidden agendas, strategic retreat, betrayal or self-deception",
"keywords": ["deception", "cunning", "betrayal", "hidden agenda", "strategy"],
"reversed_keywords": ["coming clean", "honesty", "truth revealed", "facing consequences"],
"guidance": "Look for hidden truths. Deception may be at play. Where are you deceiving yourself?",
},
"Eight of Swords": {
"explanation": "The Eight of Swords represents restriction, bondage, and self-imposed limitations. It embodies feeling trapped, mental imprisonment, and powerlessness.",
"interpretation": "Restriction and bondage, self-imposed limitations, feeling trapped, helplessness",
"keywords": ["bondage", "restriction", "trapped", "helplessness", "powerlessness"],
"reversed_keywords": ["freedom", "release", "empowerment", "breaking free"],
"guidance": "You have more power than you believe. The restrictions may be self-imposed. Free yourself.",
},
"Nine of Swords": {
"explanation": "The Nine of Swords represents anxiety, nightmares, and mental torment. It embodies overthinking, worry, and the burden of negative thoughts.",
"interpretation": "Anxiety and worry, nightmares and turmoil, overthinking, mental burden",
"keywords": ["anxiety", "worry", "nightmares", "overthinking", "despair"],
"reversed_keywords": ["relief", "healing", "moving past", "mental clarity"],
"guidance": "Your mind is your greatest torment. Seek support. This darkness passes. Morning follows night.",
},
"Ten of Swords": {
"explanation": "The Ten of Swords represents complete mental/emotional defeat, rock bottom, and the end of suffering. It embodies the culmination of difficulty and the promise of renewal.",
"interpretation": "Defeat and rock bottom, end of suffering, difficult conclusion, release from burden",
"keywords": ["defeat", "rock bottom", "ending", "relief", "betrayal"],
"reversed_keywords": ["recovery", "beginning again", "healing", "hope"],
"guidance": "The worst has passed. You've hit bottom. From here, only recovery is possible.",
},
"Page of Swords": {
"explanation": "The Page of Swords represents curious inquiry, new ideas, and youthful intellectual energy. It embodies investigation, learning, and the drive to understand.",
"interpretation": "Curiosity and new learning, investigation and inquiry, youthful energy, intellectual development",
"keywords": ["curiosity", "inquiry", "new learning", "messages", "vigilance"],
"reversed_keywords": ["cynicism", "misinformation", "scattered thinking", "mischief"],
"guidance": "Curiosity leads to discovery. Ask questions and investigate. Knowledge empowers.",
},
"Knight of Swords": {
"explanation": "The Knight of Swords represents swift action, directness, and intellectual courage. It embodies confrontation, truth-seeking, and the willingness to challenge.",
"interpretation": "Direct communication and action, intellectual courage, challenging situations, swift movement",
"keywords": ["action", "impulsiveness", "courage", "conflict", "truth"],
"reversed_keywords": ["scatter-brained", "dishonest", "confusion", "retreat"],
"guidance": "Speak your truth directly. Act with courage. Swift action brings results.",
},
"Queen of Swords": {
"explanation": "The Queen of Swords represents intellectual power, clarity, and independent thinking. It embodies wisdom gained through experience and clear perception.",
"interpretation": "Intellectual power and clarity, independence and perception, wisdom and experience, communication",
"keywords": ["clarity", "intelligence", "independence", "truth", "perception"],
"reversed_keywords": ["bitter", "manipulative", "cold", "cruel"],
"guidance": "Trust your keen intellect. Speak your truth with grace. Clarity empowers.",
},
"King of Swords": {
"explanation": "The King of Swords represents mental mastery, authority through intellect, and the power of truth. It embodies leadership, clear judgment, and strategic thinking.",
"interpretation": "Mental mastery and intellect, authority and leadership, justice and fairness, clear judgment",
"keywords": ["authority", "intellect", "truth", "leadership", "justice"],
"reversed_keywords": ["tyrant", "manipulation", "abuse of power", "cruelty"],
"guidance": "Lead with intellect and integrity. Your clarity creates order. Speak truth with authority.",
},
"Princess of Swords": {
"explanation": "The Princess of Swords represents intellectual potential, youthful curiosity, and emerging clarity. It embodies the development of mental acuity and the pursuit of knowledge.",
"interpretation": "Intellectual development and potential, emerging clarity, youthful inquiry, pursuit of truth",
"keywords": ["clarity emerging", "intellectual potential", "investigation", "truth-seeking", "perception"],
"reversed_keywords": ["confusion", "scattered thoughts", "deception", "lack of focus"],
"guidance": "Your ability to perceive truth is developing. Stay curious and focused. Clarity is emerging.",
},
# Minor Arcana - Cups
"Ace of Cups": {
"explanation": "The Ace of Cups represents new emotional beginning, love, and spiritual awakening. It embodies the opening of the heart and new emotional connections.",
"interpretation": "New emotional beginning, love and compassion, spiritual awakening, emotional clarity",
"keywords": ["love", "new emotion", "compassion", "beginning", "spirituality"],
"reversed_keywords": ["blocked emotion", "closed heart", "emotional confusion"],
"guidance": "Your heart opens to new possibilities. Emotional connections deepen. Love flows.",
},
"Two of Cups": {
"explanation": "The Two of Cups represents partnership, mutual respect, and emotional connection. It embodies balance, harmony, and the foundation of relationships.",
"interpretation": "Partnership and connection, mutual respect and harmony, emotional balance, agreements",
"keywords": ["partnership", "love", "connection", "harmony", "commitment"],
"reversed_keywords": ["imbalance", "separation", "misalignment", "broken agreement"],
"guidance": "Deep connection and harmony are possible. Mutual respect forms the foundation.",
},
"Three of Cups": {
"explanation": "The Three of Cups represents celebration, friendship, and community. It embodies joy, shared experiences, and the warmth of connection.",
"interpretation": "Celebration and community, friendship and joy, shared experiences, social harmony",
"keywords": ["celebration", "community", "friendship", "joy", "creativity"],
"reversed_keywords": ["isolation", "loneliness", "overindulgence", "discord"],
"guidance": "Celebrate with friends. Community and connection bring joy. Share in the abundance.",
},
# Minor Arcana - Pentacles
"Ace of Pentacles": {
"explanation": "The Ace of Pentacles represents new prosperity, material opportunity, and earthly beginnings. It embodies abundance, security, and practical gifts.",
"interpretation": "New material opportunity, abundance and prosperity, earthly beginnings, practical gifts",
"keywords": ["abundance", "opportunity", "prosperity", "security", "gift"],
"reversed_keywords": ["lost opportunity", "scarcity", "blocked prosperity"],
"guidance": "Material opportunity arrives. Seize it. Abundance begins with gratitude.",
},
"Two of Pentacles": {
"explanation": "The Two of Pentacles represents balance, flexibility, and managing resources. It embodies juggling priorities, adaptability, and resourcefulness.",
"interpretation": "Balance and flexibility, managing resources, adaptability, juggling priorities",
"keywords": ["balance", "flexibility", "adaptability", "resourcefulness", "management"],
"reversed_keywords": ["imbalance", "mismanagement", "chaos", "loss"],
"guidance": "Balance your priorities carefully. Flexibility allows you to manage what comes.",
},
# Minor Arcana - Wands
"Ace of Wands": {
"explanation": "The Ace of Wands represents new inspiration, creative spark, and passionate new beginning. It embodies potential, growth, and spiritual fire.",
"interpretation": "New creative spark, inspiration and potential, passionate beginning, growth opportunity",
"keywords": ["inspiration", "potential", "growth", "new beginning", "creativity"],
"reversed_keywords": ["blocked inspiration", "delays", "lost potential"],
"guidance": "Creative inspiration ignites. Channel this energy into action. Your passion becomes power.",
},
"Two of Wands": {
"explanation": "The Two of Wands represents planning, future vision, and resourcefulness. It embodies potential growth, decisions about direction, and careful preparation.",
"interpretation": "Planning and vision, resource management, decisions about direction, future planning",
"keywords": ["vision", "planning", "potential", "resourcefulness", "future"],
"reversed_keywords": ["lack of vision", "poor planning", "blocked growth"],
"guidance": "Plan your future with vision. You have resources to build something great.",
},
}
def get(self, card_name: str) -> Optional[Dict[str, Any]]:
"""
Get details for a specific card by name.
Args:
card_name: The card's name (e.g., "Princess of Swords", "Ace of Cups")
Returns:
Dictionary containing card details, or None if not found
"""
return self._details.get(card_name)
def get_key_as_roman(self, card_name: str) -> Optional[str]:
"""
Get the card's key displayed as Roman numerals.
Args:
card_name: The card's name (e.g., "The Fool", "The Magician")
Returns:
Roman numeral representation of the key (e.g., "XXI" for 21), or None if not found
"""
details = self.get(card_name)
if details and "key" in details:
return self.key_to_roman(details["key"])
return None
def get_all_by_suit(self, suit_name: str) -> Dict[str, Dict[str, Any]]:
"""
Get all details for cards in a specific suit.
Args:
suit_name: The suit name ("Cups", "Pentacles", "Swords", "Wands")
Returns:
Dictionary of card details for that suit
"""
return {
name: details for name, details in self._details.items()
if suit_name.lower() in name.lower()
}
def _get_registry_key_for_card(self, card: 'Card') -> Optional[str]:
"""
Get the registry key for a card based on deck position (1-78).
Card position is independent of deck-specific names, allowing different
deck variants to use the same registry entries.
Args:
card: The Card object to look up
Returns:
Registry key string, or None if card cannot be mapped
"""
return self._position_map.get(card.number)
def load_into_card(self, card: 'Card') -> bool:
"""
Load details from registry into a Card object using its position.
Args:
card: The Card object to populate
Returns:
True if details were found and loaded, False otherwise
"""
# Use position-based lookup instead of name-based
details = self.get_by_position(card.number)
if not details:
return False
card.explanation = details.get("explanation", "")
card.interpretation = details.get("interpretation", "")
card.keywords = details.get("keywords", [])
card.reversed_keywords = details.get("reversed_keywords", [])
card.guidance = details.get("guidance", "")
return True
def __getitem__(self, card_name: str) -> Optional[Dict[str, Any]]:
"""Allow dict-like access: registry['Princess of Swords']"""
return self.get(card_name)

View File

@@ -0,0 +1,346 @@
"""Image deck loader for matching Tarot card images to cards.
This module provides intelligent image matching and loading, supporting:
- Numbered format: 0.jpg, 1.jpg, ... or 00_foolish.jpg, 01_magic_man.jpg
- Custom naming with override: ##_custom_name.jpg overrides default card names
- Intelligent fuzzy matching for card identification
- Hybrid modes with intelligent fallbacks
Usage:
from tarot.card.image_loader import load_deck_images
deck = Deck()
count = load_deck_images(deck, "/path/to/deck/folder")
print(f"Loaded {count} card images")
"""
import os
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING:
from tarot.deck import Card, Deck
class ImageDeckLoader:
"""Loader for matching Tarot card images to deck cards."""
# Supported image extensions
SUPPORTED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
# Regex patterns for file matching
NUMBERED_PATTERN = re.compile(r'^(\d+)(?:_(.+))?\.(?:jpg|jpeg|png|gif|bmp|webp)$', re.IGNORECASE)
def __init__(self, deck_folder: str) -> None:
"""
Initialize the image deck loader.
Args:
deck_folder: Path to the folder containing card images
Raises:
ValueError: If folder doesn't exist or is not a directory
"""
self.deck_folder = Path(deck_folder)
if not self.deck_folder.exists():
raise ValueError(f"Deck folder does not exist: {deck_folder}")
if not self.deck_folder.is_dir():
raise ValueError(f"Deck path is not a directory: {deck_folder}")
self.image_files = self._scan_folder()
self.card_mapping: Dict[int, Tuple[str, bool]] = {} # card_number -> (path, has_custom_name)
self._build_mapping()
def _scan_folder(self) -> List[Path]:
"""Scan folder for image files."""
images = []
for ext in self.SUPPORTED_EXTENSIONS:
images.extend(self.deck_folder.glob(f'*{ext}'))
images.extend(self.deck_folder.glob(f'*{ext.upper()}'))
# Sort by filename for consistent ordering
return sorted(images)
def _parse_filename(self, filename: str) -> Tuple[Optional[int], Optional[str], bool]:
"""
Parse image filename to extract card number and optional custom name.
Args:
filename: The filename (without path)
Returns:
Tuple of (card_number, custom_name, has_custom_name)
- card_number: Parsed number if found, else None
- custom_name: Custom name if present (e.g., "foolish" from "00_foolish.jpg")
- has_custom_name: True if custom name was found
Examples:
"0.jpg" -> (0, None, False)
"00_foolish.jpg" -> (0, "foolish", True)
"01_magic_man.jpg" -> (1, "magic_man", True)
"invalid.jpg" -> (None, None, False)
"""
match = self.NUMBERED_PATTERN.match(filename)
if not match:
return None, None, False
card_number = int(match.group(1))
custom_name = match.group(2)
has_custom_name = custom_name is not None
return card_number, custom_name, has_custom_name
def _build_mapping(self) -> None:
"""Build mapping from card numbers to image file paths."""
for image_path in self.image_files:
card_num, custom_name, has_custom_name = self._parse_filename(image_path.name)
if card_num is not None:
# Store path and whether it has a custom name
self.card_mapping[card_num] = (str(image_path), has_custom_name)
def _normalize_card_name(self, name: str) -> str:
"""
Normalize card name for matching.
Converts to lowercase, removes special characters, collapses whitespace.
Args:
name: Original card name
Returns:
Normalized name
Examples:
"The Fool" -> "the fool"
"Princess of Swords" -> "princess of swords"
"Ace of Cups" -> "ace of cups"
"""
# Convert to lowercase
normalized = name.lower()
# Replace special characters with spaces
normalized = re.sub(r'[^\w\s]', ' ', normalized)
# Collapse multiple spaces
normalized = re.sub(r'\s+', ' ', normalized).strip()
return normalized
def _find_fuzzy_match(self, card_name_normalized: str) -> Optional[int]:
"""
Find matching card number using fuzzy name matching.
This is a fallback when card names don't parse as numbers.
Args:
card_name_normalized: Normalized card name
Returns:
Card number if a match is found, else None
"""
best_match = None
best_score = 0
threshold = 0.6
# Check all parsed custom names
for card_num, (_, has_custom_name) in self.card_mapping.items():
if not has_custom_name:
continue
# Get the actual filename to extract custom name
for image_path in self.image_files:
parsed_num, custom_name, _ = self._parse_filename(image_path.name)
if parsed_num == card_num and custom_name:
normalized_custom = self._normalize_card_name(custom_name)
# Simple similarity score: words that match
query_words = set(card_name_normalized.split())
custom_words = set(normalized_custom.split())
if query_words and custom_words:
intersection = len(query_words & custom_words)
union = len(query_words | custom_words)
score = intersection / union if union > 0 else 0
if score > best_score and score >= threshold:
best_score = score
best_match = card_num
return best_match
def get_image_path(self, card: 'Card') -> Optional[str]:
"""
Get the image path for a specific card.
Matches cards by:
1. Card number (primary method)
2. Fuzzy matching on card name (fallback)
Args:
card: The Card object to find an image for
Returns:
Full path to image file, or None if not found
"""
# Try direct number match first
if card.number in self.card_mapping:
path, _ = self.card_mapping[card.number]
return path
# Try fuzzy match on name as fallback
normalized_name = self._normalize_card_name(card.name)
fuzzy_match = self._find_fuzzy_match(normalized_name)
if fuzzy_match is not None and fuzzy_match in self.card_mapping:
path, _ = self.card_mapping[fuzzy_match]
return path
return None
def should_override_name(self, card_number: int) -> bool:
"""
Check if card name should be overridden from filename.
Returns True only if:
- Image file has a custom name component (##_name.jpg format)
- Not just a plain number (##.jpg format)
Args:
card_number: The card's number
Returns:
True if name should be overridden from filename, False otherwise
"""
if card_number not in self.card_mapping:
return False
_, has_custom_name = self.card_mapping[card_number]
return has_custom_name
def get_custom_name(self, card_number: int) -> Optional[str]:
"""
Get the custom card name from the filename.
Args:
card_number: The card's number
Returns:
Custom name if present, None otherwise
Example:
If filename is "00_the_foolish.jpg", returns "the_foolish"
If filename is "00.jpg", returns None
"""
if card_number not in self.card_mapping:
return None
# Find the image file for this card number
for image_path in self.image_files:
_, custom_name, _ = self._parse_filename(image_path.name)
parsed_num, _, _ = self._parse_filename(image_path.name)
if parsed_num == card_number and custom_name:
# Convert underscore-separated name to title case
name_words = custom_name.split('_')
return ' '.join(word.capitalize() for word in name_words)
return None
def load_into_deck(self, deck: 'Deck',
override_names: bool = True,
verbose: bool = False) -> int:
"""
Load image paths into all cards in a deck.
Args:
deck: The Deck to load images into
override_names: If True, use custom names from filenames when available
verbose: If True, print progress information
Returns:
Number of cards that had images loaded
Example:
>>> loader = ImageDeckLoader("/path/to/deck")
>>> deck = Deck()
>>> count = loader.load_into_deck(deck, override_names=True)
>>> print(f"Loaded {count} card images")
"""
loaded_count = 0
for card in deck.cards:
image_path = self.get_image_path(card)
if image_path:
card.image_path = image_path
loaded_count += 1
# Override name if appropriate
if override_names and self.should_override_name(card.number):
custom_name = self.get_custom_name(card.number)
if custom_name:
if verbose:
print(f" {card.number}: {card.name} -> {custom_name}")
card.name = custom_name
elif verbose:
print(f"{card.number}: {card.name}")
return loaded_count
def get_summary(self) -> Dict[str, any]:
"""
Get a summary of loaded images and statistics.
Returns:
Dictionary with loader statistics
"""
total_images = len(self.image_files)
mapped_cards = len(self.card_mapping)
custom_named = sum(1 for _, has_custom in self.card_mapping.values() if has_custom)
return {
'deck_folder': str(self.deck_folder),
'total_image_files': total_images,
'total_image_filenames': len(set(f.name for f in self.image_files)),
'mapped_card_numbers': mapped_cards,
'cards_with_custom_names': custom_named,
'cards_with_generic_numbers': mapped_cards - custom_named,
'image_extensions_found': list(set(f.suffix.lower() for f in self.image_files)),
}
def load_deck_images(deck: 'Deck',
deck_folder: str,
override_names: bool = True,
verbose: bool = False) -> int:
"""
Convenience function to load deck images.
Args:
deck: The Deck object to load images into
deck_folder: Path to folder containing card images
override_names: If True, use custom names from filenames when available
verbose: If True, print progress information
Returns:
Number of cards that had images loaded
Raises:
ValueError: If deck_folder doesn't exist or is invalid
Example:
>>> from tarot import Deck
>>> from tarot.card.image_loader import load_deck_images
>>>
>>> deck = Deck()
>>> count = load_deck_images(deck, "/path/to/deck/images")
>>> print(f"Loaded {count} card images")
"""
loader = ImageDeckLoader(deck_folder)
return loader.load_into_deck(deck, override_names=override_names, verbose=verbose)

259
src/tarot/card/loader.py Normal file
View File

@@ -0,0 +1,259 @@
"""Card loader for populating card details from the registry.
This module provides utilities to load card details from the CardDetailsRegistry
into Card objects, supporting both individual cards and full decks.
Usage:
from tarot.card.loader import load_card_details, load_deck_details
from tarot.card.details import CardDetailsRegistry
# Load single card
loader = CardDetailsRegistry()
card = my_deck.minor.swords(11)
load_card_details(card, loader)
# Load entire deck
load_deck_details(my_deck, loader)
"""
from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING:
from tarot.card.card import Card
from tarot.card.details import CardDetailsRegistry
from tarot.deck import Deck
def load_card_details(
card: 'Card',
registry: Optional['CardDetailsRegistry'] = None
) -> bool:
"""
Load details for a single card from the registry.
Args:
card: The Card object to populate with details
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
Returns:
True if details were found and loaded, False otherwise
Example:
>>> from tarot import Deck
>>> deck = Deck()
>>> card = deck.major[0] # The Fool
>>> load_card_details(card)
True
>>> print(card.keywords)
['new beginnings', 'innocence', 'faith', ...]
"""
if registry is None:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
return registry.load_into_card(card)
def load_deck_details(
deck: 'Deck',
registry: Optional['CardDetailsRegistry'] = None,
verbose: bool = False
) -> int:
"""
Load details for all cards in a deck.
Args:
deck: The Deck object containing cards to populate
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
verbose: If True, prints information about each card loaded
Returns:
Number of cards successfully loaded with details
Example:
>>> from tarot import Deck
>>> deck = Deck()
>>> count = load_deck_details(deck, verbose=True)
>>> print(f"Loaded details for {count} cards")
"""
if registry is None:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
loaded_count = 0
failed_cards = []
# Load all cards from the deck
for card in deck.cards:
if load_card_details(card, registry):
loaded_count += 1
if verbose:
print(f"✓ Loaded: {card.name}")
else:
failed_cards.append(card.name)
if verbose:
print(f"✗ Failed: {card.name}")
if verbose and failed_cards:
print(f"\n{len(failed_cards)} cards failed to load:")
for name in failed_cards:
print(f" - {name}")
return loaded_count
def get_cards_by_suit(
deck: 'Deck',
suit_name: str
) -> List['Card']:
"""
Get all cards from a specific suit in the deck.
Args:
deck: The Deck object
suit_name: The suit name ("Cups", "Pentacles", "Swords", "Wands")
Returns:
List of Card objects from that suit
Example:
>>> from tarot import Deck
>>> from tarot.card.loader import get_cards_by_suit
>>> deck = Deck()
>>> swords = get_cards_by_suit(deck, "Swords")
>>> print(len(swords)) # Should be 14
14
"""
if hasattr(deck, 'suit') and callable(deck.suit):
# Deck has a suit method, use it
return deck.suit(suit_name)
# Fallback: filter cards manually
return [card for card in deck.cards if hasattr(card, 'suit') and
card.suit and card.suit.name == suit_name]
def filter_cards_by_keywords(
cards: List['Card'],
keyword: str
) -> List['Card']:
"""
Filter a list of cards by keyword.
Args:
cards: List of Card objects to filter
keyword: The keyword to search for (case-insensitive)
Returns:
List of cards that have the keyword
Example:
>>> from tarot import Deck
>>> deck = Deck()
>>> all_cards = list(deck.major.cards()) + list(deck.minor.cups.cards())
>>> love_cards = filter_cards_by_keywords(all_cards, "love")
"""
keyword_lower = keyword.lower()
return [
card for card in cards
if hasattr(card, 'keywords') and card.keywords and
any(keyword_lower in kw.lower() for kw in card.keywords)
]
def print_card_details(card: 'Card', include_reversed: bool = False) -> None:
"""
Pretty print card details to console.
Args:
card: The Card object to print
include_reversed: If True, also print reversed keywords and interpretation
Example:
>>> from tarot import Deck
>>> deck = Deck()
>>> card = deck.major[0] # The Fool
>>> print_card_details(card)
"""
print(f"\n{'=' * 60}")
print(f" {card.name}")
print(f"{'=' * 60}")
# Define attributes to print with their formatting
attributes = {
'explanation': ('Explanation', False),
'interpretation': ('Interpretation', False),
'guidance': ('Guidance', False),
}
# Add reversed attributes only if requested
if include_reversed:
attributes['reversed_interpretation'] = ('Reversed Interpretation', False)
# List attributes (joined with commas)
list_attributes = {
'keywords': 'Keywords',
'reversed_keywords': ('Reversed Keywords', include_reversed),
}
# Numeric attributes
numeric_attributes = {
'numerology': 'Numerology',
}
# Print text attributes
for attr_name, (display_name, _) in attributes.items():
if hasattr(card, attr_name):
value = getattr(card, attr_name)
if value:
print(f"\n{display_name}:\n{value}")
# Print list attributes
for attr_name, display_info in list_attributes.items():
if isinstance(display_info, tuple):
display_name, should_show = display_info
if not should_show:
continue
else:
display_name = display_info
if hasattr(card, attr_name):
value = getattr(card, attr_name)
if value:
print(f"\n{display_name}: {', '.join(value)}")
# Print numeric attributes
for attr_name, display_name in numeric_attributes.items():
if hasattr(card, attr_name):
value = getattr(card, attr_name)
if value is not None:
print(f"\n{display_name}: {value}")
print(f"\n{'=' * 60}\n")
def get_card_info(
card_name: str,
registry: Optional['CardDetailsRegistry'] = None
) -> Optional[dict]:
"""
Get card information by card name.
Args:
card_name: The name of the card (e.g., "Princess of Swords")
registry: Optional CardDetailsRegistry. If not provided, creates a new one.
Returns:
Dictionary containing card details, or None if not found
Example:
>>> from tarot.card.loader import get_card_info
>>> info = get_card_info("Princess of Swords")
>>> if info:
... print(info['explanation'])
"""
if registry is None:
from tarot.card.details import CardDetailsRegistry
registry = CardDetailsRegistry()
return registry.get(card_name)

323
src/tarot/card/spread.py Normal file
View File

@@ -0,0 +1,323 @@
"""
Tarot spread definitions and management with card drawing.
Provides predefined spreads like Celtic Cross, Golden Dawn (3-card), etc.
with position meanings and automatic card drawing.
Usage:
from tarot import Tarot
# Draw cards for a spread
reading = Tarot.deck.card.spread('Celtic Cross')
print(reading)
# Can also access spread with/without cards
from tarot.card.spread import Spread, draw_spread
spread = Spread('Celtic Cross')
reading = draw_spread(spread) # Returns list of (position, card) tuples
"""
from typing import Dict, List, Optional, TYPE_CHECKING
from dataclasses import dataclass
import random
if TYPE_CHECKING:
from tarot.card import Card
@dataclass
class SpreadPosition:
"""Represents a position in a Tarot spread."""
number: int
name: str
meaning: str
reversed_meaning: Optional[str] = None
def __str__(self) -> str:
result = f"{self.number}. {self.name}: {self.meaning}"
if self.reversed_meaning:
result += f"\n (Reversed: {self.reversed_meaning})"
return result
@dataclass
class DrawnCard:
"""Represents a card drawn for a spread position."""
position: SpreadPosition
card: 'Card'
is_reversed: bool
def __str__(self) -> str:
"""Format the drawn card with position and interpretation."""
card_name = self.card.name
if self.is_reversed:
card_name += " (Reversed)"
return f"{self.position.number}. {self.position.name}\n" \
f" └─ {card_name}\n" \
f" └─ Position: {self.position.meaning}"
class Spread:
"""Represents a Tarot spread with positions and meanings."""
# Define all available spreads
SPREADS: Dict[str, Dict] = {
'three card': {
'name': '3-Card Spread',
'description': 'Simple 3-card spread for past, present, future or situation, action, outcome',
'positions': [
SpreadPosition(1, 'First Position', 'Past, Foundation, or Situation'),
SpreadPosition(2, 'Second Position', 'Present, Action, or Influence'),
SpreadPosition(3, 'Third Position', 'Future, Outcome, or Advice'),
]
},
'golden dawn': {
'name': 'Golden Dawn 3-Card',
'description': 'Three card spread used in Golden Dawn tradition',
'positions': [
SpreadPosition(1, 'Supernal Triangle', 'Spiritual/Divine aspect'),
SpreadPosition(2, 'Pillar of Severity', 'Challenging/Active force'),
SpreadPosition(3, 'Pillar of Mercy', 'Supportive/Passive force'),
]
},
'celtic cross': {
'name': 'Celtic Cross',
'description': 'Classic 10-card spread for in-depth reading',
'positions': [
SpreadPosition(1, 'The Significator', 'The main situation or person'),
SpreadPosition(2, 'The Cross', 'The challenge or heart of the matter'),
SpreadPosition(3, 'Crowning Influence', 'Conscious hopes/ideals'),
SpreadPosition(4, 'Beneath the Cross', 'Unconscious or hidden aspects'),
SpreadPosition(5, 'Behind', 'Past influences'),
SpreadPosition(6, 'Before', 'Future influences'),
SpreadPosition(7, 'Self/Attitude', 'How the querent sees themselves'),
SpreadPosition(8, 'Others/Environment', 'External factors/opinions'),
SpreadPosition(9, 'Hopes and Fears', 'What the querent hopes for or fears'),
SpreadPosition(10, 'Outcome', 'Final outcome or resolution'),
]
},
'horseshoe': {
'name': 'Horseshoe',
'description': '7-card spread in horseshoe formation for past, present, future insight',
'positions': [
SpreadPosition(1, 'Distant Past', 'Ancient influences and foundations'),
SpreadPosition(2, 'Recent Past', 'Recent events and circumstances'),
SpreadPosition(3, 'Present Situation', 'Current state of affairs'),
SpreadPosition(4, 'Immediate Future', 'Near-term developments'),
SpreadPosition(5, 'Distant Future', 'Long-term outcome'),
SpreadPosition(6, 'Inner Influence', 'Self/thoughts/emotions'),
SpreadPosition(7, 'Outer Influence', 'External forces and environment'),
]
},
'pentagram': {
'name': 'Pentagram',
'description': '5-card spread based on Earth element pentagram',
'positions': [
SpreadPosition(1, 'Spirit', 'Core essence or spiritual truth'),
SpreadPosition(2, 'Fire', 'Action and willpower'),
SpreadPosition(3, 'Water', 'Emotions and intuition'),
SpreadPosition(4, 'Air', 'Intellect and communication'),
SpreadPosition(5, 'Earth', 'Physical manifestation and grounding'),
]
},
'tree of life': {
'name': 'Tree of Life',
'description': '10-card spread mapping Sephiroth on the Tree of Life',
'positions': [
SpreadPosition(1, 'Kether (Crown)', 'Divine will and unity'),
SpreadPosition(2, 'Chokmah (Wisdom)', 'Creative force and impulse'),
SpreadPosition(3, 'Binah (Understanding)', 'Form and structure'),
SpreadPosition(4, 'Chesed (Mercy)', 'Expansion and abundance'),
SpreadPosition(5, 'Gevurah (Severity)', 'Reduction and discipline'),
SpreadPosition(6, 'Tiphareth (Beauty)', 'Core self and integration'),
SpreadPosition(7, 'Netzach (Victory)', 'Desire and passion'),
SpreadPosition(8, 'Hod (Splendor)', 'Intellect and communication'),
SpreadPosition(9, 'Yesod (Foundation)', 'Subconscious and dreams'),
SpreadPosition(10, 'Malkuth (Kingdom)', 'Manifestation and physical reality'),
]
},
'relationship': {
'name': 'Relationship',
'description': '5-card spread for relationship insight',
'positions': [
SpreadPosition(1, 'You', 'Your position, feelings, or role'),
SpreadPosition(2, 'Them', 'Their position, feelings, or perspective'),
SpreadPosition(3, 'The Relationship', 'The dynamic and connection'),
SpreadPosition(4, 'Challenge', 'Current challenge or friction point'),
SpreadPosition(5, 'Outcome', 'Where the relationship is heading'),
]
},
'yes or no': {
'name': 'Yes or No',
'description': '1-card spread for simple yes/no answers',
'positions': [
SpreadPosition(1, 'Answer', 'Major Arcana = Yes, Minor Arcana = No, Court Cards = Maybe'),
]
},
}
def __init__(self, spread_name: str) -> None:
"""
Initialize a spread by name (case-insensitive).
Args:
spread_name: Name of the spread to use
Raises:
ValueError: If spread name not found
"""
# Normalize name (case-insensitive, allow underscores or spaces)
normalized_name = spread_name.lower().replace('_', ' ')
# Find matching spread
spread_data = None
for key, data in self.SPREADS.items():
if key == normalized_name or data['name'].lower() == normalized_name:
spread_data = data
break
if not spread_data:
available = ', '.join(f"'{k}'" for k in self.SPREADS.keys())
raise ValueError(
f"Spread '{spread_name}' not found. Available spreads: {available}"
)
self.name = spread_data['name']
self.description = spread_data['description']
self.positions: List[SpreadPosition] = spread_data['positions']
def __str__(self) -> str:
"""Return formatted spread information."""
lines = [
f"═══════════════════════════════════════════",
f" {self.name}",
f"═══════════════════════════════════════════",
f"",
f"{self.description}",
f"",
f"Positions ({len(self.positions)} cards):",
f"",
]
for pos in self.positions:
lines.append(f" {pos}")
lines.append(f"")
lines.append(f"═══════════════════════════════════════════")
return "\n".join(lines)
def __repr__(self) -> str:
return f"Spread('{self.name}')"
@classmethod
def available_spreads(cls) -> str:
"""Return list of all available spreads."""
lines = [
"Available Tarot Spreads:",
"" * 50,
""
]
for key, data in cls.SPREADS.items():
lines.append(f"{data['name']}")
lines.append(f" Name for API: '{key}'")
lines.append(f" Positions: {len(data['positions'])}")
lines.append(f" {data['description']}")
lines.append("")
return "\n".join(lines)
def get_position(self, position_number: int) -> Optional[SpreadPosition]:
"""Get a specific position by number."""
for pos in self.positions:
if pos.number == position_number:
return pos
return None
def draw_spread(spread: Spread, deck: Optional[List] = None) -> List[DrawnCard]:
"""
Draw cards for all positions in a spread.
Args:
spread: The Spread object with positions defined
deck: Optional list of Card objects. If None, uses Tarot.deck.cards
Returns:
List of DrawnCard objects (one per position) with random cards and reversals
"""
import random
# Load deck if not provided
if deck is None:
from tarot.deck import Deck
deck_instance = Deck()
deck = deck_instance.cards
drawn_cards = []
for position in spread.positions:
# Draw random card
card = random.choice(deck)
# Random reversal (50% chance)
is_reversed = random.choice([True, False])
drawn_cards.append(DrawnCard(position, card, is_reversed))
return drawn_cards
class SpreadReading:
"""Represents a complete tarot reading with cards drawn for a spread."""
def __init__(self, spread: Spread, drawn_cards: List[DrawnCard]) -> None:
"""
Initialize a reading with a spread and drawn cards.
Args:
spread: The Spread object
drawn_cards: List of DrawnCard objects
"""
self.spread = spread
self.drawn_cards = drawn_cards
def __str__(self) -> str:
"""Return formatted reading with all cards and interpretations."""
lines = [
f"╔═══════════════════════════════════════════╗",
f"{self.spread.name:40}",
f"╚═══════════════════════════════════════════╝",
f"",
f"{self.spread.description}",
f"",
f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
f"",
]
for drawn in self.drawn_cards:
card = drawn.card
card_name = card.name
if drawn.is_reversed:
card_name += " ◄ REVERSED"
lines.append(f"Position {drawn.position.number}: {drawn.position.name}")
lines.append(f" Card: {card_name}")
lines.append(f" Meaning: {drawn.position.meaning}")
# Add card details if available
if hasattr(card, 'number'):
lines.append(f" Card #: {card.number}")
if hasattr(card, 'arcana'):
lines.append(f" Arcana: {card.arcana}")
if hasattr(card, 'suit') and card.suit:
lines.append(f" Suit: {card.suit.name}")
lines.append("")
lines.append(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
return "\n".join(lines)
def __repr__(self) -> str:
return f"SpreadReading({self.spread.name}, {len(self.drawn_cards)} cards)"

View File

@@ -0,0 +1,32 @@
"""
Tarot deck module - Core card and deck classes.
Provides the Deck class for managing Tarot cards and the Card, MajorCard,
MinorCard, and related classes for representing individual cards.
"""
from .deck import (
Card,
MajorCard,
MinorCard,
PipCard,
AceCard,
CourtCard,
CardQuery,
TemporalQuery,
DLT,
Deck,
)
__all__ = [
"Card",
"MajorCard",
"MinorCard",
"PipCard",
"AceCard",
"CourtCard",
"CardQuery",
"TemporalQuery",
"DLT",
"Deck",
]

734
src/tarot/deck/deck.py Normal file
View File

@@ -0,0 +1,734 @@
"""
Core Tarot deck and card classes.
This module defines the Deck class for managing Tarot cards and the Card,
MajorCard, and MinorCard classes for representing individual cards.
"""
from dataclasses import dataclass, field
from typing import List, Optional, Tuple, TYPE_CHECKING
import random
from ..attributes import (
Meaning, CardImage, Suit, Zodiac, Element, Path,
Planet, Sephera, Color, PeriodicTable, ElementType, DoublLetterTrump
)
if TYPE_CHECKING:
from ..card.data import CardDataLoader
# Global CardDataLoader instance for accessing elements
_card_data = None # Will be initialized lazily
def _get_card_data():
"""Get or initialize the global CardDataLoader instance."""
global _card_data
if _card_data is None:
from ..card.data import CardDataLoader
_card_data = CardDataLoader()
return _card_data
@dataclass
class Card:
"""Base class representing a Tarot card."""
number: int
name: str
meaning: Meaning
arcana: str # "Major" or "Minor"
image: Optional[CardImage] = None
# These are overridden in subclasses but declared here for MinorCard compatibility
suit: Optional[Suit] = None
pip: int = 0
# Card-specific details
explanation: str = ""
interpretation: str = ""
keywords: List[str] = field(default_factory=list)
reversed_keywords: List[str] = field(default_factory=list)
guidance: str = ""
numerology: Optional[int] = None
# Image path for custom deck images
image_path: Optional[str] = None
def __str__(self) -> str:
return f"{self.number}. {self.name}"
def __repr__(self) -> str:
return f"Card({self.number}, '{self.name}')"
def key(self) -> str:
"""
Get the card's key as a Roman numeral representation.
Returns:
Roman numeral string (e.g., "I", "XXI") for Major Arcana,
or the pip number as string for Minor Arcana.
"""
# Import here to avoid circular imports
from ..card.details import CardDetailsRegistry
# For Major Arcana cards, convert the key to Roman numerals
if self.arcana == "Major":
return CardDetailsRegistry.key_to_roman(self.number)
# For Minor Arcana, return the pip number as a formatted string
if hasattr(self, 'pip') and self.pip > 0:
pip_names = {
2: "Two", 3: "Three", 4: "Four", 5: "Five",
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten"
}
return pip_names.get(self.pip, str(self.pip))
return str(self.number)
@property
def type(self) -> str:
"""Get the specific card type (Major, Pip, Ace, Court)."""
if isinstance(self, MajorCard):
return "Major"
elif isinstance(self, AceCard):
return "Ace"
elif isinstance(self, CourtCard):
return "Court"
elif isinstance(self, PipCard):
return "Pip"
return "Unknown"
@dataclass
class MajorCard(Card):
"""Represents a Major Arcana card."""
kabbalistic_number: Optional[int] = None
tarot_letter: Optional[str] = None
tree_of_life_path: Optional[int] = None
def __post_init__(self) -> None:
# Kabbalistic number should be 0-21, but deck position can be anywhere
if self.kabbalistic_number is not None and (self.kabbalistic_number < 0 or self.kabbalistic_number > 21):
raise ValueError(f"Major Arcana kabbalistic number must be 0-21, got {self.kabbalistic_number}")
self.arcana = "Major"
@dataclass
class MinorCard(Card):
"""Represents a Minor Arcana card - either Pip or Court card."""
suit: Suit = None # type: ignore
astrological_influence: Optional[str] = None
element: Optional[Element] = None
def __post_init__(self) -> None:
if self.suit is None:
raise ValueError("suit must be provided for MinorCard")
self.arcana = "Minor"
@dataclass
class PipCard(MinorCard):
"""Represents a Pip card (2 through 10) - has a pip number.
Pip cards represent numbered forces in their suit, from Two
through its full development (10).
"""
pip: int = 0
def __post_init__(self) -> None:
if not (2 <= self.pip <= 10):
raise ValueError(f"Pip card number must be 2-10, got {self.pip}")
super().__post_init__()
@dataclass
class AceCard(MinorCard):
"""Represents an Ace card - the root/foundation of the suit.
The Ace is the initial force of the suit and contains the potential
for all other cards within that suit. Aces have pip=1 but are not
technically pip cards.
"""
pip: int = 1
def __post_init__(self) -> None:
if self.pip != 1:
raise ValueError(f"AceCard must have pip 1, got {self.pip}")
super().__post_init__()
@dataclass
class CourtCard(MinorCard):
"""Represents a Court Card - Knight, Prince, Princess, or Queen.
Court cards represent people/personalities and are the highest rank
in the minor arcana. They do NOT have pips - they are archetypes.
Each court card is associated with an element and Hebrew letter (Path):
- Knight: Fire + Yod (path 20)
- Prince: Air + Vav (path 16)
- Princess: Earth + Heh (path 15)
- Queen: Water + Heh (path 15)
"""
COURT_RANKS = {"Knight": 12, "Prince": 11, "Princess": 13, "Queen": 14}
court_rank: str = ""
associated_element: Optional[ElementType] = None
hebrew_letter_path: Optional['Path'] = None
def __post_init__(self) -> None:
if self.court_rank not in self.COURT_RANKS:
raise ValueError(
f"CourtCard must have court_rank in {list(self.COURT_RANKS.keys())}, "
f"got {self.court_rank}"
)
super().__post_init__()
class CardQuery:
"""Helper class for fluent card queries: deck.number(3).minor.wands"""
def __init__(self, deck: 'Deck', number: Optional[int] = None,
arcana: Optional[str] = None) -> None:
self.deck = deck
self.number = number
self.arcana = arcana
def _filter_cards(self) -> List[Card]:
"""Get filtered cards based on current query state."""
cards = self.deck.cards
if self.number is not None:
cards = [c for c in cards if c.number == self.number or
(hasattr(c, 'pip') and c.pip == self.number)]
if self.arcana is not None:
cards = [c for c in cards if c.arcana == self.arcana]
return cards
@property
def major(self) -> List[Card]:
"""Filter to Major Arcana only."""
return [c for c in self._filter_cards() if c.arcana == "Major"]
@property
def minor(self) -> 'CardQuery':
"""Filter to Minor Arcana, return new CardQuery for suit chaining."""
return CardQuery(self.deck, self.number, "Minor")
@property
def cups(self) -> List[Card]:
"""Get cards in Cups suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Cups"]
@property
def swords(self) -> List[Card]:
"""Get cards in Swords suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Swords"]
@property
def wands(self) -> List[Card]:
"""Get cards in Wands suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Wands"]
@property
def pentacles(self) -> List[Card]:
"""Get cards in Pentacles suit."""
return [c for c in self._filter_cards() if hasattr(c, 'suit') and
c.suit and c.suit.name == "Pentacles"]
def __iter__(self):
"""Allow iteration over filtered cards."""
return iter(self._filter_cards())
def __len__(self) -> int:
"""Return count of filtered cards."""
return len(self._filter_cards())
def __getitem__(self, index: int) -> Card:
"""Get card by index from filtered results."""
return self._filter_cards()[index]
def __repr__(self) -> str:
cards = self._filter_cards()
names = [c.name for c in cards]
return f"CardQuery({names})"
class TemporalQuery:
"""Helper class for fluent temporal queries: loader.month(5).day(23).hour(15)"""
def __init__(self, loader: 'CardDataLoader', month_num: Optional[int] = None,
day_num: Optional[int] = None, hour_num: Optional[int] = None) -> None:
"""
Initialize temporal query builder.
Args:
loader: CardDataLoader instance for fetching temporal data
month_num: Month number (1-12)
day_num: Day number (1-31)
hour_num: Hour number (0-23)
"""
self.loader = loader
self.month_num = month_num
self.day_num = day_num
self.hour_num = hour_num
def month(self, num: int) -> 'TemporalQuery':
"""Set month (1-12) and return new query for chaining."""
return TemporalQuery(self.loader, month_num=num,
day_num=self.day_num, hour_num=self.hour_num)
def day(self, num: int) -> 'TemporalQuery':
"""Set day (1-31) and return new query for chaining."""
if self.month_num is None:
raise ValueError("Must set month before day")
return TemporalQuery(self.loader, month_num=self.month_num,
day_num=num, hour_num=self.hour_num)
def hour(self, num: int) -> 'TemporalQuery':
"""Set hour (0-23) and return new query for chaining."""
if self.month_num is None or self.day_num is None:
raise ValueError("Must set month and day before hour")
return TemporalQuery(self.loader, month_num=self.month_num,
day_num=self.day_num, hour_num=num)
def weekday(self) -> Optional[str]:
"""Get weekday name for current month/day combination using Zeller's congruence."""
if self.month_num is None or self.day_num is None:
raise ValueError("Must set month and day to get weekday")
# Zeller's congruence (adjusted for current calendar)
month = self.month_num
day = self.day_num
year = 2024 # Use current year as reference
# Adjust month and year for March-based calculation
if month < 3:
month += 12
year -= 1
# Zeller's formula
q = day
m = month
k = year % 100
j = year // 100
h = (q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) % 7
# Convert to weekday name (0=Saturday, 1=Sunday, 2=Monday, ..., 6=Friday)
day_names = ["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
return day_names[h]
def month_info(self):
"""Return month metadata for the configured query."""
if self.month_num is None:
return None
return self.loader.month_info(self.month_num)
def day_info(self):
"""Return day metadata for the configured query."""
if self.day_num is None:
return None
return self.loader.day_info(self.day_num)
def hour_info(self):
"""Return the planetary hour metadata for the configured query."""
if self.hour_num is None:
return None
return self.loader.clock_hour(self.hour_num)
def __repr__(self) -> str:
parts = []
if self.month_num:
parts.append(f"month={self.month_num}")
if self.day_num:
parts.append(f"day={self.day_num}")
if self.hour_num:
parts.append(f"hour={self.hour_num}")
return f"TemporalQuery({', '.join(parts)})"
class DLT:
"""
Double Letter Trump (DLT) accessor.
Double Letter Trumps are Major Arcana cards 3-21 (19 cards total),
each associated with a Hebrew letter and planetary/astrological force.
Usage:
dlt = DLT(3) # Get the 3rd Double Letter Trump (The Empress)
dlt = DLT(7) # Get the 7th Double Letter Trump (The Chariot)
"""
def __init__(self, trump_number: int) -> None:
"""
Initialize a Double Letter Trump query.
Args:
trump_number: Position in DLT sequence (3-21)
Raises:
ValueError: If trump_number is not 3-21
"""
if not 3 <= trump_number <= 21:
raise ValueError(f"DLT number must be 3-21, got {trump_number}")
self.trump_number = trump_number
self._loader: Optional['CardDataLoader'] = None
self._deck: Optional[Deck] = None
@property
def loader(self) -> 'CardDataLoader':
"""Lazy-load CardDataLoader on first access."""
if self._loader is None:
from ..card.data import CardDataLoader
self._loader = CardDataLoader()
return self._loader
@property
def deck(self) -> 'Deck':
"""Lazy-load Deck on first access."""
if self._deck is None:
self._deck = Deck()
return self._deck
def card(self) -> Optional[Card]:
"""Get the Tarot card for this DLT."""
# Major Arcana cards are numbered 0-21, so DLT(3) = Major card 3
for card in self.deck.cards:
if card.arcana == "Major" and card.number == self.trump_number:
return card
return None
def periodic_entry(self) -> Optional[PeriodicTable]:
"""Get the periodic table entry with cross-correspondences."""
return self.loader.periodic_entry(self.trump_number)
def sephera(self) -> Optional[Sephera]:
"""Get the Sephira associated with this DLT."""
return self.loader.sephera(self.trump_number)
def planet(self) -> Optional[Planet]:
"""Get the planetary ruler for this DLT."""
periodic = self.periodic_entry()
return periodic.planet if periodic else None
def element(self) -> Optional[ElementType]:
"""Get the element associated with this DLT."""
periodic = self.periodic_entry()
return periodic.element if periodic else None
def hebrew_letter(self) -> Optional[str]:
"""Get the Hebrew letter associated with this DLT."""
periodic = self.periodic_entry()
return periodic.hebrew_letter if periodic else None
def color(self) -> Optional[Color]:
"""Get the color associated with this DLT."""
periodic = self.periodic_entry()
return periodic.color if periodic else None
def __repr__(self) -> str:
card = self.card()
card_name = card.name if card else "Unknown"
return f"DLT({self.trump_number}) - {card_name}"
def __str__(self) -> str:
return self.__repr__()
class Deck:
"""Represents a standard 78-card Tarot deck."""
def __init__(self) -> None:
"""Initialize a standard Tarot deck with all 78 cards."""
self.cards: List[Card] = []
self.discard_pile: List[Card] = []
self._initialize_deck()
def _initialize_deck(self) -> None:
"""Initialize the deck with all 78 Tarot cards.
Order: Cups (1-14), Pentacles/Disks (15-28), Swords (29-42),
Major Arcana (43-64), Wands (65-78)
This puts Queen of Wands as card #78, the final card.
"""
# Minor Arcana - First three suits (Cups, Pentacles, Swords)
# Organized logically: Ace, 10, 2-9, then court cards Knight, Prince, Princess, Queen
# Get ElementType instances from CardDataLoader
card_data = _get_card_data()
water_element = card_data.element("Water")
earth_element = card_data.element("Earth")
air_element = card_data.element("Air")
fire_element = card_data.element("Fire")
if not water_element or not earth_element or not air_element or not fire_element:
raise RuntimeError("Failed to load element data from CardDataLoader")
# Get Hebrew letters (Paths) for court cards
yod_path = card_data.path(20) # Yod
vav_path = card_data.path(16) # Vav
he_path = card_data.path(15) # He (Heh)
if not yod_path or not vav_path or not he_path:
raise RuntimeError("Failed to load Hebrew letter/path data from CardDataLoader")
# Map court ranks to their associated elements and Hebrew letter paths
# Knight -> Fire + Yod, Prince -> Air + Vav, Princess -> Earth + Heh, Queen -> Water + Heh
court_rank_mappings = {
"Knight": (fire_element, yod_path),
"Prince": (air_element, vav_path),
"Princess": (earth_element, he_path),
"Queen": (water_element, he_path),
}
suits_data_first = [
("Cups", water_element, 2),
("Pentacles", earth_element, 4),
("Swords", air_element, 3),
]
# Pip order: Ace (1), Ten (10), Two-Nine (2-9), Knight (12), Prince (11), Princess (13), Queen (14)
pip_order = [1, 10, 2, 3, 4, 5, 6, 7, 8, 9, 12, 11, 13, 14]
pip_names = {
1: "Ace", 2: "Two", 3: "Three", 4: "Four", 5: "Five",
6: "Six", 7: "Seven", 8: "Eight", 9: "Nine", 10: "Ten",
11: "Prince", 12: "Knight", 13: "Princess", 14: "Queen"
}
card_number = 1
# Pip order: Ace (1), Ten (10), Two-Nine (2-9), then Court cards Knight (12), Prince (11), Princess (13), Queen (14)
# Map pip_order indices to actual pip numbers (1-10 only for pips)
pip_index_to_number = {
1: 1, # Ace
10: 10, # Ten
2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9 # Two through Nine
}
court_ranks = {
12: "Knight", 11: "Prince", 13: "Princess", 14: "Queen"
}
# Loop through first three suits
for suit_name, element_name, suit_num in suits_data_first:
suit = Suit(name=suit_name, element=element_name,
tarot_correspondence=f"{suit_name} Suit", number=suit_num)
# Then loop through each position in the custom order
for pip_index in pip_order:
# Create appropriate card type based on pip_index
if pip_index <= 10:
# Pip card (Ace through 10)
actual_pip = pip_index_to_number[pip_index]
if pip_index == 1:
# Ace card
card = AceCard(
number=card_number,
name=f"{pip_names[pip_index]} of {suit_name}",
meaning=Meaning(
upright=f"{pip_names[pip_index]} of {suit_name} upright",
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
),
arcana="Minor",
suit=suit,
pip=actual_pip
)
else:
# Regular pip card (2-10)
card = PipCard(
number=card_number,
name=f"{pip_names[pip_index]} of {suit_name}",
meaning=Meaning(
upright=f"{pip_names[pip_index]} of {suit_name} upright",
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
),
arcana="Minor",
suit=suit,
pip=actual_pip
)
else:
# Court card (no pip)
court_rank = court_ranks[pip_index]
associated_element, hebrew_letter_path = court_rank_mappings[court_rank]
card = CourtCard(
number=card_number,
name=f"{pip_names[pip_index]} of {suit_name}",
meaning=Meaning(
upright=f"{pip_names[pip_index]} of {suit_name} upright",
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
),
arcana="Minor",
suit=suit,
court_rank=court_rank,
associated_element=associated_element,
hebrew_letter_path=hebrew_letter_path
)
self.cards.append(card)
card_number += 1
# Major Arcana (43-64)
# Names match filenames in src/tarot/deck/default/
major_arcana_names = [
"Fool", "Magus", "Fortune", "Lust", "Hanged Man", "Death",
"Art", "Devil", "Tower", "Star", "Moon", "Sun",
"High Priestess", "Empress", "Emperor", "Hierophant",
"Lovers", "Chariot", "Justice", "Hermit", "Aeon", "Universe"
]
for i, name in enumerate(major_arcana_names):
card = MajorCard(
number=card_number,
name=name,
meaning=Meaning(
upright=f"{name} upright meaning",
reversed=f"{name} reversed meaning"
),
arcana="Major",
kabbalistic_number=i
)
self.cards.append(card)
card_number += 1
# Minor Arcana - Last suit (Wands, 65-78)
# Organized logically: Ace, 10, 2-9, then court cards Knight, Prince, Princess, Queen
suits_data_last = [
("Wands", fire_element, 1),
]
# Loop through last suit
for suit_name, element_name, suit_num in suits_data_last:
suit = Suit(name=suit_name, element=element_name,
tarot_correspondence=f"{suit_name} Suit", number=suit_num)
# Then loop through each position in the custom order
for pip_index in pip_order:
# Create appropriate card type based on pip_index
if pip_index <= 10:
# Pip card (Ace through 10)
actual_pip = pip_index_to_number[pip_index]
if pip_index == 1:
# Ace card
card = AceCard(
number=card_number,
name=f"{pip_names[pip_index]} of {suit_name}",
meaning=Meaning(
upright=f"{pip_names[pip_index]} of {suit_name} upright",
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
),
arcana="Minor",
suit=suit,
pip=actual_pip
)
else:
# Regular pip card (2-10)
card = PipCard(
number=card_number,
name=f"{pip_names[pip_index]} of {suit_name}",
meaning=Meaning(
upright=f"{pip_names[pip_index]} of {suit_name} upright",
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
),
arcana="Minor",
suit=suit,
pip=actual_pip
)
else:
# Court card (no pip)
court_rank = court_ranks[pip_index]
associated_element, hebrew_letter_path = court_rank_mappings[court_rank]
card = CourtCard(
number=card_number,
name=f"{pip_names[pip_index]} of {suit_name}",
meaning=Meaning(
upright=f"{pip_names[pip_index]} of {suit_name} upright",
reversed=f"{pip_names[pip_index]} of {suit_name} reversed"
),
arcana="Minor",
suit=suit,
court_rank=court_rank,
associated_element=associated_element,
hebrew_letter_path=hebrew_letter_path
)
self.cards.append(card)
card_number += 1
def shuffle(self) -> None:
"""Shuffle the deck."""
random.shuffle(self.cards)
def draw(self, num_cards: int = 1) -> List[Card]:
"""
Draw cards from the deck.
Args:
num_cards: Number of cards to draw (default: 1)
Returns:
List of drawn cards
"""
if num_cards < 1:
raise ValueError("Must draw at least 1 card")
if num_cards > len(self.cards):
raise ValueError(f"Cannot draw {num_cards} cards from deck with {len(self.cards)} cards")
drawn = []
for _ in range(num_cards):
drawn.append(self.cards.pop(0))
return drawn
def reset(self) -> None:
"""Reset the deck to its initial state."""
self.cards.clear()
self.discard_pile.clear()
self._initialize_deck()
def remaining(self) -> int:
"""Return the number of cards remaining in the deck."""
return len(self.cards)
def number(self, pip_value: int) -> CardQuery:
"""
Query cards by number (pip value).
Usage:
deck.number(3) # All cards with 3
deck.number(3).minor # All minor 3s
deck.number(3).minor.wands # 3 of Wands
"""
return CardQuery(self, pip_value)
def suit(self, suit_name: str) -> List[Card]:
"""
Get all cards from a specific suit.
Usage:
deck.suit("Wands")
"""
return [c for c in self.cards if hasattr(c, 'suit') and
c.suit and c.suit.name == suit_name]
@property
def major(self) -> List[Card]:
"""Get all Major Arcana cards."""
return [c for c in self.cards if c.arcana == "Major"]
@property
def minor(self) -> List[Card]:
"""Get all Minor Arcana cards."""
return [c for c in self.cards if c.arcana == "Minor"]
def __len__(self) -> int:
"""Return the number of cards in the deck."""
return len(self.cards)
def __repr__(self) -> str:
return f"Deck({len(self.cards)} cards remaining)"

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Some files were not shown because too many files have changed in this diff Show More