stapel-reviews
Target-generic reviews and ratings: an author rates and reviews an opaque host-defined target (target_type + target_key), driven by a per-target-type policy registry (who may review, pre/post moderation, one-review-per-author, owner responses) whose authority questions are answered by host comm callbacks. The module owns the per-target aggregate (avg/count over published reviews) and emits a generic visibility-change fact carrying it, so a host catalog maintains its own rating projection without calling back.
Part of the Stapel framework — composable Django apps that deploy as a monolith or as microservices without changing module code.
Install
pip install stapel-reviews
At a glance
| Fact | Value |
|---|---|
| Version | 0.7.0 |
| Python | >=3.11 (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 6 |
| Config axes | 2 |
| Usage surface | 19 |
| Extension points | 11 |
| Error codes | 54 |
| Fleet dependencies | stapel-auth (optional) · stapel-core |
Documentation
OpenAPI · capabilities.json · llms.txt (for agents)
What this is
A generic review core — Review (author + rating + body about an opaque target) and Response (the target owner's reply) — driven entirely by a per-target-type policy registry. The module ships knowing nothing about what gets reviewed: a host registers its target types (a seller, a listing, a driver, a course), each with a policy, and answers the domain questions — "may this author review?", "who owns this target?" — through comm Function callbacks, so the module never imports a host model.
Quick start
INSTALLED_APPS = [
# ...
"stapel_reviews",
]
# urls.py
path("reviews/", include("stapel_reviews.urls"))
Concepts
- Target — opaque:
target_type(a key the host registered) +target_key(an opaque host string — a UUID, a slug, a composite). No FK to any host model; the module is domain-blind. - Policy — per target type: who may review (
can_reviewcomm callback), pre/post moderation, one-review-per-author, whether owner responses are allowed (allow_response), who may moderate/respond (can_moderatecomm callback), and — optionally — who owns a target (owner_key_for). - Aggregate — the module owns
avg/countover published reviews per target, and emits a generic fact carrying it on every visibility change, so a host catalog maintains its own rating projection (§10) without calling back. - Owner key — optional, denormalised: the answer of the type's
owner_key_forresolver, stamped on the review when it is written, so the module can also aggregate everything one owner owns (a seller-wide rating) while still knowing nothing about what a listing or a seller is.
STAPEL_REVIEWS = {
"TARGET_TYPES": {
"seller": {
"can_review": "marketplace.buyer_of_seller", # host comm Function
"can_moderate": "marketplace.is_seller_owner",
"moderation": "post",
"one_per_author": True,
"allow_response": True,
},
"listing": {
"moderation": "pre",
# Optional: who owns this target. A callable, or a comm Function
# name for a policy that has to stay JSON-shaped. Registering none
# leaves owner_key empty and changes nothing else.
"owner_key_for": lambda target_key: seller_id_of(target_key),
},
},
}
from stapel_reviews import services
review = services.create_review(
target_type="seller", target_key="s-42", author=user, rating=5, body="great",
)
services.moderate_review(review, actor=owner, action="hide", reason="spam")
services.respond(review, author=owner, body="thanks for the feedback")
agg = services.aggregate("seller", "s-42") # Aggregate(avg=..., count=...)
# Everything one owner owns, batched (the seller-wide rating):
services.aggregates_by_owner_keys(["s-42", "s-43"]) # {"s-42": {"avg": .., "count": ..}}
Settings
All configuration lives in the STAPEL_REVIEWS namespace (dict setting, flat
setting, or env var — resolved lazily):
| Key | Default | Meaning |
|---|---|---|
TARGET_TYPES |
{} |
The target-type registry {type: policy}, merged over the (empty) built-ins; None removes a type |
MODERATION_DEFAULT |
"post" |
Default moderation mode (post/pre) for types that don't override it |
RESPONSES |
True |
Whether owner responses are allowed by default |
RATING_MIN |
1 |
Inclusive minimum rating |
RATING_MAX |
5 |
Inclusive maximum rating |
MODERATION_TARGET_TYPE |
"review" |
The target_type on an incoming moderation.completed verdict that means "this is about a review" |
comm surface
| Kind | Name | Contract |
|---|---|---|
| Emit | reviews.review.published |
A review became visible — carries {aggregate: {avg, count}} for the host projection |
| Emit | reviews.review.hidden |
A review left the visible set — carries the updated aggregate |
| Function | reviews.aggregate |
{target_type, target_key} -> {avg, count} |
| Function | reviews.aggregates_by_keys |
{keys, target_type?} -> {key: {avg, count}} — a Projection's live_query |
| Function | reviews.aggregates_by_owner_keys |
{owner_keys, target_type?} -> {owner_key: {avg, count}} — the owner-wide rating |
| Function | reviews.aggregates_export |
{cursor?, limit?} -> {rows, cursor, total} — a Projection's source_of_truth |
| Function | reviews.moderation_content |
{review_id} -> {text, title, language, media, author_id, url, …} |
| Consume | moderation.completed |
{target_type, target_key, decision, …} — a platform verdict, applied to the review as the system actor |
| Callback (host) | policy can_review |
{author_id, target_type, target_key} -> bool — the host answers |
| Callback (host) | policy can_moderate |
{actor_id, target_type, target_key} -> bool — the host answers |
| Callback (host) | policy owner_key_for |
target_key -> str | None (callable), or a comm Function {target_type, target_key} -> owner key — optional |
Host rating projection
The two batch Functions are the halves a host Projection over reviews is
declared against — one keyed read for live traffic, one snapshot for rebuild:
class ListingReviewSummaryProjection(Projection):
consumes = ("reviews.review.published", "reviews.review.hidden")
source_key = "target_key"
live_query = "reviews.aggregates_by_keys" # local mode reads through it
source_of_truth = "reviews.aggregates_export" # rebuild() / drift_check()
Owner-wide ratings
A marketplace needs the rating of a seller, not only of each listing — and the module must not learn what a listing is to produce it. The seam is one optional resolver and one denormalised column:
STAPEL_REVIEWS = {
"TARGET_TYPES": {
"listing": {"owner_key_for": "catalog.owner_of_listing"}, # or a callable
},
}
The resolver is asked once, when the review is written, and its answer is
stored on Review.owner_key. Reads go through
reviews.aggregates_by_owner_keys or POST /reviews/api/v1/reviews/aggregates/by-owner
(public, up to 100 owner keys per call), which return {owner_key: {avg, count}} over published reviews with the same rounding as reviews.aggregate.
The same column answers the seller page's rows, not only its number: the
list endpoint takes owner_key instead of the target pair —
GET /reviews/api/v1/reviews?owner_key=s-42[&target_type=listing]
— every review of everything that owner owns, newest first, same visibility
rule, same anchor pagination, same item shape. Exactly one addressing per
request: naming both a target and an owner is
error.400.reviews_ambiguous_addressing, naming neither is
error.400.reviews_unknown_target_type as before.
Reviews written before the resolver was registered carry an empty owner key —
stamp them once with:
python manage.py reviews_backfill_owner_keys [--target-type listing] [--dry-run]
A host that registers no resolver stamps nothing, and the owner aggregate
simply answers {}.
Moderation verdicts
An external moderation module owns the decision; this module owns applying it.
When a case about a review resolves, moderation.completed arrives and the
review is hidden (rejected) or published (approved) — needs_review and
dismissed deliberately move nothing. The verdict is applied as
services.SYSTEM_ACTOR, the one actor that gets past the fail-closed
can_moderate gate: authorization already happened where the verdict was made,
and asking a target type with no can_moderate callback would deny the
platform its own decision. Redelivery is a no-op — idempotency is by state, and
no table of processed event ids is kept.
Extension points
See MODULE.md — the agent-facing map of every fork-free seam (the TARGET_TYPES registry and its policy callbacks, the projection emits, the aggregate Function, serializer seams, settings).
Development
pip install -e . && pip install pytest pytest-django ruff
./setup-hooks.sh
pytest tests/
License
MIT — see LICENSE.
This page is assembled by stapel-readme from docs/readme.md plus the contract artifacts in docs/. Edit the prose in docs/readme.md; the badges, facts and links above and below it are generated — do not hand-edit README.md.
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 stapel_reviews-0.7.0.tar.gz.
File metadata
- Download URL: stapel_reviews-0.7.0.tar.gz
- Upload date:
- Size: 91.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9920dc1fa10a6877b5d9f790c3b0c940071219075e580b77f23e444afdf06102
|
|
| MD5 |
bb62974766e9619ae90b5ddca4a91f52
|
|
| BLAKE2b-256 |
570520640664ddb74a091edf69904a20eef4b87ed9ca0d11b0733fa961bacc0e
|
Provenance
The following attestation bundles were made for stapel_reviews-0.7.0.tar.gz:
Publisher:
publish.yml on usestapel/stapel-reviews
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_reviews-0.7.0.tar.gz -
Subject digest:
9920dc1fa10a6877b5d9f790c3b0c940071219075e580b77f23e444afdf06102 - Sigstore transparency entry: 2789674450
- Sigstore integration time:
-
Permalink:
usestapel/stapel-reviews@6f532d13378aacfdaf928b7e1b103bbc867341aa -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6f532d13378aacfdaf928b7e1b103bbc867341aa -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_reviews-0.7.0-py3-none-any.whl.
File metadata
- Download URL: stapel_reviews-0.7.0-py3-none-any.whl
- Upload date:
- Size: 81.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8b2e51ef450eb58302b59bb3b5ac853322adaa7aeab3eff423fbcf893cf1b9a4
|
|
| MD5 |
ec3bdc0423f9423ab4117257db5e97ed
|
|
| BLAKE2b-256 |
e74882282170c37276ee148c6f5d87a252b0ed323b07273c99751b836fcfac6e
|
Provenance
The following attestation bundles were made for stapel_reviews-0.7.0-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-reviews
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_reviews-0.7.0-py3-none-any.whl -
Subject digest:
8b2e51ef450eb58302b59bb3b5ac853322adaa7aeab3eff423fbcf893cf1b9a4 - Sigstore transparency entry: 2789674503
- Sigstore integration time:
-
Permalink:
usestapel/stapel-reviews@6f532d13378aacfdaf928b7e1b103bbc867341aa -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6f532d13378aacfdaf928b7e1b103bbc867341aa -
Trigger Event:
push
-
Statement type: