Skip to main content

stapel-reviews

CI coverage pypi downloads python license llms.txt

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_review comm callback), pre/post moderation, one-review-per-author, whether owner responses are allowed (allow_response), who may moderate/respond (can_moderate comm callback), and — optionally — who owns a target (owner_key_for).
  • Aggregate — the module owns avg/count over 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_for resolver, 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

stapel_reviews-0.7.0.tar.gz (91.4 kB view details)

Uploaded Source

Built Distribution

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

stapel_reviews-0.7.0-py3-none-any.whl (81.9 kB view details)

Uploaded Python 3

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

Hashes for stapel_reviews-0.7.0.tar.gz
Algorithm Hash digest
SHA256 9920dc1fa10a6877b5d9f790c3b0c940071219075e580b77f23e444afdf06102
MD5 bb62974766e9619ae90b5ddca4a91f52
BLAKE2b-256 570520640664ddb74a091edf69904a20eef4b87ed9ca0d11b0733fa961bacc0e

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_reviews-0.7.0.tar.gz:

Publisher: publish.yml on usestapel/stapel-reviews

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for stapel_reviews-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8b2e51ef450eb58302b59bb3b5ac853322adaa7aeab3eff423fbcf893cf1b9a4
MD5 ec3bdc0423f9423ab4117257db5e97ed
BLAKE2b-256 e74882282170c37276ee148c6f5d87a252b0ed323b07273c99751b836fcfac6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_reviews-0.7.0-py3-none-any.whl:

Publisher: publish.yml on usestapel/stapel-reviews

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.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