Metadata-Version: 2.4
Name: optimalexile-crafting-sim
Version: 0.2.3
Requires-Dist: rich>=14
Summary: Python bindings for the PoE2 crafting simulator
Author: optimalexile
Requires-Python: >=3.13
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# crafting-sim

Python bindings for the PoE2 crafting simulator (`crates/crafting-sim`),
built for writing fast crafting simulations.

The extension consumes the same `crafting-sim-api` catalog, import, replay,
and query contract as the WebAssembly binding. Its only additional domain
dependency is the intentionally Python-specific offline pricing library.

Game data is embedded in the compiled extension at build time — the package
needs no data files, environment variables, or network access at runtime.
All randomness derives from an explicit seed, so every simulation is
reproducible.

## Usage

```python
from crafting_sim import Item, Omen, Tier, ops
from crafting_sim.data import Base, Family

item = Item(Base.SAPPHIRE_RING, item_level=82, seed=42)
item.alchemy()

# Spam Chaos Orbs until the ring has T2-or-better Fire Resistance.
while not item.has_mod(Family.FIRE_RESISTANCE, min_tier=2):
    item.chaos(tier=Tier.PERFECT)

item.set_active_omens([Omen.DEXTRAL_EXALTATION])
item.exalt()  # guaranteed suffix
item.clear_omens()
item.divine()
item.vaal()  # modifies the item unpredictably and corrupts it

for mod in item.mods:
    print(mod.id, mod.tier, mod.text)

# Exact outcome distributions, without applying:
p = item.outcomes(ops.annulment()).probability(
    lambda it: it.has_mod(Family.FIRE_RESISTANCE)
)

branch = item.copy()  # exact clone, including the current random stream

# Successful operations are tracked by canonical game-data currency item.
for currency, count in item.currency_used.most_common():
    print(count, currency.name, currency.id)

# Offline prices are embedded alongside the simulator.
from crafting_sim.pricing import value_currency_usage

cost = value_currency_usage(item.currency_used)
print(cost.total_divines)  # None when cost.missing is non-empty

# Applications can request current rates explicitly.
from crafting_sim.pricing import fetch_poe2_currency_rates

live_rates = fetch_poe2_currency_rates("Standard")
live_cost = value_currency_usage(item.currency_used, live_rates)
```

Existing PoE2 items can be imported from their raw trade listing JSON. The
result is the same canonical `Item` used by crafting simulations; listing
metadata such as price and indexed time intentionally remains the caller's
responsibility.

```python
item = Item.from_trade_listing(listing_json, seed=42)
# Auto-detect either trade JSON or copied in-game text:
item = Item.from_paste(clipboard_text, seed=42)
```

Everything is strongly typed. Small structural values such as `Tier`,
`Rarity`, and `Slot` are integer-backed Python enums. Bases, mod families,
essences, omens, and catalysts are generated `StrEnum`s under
`crafting_sim.data` because their values are canonical game-data IDs; members
also expose their in-game descriptions. `Item` mutates in place and verbs
chain. Equal items have the same canonical item state, and mutable items are
intentionally unhashable. `Item.currency_used` is derived from session history,
so failed operations are not counted and copied items retain their usage.

The lazy `crafting_sim.catalog` value exposes the same bases, operations,
Omens, and reachable modifiers as the browser binding. Generated identifier
enums carry member-specific runtime documentation for `help()` and IDE use.

## Development

```
just dev        # rebuild the extension into the uv venv after Rust changes
just test       # pytest
just lint       # ruff + mypy
just gen-enums  # regenerate crafting_sim/data/_generated.py after a data update
```

The extension crate lives at `crates/crafting-sim-py`. Queries such as
`has_mod` execute in Rust; a chaos-spam loop runs at roughly 10^5
applies/second single-threaded.

## Releases

Linux wheels (manylinux x86_64 and aarch64, Python >= 3.13 via abi3) are
published to PyPI (`pip install optimalexile-crafting-sim`) by
`.github/workflows/python-release.yml`; no sdist is published. To cut
a release: bump `version` here and `[workspace.package] version` in the root
`Cargo.toml` to the same value, then push a `vX.Y.Z` tag — CI fails if the
three disagree.

