Flexible taxonomy management for generic items — categories, tags, and multi-parent hierarchies with pluggable storage.
Project description
taxomesh
Flexible taxonomy management for generic items — categories, tags, and multi-parent hierarchies with pluggable storage.
What is taxomesh?
taxomesh is a Python library for organizing arbitrary items into flexible taxonomies. An "item" is any entity identified by a UUID, integer, or string — a product, a document, a user, a media file — anything. taxomesh doesn't care what your items are; it just manages how they are categorized and tagged.
Key concepts
| Concept | Description |
|---|---|
| Item | A generic reference (UUID / int / str) to any external entity |
| Category | A named node in a taxonomy graph |
| Tag | A free-form label attached to an item |
| Multi-parent hierarchy | A category can belong to multiple parent categories simultaneously |
| Sort index | A category's position within each parent is independent — "Tango" can be rank 1 under "Argentina" and rank 5 under "World Music Genres" |
| Repository | A pluggable backend that stores all of the above |
Multi-parent categories with per-parent sort index
Unlike traditional single-parent trees, taxomesh models categories as a directed acyclic graph (DAG). The relationship between a category and each of its parents carries an independent sort_index, stored in a dedicated junction record:
(category_id, parent_category_id, sort_index)
This lets the same category appear at different positions depending on which parent context is being browsed. Cyclic dependencies are detected and rejected at write time.
Features
- Generic item references (UUID, int, or str)
- Categories organized as a DAG (directed acyclic graph)
- Per-parent sort index for categories
- Cycle detection in category hierarchies
- Free-form tags on items with idempotent assign/remove
- Pluggable repository interface (
TaxomeshRepositoryBase) — no inheritance required - Built-in JSON repository backend with atomic writes
- Typed exception hierarchy for precise error handling
- YAML file backend (planned)
- SQLite3 backend (planned)
- Query/search capabilities (planned)
Installation
pip install taxomesh
Requires Python 3.11 or later. No extra dependencies needed for the default JSON backend.
Quick start
Default storage (JSON file in current directory)
from taxomesh import TaxomeshService
service = TaxomeshService() # persists to taxomesh.json
Custom storage path
from pathlib import Path
from taxomesh import TaxomeshService
from taxomesh.adapters.repositories.json_repository import JsonRepository
service = TaxomeshService(repository=JsonRepository(Path("/data/my_taxonomy.json")))
Managing categories
from taxomesh import TaxomeshService, TaxomeshCategoryNotFoundError
service = TaxomeshService()
# Create
music = service.create_category(name="Music")
jazz = service.create_category(name="Jazz", description="Improvisational genre.")
print(music.category_id) # UUID assigned by the library
# Retrieve
same = service.get_category(music.category_id)
assert same.name == "Music"
# List
all_cats = service.list_categories()
# Delete
service.delete_category(jazz.category_id)
# Missing entity raises a typed error — never returns None
try:
service.get_category(jazz.category_id)
except TaxomeshCategoryNotFoundError as e:
print(e)
Category parent relationships (DAG)
animals = service.create_category(name="Animals")
mammals = service.create_category(name="Mammals")
dogs = service.create_category(name="Dogs")
service.add_category_parent(mammals.category_id, animals.category_id)
service.add_category_parent(dogs.category_id, mammals.category_id)
# Cycle detection — raises TaxomeshCyclicDependencyError
from taxomesh import TaxomeshCyclicDependencyError
try:
service.add_category_parent(animals.category_id, dogs.category_id)
except TaxomeshCyclicDependencyError:
print("cycle rejected")
Managing items
from uuid import uuid4
from taxomesh import TaxomeshService
service = TaxomeshService()
# External ID can be UUID, int, or string slug
song = service.create_item(external_id=42)
article = service.create_item(external_id="how-to-brew-coffee")
product = service.create_item(external_id=uuid4())
print(song.item_id) # library-assigned internal UUID
print(song.external_id) # 42
# Retrieve by internal UUID
same = service.get_item(song.item_id)
all_items = service.list_items()
service.delete_item(song.item_id)
Managing tags
from taxomesh import TaxomeshService
service = TaxomeshService()
live_tag = service.create_tag(name="live")
song = service.create_item(external_id=99)
# Assign — idempotent, safe to call multiple times
service.assign_tag(tag_id=live_tag.tag_id, item_id=song.item_id)
service.assign_tag(tag_id=live_tag.tag_id, item_id=song.item_id) # no-op
# Remove — no-op if association already absent
service.remove_tag(tag_id=live_tag.tag_id, item_id=song.item_id)
Persistence across restarts
from pathlib import Path
from taxomesh import TaxomeshService
from taxomesh.adapters.repositories.json_repository import JsonRepository
DB = Path("my_taxonomy.json")
# First run — write data
s1 = TaxomeshService(repository=JsonRepository(DB))
cat = s1.create_category(name="Electronic")
# Later run — data survives process restart
s2 = TaxomeshService(repository=JsonRepository(DB))
same = s2.get_category(cat.category_id)
assert same.name == "Electronic"
Architecture overview
┌─────────────────────────────────────────────────────┐
│ Public import surface (taxomesh) │
│ TaxomeshService · exception classes │
└───────────────────┬─────────────────────────────────┘
│ delegates all I/O
┌───────────────────▼─────────────────────────────────┐
│ Ports (taxomesh.ports.repository) │
│ TaxomeshRepositoryBase ← typing.Protocol │
└───────────────────┬─────────────────────────────────┘
│ satisfied by
┌───────────────────▼─────────────────────────────────┐
│ Adapters (taxomesh.adapters.repositories) │
│ JsonRepository (default, atomic writes) │
│ … future: YamlRepository, SqliteRepository … │
└─────────────────────────────────────────────────────┘
TaxomeshService is the sole public entry point. It holds no storage logic and delegates every read and write to the repository backend. Any object that structurally satisfies TaxomeshRepositoryBase can be used — no inheritance required.
Repository interface
TaxomeshRepositoryBase is a typing.Protocol with 15 methods:
| Group | Methods |
|---|---|
| Category CRUD | save_category, get_category, list_categories, delete_category |
| Item CRUD | save_item, get_item, list_items, delete_item |
| Tag CRUD | save_tag, get_tag, list_tags |
| Tag ↔ Item association | assign_tag, remove_tag |
| Category parent links | save_category_parent_link, list_category_parent_links |
Import path for advanced use (e.g., type annotations on a custom backend):
from taxomesh.ports.repository import TaxomeshRepositoryBase
Plugging in a custom backend
No inheritance from TaxomeshRepositoryBase is required. Implement all 15 methods and pass the instance at construction time:
from taxomesh import TaxomeshService
service = TaxomeshService(repository=MyCustomBackend())
Domain models
taxomesh defines its domain model classes in taxomesh/domain/models.py:
| Class | Description |
|---|---|
Item |
A generic reference to any external entity, identified by an auto-generated UUID (item_id) and a user-supplied external_id (UUID, str, or int) |
Category |
A named node in the taxonomy DAG, with an optional description and metadata |
Tag |
A short free-form label (max 25 chars) that can be attached to items |
CategoryParentLink |
Junction record linking a category to one of its parent categories, with an independent sort_index |
ItemParentLink |
Junction record placing an item under a category, with a sort index |
ItemTagLink |
Junction record associating a tag with an item |
All models are pydantic.BaseModel subclasses with populate_by_name=True and validate_assignment=True. Every direct str field carries an explicit max_length constraint.
Error handling
All errors raised by taxomesh inherit from TaxomeshError:
TaxomeshError ← catch-all for any taxomesh error
├── TaxomeshNotFoundError ← any entity not found
│ ├── TaxomeshCategoryNotFoundError
│ ├── TaxomeshItemNotFoundError
│ └── TaxomeshTagNotFoundError
├── TaxomeshValidationError ← domain constraint violation
│ └── TaxomeshCyclicDependencyError ← DAG cycle in add_category_parent
└── TaxomeshRepositoryError ← storage I/O / parse failure
All names are importable from the top-level taxomesh package:
from taxomesh import (
TaxomeshService,
TaxomeshError,
TaxomeshNotFoundError,
TaxomeshCategoryNotFoundError,
TaxomeshItemNotFoundError,
TaxomeshTagNotFoundError,
TaxomeshValidationError,
TaxomeshCyclicDependencyError,
TaxomeshRepositoryError,
)
The service never returns None for a missing entity. Every not-found condition raises a specific, catchable typed error.
Roadmap
- v0.1 — Core models, service facade,
TaxomeshRepositoryBase, JSON backend, DAG cycle detection (in progress) - v0.2 — YAML and SQLite3 backends, bulk operations, filtering and querying
- v0.3 — Async repository interface, additional backends (PostgreSQL, MongoDB)
- v1.0 — Stable API, full test coverage, documentation site
Spec-driven development
This project is built using spec-driven development. Every feature begins as a written specification before any code is touched. See specs/ for published specifications.
Contributing
Contributions are welcome. Please open an issue to discuss any change before submitting a pull request. This project follows a spec-first workflow — implementation PRs without a corresponding spec will not be merged.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
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 taxomesh-0.1.0a2.tar.gz.
File metadata
- Download URL: taxomesh-0.1.0a2.tar.gz
- Upload date:
- Size: 161.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49c9e3c8f8a6739f62c8abe4f15a93338dc4cc4888100fa43436e162b9c08535
|
|
| MD5 |
8f15642aa6076a41422800429c1f5522
|
|
| BLAKE2b-256 |
ad84b423fa0e77e3a651d749ce8ffe54e6f26fed8d89937c8baa4883f348ad84
|
File details
Details for the file taxomesh-0.1.0a2-py3-none-any.whl.
File metadata
- Download URL: taxomesh-0.1.0a2-py3-none-any.whl
- Upload date:
- Size: 16.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.4 {"installer":{"name":"uv","version":"0.10.4","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77b31971b9835a2ef293f9cbcc70b9d9e46ee6e33f17c871d7dfb5804218b9f4
|
|
| MD5 |
68acdb522594388cfb7d83dc76b17448
|
|
| BLAKE2b-256 |
e01e850588c816313bef27931845fd229e45878c2ae4592e2f64474dfc1976f1
|