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 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
+ [![CI](https://img.shields.io/github/actions/workflow/status/modmex/modmex/ci.yml?branch=main&logo=github&label=CI)](https://github.com/modmex/modmex/actions/workflows/ci.yml)
22
+ [![Coverage](https://img.shields.io/codecov/c/github/modmex/modmex?label=coverage)](https://codecov.io/gh/modmex/modmex)
23
+ [![PyPI](https://img.shields.io/pypi/v/modmex.svg)](https://pypi.org/project/modmex/)
24
+ [![Python Versions](https://img.shields.io/pypi/pyversions/modmex.svg)](https://pypi.org/project/modmex/)
25
+ [![License](https://img.shields.io/github/license/modmex/modmex.svg)](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
+