statica 1.3.0__tar.gz → 1.6.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.3.0 → statica-1.6.0}/PKG-INFO +131 -77
- statica-1.3.0/statica.egg-info/PKG-INFO → statica-1.6.0/README.md +107 -89
- {statica-1.3.0 → statica-1.6.0}/pyproject.toml +23 -22
- statica-1.6.0/statica/config.py +33 -0
- {statica-1.3.0 → statica-1.6.0}/statica/core.py +157 -65
- statica-1.6.0/statica/py.typed +0 -0
- statica-1.6.0/statica/validation.py +296 -0
- statica-1.3.0/README.md +0 -283
- statica-1.3.0/setup.cfg +0 -4
- statica-1.3.0/statica/validation.py +0 -166
- statica-1.3.0/statica.egg-info/SOURCES.txt +0 -17
- statica-1.3.0/statica.egg-info/dependency_links.txt +0 -1
- statica-1.3.0/statica.egg-info/top_level.txt +0 -1
- statica-1.3.0/tests/test_aliasing.py +0 -163
- statica-1.3.0/tests/test_descriptor.py +0 -38
- statica-1.3.0/tests/test_python_behavior.py +0 -39
- statica-1.3.0/tests/test_statica.py +0 -211
- statica-1.3.0/tests/test_validate_or_raise.py +0 -673
- statica-1.3.0/tests/test_validation.py +0 -100
- {statica-1.3.0 → statica-1.6.0}/LICENSE +0 -0
- {statica-1.3.0 → statica-1.6.0}/statica/__init__.py +0 -0
- {statica-1.3.0 → statica-1.6.0}/statica/exceptions.py +0 -0
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: statica
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: A minimalistic data validation library
|
|
5
|
+
Author: Marcel Kröker
|
|
5
6
|
Author-email: Marcel Kröker <kroeker.marcel@gmail.com>
|
|
6
7
|
License: MIT License
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 mkrd
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
28
|
Classifier: Programming Language :: Python :: 3
|
|
29
29
|
Classifier: License :: OSI Approved :: MIT License
|
|
30
30
|
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:
|
|
34
|
+
Requires-Python: >=3.11, <3.15
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
|
|
37
37
|
Statica
|
|
38
38
|
========================================================================================
|
|
39
39
|
|
|
40
|
-

|
|
41
|
+

|
|
42
42
|
|
|
43
43
|
|
|
44
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.
|
|
@@ -47,7 +47,7 @@ Statica is a Python library for defining and validating structured data with typ
|
|
|
47
47
|
Why Statica?
|
|
48
48
|
----------------------------------------------------------------------------------------
|
|
49
49
|
|
|
50
|
-
Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to
|
|
50
|
+
Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to Pydantic.
|
|
51
51
|
While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
|
|
52
52
|
|
|
53
53
|
1. **Lightweight**: Statica has zero dependencies, making it ideal for projects where minimizing external dependencies is a priority.
|
|
@@ -61,6 +61,7 @@ Features
|
|
|
61
61
|
|
|
62
62
|
- **Type Validation**: Automatically validates types for attributes based on type hints.
|
|
63
63
|
- **Constraint Validation**: Define constraints like minimum/maximum length, value ranges, and more.
|
|
64
|
+
- **Default Field Values**: Set default values for fields that are used when not explicitly provided.
|
|
64
65
|
- **Customizable Error Handling**: Use custom exception classes for type and constraint errors.
|
|
65
66
|
- **Flexible Field Descriptors**: Add constraints, casting, and other behaviors to your fields.
|
|
66
67
|
- **Optional Fields**: Support for optional fields with default values.
|
|
@@ -152,13 +153,92 @@ payload = OptionalPayload()
|
|
|
152
153
|
print(payload.name) # Output: None
|
|
153
154
|
```
|
|
154
155
|
|
|
156
|
+
### Default Field Values
|
|
157
|
+
|
|
158
|
+
You can specify default values for fields in two ways:
|
|
159
|
+
|
|
160
|
+
#### Using Field() with default parameter
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
class User(Statica):
|
|
164
|
+
name: str
|
|
165
|
+
age: int = Field(default=25)
|
|
166
|
+
active: bool = Field(default=True)
|
|
167
|
+
|
|
168
|
+
# Using direct initialization
|
|
169
|
+
user = User(name="John")
|
|
170
|
+
print(user.name) # Output: "John"
|
|
171
|
+
print(user.age) # Output: 25
|
|
172
|
+
print(user.active) # Output: True
|
|
173
|
+
|
|
174
|
+
# Using from_map
|
|
175
|
+
user = User.from_map({"name": "Jane"})
|
|
176
|
+
print(user.age) # Output: 25 (default used)
|
|
177
|
+
|
|
178
|
+
# Explicit values override defaults
|
|
179
|
+
user = User(name="Bob", age=30, active=False)
|
|
180
|
+
print(user.age) # Output: 30
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Direct assignment (without Field)
|
|
184
|
+
|
|
185
|
+
You can also assign default values directly to fields without using `Field()`:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
class Config(Statica):
|
|
189
|
+
name: str
|
|
190
|
+
timeout: float = 30.0 # Direct assignment
|
|
191
|
+
retries: int = 3 # Direct assignment
|
|
192
|
+
debug: bool = False # Direct assignment
|
|
193
|
+
|
|
194
|
+
config = Config(name="server")
|
|
195
|
+
print(config.timeout) # Output: 30.0
|
|
196
|
+
print(config.retries) # Output: 3
|
|
197
|
+
print(config.debug) # Output: False
|
|
198
|
+
|
|
199
|
+
# Works with from_map too
|
|
200
|
+
config = Config.from_map({"name": "api-server"})
|
|
201
|
+
print(config.timeout) # Output: 30.0 (default used)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Both approaches work identically and can be mixed within the same class. Use `Field(default=...)` when you need additional constraints or options, and direct assignment for simple defaults.
|
|
205
|
+
|
|
206
|
+
#### Validation and Safety
|
|
207
|
+
|
|
208
|
+
Default values are validated against any constraints you've defined:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
class Config(Statica):
|
|
212
|
+
timeout: float = Field(default=30.0, min_value=1.0, max_value=120.0)
|
|
213
|
+
retries: int = Field(default=3, min_value=1)
|
|
214
|
+
|
|
215
|
+
config = Config() # Uses defaults: timeout=30.0, retries=3
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
For mutable default values (like lists, dicts, sets), Statica automatically creates copies to prevent shared state issues:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
class UserProfile(Statica):
|
|
222
|
+
name: str
|
|
223
|
+
tags: list[str] = Field(default=[]) # or tags: list[str] = []
|
|
224
|
+
|
|
225
|
+
user1 = UserProfile(name="Alice")
|
|
226
|
+
user2 = UserProfile(name="Bob")
|
|
227
|
+
|
|
228
|
+
user1.tags.append("admin")
|
|
229
|
+
print(user1.tags) # Output: ["admin"]
|
|
230
|
+
print(user2.tags) # Output: [] (not affected)
|
|
231
|
+
```
|
|
232
|
+
|
|
155
233
|
### Field Constraints
|
|
156
234
|
|
|
157
|
-
You can specify constraints on fields:
|
|
235
|
+
You can specify constraints and options on fields:
|
|
158
236
|
|
|
237
|
+
- **Default Values**: `default` (using `Field()`) or direct assignment
|
|
159
238
|
- **String Constraints**: `min_length`, `max_length`, `strip_whitespace`
|
|
160
239
|
- **Numeric Constraints**: `min_value`, `max_value`
|
|
161
240
|
- **Casting**: `cast_to`
|
|
241
|
+
- **Aliasing**: `alias`
|
|
162
242
|
|
|
163
243
|
```python
|
|
164
244
|
class StringTest(Statica):
|
|
@@ -166,6 +246,15 @@ class StringTest(Statica):
|
|
|
166
246
|
|
|
167
247
|
class IntTest(Statica):
|
|
168
248
|
num: int = Field(min_value=1, max_value=10, cast_to=int)
|
|
249
|
+
|
|
250
|
+
class DefaultTest(Statica):
|
|
251
|
+
# Using Field() for defaults with constraints
|
|
252
|
+
status: str = Field(default="active")
|
|
253
|
+
priority: int = Field(default=1, min_value=1, max_value=5)
|
|
254
|
+
|
|
255
|
+
# Direct assignment for simple defaults
|
|
256
|
+
timeout: float = 30.0
|
|
257
|
+
retries: int = 3
|
|
169
258
|
```
|
|
170
259
|
|
|
171
260
|
### Custom Error Classes
|
|
@@ -212,68 +301,27 @@ Aliasing
|
|
|
212
301
|
Statica supports field aliasing, allowing you to map different field names for parsing and serialization.
|
|
213
302
|
This is particularly useful when working with external APIs that use different naming conventions.
|
|
214
303
|
|
|
215
|
-
### Basic Aliases
|
|
216
|
-
|
|
217
304
|
Use the `alias` parameter to define an alternative name for both parsing and serialization:
|
|
218
305
|
|
|
219
306
|
```python
|
|
220
307
|
class User(Statica):
|
|
221
308
|
full_name: str = Field(alias="fullName")
|
|
222
|
-
age: int = Field(alias="userAge")
|
|
309
|
+
age: int = Field(alias="userAge", default=25)
|
|
223
310
|
|
|
224
311
|
# Parse data with aliases
|
|
225
|
-
data = {"fullName": "John Doe"
|
|
312
|
+
data = {"fullName": "John Doe"} # userAge not provided, uses default
|
|
226
313
|
user = User.from_map(data)
|
|
227
314
|
print(user.full_name) # Output: "John Doe"
|
|
228
|
-
print(user.age) # Output:
|
|
315
|
+
print(user.age) # Output: 25
|
|
229
316
|
|
|
230
|
-
# Serialize back with aliases
|
|
317
|
+
# Serialize back with aliases (uses the alias for serialization by default)
|
|
231
318
|
result = user.to_dict()
|
|
232
|
-
print(result) # Output: {"fullName": "John Doe", "userAge":
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Separate Parsing and Serialization Aliases
|
|
319
|
+
print(result) # Output: {"fullName": "John Doe", "userAge": 25}
|
|
236
320
|
|
|
237
|
-
|
|
321
|
+
# Serialize without aliases
|
|
322
|
+
result_no_alias = user.to_dict(with_aliases=False)
|
|
323
|
+
print(result_no_alias) # Output: {"full_name": "John Doe", "age": 25}
|
|
238
324
|
|
|
239
|
-
```python
|
|
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
|
-
```
|
|
255
|
-
|
|
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"}
|
|
277
325
|
```
|
|
278
326
|
|
|
279
327
|
Advanced Usage
|
|
@@ -296,6 +344,12 @@ print(instance.num) # Output: 42
|
|
|
296
344
|
```
|
|
297
345
|
|
|
298
346
|
|
|
347
|
+
Design Decisions
|
|
348
|
+
----------------------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
- [Inheritance over Decorators](docs/decision-inheritance-over-decorators.md)
|
|
351
|
+
|
|
352
|
+
|
|
299
353
|
Contributing
|
|
300
354
|
----------------------------------------------------------------------------------------
|
|
301
355
|
|
|
@@ -1,44 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: statica
|
|
3
|
-
Version: 1.3.0
|
|
4
|
-
Summary: A minimalistic data validation library
|
|
5
|
-
Author-email: Marcel Kröker <kroeker.marcel@gmail.com>
|
|
6
|
-
License: MIT License
|
|
7
|
-
|
|
8
|
-
Copyright (c) 2025 mkrd
|
|
9
|
-
|
|
10
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
-
in the Software without restriction, including without limitation the rights
|
|
13
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
-
furnished to do so, subject to the following conditions:
|
|
16
|
-
|
|
17
|
-
The above copyright notice and this permission notice shall be included in all
|
|
18
|
-
copies or substantial portions of the Software.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
-
SOFTWARE.
|
|
27
|
-
|
|
28
|
-
Classifier: Programming Language :: Python :: 3
|
|
29
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
30
|
-
Classifier: Operating System :: OS Independent
|
|
31
|
-
Classifier: Intended Audience :: Developers
|
|
32
|
-
Classifier: Programming Language :: Python
|
|
33
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
34
|
-
Requires-Python: <3.14,>=3.11
|
|
35
|
-
Description-Content-Type: text/markdown
|
|
36
|
-
|
|
37
1
|
Statica
|
|
38
2
|
========================================================================================
|
|
39
3
|
|
|
40
|
-

|
|
5
|
+

|
|
42
6
|
|
|
43
7
|
|
|
44
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.
|
|
@@ -47,7 +11,7 @@ Statica is a Python library for defining and validating structured data with typ
|
|
|
47
11
|
Why Statica?
|
|
48
12
|
----------------------------------------------------------------------------------------
|
|
49
13
|
|
|
50
|
-
Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to
|
|
14
|
+
Statica was created to address the need for a lightweight, flexible, and dependency-free alternative to Pydantic.
|
|
51
15
|
While pydantic is a powerful tool for data validation and parsing, Statica offers some distinct advantages in specific situations:
|
|
52
16
|
|
|
53
17
|
1. **Lightweight**: Statica has zero dependencies, making it ideal for projects where minimizing external dependencies is a priority.
|
|
@@ -61,6 +25,7 @@ Features
|
|
|
61
25
|
|
|
62
26
|
- **Type Validation**: Automatically validates types for attributes based on type hints.
|
|
63
27
|
- **Constraint Validation**: Define constraints like minimum/maximum length, value ranges, and more.
|
|
28
|
+
- **Default Field Values**: Set default values for fields that are used when not explicitly provided.
|
|
64
29
|
- **Customizable Error Handling**: Use custom exception classes for type and constraint errors.
|
|
65
30
|
- **Flexible Field Descriptors**: Add constraints, casting, and other behaviors to your fields.
|
|
66
31
|
- **Optional Fields**: Support for optional fields with default values.
|
|
@@ -152,13 +117,92 @@ payload = OptionalPayload()
|
|
|
152
117
|
print(payload.name) # Output: None
|
|
153
118
|
```
|
|
154
119
|
|
|
120
|
+
### Default Field Values
|
|
121
|
+
|
|
122
|
+
You can specify default values for fields in two ways:
|
|
123
|
+
|
|
124
|
+
#### Using Field() with default parameter
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
class User(Statica):
|
|
128
|
+
name: str
|
|
129
|
+
age: int = Field(default=25)
|
|
130
|
+
active: bool = Field(default=True)
|
|
131
|
+
|
|
132
|
+
# Using direct initialization
|
|
133
|
+
user = User(name="John")
|
|
134
|
+
print(user.name) # Output: "John"
|
|
135
|
+
print(user.age) # Output: 25
|
|
136
|
+
print(user.active) # Output: True
|
|
137
|
+
|
|
138
|
+
# Using from_map
|
|
139
|
+
user = User.from_map({"name": "Jane"})
|
|
140
|
+
print(user.age) # Output: 25 (default used)
|
|
141
|
+
|
|
142
|
+
# Explicit values override defaults
|
|
143
|
+
user = User(name="Bob", age=30, active=False)
|
|
144
|
+
print(user.age) # Output: 30
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Direct assignment (without Field)
|
|
148
|
+
|
|
149
|
+
You can also assign default values directly to fields without using `Field()`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
class Config(Statica):
|
|
153
|
+
name: str
|
|
154
|
+
timeout: float = 30.0 # Direct assignment
|
|
155
|
+
retries: int = 3 # Direct assignment
|
|
156
|
+
debug: bool = False # Direct assignment
|
|
157
|
+
|
|
158
|
+
config = Config(name="server")
|
|
159
|
+
print(config.timeout) # Output: 30.0
|
|
160
|
+
print(config.retries) # Output: 3
|
|
161
|
+
print(config.debug) # Output: False
|
|
162
|
+
|
|
163
|
+
# Works with from_map too
|
|
164
|
+
config = Config.from_map({"name": "api-server"})
|
|
165
|
+
print(config.timeout) # Output: 30.0 (default used)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Both approaches work identically and can be mixed within the same class. Use `Field(default=...)` when you need additional constraints or options, and direct assignment for simple defaults.
|
|
169
|
+
|
|
170
|
+
#### Validation and Safety
|
|
171
|
+
|
|
172
|
+
Default values are validated against any constraints you've defined:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
class Config(Statica):
|
|
176
|
+
timeout: float = Field(default=30.0, min_value=1.0, max_value=120.0)
|
|
177
|
+
retries: int = Field(default=3, min_value=1)
|
|
178
|
+
|
|
179
|
+
config = Config() # Uses defaults: timeout=30.0, retries=3
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
For mutable default values (like lists, dicts, sets), Statica automatically creates copies to prevent shared state issues:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
class UserProfile(Statica):
|
|
186
|
+
name: str
|
|
187
|
+
tags: list[str] = Field(default=[]) # or tags: list[str] = []
|
|
188
|
+
|
|
189
|
+
user1 = UserProfile(name="Alice")
|
|
190
|
+
user2 = UserProfile(name="Bob")
|
|
191
|
+
|
|
192
|
+
user1.tags.append("admin")
|
|
193
|
+
print(user1.tags) # Output: ["admin"]
|
|
194
|
+
print(user2.tags) # Output: [] (not affected)
|
|
195
|
+
```
|
|
196
|
+
|
|
155
197
|
### Field Constraints
|
|
156
198
|
|
|
157
|
-
You can specify constraints on fields:
|
|
199
|
+
You can specify constraints and options on fields:
|
|
158
200
|
|
|
201
|
+
- **Default Values**: `default` (using `Field()`) or direct assignment
|
|
159
202
|
- **String Constraints**: `min_length`, `max_length`, `strip_whitespace`
|
|
160
203
|
- **Numeric Constraints**: `min_value`, `max_value`
|
|
161
204
|
- **Casting**: `cast_to`
|
|
205
|
+
- **Aliasing**: `alias`
|
|
162
206
|
|
|
163
207
|
```python
|
|
164
208
|
class StringTest(Statica):
|
|
@@ -166,6 +210,15 @@ class StringTest(Statica):
|
|
|
166
210
|
|
|
167
211
|
class IntTest(Statica):
|
|
168
212
|
num: int = Field(min_value=1, max_value=10, cast_to=int)
|
|
213
|
+
|
|
214
|
+
class DefaultTest(Statica):
|
|
215
|
+
# Using Field() for defaults with constraints
|
|
216
|
+
status: str = Field(default="active")
|
|
217
|
+
priority: int = Field(default=1, min_value=1, max_value=5)
|
|
218
|
+
|
|
219
|
+
# Direct assignment for simple defaults
|
|
220
|
+
timeout: float = 30.0
|
|
221
|
+
retries: int = 3
|
|
169
222
|
```
|
|
170
223
|
|
|
171
224
|
### Custom Error Classes
|
|
@@ -212,68 +265,27 @@ Aliasing
|
|
|
212
265
|
Statica supports field aliasing, allowing you to map different field names for parsing and serialization.
|
|
213
266
|
This is particularly useful when working with external APIs that use different naming conventions.
|
|
214
267
|
|
|
215
|
-
### Basic Aliases
|
|
216
|
-
|
|
217
268
|
Use the `alias` parameter to define an alternative name for both parsing and serialization:
|
|
218
269
|
|
|
219
270
|
```python
|
|
220
271
|
class User(Statica):
|
|
221
272
|
full_name: str = Field(alias="fullName")
|
|
222
|
-
age: int = Field(alias="userAge")
|
|
273
|
+
age: int = Field(alias="userAge", default=25)
|
|
223
274
|
|
|
224
275
|
# Parse data with aliases
|
|
225
|
-
data = {"fullName": "John Doe"
|
|
276
|
+
data = {"fullName": "John Doe"} # userAge not provided, uses default
|
|
226
277
|
user = User.from_map(data)
|
|
227
278
|
print(user.full_name) # Output: "John Doe"
|
|
228
|
-
print(user.age) # Output:
|
|
279
|
+
print(user.age) # Output: 25
|
|
229
280
|
|
|
230
|
-
# Serialize back with aliases
|
|
281
|
+
# Serialize back with aliases (uses the alias for serialization by default)
|
|
231
282
|
result = user.to_dict()
|
|
232
|
-
print(result) # Output: {"fullName": "John Doe", "userAge":
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Separate Parsing and Serialization Aliases
|
|
283
|
+
print(result) # Output: {"fullName": "John Doe", "userAge": 25}
|
|
236
284
|
|
|
237
|
-
|
|
285
|
+
# Serialize without aliases
|
|
286
|
+
result_no_alias = user.to_dict(with_aliases=False)
|
|
287
|
+
print(result_no_alias) # Output: {"full_name": "John Doe", "age": 25}
|
|
238
288
|
|
|
239
|
-
```python
|
|
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
|
-
```
|
|
255
|
-
|
|
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"}
|
|
277
289
|
```
|
|
278
290
|
|
|
279
291
|
Advanced Usage
|
|
@@ -296,6 +308,12 @@ print(instance.num) # Output: 42
|
|
|
296
308
|
```
|
|
297
309
|
|
|
298
310
|
|
|
311
|
+
Design Decisions
|
|
312
|
+
----------------------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
- [Inheritance over Decorators](docs/decision-inheritance-over-decorators.md)
|
|
315
|
+
|
|
316
|
+
|
|
299
317
|
Contributing
|
|
300
318
|
----------------------------------------------------------------------------------------
|
|
301
319
|
|
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "statica"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.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" }]
|
|
7
7
|
license = { file = "LICENSE" }
|
|
8
|
-
classifiers=[
|
|
8
|
+
classifiers = [
|
|
9
9
|
"Programming Language :: Python :: 3",
|
|
10
10
|
"License :: OSI Approved :: MIT License",
|
|
11
11
|
"Operating System :: OS Independent",
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Programming Language :: Python",
|
|
14
|
-
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
14
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
15
15
|
]
|
|
16
|
-
requires-python = ">=3.11, <3.14"
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
########################################################################################
|
|
20
19
|
#### MARK: Dependencies
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
requires-python = ">=3.11, <3.15"
|
|
24
23
|
|
|
24
|
+
dependencies = []
|
|
25
25
|
|
|
26
26
|
[dependency-groups]
|
|
27
27
|
dev = [
|
|
28
|
-
"
|
|
29
|
-
"pyinstrument
|
|
30
|
-
"pytest-cov ~=
|
|
31
|
-
"ruff ~= 0.
|
|
28
|
+
"pyrefly ~= 1.1.0",
|
|
29
|
+
"pyinstrument ~= 5.1.0",
|
|
30
|
+
"pytest-cov ~= 7.1.0",
|
|
31
|
+
"ruff ~= 0.15.0",
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
########################################################################################
|
|
36
|
-
#### MARK:
|
|
37
|
-
|
|
36
|
+
#### MARK: Build System
|
|
38
37
|
|
|
39
|
-
[tool.setuptools]
|
|
40
|
-
packages = ["statica"]
|
|
41
|
-
license-files = [] # Workaround for https://github.com/astral-sh/uv/issues/9513
|
|
42
38
|
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.11.32,<0.12.0"]
|
|
41
|
+
build-backend = "uv_build"
|
|
43
42
|
|
|
44
|
-
[tool.uv]
|
|
45
|
-
|
|
43
|
+
[tool.uv.build-backend]
|
|
44
|
+
module-root = ""
|
|
46
45
|
|
|
47
46
|
|
|
48
47
|
########################################################################################
|
|
@@ -53,22 +52,21 @@ package = true
|
|
|
53
52
|
show-fixes = true
|
|
54
53
|
line-length = 100
|
|
55
54
|
|
|
56
|
-
|
|
57
55
|
[tool.ruff.lint]
|
|
58
56
|
select = ["ALL"]
|
|
59
57
|
ignore = [
|
|
60
58
|
"ANN202", # Missing return type annotation for private function
|
|
61
59
|
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
|
|
60
|
+
"COM812", # Missing trailing comma
|
|
62
61
|
"D", # docstrings
|
|
63
62
|
"ERA001", # Found commented-out code
|
|
64
63
|
"INP001", # File is part of an implicit namespace package. Add an `__init__.py`.
|
|
64
|
+
"PLR0913", # Too many arguments in function definition (6 > 5)
|
|
65
65
|
"S101", # Use of `assert` detected
|
|
66
66
|
"T201", # `print` found
|
|
67
67
|
"W191", # indentation contains tabs
|
|
68
|
-
"PLR0913", # Too many arguments in function definition (6 > 5)
|
|
69
68
|
]
|
|
70
69
|
|
|
71
|
-
|
|
72
70
|
[tool.ruff.format]
|
|
73
71
|
indent-style = "tab"
|
|
74
72
|
quote-style = "double"
|
|
@@ -77,6 +75,7 @@ quote-style = "double"
|
|
|
77
75
|
########################################################################################
|
|
78
76
|
#### MARK: Testing
|
|
79
77
|
|
|
78
|
+
|
|
80
79
|
[tool.coverage.report]
|
|
81
80
|
omit = [
|
|
82
81
|
"benchmark_*.py",
|
|
@@ -88,7 +87,9 @@ exclude_also = [
|
|
|
88
87
|
|
|
89
88
|
|
|
90
89
|
########################################################################################
|
|
91
|
-
#### MARK:
|
|
90
|
+
#### MARK: Pyrefly
|
|
91
|
+
|
|
92
92
|
|
|
93
|
-
[tool.
|
|
94
|
-
|
|
93
|
+
[tool.pyrefly]
|
|
94
|
+
preset = "strict"
|
|
95
|
+
min-severity = "warn"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class StaticaConfig:
|
|
8
|
+
type_error_message: str
|
|
9
|
+
min_length_error_message: str
|
|
10
|
+
max_length_error_message: str
|
|
11
|
+
min_value_error_message: str
|
|
12
|
+
max_value_error_message: str
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def create(
|
|
16
|
+
cls,
|
|
17
|
+
*,
|
|
18
|
+
type_error_message: str = "expected type '{expected_type}', got '{found_type}'",
|
|
19
|
+
min_length_error_message: str = "{field_name} length must be at least {min_length}",
|
|
20
|
+
max_length_error_message: str = "{field_name} length must be at most {max_length}",
|
|
21
|
+
min_value_error_message: str = "{field_name} must be at least {min_value}",
|
|
22
|
+
max_value_error_message: str = "{field_name} must be at most {max_value}",
|
|
23
|
+
) -> StaticaConfig:
|
|
24
|
+
return cls(
|
|
25
|
+
type_error_message=type_error_message,
|
|
26
|
+
min_length_error_message=min_length_error_message,
|
|
27
|
+
max_length_error_message=max_length_error_message,
|
|
28
|
+
min_value_error_message=min_value_error_message,
|
|
29
|
+
max_value_error_message=max_value_error_message,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
default_config = StaticaConfig.create()
|