Metadata-Version: 2.4
Name: stapel-attributes
Version: 0.4.7
Summary: Typed attributes engine for the Stapel framework
License: MIT
Keywords: django,stapel,attributes
Classifier: Framework :: Django
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stapel-core<1.0,>=0.10
Requires-Dist: djangorestframework>=3.14
Requires-Dist: djangorestframework-dataclasses>=1.2
Requires-Dist: drf-polymorphic<3,>=2.1
Requires-Dist: drf-spectacular>=0.27
Provides-Extra: all
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-attributes

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

> Typed attributes engine: an L1 library (no models, migrations, views, urls, comm surface or service identity of its own) providing a polymorphic type-plugin system (config/dto/dao/type per feature type) behind an open registry, nine built-in types, DTO/DAO validation and normalization, polymorphic DRF serializers with OpenAPI schemas, and a schema-driven (Lit 3) admin config editor. Imported directly by stapel-categories (feature schema) and stapel-listings (value validation).

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

## At a glance

| Fact | Value |
|---|---|
| Version | `0.4.7` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| Django | `djangorestframework>=3.14` |
| Config axes | 1 |
| Usage surface | 36 |
| Extension points | 4 |
| Fleet dependencies | [`stapel-core`](https://github.com/usestapel/stapel-core) |

## Documentation

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

## What this is

This is an **L1 library**, not a module: it ships no models, migrations,
views, urls or comm surface. Both `stapel-categories` (attribute schema) and
`stapel-listings` (attribute values) import it — the code both need
synchronously lives one layer down, like `stapel-core` itself.

Provenance: port of the `categories/feature_types` engine and the
`ads` validation pipeline from a legacy catalog app (see CHANGELOG for what
was fixed in transit).

## Quick start

No `INSTALLED_APPS` entry, no urls — just import it:

```python
from stapel_attributes import (
    FeatureDef,
    validate_dto,
    normalize_to_dao,
    validate_dto_structured,
)

configs = [
    FeatureDef(slug="mileage", config={"type": "int", "min": 0, "postfix": "km"}),
    FeatureDef(slug="condition", config={"type": "bool"}, mandatory=True),
]
payload = {"mileage": {"type": "int", "value": 120000},
           "condition": {"type": "bool", "value": True}}

validate_dto(configs, payload)              # raises ValidationError on failure
dao = normalize_to_dao(configs, payload)    # {"mileage": {"type": "int", "value": 120000, "postfix": "km", "order": 0}, ...}
result = validate_dto_structured(configs, payload)  # machine-readable batch result
```

## Built-in types

`int`, `float`, `string`, `bool`, `hex_color`, `select`, `date`, `header`,
`hierarchical_select`, `convertible_unit`. Each type is a plugin: a Config
dataclass (schema), a DTO (client input), a DAO (stored value + display
metadata) and a handler (`BaseFeatureType[TConfig, TDto, TDao]`).
`convertible_unit` stores values in a canonical base unit per family
(length/weight/area/volume/temperature) and converts to/from the user-facing
unit — see MODULE.md for the storage/conversion/range-filter contract.

Other marketplace-specific types (size grids, ...) are **not** shipped —
hosts register their own via the open registry (see MODULE.md for the worked
example).

## Settings

All configuration lives in the `STAPEL_ATTRIBUTES` namespace (dict setting,
flat setting, or env var — resolved lazily):

| Key | Default | Meaning |
|---|---|---|
| `EXTRA_TYPES` | `[]` | Dotted paths of extra feature types, **merged** over the built-ins (each entry: a `BaseFeatureType` subclass or a module that registers types on import). |

## Structured validation

Every validation failure carries a machine code end-to-end
(`ValidationErrorCode`) via `FeatureValidationError` — no message parsing.
Batch validators return `ValidationBatchResult` rows with `error`,
`ref_value`, `localizable_error` (an `error.400.feature_*` key) and `params`.

## Extension points

See [MODULE.md](https://github.com/usestapel/stapel-attributes/blob/main/MODULE.md) — the agent-facing map of every fork-free seam
(settings, the type registry, serializer factories, translation-key hooks).

## Admin UI

A schema-driven config editor (Lit 3 web components) ships with the package.
Each type declares its admin form in Python (`config_form()` → field-kinds); the
committed bundle renders it, so a new type needs zero JS for standard kinds. Use
`ConfigEditorWidget` on a config `JSONField`:

```python
from stapel_attributes import ConfigEditorWidget

class FeatureForm(forms.ModelForm):
    class Meta:
        widgets = {"config": ConfigEditorWidget()}
```

Two themes (`--stapel-*`, light + dark), en/ru locales (merge without fork via
`ADMIN_LOCALES`), and JS widget registries for exotic types. See MODULE.md
"Admin UI" and docs/done/attributes-admin-ui.md. Frontend source is in `static_src/`
(`npm test` / `npm run build`); the built bundle is committed — consumers need no
Node.

## Development

```bash
pip install -e . && pip install pytest pytest-django ruff
./setup-hooks.sh
pytest tests/
```

## License

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