Skip to main content

allowances

Grants and consumable budgets — enforced before the fact, auditable after it. Storage-agnostic, with a Django backend.

📖 Documentation: docs.velis.si/allowances. The source lives in docs/npm install && npm run docs:dev serves it locally.


What it does

Applications keep needing to answer the same shape of question:

Can this user send an email right now? How many API calls does this organisation have left this month? Does this device still have a service contract, and does the customer have enough coolant credit to fill it?

allowances answers it, records what the answer cost, and lets you take it back if the operation it paid for never happened.

It is not a payment system — no invoices, no card charges, no money anywhere. The units it counts are whatever you say they are. Use it for rights that are bought, expire, or get spent.

Install

Django is optional. The core is import allowances and depends on nothing at all — not Django, not a database driver, not anything outside the standard library. Everything that matters (the ordering walk, conversion, rounding, overdraft, sliding windows) lives there, and talks to storage through a sixteen-method port.

pip install allowances            # the engine, zero dependencies
pip install "allowances[django]"  # ...and the Django models, store and admin

The Django binding is one extra line, and the rest of this README assumes you took it:

INSTALLED_APPS = ["allowances_django"]

Without it you supply your own Store — a dict-backed one ships in the box for tests, and writing another is an afternoon.

Use

# settings.py
ALLOWANCES = {
    "currencies": {
        "email_send": {},
        "credits": {"general": True, "round_fn": "ceil"},
    },
    "features": {
        "send_email": {
            "consumable": {"currencies": ["email_send", ("credits", 0.5)]},
            "subjects": {"user", "team"},
            "fulfillment_order": [("consumable", "user"), ("consumable", "team")],
        },
    },
}
from allowances import InsufficientBalanceError
from allowances_django.shortcuts import fulfill, attach, unfulfill

try:
    result = fulfill("send_email", 1, {"user": request.user, "team": team})
except InsufficientBalanceError as exc:
    return http_402(f"Only {exc.result.granted} left")

message = send_the_email()
attach(result.guid, message)   # record what the quota bought
# ...or unfulfill(result.guid) if it failed

The shape of it

Two layers, kept apart. Definition — what rights exist at all: features, currencies, conversion rates, priority order. Configuration, not rows. State — what a subject holds and has spent: wallets and a transaction ledger.

The engine sits between them and knows nothing about either's storage. It receives definitions through a provider and state through a sixteen-method store port, which is why the same code runs against Postgres, SQLite or a dict.

   allowances.engine        peek · fulfill · attach · unfulfill · plan_switch
           │
   ┌───────┴────────┐
   ▼                ▼
Store port   DefinitionProvider
   │                │
   ▼                ▼
DjangoStore    SettingsProvider
MemoryStore    DictProvider

Highlights:

  • Grants and consumables in one flow. A boolean entitlement and a metered budget resolve together, in an order you declare.
  • Multi-source payment. One operation can draw from a dedicated budget, then general credits, then a promotional grant, at conversion rates you set.
  • Nothing edited in place. Consumption writes a ledger entry; reversal writes a matching storno; spent-out wallets move to an archive.
  • Sliding windows without a reset job. "200 a day" works by ageing consumption out of a window, not by a nightly job that can fail.
  • Multi-subject. A licence can belong to a user, an organisation, a device, a location — resolved across all of them in one call.

What it could also be

A permissions layer, if you push it. Not the point of the library, but the pieces line up: a grant with no expiry is a permission bit, the fulfillment order is a resolution order across user, team and organisation, and the ledger is the audit trail permission systems usually bolt on afterwards. You get things they rarely offer natively — rights that expire on a date, that may be exercised n times, or that are inherited from an organisation and revoked with it.

Only the plumbing is missing, and NEXT_STEPS.md already scopes it: a grant-only permissions backend, after which the decorators, mixins and checks you have already written keep working unchanged. See the guide.

Development

pip install -e ".[test]"
pytest                                 # 188 tests, both stores, no setup
ruff check . && ruff format --check .

npm install
npm run docs:dev

No database server required: the suite defaults to a file-backed SQLite. Copy tests/env.example.py to tests/env.py to point it at a local Postgres instead, and ALLOWANCES_TEST_DB=sqlite pytest to go back. Both engines are supported and both are covered — including tests/django/test_concurrency.py, which races eight threads for the same wallet and insists it is granted exactly once.

Locally that runs against whichever Django you have installed. CI runs the suite five times — Django 5.2 on Python 3.11, 6.0 on 3.12 and 6.1 on 3.13 against Postgres, plus each end of that range against SQLite — which is what the Django>=5.2,<7 pin is based on.

DECISIONS.md records every judgment call and what it would cost to revisit — read it before changing behaviour.

Licence

MIT.

Download files

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

Source Distribution

allowances-0.2.1.tar.gz (43.6 kB view details)

Uploaded Source

Built Distribution

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

allowances-0.2.1-py3-none-any.whl (40.6 kB view details)

Uploaded Python 3

File details

Details for the file allowances-0.2.1.tar.gz.

File metadata

  • Download URL: allowances-0.2.1.tar.gz
  • Upload date:
  • Size: 43.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.2

File hashes

Hashes for allowances-0.2.1.tar.gz
Algorithm Hash digest
SHA256 bb2dc08a88530788340c46d3b48cca087db3d1ce996810f63a0f9f77dc2310c2
MD5 078cbbf7077abe0c6793c10151aa3bf3
BLAKE2b-256 ff2456f89db6c025b022aa6a31f9bd16bb2fbdbc0fb81c505377292fc1406ddd

See more details on using hashes here.

File details

Details for the file allowances-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: allowances-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 40.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.2

File hashes

Hashes for allowances-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c71a3a44fea931df98f4bb7b2c2a88ccc9ec78c4348e13d1b1b765567ca92b35
MD5 8cf0d313157f18cfec664170964f8ec4
BLAKE2b-256 09ba4d6ecd96b51e9d9d49b085b5f13ac4f021b1cb0dc5414102f0631752f56e

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 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