# 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"
}