cbor-model 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: cbor-model
3
+ Version: 0.1.0
4
+ Summary: CBOR Model: Add CBOR and CDDL support to Pydantic models
5
+ Keywords: serialization,cbor,cddl
6
+ Author: William Häggqvist
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: cbor2>=5.8.0
20
+ Requires-Dist: pydantic>=2.12.5
21
+ Requires-Python: >=3.12
22
+ Project-URL: repository, https://github.com/haggqvist/cbor-model
23
+ Project-URL: issues, https://github.com/haggqvist/cbor-model/issues
24
+ Description-Content-Type: text/markdown
25
+
26
+ # CBOR Model
27
+
28
+ `cbor-model` adds [CBOR] serialization and [CDDL] schema generation to [Pydantic]
29
+ models.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install cbor-model
35
+ ```
36
+
37
+ or with [uv]:
38
+
39
+ ```bash
40
+ uv add cbor-model
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ### Map encoding
46
+
47
+ Fields are encoded as a CBOR map keyed by the integer or string supplied to
48
+ `CBORField(key=...)`.
49
+
50
+ ```python
51
+ from typing import Annotated
52
+ from cbor_model import CBORModel, CBORField
53
+
54
+ class Sensor(CBORModel):
55
+ name: Annotated[str, CBORField(key=0)]
56
+ value: Annotated[float, CBORField(key=1)]
57
+
58
+ sensor = Sensor(name="temp", value=21.5)
59
+ data = sensor.model_dump_cbor() # a2006474656d7001fb4035800000000000
60
+ assert Sensor.model_validate_cbor(data) == sensor
61
+ ```
62
+
63
+ ### Array encoding
64
+
65
+ Switch to array encoding by setting `CBORConfig(encoding="array")` and using
66
+ `CBORField(index=...)` — fields are serialized in index order.
67
+
68
+ ```python
69
+ from typing import Annotated
70
+ from cbor_model import CBORModel, CBORField, CBORConfig
71
+
72
+ class Point(CBORModel):
73
+ cbor_config = CBORConfig(encoding="array")
74
+
75
+ x: Annotated[int, CBORField(index=0)]
76
+ y: Annotated[int, CBORField(index=1)]
77
+
78
+ pt = Point(x=4, y=2)
79
+ data = pt.model_dump_cbor() # 820402
80
+ assert Point.model_validate_cbor(data) == pt
81
+ ```
82
+
83
+ ### CBOR tags
84
+
85
+ Wrap a field's value in a CBOR tag using `CBORField(tag=...)`, or tag the entire
86
+ model with `CBORConfig(tag=...)`.
87
+
88
+ ```python
89
+ from typing import Annotated
90
+ from cbor_model import CBORModel, CBORField, CBORConfig
91
+
92
+ class Reading(CBORModel):
93
+ cbor_config = CBORConfig(tag=40001)
94
+
95
+ sensor_id: Annotated[int, CBORField(key=0)]
96
+ raw: Annotated[bytes, CBORField(key=1, tag=40002)]
97
+ ```
98
+
99
+ ### Serialization context
100
+
101
+ Pass a `CBORSerializationContext` to control `None` and empty-collection exclusion:
102
+
103
+ ```python
104
+ from cbor_model import CBORSerializationContext
105
+
106
+ ctx = CBORSerializationContext(exclude_none=False, exclude_empty=False)
107
+ data = sensor.model_dump_cbor(context=ctx)
108
+ ```
109
+
110
+ ### Custom encoders
111
+
112
+ Register encoders for types not natively supported by [cbor2]:
113
+
114
+ ```python
115
+ import decimal
116
+ from cbor_model import CBORConfig
117
+
118
+ class MyModel(CBORModel):
119
+ cbor_config = CBORConfig(
120
+ encoders={decimal.Decimal: lambda d: str(d)}
121
+ )
122
+ amount: Annotated[decimal.Decimal, CBORField(key=0)]
123
+ ```
124
+
125
+ ### CDDL generation
126
+
127
+ Generate a [CDDL] schema from one or more models:
128
+
129
+ ```python
130
+ from cbor_model.cddl import CDDLGenerator
131
+
132
+ print(CDDLGenerator().generate(Sensor))
133
+ # Sensor = {
134
+ # ? 0: text, ; name
135
+ # ? 1: float, ; value
136
+ # }
137
+ ```
138
+
139
+ [CBOR]: https://cbor.io/
140
+ [CDDL]: https://www.rfc-editor.org/rfc/rfc8610
141
+ [Pydantic]: https://github.com/pydantic/pydantic
142
+ [cbor2]: https://github.com/agronholm/cbor2
143
+ [uv]: https://docs.astral.sh/uv/
144
+
145
+
146
+ [pydantic]: https://github.com/pydantic/pydantic
@@ -0,0 +1,121 @@
1
+ # CBOR Model
2
+
3
+ `cbor-model` adds [CBOR] serialization and [CDDL] schema generation to [Pydantic]
4
+ models.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install cbor-model
10
+ ```
11
+
12
+ or with [uv]:
13
+
14
+ ```bash
15
+ uv add cbor-model
16
+ ```
17
+
18
+ ## Quick start
19
+
20
+ ### Map encoding
21
+
22
+ Fields are encoded as a CBOR map keyed by the integer or string supplied to
23
+ `CBORField(key=...)`.
24
+
25
+ ```python
26
+ from typing import Annotated
27
+ from cbor_model import CBORModel, CBORField
28
+
29
+ class Sensor(CBORModel):
30
+ name: Annotated[str, CBORField(key=0)]
31
+ value: Annotated[float, CBORField(key=1)]
32
+
33
+ sensor = Sensor(name="temp", value=21.5)
34
+ data = sensor.model_dump_cbor() # a2006474656d7001fb4035800000000000
35
+ assert Sensor.model_validate_cbor(data) == sensor
36
+ ```
37
+
38
+ ### Array encoding
39
+
40
+ Switch to array encoding by setting `CBORConfig(encoding="array")` and using
41
+ `CBORField(index=...)` — fields are serialized in index order.
42
+
43
+ ```python
44
+ from typing import Annotated
45
+ from cbor_model import CBORModel, CBORField, CBORConfig
46
+
47
+ class Point(CBORModel):
48
+ cbor_config = CBORConfig(encoding="array")
49
+
50
+ x: Annotated[int, CBORField(index=0)]
51
+ y: Annotated[int, CBORField(index=1)]
52
+
53
+ pt = Point(x=4, y=2)
54
+ data = pt.model_dump_cbor() # 820402
55
+ assert Point.model_validate_cbor(data) == pt
56
+ ```
57
+
58
+ ### CBOR tags
59
+
60
+ Wrap a field's value in a CBOR tag using `CBORField(tag=...)`, or tag the entire
61
+ model with `CBORConfig(tag=...)`.
62
+
63
+ ```python
64
+ from typing import Annotated
65
+ from cbor_model import CBORModel, CBORField, CBORConfig
66
+
67
+ class Reading(CBORModel):
68
+ cbor_config = CBORConfig(tag=40001)
69
+
70
+ sensor_id: Annotated[int, CBORField(key=0)]
71
+ raw: Annotated[bytes, CBORField(key=1, tag=40002)]
72
+ ```
73
+
74
+ ### Serialization context
75
+
76
+ Pass a `CBORSerializationContext` to control `None` and empty-collection exclusion:
77
+
78
+ ```python
79
+ from cbor_model import CBORSerializationContext
80
+
81
+ ctx = CBORSerializationContext(exclude_none=False, exclude_empty=False)
82
+ data = sensor.model_dump_cbor(context=ctx)
83
+ ```
84
+
85
+ ### Custom encoders
86
+
87
+ Register encoders for types not natively supported by [cbor2]:
88
+
89
+ ```python
90
+ import decimal
91
+ from cbor_model import CBORConfig
92
+
93
+ class MyModel(CBORModel):
94
+ cbor_config = CBORConfig(
95
+ encoders={decimal.Decimal: lambda d: str(d)}
96
+ )
97
+ amount: Annotated[decimal.Decimal, CBORField(key=0)]
98
+ ```
99
+
100
+ ### CDDL generation
101
+
102
+ Generate a [CDDL] schema from one or more models:
103
+
104
+ ```python
105
+ from cbor_model.cddl import CDDLGenerator
106
+
107
+ print(CDDLGenerator().generate(Sensor))
108
+ # Sensor = {
109
+ # ? 0: text, ; name
110
+ # ? 1: float, ; value
111
+ # }
112
+ ```
113
+
114
+ [CBOR]: https://cbor.io/
115
+ [CDDL]: https://www.rfc-editor.org/rfc/rfc8610
116
+ [Pydantic]: https://github.com/pydantic/pydantic
117
+ [cbor2]: https://github.com/agronholm/cbor2
118
+ [uv]: https://docs.astral.sh/uv/
119
+
120
+
121
+ [pydantic]: https://github.com/pydantic/pydantic
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "cbor-model"
3
+ version = "0.1.0"
4
+ description = "CBOR Model: Add CBOR and CDDL support to Pydantic models"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "William Häggqvist" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "cbor2>=5.8.0",
12
+ "pydantic>=2.12.5",
13
+ ]
14
+ license = "MIT"
15
+ keywords = [
16
+ "serialization",
17
+ "cbor",
18
+ "cddl"
19
+ ]
20
+
21
+ classifiers = [
22
+ "Development Status :: 2 - Pre-Alpha",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Typing :: Typed"
33
+ ]
34
+
35
+ [project.urls]
36
+ repository = "https://github.com/haggqvist/cbor-model"
37
+ issues = "https://github.com/haggqvist/cbor-model/issues"
38
+
39
+ [build-system]
40
+ requires = ["uv_build>=0.10,<0.11.0"]
41
+ build-backend = "uv_build"
42
+
43
+ [tool.ruff]
44
+ line-length = 88
45
+ fix = true
46
+ target-version = "py312"
47
+
48
+ [tool.ruff.lint]
49
+ ignore = ["D104"]
50
+ extend-select = ["I"]
51
+
52
+ [tool.ruff.lint.per-file-ignores]
53
+ "**/tests/*" = ["S101", "D"]
54
+
55
+ [tool.pytest.ini_options]
56
+ pythonpath = ["."]
57
+
58
+ [dependency-groups]
59
+ dev = [
60
+ "pre-commit>=4.5.1",
61
+ "pytest>=9.0.2",
62
+ ]
@@ -0,0 +1,5 @@
1
+ import importlib.metadata
2
+
3
+ __author__ = "William Häggqvist"
4
+ __application__ = "cbor-model"
5
+ __version__ = importlib.metadata.version("cbor-model")
@@ -0,0 +1,16 @@
1
+ from .__about__ import __application__, __author__, __version__
2
+ from ._config import CBORConfig
3
+ from ._field import CBORField
4
+ from ._model import CBORModel, CBORSerializationContext
5
+ from .cddl import CDDLGenerator
6
+
7
+ __all__ = [
8
+ "CBORConfig",
9
+ "CBORField",
10
+ "CBORModel",
11
+ "CBORSerializationContext",
12
+ "CDDLGenerator",
13
+ "__application__",
14
+ "__author__",
15
+ "__version__",
16
+ ]
@@ -0,0 +1,46 @@
1
+ from collections.abc import Callable
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Literal
4
+
5
+ type CBOREncoders = dict[type, Callable[[Any], Any]]
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class CBORConfig:
10
+ """Configuration options for :class:`~cbor_model.CBORModel` instances.
11
+
12
+ Attributes:
13
+ encoding: Whether to encode the model as a CBOR map (keyed by
14
+ `CBORField(key=...)`) or as a CBOR array (ordered by
15
+ `CBORField(index=...)`). Defaults to `"map"`.
16
+ tag: Wrap the encoded model in a CBOR tag with this tag number.
17
+ `None` disables tagging (default).
18
+ canonical: Use canonical CBOR encoding (deterministic key ordering
19
+ and minimal integer encoding). Defaults to `False`.
20
+ encoders: Custom encoders for types not natively supported by
21
+ cbor2. Keys are Python types; values are callables that
22
+ convert an instance of that type to a cbor2-encodable value
23
+ (e.g. `str`, `int`, `list`, `dict`).
24
+
25
+ """
26
+
27
+ encoding: Literal["map", "array"] = "map"
28
+ """Whether to encode the model as a CBOR map (key-value pairs) or array
29
+ (positional). Defaults to "map"."""
30
+
31
+ tag: int | None = None
32
+ """Wrap the CBORModel in a CBOR Tag on serialization."""
33
+
34
+ canonical: bool = False
35
+ """Whether to use canonical CBOR encoding. Defaults to `False`."""
36
+
37
+ encoders: CBOREncoders = field(default_factory=dict)
38
+ """Custom encoders for types that are not natively supported by cbor2.
39
+ The keys should be types, and the values should be callables that take an
40
+ instance of the type and return a value that can be encoded by cbor2 (e.g.
41
+ a string, int, list, dict, etc.)."""
42
+
43
+ def __post_init__(self) -> None:
44
+ if self.tag is not None and self.tag < 0:
45
+ err = f"CBOR tag {self.tag} is invalid. Tags must be non-negative integers."
46
+ raise ValueError(err)
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ if TYPE_CHECKING:
7
+ from collections.abc import Callable
8
+
9
+ CBOR2_RESERVED_TAGS = frozenset(
10
+ {0, 1, 2, 3, 4, 5, 25, 28, 29, 30, 35, 36, 37, 100, 256, 258, 260, 261},
11
+ )
12
+ """CBOR tags reserved by cbor2 library with semantic decoders."""
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class CBORField:
17
+ """Marks a :class:`~pydantic.BaseModel` field for CBOR serialization.
18
+
19
+ Exactly one of `key` or `index` must be provided. Attach a `CBORField` to a
20
+ field via :data:`typing.Annotated`:
21
+
22
+ ```python
23
+ from typing import Annotated
24
+ from cbor_model import CBORModel, CBORField
25
+
26
+ class MyModel(CBORModel):
27
+ name: Annotated[str, CBORField(key=0)]
28
+ value: Annotated[int, CBORField(key=1)]
29
+ ```
30
+
31
+ Attributes:
32
+ key: Map key used when the parent model uses `encoding="map"`. May be
33
+ an integer or a string.
34
+ index: Zero-based position used when the parent model uses
35
+ `encoding="array"`. Indices must be contiguous starting from 0,
36
+ with optional fields at the tail.
37
+ tag: CBOR tag number to wrap this field's value in on serialization.
38
+ Use values above 1000 to avoid conflicts with standard tags. See
39
+ `CBOR2_RESERVED_TAGS` for tags reserved by cbor2.
40
+ override_type: Override the CDDL type name emitted by
41
+ :class:`CDDLGenerator` for this field.
42
+ override_name: Override the CDDL field name emitted by
43
+ :class:`CDDLGenerator` for this field.
44
+ optional: Mark the field as optional in CDDL output regardless of
45
+ its Python type annotation.
46
+ exclude_if: A callable that receives the field value and returns
47
+ `True` if the field should be omitted from the serialized output.
48
+ Useful for custom exclusion logic beyond `None` or empty values.
49
+
50
+ """
51
+
52
+ key: int | str | None = None
53
+ index: int | None = None
54
+ tag: int | None = None
55
+ """Custom CBOR tag number. Use values >1000 to avoid conflicts with
56
+ standard tags.
57
+
58
+ See `CBOR2_RESERVED_TAGS` for tags reserved by cbor2 library.
59
+ """
60
+ override_type: str | None = None
61
+ override_name: str | None = None
62
+ optional: bool = False
63
+ exclude_if: Callable[[Any], bool] | None = None
64
+ """A Callable that takes the field value and returns `True` if the field should be excluded from serialization. This can be used to implement custom exclusion logic beyond just `None` or empty values.""" # noqa: E501
65
+
66
+ @property
67
+ def identifier(self) -> int | str:
68
+ """The CBOR map key or array index used to identify this field.
69
+
70
+ Returns `key` when the field belongs to a map-encoded model, or
71
+ `index` when it belongs to an array-encoded model.
72
+ """
73
+ return self.key if self.key is not None else cast("int", self.index)
74
+
75
+ def __post_init__(self) -> None:
76
+ if self.key is not None and self.index is not None:
77
+ err = "Cannot specify both key and index for CBORField"
78
+ raise ValueError(err)
79
+ if self.key is None and self.index is None:
80
+ err = "Must specify either key or index for CBORField"
81
+ raise ValueError(err)
82
+ if self.tag is not None:
83
+ if self.tag < 0:
84
+ err = f"CBOR tag {self.tag} is invalid. Tags must be non-negative integers."
85
+ raise ValueError(err)
86
+ if self.tag in CBOR2_RESERVED_TAGS:
87
+ err = (
88
+ f"CBOR tag {self.tag} conflicts with cbor2 reserved tags. "
89
+ f"Use tag values > 1000 to avoid conflicts with standard CBOR tags."
90
+ )
91
+ raise ValueError(err)