k
21
LICENSE
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
6
src/kaballah/cube/__init__.py
Normal 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"]
|
||||||
513
src/kaballah/cube/attributes.py
Normal 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
@@ -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)
|
||||||
5
src/kaballah/tree/__init__.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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": "Lí", "element": "Fire", "attribute": "Clinging", "binary": "101", "description": "Radiant clarity that adheres to insight."},
|
||||||
|
{"name": "Zhen", "chinese": "震", "pinyin": "Zhèn", "element": "Thunder", "attribute": "Arousing", "binary": "001", "description": "Sudden awakening that shakes stagnation."},
|
||||||
|
{"name": "Xun", "chinese": "巽", "pinyin": "Xùn", "element": "Wind", "attribute": "Gentle", "binary": "110", "description": "Penetrating influence that persuades subtly."},
|
||||||
|
{"name": "Kan", "chinese": "坎", "pinyin": "Kǎn", "element": "Water", "attribute": "Abysmal", "binary": "010", "description": "Depth, risk, and sincere feeling."},
|
||||||
|
{"name": "Gen", "chinese": "艮", "pinyin": "Gèn", "element": "Mountain", "attribute": "Stillness", "binary": "100", "description": "Grounded rest that establishes boundaries."},
|
||||||
|
{"name": "Kun", "chinese": "坤", "pinyin": "Kūn", "element": "Earth", "attribute": "Receptive", "binary": "000", "description": "Vast receptivity that nurtures form."},
|
||||||
|
]
|
||||||
|
self._trigrams = {}
|
||||||
|
for spec in trigram_specs:
|
||||||
|
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": "Xū", "judgement": "Hold position until nourishment arrives.", "image": "Water above heaven depicts clouds gathering provision.", "upper": "Kan", "lower": "Qian", "keywords": "Patience|Faith|Preparation"},
|
||||||
|
{"number": 6, "name": "Conflict", "chinese": "訟", "pinyin": "Sòng", "judgement": "Clarity and fairness prevent escalation.", "image": "Heaven above water shows tension seeking balance.", "upper": "Qian", "lower": "Kan", "keywords": "Debate|Justice|Boundaries"},
|
||||||
|
{"number": 7, "name": "Collective Force", "chinese": "師", "pinyin": "Shī", "judgement": "Coordinated effort requires disciplined leadership.", "image": "Earth over water mirrors troops marshaling supplies.", "upper": "Kun", "lower": "Kan", "keywords": "Discipline|Leadership|Community"},
|
||||||
|
{"number": 8, "name": "Union", "chinese": "比", "pinyin": "Bǐ", "judgement": "Shared values attract loyal allies.", "image": "Water over earth highlights bonds formed through empathy.", "upper": "Kan", "lower": "Kun", "keywords": "Alliance|Affinity|Trust"},
|
||||||
|
{"number": 9, "name": "Small Accumulating", "chinese": "小畜", "pinyin": "Xiǎo Chù", "judgement": "Gentle restraint nurtures gradual gains.", "image": "Wind over heaven indicates tender guidance on great power.", "upper": "Xun", "lower": "Qian", "keywords": "Restraint|Cultivation|Care"},
|
||||||
|
{"number": 10, "name": "Treading", "chinese": "履", "pinyin": "Lǚ", "judgement": "Walk with awareness when near power.", "image": "Heaven over lake shows respect between ranks.", "upper": "Qian", "lower": "Dui", "keywords": "Conduct|Respect|Sensitivity"},
|
||||||
|
{"number": 11, "name": "Peace", "chinese": "泰", "pinyin": "Tài", "judgement": "Harmony thrives when resources circulate freely.", "image": "Earth over heaven signals prosperity descending.", "upper": "Kun", "lower": "Qian", "keywords": "Harmony|Prosperity|Flourish"},
|
||||||
|
{"number": 12, "name": "Standstill", "chinese": "否", "pinyin": "Pǐ", "judgement": "When channels close, conserve strength.", "image": "Heaven over earth reveals blocked exchange.", "upper": "Qian", "lower": "Kun", "keywords": "Stagnation|Reflection|Pause"},
|
||||||
|
{"number": 13, "name": "Fellowship", "chinese": "同人", "pinyin": "Tóng Rén", "judgement": "Shared purpose unites distant hearts.", "image": "Heaven over fire shows clarity within community.", "upper": "Qian", "lower": "Li", "keywords": "Community|Shared Vision|Openness"},
|
||||||
|
{"number": 14, "name": "Great Possession", "chinese": "大有", "pinyin": "Dà Yǒu", "judgement": "Generosity cements lasting influence.", "image": "Fire over heaven reflects radiance sustained by ethics.", "upper": "Li", "lower": "Qian", "keywords": "Wealth|Stewardship|Confidence"},
|
||||||
|
{"number": 15, "name": "Modesty", "chinese": "謙", "pinyin": "Qiān", "judgement": "Balance is found by lowering the proud.", "image": "Earth over mountain reveals humility safeguarding strength.", "upper": "Kun", "lower": "Gen", "keywords": "Humility|Balance|Service"},
|
||||||
|
{"number": 16, "name": "Enthusiasm", "chinese": "豫", "pinyin": "Yù", "judgement": "Inspired music rallies the people.", "image": "Thunder over earth depicts drums stirring hearts.", "upper": "Zhen", "lower": "Kun", "keywords": "Inspiration|Celebration|Momentum"},
|
||||||
|
{"number": 17, "name": "Following", "chinese": "隨", "pinyin": "Suí", "judgement": "Adapt willingly to timely leadership.", "image": "Lake over thunder points to joyful allegiance.", "upper": "Dui", "lower": "Zhen", "keywords": "Adaptation|Loyalty|Flow"},
|
||||||
|
{"number": 18, "name": "Repairing", "chinese": "蠱", "pinyin": "Gǔ", "judgement": "Address decay with responsibility and care.", "image": "Mountain over wind shows correction of lineages.", "upper": "Gen", "lower": "Xun", "keywords": "Restoration|Accountability|Healing"},
|
||||||
|
{"number": 19, "name": "Approach", "chinese": "臨", "pinyin": "Lín", "judgement": "Leaders draw near to listen sincerely.", "image": "Earth over lake signifies compassion visiting the people.", "upper": "Kun", "lower": "Dui", "keywords": "Empathy|Guidance|Presence"},
|
||||||
|
{"number": 20, "name": "Contemplation", "chinese": "觀", "pinyin": "Guān", "judgement": "Observation inspires ethical alignment.", "image": "Wind over earth is the elevated view of the sage.", "upper": "Xun", "lower": "Kun", "keywords": "Perspective|Ritual|Vision"},
|
||||||
|
{"number": 21, "name": "Biting Through", "chinese": "噬嗑", "pinyin": "Shì Kè", "judgement": "Decisive action cuts through obstruction.", "image": "Fire over thunder shows justice enforced with clarity.", "upper": "Li", "lower": "Zhen", "keywords": "Decision|Justice|Resolve"},
|
||||||
|
{"number": 22, "name": "Grace", "chinese": "賁", "pinyin": "Bì", "judgement": "Beauty adorns substance when humility remains.", "image": "Mountain over fire highlights poise and restraint.", "upper": "Gen", "lower": "Li", "keywords": "Aesthetics|Poise|Form"},
|
||||||
|
{"number": 23, "name": "Splitting Apart", "chinese": "剝", "pinyin": "Bō", "judgement": "When decay spreads, strip away excess.", "image": "Mountain over earth signals outer shells falling.", "upper": "Gen", "lower": "Kun", "keywords": "Decline|Release|Truth"},
|
||||||
|
{"number": 24, "name": "Return", "chinese": "復", "pinyin": "Fù", "judgement": "Cycles renew when rest follows completion.", "image": "Earth over thunder marks the turning of the year.", "upper": "Kun", "lower": "Zhen", "keywords": "Renewal|Rhythm|Faith"},
|
||||||
|
{"number": 25, "name": "Innocence", "chinese": "無妄", "pinyin": "Wú Wàng", "judgement": "Sincerity triumphs over scheming.", "image": "Heaven over thunder shows spontaneous virtue.", "upper": "Qian", "lower": "Zhen", "keywords": "Authenticity|Spontaneity|Trust"},
|
||||||
|
{"number": 26, "name": "Great Taming", "chinese": "大畜", "pinyin": "Dà Chù", "judgement": "Conserve strength until action serves wisdom.", "image": "Mountain over heaven portrays restraint harnessing power.", "upper": "Gen", "lower": "Qian", "keywords": "Discipline|Reserve|Mastery"},
|
||||||
|
{"number": 27, "name": "Nourishment", "chinese": "頤", "pinyin": "Yí", "judgement": "Words and food alike must be chosen with care.", "image": "Mountain over thunder emphasizes mindful sustenance.", "upper": "Gen", "lower": "Zhen", "keywords": "Nutrition|Speech|Mindfulness"},
|
||||||
|
{"number": 28, "name": "Great Exceeding", "chinese": "大過", "pinyin": "Dà Guò", "judgement": "Bearing heavy loads demands flexibility.", "image": "Lake over wind shows a beam bending before it breaks.", "upper": "Dui", "lower": "Xun", "keywords": "Weight|Adaptability|Responsibility"},
|
||||||
|
{"number": 29, "name": "The Abyss", "chinese": "坎", "pinyin": "Kǎn", "judgement": "Repeated trials teach sincere caution.", "image": "Water over water is the perilous gorge.", "upper": "Kan", "lower": "Kan", "keywords": "Trial|Honesty|Depth"},
|
||||||
|
{"number": 30, "name": "Radiance", "chinese": "離", "pinyin": "Lí", "judgement": "Clarity is maintained by tending the flame.", "image": "Fire over fire represents brilliance sustained through care.", "upper": "Li", "lower": "Li", "keywords": "Illumination|Culture|Attention"},
|
||||||
|
{"number": 31, "name": "Influence", "chinese": "咸", "pinyin": "Xián", "judgement": "Sincere attraction arises from mutual respect.", "image": "Lake over mountain highlights responsive hearts.", "upper": "Dui", "lower": "Gen", "keywords": "Attraction|Mutuality|Sensitivity"},
|
||||||
|
{"number": 32, "name": "Duration", "chinese": "恒", "pinyin": "Héng", "judgement": "Commitment endures when balanced.", "image": "Thunder over wind speaks of constancy amid change.", "upper": "Zhen", "lower": "Xun", "keywords": "Commitment|Consistency|Rhythm"},
|
||||||
|
{"number": 33, "name": "Retreat", "chinese": "遯", "pinyin": "Dùn", "judgement": "Strategic withdrawal preserves integrity.", "image": "Heaven over mountain shows noble retreat.", "upper": "Qian", "lower": "Gen", "keywords": "Withdrawal|Strategy|Self-care"},
|
||||||
|
{"number": 34, "name": "Great Power", "chinese": "大壯", "pinyin": "Dà Zhuàng", "judgement": "Strength must remain aligned with virtue.", "image": "Thunder over heaven affirms action matched with purpose.", "upper": "Zhen", "lower": "Qian", "keywords": "Power|Ethics|Momentum"},
|
||||||
|
{"number": 35, "name": "Progress", "chinese": "晉", "pinyin": "Jìn", "judgement": "Advancement arrives through clarity and loyalty.", "image": "Fire over earth depicts dawn spreading across the plain.", "upper": "Li", "lower": "Kun", "keywords": "Advancement|Visibility|Service"},
|
||||||
|
{"number": 36, "name": "Darkening Light", "chinese": "明夷", "pinyin": "Míng Yí", "judgement": "Protect the inner light when circumstances grow harsh.", "image": "Earth over fire shows brilliance concealed for safety.", "upper": "Kun", "lower": "Li", "keywords": "Protection|Subtlety|Endurance"},
|
||||||
|
{"number": 37, "name": "Family", "chinese": "家人", "pinyin": "Jiā Rén", "judgement": "Clear roles nourish household harmony.", "image": "Wind over fire indicates rituals ordering the home.", "upper": "Xun", "lower": "Li", "keywords": "Home|Roles|Care"},
|
||||||
|
{"number": 38, "name": "Opposition", "chinese": "睽", "pinyin": "Kuí", "judgement": "Recognize difference without hostility.", "image": "Fire over lake reflects contrast seeking balance.", "upper": "Li", "lower": "Dui", "keywords": "Contrast|Perspective|Tolerance"},
|
||||||
|
{"number": 39, "name": "Obstruction", "chinese": "蹇", "pinyin": "Jiǎn", "judgement": "Turn hindrance into training.", "image": "Water over mountain shows difficult ascent.", "upper": "Kan", "lower": "Gen", "keywords": "Obstacle|Effort|Learning"},
|
||||||
|
{"number": 40, "name": "Deliverance", "chinese": "解", "pinyin": "Xiè", "judgement": "Relief comes when knots are untied.", "image": "Thunder over water portrays release after storm.", "upper": "Zhen", "lower": "Kan", "keywords": "Release|Solution|Breath"},
|
||||||
|
{"number": 41, "name": "Decrease", "chinese": "損", "pinyin": "Sǔn", "judgement": "Voluntary simplicity restores balance.", "image": "Mountain over lake shows graceful sharing of resources.", "upper": "Gen", "lower": "Dui", "keywords": "Simplicity|Offering|Balance"},
|
||||||
|
{"number": 42, "name": "Increase", "chinese": "益", "pinyin": "Yì", "judgement": "Blessings multiply when shared.", "image": "Wind over thunder reveals generous expansion.", "upper": "Xun", "lower": "Zhen", "keywords": "Growth|Generosity|Opportunity"},
|
||||||
|
{"number": 43, "name": "Breakthrough", "chinese": "夬", "pinyin": "Guài", "judgement": "Speak truth boldly to clear corruption.", "image": "Lake over heaven highlights decisive proclamation.", "upper": "Dui", "lower": "Qian", "keywords": "Resolution|Declaration|Courage"},
|
||||||
|
{"number": 44, "name": "Encounter", "chinese": "姤", "pinyin": "Gòu", "judgement": "Unexpected influence requires discernment.", "image": "Heaven over wind shows potent visitors arriving.", "upper": "Qian", "lower": "Xun", "keywords": "Encounter|Discernment|Temptation"},
|
||||||
|
{"number": 45, "name": "Gathering", "chinese": "萃", "pinyin": "Cuì", "judgement": "Unity grows when motive is sincere.", "image": "Lake over earth signifies assembly around shared cause.", "upper": "Dui", "lower": "Kun", "keywords": "Assembly|Devotion|Focus"},
|
||||||
|
{"number": 46, "name": "Ascending", "chinese": "升", "pinyin": "Shēng", "judgement": "Slow steady progress pierces obstacles.", "image": "Earth over wind shows roots pushing upward.", "upper": "Kun", "lower": "Xun", "keywords": "Growth|Perseverance|Aspiration"},
|
||||||
|
{"number": 47, "name": "Oppression", "chinese": "困", "pinyin": "Kùn", "judgement": "Constraints refine inner resolve.", "image": "Lake over water indicates fatigue relieved only by integrity.", "upper": "Dui", "lower": "Kan", "keywords": "Constraint|Endurance|Faith"},
|
||||||
|
{"number": 48, "name": "The Well", "chinese": "井", "pinyin": "Jǐng", "judgement": "Communal resources must be maintained.", "image": "Water over wind depicts a well drawing fresh insight.", "upper": "Kan", "lower": "Xun", "keywords": "Resource|Maintenance|Depth"},
|
||||||
|
{"number": 49, "name": "Revolution", "chinese": "革", "pinyin": "Gé", "judgement": "Change succeeds when timing and virtue align.", "image": "Lake over fire indicates shedding the old skin.", "upper": "Dui", "lower": "Li", "keywords": "Change|Timing|Renewal"},
|
||||||
|
{"number": 50, "name": "The Vessel", "chinese": "鼎", "pinyin": "Dǐng", "judgement": "Elevated service transforms the culture.", "image": "Fire over wind depicts the cauldron that refines offerings.", "upper": "Li", "lower": "Xun", "keywords": "Service|Transformation|Heritage"},
|
||||||
|
{"number": 51, "name": "Arousing Thunder", "chinese": "震", "pinyin": "Zhèn", "judgement": "Shock awakens the heart to reverence.", "image": "Thunder over thunder doubles the drumbeat of alertness.", "upper": "Zhen", "lower": "Zhen", "keywords": "Shock|Awakening|Movement"},
|
||||||
|
{"number": 52, "name": "Still Mountain", "chinese": "艮", "pinyin": "Gèn", "judgement": "Cultivate stillness to master desire.", "image": "Mountain over mountain shows unmoving focus.", "upper": "Gen", "lower": "Gen", "keywords": "Stillness|Meditation|Boundaries"},
|
||||||
|
{"number": 53, "name": "Gradual Development", "chinese": "漸", "pinyin": "Jiàn", "judgement": "Lasting progress resembles a tree growing rings.", "image": "Wind over mountain displays slow maturation.", "upper": "Xun", "lower": "Gen", "keywords": "Patience|Evolution|Commitment"},
|
||||||
|
{"number": 54, "name": "Marrying Maiden", "chinese": "歸妹", "pinyin": "Guī Mèi", "judgement": "Adjust expectations when circumstances limit rank.", "image": "Thunder over lake spotlights unequal partnerships.", "upper": "Zhen", "lower": "Dui", "keywords": "Transition|Adaptation|Protocol"},
|
||||||
|
{"number": 55, "name": "Abundance", "chinese": "豐", "pinyin": "Fēng", "judgement": "Radiant success must be handled with balance.", "image": "Thunder over fire illuminates the hall at noon.", "upper": "Zhen", "lower": "Li", "keywords": "Splendor|Responsibility|Timing"},
|
||||||
|
{"number": 56, "name": "The Wanderer", "chinese": "旅", "pinyin": "Lǚ", "judgement": "Travel lightly and guard reputation.", "image": "Fire over mountain marks a traveler tending the campfire.", "upper": "Li", "lower": "Gen", "keywords": "Travel|Restraint|Awareness"},
|
||||||
|
{"number": 57, "name": "Gentle Wind", "chinese": "巽", "pinyin": "Xùn", "judgement": "Persistent influence accomplishes what force cannot.", "image": "Wind over wind indicates subtle penetration.", "upper": "Xun", "lower": "Xun", "keywords": "Penetration|Diplomacy|Subtlety"},
|
||||||
|
{"number": 58, "name": "Joyous Lake", "chinese": "兌", "pinyin": "Duì", "judgement": "Openhearted dialogue dissolves resentment.", "image": "Lake over lake celebrates shared delight.", "upper": "Dui", "lower": "Dui", "keywords": "Joy|Conversation|Trust"},
|
||||||
|
{"number": 59, "name": "Dispersion", "chinese": "渙", "pinyin": "Huàn", "judgement": "Loosen rigid structures so spirit can move.", "image": "Wind over water shows breath dispersing fear.", "upper": "Xun", "lower": "Kan", "keywords": "Dissolve|Freedom|Relief"},
|
||||||
|
{"number": 60, "name": "Limitation", "chinese": "節", "pinyin": "Jié", "judgement": "Clear boundaries enable real freedom.", "image": "Water over lake portrays calibrated vessels.", "upper": "Kan", "lower": "Dui", "keywords": "Boundaries|Measure|Discipline"},
|
||||||
|
{"number": 61, "name": "Inner Truth", "chinese": "中孚", "pinyin": "Zhōng Fú", "judgement": "Trustworthiness unites disparate groups.", "image": "Wind over lake depicts resonance within the heart.", "upper": "Xun", "lower": "Dui", "keywords": "Sincerity|Empathy|Alignment"},
|
||||||
|
{"number": 62, "name": "Small Exceeding", "chinese": "小過", "pinyin": "Xiǎo Guò", "judgement": "Attend to details when stakes are delicate.", "image": "Thunder over mountain reveals careful movement.", "upper": "Zhen", "lower": "Gen", "keywords": "Detail|Caution|Adjustment"},
|
||||||
|
{"number": 63, "name": "After Completion", "chinese": "既濟", "pinyin": "Jì Jì", "judgement": "Success endures only if vigilance continues.", "image": "Water over fire displays balance maintained through work.", "upper": "Kan", "lower": "Li", "keywords": "Completion|Maintenance|Balance"},
|
||||||
|
{"number": 64, "name": "Before Completion", "chinese": "未濟", "pinyin": "Wèi Jì", "judgement": "Stay attentive as outcomes crystallize.", "image": "Fire over water illustrates the final push before harmony.", "upper": "Li", "lower": "Kan", "keywords": "Transition|Focus|Preparation"},
|
||||||
|
]
|
||||||
|
planet_cycle = ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Earth"]
|
||||||
|
self._hexagrams = {}
|
||||||
|
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()
|
||||||
42
src/letter/iChing_attributes.py
Normal 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
@@ -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
@@ -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()
|
||||||
5
src/letter/words/__init__.py
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
26
src/tarot/card/__init__.py
Normal 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
@@ -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
557
src/tarot/card/details.py
Normal 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)
|
||||||
346
src/tarot/card/image_loader.py
Normal 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
@@ -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
@@ -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)"
|
||||||
32
src/tarot/deck/__init__.py
Normal 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
@@ -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)"
|
||||||
BIN
src/tarot/deck/default/01_Ace Cups.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/02_Ten Cups.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/03_Two Cups.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/04_Three Cups.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/tarot/deck/default/05_Four Cups.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/06_Five Cups.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/tarot/deck/default/07_Six Cups.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/08_Seven Cups.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/tarot/deck/default/09_Eight Cups.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
src/tarot/deck/default/10_Nine Cups.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/11_Knight Cups.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
src/tarot/deck/default/12_Prince Cups.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/tarot/deck/default/13_Princess Cups.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/14_Queen Cups.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src/tarot/deck/default/15_Ace Disks.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/tarot/deck/default/16_Ten Disks.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/17_Two Disks.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/18_Three Disks.webp
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src/tarot/deck/default/19_Four Disks.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/tarot/deck/default/20_Five Disks.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/tarot/deck/default/21_Six Disks.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/22_Seven Disks.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
src/tarot/deck/default/23_Eight Disks.webp
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
src/tarot/deck/default/24_Nine Disks.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/25_Knight Disks.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/26_Prince Disks.webp
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
src/tarot/deck/default/27_Princess Disks.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/28_Queen Disks.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/29_Ace Swords.webp
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
src/tarot/deck/default/30_Ten Swords.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/31_Two Swords.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src/tarot/deck/default/32_Three Swords.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/tarot/deck/default/33_Four Swords.webp
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
src/tarot/deck/default/34_Five Swords.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/tarot/deck/default/35_Six Swords.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/tarot/deck/default/36_Seven Swords.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
src/tarot/deck/default/37_Eight Swords.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/tarot/deck/default/38_Nine Swords.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/tarot/deck/default/39_Knight Swords.webp
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
src/tarot/deck/default/40_Prince Swords.webp
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/tarot/deck/default/41_Princess Swords.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
src/tarot/deck/default/42_Queen Swords.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/43_Fool.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
src/tarot/deck/default/44_Magus.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/45_Fortune.webp
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
src/tarot/deck/default/46_Lust.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
src/tarot/deck/default/47_Hanged Man.webp
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
src/tarot/deck/default/48_Death.webp
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/tarot/deck/default/49_Art.webp
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/tarot/deck/default/50_Devil.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/tarot/deck/default/51_Tower.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/tarot/deck/default/52_Star.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src/tarot/deck/default/53_Moon.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/tarot/deck/default/54_Sun.webp
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
src/tarot/deck/default/55_High Priestess.webp
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/tarot/deck/default/56_Aeon.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/tarot/deck/default/57_Universe.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
src/tarot/deck/default/58_Empress.webp
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
src/tarot/deck/default/59_Emperor.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
src/tarot/deck/default/60_Hierophant.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
src/tarot/deck/default/61_Lovers.webp
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/tarot/deck/default/62_Chariot.webp
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/tarot/deck/default/63_Justice.webp
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
src/tarot/deck/default/64_Hermit.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |