humanoid-crud 0.1.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.
- humanoid_crud-0.1.0/LICENSE +21 -0
- humanoid_crud-0.1.0/PKG-INFO +172 -0
- humanoid_crud-0.1.0/README.md +149 -0
- humanoid_crud-0.1.0/pyproject.toml +37 -0
- humanoid_crud-0.1.0/setup.cfg +4 -0
- humanoid_crud-0.1.0/src/humanoid_crud/__init__.py +13 -0
- humanoid_crud-0.1.0/src/humanoid_crud/registry.py +178 -0
- humanoid_crud-0.1.0/src/humanoid_crud.egg-info/PKG-INFO +172 -0
- humanoid_crud-0.1.0/src/humanoid_crud.egg-info/SOURCES.txt +10 -0
- humanoid_crud-0.1.0/src/humanoid_crud.egg-info/dependency_links.txt +1 -0
- humanoid_crud-0.1.0/src/humanoid_crud.egg-info/requires.txt +4 -0
- humanoid_crud-0.1.0/src/humanoid_crud.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
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.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: humanoid-crud
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One-line CRUD registration for Django REST Framework. Built on top of DRF's mixins/generics so you never have to write a Serializer, ViewSet, or router.register() by hand for the common case.
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourname/humanoid-crud
|
|
8
|
+
Project-URL: Repository, https://github.com/yourname/humanoid-crud
|
|
9
|
+
Keywords: django,django-rest-framework,drf,crud,api,rest
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 4
|
|
12
|
+
Classifier: Framework :: Django :: 5
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: djangorestframework>=3.14
|
|
20
|
+
Provides-Extra: filters
|
|
21
|
+
Requires-Dist: django-filter>=23.0; extra == "filters"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# humanoid-crud
|
|
25
|
+
|
|
26
|
+
A one-line CRUD registrar for Django REST Framework. Built **on top of** DRF's
|
|
27
|
+
mixins/generics (it generates real `ModelSerializer` and `ModelViewSet`
|
|
28
|
+
classes for you) — the point isn't to avoid mixins, it's that you never have
|
|
29
|
+
to write or stack them by hand.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install humanoid-crud
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you want `filterset_fields` (exact-match filtering), install the extra:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install humanoid-crud[filters]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Note: the **package name on PyPI** is `humanoid-crud` (hyphen, matching PyPI
|
|
44
|
+
convention), but you **import it in Python** as `humanoid_crud` (underscore,
|
|
45
|
+
since hyphens aren't valid in Python identifiers). This is normal — the same
|
|
46
|
+
is true for packages like `django-rest-framework` → `import rest_framework`.
|
|
47
|
+
|
|
48
|
+
## The problem this solves
|
|
49
|
+
|
|
50
|
+
The normal DRF CRUD recipe for one model is:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# serializers.py
|
|
54
|
+
class BookSerializer(serializers.ModelSerializer):
|
|
55
|
+
class Meta:
|
|
56
|
+
model = Book
|
|
57
|
+
fields = '__all__'
|
|
58
|
+
|
|
59
|
+
# views.py
|
|
60
|
+
class BookViewSet(viewsets.ModelViewSet):
|
|
61
|
+
queryset = Book.objects.all()
|
|
62
|
+
serializer_class = BookSerializer
|
|
63
|
+
|
|
64
|
+
# urls.py
|
|
65
|
+
router.register('books', BookViewSet)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Three files, three pieces of boilerplate, for every single model.
|
|
69
|
+
`humanoid_crud` collapses that to:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import humanoid_crud
|
|
73
|
+
humanoid_crud.register(Book)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
1. `pip install humanoid-crud`
|
|
79
|
+
2. Add `'rest_framework'` to `INSTALLED_APPS` in `settings.py` if it isn't
|
|
80
|
+
already there.
|
|
81
|
+
3. Create a small file — e.g. `myapp/api.py` — where you register your
|
|
82
|
+
models:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# myapp/api.py
|
|
86
|
+
import humanoid_crud
|
|
87
|
+
from myapp.models import Book, Author
|
|
88
|
+
|
|
89
|
+
humanoid_crud.register(Author)
|
|
90
|
+
humanoid_crud.register(Book)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
4. In your project's `urls.py`, import that file (so the registrations run)
|
|
94
|
+
and add the generated routes:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from django.contrib import admin
|
|
98
|
+
from django.urls import path
|
|
99
|
+
|
|
100
|
+
from myapp import api # running this import triggers registration
|
|
101
|
+
import humanoid_crud
|
|
102
|
+
|
|
103
|
+
urlpatterns = [
|
|
104
|
+
path('admin/', admin.site.urls),
|
|
105
|
+
] + humanoid_crud.urls
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That's it. You now have, for each model:
|
|
109
|
+
|
|
110
|
+
| Method | URL | Action |
|
|
111
|
+
|--------|-------------------|-----------------|
|
|
112
|
+
| GET | `/books/` | list |
|
|
113
|
+
| POST | `/books/` | create |
|
|
114
|
+
| GET | `/books/<pk>/` | retrieve |
|
|
115
|
+
| PUT | `/books/<pk>/` | full update |
|
|
116
|
+
| PATCH | `/books/<pk>/` | partial update |
|
|
117
|
+
| DELETE | `/books/<pk>/` | delete |
|
|
118
|
+
|
|
119
|
+
All validation, 404 handling, and FK integrity checks are standard DRF
|
|
120
|
+
behavior — nothing is hidden or changed, just wired up for you.
|
|
121
|
+
|
|
122
|
+
## Going beyond the default
|
|
123
|
+
|
|
124
|
+
Every option is optional — pass only what you need:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from rest_framework.permissions import IsAuthenticated
|
|
128
|
+
|
|
129
|
+
humanoid_crud.register(
|
|
130
|
+
Book,
|
|
131
|
+
fields=["title", "author", "price"], # restrict exposed fields
|
|
132
|
+
read_only_fields=["created_at"],
|
|
133
|
+
permissions=[IsAuthenticated], # default is AllowAny — lock this down!
|
|
134
|
+
search_fields=["title"], # enables ?search=
|
|
135
|
+
ordering_fields=["price", "created_at"], # enables ?ordering=
|
|
136
|
+
filterset_fields=["author"], # exact-match filtering (needs the [filters] extra)
|
|
137
|
+
lookup="slug", # use a non-pk field in the URL
|
|
138
|
+
url="library-books", # custom URL segment
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Escape hatch: bring your own serializer or viewset
|
|
143
|
+
|
|
144
|
+
If one model needs custom logic (e.g. a custom `create()`, extra validation,
|
|
145
|
+
nested writes), you don't lose `humanoid_crud`'s URL registration — just hand
|
|
146
|
+
it your own class:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
class BookViewSet(viewsets.ModelViewSet):
|
|
150
|
+
queryset = Book.objects.all()
|
|
151
|
+
serializer_class = BookSerializer
|
|
152
|
+
|
|
153
|
+
def perform_create(self, serializer):
|
|
154
|
+
serializer.save(added_by=self.request.user)
|
|
155
|
+
|
|
156
|
+
humanoid_crud.register(Book, viewset_class=BookViewSet)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The generated classes are ordinary `ModelSerializer` / `ModelViewSet`
|
|
160
|
+
subclasses — if you ever outgrow the one-liner for a specific model, you
|
|
161
|
+
write that one model the normal DRF way and `humanoid_crud` still handles
|
|
162
|
+
the routing.
|
|
163
|
+
|
|
164
|
+
## Important: default permissions are wide open
|
|
165
|
+
|
|
166
|
+
If you don't pass `permissions=[...]`, every registered model defaults to
|
|
167
|
+
`AllowAny` — anyone can read and write it, no login required. Fine for local
|
|
168
|
+
prototyping. **Set explicit permissions before deploying anything real.**
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# humanoid-crud
|
|
2
|
+
|
|
3
|
+
A one-line CRUD registrar for Django REST Framework. Built **on top of** DRF's
|
|
4
|
+
mixins/generics (it generates real `ModelSerializer` and `ModelViewSet`
|
|
5
|
+
classes for you) — the point isn't to avoid mixins, it's that you never have
|
|
6
|
+
to write or stack them by hand.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install humanoid-crud
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If you want `filterset_fields` (exact-match filtering), install the extra:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install humanoid-crud[filters]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Note: the **package name on PyPI** is `humanoid-crud` (hyphen, matching PyPI
|
|
21
|
+
convention), but you **import it in Python** as `humanoid_crud` (underscore,
|
|
22
|
+
since hyphens aren't valid in Python identifiers). This is normal — the same
|
|
23
|
+
is true for packages like `django-rest-framework` → `import rest_framework`.
|
|
24
|
+
|
|
25
|
+
## The problem this solves
|
|
26
|
+
|
|
27
|
+
The normal DRF CRUD recipe for one model is:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# serializers.py
|
|
31
|
+
class BookSerializer(serializers.ModelSerializer):
|
|
32
|
+
class Meta:
|
|
33
|
+
model = Book
|
|
34
|
+
fields = '__all__'
|
|
35
|
+
|
|
36
|
+
# views.py
|
|
37
|
+
class BookViewSet(viewsets.ModelViewSet):
|
|
38
|
+
queryset = Book.objects.all()
|
|
39
|
+
serializer_class = BookSerializer
|
|
40
|
+
|
|
41
|
+
# urls.py
|
|
42
|
+
router.register('books', BookViewSet)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Three files, three pieces of boilerplate, for every single model.
|
|
46
|
+
`humanoid_crud` collapses that to:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import humanoid_crud
|
|
50
|
+
humanoid_crud.register(Book)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
1. `pip install humanoid-crud`
|
|
56
|
+
2. Add `'rest_framework'` to `INSTALLED_APPS` in `settings.py` if it isn't
|
|
57
|
+
already there.
|
|
58
|
+
3. Create a small file — e.g. `myapp/api.py` — where you register your
|
|
59
|
+
models:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# myapp/api.py
|
|
63
|
+
import humanoid_crud
|
|
64
|
+
from myapp.models import Book, Author
|
|
65
|
+
|
|
66
|
+
humanoid_crud.register(Author)
|
|
67
|
+
humanoid_crud.register(Book)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
4. In your project's `urls.py`, import that file (so the registrations run)
|
|
71
|
+
and add the generated routes:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from django.contrib import admin
|
|
75
|
+
from django.urls import path
|
|
76
|
+
|
|
77
|
+
from myapp import api # running this import triggers registration
|
|
78
|
+
import humanoid_crud
|
|
79
|
+
|
|
80
|
+
urlpatterns = [
|
|
81
|
+
path('admin/', admin.site.urls),
|
|
82
|
+
] + humanoid_crud.urls
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That's it. You now have, for each model:
|
|
86
|
+
|
|
87
|
+
| Method | URL | Action |
|
|
88
|
+
|--------|-------------------|-----------------|
|
|
89
|
+
| GET | `/books/` | list |
|
|
90
|
+
| POST | `/books/` | create |
|
|
91
|
+
| GET | `/books/<pk>/` | retrieve |
|
|
92
|
+
| PUT | `/books/<pk>/` | full update |
|
|
93
|
+
| PATCH | `/books/<pk>/` | partial update |
|
|
94
|
+
| DELETE | `/books/<pk>/` | delete |
|
|
95
|
+
|
|
96
|
+
All validation, 404 handling, and FK integrity checks are standard DRF
|
|
97
|
+
behavior — nothing is hidden or changed, just wired up for you.
|
|
98
|
+
|
|
99
|
+
## Going beyond the default
|
|
100
|
+
|
|
101
|
+
Every option is optional — pass only what you need:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from rest_framework.permissions import IsAuthenticated
|
|
105
|
+
|
|
106
|
+
humanoid_crud.register(
|
|
107
|
+
Book,
|
|
108
|
+
fields=["title", "author", "price"], # restrict exposed fields
|
|
109
|
+
read_only_fields=["created_at"],
|
|
110
|
+
permissions=[IsAuthenticated], # default is AllowAny — lock this down!
|
|
111
|
+
search_fields=["title"], # enables ?search=
|
|
112
|
+
ordering_fields=["price", "created_at"], # enables ?ordering=
|
|
113
|
+
filterset_fields=["author"], # exact-match filtering (needs the [filters] extra)
|
|
114
|
+
lookup="slug", # use a non-pk field in the URL
|
|
115
|
+
url="library-books", # custom URL segment
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Escape hatch: bring your own serializer or viewset
|
|
120
|
+
|
|
121
|
+
If one model needs custom logic (e.g. a custom `create()`, extra validation,
|
|
122
|
+
nested writes), you don't lose `humanoid_crud`'s URL registration — just hand
|
|
123
|
+
it your own class:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
class BookViewSet(viewsets.ModelViewSet):
|
|
127
|
+
queryset = Book.objects.all()
|
|
128
|
+
serializer_class = BookSerializer
|
|
129
|
+
|
|
130
|
+
def perform_create(self, serializer):
|
|
131
|
+
serializer.save(added_by=self.request.user)
|
|
132
|
+
|
|
133
|
+
humanoid_crud.register(Book, viewset_class=BookViewSet)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The generated classes are ordinary `ModelSerializer` / `ModelViewSet`
|
|
137
|
+
subclasses — if you ever outgrow the one-liner for a specific model, you
|
|
138
|
+
write that one model the normal DRF way and `humanoid_crud` still handles
|
|
139
|
+
the routing.
|
|
140
|
+
|
|
141
|
+
## Important: default permissions are wide open
|
|
142
|
+
|
|
143
|
+
If you don't pass `permissions=[...]`, every registered model defaults to
|
|
144
|
+
`AllowAny` — anyone can read and write it, no login required. Fine for local
|
|
145
|
+
prototyping. **Set explicit permissions before deploying anything real.**
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "humanoid-crud"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One-line CRUD registration for Django REST Framework. Built on top of DRF's mixins/generics so you never have to write a Serializer, ViewSet, or router.register() by hand for the common case."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Your Name" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["django", "django-rest-framework", "drf", "crud", "api", "rest"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Framework :: Django :: 4",
|
|
19
|
+
"Framework :: Django :: 5",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"djangorestframework>=3.14",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
filters = ["django-filter>=23.0"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/yourname/humanoid-crud"
|
|
33
|
+
Repository = "https://github.com/yourname/humanoid-crud"
|
|
34
|
+
|
|
35
|
+
# Tell setuptools the package lives under src/, named humanoid_crud
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .registry import register, router, _registry as registry # noqa: F401
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name):
|
|
7
|
+
# `humanoid_crud.urls` must be evaluated lazily, not at import time —
|
|
8
|
+
# otherwise it gets cached empty before any register() calls happen.
|
|
9
|
+
# This module-level __getattr__ (PEP 562) makes `import humanoid_crud;
|
|
10
|
+
# humanoid_crud.urls` always reflect the router's *current* state.
|
|
11
|
+
if name == "urls":
|
|
12
|
+
return router.urls
|
|
13
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HumanoidCRUD — a one-line CRUD registrar built on top of DRF's mixins/generics.
|
|
3
|
+
|
|
4
|
+
Goal: you should never have to write a Serializer, a ViewSet, or a router.register()
|
|
5
|
+
line by hand for the common case. Everything is inferred from the model, with
|
|
6
|
+
optional overrides for when you need more control.
|
|
7
|
+
|
|
8
|
+
Usage (the entire API, in your urls.py, api.py, or apps.py):
|
|
9
|
+
|
|
10
|
+
import humanoid_crud
|
|
11
|
+
humanoid_crud.register(Book)
|
|
12
|
+
|
|
13
|
+
That's it. GET/POST /books/, GET/PUT/PATCH/DELETE /books/<pk>/ all work.
|
|
14
|
+
|
|
15
|
+
Overrides when you need them:
|
|
16
|
+
|
|
17
|
+
humanoid_crud.register(
|
|
18
|
+
Book,
|
|
19
|
+
fields=["title", "author", "price"], # restrict serializer fields
|
|
20
|
+
read_only_fields=["created_at"],
|
|
21
|
+
permissions=[IsAuthenticatedOrReadOnly],
|
|
22
|
+
search_fields=["title", "author"],
|
|
23
|
+
ordering_fields=["price", "created_at"],
|
|
24
|
+
lookup="slug", # use a non-pk field in the URL
|
|
25
|
+
url="library-books", # custom URL segment
|
|
26
|
+
)
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from rest_framework import serializers, viewsets, permissions as drf_permissions
|
|
31
|
+
from rest_framework.routers import DefaultRouter
|
|
32
|
+
|
|
33
|
+
# A single shared router. Every call to register() adds to this.
|
|
34
|
+
# The project's urls.py just does: urlpatterns += humanoid_crud.urls
|
|
35
|
+
router = DefaultRouter()
|
|
36
|
+
|
|
37
|
+
# Keep track of what's registered, mostly useful for introspection/debugging.
|
|
38
|
+
_registry: dict[str, dict] = {}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _default_url_name(model) -> str:
|
|
42
|
+
"""books, not book — DRF routers expect a plural-ish URL segment."""
|
|
43
|
+
name = model._meta.model_name # e.g. "book"
|
|
44
|
+
return name if name.endswith("s") else f"{name}s"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _make_serializer(model, *, fields=None, read_only_fields=None, extra_kwargs=None):
|
|
48
|
+
"""Build a ModelSerializer class on the fly. This is the auto-inferred part."""
|
|
49
|
+
meta_attrs = {
|
|
50
|
+
"model": model,
|
|
51
|
+
"fields": fields or "__all__",
|
|
52
|
+
}
|
|
53
|
+
if read_only_fields:
|
|
54
|
+
meta_attrs["read_only_fields"] = read_only_fields
|
|
55
|
+
if extra_kwargs:
|
|
56
|
+
meta_attrs["extra_kwargs"] = extra_kwargs
|
|
57
|
+
|
|
58
|
+
Meta = type("Meta", (), meta_attrs)
|
|
59
|
+
serializer_cls = type(
|
|
60
|
+
f"{model.__name__}AutoSerializer",
|
|
61
|
+
(serializers.ModelSerializer,),
|
|
62
|
+
{"Meta": Meta},
|
|
63
|
+
)
|
|
64
|
+
return serializer_cls
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_viewset(
|
|
68
|
+
model,
|
|
69
|
+
serializer_cls,
|
|
70
|
+
*,
|
|
71
|
+
queryset=None,
|
|
72
|
+
permissions=None,
|
|
73
|
+
search_fields=None,
|
|
74
|
+
ordering_fields=None,
|
|
75
|
+
filterset_fields=None,
|
|
76
|
+
lookup="pk",
|
|
77
|
+
):
|
|
78
|
+
"""Build a ModelViewSet — this is the part that replaces hand-stacked mixins."""
|
|
79
|
+
from rest_framework import filters as drf_filters
|
|
80
|
+
|
|
81
|
+
attrs = {
|
|
82
|
+
"queryset": queryset if queryset is not None else model.objects.all(),
|
|
83
|
+
"serializer_class": serializer_cls,
|
|
84
|
+
"lookup_field": lookup,
|
|
85
|
+
"permission_classes": permissions or [drf_permissions.AllowAny],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
filter_backends = []
|
|
89
|
+
|
|
90
|
+
if search_fields:
|
|
91
|
+
attrs["search_fields"] = search_fields
|
|
92
|
+
filter_backends.append(drf_filters.SearchFilter)
|
|
93
|
+
if ordering_fields:
|
|
94
|
+
attrs["ordering_fields"] = ordering_fields
|
|
95
|
+
filter_backends.append(drf_filters.OrderingFilter)
|
|
96
|
+
if filterset_fields:
|
|
97
|
+
# django-filter is an optional dependency — only import if actually used.
|
|
98
|
+
try:
|
|
99
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
|
100
|
+
except ImportError as exc:
|
|
101
|
+
raise ImportError(
|
|
102
|
+
"filterset_fields requires django-filter. "
|
|
103
|
+
"Install it with: pip install django-filter"
|
|
104
|
+
) from exc
|
|
105
|
+
attrs["filterset_fields"] = filterset_fields
|
|
106
|
+
filter_backends.append(DjangoFilterBackend)
|
|
107
|
+
|
|
108
|
+
if filter_backends:
|
|
109
|
+
attrs["filter_backends"] = filter_backends
|
|
110
|
+
|
|
111
|
+
viewset_cls = type(
|
|
112
|
+
f"{model.__name__}AutoViewSet",
|
|
113
|
+
(viewsets.ModelViewSet,),
|
|
114
|
+
attrs,
|
|
115
|
+
)
|
|
116
|
+
return viewset_cls
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def register(
|
|
120
|
+
model,
|
|
121
|
+
*,
|
|
122
|
+
fields=None,
|
|
123
|
+
read_only_fields=None,
|
|
124
|
+
extra_kwargs=None,
|
|
125
|
+
queryset=None,
|
|
126
|
+
permissions=None,
|
|
127
|
+
search_fields=None,
|
|
128
|
+
ordering_fields=None,
|
|
129
|
+
filterset_fields=None,
|
|
130
|
+
lookup="pk",
|
|
131
|
+
url=None,
|
|
132
|
+
serializer_class=None,
|
|
133
|
+
viewset_class=None,
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Register full CRUD for `model` in one call.
|
|
137
|
+
|
|
138
|
+
Required:
|
|
139
|
+
model: a Django model class.
|
|
140
|
+
|
|
141
|
+
Everything else is optional. If you don't pass anything, you get a
|
|
142
|
+
sensible default: all fields exposed, AllowAny permissions, pk lookup,
|
|
143
|
+
URL inferred from the model name.
|
|
144
|
+
|
|
145
|
+
Escape hatches:
|
|
146
|
+
serializer_class / viewset_class: pass your own hand-written class
|
|
147
|
+
if auto-inference isn't enough for this one model. HumanoidCRUD will
|
|
148
|
+
still handle the router registration for you.
|
|
149
|
+
"""
|
|
150
|
+
url_name = url or _default_url_name(model)
|
|
151
|
+
|
|
152
|
+
serializer_cls = serializer_class or _make_serializer(
|
|
153
|
+
model,
|
|
154
|
+
fields=fields,
|
|
155
|
+
read_only_fields=read_only_fields,
|
|
156
|
+
extra_kwargs=extra_kwargs,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
viewset_cls = viewset_class or _make_viewset(
|
|
160
|
+
model,
|
|
161
|
+
serializer_cls,
|
|
162
|
+
queryset=queryset,
|
|
163
|
+
permissions=permissions,
|
|
164
|
+
search_fields=search_fields,
|
|
165
|
+
ordering_fields=ordering_fields,
|
|
166
|
+
filterset_fields=filterset_fields,
|
|
167
|
+
lookup=lookup,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
router.register(url_name, viewset_cls, basename=url_name)
|
|
171
|
+
|
|
172
|
+
_registry[url_name] = {
|
|
173
|
+
"model": model,
|
|
174
|
+
"serializer": serializer_cls,
|
|
175
|
+
"viewset": viewset_cls,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return viewset_cls # handy if caller wants to extend it further
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: humanoid-crud
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One-line CRUD registration for Django REST Framework. Built on top of DRF's mixins/generics so you never have to write a Serializer, ViewSet, or router.register() by hand for the common case.
|
|
5
|
+
Author: Your Name
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourname/humanoid-crud
|
|
8
|
+
Project-URL: Repository, https://github.com/yourname/humanoid-crud
|
|
9
|
+
Keywords: django,django-rest-framework,drf,crud,api,rest
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Framework :: Django :: 4
|
|
12
|
+
Classifier: Framework :: Django :: 5
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: djangorestframework>=3.14
|
|
20
|
+
Provides-Extra: filters
|
|
21
|
+
Requires-Dist: django-filter>=23.0; extra == "filters"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# humanoid-crud
|
|
25
|
+
|
|
26
|
+
A one-line CRUD registrar for Django REST Framework. Built **on top of** DRF's
|
|
27
|
+
mixins/generics (it generates real `ModelSerializer` and `ModelViewSet`
|
|
28
|
+
classes for you) — the point isn't to avoid mixins, it's that you never have
|
|
29
|
+
to write or stack them by hand.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install humanoid-crud
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you want `filterset_fields` (exact-match filtering), install the extra:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install humanoid-crud[filters]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Note: the **package name on PyPI** is `humanoid-crud` (hyphen, matching PyPI
|
|
44
|
+
convention), but you **import it in Python** as `humanoid_crud` (underscore,
|
|
45
|
+
since hyphens aren't valid in Python identifiers). This is normal — the same
|
|
46
|
+
is true for packages like `django-rest-framework` → `import rest_framework`.
|
|
47
|
+
|
|
48
|
+
## The problem this solves
|
|
49
|
+
|
|
50
|
+
The normal DRF CRUD recipe for one model is:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# serializers.py
|
|
54
|
+
class BookSerializer(serializers.ModelSerializer):
|
|
55
|
+
class Meta:
|
|
56
|
+
model = Book
|
|
57
|
+
fields = '__all__'
|
|
58
|
+
|
|
59
|
+
# views.py
|
|
60
|
+
class BookViewSet(viewsets.ModelViewSet):
|
|
61
|
+
queryset = Book.objects.all()
|
|
62
|
+
serializer_class = BookSerializer
|
|
63
|
+
|
|
64
|
+
# urls.py
|
|
65
|
+
router.register('books', BookViewSet)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Three files, three pieces of boilerplate, for every single model.
|
|
69
|
+
`humanoid_crud` collapses that to:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import humanoid_crud
|
|
73
|
+
humanoid_crud.register(Book)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
1. `pip install humanoid-crud`
|
|
79
|
+
2. Add `'rest_framework'` to `INSTALLED_APPS` in `settings.py` if it isn't
|
|
80
|
+
already there.
|
|
81
|
+
3. Create a small file — e.g. `myapp/api.py` — where you register your
|
|
82
|
+
models:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# myapp/api.py
|
|
86
|
+
import humanoid_crud
|
|
87
|
+
from myapp.models import Book, Author
|
|
88
|
+
|
|
89
|
+
humanoid_crud.register(Author)
|
|
90
|
+
humanoid_crud.register(Book)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
4. In your project's `urls.py`, import that file (so the registrations run)
|
|
94
|
+
and add the generated routes:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from django.contrib import admin
|
|
98
|
+
from django.urls import path
|
|
99
|
+
|
|
100
|
+
from myapp import api # running this import triggers registration
|
|
101
|
+
import humanoid_crud
|
|
102
|
+
|
|
103
|
+
urlpatterns = [
|
|
104
|
+
path('admin/', admin.site.urls),
|
|
105
|
+
] + humanoid_crud.urls
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That's it. You now have, for each model:
|
|
109
|
+
|
|
110
|
+
| Method | URL | Action |
|
|
111
|
+
|--------|-------------------|-----------------|
|
|
112
|
+
| GET | `/books/` | list |
|
|
113
|
+
| POST | `/books/` | create |
|
|
114
|
+
| GET | `/books/<pk>/` | retrieve |
|
|
115
|
+
| PUT | `/books/<pk>/` | full update |
|
|
116
|
+
| PATCH | `/books/<pk>/` | partial update |
|
|
117
|
+
| DELETE | `/books/<pk>/` | delete |
|
|
118
|
+
|
|
119
|
+
All validation, 404 handling, and FK integrity checks are standard DRF
|
|
120
|
+
behavior — nothing is hidden or changed, just wired up for you.
|
|
121
|
+
|
|
122
|
+
## Going beyond the default
|
|
123
|
+
|
|
124
|
+
Every option is optional — pass only what you need:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from rest_framework.permissions import IsAuthenticated
|
|
128
|
+
|
|
129
|
+
humanoid_crud.register(
|
|
130
|
+
Book,
|
|
131
|
+
fields=["title", "author", "price"], # restrict exposed fields
|
|
132
|
+
read_only_fields=["created_at"],
|
|
133
|
+
permissions=[IsAuthenticated], # default is AllowAny — lock this down!
|
|
134
|
+
search_fields=["title"], # enables ?search=
|
|
135
|
+
ordering_fields=["price", "created_at"], # enables ?ordering=
|
|
136
|
+
filterset_fields=["author"], # exact-match filtering (needs the [filters] extra)
|
|
137
|
+
lookup="slug", # use a non-pk field in the URL
|
|
138
|
+
url="library-books", # custom URL segment
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Escape hatch: bring your own serializer or viewset
|
|
143
|
+
|
|
144
|
+
If one model needs custom logic (e.g. a custom `create()`, extra validation,
|
|
145
|
+
nested writes), you don't lose `humanoid_crud`'s URL registration — just hand
|
|
146
|
+
it your own class:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
class BookViewSet(viewsets.ModelViewSet):
|
|
150
|
+
queryset = Book.objects.all()
|
|
151
|
+
serializer_class = BookSerializer
|
|
152
|
+
|
|
153
|
+
def perform_create(self, serializer):
|
|
154
|
+
serializer.save(added_by=self.request.user)
|
|
155
|
+
|
|
156
|
+
humanoid_crud.register(Book, viewset_class=BookViewSet)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The generated classes are ordinary `ModelSerializer` / `ModelViewSet`
|
|
160
|
+
subclasses — if you ever outgrow the one-liner for a specific model, you
|
|
161
|
+
write that one model the normal DRF way and `humanoid_crud` still handles
|
|
162
|
+
the routing.
|
|
163
|
+
|
|
164
|
+
## Important: default permissions are wide open
|
|
165
|
+
|
|
166
|
+
If you don't pass `permissions=[...]`, every registered model defaults to
|
|
167
|
+
`AllowAny` — anyone can read and write it, no login required. Fine for local
|
|
168
|
+
prototyping. **Set explicit permissions before deploying anything real.**
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/humanoid_crud/__init__.py
|
|
5
|
+
src/humanoid_crud/registry.py
|
|
6
|
+
src/humanoid_crud.egg-info/PKG-INFO
|
|
7
|
+
src/humanoid_crud.egg-info/SOURCES.txt
|
|
8
|
+
src/humanoid_crud.egg-info/dependency_links.txt
|
|
9
|
+
src/humanoid_crud.egg-info/requires.txt
|
|
10
|
+
src/humanoid_crud.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
humanoid_crud
|