|
3 | 3 | [](https://badge.fury.io/py/django-api-forms) |
4 | 4 | [](https://codecov.io/gh/Sibyx/django_api_forms) |
5 | 5 |
|
6 | | -[Django Forms](https://docs.djangoproject.com/en/4.1/topics/forms/) approach in the processing of a RESTful HTTP |
7 | | -request payload (especially for content type like [JSON](https://www.json.org/) or [MessagePack](https://msgpack.org/)) |
8 | | -without HTML front-end. |
| 6 | +**Django API Forms** is a Python library that brings the [Django Forms](https://docs.djangoproject.com/en/4.1/topics/forms/) approach to processing RESTful HTTP request payloads (such as [JSON](https://www.json.org/) or [MessagePack](https://msgpack.org/)) without the HTML front-end overhead. |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Django API Forms provides a declarative way to: |
| 11 | + |
| 12 | +- Define request validation schemas using a familiar Django-like syntax |
| 13 | +- Parse and validate incoming API requests |
| 14 | +- Handle nested data structures and complex validation rules |
| 15 | +- Convert validated data into Python objects |
| 16 | +- Populate Django models or other objects with validated data |
| 17 | + |
| 18 | +[**📚 Read the full documentation**](https://sibyx.github.io/django_api_forms/) |
| 19 | + |
| 20 | +## Key Features |
| 21 | + |
| 22 | +- **Declarative Form Definition**: Define your API request schemas using a familiar Django Forms-like syntax |
| 23 | +- **Request Validation**: Validate incoming requests against your defined schemas |
| 24 | +- **Nested Data Structures**: Handle complex nested JSON objects and arrays |
| 25 | +- **Custom Field Types**: Specialized fields for common API use cases (BooleanField, EnumField, etc.) |
| 26 | +- **File Uploads**: Support for BASE64-encoded file and image uploads |
| 27 | +- **Object Population**: Easily populate Django models or other objects with validated data |
| 28 | +- **Customizable Validation**: Define custom validation rules at the field or form level |
| 29 | +- **Multiple Content Types**: Support for JSON, MessagePack, and extensible to other formats |
9 | 30 |
|
10 | 31 | ## Motivation |
11 | 32 |
|
12 | | -The main idea was to create a simple and declarative way to specify the format of expecting requests with the ability |
| 33 | +The main idea was to create a simple and declarative way to specify the format of expected requests with the ability |
13 | 34 | to validate them. Firstly, I tried to use [Django Forms](https://docs.djangoproject.com/en/4.1/topics/forms/) to |
14 | | -validate my API requests (I use pure Django in my APIs). I have encountered a problem with nesting my requests without |
| 35 | +validate my API requests (I use pure Django in my APIs). I encountered a problem with nesting my requests without |
15 | 36 | a huge boilerplate. Also, the whole HTML thing was pretty useless in my RESTful APIs. |
16 | 37 |
|
17 | 38 | I wanted to: |
18 | 39 |
|
19 | | -- define my requests as object (`Form`), |
20 | | -- pass the request with optional arguments to my defined object (`form = Form.create_from_request(request, param=param)`), |
21 | | -- validate my request `form.is_valid()`, |
22 | | -- extract data `form.clean_data` property. |
| 40 | +- Define my requests as objects (`Form`) |
| 41 | +- Pass the request to my defined object (`form = Form.create_from_request(request, param=param))`) |
| 42 | + - With the ability to pass any extra optional arguments |
| 43 | +- Validate my request (`form.is_valid()`) |
| 44 | +- Extract data (`form.cleaned_data` property) |
23 | 45 |
|
24 | 46 | I wanted to keep: |
25 | 47 |
|
26 | | -- friendly declarative Django syntax, |
27 | | -([DeclarativeFieldsMetaclass](https://github.com/django/django/blob/master/django/forms/forms.py#L25) is beautiful), |
28 | | -- [Validators](https://docs.djangoproject.com/en/4.1/ref/validators/), |
29 | | -- [ValidationError](https://docs.djangoproject.com/en/4.1/ref/exceptions/#validationerror), |
30 | | -- [Form fields](https://docs.djangoproject.com/en/4.1/ref/forms/fields/) (In the end, I had to "replace" some of them). |
| 48 | +- Friendly declarative Django syntax |
| 49 | + ([DeclarativeFieldsMetaclass](https://github.com/django/django/blob/master/django/forms/forms.py#L22) is beautiful) |
| 50 | +- [Validators](https://docs.djangoproject.com/en/4.1/ref/validators/) |
| 51 | +- [ValidationError](https://docs.djangoproject.com/en/4.1/ref/exceptions/#validationerror) |
| 52 | +- [Form fields](https://docs.djangoproject.com/en/4.1/ref/forms/fields/) (In the end, I had to "replace" some of them) |
31 | 53 |
|
32 | | -So I have decided to create a simple Python package to cover all my expectations. |
| 54 | +So I created this Python package to cover all these expectations. |
33 | 55 |
|
34 | 56 | ## Installation |
35 | 57 |
|
36 | | -```shell script |
| 58 | +```shell |
37 | 59 | # Using pip |
38 | 60 | pip install django-api-forms |
39 | 61 |
|
40 | 62 | # Using poetry |
41 | 63 | poetry add django-api-forms |
42 | 64 |
|
43 | | -# Local installation |
| 65 | +# Local installation from source |
44 | 66 | python -m pip install . |
45 | 67 | ``` |
46 | 68 |
|
47 | | -Optional: |
48 | | -```shell script |
49 | | -# msgpack support (for requests with Content-Type: application/x-msgpack) |
50 | | -poetry add msgpack |
| 69 | +### Requirements |
| 70 | + |
| 71 | +- Python 3.9+ |
| 72 | +- Django 2.0+ |
51 | 73 |
|
52 | | -# ImageField support |
53 | | -poetry add Pillow |
| 74 | +### Optional Dependencies |
| 75 | + |
| 76 | +Django API Forms supports additional functionality through optional dependencies: |
| 77 | + |
| 78 | +```shell |
| 79 | +# MessagePack support (for Content-Type: application/x-msgpack) |
| 80 | +pip install django-api-forms[msgpack] |
| 81 | + |
| 82 | +# File and Image Fields support |
| 83 | +pip install django-api-forms[Pillow] |
| 84 | + |
| 85 | +# RRule Field support |
| 86 | +pip install django-api-forms[rrule] |
| 87 | + |
| 88 | +# GeoJSON Field support |
| 89 | +pip install django-api-forms[gdal] |
| 90 | + |
| 91 | +# Install multiple extras at once |
| 92 | +pip install django-api-forms[Pillow,msgpack] |
54 | 93 | ``` |
55 | 94 |
|
| 95 | +For more detailed installation instructions, see the [Installation Guide](https://sibyx.github.io/django_api_forms/install/). |
| 96 | + |
56 | 97 | Install application in your Django project by adding `django_api_forms` to yours `INSTALLED_APPS`: |
57 | 98 |
|
58 | 99 | ```python |
@@ -88,146 +129,98 @@ DJANGO_API_FORMS_PARSERS = { |
88 | 129 | } |
89 | 130 | ``` |
90 | 131 |
|
91 | | -## Example |
| 132 | +## Quick Example |
92 | 133 |
|
93 | | -**Simple nested JSON request** |
94 | | - |
95 | | -```json |
96 | | -{ |
97 | | - "title": "Unknown Pleasures", |
98 | | - "type": "vinyl", |
99 | | - "artist": { |
100 | | - "_name": "Joy Division", |
101 | | - "genres": [ |
102 | | - "rock", |
103 | | - "punk" |
104 | | - ], |
105 | | - "members": 4 |
106 | | - }, |
107 | | - "year": 1979, |
108 | | - "songs": [ |
109 | | - { |
110 | | - "title": "Disorder", |
111 | | - "duration": "3:29" |
112 | | - }, |
113 | | - { |
114 | | - "title": "Day of the Lords", |
115 | | - "duration": "4:48", |
116 | | - "metadata": { |
117 | | - "_section": { |
118 | | - "type": "ID3v2", |
119 | | - "offset": 0, |
120 | | - "byteLength": 2048 |
121 | | - }, |
122 | | - "header": { |
123 | | - "majorVersion": 3, |
124 | | - "minorRevision": 0, |
125 | | - "size": 2038 |
126 | | - } |
127 | | - } |
128 | | - } |
129 | | - ], |
130 | | - "metadata": { |
131 | | - "created_at": "2019-10-21T18:57:03+0100", |
132 | | - "updated_at": "2019-10-21T18:57:03+0100" |
133 | | - } |
134 | | -} |
135 | | -``` |
136 | | - |
137 | | -**Django API Forms equivalent + validation** |
| 134 | +Here's a simple example demonstrating how to use Django API Forms: |
138 | 135 |
|
139 | 136 | ```python |
140 | | -from enum import Enum |
141 | | - |
142 | | -from django.core.exceptions import ValidationError |
143 | 137 | from django.forms import fields |
| 138 | +from django.http import JsonResponse |
| 139 | +from django_api_forms import Form, FormField, FieldList |
144 | 140 |
|
145 | | -from django_api_forms import FieldList, FormField, FormFieldList, DictionaryField, EnumField, AnyField, Form |
146 | | - |
147 | | - |
148 | | -class AlbumType(Enum): |
149 | | - CD = 'cd' |
150 | | - VINYL = 'vinyl' |
151 | | - |
152 | | - |
| 141 | +# Define a nested form |
153 | 142 | class ArtistForm(Form): |
154 | | - class Meta: |
155 | | - mapping = { |
156 | | - '_name': 'name' |
157 | | - } |
158 | | - |
159 | 143 | name = fields.CharField(required=True, max_length=100) |
160 | 144 | genres = FieldList(field=fields.CharField(max_length=30)) |
161 | 145 | members = fields.IntegerField() |
162 | 146 |
|
163 | | - |
164 | | -class SongForm(Form): |
165 | | - title = fields.CharField(required=True, max_length=100) |
166 | | - duration = fields.DurationField(required=False) |
167 | | - metadata = AnyField(required=False) |
168 | | - |
169 | | - |
| 147 | +# Define the main form |
170 | 148 | class AlbumForm(Form): |
171 | 149 | title = fields.CharField(max_length=100) |
172 | 150 | year = fields.IntegerField() |
173 | 151 | artist = FormField(form=ArtistForm) |
174 | | - songs = FormFieldList(form=SongForm) |
175 | | - type = EnumField(enum=AlbumType, required=True) |
176 | | - metadata = DictionaryField(value_field=fields.DateTimeField()) |
177 | | - |
178 | | - def clean_year(self): |
179 | | - if self.cleaned_data['year'] == 1992: |
180 | | - raise ValidationError("Year 1992 is forbidden!", 'forbidden-value') |
181 | | - if 'param' not in self.extras: |
182 | | - self.add_error( |
183 | | - ('param', ), |
184 | | - ValidationError("You can use extra optional arguments in form validation!", code='param-where') |
185 | | - ) |
186 | | - return self.cleaned_data['year'] |
187 | | - |
188 | | - def clean(self): |
189 | | - if (self.cleaned_data['year'] == 1998) and (self.cleaned_data['artist']['name'] == "Nirvana"): |
190 | | - raise ValidationError("Sounds like a bullshit", code='time-traveling') |
191 | | - if not self._request.user.is_authenticated(): |
192 | | - raise ValidationError("You can use request in form validation!") |
193 | | - if 'param' not in self.extras: |
194 | | - raise ValidationError("You can use extra optional arguments in form validation!") |
195 | | - return self.cleaned_data |
196 | | - |
197 | | - |
198 | | - |
199 | | -""" |
200 | | -Django view example |
201 | | -""" |
| 152 | + |
| 153 | +# In your view |
202 | 154 | def create_album(request): |
203 | | - form = AlbumForm.create_from_request(request, param=request.GET.get('param')) |
| 155 | + form = AlbumForm.create_from_request(request) |
204 | 156 | if not form.is_valid(): |
205 | | - # Process your validation error |
206 | | - print(form.errors) |
| 157 | + # Handle validation errors |
| 158 | + return JsonResponse({"errors": form.errors}, status=400) |
207 | 159 |
|
208 | | - # Cleaned valid payload |
209 | | - payload = form.cleaned_data |
210 | | - print(payload) |
| 160 | + # Access validated data |
| 161 | + album_data = form.cleaned_data |
| 162 | + # Do something with the data... |
| 163 | + |
| 164 | + return JsonResponse({"status": "success"}) |
| 165 | +``` |
| 166 | + |
| 167 | +This form can validate a JSON request like: |
| 168 | + |
| 169 | +```json |
| 170 | +{ |
| 171 | + "title": "Unknown Pleasures", |
| 172 | + "year": 1979, |
| 173 | + "artist": { |
| 174 | + "name": "Joy Division", |
| 175 | + "genres": ["rock", "punk"], |
| 176 | + "members": 4 |
| 177 | + } |
| 178 | +} |
211 | 179 | ``` |
212 | 180 |
|
213 | | -If you want example with whole Django project, check out repository created by [pawl](https://github.com/pawl) |
214 | | -[django_api_forms_modelchoicefield_example](https://github.com/pawl/django_api_forms_modelchoicefield_example), where |
215 | | -he uses library with |
216 | | -[ModelChoiceField](https://docs.djangoproject.com/en/4.1/ref/forms/fields/#django.forms.ModelChoiceField). |
| 181 | +### More Examples |
| 182 | + |
| 183 | +For more comprehensive examples, check out the documentation: |
| 184 | + |
| 185 | +- [Basic Example with Nested Data](https://sibyx.github.io/django_api_forms/example/#basic-example-music-album-api) |
| 186 | +- [User Registration with File Upload](https://sibyx.github.io/django_api_forms/example/#example-user-registration-with-file-upload) |
| 187 | +- [API with Django Models](https://sibyx.github.io/django_api_forms/example/#example-api-with-django-models) |
| 188 | +- [ModelChoiceField Example](https://github.com/pawl/django_api_forms_modelchoicefield_example) - External repository by [pawl](https://github.com/pawl) |
| 189 | + |
| 190 | +## Documentation |
| 191 | + |
| 192 | +Comprehensive documentation is available at [https://sibyx.github.io/django_api_forms/](https://sibyx.github.io/django_api_forms/) |
| 193 | + |
| 194 | +The documentation includes: |
| 195 | + |
| 196 | +- [Installation Guide](https://sibyx.github.io/django_api_forms/install/) |
| 197 | +- [Tutorial](https://sibyx.github.io/django_api_forms/tutorial/) |
| 198 | +- [Field Reference](https://sibyx.github.io/django_api_forms/fields/) |
| 199 | +- [Examples](https://sibyx.github.io/django_api_forms/example/) |
| 200 | +- [API Reference](https://sibyx.github.io/django_api_forms/api_reference/) |
| 201 | +- [Contributing Guide](https://sibyx.github.io/django_api_forms/contributing/) |
217 | 202 |
|
218 | 203 | ## Running Tests |
219 | 204 |
|
220 | | -```shell script |
221 | | -# install all dependencies |
| 205 | +```shell |
| 206 | +# Install all dependencies |
222 | 207 | poetry install |
223 | 208 |
|
224 | | -# run code-style check |
| 209 | +# Run code-style check |
225 | 210 | poetry run flake8 . |
226 | 211 |
|
227 | | -# run the tests |
| 212 | +# Run the tests |
228 | 213 | poetry run python runtests.py |
| 214 | + |
| 215 | +# Run tests with coverage |
| 216 | +poetry run coverage run runtests.py |
| 217 | +poetry run coverage report |
229 | 218 | ``` |
230 | 219 |
|
| 220 | +## License |
| 221 | + |
| 222 | +Django API Forms is released under the MIT License. |
| 223 | + |
231 | 224 | --- |
232 | 225 | Made with ❤️ and ☕️ by Jakub Dubec, [BACKBONE s.r.o.](https://www.backbone.sk/en/) & |
233 | 226 | [contributors](https://github.com/Sibyx/django_api_forms/graphs/contributors). |
0 commit comments