statica 1.2.0__tar.gz → 1.3.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.
- {statica-1.2.0/statica.egg-info → statica-1.3.0}/PKG-INFO +115 -22
- {statica-1.2.0 → statica-1.3.0}/README.md +113 -20
- {statica-1.2.0 → statica-1.3.0}/pyproject.toml +3 -3
- {statica-1.2.0 → statica-1.3.0}/statica/core.py +124 -45
- statica-1.3.0/statica/validation.py +166 -0
- {statica-1.2.0 → statica-1.3.0/statica.egg-info}/PKG-INFO +115 -22
- {statica-1.2.0 → statica-1.3.0}/statica.egg-info/SOURCES.txt +5 -2
- statica-1.3.0/tests/test_descriptor.py +38 -0
- statica-1.3.0/tests/test_python_behavior.py +39 -0
- {statica-1.2.0 → statica-1.3.0}/tests/test_statica.py +35 -0
- statica-1.3.0/tests/test_validate_or_raise.py +673 -0
- statica-1.3.0/tests/test_validation.py +100 -0
- statica-1.2.0/statica/validators.py +0 -69
- statica-1.2.0/tests/test_validators.py +0 -24
- {statica-1.2.0 → statica-1.3.0}/LICENSE +0 -0
- {statica-1.2.0 → statica-1.3.0}/setup.cfg +0 -0
- {statica-1.2.0 → statica-1.3.0}/statica/__init__.py +0 -0
- {statica-1.2.0 → statica-1.3.0}/statica/exceptions.py +0 -0
- {statica-1.2.0 → statica-1.3.0}/statica.egg-info/dependency_links.txt +0 -0
- {statica-1.2.0 → statica-1.3.0}/statica.egg-info/top_level.txt +0 -0
- {statica-1.2.0 → statica-1.3.0}/tests/test_aliasing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statica
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: A minimalistic data validation library
|
|
5
5
|
Author-email: Marcel Kröker <kroeker.marcel@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -31,10 +31,11 @@ Classifier: Operating System :: OS Independent
|
|
|
31
31
|
Classifier: Intended Audience :: Developers
|
|
32
32
|
Classifier: Programming Language :: Python
|
|
33
33
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
-
Requires-Python: <3.14,>=3.
|
|
34
|
+
Requires-Python: <3.14,>=3.11
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Statica
|
|
38
|
+
========================================================================================
|
|
38
39
|
|
|
39
40
|

|
|
40
41
|

|
|
@@ -42,17 +43,21 @@ Description-Content-Type: text/markdown
|
|
|
42
43
|
|
|
43
44
|
Statica is a Python library for defining and validating structured data with type annotations and constraints. It provides an easy-to-use framework for creating type-safe models with comprehensive validation for both types and constraints.
|
|
44
45
|
|
|
45
|
-
## Why Statica?
|
|
46
46
|
|
|
47
|
-
Statica
|
|
47
|
+
Why Statica?
|
|
48
|
+
----------------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like Pydantic.
|
|
48
51
|
While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
|
|
49
52
|
|
|
50
|
-
1. **Lightweight**: Statica
|
|
53
|
+
1. **Lightweight**: Statica has zero dependencies, making it ideal for projects where minimizing external dependencies is a priority.
|
|
51
54
|
2. **Performance**: For use cases where performance is critical. Pydantic needs `3x` more memory than Statica for the same models.
|
|
52
|
-
3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic
|
|
53
|
-
4. **Customizable
|
|
55
|
+
3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic and complexity of Pydantic.
|
|
56
|
+
4. **Customizable**: Statica allows fine-grained control over type and constraint validation through customizable fields and error classes.
|
|
57
|
+
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
Features
|
|
60
|
+
----------------------------------------------------------------------------------------
|
|
56
61
|
|
|
57
62
|
- **Type Validation**: Automatically validates types for attributes based on type hints.
|
|
58
63
|
- **Constraint Validation**: Define constraints like minimum/maximum length, value ranges, and more.
|
|
@@ -62,8 +67,11 @@ While pydantic is a powerful tool for data validation and parsing, Statica offer
|
|
|
62
67
|
- **Automatic Initialization**: Automatically generate constructors (`__init__`) for your models.
|
|
63
68
|
- **String Manipulation**: Strip whitespace from string fields if needed.
|
|
64
69
|
- **Casting**: Automatically cast values to the desired type.
|
|
70
|
+
- **Field Aliasing**: Support for field aliases for parsing and serialization.
|
|
65
71
|
|
|
66
|
-
|
|
72
|
+
|
|
73
|
+
Installation
|
|
74
|
+
----------------------------------------------------------------------------------------
|
|
67
75
|
|
|
68
76
|
You can install Statica via pip:
|
|
69
77
|
|
|
@@ -71,7 +79,9 @@ You can install Statica via pip:
|
|
|
71
79
|
pip install statica
|
|
72
80
|
```
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
|
|
83
|
+
Getting Started
|
|
84
|
+
----------------------------------------------------------------------------------------
|
|
75
85
|
|
|
76
86
|
### Basic Usage
|
|
77
87
|
|
|
@@ -177,20 +187,98 @@ except CustomError as e:
|
|
|
177
187
|
print(e) # Output: "num: must be at least 1"
|
|
178
188
|
```
|
|
179
189
|
|
|
180
|
-
|
|
190
|
+
Or, define a BaseClass which configures the error classes globally:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from statica.core import Statica, ConstraintValidationError, TypeValidationError
|
|
194
|
+
|
|
195
|
+
class BaseClass(Statica):
|
|
196
|
+
constraint_error_class = ConstraintValidationError
|
|
197
|
+
type_error_class = TypeValidationError
|
|
198
|
+
|
|
199
|
+
class CustomPayload(BaseClass):
|
|
200
|
+
num: int = Field(min_value=1, max_value=10)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
payload = CustomPayload(num=0)
|
|
204
|
+
except ConstraintValidationError as e:
|
|
205
|
+
print(e) # Output: "num: must be at least 1"
|
|
206
|
+
```
|
|
207
|
+
|
|
181
208
|
|
|
182
|
-
|
|
209
|
+
Aliasing
|
|
210
|
+
----------------------------------------------------------------------------------------
|
|
183
211
|
|
|
184
|
-
|
|
212
|
+
Statica supports field aliasing, allowing you to map different field names for parsing and serialization.
|
|
213
|
+
This is particularly useful when working with external APIs that use different naming conventions.
|
|
214
|
+
|
|
215
|
+
### Basic Aliases
|
|
216
|
+
|
|
217
|
+
Use the `alias` parameter to define an alternative name for both parsing and serialization:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
class User(Statica):
|
|
221
|
+
full_name: str = Field(alias="fullName")
|
|
222
|
+
age: int = Field(alias="userAge")
|
|
223
|
+
|
|
224
|
+
# Parse data with aliases
|
|
225
|
+
data = {"fullName": "John Doe", "userAge": 30}
|
|
226
|
+
user = User.from_map(data)
|
|
227
|
+
print(user.full_name) # Output: "John Doe"
|
|
228
|
+
print(user.age) # Output: 30
|
|
229
|
+
|
|
230
|
+
# Serialize back with aliases
|
|
231
|
+
result = user.to_dict()
|
|
232
|
+
print(result) # Output: {"fullName": "John Doe", "userAge": 30}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Separate Parsing and Serialization Aliases
|
|
236
|
+
|
|
237
|
+
You can define different aliases for parsing and serialization:
|
|
185
238
|
|
|
186
239
|
```python
|
|
187
|
-
class
|
|
188
|
-
|
|
240
|
+
class APIModel(Statica):
|
|
241
|
+
user_name: str = Field(
|
|
242
|
+
alias_for_parsing="userName",
|
|
243
|
+
alias_for_serialization="username"
|
|
244
|
+
)
|
|
245
|
+
user_id: int = Field(alias_for_parsing="userId")
|
|
246
|
+
|
|
247
|
+
# Parse from camelCase API response
|
|
248
|
+
api_data = {"userName": "alice", "userId": 123}
|
|
249
|
+
model = APIModel.from_map(api_data)
|
|
250
|
+
|
|
251
|
+
# Serialize to snake_case for internal use
|
|
252
|
+
internal_data = model.to_dict()
|
|
253
|
+
print(internal_data) # Output: {"username": "alice", "user_id": 123}
|
|
254
|
+
```
|
|
189
255
|
|
|
190
|
-
|
|
191
|
-
|
|
256
|
+
### Alias Priority
|
|
257
|
+
|
|
258
|
+
When multiple alias types are defined, the priority is:
|
|
259
|
+
1. `alias_for_parsing` for parsing operations
|
|
260
|
+
2. `alias_for_serialization` for serialization operations
|
|
261
|
+
3. `alias` as a fallback for both operations
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
class PriorityExample(Statica):
|
|
265
|
+
field_name: str = Field(
|
|
266
|
+
alias="generalAlias",
|
|
267
|
+
alias_for_parsing="parseAlias",
|
|
268
|
+
alias_for_serialization="serializeAlias"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Uses alias_for_parsing
|
|
272
|
+
instance = PriorityExample.from_map({"parseAlias": "value"})
|
|
273
|
+
|
|
274
|
+
# Uses alias_for_serialization
|
|
275
|
+
result = instance.to_dict()
|
|
276
|
+
print(result) # Output: {"serializeAlias": "value"}
|
|
192
277
|
```
|
|
193
278
|
|
|
279
|
+
Advanced Usage
|
|
280
|
+
----------------------------------------------------------------------------------------
|
|
281
|
+
|
|
194
282
|
### Custom Initialization
|
|
195
283
|
|
|
196
284
|
Statica automatically generates an `__init__` method based on type annotations, ensuring that all required fields are provided during initialization.
|
|
@@ -207,7 +295,9 @@ instance = CastingExample(num="42")
|
|
|
207
295
|
print(instance.num) # Output: 42
|
|
208
296
|
```
|
|
209
297
|
|
|
210
|
-
|
|
298
|
+
|
|
299
|
+
Contributing
|
|
300
|
+
----------------------------------------------------------------------------------------
|
|
211
301
|
|
|
212
302
|
We welcome contributions to Statica! To contribute:
|
|
213
303
|
|
|
@@ -216,11 +306,14 @@ We welcome contributions to Statica! To contribute:
|
|
|
216
306
|
3. Write tests for your changes.
|
|
217
307
|
4. Submit a pull request.
|
|
218
308
|
|
|
219
|
-
|
|
309
|
+
|
|
310
|
+
License
|
|
311
|
+
----------------------------------------------------------------------------------------
|
|
220
312
|
|
|
221
313
|
Statica is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
222
314
|
|
|
223
|
-
## Acknowledgments
|
|
224
315
|
|
|
225
|
-
|
|
316
|
+
Acknowledgments
|
|
317
|
+
----------------------------------------------------------------------------------------
|
|
226
318
|
|
|
319
|
+
Statica was built to simplify data validation and provide a robust and simple framework for type-safe models in Python, inspired by `pydantic` and `dataclasses`.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
Statica
|
|
2
|
+
========================================================================================
|
|
2
3
|
|
|
3
4
|

|
|
4
5
|

|
|
@@ -6,17 +7,21 @@
|
|
|
6
7
|
|
|
7
8
|
Statica is a Python library for defining and validating structured data with type annotations and constraints. It provides an easy-to-use framework for creating type-safe models with comprehensive validation for both types and constraints.
|
|
8
9
|
|
|
9
|
-
## Why Statica?
|
|
10
10
|
|
|
11
|
-
Statica
|
|
11
|
+
Why Statica?
|
|
12
|
+
----------------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to libraries like Pydantic.
|
|
12
15
|
While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
|
|
13
16
|
|
|
14
|
-
1. **Lightweight**: Statica
|
|
17
|
+
1. **Lightweight**: Statica has zero dependencies, making it ideal for projects where minimizing external dependencies is a priority.
|
|
15
18
|
2. **Performance**: For use cases where performance is critical. Pydantic needs `3x` more memory than Statica for the same models.
|
|
16
|
-
3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic
|
|
17
|
-
4. **Customizable
|
|
19
|
+
3. **Ease of Use**: With its simple, Pythonic design, Statica is intuitive for developers already familiar with Python's `dataclasses` and type hinting. It avoids much of the magic and complexity of Pydantic.
|
|
20
|
+
4. **Customizable**: Statica allows fine-grained control over type and constraint validation through customizable fields and error classes.
|
|
21
|
+
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
Features
|
|
24
|
+
----------------------------------------------------------------------------------------
|
|
20
25
|
|
|
21
26
|
- **Type Validation**: Automatically validates types for attributes based on type hints.
|
|
22
27
|
- **Constraint Validation**: Define constraints like minimum/maximum length, value ranges, and more.
|
|
@@ -26,8 +31,11 @@ While pydantic is a powerful tool for data validation and parsing, Statica offer
|
|
|
26
31
|
- **Automatic Initialization**: Automatically generate constructors (`__init__`) for your models.
|
|
27
32
|
- **String Manipulation**: Strip whitespace from string fields if needed.
|
|
28
33
|
- **Casting**: Automatically cast values to the desired type.
|
|
34
|
+
- **Field Aliasing**: Support for field aliases for parsing and serialization.
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
|
|
37
|
+
Installation
|
|
38
|
+
----------------------------------------------------------------------------------------
|
|
31
39
|
|
|
32
40
|
You can install Statica via pip:
|
|
33
41
|
|
|
@@ -35,7 +43,9 @@ You can install Statica via pip:
|
|
|
35
43
|
pip install statica
|
|
36
44
|
```
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
|
|
47
|
+
Getting Started
|
|
48
|
+
----------------------------------------------------------------------------------------
|
|
39
49
|
|
|
40
50
|
### Basic Usage
|
|
41
51
|
|
|
@@ -141,20 +151,98 @@ except CustomError as e:
|
|
|
141
151
|
print(e) # Output: "num: must be at least 1"
|
|
142
152
|
```
|
|
143
153
|
|
|
144
|
-
|
|
154
|
+
Or, define a BaseClass which configures the error classes globally:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from statica.core import Statica, ConstraintValidationError, TypeValidationError
|
|
158
|
+
|
|
159
|
+
class BaseClass(Statica):
|
|
160
|
+
constraint_error_class = ConstraintValidationError
|
|
161
|
+
type_error_class = TypeValidationError
|
|
162
|
+
|
|
163
|
+
class CustomPayload(BaseClass):
|
|
164
|
+
num: int = Field(min_value=1, max_value=10)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
payload = CustomPayload(num=0)
|
|
168
|
+
except ConstraintValidationError as e:
|
|
169
|
+
print(e) # Output: "num: must be at least 1"
|
|
170
|
+
```
|
|
171
|
+
|
|
145
172
|
|
|
146
|
-
|
|
173
|
+
Aliasing
|
|
174
|
+
----------------------------------------------------------------------------------------
|
|
147
175
|
|
|
148
|
-
|
|
176
|
+
Statica supports field aliasing, allowing you to map different field names for parsing and serialization.
|
|
177
|
+
This is particularly useful when working with external APIs that use different naming conventions.
|
|
178
|
+
|
|
179
|
+
### Basic Aliases
|
|
180
|
+
|
|
181
|
+
Use the `alias` parameter to define an alternative name for both parsing and serialization:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
class User(Statica):
|
|
185
|
+
full_name: str = Field(alias="fullName")
|
|
186
|
+
age: int = Field(alias="userAge")
|
|
187
|
+
|
|
188
|
+
# Parse data with aliases
|
|
189
|
+
data = {"fullName": "John Doe", "userAge": 30}
|
|
190
|
+
user = User.from_map(data)
|
|
191
|
+
print(user.full_name) # Output: "John Doe"
|
|
192
|
+
print(user.age) # Output: 30
|
|
193
|
+
|
|
194
|
+
# Serialize back with aliases
|
|
195
|
+
result = user.to_dict()
|
|
196
|
+
print(result) # Output: {"fullName": "John Doe", "userAge": 30}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Separate Parsing and Serialization Aliases
|
|
200
|
+
|
|
201
|
+
You can define different aliases for parsing and serialization:
|
|
149
202
|
|
|
150
203
|
```python
|
|
151
|
-
class
|
|
152
|
-
|
|
204
|
+
class APIModel(Statica):
|
|
205
|
+
user_name: str = Field(
|
|
206
|
+
alias_for_parsing="userName",
|
|
207
|
+
alias_for_serialization="username"
|
|
208
|
+
)
|
|
209
|
+
user_id: int = Field(alias_for_parsing="userId")
|
|
210
|
+
|
|
211
|
+
# Parse from camelCase API response
|
|
212
|
+
api_data = {"userName": "alice", "userId": 123}
|
|
213
|
+
model = APIModel.from_map(api_data)
|
|
214
|
+
|
|
215
|
+
# Serialize to snake_case for internal use
|
|
216
|
+
internal_data = model.to_dict()
|
|
217
|
+
print(internal_data) # Output: {"username": "alice", "user_id": 123}
|
|
218
|
+
```
|
|
153
219
|
|
|
154
|
-
|
|
155
|
-
|
|
220
|
+
### Alias Priority
|
|
221
|
+
|
|
222
|
+
When multiple alias types are defined, the priority is:
|
|
223
|
+
1. `alias_for_parsing` for parsing operations
|
|
224
|
+
2. `alias_for_serialization` for serialization operations
|
|
225
|
+
3. `alias` as a fallback for both operations
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
class PriorityExample(Statica):
|
|
229
|
+
field_name: str = Field(
|
|
230
|
+
alias="generalAlias",
|
|
231
|
+
alias_for_parsing="parseAlias",
|
|
232
|
+
alias_for_serialization="serializeAlias"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Uses alias_for_parsing
|
|
236
|
+
instance = PriorityExample.from_map({"parseAlias": "value"})
|
|
237
|
+
|
|
238
|
+
# Uses alias_for_serialization
|
|
239
|
+
result = instance.to_dict()
|
|
240
|
+
print(result) # Output: {"serializeAlias": "value"}
|
|
156
241
|
```
|
|
157
242
|
|
|
243
|
+
Advanced Usage
|
|
244
|
+
----------------------------------------------------------------------------------------
|
|
245
|
+
|
|
158
246
|
### Custom Initialization
|
|
159
247
|
|
|
160
248
|
Statica automatically generates an `__init__` method based on type annotations, ensuring that all required fields are provided during initialization.
|
|
@@ -171,7 +259,9 @@ instance = CastingExample(num="42")
|
|
|
171
259
|
print(instance.num) # Output: 42
|
|
172
260
|
```
|
|
173
261
|
|
|
174
|
-
|
|
262
|
+
|
|
263
|
+
Contributing
|
|
264
|
+
----------------------------------------------------------------------------------------
|
|
175
265
|
|
|
176
266
|
We welcome contributions to Statica! To contribute:
|
|
177
267
|
|
|
@@ -180,11 +270,14 @@ We welcome contributions to Statica! To contribute:
|
|
|
180
270
|
3. Write tests for your changes.
|
|
181
271
|
4. Submit a pull request.
|
|
182
272
|
|
|
183
|
-
|
|
273
|
+
|
|
274
|
+
License
|
|
275
|
+
----------------------------------------------------------------------------------------
|
|
184
276
|
|
|
185
277
|
Statica is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
|
|
186
278
|
|
|
187
|
-
## Acknowledgments
|
|
188
279
|
|
|
189
|
-
|
|
280
|
+
Acknowledgments
|
|
281
|
+
----------------------------------------------------------------------------------------
|
|
190
282
|
|
|
283
|
+
Statica was built to simplify data validation and provide a robust and simple framework for type-safe models in Python, inspired by `pydantic` and `dataclasses`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "statica"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "A minimalistic data validation library"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Marcel Kröker", email = "kroeker.marcel@gmail.com" }]
|
|
@@ -13,7 +13,7 @@ classifiers=[
|
|
|
13
13
|
"Programming Language :: Python",
|
|
14
14
|
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
15
15
|
]
|
|
16
|
-
requires-python = ">=3.
|
|
16
|
+
requires-python = ">=3.11, <3.14"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
########################################################################################
|
|
@@ -51,7 +51,7 @@ package = true
|
|
|
51
51
|
|
|
52
52
|
[tool.ruff]
|
|
53
53
|
show-fixes = true
|
|
54
|
-
line-length =
|
|
54
|
+
line-length = 100
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
[tool.ruff.lint]
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from dataclasses import field as dataclass_field
|
|
5
|
+
from types import UnionType
|
|
5
6
|
from typing import (
|
|
6
7
|
TYPE_CHECKING,
|
|
7
8
|
Any,
|
|
@@ -11,11 +12,10 @@ from typing import (
|
|
|
11
12
|
cast,
|
|
12
13
|
dataclass_transform,
|
|
13
14
|
get_type_hints,
|
|
14
|
-
overload,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
from statica.exceptions import ConstraintValidationError, TypeValidationError
|
|
18
|
-
from statica.
|
|
18
|
+
from statica.validation import validate_constraints, validate_or_raise
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from collections.abc import Callable, Mapping
|
|
@@ -27,15 +27,53 @@ T = TypeVar("T")
|
|
|
27
27
|
#### MARK: Field descriptor
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
@dataclass
|
|
30
|
+
@dataclass(slots=True)
|
|
31
31
|
class FieldDescriptor(Generic[T]):
|
|
32
32
|
"""
|
|
33
|
-
Descriptor for
|
|
33
|
+
Descriptor for statica fields. They will be used to validate fields:
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
.. code-block:: python
|
|
37
|
+
class User(Statica):
|
|
38
|
+
name: str # Will be initialized as FieldDescriptor during class creation
|
|
39
|
+
age: int | None = Field(min_value=0, max_value=120)
|
|
34
40
|
"""
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
# Descriptor attributes
|
|
43
|
+
|
|
44
|
+
name: str = dataclass_field(init=False)
|
|
45
|
+
"""
|
|
46
|
+
Name of the field in the class.
|
|
47
|
+
Example: `"age"` for the field `age: int | None`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
owner: type = dataclass_field(init=False)
|
|
51
|
+
"""
|
|
52
|
+
Owner class of the field descriptor.
|
|
53
|
+
Example: `<class '__main__.User'>` for the field `age: int | None`.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
expected_type: Any = dataclass_field(init=False)
|
|
57
|
+
"""
|
|
58
|
+
Expected type of the field.
|
|
59
|
+
Example 1: `int | None` (of type UnionType) for the field `age: int | None`.
|
|
60
|
+
Example 2: `<class 'int'>` for a field `number: int`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
sub_types: tuple[type, ...] = dataclass_field(init=False, default_factory=tuple)
|
|
64
|
+
"""
|
|
65
|
+
List of types that expected_type is composed of.
|
|
66
|
+
If expected_type is not of type UnionType, then it is just a tuple with one element.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
statica_sub_class: type[Statica] | None = dataclass_field(init=False, repr=False)
|
|
70
|
+
"""
|
|
71
|
+
Statica subclass if the expected_type is a Statica subclass or a union where one
|
|
72
|
+
of the types is a Statica subclass.
|
|
73
|
+
For example, if expected_type is `Union[Statica, int]`, then this will be `Statica`.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
# User-facing dataclass fields
|
|
39
77
|
|
|
40
78
|
min_length: int | None = None
|
|
41
79
|
max_length: int | None = None
|
|
@@ -51,44 +89,79 @@ class FieldDescriptor(Generic[T]):
|
|
|
51
89
|
def __set_name__(self, owner: Any, name: str) -> None:
|
|
52
90
|
self.name = name
|
|
53
91
|
self.owner = owner
|
|
54
|
-
self.expected_type = owner
|
|
92
|
+
self.expected_type = get_type_hints(owner)[name]
|
|
93
|
+
self.sub_types = self.get_sub_types(self.expected_type)
|
|
94
|
+
self.statica_sub_class = self.get_statica_subclass(self.sub_types)
|
|
95
|
+
|
|
96
|
+
def get_sub_types(self, expected_type: Any) -> tuple[type | Any, ...]:
|
|
97
|
+
"""
|
|
98
|
+
Get all subtypes of the expected type.
|
|
99
|
+
For example, if the expected type is Union[str, int], return [str, int].
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
if type(expected_type) is UnionType:
|
|
103
|
+
return tuple(t for t in expected_type.__args__)
|
|
55
104
|
|
|
56
|
-
|
|
57
|
-
def __get__(self, instance: None, owner: Any) -> FieldDescriptor[T]: ...
|
|
105
|
+
return (expected_type,)
|
|
58
106
|
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
def get_statica_subclass(self, sub_types: tuple[type, ...]) -> type[Statica] | None:
|
|
108
|
+
"""
|
|
109
|
+
Get the first Statica subclass from the sub_types.
|
|
110
|
+
Returns None if no Statica subclass is found.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
for sub_type in sub_types:
|
|
115
|
+
if issubclass(sub_type, Statica):
|
|
116
|
+
return sub_type
|
|
117
|
+
except TypeError:
|
|
118
|
+
pass
|
|
119
|
+
return None
|
|
61
120
|
|
|
62
121
|
def __get__(self, instance: object | None, owner: Any) -> Any:
|
|
122
|
+
"""
|
|
123
|
+
Get the value of the field from the instance.
|
|
124
|
+
For example user.age will invoke this method.
|
|
125
|
+
"""
|
|
126
|
+
|
|
63
127
|
if instance is None:
|
|
64
128
|
return self # Accessed on the class, return the descriptor
|
|
65
129
|
return instance.__dict__.get(self.name)
|
|
66
130
|
|
|
67
131
|
def __set__(self, instance: object, value: T) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Set the value of the field on the instance.
|
|
134
|
+
For example user.age = 30 will invoke this method.
|
|
135
|
+
"""
|
|
136
|
+
|
|
68
137
|
instance.__dict__[self.name] = self.validate(value)
|
|
69
138
|
|
|
70
139
|
def validate(self, value: Any) -> Any:
|
|
71
140
|
try:
|
|
141
|
+
# (1/4) Cast to required type if cast_to is provided
|
|
142
|
+
|
|
72
143
|
if self.cast_to is not None:
|
|
73
144
|
value = self.cast_to(value)
|
|
74
145
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
self.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
146
|
+
# (2/4) On dict assignments initialize Statica subclass if it exists
|
|
147
|
+
|
|
148
|
+
if self.statica_sub_class is not None and isinstance(value, dict):
|
|
149
|
+
value = self.statica_sub_class.from_map(value)
|
|
150
|
+
|
|
151
|
+
# (3/4) Validate type of the value
|
|
152
|
+
|
|
153
|
+
validate_or_raise(value, self.expected_type)
|
|
154
|
+
|
|
155
|
+
# (4/4) Validate constraints if any are set
|
|
156
|
+
|
|
157
|
+
value = validate_constraints(
|
|
158
|
+
value,
|
|
159
|
+
min_length=self.min_length,
|
|
160
|
+
max_length=self.max_length,
|
|
161
|
+
min_value=self.min_value,
|
|
162
|
+
max_value=self.max_value,
|
|
163
|
+
strip_whitespace=self.strip_whitespace,
|
|
164
|
+
)
|
|
92
165
|
|
|
93
166
|
except TypeValidationError as e:
|
|
94
167
|
msg = f"{self.name}: {e!s}"
|
|
@@ -185,11 +258,7 @@ class StaticaMeta(type):
|
|
|
185
258
|
# Generate custom __init__ method
|
|
186
259
|
|
|
187
260
|
def statica_init(self: Statica, **kwargs: Any) -> None:
|
|
188
|
-
for field_name
|
|
189
|
-
if field_name not in kwargs and not isinstance(None, field_type):
|
|
190
|
-
msg = f"Missing required field: {field_name}"
|
|
191
|
-
raise TypeValidationError(msg)
|
|
192
|
-
|
|
261
|
+
for field_name in annotations:
|
|
193
262
|
setattr(self, field_name, kwargs.get(field_name))
|
|
194
263
|
|
|
195
264
|
namespace["__init__"] = statica_init
|
|
@@ -218,23 +287,24 @@ class StaticaMeta(type):
|
|
|
218
287
|
class Statica(metaclass=StaticaMeta):
|
|
219
288
|
@classmethod
|
|
220
289
|
def from_map(cls, mapping: Mapping[str, Any]) -> Self:
|
|
221
|
-
|
|
290
|
+
# Fields might have aliases, so we need to map them correctly.
|
|
291
|
+
# Here we map the chosen alias to the original field name.
|
|
292
|
+
# If no alias is provided, we use the field name itself.
|
|
293
|
+
# Priority: parsing alias > general alias > field name
|
|
294
|
+
mapping_key_to_field_keys = {}
|
|
222
295
|
|
|
223
296
|
for field_descriptor in get_field_descriptors(cls):
|
|
224
297
|
# Use alias for parsing if it exists
|
|
225
|
-
alias =
|
|
298
|
+
alias = (
|
|
299
|
+
field_descriptor.alias_for_parsing
|
|
300
|
+
or field_descriptor.alias
|
|
301
|
+
or field_descriptor.name
|
|
302
|
+
)
|
|
226
303
|
mapping_key_to_field_keys[alias] = field_descriptor.name
|
|
227
304
|
|
|
228
305
|
parsed_mapping = {mapping_key_to_field_keys[k]: v for k, v in mapping.items()}
|
|
229
306
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
# Go through type hints and set values
|
|
233
|
-
for attribute_name in get_type_hints(instance.__class__):
|
|
234
|
-
value = parsed_mapping.get(attribute_name)
|
|
235
|
-
setattr(instance, attribute_name, value) # Descriptor __set__ validates
|
|
236
|
-
|
|
237
|
-
return instance
|
|
307
|
+
return cls(**parsed_mapping) # Init function will validate fields
|
|
238
308
|
|
|
239
309
|
def to_dict(self) -> dict[str, Any]:
|
|
240
310
|
"""
|
|
@@ -243,7 +313,11 @@ class Statica(metaclass=StaticaMeta):
|
|
|
243
313
|
result = {}
|
|
244
314
|
for field_descriptor in get_field_descriptors(self.__class__):
|
|
245
315
|
# Use alias for serialization if it exists
|
|
246
|
-
alias =
|
|
316
|
+
alias = (
|
|
317
|
+
field_descriptor.alias_for_serialization
|
|
318
|
+
or field_descriptor.alias
|
|
319
|
+
or field_descriptor.name
|
|
320
|
+
)
|
|
247
321
|
result[alias] = getattr(self, field_descriptor.name)
|
|
248
322
|
|
|
249
323
|
return result
|
|
@@ -254,6 +328,11 @@ class Statica(metaclass=StaticaMeta):
|
|
|
254
328
|
|
|
255
329
|
if __name__ == "__main__":
|
|
256
330
|
|
|
331
|
+
class User(Statica):
|
|
332
|
+
data: dict[str, int]
|
|
333
|
+
|
|
334
|
+
u = User.from_map({})
|
|
335
|
+
|
|
257
336
|
class Payload(Statica):
|
|
258
337
|
type_error_class = ValueError
|
|
259
338
|
|