pydantic-construct 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.
- pydantic_construct-0.1.0/PKG-INFO +204 -0
- pydantic_construct-0.1.0/README.md +186 -0
- pydantic_construct-0.1.0/pyproject.toml +38 -0
- pydantic_construct-0.1.0/src/pydantic_construct/__init__.py +15 -0
- pydantic_construct-0.1.0/src/pydantic_construct/base.py +304 -0
- pydantic_construct-0.1.0/src/pydantic_construct/py.typed +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydantic-construct
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Interfaces between construct and Pydantic to allow typed serializing to bytes.
|
|
5
|
+
Keywords: pydantic,construct,binary,serializing
|
|
6
|
+
Author: Jay184
|
|
7
|
+
Author-email: Jay184 <me@jay0.mozmail.com>
|
|
8
|
+
License-Expression: 0BSD
|
|
9
|
+
Requires-Dist: construct>=2.10.70
|
|
10
|
+
Requires-Dist: construct-typing>=0.7.0
|
|
11
|
+
Requires-Dist: pydantic>=2.12.5
|
|
12
|
+
Maintainer: Jay184
|
|
13
|
+
Maintainer-email: Jay184 <me@jay0.mozmail.com>
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Project-URL: Issues, https://github.com/Jay184/pydantic-construct/issues
|
|
16
|
+
Project-URL: Repository, https://github.com/Jay184/pydantic-construct.git
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Pydantic-Construct
|
|
20
|
+
|
|
21
|
+
**Pydantic-Construct** integrates Pydantic with `construct` to provide **typed binary serialization and parsing** using standard Pydantic models.
|
|
22
|
+
|
|
23
|
+
Define binary layouts declaratively with type annotations, while keeping Pydantic’s validation and serialization.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
* Declarative binary schemas via `Annotated`
|
|
30
|
+
* `model_dump_bytes()` / `model_validate_bytes()`
|
|
31
|
+
* Nested models
|
|
32
|
+
* Computed fields with ordering control
|
|
33
|
+
* Mode-aware field omission (`json`, `python`, `binary`)
|
|
34
|
+
* Async parsing from streams
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install pydantic-construct
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
Minimal example:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from typing import Annotated
|
|
52
|
+
from construct import Int32ul
|
|
53
|
+
from pydantic_construct import ConstructModel
|
|
54
|
+
|
|
55
|
+
class Model(ConstructModel):
|
|
56
|
+
x: Annotated[int, Int32ul]
|
|
57
|
+
|
|
58
|
+
m = Model(x=123)
|
|
59
|
+
|
|
60
|
+
data = m.model_dump_bytes()
|
|
61
|
+
parsed = Model.model_validate_bytes(data)
|
|
62
|
+
|
|
63
|
+
assert data == b"\x7B\x00\x00\x00"
|
|
64
|
+
assert parsed.x == 123
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With padding (ignored outside binary mode):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from typing import Annotated
|
|
71
|
+
from pydantic_construct import ConstructModel, OmitInMode
|
|
72
|
+
from construct import Padding, Int32ul
|
|
73
|
+
|
|
74
|
+
class Model(ConstructModel):
|
|
75
|
+
x: Annotated[int, Int32ul]
|
|
76
|
+
pad: Annotated[bytes | None, Padding(4), OmitInMode({"json", "python"})] = None
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Core Concepts
|
|
82
|
+
|
|
83
|
+
### Binary Fields
|
|
84
|
+
|
|
85
|
+
Each field must define a `construct` type via `Annotated`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
x: Annotated[int, Int32ul]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### Mode-Based Omission
|
|
94
|
+
|
|
95
|
+
Exclude fields depending on serialization mode:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from typing import Annotated
|
|
99
|
+
from pydantic_construct import OmitInMode
|
|
100
|
+
from construct import Padding
|
|
101
|
+
|
|
102
|
+
pad: Annotated[
|
|
103
|
+
bytes | None,
|
|
104
|
+
Padding(4),
|
|
105
|
+
OmitInMode({"json", "python"})
|
|
106
|
+
]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Modes:
|
|
110
|
+
|
|
111
|
+
* `"python"`
|
|
112
|
+
* `"json"`
|
|
113
|
+
* `"binary"`
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Nested Models
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from typing import Annotated
|
|
121
|
+
from pydantic_construct import ConstructModel
|
|
122
|
+
from construct import Int32ul
|
|
123
|
+
|
|
124
|
+
class Header(ConstructModel):
|
|
125
|
+
length: Annotated[int, Int32ul]
|
|
126
|
+
|
|
127
|
+
class Packet(ConstructModel):
|
|
128
|
+
header: Header
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Computed Fields (Binary)
|
|
134
|
+
|
|
135
|
+
Computed fields can participate in binary serialization if they return `Annotated[..., Construct]`.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from typing import Annotated
|
|
139
|
+
from pydantic_construct import ConstructModel, binary_after
|
|
140
|
+
from pydantic import computed_field
|
|
141
|
+
from construct import Int32ul
|
|
142
|
+
|
|
143
|
+
class Example(ConstructModel):
|
|
144
|
+
x: Annotated[int, Int32ul]
|
|
145
|
+
|
|
146
|
+
@computed_field
|
|
147
|
+
@property
|
|
148
|
+
@binary_after("x")
|
|
149
|
+
def checksum(self) -> Annotated[int, Int32ul]:
|
|
150
|
+
return self.x ^ 0xFFFFFFFF
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Positioning:
|
|
154
|
+
|
|
155
|
+
* `@binary_after("field")`
|
|
156
|
+
* `@binary_before("field")`
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## API Overview
|
|
161
|
+
|
|
162
|
+
### Serialize to Binary
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
data = model.model_dump_bytes()
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Parse from Binary
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
model = Model.model_validate_bytes(data)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Async Stream Parsing
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
model = await Model.model_validate_reader(reader)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Design Notes
|
|
187
|
+
|
|
188
|
+
* A `construct.Struct` is generated at class creation time
|
|
189
|
+
* Field order is deterministic and includes computed fields
|
|
190
|
+
* Multiple `ConstructModel` roots in inheritance are disallowed
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Constraints
|
|
195
|
+
|
|
196
|
+
* Every field must define a `construct` type
|
|
197
|
+
* Computed fields must return `Annotated[..., Construct]`
|
|
198
|
+
* Binary layout must be deterministic
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
0BSD
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Pydantic-Construct
|
|
2
|
+
|
|
3
|
+
**Pydantic-Construct** integrates Pydantic with `construct` to provide **typed binary serialization and parsing** using standard Pydantic models.
|
|
4
|
+
|
|
5
|
+
Define binary layouts declaratively with type annotations, while keeping Pydantic’s validation and serialization.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
* Declarative binary schemas via `Annotated`
|
|
12
|
+
* `model_dump_bytes()` / `model_validate_bytes()`
|
|
13
|
+
* Nested models
|
|
14
|
+
* Computed fields with ordering control
|
|
15
|
+
* Mode-aware field omission (`json`, `python`, `binary`)
|
|
16
|
+
* Async parsing from streams
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pydantic-construct
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
Minimal example:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from typing import Annotated
|
|
34
|
+
from construct import Int32ul
|
|
35
|
+
from pydantic_construct import ConstructModel
|
|
36
|
+
|
|
37
|
+
class Model(ConstructModel):
|
|
38
|
+
x: Annotated[int, Int32ul]
|
|
39
|
+
|
|
40
|
+
m = Model(x=123)
|
|
41
|
+
|
|
42
|
+
data = m.model_dump_bytes()
|
|
43
|
+
parsed = Model.model_validate_bytes(data)
|
|
44
|
+
|
|
45
|
+
assert data == b"\x7B\x00\x00\x00"
|
|
46
|
+
assert parsed.x == 123
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
With padding (ignored outside binary mode):
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from typing import Annotated
|
|
53
|
+
from pydantic_construct import ConstructModel, OmitInMode
|
|
54
|
+
from construct import Padding, Int32ul
|
|
55
|
+
|
|
56
|
+
class Model(ConstructModel):
|
|
57
|
+
x: Annotated[int, Int32ul]
|
|
58
|
+
pad: Annotated[bytes | None, Padding(4), OmitInMode({"json", "python"})] = None
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Core Concepts
|
|
64
|
+
|
|
65
|
+
### Binary Fields
|
|
66
|
+
|
|
67
|
+
Each field must define a `construct` type via `Annotated`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
x: Annotated[int, Int32ul]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Mode-Based Omission
|
|
76
|
+
|
|
77
|
+
Exclude fields depending on serialization mode:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from typing import Annotated
|
|
81
|
+
from pydantic_construct import OmitInMode
|
|
82
|
+
from construct import Padding
|
|
83
|
+
|
|
84
|
+
pad: Annotated[
|
|
85
|
+
bytes | None,
|
|
86
|
+
Padding(4),
|
|
87
|
+
OmitInMode({"json", "python"})
|
|
88
|
+
]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Modes:
|
|
92
|
+
|
|
93
|
+
* `"python"`
|
|
94
|
+
* `"json"`
|
|
95
|
+
* `"binary"`
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Nested Models
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from typing import Annotated
|
|
103
|
+
from pydantic_construct import ConstructModel
|
|
104
|
+
from construct import Int32ul
|
|
105
|
+
|
|
106
|
+
class Header(ConstructModel):
|
|
107
|
+
length: Annotated[int, Int32ul]
|
|
108
|
+
|
|
109
|
+
class Packet(ConstructModel):
|
|
110
|
+
header: Header
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### Computed Fields (Binary)
|
|
116
|
+
|
|
117
|
+
Computed fields can participate in binary serialization if they return `Annotated[..., Construct]`.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from typing import Annotated
|
|
121
|
+
from pydantic_construct import ConstructModel, binary_after
|
|
122
|
+
from pydantic import computed_field
|
|
123
|
+
from construct import Int32ul
|
|
124
|
+
|
|
125
|
+
class Example(ConstructModel):
|
|
126
|
+
x: Annotated[int, Int32ul]
|
|
127
|
+
|
|
128
|
+
@computed_field
|
|
129
|
+
@property
|
|
130
|
+
@binary_after("x")
|
|
131
|
+
def checksum(self) -> Annotated[int, Int32ul]:
|
|
132
|
+
return self.x ^ 0xFFFFFFFF
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Positioning:
|
|
136
|
+
|
|
137
|
+
* `@binary_after("field")`
|
|
138
|
+
* `@binary_before("field")`
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## API Overview
|
|
143
|
+
|
|
144
|
+
### Serialize to Binary
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
data = model.model_dump_bytes()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Parse from Binary
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
model = Model.model_validate_bytes(data)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Async Stream Parsing
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
model = await Model.model_validate_reader(reader)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Design Notes
|
|
169
|
+
|
|
170
|
+
* A `construct.Struct` is generated at class creation time
|
|
171
|
+
* Field order is deterministic and includes computed fields
|
|
172
|
+
* Multiple `ConstructModel` roots in inheritance are disallowed
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Constraints
|
|
177
|
+
|
|
178
|
+
* Every field must define a `construct` type
|
|
179
|
+
* Computed fields must return `Annotated[..., Construct]`
|
|
180
|
+
* Binary layout must be deterministic
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
0BSD
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pydantic-construct"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Interfaces between construct and Pydantic to allow typed serializing to bytes."
|
|
5
|
+
keywords = ["pydantic", "construct", "binary", "serializing"]
|
|
6
|
+
authors = [
|
|
7
|
+
{name = "Jay184", email = "me@jay0.mozmail.com"},
|
|
8
|
+
]
|
|
9
|
+
maintainers = [
|
|
10
|
+
{name = "Jay184", email = "me@jay0.mozmail.com"},
|
|
11
|
+
]
|
|
12
|
+
license = "0BSD"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"construct>=2.10.70",
|
|
17
|
+
"construct-typing>=0.7.0",
|
|
18
|
+
"pydantic>=2.12.5",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Repository = "https://github.com/Jay184/pydantic-construct.git"
|
|
23
|
+
Issues = "https://github.com/Jay184/pydantic-construct/issues"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"coverage>=7.13.5",
|
|
28
|
+
"invoke>=2.2.1",
|
|
29
|
+
"mypy>=1.19.1",
|
|
30
|
+
"pytest>=9.0.2",
|
|
31
|
+
"pytest-randomly>=4.0.1",
|
|
32
|
+
"hypothesis>=6.151.10",
|
|
33
|
+
"ruff>=0.15.8",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.8.12,<0.9.0"]
|
|
38
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from typing import ClassVar, Any, Self, Literal, Callable, Iterable
|
|
2
|
+
from typing import Annotated, get_origin, get_args
|
|
3
|
+
from typing_extensions import Buffer
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from asyncio import StreamReader
|
|
6
|
+
|
|
7
|
+
import dataclasses
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, model_serializer, main
|
|
10
|
+
from construct import Construct, Container, Struct
|
|
11
|
+
from pydantic_core.core_schema import SerializerFunctionWrapHandler, SerializationInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extract_construct(annotation):
|
|
15
|
+
"""Extract Construct instance from Annotated[...]"""
|
|
16
|
+
if get_origin(annotation) is Annotated:
|
|
17
|
+
base_type, *metadata = get_args(annotation)
|
|
18
|
+
|
|
19
|
+
for meta in metadata:
|
|
20
|
+
if isinstance(meta, Construct):
|
|
21
|
+
return meta
|
|
22
|
+
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def binary_after(field_name: str):
|
|
27
|
+
def decorator(func):
|
|
28
|
+
setattr(func, "__binary_after__", field_name)
|
|
29
|
+
return func
|
|
30
|
+
return decorator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def binary_before(field_name: str):
|
|
34
|
+
def decorator(func):
|
|
35
|
+
setattr(func, "__binary_before__", field_name)
|
|
36
|
+
return func
|
|
37
|
+
return decorator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Mode = Literal["python", "json", "binary"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclasses.dataclass
|
|
44
|
+
class OmitInMode:
|
|
45
|
+
modes: set[Mode] = dataclasses.field(default_factory=lambda: {"json"})
|
|
46
|
+
|
|
47
|
+
def __init__(self, modes: Mode | Iterable[Mode] = "json"):
|
|
48
|
+
if isinstance(modes, str):
|
|
49
|
+
self.modes = {modes}
|
|
50
|
+
else:
|
|
51
|
+
self.modes = set(modes)
|
|
52
|
+
|
|
53
|
+
def matches(self, current_mode: str) -> bool:
|
|
54
|
+
return current_mode in self.modes
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ConstructModel(BaseModel):
|
|
58
|
+
# Dynamically generated struct
|
|
59
|
+
struct: ClassVar[Struct]
|
|
60
|
+
_computed_subcons: ClassVar[dict[str, Construct]]
|
|
61
|
+
|
|
62
|
+
# Cached order for computed fields
|
|
63
|
+
_binary_final_order: ClassVar[list[str]]
|
|
64
|
+
_binary_ordered_struct: ClassVar[Struct]
|
|
65
|
+
|
|
66
|
+
@model_serializer(mode="wrap")
|
|
67
|
+
def exclude_omissions(
|
|
68
|
+
self,
|
|
69
|
+
handler: SerializerFunctionWrapHandler,
|
|
70
|
+
info: SerializationInfo,
|
|
71
|
+
) -> dict[str, object]:
|
|
72
|
+
serialized = handler(self)
|
|
73
|
+
cls = type(self)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
name: value
|
|
77
|
+
for name, value in serialized.items()
|
|
78
|
+
if not cls._is_omitted_in_mode(name, info.mode)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def __pydantic_init_subclass__(cls, **kwargs):
|
|
83
|
+
"""Hook for Pydantic subclass initialization."""
|
|
84
|
+
super().__pydantic_init_subclass__(**kwargs)
|
|
85
|
+
|
|
86
|
+
roots = cls._get_construct_roots()
|
|
87
|
+
|
|
88
|
+
if len(roots) > 1:
|
|
89
|
+
raise TypeError(
|
|
90
|
+
f"{cls.__name__} has multiple ConstructModel roots: "
|
|
91
|
+
f"{[r.__name__ for r in roots]}. "
|
|
92
|
+
"This leads to ambiguous binary layout. Use composition instead."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
subcons = {}
|
|
96
|
+
computed_subcons = {}
|
|
97
|
+
|
|
98
|
+
for name, field in cls.model_fields.items():
|
|
99
|
+
if cls._is_omitted_in_mode(name, mode="binary"):
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
construct_type = None
|
|
103
|
+
|
|
104
|
+
if isinstance(field.annotation, type) and issubclass(field.annotation, ConstructModel):
|
|
105
|
+
construct_type = field.annotation.struct
|
|
106
|
+
else:
|
|
107
|
+
for meta in field.metadata:
|
|
108
|
+
if isinstance(meta, Construct):
|
|
109
|
+
construct_type = meta
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
if construct_type is None:
|
|
113
|
+
raise TypeError(
|
|
114
|
+
f"Field '{name}' must be Annotated with a Construct type"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
subcons[name] = construct_type
|
|
118
|
+
|
|
119
|
+
for name, field in cls.model_computed_fields.items():
|
|
120
|
+
if cls._is_omitted_in_mode(name, mode="binary"):
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
construct_type = extract_construct(field.return_type)
|
|
124
|
+
|
|
125
|
+
if construct_type is None:
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"Computed field '{name}' must return Annotated[..., Construct]"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
computed_subcons[name] = construct_type
|
|
131
|
+
|
|
132
|
+
cls.struct = Struct(**subcons)
|
|
133
|
+
cls._computed_subcons = computed_subcons
|
|
134
|
+
|
|
135
|
+
base_order = list(subcons.keys())
|
|
136
|
+
final_order = base_order.copy()
|
|
137
|
+
|
|
138
|
+
for name, cons in computed_subcons.items():
|
|
139
|
+
func = getattr(cls, name).fget
|
|
140
|
+
after = getattr(func, "__binary_after__", None)
|
|
141
|
+
before = getattr(func, "__binary_before__", None)
|
|
142
|
+
|
|
143
|
+
if after and before:
|
|
144
|
+
raise TypeError(f"{name} cannot have both before and after")
|
|
145
|
+
if after:
|
|
146
|
+
idx = final_order.index(after) + 1
|
|
147
|
+
elif before:
|
|
148
|
+
idx = final_order.index(before)
|
|
149
|
+
else:
|
|
150
|
+
idx = len(final_order)
|
|
151
|
+
final_order.insert(idx, name)
|
|
152
|
+
|
|
153
|
+
cls._binary_final_order = final_order
|
|
154
|
+
|
|
155
|
+
# Build a cached Struct for serialization
|
|
156
|
+
ordered_subcons = []
|
|
157
|
+
for name in final_order:
|
|
158
|
+
if name in subcons:
|
|
159
|
+
ordered_subcons.append(name / subcons[name])
|
|
160
|
+
elif name in computed_subcons:
|
|
161
|
+
ordered_subcons.append(name / computed_subcons[name])
|
|
162
|
+
|
|
163
|
+
cls._binary_ordered_struct = Struct(*ordered_subcons)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def _get_construct_roots(cls: type):
|
|
167
|
+
roots = set()
|
|
168
|
+
|
|
169
|
+
for base in cls.__mro__:
|
|
170
|
+
if (
|
|
171
|
+
isinstance(base, type)
|
|
172
|
+
and issubclass(base, ConstructModel)
|
|
173
|
+
and base is not ConstructModel
|
|
174
|
+
):
|
|
175
|
+
# Check if this base has a ConstructModel parent (excluding base class)
|
|
176
|
+
has_construct_parent = any(
|
|
177
|
+
issubclass(parent, ConstructModel)
|
|
178
|
+
and parent is not ConstructModel
|
|
179
|
+
for parent in base.__bases__
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not has_construct_parent:
|
|
183
|
+
roots.add(base)
|
|
184
|
+
|
|
185
|
+
return roots
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def _is_omitted_in_mode(cls, name: str, mode: Mode | str) -> bool:
|
|
189
|
+
print(name, mode)
|
|
190
|
+
return any(
|
|
191
|
+
isinstance(meta, OmitInMode) and meta.matches(mode)
|
|
192
|
+
for meta in cls._get_field_metadata(name)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
@lru_cache
|
|
197
|
+
def _get_field_metadata(cls, name: str):
|
|
198
|
+
# Regular field
|
|
199
|
+
if name in cls.model_fields:
|
|
200
|
+
return cls.model_fields[name].metadata
|
|
201
|
+
|
|
202
|
+
# Computed field
|
|
203
|
+
if name in cls.model_computed_fields:
|
|
204
|
+
annotation = cls.model_computed_fields[name].return_type
|
|
205
|
+
if get_origin(annotation) is Annotated:
|
|
206
|
+
return get_args(annotation)[1:]
|
|
207
|
+
return ()
|
|
208
|
+
|
|
209
|
+
# Unknown field
|
|
210
|
+
return ()
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def model_validate_bytes(
|
|
214
|
+
cls,
|
|
215
|
+
obj: bytes | bytearray | Buffer,
|
|
216
|
+
*,
|
|
217
|
+
strict: bool | None = None,
|
|
218
|
+
extra: main.ExtraValues | None = None,
|
|
219
|
+
from_attributes: bool | None = None,
|
|
220
|
+
context: Any | None = None,
|
|
221
|
+
by_alias: bool | None = None,
|
|
222
|
+
by_name: bool | None = None,
|
|
223
|
+
) -> Self:
|
|
224
|
+
parsed: Container = cls.struct.parse(obj)
|
|
225
|
+
|
|
226
|
+
filtered = {
|
|
227
|
+
k: v for k, v in dict(parsed).items()
|
|
228
|
+
if not k.startswith("_")
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return cls.model_validate(
|
|
232
|
+
filtered,
|
|
233
|
+
strict=strict,
|
|
234
|
+
extra=extra,
|
|
235
|
+
from_attributes=from_attributes,
|
|
236
|
+
context=context,
|
|
237
|
+
by_alias=by_alias,
|
|
238
|
+
by_name=by_name,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def model_dump_bytes(
|
|
242
|
+
self,
|
|
243
|
+
*,
|
|
244
|
+
mode: Literal["json", "python", "binary"] | str = "binary",
|
|
245
|
+
include: main.IncEx | None = None,
|
|
246
|
+
exclude: main.IncEx | None = None,
|
|
247
|
+
context: Any | None = None,
|
|
248
|
+
by_alias: bool | None = None,
|
|
249
|
+
exclude_unset: bool = False,
|
|
250
|
+
exclude_defaults: bool = False,
|
|
251
|
+
exclude_none: bool = False,
|
|
252
|
+
exclude_computed_fields: bool = False,
|
|
253
|
+
round_trip: bool = False,
|
|
254
|
+
warnings: bool | Literal["none", "warn", "error"] = True,
|
|
255
|
+
fallback: Callable[[Any], Any] | None = None,
|
|
256
|
+
serialize_as_any: bool = False,
|
|
257
|
+
) -> bytes:
|
|
258
|
+
data = self.model_dump(
|
|
259
|
+
mode=mode,
|
|
260
|
+
include=include,
|
|
261
|
+
exclude=exclude,
|
|
262
|
+
context=context,
|
|
263
|
+
by_alias=by_alias,
|
|
264
|
+
exclude_unset=exclude_unset,
|
|
265
|
+
exclude_defaults=exclude_defaults,
|
|
266
|
+
exclude_none=exclude_none,
|
|
267
|
+
exclude_computed_fields=exclude_computed_fields,
|
|
268
|
+
round_trip=round_trip,
|
|
269
|
+
warnings=warnings,
|
|
270
|
+
fallback=fallback,
|
|
271
|
+
serialize_as_any=serialize_as_any,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Only include fields that exist in struct
|
|
275
|
+
# noinspection PyProtectedMember
|
|
276
|
+
values = {
|
|
277
|
+
k: v for k, v in data.items()
|
|
278
|
+
if k in self.struct._subcons or k in self._computed_subcons
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return self._binary_ordered_struct.build(values)
|
|
282
|
+
|
|
283
|
+
@classmethod
|
|
284
|
+
async def model_validate_reader(
|
|
285
|
+
cls,
|
|
286
|
+
obj: StreamReader,
|
|
287
|
+
*,
|
|
288
|
+
strict: bool | None = None,
|
|
289
|
+
extra: main.ExtraValues | None = None,
|
|
290
|
+
from_attributes: bool | None = None,
|
|
291
|
+
context: Any | None = None,
|
|
292
|
+
by_alias: bool | None = None,
|
|
293
|
+
by_name: bool | None = None,
|
|
294
|
+
) -> Self:
|
|
295
|
+
data = await obj.readexactly(cls.struct.sizeof())
|
|
296
|
+
return cls.model_validate_bytes(
|
|
297
|
+
data,
|
|
298
|
+
strict=strict,
|
|
299
|
+
extra=extra,
|
|
300
|
+
from_attributes=from_attributes,
|
|
301
|
+
context=context,
|
|
302
|
+
by_alias=by_alias,
|
|
303
|
+
by_name=by_name,
|
|
304
|
+
)
|
|
File without changes
|