Metadata-Version: 2.4
Name: vcti-properties
Version: 1.0.0
Summary: Sparse, scoped, multi-level property store with pluggable key mapping
Author: Visual Collaboration Technologies Inc.
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-properties

Sparse, scoped, multi-level property store with pluggable key mapping.

## Overview

`vcti-properties` stores sparse **named properties** attached to entities or
collections, with optional **layered resolution** — a per-entity value that
falls back to a shared default. It spans the whole range from fixed **metadata**
(units, components, a creator — authoritative, rarely overridden) to overridable
**properties** (a plot's color, opacity, visibility — set as chart-wide defaults
and overridden per series). Both are just values addressed by a structured key.

Rather than a tangle of nested dictionaries, every property lives in one sparse
store keyed by a flat string, and three concerns are kept cleanly separated:

- **Authority** — application-defined (**system**) properties vs. user-supplied
  ones, held in *separate namespaces* so they never collide. System names are
  drawn from a controlled vocabulary; user names are free-form.
- **Locus** — what a property is bound to: a specific **entity** (one column,
  element, node), or the whole collection.
- **Resolution** — a per-entity lookup can fall back to a shared default, so one
  value stands in for every entity that has not set its own.

Nothing here is dataset-specific: any set of entities — form fields, scene
objects, config sections — each carrying its own properties over shared
defaults, with system settings kept apart from user settings, fits the same
shape. The **levels** you declare encode the `(authority, locus, name)` parts
into one flat string key, so the store stays sparse (unset combinations cost
nothing) and lookups stay O(1). Pure standard library, zero runtime
dependencies.

### Why not nested dicts or `ChainMap`?

Nested dictionaries — one level per authority, per entity, per name — mean a lot
of structure for little data, plus hand-rolled lookup repeated at every call
site. `ChainMap` gives you fallthrough for free, but you would need one dict per
(authority × locus) combination and must reassemble the chain per entity — the
layer count explodes with the number of entities. This package keeps the
unbounded axis (locus) in a flat key over one sparse dict, and reserves layered
fallthrough for the small, bounded axis where it belongs (see
[Design & Concepts](docs/design.md)).

## Installation

```bash
pip install vcti-properties
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-properties>=1.0.0",
]
```

---

## Quick Start

A store is defined by an ordered list of **levels** passed straight to
`PropertyStore` (it builds the key encoder for you). Each level is one part of
the key; the **last** level is the property name, and the **value** is stored
separately against the assembled key.

Here's a chart whose series are styled with **chart-wide defaults that any series
can override** — exactly the overridable kind of value that is a *property*
(unlike fixed metadata such as units or a creator):

```python
from enum import StrEnum

from vcti.properties import PropertyStore, Level

class Style(StrEnum):                     # the plot properties we recognise
    COLOR = "color"
    OPACITY = "opacity"
    VISIBLE = "visible"

chart = PropertyStore(
    Level("series", sentinel="@"),        # a specific series, or chart-wide (@)
    Level("style", vocabulary=Style),     # the property name
)

# Chart-wide defaults — apply to every series unless overridden:
chart.set(style=Style.COLOR, value="steelblue")     # -> "@.color"
chart.set(style=Style.OPACITY, value=1.0)
chart.set(style=Style.VISIBLE, value=True)

# Per-series overrides:
chart.set(series="revenue", style=Style.COLOR, value="crimson")
chart.set(series="forecast", style=Style.OPACITY, value=0.4)

# resolve() = the series' own value, else the chart-wide default:
chart.resolve(series="revenue",  style=Style.COLOR)    # "crimson"    (overridden)
chart.resolve(series="forecast", style=Style.COLOR)    # "steelblue"  (chart-wide default)
chart.resolve(series="forecast", style=Style.OPACITY)  # 0.4          (overridden)
chart.get(series="forecast", style=Style.COLOR)        # None         (exact read, no fallback)
```

Add a system/user **scope** as one more level when the application's own defaults
must stay separate from a user's edits — `system.*` and `user.*` are distinct
namespaces that never collide (each resolves within itself):

```python
from vcti.properties import PropertyStore, KeyScope, Level

chart = PropertyStore(
    Level("scope", vocabulary=KeyScope),   # application theme (system) vs. user edits (user)
    Level("series", sentinel="@"),
    Level("name"),
)

chart.set(scope=KeyScope.SYSTEM, name="color", value="steelblue")                 # "system.@.color"
chart.set(scope=KeyScope.USER, series="revenue", name="color", value="crimson")   # "user.revenue.color"
```

With no levels, the store uses dot-joined paths (`DefaultKeyMapper`):

```python
from vcti.properties import PropertyStore

store = PropertyStore()                                   # or PropertyStore(separator="/")
store.set("user", "profile", value={"name": "Alice"})     # key "user.profile"
store.get("user", "profile")                              # {"name": "Alice"}
```

---

## Key mappers

Passing levels to `PropertyStore` builds a `KeyMapper` — the codec that encodes
the `(authority, locus, name)` parts into a flat key and back. It's pluggable, so
one store serves every layout:

| Mapper | What it does |
|--------|--------------|
| `DefaultKeyMapper` | Unstructured positional parts joined by a separator (`a.b.c`) — the default when no levels are given |
| `LeveledKeyMapper` | Structured named levels — built from the `Level`s you pass to `PropertyStore` |

Scenario-specific layouts (a system/user scoped key, a single-level config key)
are *configurations* of levels, not separate classes. For a bespoke scheme, pass
`key_mapper=` any object implementing `parts_to_key(...)` and `key_to_parts(key)`.
See [Extending](docs/extending.md).

---

## Dependencies

None. Standard library only.
