modmex 1.0.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.
- modmex-1.0.0/LICENSE +21 -0
- modmex-1.0.0/PKG-INFO +407 -0
- modmex-1.0.0/README.md +390 -0
- modmex-1.0.0/modmex/__init__.py +15 -0
- modmex-1.0.0/modmex/base_model.py +166 -0
- modmex-1.0.0/modmex/datetime_parser.py +239 -0
- modmex-1.0.0/modmex/errors.py +16 -0
- modmex-1.0.0/modmex/fields.py +57 -0
- modmex-1.0.0/modmex/serialization.py +123 -0
- modmex-1.0.0/modmex/validation.py +339 -0
- modmex-1.0.0/pyproject.toml +33 -0
modmex-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 modmex
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
modmex-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: modmex
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Lightweight Python models built on dataclasses with validation, serialization, and type-safe data mapping
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: clandro89@gmail.com
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: orjson (>=3.11.9,<4.0.0)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# modmex
|
|
18
|
+
|
|
19
|
+
Lightweight Python models built on dataclasses with validation, serialization, and type-safe data mapping.
|
|
20
|
+
|
|
21
|
+
[](https://github.com/modmex/modmex/actions/workflows/ci.yml)
|
|
22
|
+
[](https://codecov.io/gh/modmex/modmex)
|
|
23
|
+
[](https://pypi.org/project/modmex/)
|
|
24
|
+
[](https://pypi.org/project/modmex/)
|
|
25
|
+
[](https://github.com/modmex/modmex/blob/main/LICENSE)
|
|
26
|
+
|
|
27
|
+
modmex gives you a small but powerful toolkit for:
|
|
28
|
+
|
|
29
|
+
- Typed models with minimal boilerplate.
|
|
30
|
+
- Automatic coercion and validation at initialization time.
|
|
31
|
+
- Recursive serialization to Python primitives and JSON.
|
|
32
|
+
- Per-field and per-model validation hooks.
|
|
33
|
+
- Type-based custom serializers to adapt output for different consumers.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Why modmex
|
|
37
|
+
|
|
38
|
+
If you want stricter models than plain dataclasses, but without the weight of a large framework, modmex is designed for that middle ground.
|
|
39
|
+
|
|
40
|
+
It focuses on:
|
|
41
|
+
|
|
42
|
+
- Simplicity: small API surface.
|
|
43
|
+
- Predictability: explicit model lifecycle.
|
|
44
|
+
- Flexibility: configurable serialization without changing your model definitions.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
With pip:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install modmex
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With Poetry:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
poetry add modmex
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from decimal import Decimal
|
|
68
|
+
|
|
69
|
+
from modmex import BaseModel, Field
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class User(BaseModel):
|
|
73
|
+
id: int
|
|
74
|
+
name: str
|
|
75
|
+
balance: Decimal = Decimal("0")
|
|
76
|
+
password: str = Field("", exclude=True)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
user = User(id="1", name=123, balance="10.50")
|
|
80
|
+
|
|
81
|
+
# Type coercion happens during initialization.
|
|
82
|
+
assert user.id == 1
|
|
83
|
+
assert user.name == "123"
|
|
84
|
+
assert user.balance == Decimal("10.50")
|
|
85
|
+
|
|
86
|
+
# model_dump returns primitive/serializable values.
|
|
87
|
+
assert user.model_dump() == {
|
|
88
|
+
"id": 1,
|
|
89
|
+
"name": "123",
|
|
90
|
+
"balance": 10.5,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# model_dump_json returns a JSON string.
|
|
94
|
+
assert user.model_dump_json() == '{"id":1,"name":"123","balance":10.5}'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Field Configuration
|
|
98
|
+
|
|
99
|
+
Use `Field(...)` to add serialization metadata to a model field.
|
|
100
|
+
|
|
101
|
+
Main options:
|
|
102
|
+
|
|
103
|
+
- `exclude=True`
|
|
104
|
+
- Always excludes this field from `model_dump` and `model_dump_json`.
|
|
105
|
+
- `exclude_from={"profile_name"}`
|
|
106
|
+
- Excludes this field only for selected serialization profiles.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from modmex import BaseModel, Field
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Session(BaseModel):
|
|
115
|
+
id: str
|
|
116
|
+
secret: str = Field("", exclude_from={"public"})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class User(BaseModel):
|
|
120
|
+
id: int
|
|
121
|
+
private_note: str = Field("x", exclude=True)
|
|
122
|
+
sessions: list[Session] = Field(default_factory=list)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
user = User(id=1, sessions=[Session(id="s1", secret="abc")])
|
|
126
|
+
|
|
127
|
+
assert user.model_dump(profile="public") == {
|
|
128
|
+
"id": 1,
|
|
129
|
+
"sessions": [{"id": "s1"}],
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Tip:
|
|
134
|
+
|
|
135
|
+
- Use `exclude=True` for values that should never leave the model.
|
|
136
|
+
- Use `exclude_from={...}` when omission depends on the output profile.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
## Everyday Usage
|
|
141
|
+
|
|
142
|
+
### 1) Parse and normalize input data
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from modmex import BaseModel
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Product(BaseModel):
|
|
149
|
+
id: int
|
|
150
|
+
name: str
|
|
151
|
+
active: bool
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
product = Product(id="10", name=123, active="true")
|
|
155
|
+
|
|
156
|
+
assert product.id == 10
|
|
157
|
+
assert product.name == "123"
|
|
158
|
+
assert product.active is True
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 2) Work with nested models
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from modmex import BaseModel
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Address(BaseModel):
|
|
168
|
+
zipcode: int
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class User(BaseModel):
|
|
172
|
+
id: int
|
|
173
|
+
address: Address
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
user = User(id="1", address={"zipcode": "90210"})
|
|
177
|
+
assert user.address.zipcode == 90210
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 3) Prepare different payloads from the same model
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
api_payload = account.model_dump(profile="public")
|
|
184
|
+
internal_payload = account.model_dump()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 4) Build JSON directly
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
json_payload = account.model_dump_json(profile="public")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
## Validators
|
|
195
|
+
|
|
196
|
+
### Field validators
|
|
197
|
+
|
|
198
|
+
Use `@field_validator("field_name")` to transform or validate a single field.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from modmex import BaseModel, field_validator
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Product(BaseModel):
|
|
205
|
+
name: str
|
|
206
|
+
|
|
207
|
+
@field_validator("name")
|
|
208
|
+
def normalize_name(self, value: str) -> str:
|
|
209
|
+
return value.strip().title()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
### Model validators
|
|
214
|
+
|
|
215
|
+
Use `@model_validator(mode="before" | "after")` to work with full model state.
|
|
216
|
+
|
|
217
|
+
- `before`: runs before type coercion.
|
|
218
|
+
- `after`: runs after field-level validation.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from modmex import BaseModel, model_validator
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Product(BaseModel):
|
|
225
|
+
name: str
|
|
226
|
+
slug: str = ""
|
|
227
|
+
|
|
228
|
+
@model_validator(mode="before")
|
|
229
|
+
def build_slug(self, values: dict) -> dict:
|
|
230
|
+
values["slug"] = values["name"].lower().replace(" ", "-")
|
|
231
|
+
return values
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
## Serialization
|
|
236
|
+
|
|
237
|
+
### `model_dump(...)`
|
|
238
|
+
|
|
239
|
+
Use `model_dump` when you need a dictionary payload.
|
|
240
|
+
|
|
241
|
+
Most common options:
|
|
242
|
+
|
|
243
|
+
- `exclude={...}` to omit fields for a specific call.
|
|
244
|
+
- `profile="..."` to apply `exclude_from` rules.
|
|
245
|
+
- `include_excluded=True` to force metadata-excluded fields into the payload.
|
|
246
|
+
- `type_serializers={...}` to control how specific Python types are represented.
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
### `model_dump_json(...)`
|
|
250
|
+
|
|
251
|
+
Use `model_dump_json` when you need a JSON string output.
|
|
252
|
+
|
|
253
|
+
It supports the same practical options as `model_dump` (`exclude`, `profile`, `include_excluded`, `type_serializers`).
|
|
254
|
+
|
|
255
|
+
## Omitting Fields During Serialization
|
|
256
|
+
|
|
257
|
+
Use this feature when the same model must produce different payloads depending on where the data is going.
|
|
258
|
+
|
|
259
|
+
- `exclude_from` defines where a field should be omitted.
|
|
260
|
+
- `profile` selects which omission rules to apply in a specific dump call.
|
|
261
|
+
|
|
262
|
+
### What each option does
|
|
263
|
+
|
|
264
|
+
- `exclude_from={"public"}`
|
|
265
|
+
- Omit this field when serializing with `profile="public"`.
|
|
266
|
+
- `profile="public"`
|
|
267
|
+
- Apply all field rules tagged for `public` during serialization.
|
|
268
|
+
|
|
269
|
+
### Common pattern
|
|
270
|
+
|
|
271
|
+
You may want one shape for API responses and another for internal flows (logs, queues, exports, persistence payloads, etc.).
|
|
272
|
+
|
|
273
|
+
- API payload (`profile="public"`): hide internal fields.
|
|
274
|
+
- Internal payload (no profile, or another profile): keep those fields.
|
|
275
|
+
|
|
276
|
+
### Example
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from modmex import BaseModel, Field
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class Account(BaseModel):
|
|
283
|
+
id: int
|
|
284
|
+
email: str = Field("", exclude_from={"public"})
|
|
285
|
+
internal_note: str = Field("", exclude=True)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
account = Account(id=1, email="a@x.com", internal_note="secret")
|
|
289
|
+
|
|
290
|
+
# No profile: only always-excluded fields are removed.
|
|
291
|
+
assert account.model_dump() == {
|
|
292
|
+
"id": 1,
|
|
293
|
+
"email": "a@x.com",
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# public profile: profile-based exclusions are applied.
|
|
297
|
+
assert account.model_dump(profile="public") == {
|
|
298
|
+
"id": 1,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# include_excluded=True: ignore Field exclusion metadata.
|
|
302
|
+
assert account.model_dump(profile="public", include_excluded=True) == {
|
|
303
|
+
"id": 1,
|
|
304
|
+
"email": "a@x.com",
|
|
305
|
+
"internal_note": "secret",
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Dynamic omission for one call (without metadata changes).
|
|
309
|
+
assert account.model_dump(exclude={"email"}) == {
|
|
310
|
+
"id": 1,
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
## Type-Based Custom Serializers
|
|
316
|
+
|
|
317
|
+
You can override serialization behavior by type with `type_serializers`.
|
|
318
|
+
|
|
319
|
+
Shape:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
type_serializers = {
|
|
323
|
+
SomeType: serializer_function,
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Keep Decimal values as Decimal in `model_dump`
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from decimal import Decimal
|
|
331
|
+
|
|
332
|
+
dumped = model.model_dump(
|
|
333
|
+
type_serializers={
|
|
334
|
+
Decimal: lambda value: value,
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Convert float to Decimal for a specific output contract
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from decimal import Decimal
|
|
343
|
+
|
|
344
|
+
from modmex import BaseModel
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class Price(BaseModel):
|
|
348
|
+
amount: float
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
p = Price(amount=10.25)
|
|
352
|
+
dumped = p.model_dump(
|
|
353
|
+
type_serializers={
|
|
354
|
+
float: lambda value: Decimal(str(value)),
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
assert dumped["amount"] == Decimal("10.25")
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Emit Decimal as string in JSON
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
from decimal import Decimal
|
|
365
|
+
|
|
366
|
+
dumped_json = model.model_dump_json(
|
|
367
|
+
type_serializers={
|
|
368
|
+
Decimal: lambda value: str(value),
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Note: some client libraries expect `Decimal` instead of `float` values (for example, common `boto3` workflows). Type serializers let you adapt output contracts cleanly, without hard-coding backend-specific behavior into your models.
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
## Error Handling
|
|
380
|
+
|
|
381
|
+
Validation issues raise `ValidationError`.
|
|
382
|
+
|
|
383
|
+
Each error includes:
|
|
384
|
+
|
|
385
|
+
- `loc`: location path (supports nested structures).
|
|
386
|
+
- `msg`: human-readable message.
|
|
387
|
+
- `type`: error category.
|
|
388
|
+
|
|
389
|
+
Example locations:
|
|
390
|
+
|
|
391
|
+
- `["address", "zipcode"]`
|
|
392
|
+
- `["tags", 1]`
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
## Practical Usage Pattern
|
|
396
|
+
|
|
397
|
+
Use this rule of thumb:
|
|
398
|
+
|
|
399
|
+
- Keep rich Python types in the in-memory model instance.
|
|
400
|
+
- Use `model_dump` / `model_dump_json` to produce transport-friendly payloads.
|
|
401
|
+
- Use `type_serializers` when a specific consumer requires a different type format.
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
## Compatibility
|
|
405
|
+
|
|
406
|
+
- Python 3.10+
|
|
407
|
+
|