django-pledge 0.0.1a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ # Rust
2
+ debug
3
+ target
4
+ **/*.rs.bk
5
+ *.pdb
6
+ **/mutants.out*/
7
+ rustc-ice-*.txt
8
+
9
+ # Python
10
+ __pycache__/
11
+ *.py[codz]
12
+ *$py.class
13
+ *.so
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+ *.manifest
33
+ *.spec
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+ htmlcov/
37
+ .tox/
38
+ .nox/
39
+ .coverage
40
+ .coverage.*
41
+ .cache
42
+ nosetests.xml
43
+ coverage.xml
44
+ *.cover
45
+ *.py.cover
46
+ *.lcov
47
+ .hypothesis/
48
+ .pytest_cache/
49
+ cover/
50
+ *.mo
51
+ *.pot
52
+ *.log
53
+ local_settings.py
54
+ db.sqlite3
55
+ db.sqlite3-journal
56
+ instance/
57
+ .webassets-cache
58
+ .scrapy
59
+ docs/_build/
60
+ .pybuilder/
61
+ target/
62
+ .ipynb_checkpoints
63
+ profile_default/
64
+ ipython_config.py
65
+ .pdm-python
66
+ .pdm-build/
67
+ .pixi/*
68
+ !.pixi/config.toml
69
+ __pypackages__/
70
+ celerybeat-schedule*
71
+ celerybeat.pid
72
+ *.rdb
73
+ *.aof
74
+ *.pid
75
+ mnesia/
76
+ rabbitmq/
77
+ rabbitmq-data/
78
+ activemq-data/
79
+ *.sage.py
80
+ .env
81
+ .envrc
82
+ .venv
83
+ env/
84
+ venv/
85
+ ENV/
86
+ env.bak/
87
+ venv.bak/
88
+ conda-meta/
89
+ .spyderproject
90
+ .spyproject
91
+ .ropeproject
92
+ /site
93
+ .mypy_cache/
94
+ .dmypy.json
95
+ dmypy.json
96
+ .pyre/
97
+ .pytype/
98
+ cython_debug/
99
+ .abstra/
100
+ tempCodeRunnerFile.py
101
+ .ruff_cache/
102
+ .pypirc
103
+ marimo/_static/
104
+ marimo/_lsp/
105
+ __marimo__/
106
+ .streamlit/secrets.toml
107
+
108
+ # pledge
109
+ .DS_Store
110
+ .idea/
111
+ *.iml
112
+
113
+ # Harness
114
+ .claude/
115
+ .skills/
116
+ .agent/
117
+ plans/
118
+ CLAUDE.md
119
+ .tackline/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 terminalkitten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,387 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-pledge
3
+ Version: 0.0.1a1
4
+ Summary: Django support for the pledge PostgreSQL ledger extension
5
+ Project-URL: Documentation, https://pgpledge.github.io/pledge/
6
+ Project-URL: Source, https://github.com/pgpledge/pledge
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: Django
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Database
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: django>=4.2
15
+ Requires-Dist: psycopg>=3.1
16
+ Description-Content-Type: text/markdown
17
+
18
+ <p align="center">
19
+ <a href="https://pgpledge.github.io/pledge/">
20
+ <picture>
21
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/pgpledge/pledge/main/docs/public/logo-dark.svg">
22
+ <img src="https://raw.githubusercontent.com/pgpledge/pledge/main/docs/public/logo.svg" alt="pledge logo" width="72" height="72">
23
+ </picture>
24
+ </a>
25
+ </p>
26
+
27
+ <h1 align="center">django-pledge</h1>
28
+
29
+ <p align="center">
30
+ <strong>Django support for pledge, a double-entry ledger inside PostgreSQL.</strong>
31
+ </p>
32
+
33
+ > [!WARNING]
34
+ > **Alpha.** The SQL API, the stored schema and the Python API can still change between
35
+ > versions without upgrade scripts. Don't use it for real money yet. Feedback is welcome.
36
+
37
+ `django_pledge` gives you `Account`/`Transfer`/`Entry` models over pledge's views, written through
38
+ the extension's functions behind the usual Django API (`objects.create()`, `save()`,
39
+ `bulk_create()`), plus typed ids (`AccountId`, `TransferId`, `EntryId`), ledger errors, model
40
+ fields and a migration operation.
41
+
42
+ Requires Python 3.11+ and a database with the pledge extension available.
43
+
44
+ ## Install
45
+
46
+ ```sh
47
+ pip install django-pledge
48
+ ```
49
+
50
+ django-pledge pulls in Django and psycopg 3, Django's PostgreSQL driver, as usual for a
51
+ Postgres project (psycopg2 is not supported: error translation reads psycopg 3's diagnostics).
52
+ No psycopg adapters need to be registered; ids are sent as text and cast in SQL.
53
+
54
+ ## Database setup
55
+
56
+ The extension is installed by a superuser, either by hand (`CREATE EXTENSION pledge;`) or by
57
+ the app's first migration (`pledge.0001_initial` runs `CREATE EXTENSION IF NOT EXISTS pledge`).
58
+
59
+ The ledger functions run with the caller's rights (they are not `SECURITY DEFINER`), so the
60
+ application role needs these grants. Run them as the extension owner, in each database:
61
+
62
+ ```sql
63
+ GRANT USAGE ON SCHEMA pledge TO app;
64
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pledge TO app;
65
+ -- reading: the public views
66
+ GRANT SELECT ON pledge.accounts_view, pledge.transfers_view, pledge.entries_view TO app;
67
+ -- writing through pledge.create_account/create_transfer(s)/close_account
68
+ GRANT SELECT, INSERT, UPDATE ON pledge.accounts, pledge.transfers, pledge.entries TO app;
69
+ ```
70
+
71
+ Do not grant `DELETE` or `TRUNCATE`: ledger rows are never removed (triggers reject it anyway).
72
+ No `search_path` setup is needed: the id comparison operators live in `pg_catalog`, so
73
+ `=`, `IN`, joins and `ORDER BY` on id columns work for any role.
74
+
75
+ ## Django
76
+
77
+ ```python
78
+ INSTALLED_APPS = [
79
+ # ...
80
+ "django_pledge", # app label "pledge"
81
+ ]
82
+ ```
83
+
84
+ ```sh
85
+ python manage.py migrate pledge
86
+ ```
87
+
88
+ The models are unmanaged and read the public views. Writes call the extension's functions
89
+ through the familiar Django entry points:
90
+
91
+ ```python
92
+ from decimal import Decimal
93
+ from django_pledge.models import Account, Entry, Transfer
94
+
95
+ cash = Account.objects.create(name="cash", currency="EUR", allow_negative_balance=False)
96
+ alice = Account.objects.create(name="alice", currency="EUR", metadata={"user": 42})
97
+
98
+ t = Transfer.objects.create(
99
+ from_account=cash, to_account=alice, amount=Decimal("10.00"), metadata={"order": 7}
100
+ )
101
+ t.id # TransferId('plt_01K9...'), t.created_at, t.event_at are filled in
102
+ cash.transfer(alice, 5) # shortcut for the same thing
103
+ Transfer.objects.create(from_account=cash.id, to_account="pla_01K9...", amount=1) # ids work too
104
+
105
+ t = Transfer(from_account=alice, to_account=cash, amount=1)
106
+ t.save() # a new instance is created the same way
107
+
108
+ # batch: atomic, one statement, returns the same instances populated, in order
109
+ Transfer.objects.bulk_create(
110
+ [
111
+ Transfer(from_account=alice, to_account=cash, amount=Decimal("2.50")),
112
+ Transfer(from_account=cash, to_account=alice, amount=1, metadata={"line": 2}),
113
+ ],
114
+ metadata={"batch": "2025-01"}, # defaults for instances that don't set their own
115
+ event_at=None, # likewise for event_at
116
+ )
117
+
118
+ alice.refresh_from_db()
119
+ alice.balance # Decimal('...')
120
+ alice.history() # entries, oldest first (running balances)
121
+ alice.transfers() # transfers in/out, in the order they hit the account
122
+ alice.balance_at(some_datetime) # balance as of a point in time
123
+ alice.outgoing.all(), alice.incoming.all(), alice.entries.all()
124
+ Transfer.objects.filter(from_account__name="cash", amount__gte=5)
125
+ Entry.objects.select_related("account", "transfer").filter(account=alice)
126
+
127
+ alice.close() # keeps history, rejects new transfers; sets closed_at
128
+ Account.objects.verify() # [Check(check_name, ok, detail), ...]
129
+ Account.objects.assert_verified() # raises django_pledge.VerificationFailed on a failed check
130
+ ```
131
+
132
+ | Django call | runs |
133
+ |--------------------------------------------------|------------------------------|
134
+ | `Account.objects.create(...)`, new `Account.save()` | `pledge.create_account` |
135
+ | `Transfer.objects.create(...)`, new `Transfer.save()`, `account.transfer(...)` | `pledge.create_transfer` |
136
+ | `Transfer.objects.bulk_create([...])` | `pledge.create_transfers` |
137
+ | `account.close()` | `pledge.close_account` |
138
+ | `Account.objects.verify()` / `assert_verified()` | `pledge.verify()` |
139
+
140
+ Amounts are `Decimal`, `int` or a numeric string; `float` and `bool` raise `ValidationError`. Every write runs in
141
+ `transaction.atomic(using=...)`, so a failure inside your own transaction rolls back only its
142
+ savepoint. Database routing works as usual: `Account.objects.db_manager("ledger").create(...)`,
143
+ `Transfer.objects.using("ledger").bulk_create(...)`, `obj.save(using="ledger")`, or your
144
+ router's `db_for_write`; `account.transfer()`/`close()` use the account's own database. The async
145
+ variants (`acreate`, `abulk_create`, `asave`) work too.
146
+
147
+ ### Append-only
148
+
149
+ Ledger rows are never updated or deleted through the ORM. Saving an instance that is already
150
+ stored (or `save(update_fields=...)`), `delete()`, `QuerySet.update()`/`delete()`,
151
+ `bulk_update()`, `get_or_create()`, `update_or_create()` and `loaddata` raise
152
+ `django_pledge.models.ReadOnlyError`. `Entry` rows are written by the transfer functions only.
153
+
154
+ An account's balance and version change only through transfers, and closing is
155
+ `account.close()`. Account ids are assigned by the database (leave `id` unset). The extension
156
+ does allow renaming an account; that is deliberately not exposed through `save()`, so do it in
157
+ SQL if you need it (`UPDATE pledge.accounts SET name = %s, updated_at = now() WHERE id = %s`).
158
+
159
+ ### Idempotent retries
160
+
161
+ Transfer ids are generated client-side. Generate one before the call, store it with your own
162
+ record, and send the same id when you retry; a retry with the same values returns the stored
163
+ transfer and moves no money, different values raise `TransferConflict`:
164
+
165
+ ```python
166
+ from django_pledge import TransferId
167
+
168
+ payment.transfer_id = TransferId.new()
169
+ payment.save()
170
+ cash.transfer(alice, payment.amount, id=payment.transfer_id) # safe to retry
171
+ ```
172
+
173
+ When `id` is omitted a fresh `TransferId.new()` is set on the instance before the call, so the
174
+ transfer always has an id you can record, and calling `save()` (or `bulk_create()` with the same
175
+ instances) again after a failure is itself an idempotent retry.
176
+
177
+ ### Linking your models to accounts
178
+
179
+ Use a normal `ForeignKey` to `pledge.Account` with `db_constraint=False` (the target is a view,
180
+ which cannot carry a foreign key constraint):
181
+
182
+ ```python
183
+ from django.db import models
184
+ from django_pledge import AccountIdField
185
+
186
+
187
+ class Wallet(models.Model):
188
+ user = models.OneToOneField("auth.User", models.CASCADE)
189
+ account = models.ForeignKey(
190
+ "pledge.Account", models.PROTECT, db_constraint=False, related_name="wallets"
191
+ )
192
+
193
+
194
+ Wallet.objects.filter(account__currency="EUR", account__balance__gt=0)
195
+ Account.objects.filter(wallets__user=request.user)
196
+ ```
197
+
198
+ The column type is `pledge.account_id`. `AccountIdField`, `TransferIdField` and `EntryIdField`
199
+ can also be used directly, and `NumericField` is an unbounded `numeric` (no precision/scale),
200
+ like pledge's amounts.
201
+
202
+ `django_pledge.admin.ReadOnlyAdmin` is a `ModelAdmin` mixin for browsing the ledger in the admin.
203
+
204
+ ## Errors
205
+
206
+ Ledger rule violations raise `django_pledge.LedgerError` subclasses (from the model write methods, or
207
+ via `django_pledge.translate(exc)` for any Django/psycopg database error, e.g. from raw SQL). They are
208
+ matched on the `code` in the JSON object the extension puts in the error DETAIL, not on the
209
+ message. The other keys of that object become typed attributes:
210
+
211
+ | class | code | attributes |
212
+ |-----------------------------|---------------------------------------|----------------------------------|
213
+ | `InvalidRequest` | `pledge.invalid_request` | |
214
+ | `AccountNotFound` | `pledge.account_not_found` | `account_id` |
215
+ | `AccountClosed` | `pledge.account_closed` | `account_id` |
216
+ | `AccountAlreadyClosed` | `pledge.account_already_closed` | `account_id` |
217
+ | `AccountNotEmpty` | `pledge.account_not_empty` | `account_id`, `balance` |
218
+ | `NegativeBalanceNotAllowed` | `pledge.negative_balance_not_allowed` | `account_id` |
219
+ | `PositiveBalanceNotAllowed` | `pledge.positive_balance_not_allowed` | `account_id` |
220
+ | `CurrencyMismatch` | `pledge.currency_mismatch` | `from_currency`, `to_currency` |
221
+ | `TransferConflict` | `pledge.transfer_conflict` | `transfer_id` |
222
+ | `DuplicateTransferId` | `pledge.duplicate_transfer_id` | `transfer_id` |
223
+ | `Immutable` | `pledge.immutable` | `table` |
224
+
225
+ `account_id` is an `AccountId`, `transfer_id` a `TransferId`, `balance` a `Decimal` and the
226
+ others are `str`. A class only has the attributes in its row. `TransferConflict.transfer_id` is
227
+ `None` when two transactions raced to create the same new id: Postgres reports that as a unique
228
+ violation on `transfers_pkey`, which has no JSON DETAIL. `e.detail` is the parsed DETAIL as a
229
+ dict.
230
+
231
+ ```python
232
+ import django_pledge
233
+
234
+ try:
235
+ cash.transfer(alice, amount, id=tid)
236
+ except django_pledge.NegativeBalanceNotAllowed as e:
237
+ e.account_id # the AccountId that would go negative
238
+ except django_pledge.LedgerError as e:
239
+ e.code, e.message, e.detail
240
+ ```
241
+
242
+ ## JSON
243
+
244
+ Balances keep the scale of their transfers, so a zero BTC balance is `Decimal('0E-8')` and
245
+ `DjangoJSONEncoder` writes `"0E-8"`. `django_pledge.json.LedgerJSONEncoder` writes Decimals in
246
+ plain notation (`"0.00000000"`). The models' `metadata` fields use it; pass it to `JsonResponse`
247
+ or `json.dumps(..., cls=LedgerJSONEncoder)` for your own output.
248
+
249
+ ## Ids
250
+
251
+ ```python
252
+ from django_pledge import AccountId
253
+
254
+ i = AccountId.new() # UUIDv7: millisecond timestamp + random
255
+ str(i) # 'pla_01K9FKX2S1FGYSWKX1PB1X9SFP'
256
+ AccountId("pla_01k9fkx2s1fgyswkx1pb1x9sfp") == i # lowercase, O->0, I/L->1 accepted
257
+ i.uuid, i.bytes, i.timestamp
258
+ ```
259
+
260
+ Ids of different kinds never compare equal and cannot be ordered against each other.
261
+
262
+ ## Development
263
+
264
+ ```sh
265
+ docker compose up -d --wait db # from the repo root: Postgres with pledge on :5432
266
+ uv sync --all-groups
267
+ uv run pytest
268
+ uv run ruff check src tests example && uv run --group lint ty check
269
+ ```
270
+
271
+ Tests create a fresh database (`PLEDGE_TEST_DB`, default `pledge_python_test`) and a role
272
+ `pledge_app` with only the grants above, using the superuser DSN in `PLEDGE_TEST_DSN`
273
+ (default `host=localhost port=5432 user=pledge password=pledge dbname=pledge`).
274
+
275
+ ## Example project
276
+
277
+ `example/` is a small currency exchange (`core` settings, `exchange` app) built on the
278
+ public API. Start with `example/exchange/services.py`.
279
+
280
+ Run it with Docker from the repository root, no local Python needed:
281
+
282
+ ```sh
283
+ just example # http://localhost:8000/ and /admin/ (admin / admin)
284
+ just example-simulate # 20 traders, 500 random trades, then checks every currency sums to 0
285
+ ```
286
+
287
+ The first start creates the `exchange` database, funds the exchange's liquidity and creates the
288
+ admin user. Open http://localhost:8000/ for a small web interface: create a trader, then deposit
289
+ by bank transfer or card, preview and place trades, withdraw, close wallets and read each wallet's
290
+ statement. The home page shows the broker's accounts per currency, a button for the card provider
291
+ to pay out, and whether `Account.objects.verify()` passes. Every form carries a request id
292
+ rendered with the page, so a double submit or a refresh books nothing twice.
293
+
294
+ The same exchange is also a JSON API under `/exchange/`:
295
+
296
+ ```sh
297
+ curl -X POST localhost:8000/exchange/traders/ -d '{"name": "alice"}'
298
+ curl -X POST localhost:8000/exchange/traders/1/deposits/ -d '{"currency": "EUR", "amount": "1000.00"}'
299
+ curl -X POST localhost:8000/exchange/traders/1/deposits/ \
300
+ -d '{"currency": "EUR", "amount": "250.00", "method": "card"}'
301
+ curl -X POST localhost:8000/exchange/house/payouts/ -d '{"currency": "EUR"}'
302
+ curl -X POST localhost:8000/exchange/traders/1/trades/ \
303
+ -d '{"request_id": "t1", "sell": "EUR", "buy": "BTC", "amount": "500.00"}'
304
+ curl localhost:8000/exchange/traders/1/
305
+ curl localhost:8000/exchange/traders/
306
+ ```
307
+
308
+ - Customers are the app's own `Trader` model; Django's auth users only log in to the admin.
309
+ - Every trader gets one ledger account per currency, EUR, USD and BTC with 8 decimals, linked by
310
+ a `Wallet` model with `OneToOneField("pledge.Account", db_constraint=False)`. Trader accounts
311
+ can't go negative.
312
+ - Per currency the broker owns a liquidity account holding its inventory and a fee revenue
313
+ account, plus three accounts for money outside the exchange, which never go above zero: `card`
314
+ (money held at the card provider, not yet paid out), `bank` (the broker's own bank account) and
315
+ `card_costs` (the card provider's fees, an expense). Ledger names look like `house:card:EUR`.
316
+ - A bank deposit moves money from `bank` to the trader. A card deposit is one atomic batch of
317
+ `card` → trader for the full amount and `card_costs` → `card` for the 1.5% card fee
318
+ (`EXCHANGE_CARD_FEE_RATE`, rounded up). The fee leg's id is derived from the deposit's id, so a
319
+ retried card deposit books nothing twice. When the card provider pays out, one `bank` → `card`
320
+ transfer brings `card` back to zero. Withdrawals pay out from `bank`, and `fund_liquidity` moves
321
+ the owner's capital from `bank` to liquidity.
322
+ - A trade converts at a quoted rate: pledge never crosses currencies, so it is one atomic
323
+ `bulk_create` batch of trader → house in the sold currency, house → trader in the bought one
324
+ and a fee leg, each with `{"kind": "trade", "trade": id, "leg": ...}` metadata. If any leg
325
+ fails because the trader or the exchange runs short or an account is closed, nothing is booked.
326
+ - Trades and withdrawals carry a client `request_id` and store their transfer ids before pledge
327
+ is called, so a retried request replays the original outcome instead of booking twice.
328
+ - Closing a wallet pays out its balance and closes the account in one transaction. There's a
329
+ portfolio, a statement from `account.history()`, a proxy model, the web pages, JSON views and
330
+ a read-only admin for the ledger.
331
+
332
+ `uv run pytest` runs its tests too, in a child process with their own database
333
+ (`PLEDGE_EXAMPLE_TEST_DB`, default `pledge_example_test`, as role `pledge_app`); run them
334
+ directly with `uv run pytest example`.
335
+
336
+ To run it by hand, point `PLEDGE_DSN` at a database with the extension and, from `example/`:
337
+
338
+ ```sh
339
+ python manage.py migrate
340
+ python manage.py fund_liquidity BTC 10
341
+ python manage.py simulate --traders 20 --trades 500
342
+ python manage.py runserver
343
+ ```
344
+
345
+ `simulate` signs up traders, runs random card and bank deposits, trades (some retried),
346
+ withdrawals and card payouts, then runs `Account.objects.assert_verified()` and prints per
347
+ currency what the customers hold and each house account, which always sum to zero.
348
+
349
+ ## Extending accounts with your own data
350
+
351
+ Ledger rows are immutable and only come from pledge's functions, so keep your own data next to
352
+ them instead of inside them:
353
+
354
+ ```python
355
+ class Wallet(models.Model): # your mutable data, your table
356
+ customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
357
+ kind = models.CharField(max_length=20)
358
+ account = models.OneToOneField(
359
+ "pledge.Account", on_delete=models.PROTECT, db_constraint=False, related_name="wallet"
360
+ )
361
+
362
+
363
+ wallet.account.balance, account.wallet.customer
364
+ ```
365
+
366
+ For extra *behaviour* (methods, managers, a different admin), a proxy model is fine:
367
+
368
+ ```python
369
+ class Pot(Account):
370
+ class Meta:
371
+ proxy = True
372
+
373
+ def is_empty(self) -> bool:
374
+ return self.balance == 0
375
+ ```
376
+
377
+ Multi-table inheritance (`class CustomerAccount(Account)` without `proxy = True`) is rejected
378
+ with `ImproperlyConfigured`: Django would write a parent row through the ORM.
379
+
380
+ ## Names
381
+
382
+ Everything pledge stores lives in its own PostgreSQL schema `pledge` (`pledge.accounts`,
383
+ `pledge.transfers`, `pledge.entries`, read through `pledge.*_view`), so it can't collide with
384
+ your tables in `public`, including your own `Account` model. In Django the models are
385
+ `pledge.Account` / `pledge.Transfer` / `pledge.Entry` (app label `pledge`); import with an alias
386
+ if you also have an `Account` (`from django_pledge.models import Account as LedgerAccount`). The
387
+ only names pledge claims are the schema `pledge` and the Django app label `pledge`.