Metadata-Version: 2.4
Name: stapel-classified
Version: 0.7.2
Summary: Composite: classified ads — the shop composite + geo + search + moderation + vocabularies
License: MIT
Project-URL: Homepage, https://github.com/usestapel/stapel-classified
Project-URL: Repository, https://github.com/usestapel/stapel-classified
Project-URL: Documentation, https://github.com/usestapel/stapel-classified#readme
Project-URL: Changelog, https://github.com/usestapel/stapel-classified/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/usestapel/stapel-classified/issues
Keywords: django,stapel,classified,composite,geo,search,moderation,vocabularies
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stapel-core<1.0,>=0.45
Requires-Dist: stapel-attributes<0.8,>=0.7
Requires-Dist: stapel-reviews<0.6,>=0.5
Requires-Dist: stapel-shop<0.3,>=0.2.14
Requires-Dist: stapel-categories<0.11,>=0.10
Requires-Dist: stapel-vocabularies<0.2,>=0.1
Requires-Dist: stapel-listings<0.12,>=0.11
Requires-Dist: stapel-geo<0.5,>=0.2
Requires-Dist: stapel-search<0.9,>=0.8.1
Requires-Dist: stapel-moderation<0.5,>=0.4
Requires-Dist: stapel-chat<0.8,>=0.6.3
Provides-Extra: all
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-django; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: channels; extra == "test"
Requires-Dist: daphne; extra == "test"
Requires-Dist: stapel-tools<1,>=0.9.1; extra == "test"
Requires-Dist: stapel-profiles<1.0,>=0.17.0; extra == "test"
Requires-Dist: stapel-realtime[channels]<1.0,>=0.1.2; extra == "test"
Dynamic: license-file

<!-- Generated by stapel-readme from docs/readme.md + docs/*.json. Do not edit this file; edit docs/readme.md and re-run `make readme`. -->

# stapel-classified

[![CI](https://img.shields.io/github/actions/workflow/status/usestapel/stapel-classified/ci.yml?branch=main&logo=github&label=CI)](https://github.com/usestapel/stapel-classified/actions/workflows/ci.yml?query=branch%3Amain)
[![coverage](https://img.shields.io/codecov/c/github/usestapel/stapel-classified?branch=main&logo=codecov&label=coverage)](https://app.codecov.io/gh/usestapel/stapel-classified)
[![pypi](https://img.shields.io/pypi/v/stapel-classified?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/stapel-classified/)
[![downloads](https://static.pepy.tech/badge/stapel-classified/month)](https://pepy.tech/project/stapel-classified)
[![python](https://img.shields.io/pypi/pyversions/stapel-classified?logo=python&logoColor=white)](https://pypi.org/project/stapel-classified/)
[![license](https://img.shields.io/github/license/usestapel/stapel-classified)](https://github.com/usestapel/stapel-classified/blob/main/LICENSE)
[![llms.txt](https://img.shields.io/badge/llms.txt-blue)](https://github.com/usestapel/stapel-classified/blob/main/docs/llms.txt)

> Composite for location-bound classified ads: the stapel-shop composite (categories + listings + reviews) plus stapel-geo, stapel-search and stapel-moderation, and the cross-domain declarations no member is allowed to write — the `listing` search source, the `listing`/`review`/`seller`/`chat_message` moderation target policies, the marketplace reason taxonomy, and (since 0.3.2) the `listing` subject type stapel-chat's registry ships empty of. Its own HTTP surface is a conversation HEADER, assembled from three modules that may not know about each other: chat says who is in the thread and what it is about, listings answers the short card (title, price, the photo gallery with CDN render metadata, and a state that says `available`, `unavailable` or `gone` — exactly the answer a public read cannot give), profiles answers the counterparty. It owns NO table: the binding it kept from 0.2.0 to 0.3.1 existed only because chat could not tell two threads about two listings apart, and it was deleted rather than kept in sync the release chat could. It enforces no block of its own: stapel-chat 0.6.1 holds both write doors (opening a direct thread, sending into one) at the one point every client passes, and this composite's whole contribution is the value `required` it sets on chat's BLOCK_ENFORCEMENT axis in the preset. The pre-creation door it kept until 0.3.x was deleted in 0.4.0, and a deployment that still declares the old STAPEL_CLASSIFIED keys is told so at boot (E003) rather than silently inheriting chat's default.

Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps that deploy as a monolith or as microservices without changing module code.

## Install

```bash
pip install stapel-classified
```

## At a glance

| Fact | Value |
|---|---|
| Version | `0.7.2` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 3 |
| Config axes | 3 |
| Usage surface | 7 |
| Extension points | 3 |
| Error codes | 46 |
| Fleet dependencies | [`stapel-attributes`](https://github.com/usestapel/stapel-attributes) · [`stapel-categories`](https://github.com/usestapel/stapel-categories) · [`stapel-cdn`](https://github.com/usestapel/stapel-cdn) (optional) · [`stapel-chat`](https://github.com/usestapel/stapel-chat) · [`stapel-geo`](https://github.com/usestapel/stapel-geo) · [`stapel-listings`](https://github.com/usestapel/stapel-listings) · [`stapel-moderation`](https://github.com/usestapel/stapel-moderation) · [`stapel-notifications`](https://github.com/usestapel/stapel-notifications) (optional) · [`stapel-profiles`](https://github.com/usestapel/stapel-profiles) (optional) · [`stapel-reviews`](https://github.com/usestapel/stapel-reviews) · [`stapel-search`](https://github.com/usestapel/stapel-search) |

## Documentation

[OpenAPI](https://github.com/usestapel/stapel-classified/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-classified/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-classified/blob/main/docs/llms.txt)

## Assemble (one line)

```bash
pip install stapel-tools
stapel-assemble myads --libs classified
cd myads && make test
```

That expands `classified` through the STAPEL_LIBS `requires`
closure and wires every member module into INSTALLED_APPS,
requirements.txt, urls.py and CONFIG.MD, then runs the verify gates.

## Manual wiring (no scaffold)

```python
# settings.py
from stapel_classified import preset

INSTALLED_APPS = [
    # ... django/stapel-core baseline (incl. stapel_core.django.projections)
    *preset.INSTALLED_APPS,
]
for _k, _v in preset.SETTINGS_DEFAULTS.items():
    globals().setdefault(_k, _v)

# urls.py
from django.urls import include, path

from stapel_classified import preset

urlpatterns = [
    path(prefix, include(module)) for prefix, module in preset.URL_INCLUDES
]
```

Mount from `preset.URL_INCLUDES` rather than by hand: `stapel-classified`
(its own conversation surface), `stapel-categories` and `stapel-listings`
contribute only the `v1/` segment and belong under `<mod>/api/`, while
`reviews`, `geo`, `search` and `moderation` bake `api/v1/` in themselves. Both end at `/<mod>/api/v1/...`, and getting it wrong is a
`stapel_core.mounts.E004` refusal to boot, not a cosmetic difference.

## Config checklist (fill these, in the generated project's CONFIG.MD too)

| Key | Note |
|-----|------|
| `STAPEL_REVIEWS["TARGET_TYPES"]` | prefilled by the preset (targets `listing`) |
| `STAPEL_SEARCH["SOURCES"]` | prefilled by the preset (the `listing` source) |
| `STAPEL_MODERATION["TARGET_TYPES"]` | prefilled by the preset (`listing` pre-publication, `review`/`seller`/`chat_message` post) |
| `STAPEL_CHAT["BLOCK_ENFORCEMENT"]` | prefilled `required` by the preset — the ONE block switch, and it is stapel-chat's. Chat holds both doors (opening a direct thread, sending into one) and says at every boot which state you are in. A key of this name under `STAPEL_CLASSIFIED` moved here in 0.4.0 and is a boot error (`stapel_classified.E003`). |
| `STAPEL_ACCESS["ROLES"]` | **yours** — the moderation console is staff-only; `preset.RECOMMENDED_ACCESS_ROLES` shows the shape |
| `STAPEL_GDPR["DATA_OWNERS"]` | **yours** — must list `"moderation"`, or erasure never closes over complaint data |
| `STAPEL_MODERATION["APPEAL_URL_TEMPLATE"]` | **yours** — an empty appeal link is what DSA Art. 17 notices |
| `STAPEL_SEARCH["BACKEND"]` | defaults to Postgres; name `naive` or `meili` if that is not your engine |
| `STAPEL_LISTINGS["BASE_CURRENCY"]` | default `USD` — set your currency |
| `STAPEL_GEO[...]` | geocoder provider/keys — see stapel-geo CONFIG.MD |
| listing coordinates | lat/lon are LISTING fields (no projection needed) — see stapel-listings |

## Glue

Two members ship deliberately empty registries, because neither may know what
a listing is. This package is the one place that knows both sides:

- **`STAPEL_SEARCH["SOURCES"]["listing"]`** →
  `stapel_classified.search_sources.listing_source`. Pulls documents through
  `listings.search_documents` / `listings.search_export`, invalidated by
  `listing.published` / `listing.updated` / `listing.removed`. Registering it
  is also what wires the subscribers — you write no signal handler. Facets are
  built from listings' `features_search` (stapel-search's declared lossy
  fallback: attribute *range* filters do not work until listings serves DAOs).
- **`STAPEL_MODERATION["TARGET_TYPES"]`** → `listing` (pre-publication:
  `listing.submitted` opens the case and nothing is public until the verdict)
  and `review` (post: live on arrival, a verdict is a takedown).

Coordinates need no glue at all: they are the listing's OWN fields (lat/lon on
the listing), not a foreign aggregate.

## The conversation header (0.2.0)

A chat in a classified marketplace is always ABOUT something and BETWEEN two
identified people. Neither fact belongs to the messaging engine, so this
composite owns the join and serves the header:

```
POST /classified/api/v1/conversations           bind a chat thread to a listing
GET  /classified/api/v1/conversations/{id}      one header
POST /classified/api/v1/conversations/contexts  a page of them, for the inbox
```

A header carries the **short listing card** — title, price, the photo
gallery with CDN render metadata over every frame, and a `state` of `available` / `unavailable`
(sold, paused, expired) / `gone`, which is the answer a public listing read
cannot give and exactly the case a buyer is most confused by — plus the
**counterparty's public seller card**. Never more of a person than their
public profile.

`docs/frontend-contract.md` is the document the default skins build against:
every payload, every refusal, and the six things the fleet does not serve yet
with what the UI does about each in the meantime.

## License

MIT — see [LICENSE](https://github.com/usestapel/stapel-classified/blob/main/LICENSE).

---

<sub>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`.</sub>
