stapel-vocabularies
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 — composable Django apps that deploy as a monolith or as microservices without changing module code.
Install
pip install stapel-vocabularies
At a glance
| Fact | Value |
|---|---|
| Version | 0.2.1 |
| Python | >=3.11 (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 4 |
| Config axes | 1 |
| Usage surface | 13 |
| Extension points | 6 |
| Error codes | 45 |
| Fleet dependencies | stapel-attributes · stapel-categories (optional) · stapel-core |
Documentation
OpenAPI · capabilities.json · llms.txt (for agents)
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 → Coloris 56 921 distinct paths in a real phone catalogue and only 15 844 distinct terms. Storing the terms and the parent/child edges between them means oneColor=chernyyshared 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=proanswers a page withtotalandhas_children, prefix matches ranked first. Anonymous,ETag'd on the vocabulary's revision,Cache-Control: public, and noSet-Cookie— so the shared cache in front of it works and a crawler does not start a session per request. - A popular band, so a dictionary does not open on the alphabet. A
529-vendor level sorted by name opens on
3Q, 4Good, 8848, A1, Acelinewhile the brands carrying the volume sit hundreds of rows down. Push the listing counts you already have —vocabularies.set_popularity— and the top twelve lead every page, withbandon each row andpopular_counton the page so a control draws its separator without guessing. Idempotent: unchanged counts write nothing and invalidate nothing. - A match that can say no.
vocabularies.matchturns one free-text guess into one code with a score — 1.0 for an exact or transliterated hit, 0.9 for a unique prefix, and for a similarity hit the number the vector layer actually reported. BelowMATCH_MIN_SCOREit answers{"matched": false}. That is the difference between a typeahead, where a person picks one of five rows, and a composer, which writes the answer into a listing unread. - Two resolvers, one protocol.
ref_select/ref_hierarchical_selectin stapel-attributes validate values through aVocabularyResolver.OrmResolveranswers from these tables and is registered at startup;CommResolveranswers the same questions over the bus, for a service that validates listings but holds no catalogues. Both cachedescribeby 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.jsonis one transaction, one revision increment and onevocabulary.changedevent for the whole file, whatever its size. The real 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 for the agent-facing map of seams.
Quick start
pip install stapel-vocabularies
# settings.py
INSTALLED_APPS = [..., "stapel_vocabularies"]
# urls.py
path("vocabularies/", include("stapel_vocabularies.urls")) # -> /vocabularies/api/v1/...
python manage.py convert_vocabulary phone_catalog.xml \
--slug phone-models --name "Phone models" --out fixtures/phone-models.json
python manage.py load_vocabulary fixtures/phone-models.json --replace
A feature then points at it instead of carrying options:
{"type": "ref_select", "optionsRef": {"vocabulary": "phone-models",
"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, band}], total, popular_count} |
| 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 Varys on it and the ETag covers it.
Rows lead with the popular band, then the level's own rank, then the
alphabet — except under ?q=, where a prefix match still outranks
everything, because that is what a typeahead is. popular_count is how many
LEADING rows are in the band, so the separator goes after index
popular_count - 1; 0 means this page has no band.
Asking over the bus
from stapel_core.comm import call
call("vocabularies.match", {"vocabulary": "phone-models", "level": "Vendor",
"text": "Самсунг"})
# -> {"matched": True, "code": "samsung", "label": "Samsung",
# "score": 1.0, "method": "exact"}
call("vocabularies.match", {"vocabulary": "phone-models", "level": "Vendor",
"text": "айфон"})
# -> {"matched": False, "reason": "no_confident_match"}
call("vocabularies.set_popularity", {"vocabulary": "phone-models", "level": "Vendor",
"counts": {"samsung": 41233, "apple": 38902}})
# -> {"ranked": 2, "revision": 8}
The fixture format
One file per vocabulary, byte-stable, reviewed as code (schema):
{ "slug": "phone-models", "name": "Phone models", "source": "https://…/phone_catalog.xml",
"levels": [{"name": "Vendor"}, {"name": "Model", "parent": "Vendor"}],
"terms": [["Vendor", "apple", "Apple", null, 0, 90], ["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.
A term row is [level, code, label, external_id, sort?, popularity?]. sort
ranks within a band; popularity says which band. Both are optional, and a
row that omits popularity leaves whatever the live term holds — so a
catalogue re-import never erases a band pushed from observed counts.
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_vocabularies-0.2.1.tar.gz.
File metadata
- Download URL: stapel_vocabularies-0.2.1.tar.gz
- Upload date:
- Size: 104.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20a4f61c8ec83f433f481817fc67e7adf9d07044579ee70561690a159e3a17bc
|
|
| MD5 |
8d54a403b0e45885e86172385118b00e
|
|
| BLAKE2b-256 |
6163f0ed692c21674c1765b6a2773651e8874a5ceac3c9ce36d197b956f939aa
|
Provenance
The following attestation bundles were made for stapel_vocabularies-0.2.1.tar.gz:
Publisher:
publish.yml on usestapel/stapel-vocabularies
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_vocabularies-0.2.1.tar.gz -
Subject digest:
20a4f61c8ec83f433f481817fc67e7adf9d07044579ee70561690a159e3a17bc - Sigstore transparency entry: 2732694238
- Sigstore integration time:
-
Permalink:
usestapel/stapel-vocabularies@08f8ed981c61de5ecb50d8d384fd3834bcc5650c -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@08f8ed981c61de5ecb50d8d384fd3834bcc5650c -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_vocabularies-0.2.1-py3-none-any.whl.
File metadata
- Download URL: stapel_vocabularies-0.2.1-py3-none-any.whl
- Upload date:
- Size: 90.3 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 |
2a25edb5a7c8c07eb2fb40efd623eddb1cde773dc4695daefff126a7e68e05d8
|
|
| MD5 |
58dba29a774d2c75a6ef29ce0cd4a802
|
|
| BLAKE2b-256 |
f83abdde354424110802eee5775696502ccd41031503d4b2613c76bfa569a8c4
|
Provenance
The following attestation bundles were made for stapel_vocabularies-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-vocabularies
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_vocabularies-0.2.1-py3-none-any.whl -
Subject digest:
2a25edb5a7c8c07eb2fb40efd623eddb1cde773dc4695daefff126a7e68e05d8 - Sigstore transparency entry: 2732694278
- Sigstore integration time:
-
Permalink:
usestapel/stapel-vocabularies@08f8ed981c61de5ecb50d8d384fd3834bcc5650c -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@08f8ed981c61de5ecb50d8d384fd3834bcc5650c -
Trigger Event:
push
-
Statement type: