corbel


Namecorbel JSON
Version 0.1.2 PyPI version JSON
download
home_pagehttps://github.com/bnlucas/corbel
SummaryLightweight Python library extending dataclasses with serialization, deserialization, validation, and mixins for comparison, hashing, and immutable updates.
upload_time2025-09-10 09:10:01
maintainerNone
docs_urlNone
authorNathan Lucas
requires_python<4.0,>=3.10
licenseMIT
keywords dataclass serialization deserialization validation mixin python
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            # Corbel

Corbel is a Python dataclass extension library providing **mixins and utilities** for:

- Validation
- Comparison and hashing
- Copying and updating
- Serialization to/from dicts and JSON
- Property-level metadata (`@corbel_property`)
- Protocol-based hooks for validation, serialization, and deserialization

---

## Installation

```base
pip install corbel
```

---

## Core Mixins

### 1. Corbel

The base mixin that provides:

- Cached `asdict` results
- Field and property introspection
- Hook methods for updates and validation

```python
from dataclasses import dataclass
from corbel import Corbel, field

@dataclass
class Base(Corbel):
    x: int = field()
    y: str = field()

inst = Base(1, "hello")
print(inst.asdict())  # {'x': 1, 'y': 'hello'}
```

---

### 2. Serializable

Provides `to_dict`, `from_dict`, `to_json`, `from_json` for dataclasses.

- Supports nested dataclasses
- Optional wrapper key for JSON (`__json_wrapper__`)
- Configurable inclusion rules (`__inclusion__`)

```python
from dataclasses import dataclass
from corbel import Serializable, field, Include

@dataclass
class User(Serializable):
    id: int = field()
    name: str = field()
    email: str | None = field(default=None)

user = User(1, "Alice")
print(user.to_dict())  # {'id': 1, 'name': 'Alice', 'email': None}
alice = User.from_dict({"id": 1, "name": "Alice", "email": None})

# JSON with wrapper
print(user.to_json(wrapper="user"))  # {"user": {"id":1,"name":"Alice","email":null}}
alice = User.from_json('{"user": {"id":1,"name":"Alice","email":null}', wrapper="user")

# Custom class-level JSON wrapper
@dataclass
class WrappedUser(Serializable):
    __json_wrapper__ = "account"
    id: int = field()
    name: str = field()

u = WrappedUser(5, "Bob")
print(u.to_json())  # {"account": {"id":5,"name":"Bob"}}

# Using inclusion rules
@dataclass
class PartialUser(Serializable):
    __inclusion__ = Include.NON_NONE
    id: int = field()
    name: str = field()
    email: str | None = field(default=None)

pu = PartialUser(1, "Alice")
print(pu.to_dict())  # {'id': 1, 'name': 'Alice'}  # email omitted
```

---

### 3. Updatable

Provides immutable-style updates:

- `copy()`: shallow copy
- `update(**kwargs)`: returns a new instance with updated fields
- `batch_update()`: context manager to temporarily disable validation

```python
from dataclasses import dataclass
from corbel import Updatable, field

@dataclass
class Point(Updatable):
    x: int = field()
    y: int = field()

p1 = Point(1, 2)
p2 = p1.update(x=10)  # new instance
print(p1.asdict())    # {'x': 1, 'y': 2}
print(p2.asdict())    # {'x': 10, 'y': 2}

# batch update
with p2.batch_update() as temp:
    temp.x = 20
    temp.y = 30
print(p2.asdict())  # {'x': 10, 'y': 2}, p2 unchanged
```

---

### 4. Validated

Automatically validates fields on initialization and update:

- Define a `validator` in `field()` metadata
- Supports `allow_none=True`
- Raises `ValidationError` on failure

```python
from dataclasses import dataclass
from corbel import Validated, field, ValidationError

def positive(value: int) -> bool:
    return value > 0

@dataclass
class BankAccount(Validated):
    balance: int = field(validator=positive)

try:
    acct = BankAccount(-10)  # raises ValidationError
except ValidationError as e:
    print(e)

acct = BankAccount(100)
acct.balance = -50  # raises ValidationError
```

---

### 5. Hashable

Caches a hash based on dataclass fields:

- Automatically invalidates on field update
- Suitable for dict keys and set members

```python
from dataclasses import dataclass
from corbel import Hashable, field

@dataclass
class Coord(Hashable):
    x: int = field()
    y: int = field()

c1 = Coord(1, 2)
c2 = Coord(1, 2)

print(hash(c1) == hash(c2))  # True
c1.x = 3
print(hash(c1) == hash(c2))  # False
```

---

### 6. Comparable

Provides `<`, `<=`, `>`, `>=`, `==` based on field values:

- Lexicographic comparison of fields
- Supports total ordering

```python
from dataclasses import dataclass
from corbel import Comparable, field

@dataclass
class Version(Comparable):
    major: int = field()
    minor: int = field()

v1 = Version(1, 0)
v2 = Version(1, 1)
print(v1 < v2)  # True
print(v1 == v2) # False
```

---

### 7. @corbel_property

Custom property decorator supporting:

- `validator`
- `serializer` / `deserializer`
- `allow_none` / `ignore`

```python
from dataclasses import dataclass
from corbel import Corbel, corbel_property, field

def positive(x: int) -> bool:
    return x > 0

@dataclass
class Example(Corbel):
    _value: int = field()

    @corbel_property(validator=lambda v: positive(v))
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, val: int) -> None:
        self._value = val

ex = Example(5)
print(ex.value)  # 5
ex.value = 10    # OK
# ex.value = -1  # Raises ValueError
```

---

### 8. Protocol Examples

#### ValidatorProtocol

```python
from typing import Any

def positive_validator(value: int) -> bool:
    return value > 0

print(positive_validator(5))   # True
print(positive_validator(-1))  # False
```

#### SerializerProtocol

```python
from typing import Any

def uppercase_serializer(value: Any) -> Any:
    if isinstance(value, str):
        return value.upper()
    return value

print(uppercase_serializer("hello"))  # "HELLO"
```

#### DeserializerProtocol

```python
from typing import Any

def deserialize_int(value: Any, type_hint: int) -> int:
    if type_hint == int and isinstance(value, str):
        return int(value)
    return value

print(deserialize_int("42", int))  # 42
```

---

### 9. Combining Mixins

Mixins can be combined for full-featured dataclasses:

```python
from dataclasses import dataclass
from corbel import Serializable, Updatable, Validated, Hashable, Comparable, field, corbel_property

@dataclass
class Product(Serializable, Updatable, Validated, Hashable, Comparable):
    name: str = field()
    price: float = field()

    @corbel_property()
    def discounted_price(self) -> float:
        return self.price * 0.9

prod = Product("Widget", 100)
prod2 = prod.update(price=120)
print(prod.to_dict())  # {'name': 'Widget', 'price': 100}
print(prod2.discounted_price)  # 108.0
```

---

## Utilities

- `asdict(obj, include_private=False)`: convert instance to dict
- `field(**kwargs)`: wrapper for dataclass fields with Corbel metadata
- `fields(obj)`: returns dataclass fields
- `Include` enum: `ALWAYS`, `NON_NONE`, `NON_EMPTY`, `NON_DEFAULT`
- Exceptions: `ValidationError`, `DeserializeError`, `InclusionError`, `CorbelError`
- Class-level options for Serializable:  
  - `__json_wrapper__` – wrap the JSON output under a key  
  - `__inclusion__` – control which fields are included

---

## License

MIT License. See [LICENSE](https://github.com/bnlucas/corbel/blob/main/LICENSE).

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/bnlucas/corbel",
    "name": "corbel",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.10",
    "maintainer_email": null,
    "keywords": "dataclass, serialization, deserialization, validation, mixin, python",
    "author": "Nathan Lucas",
    "author_email": "nlucas@bnlucas.com",
    "download_url": "https://files.pythonhosted.org/packages/4f/43/0b6fcac1c316541611deb49e4c61adf244b00ca1e99c7ecc8aaf4ee848c3/corbel-0.1.2.tar.gz",
    "platform": null,
    "description": "# Corbel\n\nCorbel is a Python dataclass extension library providing **mixins and utilities** for:\n\n- Validation\n- Comparison and hashing\n- Copying and updating\n- Serialization to/from dicts and JSON\n- Property-level metadata (`@corbel_property`)\n- Protocol-based hooks for validation, serialization, and deserialization\n\n---\n\n## Installation\n\n```base\npip install corbel\n```\n\n---\n\n## Core Mixins\n\n### 1. Corbel\n\nThe base mixin that provides:\n\n- Cached `asdict` results\n- Field and property introspection\n- Hook methods for updates and validation\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Corbel, field\n\n@dataclass\nclass Base(Corbel):\n    x: int = field()\n    y: str = field()\n\ninst = Base(1, \"hello\")\nprint(inst.asdict())  # {'x': 1, 'y': 'hello'}\n```\n\n---\n\n### 2. Serializable\n\nProvides `to_dict`, `from_dict`, `to_json`, `from_json` for dataclasses.\n\n- Supports nested dataclasses\n- Optional wrapper key for JSON (`__json_wrapper__`)\n- Configurable inclusion rules (`__inclusion__`)\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Serializable, field, Include\n\n@dataclass\nclass User(Serializable):\n    id: int = field()\n    name: str = field()\n    email: str | None = field(default=None)\n\nuser = User(1, \"Alice\")\nprint(user.to_dict())  # {'id': 1, 'name': 'Alice', 'email': None}\nalice = User.from_dict({\"id\": 1, \"name\": \"Alice\", \"email\": None})\n\n# JSON with wrapper\nprint(user.to_json(wrapper=\"user\"))  # {\"user\": {\"id\":1,\"name\":\"Alice\",\"email\":null}}\nalice = User.from_json('{\"user\": {\"id\":1,\"name\":\"Alice\",\"email\":null}', wrapper=\"user\")\n\n# Custom class-level JSON wrapper\n@dataclass\nclass WrappedUser(Serializable):\n    __json_wrapper__ = \"account\"\n    id: int = field()\n    name: str = field()\n\nu = WrappedUser(5, \"Bob\")\nprint(u.to_json())  # {\"account\": {\"id\":5,\"name\":\"Bob\"}}\n\n# Using inclusion rules\n@dataclass\nclass PartialUser(Serializable):\n    __inclusion__ = Include.NON_NONE\n    id: int = field()\n    name: str = field()\n    email: str | None = field(default=None)\n\npu = PartialUser(1, \"Alice\")\nprint(pu.to_dict())  # {'id': 1, 'name': 'Alice'}  # email omitted\n```\n\n---\n\n### 3. Updatable\n\nProvides immutable-style updates:\n\n- `copy()`: shallow copy\n- `update(**kwargs)`: returns a new instance with updated fields\n- `batch_update()`: context manager to temporarily disable validation\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Updatable, field\n\n@dataclass\nclass Point(Updatable):\n    x: int = field()\n    y: int = field()\n\np1 = Point(1, 2)\np2 = p1.update(x=10)  # new instance\nprint(p1.asdict())    # {'x': 1, 'y': 2}\nprint(p2.asdict())    # {'x': 10, 'y': 2}\n\n# batch update\nwith p2.batch_update() as temp:\n    temp.x = 20\n    temp.y = 30\nprint(p2.asdict())  # {'x': 10, 'y': 2}, p2 unchanged\n```\n\n---\n\n### 4. Validated\n\nAutomatically validates fields on initialization and update:\n\n- Define a `validator` in `field()` metadata\n- Supports `allow_none=True`\n- Raises `ValidationError` on failure\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Validated, field, ValidationError\n\ndef positive(value: int) -> bool:\n    return value > 0\n\n@dataclass\nclass BankAccount(Validated):\n    balance: int = field(validator=positive)\n\ntry:\n    acct = BankAccount(-10)  # raises ValidationError\nexcept ValidationError as e:\n    print(e)\n\nacct = BankAccount(100)\nacct.balance = -50  # raises ValidationError\n```\n\n---\n\n### 5. Hashable\n\nCaches a hash based on dataclass fields:\n\n- Automatically invalidates on field update\n- Suitable for dict keys and set members\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Hashable, field\n\n@dataclass\nclass Coord(Hashable):\n    x: int = field()\n    y: int = field()\n\nc1 = Coord(1, 2)\nc2 = Coord(1, 2)\n\nprint(hash(c1) == hash(c2))  # True\nc1.x = 3\nprint(hash(c1) == hash(c2))  # False\n```\n\n---\n\n### 6. Comparable\n\nProvides `<`, `<=`, `>`, `>=`, `==` based on field values:\n\n- Lexicographic comparison of fields\n- Supports total ordering\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Comparable, field\n\n@dataclass\nclass Version(Comparable):\n    major: int = field()\n    minor: int = field()\n\nv1 = Version(1, 0)\nv2 = Version(1, 1)\nprint(v1 < v2)  # True\nprint(v1 == v2) # False\n```\n\n---\n\n### 7. @corbel_property\n\nCustom property decorator supporting:\n\n- `validator`\n- `serializer` / `deserializer`\n- `allow_none` / `ignore`\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Corbel, corbel_property, field\n\ndef positive(x: int) -> bool:\n    return x > 0\n\n@dataclass\nclass Example(Corbel):\n    _value: int = field()\n\n    @corbel_property(validator=lambda v: positive(v))\n    def value(self) -> int:\n        return self._value\n\n    @value.setter\n    def value(self, val: int) -> None:\n        self._value = val\n\nex = Example(5)\nprint(ex.value)  # 5\nex.value = 10    # OK\n# ex.value = -1  # Raises ValueError\n```\n\n---\n\n### 8. Protocol Examples\n\n#### ValidatorProtocol\n\n```python\nfrom typing import Any\n\ndef positive_validator(value: int) -> bool:\n    return value > 0\n\nprint(positive_validator(5))   # True\nprint(positive_validator(-1))  # False\n```\n\n#### SerializerProtocol\n\n```python\nfrom typing import Any\n\ndef uppercase_serializer(value: Any) -> Any:\n    if isinstance(value, str):\n        return value.upper()\n    return value\n\nprint(uppercase_serializer(\"hello\"))  # \"HELLO\"\n```\n\n#### DeserializerProtocol\n\n```python\nfrom typing import Any\n\ndef deserialize_int(value: Any, type_hint: int) -> int:\n    if type_hint == int and isinstance(value, str):\n        return int(value)\n    return value\n\nprint(deserialize_int(\"42\", int))  # 42\n```\n\n---\n\n### 9. Combining Mixins\n\nMixins can be combined for full-featured dataclasses:\n\n```python\nfrom dataclasses import dataclass\nfrom corbel import Serializable, Updatable, Validated, Hashable, Comparable, field, corbel_property\n\n@dataclass\nclass Product(Serializable, Updatable, Validated, Hashable, Comparable):\n    name: str = field()\n    price: float = field()\n\n    @corbel_property()\n    def discounted_price(self) -> float:\n        return self.price * 0.9\n\nprod = Product(\"Widget\", 100)\nprod2 = prod.update(price=120)\nprint(prod.to_dict())  # {'name': 'Widget', 'price': 100}\nprint(prod2.discounted_price)  # 108.0\n```\n\n---\n\n## Utilities\n\n- `asdict(obj, include_private=False)`: convert instance to dict\n- `field(**kwargs)`: wrapper for dataclass fields with Corbel metadata\n- `fields(obj)`: returns dataclass fields\n- `Include` enum: `ALWAYS`, `NON_NONE`, `NON_EMPTY`, `NON_DEFAULT`\n- Exceptions: `ValidationError`, `DeserializeError`, `InclusionError`, `CorbelError`\n- Class-level options for Serializable:  \n  - `__json_wrapper__` \u2013 wrap the JSON output under a key  \n  - `__inclusion__` \u2013 control which fields are included\n\n---\n\n## License\n\nMIT License. See [LICENSE](https://github.com/bnlucas/corbel/blob/main/LICENSE).\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Lightweight Python library extending dataclasses with serialization, deserialization, validation, and mixins for comparison, hashing, and immutable updates.",
    "version": "0.1.2",
    "project_urls": {
        "Homepage": "https://github.com/bnlucas/corbel",
        "Repository": "https://github.com/bnlucas/corbel"
    },
    "split_keywords": [
        "dataclass",
        " serialization",
        " deserialization",
        " validation",
        " mixin",
        " python"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "06d0caca494e2ab3f883b560e3d281aa7c700db4a222c001f8e842e3060b36b9",
                "md5": "75437caef06c27c4646f440881c6662f",
                "sha256": "9dff4bb7d3ca8f4f3b19aa0c1336dfe812baa98749f0c66bc321e3c9cb657db5"
            },
            "downloads": -1,
            "filename": "corbel-0.1.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "75437caef06c27c4646f440881c6662f",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.10",
            "size": 44186,
            "upload_time": "2025-09-10T09:10:00",
            "upload_time_iso_8601": "2025-09-10T09:10:00.580853Z",
            "url": "https://files.pythonhosted.org/packages/06/d0/caca494e2ab3f883b560e3d281aa7c700db4a222c001f8e842e3060b36b9/corbel-0.1.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "4f430b6fcac1c316541611deb49e4c61adf244b00ca1e99c7ecc8aaf4ee848c3",
                "md5": "017061840e8652dc6703cd2d045dedad",
                "sha256": "6d8403ec4a9560008702adb4fb705e55e5fd28f11a0b57a8e7376e2c9962266d"
            },
            "downloads": -1,
            "filename": "corbel-0.1.2.tar.gz",
            "has_sig": false,
            "md5_digest": "017061840e8652dc6703cd2d045dedad",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.10",
            "size": 27195,
            "upload_time": "2025-09-10T09:10:01",
            "upload_time_iso_8601": "2025-09-10T09:10:01.821958Z",
            "url": "https://files.pythonhosted.org/packages/4f/43/0b6fcac1c316541611deb49e4c61adf244b00ca1e99c7ecc8aaf4ee848c3/corbel-0.1.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-09-10 09:10:01",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "bnlucas",
    "github_project": "corbel",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "tox": true,
    "lcname": "corbel"
}
        
Elapsed time: 1.85103s