Appearance
Getting started
Nothing here needs a web framework. The engine is plain Python that does arithmetic against a store you hand it, so the walkthrough below runs in a script, a Celery worker, a FastAPI handler or a Django view alike.
Two things have to be decided before the first call, and they are the same two everywhere:
- A definition provider — what features and currencies exist. Configuration, not rows.
- A store — where wallets and transactions live.
Every example on this page comes in two flavours. Core is the library on its own, with the in-memory store; swap that store and the code is unchanged. Django is the same thing with both choices already made for you — definitions from settings.ALLOWANCES, state in your database.
Install
bash
pip install allowancesbash
pip install "allowances[django]"The core has no dependencies at all. The django extra adds Django and the ORM-backed store.
python
# Nothing to set up. The in-memory store ships with the core:
from allowances.backends import MemoryStore
store = MemoryStore()python
# settings.py
INSTALLED_APPS = [
# ...
"allowances_django",
]
# then: python manage.py migrate
# Six tables appear, prefixed `allowances_`.MemoryStore is for tests and for trying things out — it dies with the process. For real storage, either install the Django backend or write your own: it is sixteen methods.
Declare what exists
Currencies are the units you count; features are the capabilities that consume them. The dict is identical in both cases — only where it is read from differs.
python
from allowances import DictProvider
provider = DictProvider({
"currencies": {
"email_send": {},
"credits": {"general": True, "round_fn": "ceil"},
},
"features": {
"send_email": {
# A dedicated email budget, or half a credit if that runs out.
"consumable": {"currencies": ["email_send", ("credits", 0.5)]},
"subjects": {"user", "team"},
"fulfillment_order": [
("consumable", "user"), # spend the user's own budget first
("consumable", "team"), # then fall back to the team's
],
},
},
})python
# settings.py
ALLOWANCES = {
"currencies": {
"email_send": {},
"credits": {"general": True, "round_fn": "ceil"},
},
"features": {
"send_email": {
# A dedicated email budget, or half a credit if that runs out.
"consumable": {"currencies": ["email_send", ("credits", 0.5)]},
"subjects": {"user", "team"},
"fulfillment_order": [
("consumable", "user"), # spend the user's own budget first
("consumable", "team"), # then fall back to the team's
],
},
},
}Read as: sending one email costs one email_send, or half a credit. Try the user's wallets before the team's. round_fn: "ceil" on credits means a fractional cost is charged as a whole credit — you never charge 0.5 of something you can't divide.
A provider is an interface, not a dict reader: definitions can just as well come from a database table or a service call. See DefinitionProvider.
Name the subjects
A subject is whatever holds the licence — a user, a team, a device, a location. The core never sees your objects, only a (type, id) pair.
python
from allowances import SubjectRef
user = SubjectRef("user", 42)
team = SubjectRef("team", 7)
subjects = {"user": user, "team": team}python
# Pass the model instances you already have; the adapter makes the refs.
subjects = {"user": request.user, "team": team}The keys ("user", "team") are the names your fulfillment_order refers to. The type string is yours to choose; the Django adapter uses "app_label.modelname".
Give somebody something to spend
Wallets hold balances. plan_switch is the usual way to open them — it closes whatever the subject had and opens a new set atomically, which is exactly what putting someone on a plan means:
python
from decimal import Decimal
from allowances import WalletSpec, WalletType, plan_switch
plan_switch(
store,
{"user": user},
[
WalletSpec(WalletType.CONSUMABLE, "email_send", Decimal("200")),
WalletSpec(WalletType.CONSUMABLE, "credits", Decimal("50")),
],
)python
from decimal import Decimal
from allowances import WalletSpec, WalletType
from allowances_django.shortcuts import plan_switch
plan_switch(
{"user": request.user},
[
WalletSpec(WalletType.CONSUMABLE, "email_send", Decimal("200")),
WalletSpec(WalletType.CONSUMABLE, "credits", Decimal("50")),
],
)The only difference is the first argument: the core functions take the store explicitly, the Django shortcuts supply it — and the provider — for you.
Spend it
python
from allowances import InsufficientBalanceError, fulfill
try:
result = fulfill(store, "send_email", 1, subjects, provider)
except InsufficientBalanceError as exc:
raise RuntimeError(f"Only {exc.result.granted} left")python
from allowances import InsufficientBalanceError
from allowances_django.shortcuts import fulfill
try:
result = fulfill("send_email", 1, subjects)
except InsufficientBalanceError as exc:
return http_402(f"Only {exc.result.granted} left")fulfill either covers the whole request or raises and rolls back — there is no half-charged state to clean up. It returns a FulfillmentResult:
python
result.requested # Decimal("1")
result.granted # Decimal("1")
result.satisfied # True
result.guid # UUID — the handle for attaching and reversing
result.transactions # the ledger entries just writtenClose the loop
Fulfillment happens before the work: you take the quota, then send the email. Tell the ledger which object it paid for once that succeeds:
python
from allowances import ObjectRef, attach
message = send_the_email()
attach(store, result.guid, ObjectRef("message", message.id))python
from allowances_django.shortcuts import attach
message = send_the_email()
attach(result.guid, message)This matters more than it looks. A transaction with nothing attached is quota that was spent on something that may never have happened — and that is a query you can run.
Undo it
If the operation fails, give it back:
python
from allowances import unfulfill
unfulfill(store, result.guid)python
from allowances_django.shortcuts import unfulfill
unfulfill(result.guid)Nothing is deleted. Each original entry gets a matching storno entry, so the history still shows what was taken and what gave it back. Calling it twice is safe.
Ask without spending
python
from allowances import peek
if peek(store, "send_email", 10, {"user": user}, provider).satisfied:
show_the_bulk_send_button()python
from allowances_django.shortcuts import peek
if peek("send_email", 10, {"user": request.user}).satisfied:
show_the_bulk_send_button()peek runs the same planning walk as fulfill and writes nothing. It is a snapshot, not a reservation — a concurrent caller can spend what it just told you was there.
Where to go next
- Concepts — the vocabulary in full.
- Fulfillment order — the part worth understanding properly.
- Sliding windows — "200 a day" without a reset job.
- How the port works — if the store you want isn't one of the two shipped.