django-cfg 1.2.12__py3-none-any.whl → 1.2.14__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.
django_cfg/__init__.py CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.2.12"
35
+ __version__ = "1.2.14"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
django_cfg/apps/urls.py CHANGED
@@ -45,8 +45,8 @@ def get_django_cfg_urlpatterns() -> List[URLPattern]:
45
45
  # patterns.append(path('leads/', include('django_cfg.apps.leads.urls')))
46
46
 
47
47
  # Tasks app - enabled when knowbase or agents are enabled
48
- # if base_module.is_tasks_enabled():
49
- # patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
48
+ if base_module.is_tasks_enabled():
49
+ patterns.append(path('tasks/', include('django_cfg.apps.tasks.urls')))
50
50
 
51
51
  except Exception:
52
52
  # Fallback: include all URLs if config is not available
@@ -467,24 +467,52 @@ class SettingsGenerator:
467
467
  except Exception as e:
468
468
  logger.warning(f"Could not generate DRF config from Revolution: {e}")
469
469
 
470
- # Apply django-cfg DRF/Spectacular extensions (only if explicitly configured by user)
470
+ # Apply django-cfg DRF/Spectacular extensions
471
471
  try:
472
- if config.drf or config.spectacular:
473
- # Extend REST_FRAMEWORK settings if config.drf is provided
474
- if config.drf and "REST_FRAMEWORK" in settings:
475
- drf_extensions = config.drf.get_rest_framework_settings()
476
- # Merge with existing settings (django-cfg extensions take priority)
477
- settings["REST_FRAMEWORK"].update(drf_extensions)
478
- logger.info("🔧 Extended REST_FRAMEWORK settings with django-cfg DRF config")
479
-
480
- # Extend SPECTACULAR_SETTINGS if config.spectacular is provided
481
- if config.spectacular and "SPECTACULAR_SETTINGS" in settings:
482
- spectacular_extensions = config.spectacular.get_spectacular_settings()
483
- # Merge with existing settings (django-cfg extensions take priority)
472
+ # Always apply project name to Spectacular settings if they exist
473
+ if "SPECTACULAR_SETTINGS" in settings:
474
+ if config.spectacular:
475
+ # User provided explicit spectacular config
476
+ spectacular_extensions = config.spectacular.get_spectacular_settings(project_name=config.project_name)
484
477
  settings["SPECTACULAR_SETTINGS"].update(spectacular_extensions)
485
478
  logger.info("🔧 Extended SPECTACULAR_SETTINGS with django-cfg Spectacular config")
486
-
479
+ else:
480
+ # Auto-create minimal spectacular config to set project name
481
+ from django_cfg.models.drf import SpectacularConfig
482
+ auto_spectacular = SpectacularConfig()
483
+ spectacular_extensions = auto_spectacular.get_spectacular_settings(project_name=config.project_name)
484
+ settings["SPECTACULAR_SETTINGS"].update(spectacular_extensions)
485
+ logger.info(f"🚀 Auto-configured API title as '{config.project_name} API'")
486
+
487
487
  integrations.append("drf_spectacular_extended")
488
+
489
+ # Always apply django-cfg DRF settings (create REST_FRAMEWORK if needed)
490
+ if config.drf:
491
+ # User provided explicit DRF config
492
+ drf_extensions = config.drf.get_rest_framework_settings()
493
+ if "REST_FRAMEWORK" in settings:
494
+ settings["REST_FRAMEWORK"].update(drf_extensions)
495
+ else:
496
+ settings["REST_FRAMEWORK"] = drf_extensions
497
+ logger.info("🔧 Extended REST_FRAMEWORK settings with django-cfg DRF config")
498
+ else:
499
+ # Auto-create minimal DRF config to set default pagination
500
+ from django_cfg.models.drf import DRFConfig
501
+ auto_drf = DRFConfig()
502
+ drf_extensions = auto_drf.get_rest_framework_settings()
503
+
504
+ if "REST_FRAMEWORK" in settings:
505
+ # Only apply pagination and page_size, don't override other settings
506
+ pagination_settings = {
507
+ 'DEFAULT_PAGINATION_CLASS': drf_extensions['DEFAULT_PAGINATION_CLASS'],
508
+ 'PAGE_SIZE': drf_extensions['PAGE_SIZE'],
509
+ }
510
+ settings["REST_FRAMEWORK"].update(pagination_settings)
511
+ else:
512
+ # Create new REST_FRAMEWORK settings with our defaults
513
+ settings["REST_FRAMEWORK"] = drf_extensions
514
+
515
+ logger.info(f"🚀 Auto-configured default pagination: {drf_extensions['DEFAULT_PAGINATION_CLASS']}")
488
516
 
489
517
  except Exception as e:
490
518
  logger.warning(f"Could not apply DRF/Spectacular extensions from django-cfg: {e}")
@@ -0,0 +1,258 @@
1
+ """
2
+ Django CFG Default Pagination Classes
3
+
4
+ Provides enhanced pagination classes with better response format and schema support.
5
+ """
6
+
7
+ from rest_framework.pagination import PageNumberPagination
8
+ from rest_framework.response import Response
9
+ from typing import Dict, Any, Optional
10
+ from django.core.paginator import InvalidPage
11
+ from rest_framework.exceptions import NotFound
12
+
13
+
14
+ class DefaultPagination(PageNumberPagination):
15
+ """
16
+ Enhanced default pagination class for django-cfg projects.
17
+
18
+ Features:
19
+ - Configurable page size via query parameter
20
+ - Enhanced response format with detailed pagination info
21
+ - Better OpenAPI schema support
22
+ - Consistent error handling
23
+ """
24
+
25
+ # Page size configuration
26
+ page_size = 100
27
+ page_size_query_param = 'page_size'
28
+ max_page_size = 1000
29
+
30
+ # Page number configuration
31
+ page_query_param = 'page'
32
+
33
+ # Template for invalid page messages
34
+ invalid_page_message = 'Invalid page "{page_number}": {message}.'
35
+
36
+ def paginate_queryset(self, queryset, request, view=None):
37
+ """
38
+ Paginate a queryset if required, either returning a page object,
39
+ or `None` if pagination is not configured for this view.
40
+ """
41
+ try:
42
+ return super().paginate_queryset(queryset, request, view)
43
+ except InvalidPage as exc:
44
+ msg = self.invalid_page_message.format(
45
+ page_number=request.query_params.get(self.page_query_param, 1),
46
+ message=str(exc)
47
+ )
48
+ raise NotFound(msg)
49
+
50
+ def get_paginated_response(self, data):
51
+ """
52
+ Return a paginated style `Response` object with enhanced format.
53
+
54
+ Response format:
55
+ {
56
+ "count": 150, # Total number of items
57
+ "page": 2, # Current page number
58
+ "pages": 15, # Total number of pages
59
+ "page_size": 10, # Items per page
60
+ "has_next": true, # Whether there is a next page
61
+ "has_previous": true, # Whether there is a previous page
62
+ "next_page": 3, # Next page number (null if no next)
63
+ "previous_page": 1, # Previous page number (null if no previous)
64
+ "results": [...] # Actual data
65
+ }
66
+ """
67
+ return Response({
68
+ 'count': self.page.paginator.count,
69
+ 'page': self.page.number,
70
+ 'pages': self.page.paginator.num_pages,
71
+ 'page_size': self.page.paginator.per_page,
72
+ 'has_next': self.page.has_next(),
73
+ 'has_previous': self.page.has_previous(),
74
+ 'next_page': self.page.next_page_number() if self.page.has_next() else None,
75
+ 'previous_page': self.page.previous_page_number() if self.page.has_previous() else None,
76
+ 'results': data,
77
+ })
78
+
79
+ def get_paginated_response_schema(self, schema):
80
+ """
81
+ Return the OpenAPI schema for paginated responses.
82
+
83
+ This ensures proper API documentation generation.
84
+ """
85
+ return {
86
+ 'type': 'object',
87
+ 'required': ['count', 'page', 'pages', 'page_size', 'has_next', 'has_previous', 'results'],
88
+ 'properties': {
89
+ 'count': {
90
+ 'type': 'integer',
91
+ 'description': 'Total number of items across all pages',
92
+ 'example': 150
93
+ },
94
+ 'page': {
95
+ 'type': 'integer',
96
+ 'description': 'Current page number (1-based)',
97
+ 'example': 2
98
+ },
99
+ 'pages': {
100
+ 'type': 'integer',
101
+ 'description': 'Total number of pages',
102
+ 'example': 15
103
+ },
104
+ 'page_size': {
105
+ 'type': 'integer',
106
+ 'description': 'Number of items per page',
107
+ 'example': 10
108
+ },
109
+ 'has_next': {
110
+ 'type': 'boolean',
111
+ 'description': 'Whether there is a next page',
112
+ 'example': True
113
+ },
114
+ 'has_previous': {
115
+ 'type': 'boolean',
116
+ 'description': 'Whether there is a previous page',
117
+ 'example': True
118
+ },
119
+ 'next_page': {
120
+ 'type': 'integer',
121
+ 'nullable': True,
122
+ 'description': 'Next page number (null if no next page)',
123
+ 'example': 3
124
+ },
125
+ 'previous_page': {
126
+ 'type': 'integer',
127
+ 'nullable': True,
128
+ 'description': 'Previous page number (null if no previous page)',
129
+ 'example': 1
130
+ },
131
+ 'results': {
132
+ **schema,
133
+ 'description': 'Array of items for current page'
134
+ },
135
+ },
136
+ }
137
+
138
+ def get_html_context(self):
139
+ """
140
+ Return context for HTML template rendering (browsable API).
141
+ """
142
+ base_context = super().get_html_context()
143
+ base_context.update({
144
+ 'page_size': self.page.paginator.per_page,
145
+ 'total_pages': self.page.paginator.num_pages,
146
+ })
147
+ return base_context
148
+
149
+
150
+ class LargePagination(DefaultPagination):
151
+ """
152
+ Pagination class for large datasets.
153
+
154
+ Uses larger page sizes for better performance with big collections.
155
+ """
156
+ page_size = 500
157
+ max_page_size = 2000
158
+
159
+
160
+ class SmallPagination(DefaultPagination):
161
+ """
162
+ Pagination class for small datasets or detailed views.
163
+
164
+ Uses smaller page sizes for better user experience.
165
+ """
166
+ page_size = 20
167
+ max_page_size = 100
168
+
169
+
170
+ class NoPagination(PageNumberPagination):
171
+ """
172
+ Pagination class that effectively disables pagination.
173
+
174
+ Returns all results in a single page. Use with caution on large datasets.
175
+ """
176
+ page_size = None
177
+ page_size_query_param = None
178
+ max_page_size = None
179
+
180
+ def paginate_queryset(self, queryset, request, view=None):
181
+ """
182
+ Don't paginate the queryset, return None to indicate no pagination.
183
+ """
184
+ return None
185
+
186
+ def get_paginated_response(self, data):
187
+ """
188
+ Return a non-paginated response.
189
+ """
190
+ return Response(data)
191
+
192
+
193
+ class CursorPaginationEnhanced(PageNumberPagination):
194
+ """
195
+ Enhanced cursor-based pagination for large datasets.
196
+
197
+ Better performance for large datasets but doesn't support jumping to arbitrary pages.
198
+ """
199
+ page_size = 100
200
+ page_size_query_param = 'page_size'
201
+ max_page_size = 1000
202
+ cursor_query_param = 'cursor'
203
+ ordering = '-created_at' # Default ordering, should be overridden
204
+
205
+ def get_paginated_response(self, data):
206
+ """
207
+ Return cursor-paginated response with enhanced format.
208
+ """
209
+ return Response({
210
+ 'next': self.get_next_link(),
211
+ 'previous': self.get_previous_link(),
212
+ 'page_size': self.page_size,
213
+ 'results': data,
214
+ })
215
+
216
+ def get_paginated_response_schema(self, schema):
217
+ """
218
+ Return the OpenAPI schema for cursor-paginated responses.
219
+ """
220
+ return {
221
+ 'type': 'object',
222
+ 'required': ['results'],
223
+ 'properties': {
224
+ 'next': {
225
+ 'type': 'string',
226
+ 'nullable': True,
227
+ 'format': 'uri',
228
+ 'description': 'URL to next page of results',
229
+ 'example': 'http://api.example.org/accounts/?cursor=cD0yMDIzLTEyLTE1KzAyJTNBMDA%3D'
230
+ },
231
+ 'previous': {
232
+ 'type': 'string',
233
+ 'nullable': True,
234
+ 'format': 'uri',
235
+ 'description': 'URL to previous page of results',
236
+ 'example': 'http://api.example.org/accounts/?cursor=bD0yMDIzLTEyLTEzKzAyJTNBMDA%3D'
237
+ },
238
+ 'page_size': {
239
+ 'type': 'integer',
240
+ 'description': 'Number of items per page',
241
+ 'example': 100
242
+ },
243
+ 'results': {
244
+ **schema,
245
+ 'description': 'Array of items for current page'
246
+ },
247
+ },
248
+ }
249
+
250
+
251
+ # Export all pagination classes
252
+ __all__ = [
253
+ 'DefaultPagination',
254
+ 'LargePagination',
255
+ 'SmallPagination',
256
+ 'NoPagination',
257
+ 'CursorPaginationEnhanced',
258
+ ]
django_cfg/models/drf.py CHANGED
@@ -133,12 +133,15 @@ class SpectacularConfig(BaseModel):
133
133
  description="Enum name overrides"
134
134
  )
135
135
 
136
- def get_spectacular_settings(self) -> Dict[str, Any]:
136
+ def get_spectacular_settings(self, project_name: Optional[str] = None) -> Dict[str, Any]:
137
137
  """
138
138
  Get django-cfg Spectacular extensions.
139
139
 
140
140
  NOTE: This extends Revolution's base settings, not replaces them.
141
141
  Only include settings that are unique to django-cfg or critical fixes.
142
+
143
+ Args:
144
+ project_name: Project name from DjangoConfig to use as API title
142
145
  """
143
146
  settings = {
144
147
  # django-cfg specific UI enhancements
@@ -156,6 +159,16 @@ class SpectacularConfig(BaseModel):
156
159
  "ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
157
160
  }
158
161
 
162
+ # Use project_name as API title if provided and title is default
163
+ if project_name and self.title == "API Documentation":
164
+ settings["TITLE"] = f"{project_name} API"
165
+ elif self.title != "API Documentation":
166
+ settings["TITLE"] = self.title
167
+
168
+ # Always set description and version
169
+ settings["DESCRIPTION"] = self.description
170
+ settings["VERSION"] = self.version
171
+
159
172
  # Add optional fields if present
160
173
  if self.terms_of_service:
161
174
  settings["TERMS_OF_SERVICE"] = self.terms_of_service
@@ -205,10 +218,10 @@ class DRFConfig(BaseModel):
205
218
 
206
219
  # Pagination
207
220
  pagination_class: str = Field(
208
- default='rest_framework.pagination.PageNumberPagination',
221
+ default='django_cfg.middleware.pagination.DefaultPagination',
209
222
  description="Default pagination class"
210
223
  )
211
- page_size: int = Field(default=25, description="Default page size")
224
+ page_size: int = Field(default=100, description="Default page size")
212
225
 
213
226
  # Schema
214
227
  schema_class: str = Field(
@@ -41,6 +41,13 @@ CORE_REGISTRY = {
41
41
  "TaskConfig": ("django_cfg.models.tasks", "TaskConfig"),
42
42
  "DramatiqConfig": ("django_cfg.models.tasks", "DramatiqConfig"),
43
43
 
44
+ # Pagination classes
45
+ "DefaultPagination": ("django_cfg.middleware.pagination", "DefaultPagination"),
46
+ "LargePagination": ("django_cfg.middleware.pagination", "LargePagination"),
47
+ "SmallPagination": ("django_cfg.middleware.pagination", "SmallPagination"),
48
+ "NoPagination": ("django_cfg.middleware.pagination", "NoPagination"),
49
+ "CursorPaginationEnhanced": ("django_cfg.middleware.pagination", "CursorPaginationEnhanced"),
50
+
44
51
  # Utils
45
52
  "version_check": ("django_cfg.utils.version_check", "version_check"),
46
53
  "toolkit": ("django_cfg.utils.toolkit", "toolkit"),
@@ -0,0 +1,12 @@
1
+ {% extends "rest_framework/base.html" %}
2
+ {% load django_cfg %}
3
+
4
+ {% block title %}
5
+ {% if view.get_view_name %}{{ view.get_view_name }}{% else %}API{% endif %} - {% project_name %}
6
+ {% endblock %}
7
+
8
+ {% block branding %}
9
+ <a class='navbar-brand' rel="nofollow" href='{% url "api-root" %}'>
10
+ {% project_name %}
11
+ </a>
12
+ {% endblock %}
@@ -32,3 +32,19 @@ def lib_health_url():
32
32
  # Lazy import to avoid AppRegistryNotReady error
33
33
  from django_cfg.config import LIB_HEALTH_URL
34
34
  return LIB_HEALTH_URL
35
+
36
+
37
+ @register.simple_tag
38
+ def project_name():
39
+ """Get the project name from current config."""
40
+ # Lazy import to avoid AppRegistryNotReady error
41
+ from django_cfg.core.config import get_current_config
42
+ from django_cfg.config import LIB_NAME
43
+
44
+ # Try to get project name from current config
45
+ config = get_current_config()
46
+ if config and hasattr(config, 'project_name'):
47
+ return config.project_name
48
+
49
+ # Fallback to library name
50
+ return LIB_NAME