Skip to main content

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.

CI PyPI version Python versions License: MIT Status: Alpha


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.


CLI

taxomesh ships with a command-line interface. After installation, the taxomesh command is available:

# taxomesh.toml (optional — place in project root)
[repository]
type = "json"
path = "data/taxonomy.json"
# Create a category
taxomesh category add --name "Music"

# Register an item (integer, string slug, or UUID external ID)
taxomesh item add --external-id 42
taxomesh item add --external-id "my-article-slug"

# Create a tag and assign it to an item
taxomesh tag add --name "live"
taxomesh item add-to-tag <item-uuid> --tag-id <tag-uuid>

# Show help for any command
taxomesh --help
taxomesh category --help
taxomesh item add --help

Override the config file path per-invocation:

taxomesh --config /etc/taxomesh.toml category list

The full Python API is documented below.


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 18 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, delete_tag
Tag ↔ Item association assign_tag, remove_tag
Category parent links save_category_parent_link, list_category_parent_links
Item → Category placement save_item_parent_link, list_item_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 18 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

taxomesh-0.1.0a3.tar.gz (217.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

taxomesh-0.1.0a3-py3-none-any.whl (23.2 kB view details)

Uploaded Python 3

File details

Details for the file taxomesh-0.1.0a3.tar.gz.

File metadata

  • Download URL: taxomesh-0.1.0a3.tar.gz
  • Upload date:
  • Size: 217.5 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

Hashes for taxomesh-0.1.0a3.tar.gz
Algorithm Hash digest
SHA256 899f75d95ae148a487677ea5cd44d458080a85452fd6191003f8b8c72fc47b74
MD5 ade286b8317816d0cbfd9d53164ac8d6
BLAKE2b-256 29e0e416d3af3011ec1aa3946831fa43b004f2c75d904b5fc1ab3588396aba02

See more details on using hashes here.

File details

Details for the file taxomesh-0.1.0a3-py3-none-any.whl.

File metadata

  • Download URL: taxomesh-0.1.0a3-py3-none-any.whl
  • Upload date:
  • Size: 23.2 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

Hashes for taxomesh-0.1.0a3-py3-none-any.whl
Algorithm Hash digest
SHA256 6cdbd47466967e7e951e2f932bb8e0c8d19809c2effa4be8c94e684f7f8f508f
MD5 872acebc88c6243e922a909499852df7
BLAKE2b-256 82d7a084447375366bd3af385b60c9773d963a7a28a34b2ae13791e759f7b8ad

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page