Metadata-Version: 2.4
Name: vcti-enum
Version: 2.0.1
Summary: Value-generated string enums with automatic naming conventions for Python
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-enum
Project-URL: Changelog, https://github.com/vcollab/vcti-python-enum/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
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"
Requires-Dist: pydantic; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-enum

Value-generated string enums and a serialization bridge that keeps your
internal enums decoupled from the strings on the wire.

## Overview

`vcti-enum` does two things. First, it provides `StrEnum` base classes
whose member values are auto-generated from a naming convention —
declare the convention once (lowercase, camelCase, PascalCase, …) and
every value is derived from the member name, so nothing drifts when you
rename or add members.

Second, it separates the enum your program uses from the strings the
outside world sees. An `EnumCoder` bridges the two directions, and the
Pydantic helpers wire that bridge into a model field: internally the
enum can be an `IntEnum`, a `StrEnum`, or anything else, while the JSON,
config files, and API schema speak stable strings. Your internal
representation can change — renumber, rename, switch base class — and the
wire contract never moves.

The three pieces build on each other; use only the layer you need:

```text
Value-generated enums
        │
        ▼
     EnumCoder          ← the serialization bridge; works with any Enum
        │
        ▼
 Pydantic integration   ← optional
```

The package has **zero required dependencies**. Pydantic is optional
and only needed for the `FIELD` attribute and the `@enum_for_pydantic`
attachment to take effect.

## Contents

- [Installation](#installation)
- [Choosing an entry point](#choosing-an-entry-point)
- [Quick Start](#quick-start)
  - [Value-generated enums](#value-generated-enums)
  - [EnumCoder for serialization](#enumcoder-for-serialization)
  - [Pydantic integration](#pydantic-integration)
- [Enum base classes](#enum-base-classes)
- [Public API](#public-api)
- [Documentation](#documentation)

---

## Installation

```bash
pip install vcti-enum
```

### In `requirements.txt`

```
vcti-enum>=2.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-enum>=2.0.0",
]
```

---

## Choosing an entry point

The package has three entry points that build on each other. Pick by
what you need:

| You want to… | Use | Needs pydantic |
|---|---|---|
| Give an enum string values derived from the member names by a convention | a base class — [`EnumValueLowerCase`](#enum-base-classes) and friends | no |
| Encode/decode *any* enum to and from custom strings (serialize, parse) | [`EnumCoder`](#enumcoder-for-serialization) | no |
| A Pydantic field that holds the enum member but reads/writes strings on the wire | [`@enum_for_pydantic`](#pydantic-integration) + `MyEnum.FIELD` | yes |

The three-layer idea underneath all of this:

```mermaid
flowchart LR
    subgraph prog["Your program — internal"]
        member["Theme.DARK<br/>enum member · has .value"]
    end
    subgraph wire["JSON · config · API — external"]
        string["'dark'<br/>stable encoded string"]
    end
    member -->|"CODER.encode"| string
    string -->|"CODER.decode"| member
```

Your code works with the enum member; everyone else sees the string.
The encoded string is derived from the member *name*, not its value.

---

## Quick Start

### Value-generated enums

```python
from vcti.enum import EnumValueLowerCase, auto_enum_value

class FileFormat(EnumValueLowerCase):
    JSON = auto_enum_value()    # value: "json"
    CSV = auto_enum_value()     # value: "csv"
    PARQUET = auto_enum_value() # value: "parquet"

FileFormat.JSON.value       # "json"
FileFormat.JSON == "json"   # True (StrEnum members are str)
```

The value is derived from the member name by the base class's
convention, so renaming `JSON` to `JSON_LINES` updates its value to
`"json_lines"` automatically.

### EnumCoder for serialization

`EnumCoder` maps an enum to and from strings using any
`value_generator` you supply — independent of the enum's own `.value`.
It works with any Python `Enum`, not only the value-generated base
classes in this package:

```python
from enum import Enum
from vcti.enum import EnumCoder

class Color(Enum):
    RED = 1
    GREEN = 2

coder = EnumCoder(Color, value_generator=lambda e: e.name.lower(), default=Color.RED)

coder.encode(Color.RED)    # "red"
coder.decode("green")      # Color.GREEN
coder.default              # "red"
coder.list                 # ["red", "green"]
```

Both directions are precomputed at construction, so `encode`/`decode`
are O(1) lookups and two members mapping to the same string is rejected
immediately.

### Pydantic integration

`@enum_for_pydantic` attaches a ready-made, **member-bound** field
annotation, `MyEnum.FIELD`. Used in a model it validates against the
encoded strings, decodes them to the enum member on the way in, and
encodes back on the way out — so the model attribute is the real member
while the JSON and schema stay strings.

```python
from pydantic import BaseModel

from vcti.enum import EnumValueLowerCase, auto_enum_value, enum_for_pydantic

@enum_for_pydantic(default="JSON")
class FileFormat(EnumValueLowerCase):
    JSON = auto_enum_value()
    CSV = auto_enum_value()

class ExportConfig(BaseModel):
    format: FileFormat.FIELD          # type + default + description bundled

ExportConfig().format                          # FileFormat.JSON (the member)
ExportConfig(format="csv").format              # FileFormat.CSV  (decoded from "csv")
ExportConfig(format="csv").model_dump_json()   # '{"format":"csv"}'
```

If no `default` is given, the field is **required** — building the
model without it raises `ValidationError`.

`setup_enum_for_pydantic()` is the function form of the decorator; use
it when the enum is defined elsewhere — a stdlib `IntEnum`, a
third-party class — and you cannot decorate it in place.

The decoupling pays off most when the internal enum is *not* a string.
Here the server stores a theme as an integer while clients speak
`"light"` / `"dark"`:

```python
from enum import IntEnum
from pydantic import BaseModel

from vcti.enum import setup_enum_for_pydantic

class Theme(IntEnum):
    LIGHT = 1
    DARK = 2

setup_enum_for_pydantic(
    Theme,
    value_generator=lambda e: e.name.lower(),  # "LIGHT" -> "light"
    default_member="LIGHT",
)

class UiSettings(BaseModel):
    theme: Theme.FIELD

UiSettings().theme                          # Theme.LIGHT — an IntEnum member, .value == 1
UiSettings(theme="dark").theme              # Theme.DARK  — decoded from the wire string
UiSettings(theme="dark").model_dump_json()  # '{"theme":"dark"}'
```

Renumber `LIGHT = 100` tomorrow and nothing on the API side notices —
the encoded strings come from the member *name*, not its *value*.

---

## Enum base classes

| Class | Convention | `FIRST_VALUE` becomes |
|-------|-----------|----------------------|
| `EnumValueSameAsName` | Unchanged | `FIRST_VALUE` |
| `EnumValueLowerCase` | lower_case | `first_value` |
| `EnumValueCamelCase` | camelCase | `firstValue` |
| `EnumValuePascalCase` | PascalCase | `FirstValue` |
| `EnumValueCapitalizedPhrase` | Title Case | `First Value` |
| `EnumValueSpaceSeparatedLower` | space lower | `first value` |

All extend `StrEnum`, so members are string instances. Need another
convention? `create_enum_class()` builds a base from any
`str -> str` function — see the [Extension Guide](docs/extending.md).

---

## Public API

| Symbol | Import | Purpose |
|--------|--------|---------|
| `auto_enum_value()` | `vcti.enum` | Triggers automatic value generation |
| `create_enum_class()` | `vcti.enum` | Factory to create custom enum base classes |
| `EnumValueSameAsName` | `vcti.enum` | Base class: value = member name |
| `EnumValueLowerCase` | `vcti.enum` | Base class: value = lowercase |
| `EnumValueCamelCase` | `vcti.enum` | Base class: value = camelCase |
| `EnumValuePascalCase` | `vcti.enum` | Base class: value = PascalCase |
| `EnumValueCapitalizedPhrase` | `vcti.enum` | Base class: value = Title Case |
| `EnumValueSpaceSeparatedLower` | `vcti.enum` | Base class: value = space separated |
| `EnumCoder` | `vcti.enum` | Flexible enum serialization/deserialization |
| `setup_enum_for_pydantic()` | `vcti.enum` | Attach encoder attributes to an enum class |
| `enum_for_pydantic()` | `vcti.enum` | Decorator form of the setup function |
| `get_enum_field_description()` | `vcti.enum` | Build a field description string on its own |

### Attributes attached by `setup_enum_for_pydantic` / `@enum_for_pydantic`

| Attribute | Type | What it contains |
|---|---|---|
| `CODER` | `EnumCoder` | The encoder instance for this enum |
| `ENCODED_DEFAULT` | `str \| None` | Encoded string of the default member; `None` if the field is required |
| `ENCODED_LIST` | `list[str]` | All encoded strings, as a list |
| `ENCODED_TUPLE` | `tuple[str, ...]` | All encoded strings, as a tuple |
| `FIELD` | member-bound `Annotated[...]` | Model attribute is the enum member; wire is the encoded string. Required when no default |

> **Removed in 2.0:** the 1.x aliases `DEFAULT_VALUE`, `LIST`, `TUPLE`,
> and `LITERAL` are no longer attached. Use `ENCODED_DEFAULT`,
> `ENCODED_LIST`, `ENCODED_TUPLE`, and `FIELD` respectively.

---

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | [Quick Start](#quick-start) above |
| Understand the three-layer model and design decisions | [docs/design.md](docs/design.md) |
| Navigate and understand the source | [docs/source-guide.md](docs/source-guide.md) |
| Add a new enum base class or custom coder | [docs/extending.md](docs/extending.md) |
| Look up a specific function or type | [docs/api.md](docs/api.md) |
