Metadata-Version: 2.4
Name: stapel-vocabularies
Version: 0.1.1
Summary: Reference vocabularies (levels, terms, edges) behind stapel-attributes' ref_select types, for the Stapel framework
License: MIT
Project-URL: Homepage, https://github.com/usestapel/stapel-vocabularies
Project-URL: Repository, https://github.com/usestapel/stapel-vocabularies
Project-URL: Documentation, https://github.com/usestapel/stapel-vocabularies#readme
Project-URL: Changelog, https://github.com/usestapel/stapel-vocabularies/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/usestapel/stapel-vocabularies/issues
Keywords: django,stapel,vocabulary,taxonomy,catalog
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
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stapel-core<1.0,>=0.30.0
Requires-Dist: stapel-attributes<1.0,>=0.5
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-django; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: jsonschema>=4; extra == "test"
Requires-Dist: stapel-tools<1,>=0.9.1; 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-vocabularies

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

> Reference vocabularies too large to inline into a category's feature schema — 14 962 phone models, 107 049 car modifications — stored as levels, terms and parent/child edges rather than as paths, so one 'black' is shared by every model that comes in black and can be translated once and faceted on. Ships the public typeahead and cascade reads a listing composer needs (ETag'd on a revision, anonymous, no cookie), the two VocabularyResolver implementations stapel-attributes' ref_select / ref_hierarchical_select types validate values through (in-process and over comm), a batched transactional loader for reviewed fixtures that spends one revision and one event per file, and Django-free converters that turn a vendor's nested XML or CSV catalogue into such a fixture without loading the document into memory.

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-vocabularies
```

## At a glance

| Fact | Value |
|---|---|
| Version | `0.1.1` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 4 |
| Config axes | 1 |
| Usage surface | 13 |
| Extension points | 5 |
| Error codes | 45 |
| Fleet dependencies | [`stapel-attributes`](https://github.com/usestapel/stapel-attributes) · [`stapel-categories`](https://github.com/usestapel/stapel-categories) (optional) · [`stapel-core`](https://github.com/usestapel/stapel-core) |

## Documentation

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

## What this is

Some option lists are too big to be options. A phone catalogue has 14 962
models; a car catalogue has 107 049 modifications. Inlining those into a
category's feature schema is not a tuning question — the schema is fetched on
every form render, and it would be megabytes.

**stapel-vocabularies** is where they live instead.

- **Levels, terms and edges — not paths.** `Vendor → Model → MemorySize →
  Color` is 56 921 distinct paths in the Avito phone catalogue and only
  15 844 distinct terms. Storing the terms and the parent/child edges between
  them means one `Color=chernyy` shared by every model that comes in black:
  17 colours to translate instead of 56 921 path nodes, and a facet on a
  colour code that is answerable at all.
- **A read surface built for a typeahead.** `terms/?level=Model&parent=apple&q=pro`
  answers a page with `total` and `has_children`, prefix matches ranked first.
  Anonymous, `ETag`'d on the vocabulary's revision, `Cache-Control:
  public`, and no `Set-Cookie` — so the shared cache in front of it works and
  a crawler does not start a session per request.
- **Two resolvers, one protocol.** `ref_select` / `ref_hierarchical_select` in
  [stapel-attributes](https://github.com/usestapel/stapel-attributes) validate
  values through a `VocabularyResolver`. `OrmResolver` answers from these
  tables and is registered at startup; `CommResolver` answers the same
  questions over the bus, for a service that validates listings but holds no
  catalogues. Both cache `describe` **by revision**, so a re-imported
  catalogue stops validating against the levels it used to have the moment the
  import commits.
- **Loading is data plumbing, not an admin screen.** `manage.py
  load_vocabulary phones.json` is one transaction, one revision increment and
  one `vocabulary.changed` event for the whole file, whatever its size. The
  real Avito phone catalogue — 15 844 terms, 39 749 edges — loads in ~1.2 s.
- **Converters that do not read the file into memory.** A vendor's nested XML
  or a one-path-per-row CSV becomes a reviewable fixture, streamed through
  `iterparse`, with codes assigned deterministically (Cyrillic transliterated,
  collisions numbered in label sort order) so re-converting an unchanged
  catalogue produces an unchanged diff.

Alpha. See [MODULE.md](https://github.com/usestapel/stapel-vocabularies/blob/main/MODULE.md)
for the agent-facing map of seams.

## Quick start

```bash
pip install stapel-vocabularies
```

```python
# settings.py
INSTALLED_APPS = [..., "stapel_vocabularies"]

# urls.py
path("vocabularies/", include("stapel_vocabularies.urls"))   # -> /vocabularies/api/v1/...
```

```bash
python manage.py convert_vocabulary phone_catalog.xml \
    --slug avito-phones --name "Phones (Avito)" --out fixtures/avito-phones.json
python manage.py load_vocabulary fixtures/avito-phones.json --replace
```

A feature then points at it instead of carrying options:

```json
{"type": "ref_select", "optionsRef": {"vocabulary": "avito-phones",
                                      "level": "Model",
                                      "parentFeature": "vendor"}}
```

## API

| Method | Path | What |
|---|---|---|
| GET | `/vocabularies/api/v1/vocabularies/` | every vocabulary: `{slug, name, levels, term_count, revision}` |
| GET | `/vocabularies/api/v1/vocabularies/{slug}/` | one of them |
| GET | `/vocabularies/api/v1/vocabularies/{slug}/terms/` | `?level=` (required), `?parent=`, `?q=`, `?limit=` (≤200, default 50), `?offset=` → `{results: [{code, label, level, has_children}], total}` |
| GET | `/vocabularies/api/v1/vocabularies/{slug}/terms/resolve/` | `?level=&codes=a,b,c` (≤200) → `{code: label}`, unknown codes omitted |

`Accept-Language` selects a translated label where the term carries one; the
response `Vary`s on it and the `ETag` covers it.

## The fixture format

One file per vocabulary, byte-stable, reviewed as code
([schema](https://github.com/usestapel/stapel-vocabularies/blob/main/docs/vocabulary-fixture.schema.json)):

```json
{ "slug": "avito-phones", "name": "Phones (Avito)", "source": "https://…/phone_catalog.xml",
  "levels": [{"name": "Vendor"}, {"name": "Model", "parent": "Vendor"}],
  "terms": [["Vendor", "apple", "Apple", null], ["Model", "iphone-10", "iPhone 10", null]],
  "edges": [["Vendor", "apple", "Model", "iphone-10"]] }
```

A level's `parent` must be declared before it. That single rule is the whole
acyclicity argument: a level can only point backwards, so no chain of parents
can return to where it started.

## License

MIT — see [LICENSE](https://github.com/usestapel/stapel-vocabularies/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>
