Metadata-Version: 2.4
Name: stapel-classified
Version: 0.2.1
Summary: Composite: classified ads — the shop composite + geo + search + moderation
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
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.43
Requires-Dist: stapel-shop<0.3,>=0.2.3
Requires-Dist: stapel-categories<0.6,>=0.5.6
Requires-Dist: stapel-listings<0.8,>=0.5
Requires-Dist: stapel-geo<0.4,>=0.2
Requires-Dist: stapel-search<0.3,>=0.2.2
Requires-Dist: stapel-moderation<0.4,>=0.2
Provides-Extra: all
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 and the marketplace reason taxonomy. Since 0.2.0 it also owns the one kind of state a composite may hold: the JOIN between a chat conversation and the listing it is about, because stapel-chat may not know what a listing is and stapel-listings may not know what a conversation is. Off that join it serves a conversation HEADER — the short listing card (title, price, primary image with CDN render metadata, and a state that says `available`, `unavailable` or `gone`, which is exactly the answer a public read cannot give) plus the counterparty's public seller card — and it enforces a user-to-user block at the one place a classified conversation begins, announcing at every boot whether that enforcement is live in this deployment.

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.2.1` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 3 |
| Config axes | 4 |
| Usage surface | 9 |
| Extension points | 3 |
| Error codes | 47 |
| 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) (optional) · [`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_CLASSIFIED["BLOCK_ENFORCEMENT"]` | `auto` — enforced where a block provider answers, and `manage.py check` says at every boot which state you are in. Set `required` once one does. |
| `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, primary image
with CDN render metadata, 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>
