django-cattrs-fields 0.0.1__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.
Files changed (58) hide show
  1. django_cattrs_fields-0.0.1/.github/workflows/ci.yaml +49 -0
  2. django_cattrs_fields-0.0.1/.github/workflows/pre_commit.yaml +9 -0
  3. django_cattrs_fields-0.0.1/.gitignore +14 -0
  4. django_cattrs_fields-0.0.1/.idea/.gitignore +10 -0
  5. django_cattrs_fields-0.0.1/.idea/django-cattrs-fields.iml +17 -0
  6. django_cattrs_fields-0.0.1/.idea/inspectionProfiles/Project_Default.xml +6 -0
  7. django_cattrs_fields-0.0.1/.idea/inspectionProfiles/profiles_settings.xml +6 -0
  8. django_cattrs_fields-0.0.1/.idea/misc.xml +10 -0
  9. django_cattrs_fields-0.0.1/.idea/modules.xml +8 -0
  10. django_cattrs_fields-0.0.1/.idea/vcs.xml +6 -0
  11. django_cattrs_fields-0.0.1/.idea/workspace.xml +68 -0
  12. django_cattrs_fields-0.0.1/.pre-commit-config.yaml +29 -0
  13. django_cattrs_fields-0.0.1/LICENSE +27 -0
  14. django_cattrs_fields-0.0.1/PKG-INFO +404 -0
  15. django_cattrs_fields-0.0.1/README.md +361 -0
  16. django_cattrs_fields-0.0.1/django_cattrs_fields/__init__.py +1 -0
  17. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/__init__.py +9 -0
  18. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/bson.py +54 -0
  19. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/cbor2.py +31 -0
  20. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/json.py +39 -0
  21. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/msgpack.py +39 -0
  22. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/msgspec.py +13 -0
  23. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/orjson.py +32 -0
  24. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/pyyaml.py +37 -0
  25. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/register_hooks.py +310 -0
  26. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/tomlkit.py +30 -0
  27. django_cattrs_fields-0.0.1/django_cattrs_fields/converters/ujson.py +38 -0
  28. django_cattrs_fields-0.0.1/django_cattrs_fields/fields/__init__.py +61 -0
  29. django_cattrs_fields-0.0.1/django_cattrs_fields/fields/files.py +9 -0
  30. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/__init__.py +134 -0
  31. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/bool_hooks.py +49 -0
  32. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/char_hooks.py +139 -0
  33. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/date_hooks.py +135 -0
  34. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/empty_hooks.py +407 -0
  35. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/file_hooks.py +64 -0
  36. django_cattrs_fields-0.0.1/django_cattrs_fields/hooks/number_hooks.py +124 -0
  37. django_cattrs_fields-0.0.1/django_cattrs_fields/utils/decimal.py +14 -0
  38. django_cattrs_fields-0.0.1/django_cattrs_fields/utils/timezone.py +56 -0
  39. django_cattrs_fields-0.0.1/django_cattrs_fields/validators/__init__.py +29 -0
  40. django_cattrs_fields-0.0.1/pyproject.toml +121 -0
  41. django_cattrs_fields-0.0.1/tests/__init__.py +0 -0
  42. django_cattrs_fields-0.0.1/tests/books/__init__.py +0 -0
  43. django_cattrs_fields-0.0.1/tests/books/apps.py +5 -0
  44. django_cattrs_fields-0.0.1/tests/books/migrations/0001_initial.py +27 -0
  45. django_cattrs_fields-0.0.1/tests/books/migrations/__init__.py +0 -0
  46. django_cattrs_fields-0.0.1/tests/books/models.py +5 -0
  47. django_cattrs_fields-0.0.1/tests/books/urls.py +6 -0
  48. django_cattrs_fields-0.0.1/tests/books/views.py +45 -0
  49. django_cattrs_fields-0.0.1/tests/conf/__init__.py +0 -0
  50. django_cattrs_fields-0.0.1/tests/conf/settings.py +26 -0
  51. django_cattrs_fields-0.0.1/tests/conf/urls.py +4 -0
  52. django_cattrs_fields-0.0.1/tests/test_boolean_fields.py +231 -0
  53. django_cattrs_fields-0.0.1/tests/test_character_fields.py +411 -0
  54. django_cattrs_fields-0.0.1/tests/test_date_fields.py +389 -0
  55. django_cattrs_fields-0.0.1/tests/test_empty.py +57 -0
  56. django_cattrs_fields-0.0.1/tests/test_file_fields.py +524 -0
  57. django_cattrs_fields-0.0.1/tests/test_file_upload.py +37 -0
  58. django_cattrs_fields-0.0.1/tests/test_numeric_fields.py +355 -0
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ python-version:
12
+ - '3.12'
13
+ - '3.13'
14
+ - '3.14'
15
+ django-version:
16
+ - 'django==5.2'
17
+ - 'django==6.0'
18
+
19
+ include:
20
+ - python-version: '3.13'
21
+ django-version: "git+https://github.com/django/django.git@main#egg=Django"
22
+ experimental: true
23
+ - python-version: '3.14'
24
+ django-version: "git+https://github.com/django/django.git@main#egg=Django"
25
+ experimental: true
26
+
27
+ steps:
28
+ - uses: actions/checkout@v6
29
+ - name: Set up Python ${{ matrix.python-version }}
30
+ uses: actions/setup-python@v5
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ allow-prereleases: true
34
+
35
+ - name: Install uv
36
+ uses: astral-sh/setup-uv@v7
37
+ with:
38
+ enable-cache: true
39
+
40
+ - name: Install dependencies
41
+ run: |
42
+ uv sync --all-extras --group test
43
+
44
+ - name: Install django
45
+ run: |
46
+ uv pip install ${{ matrix.django-version }}
47
+
48
+ - name: Run tests
49
+ run: uv run pytest
@@ -0,0 +1,9 @@
1
+ name: pre-commit
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ prek:
6
+ runs-on: ubuntu-latest
7
+ steps:
8
+ - uses: actions/checkout@v6
9
+ - uses: j178/prek-action@v1
@@ -0,0 +1,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .python-version
12
+ uv.lock
13
+
14
+ **/media
@@ -0,0 +1,10 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Ignored default folder with query files
5
+ /queries/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
9
+ # Editor-based HTTP Client requests
10
+ /httpRequests/
@@ -0,0 +1,17 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="jdk" jdkName="Python 3.14 virtualenv at ~/projects/django-cattrs-fields/.venv" jdkType="Python SDK" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ <component name="PyDocumentationSettings">
11
+ <option name="format" value="PLAIN" />
12
+ <option name="myDocStringFormat" value="Plain" />
13
+ </component>
14
+ <component name="TestRunnerService">
15
+ <option name="PROJECT_TEST_RUNNER" value="py.test" />
16
+ </component>
17
+ </module>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5
+ </profile>
6
+ </component>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.14 virtualenv at ~/projects/django-cattrs-fields/.venv" />
5
+ </component>
6
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 virtualenv at ~/projects/django-cattrs-fields/.venv" project-jdk-type="Python SDK" />
7
+ <component name="TyConfiguration">
8
+ <option name="enabled" value="true" />
9
+ </component>
10
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/django-cattrs-fields.iml" filepath="$PROJECT_DIR$/.idea/django-cattrs-fields.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="AutoImportSettings">
4
+ <option name="autoReloadType" value="SELECTIVE" />
5
+ </component>
6
+ <component name="ChangeListManager">
7
+ <list default="true" id="4c34166b-f80c-4abf-aed1-4ad30d8c216e" name="Changes" comment="">
8
+ <change beforePath="$PROJECT_DIR$/django_cattrs_fields/converters/register_hooks.py" beforeDir="false" afterPath="$PROJECT_DIR$/django_cattrs_fields/converters/register_hooks.py" afterDir="false" />
9
+ <change beforePath="$PROJECT_DIR$/django_cattrs_fields/fields/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/django_cattrs_fields/fields/__init__.py" afterDir="false" />
10
+ <change beforePath="$PROJECT_DIR$/django_cattrs_fields/hooks/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/django_cattrs_fields/hooks/__init__.py" afterDir="false" />
11
+ </list>
12
+ <option name="SHOW_DIALOG" value="false" />
13
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
14
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
15
+ <option name="LAST_RESOLUTION" value="IGNORE" />
16
+ </component>
17
+ <component name="Git.Settings">
18
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
19
+ </component>
20
+ <component name="ProjectColorInfo">{
21
+ &quot;associatedIndex&quot;: 6
22
+ }</component>
23
+ <component name="ProjectId" id="38z4h3JEueKZmfomoDq5O48s8JO" />
24
+ <component name="ProjectViewState">
25
+ <option name="hideEmptyMiddlePackages" value="true" />
26
+ <option name="showLibraryContents" value="true" />
27
+ </component>
28
+ <component name="PropertiesComponent"><![CDATA[{
29
+ "keyToString": {
30
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
31
+ "RunOnceActivity.ShowReadmeOnStart": "true",
32
+ "RunOnceActivity.git.unshallow": "true",
33
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
34
+ "ai.playground.ignore.import.keys.banner.in.settings": "true",
35
+ "git-widget-placeholder": "time",
36
+ "node.js.detected.package.eslint": "true",
37
+ "node.js.detected.package.tslint": "true",
38
+ "node.js.selected.package.eslint": "(autodetect)",
39
+ "node.js.selected.package.tslint": "(autodetect)",
40
+ "nodejs_package_manager_path": "npm",
41
+ "settings.editor.selected.configurable": "com.intellij.python.ty.TyConfigurable",
42
+ "vue.rearranger.settings.migration": "true"
43
+ }
44
+ }]]></component>
45
+ <component name="SharedIndexes">
46
+ <attachedChunks>
47
+ <set>
48
+ <option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.30387.127" />
49
+ <option value="bundled-python-sdk-3944b0c99280-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.127" />
50
+ </set>
51
+ </attachedChunks>
52
+ </component>
53
+ <component name="TaskManager">
54
+ <task active="true" id="Default" summary="Default task">
55
+ <changelist id="4c34166b-f80c-4abf-aed1-4ad30d8c216e" name="Changes" comment="" />
56
+ <created>1769789183472</created>
57
+ <option name="number" value="Default" />
58
+ <option name="presentableId" value="Default" />
59
+ <updated>1769789183472</updated>
60
+ <workItem from="1769789185755" duration="2847000" />
61
+ <workItem from="1769792956034" duration="1857000" />
62
+ </task>
63
+ <servers />
64
+ </component>
65
+ <component name="TypeScriptGeneratedFilesManager">
66
+ <option name="version" value="3" />
67
+ </component>
68
+ </project>
@@ -0,0 +1,29 @@
1
+ repos:
2
+ - repo: https://github.com/adamchainz/django-upgrade
3
+ rev: 1.29.1
4
+ hooks:
5
+ - id: django-upgrade
6
+ args: [--target-version, "4.2"]
7
+
8
+ - repo: https://github.com/astral-sh/ruff-pre-commit
9
+ rev: v0.14.10
10
+ hooks:
11
+ - id: ruff-check
12
+ - id: ruff-format
13
+
14
+ - repo: https://github.com/codespell-project/codespell
15
+ rev: v2.4.1
16
+ hooks:
17
+ - id: codespell # See pyproject.toml for args
18
+ additional_dependencies:
19
+ - tomli
20
+
21
+ - repo: https://github.com/abravalheri/validate-pyproject
22
+ rev: v0.24.1
23
+ hooks:
24
+ - id: validate-pyproject
25
+
26
+ - repo: https://github.com/asottile/pyupgrade
27
+ rev: v3.21.2
28
+ hooks:
29
+ - id: pyupgrade
@@ -0,0 +1,27 @@
1
+ Copyright <2026> Amirrza Sohrabi far and individual contributors.
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used
15
+ to endorse or promote products derived from this software without
16
+ specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,404 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-cattrs-fields
3
+ Version: 0.0.1
4
+ Summary: django data type support for cattrs
5
+ Author-email: amirreza <amir.rsf1380@gmail.com>
6
+ License-Expression: BSD-3-Clause
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 1 - Planning
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Framework :: Django :: 5.2
12
+ Classifier: Framework :: Django :: 6.0
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: cattrs>=25.3.0
25
+ Requires-Dist: django>=6.0
26
+ Provides-Extra: bson
27
+ Requires-Dist: pymongo>=4.4.0; extra == 'bson'
28
+ Provides-Extra: cbor2
29
+ Requires-Dist: cbor2>=5.4.6; extra == 'cbor2'
30
+ Provides-Extra: msgpack
31
+ Requires-Dist: msgpack>=1.0.5; extra == 'msgpack'
32
+ Provides-Extra: msgspec
33
+ Requires-Dist: msgspec>=0.19.0; (implementation_name == 'cpython') and extra == 'msgspec'
34
+ Provides-Extra: orjson
35
+ Requires-Dist: orjson>=3.11.3; (implementation_name == 'cpython') and extra == 'orjson'
36
+ Provides-Extra: pyyaml
37
+ Requires-Dist: pyyaml>=6.0; extra == 'pyyaml'
38
+ Provides-Extra: tomlkit
39
+ Requires-Dist: tomlkit>=0.11.8; extra == 'tomlkit'
40
+ Provides-Extra: ujson
41
+ Requires-Dist: ujson>=5.10.0; extra == 'ujson'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # django-cattrs-fields
45
+
46
+ **Note**: this is a very experimental project, I'm mostly navigating and discovering how this could work,
47
+ as much as any help and feedback is appreciated, please do not use in a production environment.
48
+
49
+ Brings [cattrs](https://github.com/python-attrs/cattrs) support to the django world.
50
+
51
+ this project is the first step of many, it is intended to be minimal, only adding data type support.
52
+
53
+ ### current data types
54
+ * BooleanField
55
+ * CharField
56
+ * DecimalField
57
+ * EmailField
58
+ * SlugField
59
+ * URLField
60
+ * UUIDField
61
+ * IntegerField
62
+ * FloatField
63
+ * DateField
64
+ * DateTimeField
65
+ * FileField
66
+ * TimeField
67
+ * EmptyField
68
+
69
+ ## installing
70
+ this is not packaged to PyPI yet, for using you need to clone the repository first.
71
+
72
+ then install the package by running `uv sync` or `uv sync --extra <group name>` where group name is one of optional-dependencies listed in pyproject.toml.
73
+
74
+ if you want a one-shot install use `uv sync --all-extras`
75
+
76
+
77
+
78
+ ## basic usage:
79
+ data model classes are `attrs` classes, so anything you find in [attrs](https://www.attrs.org/en/stable/index.html) docs also applies here.
80
+ we also follow cattrs, so anything in their [docs](https://catt.rs/en/stable/index.html) also applies
81
+
82
+ ```py
83
+ import uuid
84
+ from datetime import date, datetime, time
85
+ from decimal import Decimal
86
+
87
+ from attrs import define
88
+
89
+ from django.core.files.uploadedfile import SimpleUploadedFile
90
+
91
+ from django-cattrs-fields.converters import converter
92
+ from django_cattrs_fields.fields import (
93
+ BooleanField,
94
+ CharField,
95
+ DecimalField,
96
+ EmailField,
97
+ SlugField,
98
+ URLField,
99
+ UUIDField,
100
+ IntegerField,
101
+ FloatField,
102
+ DateField,
103
+ DateTimeField,
104
+ TimeField,
105
+ )
106
+ from django_cattrs_fields.fields.files import FileField
107
+
108
+ @define
109
+ class Human:
110
+ id: UUIDField
111
+ username: CharField
112
+ email: EmailField
113
+ slug: SlugField
114
+ website: URLField
115
+ age: IntegerField
116
+ salary: FloatField
117
+ birth_date: DateField
118
+ signup_date: DateTimeField
119
+ picture: FileField
120
+ accurate_salary: DecimalField
121
+ lunch_time: TimeField
122
+
123
+
124
+ human = {
125
+ "id": uuid.uuid4(),
126
+ "username": "bob",
127
+ "email": "bob@email.com",
128
+ "slug": "bo-b",
129
+ "website": "https://bob.com",
130
+ "age": 25,
131
+ "salary": 1000.43,
132
+ "birth_date": date(year=2000, month=7, day=3),
133
+ "signup_date": datetime.now(),
134
+ "picture": SimpleUploadedFile(name="test_image.jpeg", content=b"wheeee", content_type="image/jpeg"),
135
+ "accurate_salary": Decimal("1000.43"),
136
+ "lunch_time": time(14, 30, 0),
137
+ }
138
+
139
+ structure = converter.structure(human, Human) # runs structure hooks and validators, then creates an instance of `Human`
140
+ normal_data = converter.unstructure(structure) # runs unstructure hooks, then makes a dict similar to `human` (or what you tell it to), no validators run.
141
+ ```
142
+
143
+ ### Comparison
144
+ in comparison with how django forms and DRF serializers work, see the examples below
145
+
146
+ in django forms we do:
147
+ ```py
148
+ form = MyForm(data)
149
+ form.is_valid()
150
+ clean_data = form.cleaned_data
151
+ ```
152
+
153
+ in drf we do:
154
+ ```py
155
+ serializer = MySerializer(data)
156
+ serializer.is_valid()
157
+ clean_data = serializer.validated_data
158
+
159
+ # to serialize data
160
+ content = JSONRenderer().render(serializer.data)
161
+ ```
162
+
163
+ the cattrs equivalent of forms is like this:
164
+ ```py
165
+ try:
166
+ form = converter.structure(data, MyForm) # where `MyForm` is a cattrs supported class (usually and attrs class)
167
+ except* ValueError: # notice the `*`, this is an exception group (unless you configure cattrs otherwise)
168
+ pass
169
+ clean_data = converter.unstructure(form)
170
+ ```
171
+
172
+ or if working with json (or other formats)
173
+ ```py
174
+ try:
175
+ form = converter.loads(data, MyForm) # take a json data and load it to python
176
+ except* ValueError: # notice the `*`, this is an exception group (unless you configure cattrs otherwise)
177
+ pass
178
+ clean_data = converter.unstructure(form)
179
+
180
+ # to serialize data
181
+ data = converter.structure(clean_data, MyForm) # to dump data, structure it first
182
+ content = converter.dumps(data)
183
+ ```
184
+
185
+ structuring and loading also validates the data, so no need for the extra step.
186
+
187
+ note that `converter.structure` raises `ValueError` as an exception group.
188
+
189
+
190
+ ### serializers
191
+ the basic `converter` you saw in basic usage section can only structure and unstructure, which is powerful, but we can do more.
192
+
193
+ `cattrs` comes with a set of [preconfigured converters](https://catt.rs/en/stable/preconf.html).
194
+
195
+ we ship our own version of these converters, which extends on top of cattrs' version, though we call them `serializer` to avoid some confusion.
196
+ these are available in `django_cattrs_fields.converters` directory:
197
+
198
+ * django_cattrs_fields.converters.bson
199
+ * django_cattrs_fields.converters.cbor2
200
+ * django_cattrs_fields.converters.json
201
+ * django_cattrs_fields.converters.msgpack
202
+ * django_cattrs_fields.converters.msgspec
203
+ * django_cattrs_fields.converters.orjson
204
+ * django_cattrs_fields.converters.pyyaml
205
+ * django_cattrs_fields.converters.tomlkit
206
+ * django_cattrs_fields.converters.ujson
207
+
208
+ just import `serializer` from each of these modules:
209
+
210
+ ```py
211
+ from django_cattrs_fields.converters import converter
212
+ from django_cattrs_fields.converters.json import serializer
213
+
214
+
215
+ structure = converter.structure(human, Human)
216
+ dump: str | bytes = serializer.dumps(structure) # takes an structured data, dumps a json string
217
+ load: Human = serializers.loads(dump, Human) # takes a dumped data, and loads that as a structured data
218
+ data = converter.unstructure(load) # a dictionary of the data, ready to be used.
219
+ ```
220
+
221
+ it is important to note, while `serializer` objects also have `structure` and `unstructure` methods, they are considered internal API,
222
+ since they are configured to feed encoding and decoding functionalities,
223
+ they don't necessarily behave the way you would expect them to.
224
+
225
+ so in most scenarios you should import a `converter` and a `serializer` to handle their specific task, unless you are fully aware how your `serializer` behaves and can handle it yourself.
226
+
227
+ the only exception (currently) is the msgspec serializer, which doesn't implement any additional logic and works like a normal `converter`,
228
+ tho if the need arises, this could change.
229
+
230
+ ### work with django views
231
+ you can use the data models you made with this package instead of django forms or serializers
232
+
233
+ ```py
234
+ from django_cattrs_fields.converters import converter
235
+ from django_cattrs_fields.converters.json import serializer
236
+
237
+
238
+ @define
239
+ class Human:
240
+ id: UUIDField
241
+ username: CharField
242
+ email: EmailField
243
+ slug: SlugField
244
+ website: URLField
245
+ age: IntegerField
246
+ salary: FloatField
247
+ birth_date: DateField
248
+ signup_date: DateTimeField
249
+ accurate_salary: DecimalField
250
+ lunch_time: TimeField
251
+
252
+
253
+ def get_data(request):
254
+ if request.method == "POST":
255
+ if request.content_type in {"application/x-www-form-urlencoded", "multipart/form-data"}:
256
+ structured_data = converter.structure({**request.POST.dict(), **request.FILES.dict()}, Human) # handle html forms, and multipart data
257
+ else:
258
+ structured_data = serializer.loads(request.body, Human) # handle json (or anything else)
259
+
260
+ data: dict[str, Any] = converter.unstructure(structured_data) # a dictionary of all the POST data (excluding data not covered by Human)
261
+
262
+ return HttpResponse("done")
263
+ ```
264
+ and just like that you have one view that handles html forms and json in one place
265
+
266
+ note that if `POST` data contains anything not in `Human`, it won't show up in the output data (such as csrf token, in this case)
267
+
268
+ also note that when working with APIs, depending on your client you might need to add `csrf_exempt` on you view.
269
+
270
+
271
+ ### saving to database
272
+ one you unstructure your data, you have a dictionary of cleaned data.
273
+ then you can just pass that to your model and create your data
274
+
275
+ ```py
276
+ data: dict[str, Any] = converter.unstructure(structured_data)
277
+
278
+ # either
279
+ obj = HumanModel(**dict)
280
+ obj.save()
281
+
282
+ # or
283
+ HumanModel.objects.create(**dict)
284
+ ```
285
+
286
+
287
+ ### nullable fields
288
+ by default all fields are required and passing a `None` value will raise an error
289
+ to make a field nullable, you can use a union
290
+
291
+ ```py
292
+ @define
293
+ class Product:
294
+ name: CharField # required
295
+ discount: FloatField | None # optional
296
+ ```
297
+
298
+
299
+ ### default values
300
+ to add a default value, the simplest way is to just add it via assignment
301
+
302
+ ```py
303
+ @define
304
+ class Product:
305
+ name: CharField # required
306
+ discount: FloatField = 5.1
307
+ ```
308
+ for more advanced use check [default docs](https://www.attrs.org/en/stable/examples.html#defaults)
309
+
310
+ ### field params
311
+ some fields like `DecimalField` can take some parameter about their data using `typing.Annotated`.
312
+
313
+ ```py
314
+ from typing import Annotated
315
+
316
+ from attrs import define
317
+
318
+ from django_cattrs_fields.fields import DecimalField, CharField, Params
319
+
320
+ @define
321
+ class Product:
322
+ name: CharField
323
+ discount: Annotated[DecimalField, Params(decimal_max_digits=5, decimal_places=3)]
324
+ ```
325
+
326
+ the use case of params differs depending on the field, in the case of Decimal field, `decimal_max_digits` as equivalent to django's DecimalField's `max_digits` parameter
327
+ and `decimal_places` is equivalent to `decimal_places` parameter, and are used when structuring the data to validate the decimal value.
328
+
329
+ like django, DecimalField's params are optional, some fields may require some params in the future.
330
+
331
+
332
+ ### EmptyField
333
+ `EmptyField` is useful when supporting PATCH requests.
334
+
335
+ if a field doesn't receive any data and has `Empty` as its value, it will be omitted when unstructuring.
336
+
337
+ ```py
338
+ from django_cattrs_fields.fields import CharField, EmptyField, Empty
339
+
340
+ @define
341
+ class Human:
342
+ name: CharField
343
+ age: IntegerField | EmptyField = Empty # default to Empty, or provide Empty manually
344
+
345
+
346
+ struct = converter.structure({"name": "bob"})
347
+ # Human(name='bob', age=Empty)
348
+ unstruct = converter.unstructure(struct)
349
+ # {'name': 'bob'}
350
+ ```
351
+
352
+ as you can see, since age is `Empty`, it won't be included in the resulting dictionary.
353
+
354
+ **Warning**: at the moment, `EmptyField` is only supported in unions that have only one other type, tho None is also supported, so:
355
+
356
+ * `CharField | EmptyField` works.
357
+ * `CharField | EmptyField | None` works.
358
+ * `CharField | IntegerField | EmptyField` doesn't work.
359
+
360
+ if complex types are required, register your custom hooks until we can figure out how to properly support this.
361
+ for inspiration, you can check `django_cattrs_fields.hooks.empty_hooks` to see how other hooks are made.
362
+
363
+ ### validation
364
+ by default this package runs some validation when you are structuring your data
365
+ but to add any custom validators you can use attrs [built-in](https://www.attrs.org/en/stable/examples.html#validators) validation mechanism.
366
+
367
+ note that the validations we run are baked in structure hooks, so they will run in any situation.
368
+ these are validations that django also runs every time you use it's data fields.
369
+ if you need to turn this off, just create a [new converter](https://catt.rs/en/stable/basics.html#converters-and-hooks)
370
+
371
+ ### File Handling
372
+ this package comes with FileField you can use to work with files.
373
+ when an uploaded file is passed to this field (e.g: user POSTs some file), it goes through validation,
374
+ then an instance of django's `UploadedFile` is returned (usually a subclass of UploadedFile is used like `InMemoryUploadedFile`).
375
+
376
+ you can save this using the ORM or any other way you do with django.
377
+
378
+ when serving a File (e.g: user sends a GET request), an instance of django's `FieldFile` should be passed (django ORM does this automatically)
379
+ in this case our hooks will return the url of the file.
380
+
381
+ note that this behavior is different in django and DRF
382
+ django returns the whole `FieldFile` object (could be useful with templates), DRF is configurable, it either returns the url or the file name.
383
+
384
+ if you require a different behaviour, you can change this by hooking your logic and set `DCF_FILE_HOOKS` to False in your settings file,
385
+ this will disable all file related hooks.
386
+
387
+
388
+
389
+ ## contribution
390
+ I appreciate any help with this project, but please follow Django's Code of Conduct
391
+ if you have ideas or have found a bug please open an [issue on github](https://github.com/amirreza8002/django-cattrs-fields/issues/new)
392
+
393
+ to help with development follow these steps:
394
+ 1. fork the repository from [github](https://github.com/amirreza8002/django-cattrs-fields).
395
+ 2. clone the project from your fork.
396
+ 3. install the package with one of the following commands:
397
+ * `uv sync --group dev`
398
+ * `uv sync --group dev --group ipython`
399
+ * `uv sync --group dev --group prek`
400
+ * `uv sync --group dev --group test`
401
+ you can combine them together or just use `uv sync --all-groups` to one-shot.
402
+ 4. run `prek install` or `pre-commit install` depending on your choice.
403
+
404
+ if you are contributing new code, please make sure to add some tests for it.