Skip to main content

fancy-catalog

A headless Stripe catalog for Python — products, prices, plans and checkout, with money as integer minor units and persistence behind adapters. No web framework, no ORM, and the Stripe SDK is injected rather than depended on.

The Python twin of particle-academy/laravel-catalog (PHP) and @particle-academy/fancy-catalog (Node/TypeScript).

pip install fancy-catalog
pip install "fancy-catalog[features]"   # + the entitlement bridge

In one minute

import stripe
from fancy_catalog import create_catalog

# NOTE the .v1 -- see "Passing a Stripe client" below.
catalog = create_catalog(stripe=stripe.StripeClient(api_key).v1)

pro = catalog.create_product("Pro plan", description="Everything, monthly")
price = catalog.create_price(pro.id, currency="USD", amount="19.99", recurring_interval="month")

price.unit_amount  # 1999   -- exactly, not 1998
catalog.sync_product_and_prices(pro)
catalog.subscription_checkout_url(price, customer="cus_123", success_url="...", cancel_url="...")

Money is the point

Stripe stores an amount as an integer in the currency's smallest unit. A human types a decimal string. The conversion between them is where money is lost, and neither twin owns it — so every consumer writes it themselves:

int(19.99 * 100)  # 1998.  One cent, on every order.
round(8.615 * 1000)  # 8614.  Rounding does not fix it, it moves it.
from fancy_catalog import to_minor_units, format_minor_units, currency_exponent

to_minor_units("19.99", 2)  # 1999
to_minor_units("8.615", 3)  # 8615
to_minor_units("1000", currency_exponent("JPY"))  # 1000, not 100000
to_minor_units("1.005", currency_exponent("KWD"))  # 1005
format_minor_units(-7, 2)  # "-0.07"

to_minor_units(19.99, 2)  # TypeError -- a float has already lost it
to_minor_units("0.005", 2)  # MoneyPrecisionError -- rounds nothing silently

Pinned by the shared shared/money-minor-units conformance table, so a second implementation in any language inherits the behaviour rather than the assumption.

Prices are immutable, and this package knows it

A change to the amount, currency, interval, billing scheme, tiers, transform_quantity or custom_unit_amount archives the Stripe price and creates a replacement; a shared ULID in metadata.price_id keeps the two linked. A change to only the metadata, active flag or lookup key updates in place — and the lookup key is transferred, which is what stops the next reprice failing with "lookup key already exists".

price.unit_amount = 2999
catalog.sync_price(price)  # old price archived, new one created
price.external_id  # a NEW Stripe id

Passing a Stripe client

stripe is not a dependency. The client is injected and used through six operations declared in fancy_catalog.stripe_client.

catalog = create_catalog(stripe=stripe.StripeClient(api_key).v1)

Use .v1. On stripe-python 15.x the bare StripeClient.products still works but emits a DeprecationWarning on every access, and a host running with warnings-as-errors would see a catalog sync crash.

Any object with the same shape works — your own wrapper, a proxy, or a recorded cassette. That is how this package's own suite runs entirely offline.

Persistence

ProductStore, PriceStore and ProductFeatureStore are protocols with in-memory defaults. Soft deletes mirror the PHP SoftDeletes trait, because an invoice referencing a hard-deleted product is an invoice nobody can explain.

catalog = create_catalog(stripe=client, products=MyProductStore(), ...)

Gating on what a plan includes

from fancy_catalog.features import create_catalog_feature_source
from fancy_features import create_features

features = create_features(
    sources=[create_catalog_feature_source(catalog, resolve_subscription=lookup)],
)

features.can_access("use-mcp", user)  # via the user's plan's product features
features.remaining("ai-tokens", user)  # includedQuantity − usage

The bridge imports the shared contract from fancy-features rather than mirroring it, so there is exactly one definition of FeatureGrant in the pair. fancy_catalog itself never imports the bridge — a host with no gating layer installs nothing extra.

Requirements

Python 3.11+. No required runtime dependencies.

License

MIT © Particle Academy

Metadata

Release files for fancy-catalog 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fancy-catalog 0.2.0
File Size Uploaded
fancy_catalog-0.2.0.tar.gz 43.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fancy-catalog 0.2.0
File Interpreter ABI Platform
fancy_catalog-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 72.2 kB

Release files / fancy_catalog-0.2.0.tar.gz

Download URL fancy_catalog-0.2.0.tar.gz
Size 43.1 kB
Tags Source
SHA-256 checksum
How to use checksums
a63d30b2ffede935cc7f91b04f99c95fc24f143c1bd2ea4be1318b7df65f09fc
BLAKE2b-256 checksum
How to use checksums
da42685b5eb023f9785141c1164805c33f93292b3b2199b878da1e2a64edc982
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release files / fancy_catalog-0.2.0-py3-none-any.whl

Download URL fancy_catalog-0.2.0-py3-none-any.whl
Size 29.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4f59c19516286948510a3bddeeacbc036884eec52c8fb299cd5346476eea154d
BLAKE2b-256 checksum
How to use checksums
6540b228464c32d083ac350f80292527193eb4e864f158086c6ce144f42b5fae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page