turbodrf 0.1.0__py3-none-any.whl

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.
turbodrf/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ TurboDRF - Dead simple Django REST API generator with role-based permissions.
3
+
4
+ TurboDRF automatically generates a complete REST API from your Django models
5
+ with just
6
+ a mixin and a configuration method. It provides:
7
+
8
+ - Automatic CRUD endpoints for all models
9
+ - Role-based permissions at model and field level
10
+ - Built-in search, filtering, ordering, and pagination
11
+ - Nested field support for relationships
12
+ - Auto-generated API documentation with Swagger/ReDoc
13
+ - Zero boilerplate code required
14
+
15
+ Quick Start:
16
+ >>> from django.db import models
17
+ >>> from turbodrf import TurboDRFMixin
18
+ >>>
19
+ >>> class Book(models.Model, TurboDRFMixin):
20
+ ... title = models.CharField(max_length=200)
21
+ ... author = models.CharField(max_length=100)
22
+ ...
23
+ ... @classmethod
24
+ ... def turbodrf(cls):
25
+ ... return {'fields': ['title', 'author']}
26
+
27
+ This automatically creates:
28
+ - GET /api/books/ - List all books
29
+ - POST /api/books/ - Create a new book
30
+ - GET /api/books/{id}/ - Get a specific book
31
+ - PUT /api/books/{id}/ - Update a book
32
+ - DELETE /api/books/{id}/ - Delete a book
33
+
34
+ For more information, see: https://github.com/yourusername/turbodrf
35
+ """
36
+
37
+ __version__ = "0.1.0"
38
+ __author__ = "Alexander Collins"
39
+ __email__ = "your.email@example.com"
40
+ __license__ = "MIT"
41
+
42
+ from .mixins import TurboDRFMixin
43
+ from .permissions import TurboDRFPermission
44
+ from .router import TurboDRFRouter
45
+ from .serializers import TurboDRFSerializer, TurboDRFSerializerFactory
46
+ from .views import TurboDRFViewSet
47
+
48
+ __all__ = [
49
+ "TurboDRFMixin",
50
+ "TurboDRFRouter",
51
+ "TurboDRFViewSet",
52
+ "TurboDRFPermission",
53
+ "TurboDRFSerializer",
54
+ "TurboDRFSerializerFactory",
55
+ ]
turbodrf/apps.py ADDED
@@ -0,0 +1,15 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class TurboDRFConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "turbodrf"
7
+ verbose_name = "TurboDRF"
8
+
9
+ def ready(self):
10
+ """
11
+ Perform initialization when Django starts.
12
+ """
13
+ # This ensures models are discovered when Django starts
14
+ # You can add any other initialization code here
15
+ pass
@@ -0,0 +1,94 @@
1
+ """
2
+ TurboDRF API Documentation Module
3
+
4
+ This module provides automatic API documentation generation for
5
+ TurboDRF-enabled applications. It integrates with drf-yasg to create
6
+ OpenAPI/Swagger documentation
7
+ that respects TurboDRF's role-based access control system.
8
+
9
+ The documentation is dynamically filtered based on user roles, showing only
10
+ the endpoints and fields that each role has permission to access.
11
+ """
12
+
13
+ from django.conf import settings
14
+ from drf_yasg import openapi
15
+ from drf_yasg.views import get_schema_view
16
+ from rest_framework import permissions
17
+
18
+
19
+ def get_turbodrf_schema_view():
20
+ """
21
+ Create and configure the schema view for TurboDRF API documentation.
22
+
23
+ This function sets up a drf-yasg schema view with TurboDRF's custom
24
+ schema generator that filters API documentation based on user roles
25
+ and permissions. The generated documentation will only show endpoints
26
+ and fields that the viewing user has permission to access.
27
+
28
+ Features:
29
+ - Role-based filtering of API endpoints
30
+ - Dynamic field visibility based on permissions
31
+ - Automatic schema generation from model configurations
32
+ - Support for both Swagger UI and ReDoc interfaces
33
+
34
+ Returns:
35
+ SchemaView: A configured drf-yasg SchemaView instance that can be
36
+ used to generate multiple documentation formats:
37
+ - Swagger UI (interactive HTML)
38
+ - ReDoc (alternative HTML documentation)
39
+ - OpenAPI JSON/YAML schemas
40
+
41
+ Returns None if TURBODRF_ENABLE_DOCS is set to False.
42
+
43
+ Usage:
44
+ # In your urls.py
45
+ from turbodrf.documentation import get_turbodrf_schema_view
46
+
47
+ schema_view = get_turbodrf_schema_view()
48
+
49
+ if schema_view: # Only add URLs if docs are enabled
50
+ urlpatterns += [
51
+ path('swagger/',
52
+ schema_view.with_ui('swagger', cache_timeout=0)),
53
+ path('redoc/', schema_view.with_ui('redoc', cache_timeout=0)),
54
+ path('swagger.json', schema_view.without_ui(cache_timeout=0)),
55
+ ]
56
+
57
+ Configuration:
58
+ The schema view uses the following configuration:
59
+ - Title: "TurboDRF API" (customizable)
60
+ - Version: "v1"
61
+ - Generator: RoleBasedSchemaGenerator for permission filtering
62
+ - Permissions: AllowAny (filtering happens at schema level)
63
+
64
+ Set TURBODRF_ENABLE_DOCS = False in settings to disable documentation.
65
+
66
+ Note:
67
+ While the view itself allows any user to access the documentation,
68
+ the RoleBasedSchemaGenerator ensures that users only see the parts
69
+ of the API they have permission to use based on their roles.
70
+ """
71
+ # Check if documentation is enabled
72
+ if not getattr(settings, "TURBODRF_ENABLE_DOCS", True):
73
+ return None
74
+
75
+ from .swagger import RoleBasedSchemaGenerator
76
+
77
+ schema_view = get_schema_view(
78
+ openapi.Info(
79
+ title="TurboDRF API",
80
+ default_version="v1",
81
+ description=(
82
+ "Auto-generated API with role-based access control "
83
+ "powered by TurboDRF"
84
+ ),
85
+ terms_of_service="https://www.example.com/terms/",
86
+ contact=openapi.Contact(email="contact@example.com"),
87
+ license=openapi.License(name="MIT License"),
88
+ ),
89
+ public=True,
90
+ permission_classes=[permissions.AllowAny],
91
+ generator_class=RoleBasedSchemaGenerator,
92
+ )
93
+
94
+ return schema_view
turbodrf/filters.py ADDED
@@ -0,0 +1,64 @@
1
+ """
2
+ Advanced filtering support for TurboDRF
3
+ """
4
+
5
+ from django.db.models import Q
6
+
7
+
8
+ class TurboDRFFilter:
9
+ """Advanced filtering with support for complex queries."""
10
+
11
+ @classmethod
12
+ def parse_filters(cls, queryset, params):
13
+ """
14
+ Parse query parameters and apply filters.
15
+
16
+ Supports:
17
+ - field=value (exact match)
18
+ - field__gt=value (greater than)
19
+ - field__contains=value (contains)
20
+ - field__in=value1,value2 (in list)
21
+ - etc.
22
+ """
23
+ for key, value in params.items():
24
+ if key in ["page", "page_size", "search", "ordering", "format"]:
25
+ continue
26
+
27
+ # Handle special operators
28
+ if "__in" in key:
29
+ # Convert comma-separated values to list
30
+ value = value.split(",")
31
+
32
+ try:
33
+ queryset = queryset.filter(**{key: value})
34
+ except Exception:
35
+ # Skip invalid filters
36
+ pass
37
+
38
+ return queryset
39
+
40
+ @classmethod
41
+ def parse_search(cls, queryset, search_term, search_fields):
42
+ """
43
+ Apply search across multiple fields.
44
+
45
+ Supports:
46
+ - Simple search: searches all fields
47
+ - Field-specific: field:term
48
+ - Quoted: "exact phrase"
49
+ """
50
+ if not search_term or not search_fields:
51
+ return queryset
52
+
53
+ # Check for field-specific search
54
+ if ":" in search_term:
55
+ field, term = search_term.split(":", 1)
56
+ if field in search_fields:
57
+ return queryset.filter(**{f"{field}__icontains": term})
58
+
59
+ # Build OR query for all search fields
60
+ q_objects = Q()
61
+ for field in search_fields:
62
+ q_objects |= Q(**{f"{field}__icontains": search_term})
63
+
64
+ return queryset.filter(q_objects)
turbodrf/metadata.py ADDED
@@ -0,0 +1,110 @@
1
+ from rest_framework.metadata import SimpleMetadata
2
+
3
+
4
+ class TurboDRFMetadata(SimpleMetadata):
5
+ """Custom metadata handler for TurboDRF OPTIONS requests."""
6
+
7
+ def determine_metadata(self, request, view):
8
+ """Return metadata for OPTIONS requests."""
9
+ metadata = super().determine_metadata(request, view)
10
+
11
+ if hasattr(view, "model"):
12
+ model = view.model
13
+ view_type = (
14
+ "detail"
15
+ if view.action in ["retrieve", "update", "partial_update"]
16
+ else "list"
17
+ )
18
+ fields = model.get_api_fields(view_type)
19
+
20
+ # Add model info
21
+ metadata["model"] = {
22
+ "name": model._meta.verbose_name,
23
+ "app_label": model._meta.app_label,
24
+ "fields": self._get_field_metadata(model, fields, request.user),
25
+ }
26
+
27
+ # Add allowed actions based on permissions
28
+ metadata["actions"] = self._get_allowed_actions(model, request.user)
29
+
30
+ return metadata
31
+
32
+ def _get_field_metadata(self, model, fields, user):
33
+ """Get metadata for each field."""
34
+ from .settings import TURBODRF_ROLES
35
+
36
+ user_permissions = set()
37
+ if hasattr(user, "roles"):
38
+ for role in user.roles:
39
+ user_permissions.update(TURBODRF_ROLES.get(role, []))
40
+
41
+ field_metadata = {}
42
+ app_label = model._meta.app_label
43
+ model_name = model._meta.model_name
44
+
45
+ for field_name in fields:
46
+ if "__" in field_name:
47
+ # Handle nested fields
48
+ base_field = field_name.split("__")[0]
49
+ if base_field not in field_metadata:
50
+ field_metadata[base_field] = {"type": "nested", "fields": []}
51
+ field_metadata[base_field]["fields"].append(
52
+ field_name.split("__", 1)[1]
53
+ )
54
+ else:
55
+ try:
56
+ field = model._meta.get_field(field_name)
57
+ can_read = (
58
+ f"{app_label}.{model_name}.{field_name}.read"
59
+ in user_permissions
60
+ )
61
+ can_write = (
62
+ f"{app_label}.{model_name}.{field_name}.write"
63
+ in user_permissions
64
+ )
65
+
66
+ field_info = {
67
+ "type": field.__class__.__name__,
68
+ "required": (
69
+ not field.blank if hasattr(field, "blank") else True
70
+ ),
71
+ "read_only": not can_write,
72
+ "write_only": not can_read and can_write,
73
+ "label": field.verbose_name,
74
+ "help_text": field.help_text or "",
75
+ }
76
+
77
+ # Add field-specific metadata
78
+ if hasattr(field, "max_length"):
79
+ field_info["max_length"] = field.max_length
80
+ if hasattr(field, "choices") and field.choices:
81
+ field_info["choices"] = [
82
+ {"value": k, "display": v} for k, v in field.choices
83
+ ]
84
+
85
+ field_metadata[field_name] = field_info
86
+ except Exception:
87
+ field_metadata[field_name] = {"type": "unknown"}
88
+
89
+ return field_metadata
90
+
91
+ def _get_allowed_actions(self, model, user):
92
+ """Get allowed actions based on user permissions."""
93
+ from .settings import TURBODRF_ROLES
94
+
95
+ user_permissions = set()
96
+ if hasattr(user, "roles"):
97
+ for role in user.roles:
98
+ user_permissions.update(TURBODRF_ROLES.get(role, []))
99
+
100
+ app_label = model._meta.app_label
101
+ model_name = model._meta.model_name
102
+
103
+ return {
104
+ "list": f"{app_label}.{model_name}.read" in user_permissions,
105
+ "retrieve": f"{app_label}.{model_name}.read" in user_permissions,
106
+ "create": f"{app_label}.{model_name}.create" in user_permissions,
107
+ "update": f"{app_label}.{model_name}.update" in user_permissions,
108
+ "partial_update": (f"{app_label}.{model_name}.update" in user_permissions),
109
+ "destroy": f"{app_label}.{model_name}.delete" in user_permissions,
110
+ }
turbodrf/mixins.py ADDED
@@ -0,0 +1,160 @@
1
+ """
2
+ Core mixins for TurboDRF.
3
+
4
+ This module provides the TurboDRFMixin that enables automatic API generation
5
+ for Django models.
6
+ """
7
+
8
+ from django.core.exceptions import FieldDoesNotExist
9
+
10
+
11
+ class TurboDRFMixin:
12
+ """
13
+ Mixin to add TurboDRF capabilities to Django models.
14
+
15
+ This mixin enables automatic REST API generation for any Django model.
16
+ By inheriting from this mixin and defining a turbodrf() classmethod,
17
+ models will be automatically discovered and exposed via REST endpoints.
18
+
19
+ Example:
20
+ >>> class Book(models.Model, TurboDRFMixin):
21
+ ... title = models.CharField(max_length=200)
22
+ ... author = models.CharField(max_length=100)
23
+ ... price = models.DecimalField(max_digits=10, decimal_places=2)
24
+ ...
25
+ ... # Optional: Define searchable fields
26
+ ... searchable_fields = ['title', 'author']
27
+ ...
28
+ ... @classmethod
29
+ ... def turbodrf(cls):
30
+ ... return {
31
+ ... 'fields': ['title', 'author', 'price']
32
+ ... }
33
+
34
+ The mixin provides several helper methods for field introspection and
35
+ configuration that are used internally by TurboDRF.
36
+ """
37
+
38
+ @classmethod
39
+ def turbodrf(cls):
40
+ """
41
+ Configure the API for this model.
42
+
43
+ This method should be overridden in subclasses to provide
44
+ model-specific configuration for the auto-generated API.
45
+
46
+ Returns:
47
+ dict: Configuration dictionary with the following keys:
48
+ - 'enabled' (bool): Whether to enable API for this model.
49
+ Defaults to True.
50
+ - 'endpoint' (str): Custom endpoint name. If not provided,
51
+ defaults to the pluralized model name.
52
+ - 'fields' (list|dict|str): Fields to expose in the API.
53
+ Can be:
54
+ - A list of field names: ['title', 'author', 'price']
55
+ - A dict with different fields for list/detail views:
56
+ {'list': ['title', 'author'], 'detail': '__all__'}
57
+ - The string '__all__' to include all fields
58
+
59
+ Example:
60
+ >>> @classmethod
61
+ ... def turbodrf(cls):
62
+ ... return {
63
+ ... 'fields': {
64
+ ... 'list': ['title', 'author__name', 'price'],
65
+ ... 'detail': ['title', 'author__name',
66
+ ... 'author__email', 'price',
67
+ ... 'description', 'created_at']
68
+ ... },
69
+ ... 'endpoint': 'books',
70
+ ... 'enabled': True
71
+ ... }
72
+ """
73
+ return {
74
+ "enabled": True,
75
+ "endpoint": f"{cls._meta.model_name}s",
76
+ "fields": "__all__",
77
+ }
78
+
79
+ @classmethod
80
+ def get_api_fields(cls, view_type="list"):
81
+ """
82
+ Get fields for a specific view type.
83
+
84
+ This method resolves the fields configuration based on the view type
85
+ (list or detail). It handles the various field configuration formats
86
+ supported by TurboDRF.
87
+
88
+ Args:
89
+ view_type (str): The type of view ('list' or 'detail').
90
+ Defaults to 'list'.
91
+
92
+ Returns:
93
+ list: List of field names to include in the API response.
94
+
95
+ Example:
96
+ >>> Book.get_api_fields('list')
97
+ ['title', 'author', 'price']
98
+ >>> Book.get_api_fields('detail')
99
+ ['title', 'author', 'price', 'description', 'created_at']
100
+ """
101
+ config = cls.turbodrf()
102
+ fields = config.get("fields", "__all__")
103
+
104
+ if fields == "__all__":
105
+ # Get all model fields except reverse relations
106
+ return [
107
+ f.name
108
+ for f in cls._meta.get_fields()
109
+ if not f.many_to_many and not f.one_to_many
110
+ ]
111
+
112
+ if isinstance(fields, dict):
113
+ # Different fields for list/detail views
114
+ return fields.get(view_type, [])
115
+
116
+ # If it's a list, use for both views
117
+ return fields
118
+
119
+ @classmethod
120
+ def get_field_type(cls, field_path):
121
+ """
122
+ Resolve field type for nested fields.
123
+
124
+ This method traverses relationships to determine the type of a field
125
+ specified using Django's double-underscore notation
126
+ (e.g., 'author__name').
127
+
128
+ Args:
129
+ field_path (str): Field path using double-underscore notation.
130
+ Example: 'author__name' or 'category__parent__name'
131
+
132
+ Returns:
133
+ Field: The Django field instance, or None if the field
134
+ doesn't exist.
135
+
136
+ Example:
137
+ >>> Book.get_field_type('author__name')
138
+ <django.db.models.fields.CharField: name>
139
+ >>> Book.get_field_type('nonexistent__field')
140
+ None
141
+ """
142
+ parts = field_path.split("__")
143
+ model = cls
144
+
145
+ # Traverse relationships
146
+ for part in parts[:-1]:
147
+ try:
148
+ field = model._meta.get_field(part)
149
+ if hasattr(field, "related_model"):
150
+ model = field.related_model
151
+ else:
152
+ return None
153
+ except FieldDoesNotExist:
154
+ return None
155
+
156
+ # Get the final field
157
+ try:
158
+ return model._meta.get_field(parts[-1])
159
+ except FieldDoesNotExist:
160
+ return None
@@ -0,0 +1,181 @@
1
+ """
2
+ Role-based permissions for TurboDRF.
3
+
4
+ This module provides the permission classes that enforce role-based
5
+ access control for the auto-generated APIs.
6
+ """
7
+
8
+ from rest_framework.permissions import BasePermission, DjangoModelPermissions
9
+
10
+
11
+ class TurboDRFPermission(BasePermission):
12
+ """
13
+ Permission class that checks against role-based permissions.
14
+
15
+ This permission class implements a flexible role-based access control
16
+ system for TurboDRF. It checks user roles against configured permissions
17
+ to determine access to API endpoints.
18
+
19
+ Permission Format:
20
+ Permissions follow the pattern: app_label.model_name.action
21
+
22
+ Model-level permissions:
23
+ - 'books.book.read' - Can view books
24
+ - 'books.book.create' - Can create books
25
+ - 'books.book.update' - Can update books
26
+ - 'books.book.delete' - Can delete books
27
+
28
+ Field-level permissions (checked separately):
29
+ - 'books.book.price.read' - Can view price field
30
+ - 'books.book.price.write' - Can modify price field
31
+
32
+ Configuration:
33
+ Define roles and permissions in Django settings:
34
+
35
+ TURBODRF_ROLES = {
36
+ 'admin': [
37
+ 'books.book.read',
38
+ 'books.book.create',
39
+ 'books.book.update',
40
+ 'books.book.delete',
41
+ 'books.book.price.read',
42
+ 'books.book.price.write',
43
+ ],
44
+ 'editor': [
45
+ 'books.book.read',
46
+ 'books.book.update',
47
+ 'books.book.price.read', # Read-only access to price
48
+ ],
49
+ 'viewer': [
50
+ 'books.book.read',
51
+ # No access to price field
52
+ ]
53
+ }
54
+
55
+ Special Cases:
56
+ - Unauthenticated users are allowed read-only access (GET requests)
57
+ - Users must have a 'roles' property that returns a list of role names
58
+ """
59
+
60
+ def has_permission(self, request, view):
61
+ """
62
+ Check if the user has permission to perform the requested action.
63
+
64
+ Args:
65
+ request: The incoming HTTP request.
66
+ view: The view being accessed.
67
+
68
+ Returns:
69
+ bool: True if permission is granted, False otherwise.
70
+ """
71
+ # Allow read access for unauthenticated users (viewer role)
72
+ if not request.user or not request.user.is_authenticated:
73
+ # Only allow GET and OPTIONS requests for unauthenticated users
74
+ return request.method in ["GET", "OPTIONS"]
75
+
76
+ if not hasattr(request.user, "roles"):
77
+ return False
78
+
79
+ user_permissions = self._get_user_permissions(request.user)
80
+ model = view.model
81
+ app_label = model._meta.app_label
82
+ model_name = model._meta.model_name
83
+
84
+ # Map HTTP methods to permission types
85
+ permission_map = {
86
+ "GET": "read",
87
+ "POST": "create",
88
+ "PUT": "update",
89
+ "PATCH": "update",
90
+ "DELETE": "delete",
91
+ "OPTIONS": "read", # OPTIONS should have same permission as read
92
+ }
93
+
94
+ permission_type = permission_map.get(request.method)
95
+ if not permission_type:
96
+ return False
97
+
98
+ # Check model-level permission
99
+ required_permission = f"{app_label}.{model_name}.{permission_type}"
100
+ return required_permission in user_permissions
101
+
102
+ def _get_user_permissions(self, user):
103
+ """
104
+ Get all permissions for a user based on their roles.
105
+
106
+ This method aggregates permissions from all roles assigned to a user.
107
+
108
+ Args:
109
+ user: The user object, which must have a 'roles' property.
110
+
111
+ Returns:
112
+ set: Set of permission strings the user has.
113
+
114
+ Example:
115
+ If a user has roles ['admin', 'editor'], this method returns
116
+ the union of all permissions defined for both roles.
117
+ """
118
+ from django.conf import settings
119
+
120
+ from .settings import TURBODRF_ROLES as default_roles
121
+
122
+ # Use Django settings if available, otherwise fall back to defaults
123
+ TURBODRF_ROLES = getattr(settings, "TURBODRF_ROLES", default_roles)
124
+
125
+ permissions = set()
126
+ for role in user.roles:
127
+ permissions.update(TURBODRF_ROLES.get(role, []))
128
+
129
+ return permissions
130
+
131
+
132
+ class DefaultDjangoPermission(DjangoModelPermissions):
133
+ """
134
+ Default permission class using Django's built-in model permissions.
135
+
136
+ This permission class provides a simpler alternative to TurboDRF's role-based
137
+ permissions by using Django's standard permission system. When a user has
138
+ write permission for a model, they have write access to all fields.
139
+
140
+ Features:
141
+ - Uses Django's built-in auth permissions (add, change, delete, view)
142
+ - Integrates with Django admin permissions
143
+ - No field-level permissions (all fields are accessible based on
144
+ model permissions)
145
+ - Works with Django's User and Group models out of the box
146
+
147
+ Usage:
148
+ To use Django's default permissions instead of TurboDRF's
149
+ role-based system,
150
+ set TURBODRF_USE_DEFAULT_PERMISSIONS = True in your settings.
151
+
152
+ Permission Mapping:
153
+ - GET: requires 'view' permission
154
+ - POST: requires 'add' permission
155
+ - PUT/PATCH: requires 'change' permission
156
+ - DELETE: requires 'delete' permission
157
+ """
158
+
159
+ # Extend the default perms map to include OPTIONS
160
+ perms_map = {
161
+ "GET": ["%(app_label)s.view_%(model_name)s"],
162
+ "OPTIONS": ["%(app_label)s.view_%(model_name)s"],
163
+ "HEAD": ["%(app_label)s.view_%(model_name)s"],
164
+ "POST": ["%(app_label)s.add_%(model_name)s"],
165
+ "PUT": ["%(app_label)s.change_%(model_name)s"],
166
+ "PATCH": ["%(app_label)s.change_%(model_name)s"],
167
+ "DELETE": ["%(app_label)s.delete_%(model_name)s"],
168
+ }
169
+
170
+ def has_permission(self, request, view):
171
+ """
172
+ Check if the user has permission to perform the requested action.
173
+
174
+ For unauthenticated users, allows read-only access by default.
175
+ """
176
+ # Allow read access for unauthenticated users
177
+ if not request.user or not request.user.is_authenticated:
178
+ return request.method in ["GET", "OPTIONS", "HEAD"]
179
+
180
+ # For authenticated users, use Django's permission system
181
+ return super().has_permission(request, view)