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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fancy_catalog-0.2.0.tar.gz (43.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fancy_catalog-0.2.0-py3-none-any.whl (29.1 kB view details)

Uploaded Python 3

File details

Details for the file fancy_catalog-0.2.0.tar.gz.

File metadata

  • Download URL: fancy_catalog-0.2.0.tar.gz
  • Upload date:
  • Size: 43.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for fancy_catalog-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a63d30b2ffede935cc7f91b04f99c95fc24f143c1bd2ea4be1318b7df65f09fc
MD5 e52f576461ecb29ce6e9eb72538e5365
BLAKE2b-256 da42685b5eb023f9785141c1164805c33f93292b3b2199b878da1e2a64edc982

See more details on using hashes here.

Provenance

The following attestation bundles were made for fancy_catalog-0.2.0.tar.gz:

Publisher: publish.yml on Particle-Academy/fancy-catalog-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file fancy_catalog-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: fancy_catalog-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 29.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for fancy_catalog-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f59c19516286948510a3bddeeacbc036884eec52c8fb299cd5346476eea154d
MD5 9a17c9d9c3bdd7ad7ee473a8bbba5a3e
BLAKE2b-256 6540b228464c32d083ac350f80292527193eb4e864f158086c6ce144f42b5fae

See more details on using hashes here.

Provenance

The following attestation bundles were made for fancy_catalog-0.2.0-py3-none-any.whl:

Publisher: publish.yml on Particle-Academy/fancy-catalog-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page