Skip to content

Core API

The tradition-agnostic core: value types, the engine contract, randomness, serialisation, and errors. These symbols are re-exported from the top-level fortune_telling_core package.

Natal vs. timed readings

A reading has two distinct moments. birth_datetime (a per-tradition querent attribute) fixes the natal chart and never changes. ReadingRequest.as_of is the moment you want time-varying fortunes computed for — the luck/annual pillars (大運/流年), flying-star charts, almanac days, and similar period results are derived from it. When as_of is unset, ReadingRequest.effective_at falls back to requested_at, so existing callers are unaffected.

from datetime import UTC, datetime

# Same person, two different points in time — only `as_of` changes.
request = ReadingRequest(
    deck_id=deck_id,
    spread_id=spread_id,
    querent=querent,  # carries birth_datetime, location, etc.
    as_of=datetime(2030, 1, 1, tzinfo=UTC),
)

as_of is the unified successor to the older per-tradition target_year / target_datetime options. Those options still work and take precedence over as_of when supplied, so they remain available for callers that only need to override a single year or date.

fortune_telling_core

SCHEMA_VERSION module-attribute

SCHEMA_VERSION = 1

Draw dataclass

Draw(
    deck_id: str,
    spread_id: str,
    selections: Sequence[Selection],
    extras: Sequence[Selection] = (),
)

Recorded, replayable outcome of selecting symbols for a spread.

A Draw is authoritative for replay. Engines can interpret a recorded draw without consulting an RNG or, for computed traditions, an ephemeris.

Parameters:

Name Type Description Default
deck_id str

Identifier of the deck used for all selections.

required
spread_id str

Identifier of the spread whose positions were filled.

required
selections Sequence[Selection]

Ordered selections, one per spread position.

required
extras Sequence[Selection]

Optional structured selections that are not bound to a spread position — variable-count, computed facts an engine wants to expose for interpretation (e.g. astrology aspects). Unlike selections they carry no per-position uniqueness or spread-binding contract; they default to empty and are serialized only when present.

()

Raises:

Type Description
ValidationError

If identifiers are empty, there are no selections, or a position appears more than once.

deck_id instance-attribute

deck_id: str

spread_id instance-attribute

spread_id: str

selections instance-attribute

selections: Sequence[Selection]

extras class-attribute instance-attribute

extras: Sequence[Selection] = ()

to_dict

to_dict() -> JsonObject

Serialize the draw to the stable JSON-compatible schema.

Returns:

Type Description
JsonObject

A dictionary containing deck, spread, and selection data, plus

JsonObject

extras when any are present.

Source code in src/fortune_telling_core/draw.py
def to_dict(self) -> JsonObject:
    """Serialize the draw to the stable JSON-compatible schema.

    Returns:
        A dictionary containing deck, spread, and selection data, plus
        ``extras`` when any are present.
    """

    result: JsonObject = {
        "deck_id": self.deck_id,
        "spread_id": self.spread_id,
        "selections": [selection.to_dict() for selection in self.selections],
    }
    if self.extras:
        result["extras"] = [extra.to_dict() for extra in self.extras]
    return result

from_dict classmethod

from_dict(data: JsonMapping) -> Draw

Deserialize a draw from the stable schema.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible mapping produced by to_dict.

required

Returns:

Type Description
Draw

The decoded draw.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/draw.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Draw:
    """Deserialize a draw from the stable schema.

    Args:
        data: JSON-compatible mapping produced by `to_dict`.

    Returns:
        The decoded draw.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        deck_id=require_str(data, "deck_id"),
        spread_id=require_str(data, "spread_id"),
        selections=tuple(
            Selection.from_dict(item)
            for item in json_object_sequence(data.get("selections"), "selections")
        ),
        extras=tuple(
            Selection.from_dict(item)
            for item in json_object_sequence(data.get("extras") or [], "extras")
        ),
    )

Selection dataclass

Selection(
    position_id: str,
    symbol_id: str,
    modifiers: Mapping[str, str] | None = None,
)

A single chosen symbol for one spread position.

Selection is the smallest replay unit in the core model. Engines may attach tradition-specific state in modifiers, but the core treats every modifier as an opaque string.

Parameters:

Name Type Description Default
position_id str

Identifier of the spread position this selection fills.

required
symbol_id str

Identifier of the selected symbol in the active deck.

required
modifiers Mapping[str, str] | None

Optional per-selection string metadata, such as tarot orientation or computed astrology longitude.

None

Raises:

Type Description
ValidationError

If position_id, symbol_id, or modifier keys/values are invalid.

position_id instance-attribute

position_id: str

symbol_id instance-attribute

symbol_id: str

modifiers class-attribute instance-attribute

modifiers: Mapping[str, str] | None = None

to_dict

to_dict() -> JsonObject

Serialize the selection to the stable JSON-compatible schema.

Returns:

Type Description
JsonObject

A dictionary containing position_id, symbol_id, and

JsonObject

modifiers.

Source code in src/fortune_telling_core/draw.py
def to_dict(self) -> JsonObject:
    """Serialize the selection to the stable JSON-compatible schema.

    Returns:
        A dictionary containing `position_id`, `symbol_id`, and
        `modifiers`.
    """

    return {
        "position_id": self.position_id,
        "symbol_id": self.symbol_id,
        "modifiers": dict(self.modifiers or {}),
    }

from_dict classmethod

from_dict(data: JsonMapping) -> Selection

Deserialize a selection from the stable schema.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible mapping produced by to_dict.

required

Returns:

Type Description
Selection

The decoded selection.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/draw.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Selection:
    """Deserialize a selection from the stable schema.

    Args:
        data: JSON-compatible mapping produced by `to_dict`.

    Returns:
        The decoded selection.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        position_id=require_str(data, "position_id"),
        symbol_id=require_str(data, "symbol_id"),
        modifiers=str_mapping(data.get("modifiers"), "modifiers"),
    )

Engine

Bases: Protocol

Protocol implemented by all reading engines.

id instance-attribute

id: str

version instance-attribute

version: str

deck

deck(request: ReadingRequest) -> Deck

Return the deck for request.

Parameters:

Name Type Description Default
request ReadingRequest

Reading request.

required

Returns:

Type Description
Deck

Deck selected by the request.

Source code in src/fortune_telling_core/engine.py
def deck(self, request: ReadingRequest) -> Deck:
    """Return the deck for `request`.

    Args:
        request: Reading request.

    Returns:
        Deck selected by the request.
    """

spread

spread(request: ReadingRequest) -> Spread

Return the spread for request.

Source code in src/fortune_telling_core/engine.py
def spread(self, request: ReadingRequest) -> Spread:
    """Return the spread for `request`."""

draw

draw(request: ReadingRequest, rng: Rng) -> Draw

Create a fully determined draw.

Source code in src/fortune_telling_core/engine.py
def draw(self, request: ReadingRequest, rng: Rng) -> Draw:
    """Create a fully determined draw."""

interpret

interpret(request: ReadingRequest, draw: Draw) -> Reading

Resolve a recorded draw without using randomness.

Source code in src/fortune_telling_core/engine.py
def interpret(self, request: ReadingRequest, draw: Draw) -> Reading:
    """Resolve a recorded draw without using randomness."""

read

read(request: ReadingRequest, *, rng: Rng) -> Reading

Draw and resolve a reading.

Source code in src/fortune_telling_core/engine.py
def read(self, request: ReadingRequest, *, rng: Rng) -> Reading:
    """Draw and resolve a reading."""

replay

replay(request: ReadingRequest, draw: Draw) -> Reading

Resolve a recorded draw without using randomness.

Source code in src/fortune_telling_core/engine.py
def replay(self, request: ReadingRequest, draw: Draw) -> Reading:
    """Resolve a recorded draw without using randomness."""

ExhaustedRngError

Bases: FortuneTellingError

Raised when a replay RNG has no remaining values.

FortuneTellingError

Bases: Exception

Base error for this package.

SchemaVersionError

Bases: FortuneTellingError

Raised when serialized data uses an unsupported schema version.

UnknownSymbolError

Bases: FortuneTellingError, LookupError

Raised when a recorded draw references symbols outside the active deck.

ValidationError

Bases: FortuneTellingError, ValueError

Raised when a value object is internally inconsistent.

Provenance dataclass

Provenance(
    engine_id: str,
    engine_version: str,
    library_version: str,
    deck_id: str,
    spread_id: str,
    rng_kind: str | None = None,
    rng_seed: str | None = None,
    created_at: datetime = utc_now(),
    notes: Sequence[str] = (),
)

Audit metadata attached to a reading.

Parameters:

Name Type Description Default
engine_id str

Engine identifier.

required
engine_version str

Engine version string.

required
library_version str

fortune-telling-core version string.

required
deck_id str

Deck used by the reading.

required
spread_id str

Spread used by the reading.

required
rng_kind str | None

Optional RNG kind for random traditions.

None
rng_seed str | None

Optional RNG seed serialized as text.

None
created_at datetime

Timezone-aware creation timestamp.

utc_now()
notes Sequence[str]

Additional key-value style audit notes.

()

Raises:

Type Description
ValidationError

If created_at is not timezone-aware.

engine_id instance-attribute

engine_id: str

engine_version instance-attribute

engine_version: str

library_version instance-attribute

library_version: str

deck_id instance-attribute

deck_id: str

spread_id instance-attribute

spread_id: str

rng_kind class-attribute instance-attribute

rng_kind: str | None = None

rng_seed class-attribute instance-attribute

rng_seed: str | None = None

created_at class-attribute instance-attribute

created_at: datetime = field(default_factory=utc_now)

notes class-attribute instance-attribute

notes: Sequence[str] = ()

to_dict

to_dict() -> JsonObject

Serialize provenance to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/provenance.py
def to_dict(self) -> JsonObject:
    """Serialize provenance to a JSON-compatible dictionary."""

    result: JsonObject = {
        "engine_id": self.engine_id,
        "engine_version": self.engine_version,
        "library_version": self.library_version,
        "deck_id": self.deck_id,
        "spread_id": self.spread_id,
        "created_at": self.created_at.isoformat(),
        "notes": list(self.notes),
    }
    if self.rng_kind is not None:
        result["rng_kind"] = self.rng_kind
    if self.rng_seed is not None:
        result["rng_seed"] = self.rng_seed
    return result

from_dict classmethod

from_dict(data: JsonMapping) -> Provenance

Deserialize provenance.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible provenance mapping.

required

Returns:

Type Description
Provenance

The decoded provenance.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/provenance.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Provenance:
    """Deserialize provenance.

    Args:
        data: JSON-compatible provenance mapping.

    Returns:
        The decoded provenance.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    created_at = data.get("created_at")
    return cls(
        engine_id=require_str(data, "engine_id"),
        engine_version=require_str(data, "engine_version"),
        library_version=require_str(data, "library_version"),
        deck_id=require_str(data, "deck_id"),
        spread_id=require_str(data, "spread_id"),
        rng_kind=optional_str(data, "rng_kind"),
        rng_seed=optional_str(data, "rng_seed"),
        created_at=utc_now()
        if created_at is None
        else parse_datetime(created_at, "created_at"),
        notes=str_sequence(data.get("notes", []), "notes"),
    )

PositionReading dataclass

PositionReading(
    position: Position, symbol: Symbol, selection: Selection
)

Resolved result for one spread position.

Parameters:

Name Type Description Default
position Position

Spread position metadata.

required
symbol Symbol

Selected symbol metadata.

required
selection Selection

Recorded selection that links the position and symbol.

required

position instance-attribute

position: Position

symbol instance-attribute

symbol: Symbol

selection instance-attribute

selection: Selection

to_dict

to_dict() -> JsonObject

Serialize the position reading to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/reading.py
def to_dict(self) -> JsonObject:
    """Serialize the position reading to a JSON-compatible dictionary."""

    return {
        "position": self.position.to_dict(),
        "symbol": self.symbol.to_dict(),
        "selection": self.selection.to_dict(),
    }

from_dict classmethod

from_dict(data: JsonMapping) -> PositionReading

Deserialize a position reading.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible position reading mapping.

required

Returns:

Type Description
PositionReading

The decoded position reading.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/reading.py
@classmethod
def from_dict(cls, data: JsonMapping) -> PositionReading:
    """Deserialize a position reading.

    Args:
        data: JSON-compatible position reading mapping.

    Returns:
        The decoded position reading.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        position=Position.from_dict(json_object(data.get("position"), "position")),
        symbol=Symbol.from_dict(json_object(data.get("symbol"), "symbol")),
        selection=Selection.from_dict(json_object(data.get("selection"), "selection")),
    )

Reading dataclass

Reading(
    request: ReadingRequest,
    spread: Spread,
    draw: Draw,
    positions: Sequence[PositionReading],
    summary: str | None,
    provenance: Provenance,
    schema_version: int,
)

Complete self-contained reading.

A reading embeds its request, spread, draw, resolved position readings, and provenance so it can be serialized, audited, and replayed later.

Parameters:

Name Type Description Default
request ReadingRequest

Original request.

required
spread Spread

Spread used to resolve positions.

required
draw Draw

Recorded draw.

required
positions Sequence[PositionReading]

Resolved position readings.

required
summary str | None

Optional engine-generated summary text. When present, this text is always plain American English.

required
provenance Provenance

Audit metadata.

required
schema_version int

Serialized schema version.

required

request instance-attribute

request: ReadingRequest

spread instance-attribute

spread: Spread

draw instance-attribute

draw: Draw

positions instance-attribute

positions: Sequence[PositionReading]

summary instance-attribute

summary: str | None

provenance instance-attribute

provenance: Provenance

schema_version instance-attribute

schema_version: int

to_dict

to_dict() -> JsonObject

Serialize the reading to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/reading.py
def to_dict(self) -> JsonObject:
    """Serialize the reading to a JSON-compatible dictionary."""

    result: JsonObject = {
        "schema_version": self.schema_version,
        "request": self.request.to_dict(),
        "spread": self.spread.to_dict(),
        "draw": self.draw.to_dict(),
        "positions": [position.to_dict() for position in self.positions],
        "provenance": self.provenance.to_dict(),
    }
    if self.summary is not None:
        result["summary"] = self.summary
    return result

from_dict classmethod

from_dict(data: JsonMapping) -> Reading

Deserialize a reading.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible reading mapping.

required

Returns:

Type Description
Reading

The decoded reading.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/reading.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Reading:
    """Deserialize a reading.

    Args:
        data: JSON-compatible reading mapping.

    Returns:
        The decoded reading.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        request=ReadingRequest.from_dict(json_object(data.get("request"), "request")),
        spread=Spread.from_dict(json_object(data.get("spread"), "spread")),
        draw=Draw.from_dict(json_object(data.get("draw"), "draw")),
        positions=tuple(
            PositionReading.from_dict(item)
            for item in json_object_sequence(data.get("positions"), "positions")
        ),
        summary=optional_str(data, "summary"),
        provenance=Provenance.from_dict(json_object(data.get("provenance"), "provenance")),
        schema_version=optional_int(data, "schema_version") or 1,
    )

Querent dataclass

Querent(
    id: str,
    display_name: str,
    attributes: Mapping[str, str] | None = None,
)

Person or subject for whom a reading is made.

Parameters:

Name Type Description Default
id str

Stable application-level identifier.

required
display_name str

Human-readable name for display.

required
attributes Mapping[str, str] | None

Optional string metadata. Computed traditions use these fields for birth data when supplied.

None

Raises:

Type Description
ValidationError

If identifiers or attributes are invalid.

id instance-attribute

id: str

display_name instance-attribute

display_name: str

attributes class-attribute instance-attribute

attributes: Mapping[str, str] | None = None

to_dict

to_dict() -> JsonObject

Serialize the querent to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/request.py
def to_dict(self) -> JsonObject:
    """Serialize the querent to a JSON-compatible dictionary."""

    return {
        "id": self.id,
        "display_name": self.display_name,
        "attributes": dict(self.attributes or {}),
    }

from_dict classmethod

from_dict(data: JsonMapping) -> Querent

Deserialize a querent.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible querent mapping.

required

Returns:

Type Description
Querent

The decoded querent.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/request.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Querent:
    """Deserialize a querent.

    Args:
        data: JSON-compatible querent mapping.

    Returns:
        The decoded querent.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        id=require_str(data, "id"),
        display_name=require_str(data, "display_name"),
        attributes=str_mapping(data.get("attributes"), "attributes"),
    )

ReadingRequest dataclass

ReadingRequest(
    spread_id: str,
    deck_id: str,
    querent: Querent | None = None,
    options: Mapping[str, str] | None = None,
    requested_at: datetime = utc_now(),
    as_of: datetime | None = None,
)

Input used by an engine to produce or replay a reading.

Parameters:

Name Type Description Default
spread_id str

Identifier of the requested spread.

required
deck_id str

Identifier of the requested deck.

required
querent Querent | None

Optional querent metadata.

None
options Mapping[str, str] | None

Engine-specific string options.

None
requested_at datetime

Timezone-aware request timestamp.

utc_now()
as_of datetime | None

Optional timezone-aware moment the reading should be computed for. Traditions that report time-varying fortunes (luck/annual pillars, flying-star charts, almanac days, transits) read this as the reference point, falling back to requested_at when unset. This is the unified successor to the per-tradition target_year / target_datetime options, which still override it.

None

Raises:

Type Description
ValidationError

If identifiers, options, or timestamps are invalid.

spread_id instance-attribute

spread_id: str

deck_id instance-attribute

deck_id: str

querent class-attribute instance-attribute

querent: Querent | None = None

options class-attribute instance-attribute

options: Mapping[str, str] | None = None

requested_at class-attribute instance-attribute

requested_at: datetime = field(default_factory=utc_now)

as_of class-attribute instance-attribute

as_of: datetime | None = None

effective_at property

effective_at: datetime

Reference moment for time-varying fortunes.

Returns as_of when set, otherwise requested_at. This is the single source of truth a tradition should consult when it needs the "as of" moment but has no explicit per-tradition override.

to_dict

to_dict() -> JsonObject

Serialize the request to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/request.py
def to_dict(self) -> JsonObject:
    """Serialize the request to a JSON-compatible dictionary."""

    result: JsonObject = {
        "spread_id": self.spread_id,
        "deck_id": self.deck_id,
        "options": dict(self.options or {}),
        "requested_at": self.requested_at.isoformat(),
    }
    if self.as_of is not None:
        result["as_of"] = self.as_of.isoformat()
    if self.querent is not None:
        result["querent"] = self.querent.to_dict()
    return result

from_dict classmethod

from_dict(data: JsonMapping) -> ReadingRequest

Deserialize a reading request.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible request mapping.

required

Returns:

Type Description
ReadingRequest

The decoded request.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/request.py
@classmethod
def from_dict(cls, data: JsonMapping) -> ReadingRequest:
    """Deserialize a reading request.

    Args:
        data: JSON-compatible request mapping.

    Returns:
        The decoded request.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    querent_data = data.get("querent")
    querent = (
        None
        if querent_data is None
        else Querent.from_dict(json_object(querent_data, "querent"))
    )
    requested_at = data.get("requested_at")
    as_of = data.get("as_of")
    return cls(
        spread_id=require_str(data, "spread_id"),
        deck_id=require_str(data, "deck_id"),
        querent=querent,
        options=str_mapping(data.get("options"), "options"),
        requested_at=utc_now()
        if requested_at is None
        else parse_datetime(requested_at, "requested_at"),
        as_of=None if as_of is None else parse_datetime(as_of, "as_of"),
    )

RandomRng

RandomRng(
    seed: int | str | bytes | bytearray | None = None,
)

Seeded RNG with an explicit Fisher-Yates implementation.

Parameters:

Name Type Description Default
seed int | str | bytes | bytearray | None

Seed passed to random.Random.

None
Example
from fortune_telling_core import RandomRng

rng = RandomRng(42)
order = rng.shuffle(3)
Source code in src/fortune_telling_core/rng.py
def __init__(self, seed: int | str | bytes | bytearray | None = None) -> None:
    self.seed = seed
    self._random = random.Random(seed)

kind class-attribute instance-attribute

kind = 'random'

seed instance-attribute

seed = seed

randint

randint(low: int, high: int) -> int

Return an inclusive random integer.

Parameters:

Name Type Description Default
low int

Inclusive lower bound.

required
high int

Inclusive upper bound.

required

Returns:

Type Description
int

A random integer in the requested range.

Raises:

Type Description
ValidationError

If high is lower than low.

Source code in src/fortune_telling_core/rng.py
def randint(self, low: int, high: int) -> int:
    """Return an inclusive random integer.

    Args:
        low: Inclusive lower bound.
        high: Inclusive upper bound.

    Returns:
        A random integer in the requested range.

    Raises:
        ValidationError: If `high` is lower than `low`.
    """

    if high < low:
        raise ValidationError("high must be greater than or equal to low")
    return self._random.randrange(low, high + 1)

shuffle

shuffle(n: int) -> list[int]

Return a deterministic Fisher-Yates permutation.

Parameters:

Name Type Description Default
n int

Number of indices to permute.

required

Returns:

Type Description
list[int]

A permutation of range(n).

Raises:

Type Description
ValidationError

If n is negative.

Source code in src/fortune_telling_core/rng.py
def shuffle(self, n: int) -> list[int]:
    """Return a deterministic Fisher-Yates permutation.

    Args:
        n: Number of indices to permute.

    Returns:
        A permutation of `range(n)`.

    Raises:
        ValidationError: If `n` is negative.
    """

    if n < 0:
        raise ValidationError("shuffle length must be non-negative")
    values = list(range(n))
    for index in range(n - 1, 0, -1):
        swap_index = self._random.randrange(index + 1)
        values[index], values[swap_index] = values[swap_index], values[index]
    return values

random

random() -> float

Return the next float from the underlying seeded generator.

Source code in src/fortune_telling_core/rng.py
def random(self) -> float:
    """Return the next float from the underlying seeded generator."""

    return self._random.random()

Rng

Bases: Protocol

Narrow RNG protocol used by engines.

Engines depend on this small protocol rather than random.Random directly so tests can inject deterministic replay streams.

randint

randint(low: int, high: int) -> int

Return an integer in the inclusive range [low, high].

Parameters:

Name Type Description Default
low int

Inclusive lower bound.

required
high int

Inclusive upper bound.

required

Returns:

Type Description
int

An integer within the requested range.

Source code in src/fortune_telling_core/rng.py
def randint(self, low: int, high: int) -> int:
    """Return an integer in the inclusive range `[low, high]`.

    Args:
        low: Inclusive lower bound.
        high: Inclusive upper bound.

    Returns:
        An integer within the requested range.
    """

shuffle

shuffle(n: int) -> list[int]

Return a permutation of range(n).

Parameters:

Name Type Description Default
n int

Number of indices to permute.

required

Returns:

Type Description
list[int]

A list containing each integer from 0 to n - 1 exactly once.

Source code in src/fortune_telling_core/rng.py
def shuffle(self, n: int) -> list[int]:
    """Return a permutation of `range(n)`.

    Args:
        n: Number of indices to permute.

    Returns:
        A list containing each integer from `0` to `n - 1` exactly once.
    """

random

random() -> float

Return a float in the half-open interval [0.0, 1.0).

Source code in src/fortune_telling_core/rng.py
def random(self) -> float:
    """Return a float in the half-open interval `[0.0, 1.0)`."""

SequenceRng

SequenceRng(
    ints: Iterable[int] = (), floats: Iterable[float] = ()
)

Replay/test RNG backed by fixed integer and float sequences.

Parameters:

Name Type Description Default
ints Iterable[int]

Integer values consumed by randint and shuffle.

()
floats Iterable[float]

Float values consumed by random.

()
Example
from fortune_telling_core import SequenceRng

rng = SequenceRng(ints=[2, 0, 1], floats=[0.25])
assert rng.shuffle(3) == [2, 0, 1]
Source code in src/fortune_telling_core/rng.py
def __init__(
    self,
    ints: Iterable[int] = (),
    floats: Iterable[float] = (),
) -> None:
    self._ints = list(ints)
    self._floats = list(floats)
    self._int_index = 0
    self._float_index = 0

kind class-attribute instance-attribute

kind = 'sequence'

randint

randint(low: int, high: int) -> int

Return the next integer from the replay sequence.

Parameters:

Name Type Description Default
low int

Inclusive lower bound.

required
high int

Inclusive upper bound.

required

Returns:

Type Description
int

The next integer.

Raises:

Type Description
ExhaustedRngError

If no integer values remain.

ValidationError

If bounds are invalid or the next value is outside the requested range.

Source code in src/fortune_telling_core/rng.py
def randint(self, low: int, high: int) -> int:
    """Return the next integer from the replay sequence.

    Args:
        low: Inclusive lower bound.
        high: Inclusive upper bound.

    Returns:
        The next integer.

    Raises:
        ExhaustedRngError: If no integer values remain.
        ValidationError: If bounds are invalid or the next value is outside
            the requested range.
    """

    if high < low:
        raise ValidationError("high must be greater than or equal to low")
    value = self._next_int()
    if value < low or value > high:
        raise ValidationError(f"integer RNG value {value} is outside [{low}, {high}]")
    return value

shuffle

shuffle(n: int) -> list[int]

Consume n integers as an explicit permutation.

Parameters:

Name Type Description Default
n int

Number of indices to consume.

required

Returns:

Type Description
list[int]

The next n integers.

Raises:

Type Description
ExhaustedRngError

If fewer than n integers remain.

ValidationError

If n is negative or values are not a permutation of range(n).

Source code in src/fortune_telling_core/rng.py
def shuffle(self, n: int) -> list[int]:
    """Consume `n` integers as an explicit permutation.

    Args:
        n: Number of indices to consume.

    Returns:
        The next `n` integers.

    Raises:
        ExhaustedRngError: If fewer than `n` integers remain.
        ValidationError: If `n` is negative or values are not a permutation
            of `range(n)`.
    """

    if n < 0:
        raise ValidationError("shuffle length must be non-negative")
    values = [self._next_int() for _ in range(n)]
    if sorted(values) != list(range(n)):
        raise ValidationError("shuffle sequence must be a permutation of range(n)")
    return values

random

random() -> float

Return the next float from the replay sequence.

Returns:

Type Description
float

The next float.

Raises:

Type Description
ExhaustedRngError

If no float values remain.

ValidationError

If the next value is outside [0.0, 1.0).

Source code in src/fortune_telling_core/rng.py
def random(self) -> float:
    """Return the next float from the replay sequence.

    Returns:
        The next float.

    Raises:
        ExhaustedRngError: If no float values remain.
        ValidationError: If the next value is outside `[0.0, 1.0)`.
    """

    if self._float_index >= len(self._floats):
        raise ExhaustedRngError("float RNG sequence is exhausted")
    value = self._floats[self._float_index]
    self._float_index += 1
    if value < 0.0 or value >= 1.0:
        raise ValidationError("float RNG value must be in [0.0, 1.0)")
    return value

Position dataclass

Position(id: str, name: str, description: str = '')

One named slot in a spread.

Parameters:

Name Type Description Default
id str

Stable position identifier.

required
name str

Human-readable position name.

required
description str

Optional explanation of the position's role.

''

Raises:

Type Description
ValidationError

If id or name is empty.

id instance-attribute

id: str

name instance-attribute

name: str

description class-attribute instance-attribute

description: str = ''

to_dict

to_dict() -> JsonObject

Serialize the position to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/spread.py
def to_dict(self) -> JsonObject:
    """Serialize the position to a JSON-compatible dictionary."""

    return {"id": self.id, "name": self.name, "description": self.description}

from_dict classmethod

from_dict(data: JsonMapping) -> Position

Deserialize a position.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible position mapping.

required

Returns:

Type Description
Position

The decoded position.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/spread.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Position:
    """Deserialize a position.

    Args:
        data: JSON-compatible position mapping.

    Returns:
        The decoded position.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        id=require_str(data, "id"),
        name=require_str(data, "name"),
        description=str(data.get("description", "")),
    )

Spread dataclass

Spread(id: str, name: str, positions: Sequence[Position])

Ordered set of positions filled by a draw.

Parameters:

Name Type Description Default
id str

Stable spread identifier.

required
name str

Human-readable spread name.

required
positions Sequence[Position]

Ordered positions in the spread.

required

Raises:

Type Description
ValidationError

If the spread is empty or contains duplicate position ids.

id instance-attribute

id: str

name instance-attribute

name: str

positions instance-attribute

positions: Sequence[Position]

size property

size: int

Number of positions in the spread.

to_dict

to_dict() -> JsonObject

Serialize the spread to a JSON-compatible dictionary.

Source code in src/fortune_telling_core/spread.py
def to_dict(self) -> JsonObject:
    """Serialize the spread to a JSON-compatible dictionary."""

    return {
        "id": self.id,
        "name": self.name,
        "positions": [position.to_dict() for position in self.positions],
    }

from_dict classmethod

from_dict(data: JsonMapping) -> Spread

Deserialize a spread.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible spread mapping.

required

Returns:

Type Description
Spread

The decoded spread.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/spread.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Spread:
    """Deserialize a spread.

    Args:
        data: JSON-compatible spread mapping.

    Returns:
        The decoded spread.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        id=require_str(data, "id"),
        name=require_str(data, "name"),
        positions=tuple(
            Position.from_dict(item)
            for item in json_object_sequence(data.get("positions"), "positions")
        ),
    )

Deck dataclass

Deck(
    id: str,
    symbols: Sequence[Symbol],
    weights: Sequence[int] | None = None,
)

Ordered pool of symbols used by an engine.

Parameters:

Name Type Description Default
id str

Stable deck identifier.

required
symbols Sequence[Symbol]

Ordered symbols available to the engine.

required
weights Sequence[int] | None

Optional positive integer weights matching symbols.

None

Raises:

Type Description
ValidationError

If the deck is empty, has duplicate symbol ids, or has invalid weights.

id instance-attribute

id: str

symbols instance-attribute

symbols: Sequence[Symbol]

weights class-attribute instance-attribute

weights: Sequence[int] | None = None

symbol_by_id

symbol_by_id(symbol_id: str) -> Symbol | None

Return a symbol by id.

Parameters:

Name Type Description Default
symbol_id str

Identifier to look up.

required

Returns:

Type Description
Symbol | None

The matching symbol, or None when absent.

Source code in src/fortune_telling_core/symbols.py
def symbol_by_id(self, symbol_id: str) -> Symbol | None:
    """Return a symbol by id.

    Args:
        symbol_id: Identifier to look up.

    Returns:
        The matching symbol, or `None` when absent.
    """

    for symbol in self.symbols:
        if symbol.id == symbol_id:
            return symbol
    return None

to_dict

to_dict() -> JsonObject

Serialize the deck to a JSON-compatible dictionary.

Returns:

Type Description
JsonObject

The stable deck representation.

Source code in src/fortune_telling_core/symbols.py
def to_dict(self) -> JsonObject:
    """Serialize the deck to a JSON-compatible dictionary.

    Returns:
        The stable deck representation.
    """

    result: JsonObject = {
        "id": self.id,
        "symbols": [symbol.to_dict() for symbol in self.symbols],
    }
    if self.weights is not None:
        result["weights"] = list(self.weights)
    return result

from_dict classmethod

from_dict(data: JsonMapping) -> Deck

Deserialize a deck.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible deck mapping.

required

Returns:

Type Description
Deck

The decoded deck.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/symbols.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Deck:
    """Deserialize a deck.

    Args:
        data: JSON-compatible deck mapping.

    Returns:
        The decoded deck.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    weights = data.get("weights")
    parsed_weights: tuple[int, ...] | None = None
    if weights is not None:
        if not isinstance(weights, Sequence) or isinstance(weights, str):
            raise ValidationError("weights must be an array")
        parsed_weight_values: list[int] = []
        for weight in weights:
            if isinstance(weight, bool) or not isinstance(weight, int):
                raise ValidationError("weights must contain only integer values")
            parsed_weight_values.append(weight)
        parsed_weights = tuple(parsed_weight_values)
    return cls(
        id=require_str(data, "id"),
        symbols=tuple(
            Symbol.from_dict(item)
            for item in json_object_sequence(data.get("symbols"), "symbols")
        ),
        weights=parsed_weights,
    )

Symbol dataclass

Symbol(
    id: str,
    name: str,
    attributes: Mapping[str, str] = dict(),
)

Tradition-neutral symbol that can appear in a deck.

Parameters:

Name Type Description Default
id str

Stable symbol identifier, unique inside its deck.

required
name str

Human-readable symbol name.

required
attributes Mapping[str, str]

Optional string metadata. Traditions use this for details such as tarot suit, zodiac element, or stem polarity.

dict()

Raises:

Type Description
ValidationError

If id, name, or attributes are invalid.

id instance-attribute

id: str

name instance-attribute

name: str

attributes class-attribute instance-attribute

attributes: Mapping[str, str] = field(default_factory=dict)

to_dict

to_dict() -> JsonObject

Serialize the symbol to a JSON-compatible dictionary.

Returns:

Type Description
JsonObject

The stable symbol representation.

Source code in src/fortune_telling_core/symbols.py
def to_dict(self) -> JsonObject:
    """Serialize the symbol to a JSON-compatible dictionary.

    Returns:
        The stable symbol representation.
    """

    return {"id": self.id, "name": self.name, "attributes": dict(self.attributes)}

from_dict classmethod

from_dict(data: JsonMapping) -> Symbol

Deserialize a symbol.

Parameters:

Name Type Description Default
data JsonMapping

JSON-compatible symbol mapping.

required

Returns:

Type Description
Symbol

The decoded symbol.

Raises:

Type Description
ValidationError

If required fields are missing or malformed.

Source code in src/fortune_telling_core/symbols.py
@classmethod
def from_dict(cls, data: JsonMapping) -> Symbol:
    """Deserialize a symbol.

    Args:
        data: JSON-compatible symbol mapping.

    Returns:
        The decoded symbol.

    Raises:
        ValidationError: If required fields are missing or malformed.
    """

    return cls(
        id=require_str(data, "id"),
        name=require_str(data, "name"),
        attributes=str_mapping(data.get("attributes"), "attributes"),
    )

reading_from_json

reading_from_json(value: str) -> Reading

Deserialize a reading from JSON.

Parameters:

Name Type Description Default
value str

JSON string produced by reading_to_json or the same schema.

required

Returns:

Type Description
Reading

The decoded reading.

Raises:

Type Description
SchemaVersionError

If the payload declares a future schema version.

ValidationError

If schema_version or required fields are malformed.

JSONDecodeError

If value is not valid JSON.

Source code in src/fortune_telling_core/serde.py
def reading_from_json(value: str) -> Reading:
    """Deserialize a reading from JSON.

    Args:
        value: JSON string produced by `reading_to_json` or the same schema.

    Returns:
        The decoded reading.

    Raises:
        SchemaVersionError: If the payload declares a future schema version.
        ValidationError: If `schema_version` or required fields are malformed.
        json.JSONDecodeError: If `value` is not valid JSON.
    """

    parsed = json.loads(value)
    data = json_object(parsed, "reading")
    version = data.get("schema_version", SCHEMA_VERSION)
    if isinstance(version, bool) or not isinstance(version, int):
        raise ValidationError("schema_version must be an integer")
    if version > SCHEMA_VERSION:
        raise SchemaVersionError(f"unsupported schema version: {version}")
    return Reading.from_dict(data)

reading_to_json

reading_to_json(reading: Reading) -> str

Serialize a reading to compact, deterministic JSON.

Parameters:

Name Type Description Default
reading Reading

Reading to encode.

required

Returns:

Type Description
str

A JSON string with sorted keys and no insignificant whitespace.

Source code in src/fortune_telling_core/serde.py
def reading_to_json(reading: Reading) -> str:
    """Serialize a reading to compact, deterministic JSON.

    Args:
        reading: Reading to encode.

    Returns:
        A JSON string with sorted keys and no insignificant whitespace.
    """

    return json.dumps(reading.to_dict(), sort_keys=True, separators=(",", ":"))