Django QuerySet façade for user-written API adapters.
Project description
callish
Django QuerySet façade for user-written API adapters.
callish lets a Django app expose data living behind an arbitrary API as if
it were a Django model — so ModelForm, generic class-based views, templates,
and admin register/instantiation keep working — without trying to
standardise the HTTP shape. You write the adapter (list / retrieve / create / update / delete); callish wires it into Django.
Why
Existing libraries either lock you into a specific HTTP shape
(wagtail/queryish — REST-only, read-only) or rebuild Django machinery
from scratch. APIs vary too much for one base class to fit all of them.
callish takes the opposite stance: you write the five-method adapter, and
callish handles the Django integration around it.
The conformance suite is the spec — when the test battery is green, Django integration works.
Install
pip install callish # core
# or, with Poetry:
poetry add callish
Requires Python ≥3.10, Django ≥5.0 (5.x or 6.x). No HTTP client dependency — that's the adapter's job.
Quick start
from callish import APIModel, APIModelForm
from callish.fields import CharField, IntegerField, BooleanField
class Invoice(APIModel):
id = IntegerField(primary_key=True)
number = CharField(max_length=64)
amount_cents = IntegerField()
paid = BooleanField(default=False)
class Meta:
adapter = "myproject.adapters:InvoiceAdapter"
app_label = "myproject"
# Use it like a Django model:
qs = Invoice.objects.filter(paid=False).order_by("-amount_cents")[:25]
for invoice in qs: # → adapter.list(...)
print(invoice.number, invoice.amount_cents)
invoice = Invoice.objects.get(pk=42) # → adapter.retrieve(42)
invoice.paid = True
invoice.save() # → adapter.update(42, {...})
new = Invoice(number="INV-100", amount_cents=10_000, paid=False)
new.save() # → adapter.create({...})
invoice.delete() # → adapter.delete(42)
Writing an adapter
Any object with these methods works:
class InvoiceAdapter:
def list(self, *, filters, ordering, offset, limit): ... # → Sequence[dict]
def retrieve(self, pk): ... # → dict
def create(self, data): ... # → dict (with pk)
def update(self, pk, data): ... # → dict
def delete(self, pk): ...
def count(self, *, filters): ... # optional
Raise callish.exceptions.NotFound / Unauthorized / RateLimited / Upstream5xx / AdapterValidationError for errors. NotFound is mapped to
the model's DoesNotExist; the rest surface as-is.
A complete reference implementation lives in
src/callish/testing/reference_adapter.py
(InMemoryAdapter — dict-backed, used by callish's own tests).
Django integration
Everything you'd expect from a Django model works:
- ModelForm — subclass
APIModelForm;form.save()dispatches toadapter.create/adapter.update. - Generic class-based views —
ListView,DetailView,CreateView,UpdateView,DeleteViewall work unchanged. - Function-based views —
Invoice.objects.filter(...)inside anydef view(request)function. - Templates —
{% for invoice in invoices %}{{ invoice.number }},{{ form.as_p }}etc. all work. - Admin —
admin.site.register([Invoice], InvoiceAdmin)(note the list wrap) registers fine andModelAdmininstances construct. The changelist is out of scope (admin assumes a real DB-backed Model).
The conformance suite
callish ships a milestone-organised pytest suite that exercises the adapter
contract. Downstream adapter authors add one fixture and run:
# your_project/conftest.py
import pytest
from your_project.adapters import StripeInvoiceAdapter
@pytest.fixture
def conform_adapter():
return StripeInvoiceAdapter(api_key="sk_test_...")
pytest --pyargs callish.testing.suite
The pytest plugin auto-loads via the pytest11 entry point. To opt out:
pytest -p no:callish.
Milestones:
- M1 — list / retrieve / count / filter / order / slice
- M2 — create / update / delete
- M3 — error and timeout propagation
Use --callish-skip-m3 to skip error-path tests during incremental adoption.
Non-goals (v1)
- Cross-source joins (API model ↔ ORM model)
- Relations between API models (FK, M2M, prefetch)
- Async adapters
- ModelAdmin changelist
- Code generation from OpenAPI / GraphQL schemas
- Wagtail Snippet / Chooser integration
callish coexists with Wagtail 6.x and 7.x without monkey-patching Django —
callish.apps.CallishConfig is intentionally inert.
Development
poetry install # core + dev deps
poetry run pytest # library suite (94 tests)
poetry run pytest --pyargs callish.testing.suite # shipping conformance suite
poetry run ruff check src tests
poetry run mypy src/callish
The full spec is in callish-spec.md. Architecture and
internals are documented in CLAUDE.md.
License
BSD-3-Clause.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file callish-0.1.0.tar.gz.
File metadata
- Download URL: callish-0.1.0.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.4 CPython/3.13.7 Darwin/25.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0974d14e109e0ae894b5be15b68af4d686d482591bdc4369b0e0a43929c61f58
|
|
| MD5 |
d0bfc85b1ac7f53c27a6c9ce6b99a8cc
|
|
| BLAKE2b-256 |
7047a0738305567925ec72a5045a3928df57b85c8495bf908817a4c46afd7de7
|
File details
Details for the file callish-0.1.0-py3-none-any.whl.
File metadata
- Download URL: callish-0.1.0-py3-none-any.whl
- Upload date:
- Size: 23.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.4 CPython/3.13.7 Darwin/25.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8507895d42d07aa70ac34ba81a816c94a8043234f9cbebcd1d800bb7f5fe4051
|
|
| MD5 |
5696f92d8ab790bbd0e306d9eb15a87c
|
|
| BLAKE2b-256 |
a5b2e8661adceab1679a032cf5fb0471c76ca5d84ea5989736fc9dd9e85a7c15
|