taxomesh
taxomesh gives a multi-parent category graph a Python stdlib-style interface: categories, items and tags take subscript, get, in, len and del as a dict does. The graph is stored in a YAML file, a JSON file, the Django ORM or a backend of your own.
What it is
taxomesh stores a taxonomy for records that your own system keeps, such as products, articles or
tracks. It is not a database for the records: each record stays in your system, and an item or a
category can stand for it through its external_id. taxomesh stores categories with any number of
parents, items placed in any number of categories, an order under each parent, tags, and typed
relations between items. It also searches items and categories. The taxomesh command, the Django
admin with its graph view, and the HTTP handlers, which need no web framework, call TaxomeshService.
Use it when the taxonomy needs its own component, for example for navigation that is not a strict tree, one item in several places, an order that depends on the place, or one set of rules for scripts, admin tools and APIs. It is not needed for a flat list of choices, a tree in which each category has one parent, a small fixed part of one screen, or a taxonomy with no need for reusable validation, traversal or integrations.
Installation
Requires Python 3.13+.
pip install taxomesh # the library
pip install "taxomesh[cli]" # with the taxomesh command (typer, rich)
pip install "taxomesh[django]" # with the Django ORM backend and admin
Quick start
from pathlib import Path
from taxomesh import TaxomeshService
from taxomesh.repositories import YamlRepository
Path("quickstart.yaml").unlink(missing_ok=True) # each run starts with an empty store
svc = TaxomeshService(YamlRepository("quickstart.yaml"))
# Categories form a graph: Jazz has two parents, Music and Genres.
music = svc.categories.create(name="Music")
genres = svc.categories.create(name="Genres")
jazz = svc.categories.create(name="Jazz")
svc.categories.add_parent(jazz, music)
svc.categories.add_parent(jazz, genres)
# An item stands for a record of yours, and is found again by its external id.
album = svc.items.create(name="Kind of Blue", external_id="catalog:42")
svc.items.place_in(album, jazz)
featured = svc.tags.create(name="featured")
svc.items.tag(album, featured)
# Read it with the forms a dict has: a row, or its UUID, is the key.
assert svc.items.get_by_external_id("catalog:42").item_id == album.item_id
assert svc.items[album.item_id].name == "Kind of Blue" # subscript raises when absent
assert svc.items.get_by_slug("no-such-slug") is None # get* never raises
assert jazz in svc.categories and len(svc.categories) == 3
assert [c.name for c in svc.categories.list(item=album)] == ["Jazz"]
assert [i.name for i in svc.items.list(tag=featured)] == ["Kind of Blue"]
assert [c.name for c in svc.categories.roots()] == ["Music", "Genres"] # in the order they were made
node = svc.graph()[jazz] # a read-only snapshot of the whole taxonomy
assert sorted(parent.category.name for parent in node.parents) == ["Genres", "Music"]
With no argument, TaxomeshService() reads taxomesh.toml in the working directory, and without one
keeps its store in data/taxomesh.yaml, between runs.
The forms
coll is svc.categories, svc.items or svc.tags, and a key is a row or its UUID. Tags have
no slug, external id or search:
| Form | Returns | On a miss |
|---|---|---|
coll[key] |
the row | raises Taxomesh<X>NotFoundError, a KeyError |
coll.get(key, default=None) |
the row | default |
key in coll |
bool |
False |
len(coll) |
int |
0 |
del coll[key] |
None |
raises |
coll.get_by_slug(s), coll.get_by_external_id(x) |
the row | None |
coll.get_many(keys) |
a new dict |
the key is left out |
coll.list(...), coll.search(...) |
a tuple |
(); a filter naming an entity that is not stored raises |
The collections do not iterate (why):
coll.list(...) enumerates, and len() is the one form that reads every row.
What it does
- Multi-parent categories: a category has any number of parents, and a cycle raises
TaxomeshCyclicDependencyError. - An order under each parent: every parent link and placement carries its own
sort_index, whichreorderandmovechange. - External ids:
str,intorUUID, stored as text and unique within its kind, and looked up one at a time or in bulk. - Tags and typed relations: free-form tags, and directed item-to-item relations (
covers,version_of) read in either direction, for many items at once in two storage reads. - Fuzzy search: ranked, typo-tolerant and accent-insensitive, on names and slugs; an external id matches as a substring only. It runs in the process, with rapidfuzz, and needs no search server.
- Graph snapshots:
svc.graph()holds one node per category, however many paths reach it. - Frozen rows:
updatereturns a new row, andexpected_versionmakes it conditional. - A cache per service: reads are cached for
cache_ttlseconds, 5 by default, and a write clears its own service's cache only. - Storage: YAML (the default), JSON or the Django ORM, chosen in code or in a
taxomesh.toml, or your own backend implementingTaxomeshRepositoryBase. One contract suite holds every shipped backend to that port. - Errors: a member raises a
TaxomeshError, a not-found one also aKeyErrorand a validation one also aValueError, or aTypeErrorfor an argument of the wrong type, except that the flagsrecursive,fuzzyandinclude_itemsare read asif flag:. Log records go to thetaxomeshlogger, which has only aNullHandlerof its own.
Documentation
The whole Python API is on one page (llms.txt), so an AI coding agent can use taxomesh without reading its source.
| Page | Covers |
|---|---|
| Python API | Every member, with runnable examples |
| Repositories | The three backends, and writing your own |
| Configuration | taxomesh.toml |
| CLI reference | Every command and global option |
| Django integration | The app, the admin, and bridging your models |
| HTTP API integration | Request schemas, handlers, serializers and error mapping |
| Design | The architecture and the decisions behind the surface, for contributors |
| Changelog | What each release changed |
Stability and versioning
taxomesh uses Semantic Versioning: what taxomesh, taxomesh.repositories,
taxomesh.contrib.api and taxomesh.contrib.django export, and the repository port, change
incompatibly only in a major release. The changelog
names every breaking change.
taxomesh supports Python 3.13 and 3.14, and Django 6.x. CI checks every change to main with
ruff, mypy --strict over the package and its tests, the Django integration included, and the full
test suite with at least 80% line coverage, a measure that leaves out the Django integration, which
its own tests exercise.
Used in production
taxomesh is used in production at LetrasTango to organise the categories and relations of its tango content.
Development
uv sync --all-extras
uv run ruff check .
uv run ruff format --check .
uv run mypy --strict .
uv run pytest
Contributions are welcome: see CONTRIBUTING.md for the setup and the quality gates. Licensed under MIT.
Metadata
Release files for taxomesh 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| taxomesh-1.0.0.tar.gz | 589.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| taxomesh-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 779.9 kB
Release files / taxomesh-1.0.0.tar.gz
| Download URL | taxomesh-1.0.0.tar.gz |
|---|---|
| Size | 589.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f5dd20cd4e821a1f6ca75238a86ead5fedfb4ca7b337c4feb30d58b919473d07
|
|
BLAKE2b-256 checksum How to use checksums |
fdbf4d08b89554e75d1d80082342098446a5d95a290a447bf2da45a1134b22ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / taxomesh-1.0.0-py3-none-any.whl
| Download URL | taxomesh-1.0.0-py3-none-any.whl |
|---|---|
| Size | 190.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cda5423058ac5d072068be9c2d40e721bbced8208797e382afe6387ba9538557
|
|
BLAKE2b-256 checksum How to use checksums |
227714fff82577b0c3e8a2691f30653c38c853c924b863e3b9b46a8831c8f9b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|