django-cfg 1.2.13__py3-none-any.whl → 1.2.15__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.13"
35
+ __version__ = "1.2.15"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
django_cfg/core/config.py CHANGED
@@ -20,6 +20,7 @@ from django_cfg import (
20
20
  DatabaseConfig, CacheConfig, EmailConfig, TelegramConfig,
21
21
  UnfoldConfig, DRFConfig, SpectacularConfig, LimitsConfig
22
22
  )
23
+ from django_cfg.models.tasks import TaskConfig
23
24
 
24
25
  # Default apps
25
26
  DEFAULT_APPS = [
@@ -244,6 +245,12 @@ class DjangoConfig(BaseModel):
244
245
  description="Unfold admin interface configuration",
245
246
  )
246
247
 
248
+ # === Background Task Processing ===
249
+ tasks: Optional[TaskConfig] = Field(
250
+ default=None,
251
+ description="Background task processing configuration (Dramatiq)",
252
+ )
253
+
247
254
  # === API Configuration ===
248
255
  # Note: DRF base configuration is handled by django-revolution
249
256
  # These fields provide additional/extended settings on top of Revolution
@@ -353,6 +360,7 @@ class DjangoConfig(BaseModel):
353
360
  if "default" not in self.databases:
354
361
  raise ConfigurationError("'default' database is required", context={"available_databases": list(self.databases.keys())}, suggestions=["Add a database with alias 'default'"])
355
362
 
363
+
356
364
  # Validate database routing consistency - check migrate_to references
357
365
  referenced_databases = set()
358
366
  for alias, db_config in self.databases.items():
@@ -418,11 +418,17 @@ class SettingsGenerator:
418
418
 
419
419
  # Check for Tasks/Dramatiq configuration
420
420
  try:
421
+ from django_cfg.models.tasks import TaskConfig
421
422
  from django_cfg.modules.django_tasks import generate_dramatiq_settings_from_config
422
- dramatiq_settings = generate_dramatiq_settings_from_config(config)
423
- if dramatiq_settings:
424
- settings.update(dramatiq_settings)
425
- integrations.append("dramatiq")
423
+
424
+ # Auto-initialize TaskConfig if needed and generate settings
425
+ task_config = TaskConfig.auto_initialize_if_needed()
426
+ if task_config is not None:
427
+ dramatiq_settings = generate_dramatiq_settings_from_config()
428
+ if dramatiq_settings:
429
+ settings.update(dramatiq_settings)
430
+ integrations.append("dramatiq")
431
+
426
432
  except ImportError as e:
427
433
  logger.warning(f"Failed to import django_tasks module: {e}")
428
434
  except Exception as e:
@@ -467,24 +473,52 @@ class SettingsGenerator:
467
473
  except Exception as e:
468
474
  logger.warning(f"Could not generate DRF config from Revolution: {e}")
469
475
 
470
- # Apply django-cfg DRF/Spectacular extensions (only if explicitly configured by user)
476
+ # Apply django-cfg DRF/Spectacular extensions
471
477
  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)
478
+ # Always apply project name to Spectacular settings if they exist
479
+ if "SPECTACULAR_SETTINGS" in settings:
480
+ if config.spectacular:
481
+ # User provided explicit spectacular config
482
+ spectacular_extensions = config.spectacular.get_spectacular_settings(project_name=config.project_name)
484
483
  settings["SPECTACULAR_SETTINGS"].update(spectacular_extensions)
485
484
  logger.info("🔧 Extended SPECTACULAR_SETTINGS with django-cfg Spectacular config")
486
-
485
+ else:
486
+ # Auto-create minimal spectacular config to set project name
487
+ from django_cfg.models.drf import SpectacularConfig
488
+ auto_spectacular = SpectacularConfig()
489
+ spectacular_extensions = auto_spectacular.get_spectacular_settings(project_name=config.project_name)
490
+ settings["SPECTACULAR_SETTINGS"].update(spectacular_extensions)
491
+ logger.info(f"🚀 Auto-configured API title as '{config.project_name} API'")
492
+
487
493
  integrations.append("drf_spectacular_extended")
494
+
495
+ # Always apply django-cfg DRF settings (create REST_FRAMEWORK if needed)
496
+ if config.drf:
497
+ # User provided explicit DRF config
498
+ drf_extensions = config.drf.get_rest_framework_settings()
499
+ if "REST_FRAMEWORK" in settings:
500
+ settings["REST_FRAMEWORK"].update(drf_extensions)
501
+ else:
502
+ settings["REST_FRAMEWORK"] = drf_extensions
503
+ logger.info("🔧 Extended REST_FRAMEWORK settings with django-cfg DRF config")
504
+ else:
505
+ # Auto-create minimal DRF config to set default pagination
506
+ from django_cfg.models.drf import DRFConfig
507
+ auto_drf = DRFConfig()
508
+ drf_extensions = auto_drf.get_rest_framework_settings()
509
+
510
+ if "REST_FRAMEWORK" in settings:
511
+ # Only apply pagination and page_size, don't override other settings
512
+ pagination_settings = {
513
+ 'DEFAULT_PAGINATION_CLASS': drf_extensions['DEFAULT_PAGINATION_CLASS'],
514
+ 'PAGE_SIZE': drf_extensions['PAGE_SIZE'],
515
+ }
516
+ settings["REST_FRAMEWORK"].update(pagination_settings)
517
+ else:
518
+ # Create new REST_FRAMEWORK settings with our defaults
519
+ settings["REST_FRAMEWORK"] = drf_extensions
520
+
521
+ logger.info(f"🚀 Auto-configured default pagination: {drf_extensions['DEFAULT_PAGINATION_CLASS']}")
488
522
 
489
523
  except Exception as e:
490
524
  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(
@@ -11,6 +11,7 @@ from typing import Optional, List, Literal, Dict, Any
11
11
  from enum import Enum
12
12
  import os
13
13
  import logging
14
+ from django_cfg.models.cfg import BaseCfgAutoModule
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -239,7 +240,7 @@ class WorkerConfig(BaseModel):
239
240
  )
240
241
 
241
242
 
242
- class TaskConfig(BaseModel):
243
+ class TaskConfig(BaseModel, BaseCfgAutoModule):
243
244
  """
244
245
  High-level task system configuration.
245
246
 
@@ -257,6 +258,12 @@ class TaskConfig(BaseModel):
257
258
  description="Task processing backend"
258
259
  )
259
260
 
261
+ def __init__(self, **data):
262
+ """Initialize TaskConfig with BaseCfgAutoModule support."""
263
+ super().__init__(**data)
264
+ # Initialize _config attribute for BaseCfgAutoModule
265
+ self._config = None
266
+
260
267
  # === Backend-Specific Configuration ===
261
268
  dramatiq: DramatiqConfig = Field(
262
269
  default_factory=DramatiqConfig,
@@ -371,6 +378,53 @@ class TaskConfig(BaseModel):
371
378
  "DRAMATIQ_QUEUES": self.dramatiq.queues,
372
379
  }
373
380
 
381
+ def get_smart_defaults(self):
382
+ """Get smart default configuration for this module."""
383
+ config = self.get_config()
384
+ debug = getattr(config, 'debug', False) if config else False
385
+ return get_default_task_config(debug=debug)
386
+
387
+ def get_module_config(self):
388
+ """Get the final configuration for this module."""
389
+ return self
390
+
391
+ @classmethod
392
+ def auto_initialize_if_needed(cls) -> Optional['TaskConfig']:
393
+ """
394
+ Auto-initialize TaskConfig if needed based on config flags.
395
+
396
+ Returns:
397
+ TaskConfig instance if should be initialized, None otherwise
398
+ """
399
+ # Get config through BaseCfgModule
400
+ from django_cfg.modules import BaseCfgModule
401
+ base_module = BaseCfgModule()
402
+ config = base_module.get_config()
403
+
404
+ if not config:
405
+ return None
406
+
407
+ # Check if TaskConfig already exists
408
+ if hasattr(config, 'tasks') and config.tasks is not None:
409
+ # Set config reference and return existing
410
+ config.tasks.set_config(config)
411
+ return config.tasks
412
+
413
+ # Check if tasks should be enabled
414
+ if base_module.is_tasks_enabled():
415
+ # Auto-initialize with smart defaults
416
+ task_config = cls().get_smart_defaults()
417
+ task_config.set_config(config)
418
+ config.tasks = task_config
419
+
420
+ import logging
421
+ logger = logging.getLogger(__name__)
422
+ logger.info("🚀 Auto-initialized TaskConfig (enabled by knowbase/agents/tasks flags)")
423
+
424
+ return task_config
425
+
426
+ return None
427
+
374
428
 
375
429
  # === Utility Functions ===
376
430
 
@@ -9,16 +9,14 @@ from typing import Optional, Dict, Any, List
9
9
  import logging
10
10
  from urllib.parse import urlparse
11
11
 
12
- from django_cfg.modules.base import BaseCfgModule
12
+ from . import BaseCfgModule
13
13
  from django_cfg.models.tasks import TaskConfig, validate_task_config
14
14
  from django_cfg.models.constance import ConstanceField
15
15
 
16
16
  # Django imports (will be available when Django is configured)
17
17
  try:
18
- from django.conf import settings
19
18
  from django.apps import apps
20
19
  except ImportError:
21
- settings = None
22
20
  apps = None
23
21
 
24
22
  # Optional imports
@@ -426,6 +424,46 @@ def extend_constance_config_with_tasks():
426
424
 
427
425
  # === Exports ===
428
426
 
427
+ def generate_dramatiq_settings_from_config() -> Optional[Dict[str, Any]]:
428
+ """
429
+ Generate Dramatiq settings from auto-discovered DjangoConfig.
430
+
431
+ Returns:
432
+ Dictionary of Dramatiq settings or None if tasks disabled
433
+ """
434
+ try:
435
+ # Get config through BaseCfgModule
436
+ base_module = BaseCfgModule()
437
+ config = base_module.get_config()
438
+
439
+ if not config or not hasattr(config, 'tasks') or not config.tasks or not config.tasks.enabled:
440
+ return None
441
+
442
+ # Get Redis URL from cache config or environment
443
+ redis_url = None
444
+ if hasattr(config, 'cache_default') and config.cache_default:
445
+ redis_url = getattr(config.cache_default, 'redis_url', None)
446
+
447
+ if not redis_url:
448
+ # Fallback to environment or default
449
+ import os
450
+ redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379/1')
451
+
452
+ # Generate Dramatiq settings
453
+ dramatiq_settings = config.tasks.get_dramatiq_settings(redis_url)
454
+
455
+ # Ensure we only use Redis broker (no RabbitMQ)
456
+ if 'DRAMATIQ_BROKER' in dramatiq_settings:
457
+ dramatiq_settings['DRAMATIQ_BROKER']['BROKER'] = 'dramatiq.brokers.redis.RedisBroker'
458
+
459
+ logger.info(f"✅ Generated Dramatiq settings with Redis broker and {len(config.tasks.dramatiq.queues)} queues")
460
+ return dramatiq_settings
461
+
462
+ except Exception as e:
463
+ logger.error(f"Failed to generate Dramatiq settings: {e}")
464
+ return None
465
+
466
+
429
467
  __all__ = [
430
468
  "DjangoTasks",
431
469
  "get_task_service",
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.2.13
3
+ Version: 1.2.15
4
4
  Summary: 🚀 Next-gen Django configuration: type-safety, AI features, blazing-fast setup, and automated best practices — all in one.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://docs.djangocfg.com
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=8MAPTQI-gX0VWLmlVC8g3gzKFKuiNOVNorWi8pgIPr0,1631
2
+ django_cfg/__init__.py,sha256=WO5eQC0F4xNgcT1pvxC4u4-1_fGdLq1oolFLlGmuRKE,1631
3
3
  django_cfg/apps.py,sha256=k84brkeXJI7EgKZLEpTkM9YFZofKI4PzhFOn1cl9Msc,1656
4
4
  django_cfg/config.py,sha256=0cuRJVEnf03WzvEqhwzLvn9Zi1805C5KG1yk27ekABA,1190
5
5
  django_cfg/urls.py,sha256=bpRFjMonQuk4UCUMxx4ueBX3YDNB7HXKFwEghQ3KR3o,793
@@ -285,10 +285,10 @@ django_cfg/cli/commands/__init__.py,sha256=EKLXDAx-QttnGmdjsmVANAfhxWplxl2V_2I0S
285
285
  django_cfg/cli/commands/create_project.py,sha256=iuf965j8Yg7zxHcPb0GtFHEj73CYXC45ZJRmd6RbA9E,21025
286
286
  django_cfg/cli/commands/info.py,sha256=o4S1xPJSHv2oEVqmH0X9RTF5f-8Wy9579yHkyd_PC3E,4923
287
287
  django_cfg/core/__init__.py,sha256=eVK57qFOok9kTeHoNEMQ1BplkUOaQ7NB9kP9eQK1vg0,358
288
- django_cfg/core/config.py,sha256=XZmW4tqHYi8zUOywpJ_8DqeFUzwwflMshZnwrpddjtw,26302
288
+ django_cfg/core/config.py,sha256=y0rvqiO5l_MMqDhQCiyA9c3GHYJlJwRpdOgvN_MfcWU,26536
289
289
  django_cfg/core/environment.py,sha256=MAoEPqIPsLVhSANT2Bz4nnus2wmbMW0RCOQxhQfDrDc,9106
290
290
  django_cfg/core/exceptions.py,sha256=RTQEoU3PfR8lqqNNv5ayd_HY2yJLs3eioqUy8VM6AG4,10378
291
- django_cfg/core/generation.py,sha256=2lovChgsZXE1q3ut23Ji0VYpm79wOflrK-rCsKM7ulc,23051
291
+ django_cfg/core/generation.py,sha256=zSVb3VIv588wzqSF1-wxOb15CW5hX2tf9fq_FRXSOzk,24990
292
292
  django_cfg/core/integration.py,sha256=5kzkZSd45ij0rfrBdeNUYnDalObqvK__XpoP31xFYKo,5224
293
293
  django_cfg/core/validation.py,sha256=PhwDBTs24nuM3xqfIT3hXmN88VrhMT-Bk8Yb8FxjmPk,6590
294
294
  django_cfg/management/__init__.py,sha256=NrLAhiS59hqjy-bipOC1abNuRiNm5BpKXmjN05VzKbM,28
@@ -316,6 +316,7 @@ django_cfg/management/commands/tree.py,sha256=rBeDOouqntFZjZGI5uYLOCpInJgSzb9Fz7
316
316
  django_cfg/management/commands/validate_config.py,sha256=2V8M6ZCOfrXA-tZ8WoXRxgnb6gDE0xXymAbBzUJ4gic,6940
317
317
  django_cfg/middleware/README.md,sha256=7XukH2HREqEnNJ9KDXhAcSw6lfpZ3gvKKAqGYvSivo4,3886
318
318
  django_cfg/middleware/__init__.py,sha256=IdcooCQLd8rNt-_8PXa2qXLMBnhsvkfdfuhI9CmgABA,195
319
+ django_cfg/middleware/pagination.py,sha256=RfLka1DbmLD-zznSAqayGD08x95ikcnkzp9LT5Jj5Bs,8655
319
320
  django_cfg/middleware/public_endpoints.py,sha256=EHJKzlYwakeU3hSPBJ53FZn8Ub-jwENCVSfFDJt3kAk,6915
320
321
  django_cfg/middleware/user_activity.py,sha256=7XVSnnIVfz-Hucx-YWRODcXd3qDH8Y8E__-6zYWLxqI,6153
321
322
  django_cfg/models/__init__.py,sha256=du24ZdqKdvc3VNXjgufEz_6B6gxdHtlqVHG4PJWaOQM,419
@@ -326,7 +327,7 @@ django_cfg/models/cfg.py,sha256=P6YowmE-VOqp_L25Ijxj2hjjNhB9xtlm8G35DHWqTfk,2702
326
327
  django_cfg/models/constance.py,sha256=BqwoPvUx5vfrZT6qF2KR0prpsSM5VEYIYJUVlB50ewk,8936
327
328
  django_cfg/models/cors.py,sha256=lozVoQJVehhNL1qzOhvAkd4ZpgwAdOJ-OUX7wkJp5W4,3567
328
329
  django_cfg/models/database.py,sha256=LpcmEljjr-2pnR8UVSziUNSnucsojfh8PcgayhrWqK4,16875
329
- django_cfg/models/drf.py,sha256=ZzxYv1KjA-iYLRGZj5HUcIyO9tg2B0Gudp1aZz9QNVg,9741
330
+ django_cfg/models/drf.py,sha256=o7p1dijT9NbQIHDp_WbWJsi1Kl5n2WS0wr5iWYuxNF0,10324
330
331
  django_cfg/models/email.py,sha256=y97W9I-Mm3TyWWLCLRh_2GO0u2PBVT2olMInqOyBiq8,4843
331
332
  django_cfg/models/environment.py,sha256=jM_KPXrxQHTFOhbaJTkkZL0Yml8bAM2Q7DJ69QsedX8,9438
332
333
  django_cfg/models/jwt.py,sha256=3R_dpLmVZIcH4zdtwA4qKnuCB8RZQACrgsbbgWY2q4c,9025
@@ -336,14 +337,14 @@ django_cfg/models/ngrok.py,sha256=MVgcKWx0DRSW0QcwhiSx2vVwTSG49vbVrzPkZqDK-zw,35
336
337
  django_cfg/models/revolution.py,sha256=gaAhdicd1ji-XiJPqUFQaUmDcgfwFHofSYS3JtLcz4A,7540
337
338
  django_cfg/models/security.py,sha256=Xv19ZVOIenB_-f0wB6fm-Ap4j9kA43bSFaT2XenpSqc,4685
338
339
  django_cfg/models/services.py,sha256=fj9JjrJFrlL4DMnMbx_D8JiiZpz4E5uBqmhquAxau7c,13159
339
- django_cfg/models/tasks.py,sha256=QdYtbwxcQgJ1Qh0tPydSRW_OrflOfm2AzvvBb7CKtbQ,13556
340
+ django_cfg/models/tasks.py,sha256=G06kEMaJ9mZhW525y-r33q4rHswAwnvPgMt7lpbgiak,15526
340
341
  django_cfg/modules/__init__.py,sha256=Ip9WMpzImEwIAywpFwU056_v0O9oIGG7nCT1YSArxkw,316
341
342
  django_cfg/modules/base.py,sha256=ALlsylzO1rYsrLNwL3MhEK_dqYSZV0BBZAObR6wlGEA,5343
342
343
  django_cfg/modules/django_email.py,sha256=2XXlIKzD6Jao3CT4_zIE2eaM9Cc9ROA1tjp2bJ9z5Lo,16592
343
344
  django_cfg/modules/django_health.py,sha256=7QzuQ6WyjWYj6lecd4auwRvEyrMUL7N6hiAp-tLyoY4,8923
344
345
  django_cfg/modules/django_logger.py,sha256=3oP9jev0lOcFUJ1tYcpbFnK524zIGA2xIOrrAiTwpb8,6331
345
346
  django_cfg/modules/django_ngrok.py,sha256=LjlAIJprbFhFYwDG93acas8XaB6iFQGu4fWvZiDNhyQ,10482
346
- django_cfg/modules/django_tasks.py,sha256=Ij8AR5RswTJ7oxWC7L3BN9WAO_JaZOtdFQoLzrSJLfA,15862
347
+ django_cfg/modules/django_tasks.py,sha256=SBas2juL6RfDL7ttm4xAxJ7bvS052WmUzixNHPlzJVo,17328
347
348
  django_cfg/modules/django_telegram.py,sha256=qz3EOJnCR_YTJVIu2-Iq8--cla6mU-WPohquQ26eHDQ,16367
348
349
  django_cfg/modules/dramatiq_setup.py,sha256=Jke4aO_L1t6F3OAc4pl12zppKzL0gb1p6ilfQ3zUIZ8,454
349
350
  django_cfg/modules/logger.py,sha256=4_zeasNehr8LGz8r_ckv15-fQS63zCodiqD4CYIEyFI,10546
@@ -406,7 +407,7 @@ django_cfg/modules/django_unfold/models/dropdown.py,sha256=bbRYl0iuzBrwgmqUMTDw9
406
407
  django_cfg/modules/django_unfold/models/navigation.py,sha256=xrP2szoe1MSiTVAfc0Hd2dS32HhCp8XHBJinF5yc_U8,2575
407
408
  django_cfg/modules/django_unfold/models/tabs.py,sha256=AHV7lmaRTjOluf9mgZvXISG5D7Re1QPdU2v-iYoCaIQ,848
408
409
  django_cfg/registry/__init__.py,sha256=q6z9hGcBdtzbVLeDeQtP-A31z-1V7myZqjlZyTs1uo0,540
409
- django_cfg/registry/core.py,sha256=lKKcqe4iDqgd42iwL8KNl6kyjtgQ6KWReJMzJ3OqfZo,2257
410
+ django_cfg/registry/core.py,sha256=FGe86oEGm66fv9ypAEIFXP_XkFog1EaE5vYggALgQ_4,2703
410
411
  django_cfg/registry/exceptions.py,sha256=b4pIakeRxhT1-ST3lbAnmAQaASRBARCoah_w6vb3VF0,399
411
412
  django_cfg/registry/modules.py,sha256=e8lCG5C-yhRgvs8El_-6Xbdrte-enZF_Xb3B4opO-Hs,1135
412
413
  django_cfg/registry/services.py,sha256=wpGSgSCUr4e9pUh5-rwN1AyPLGazK6KD-Ky4vz-elIw,1219
@@ -455,15 +456,16 @@ django_cfg/templates/admin/snippets/tabs/stats_tab.html,sha256=jIqSnp4WTo2jAzzMq
455
456
  django_cfg/templates/admin/snippets/tabs/users_tab.html,sha256=04D05VSeidn2qJnQmktVI7TNlm7tNcXYXJAsk32U5hY,2813
456
457
  django_cfg/templates/admin/snippets/zones/zones_table.html,sha256=Hg4hT61yX0um00j4HkbkANOt0ehKwYIMT-r2IzTOpVo,12358
457
458
  django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
459
+ django_cfg/templates/rest_framework/api.html,sha256=M5t75SPfZOgHDmLBNVXxyZrmCWgJKv45Ybxw8Iempj0,343
458
460
  django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
459
- django_cfg/templatetags/django_cfg.py,sha256=BpP4Q8zguNckZomtZBqdIBnQFO2gWtaTbeqfMgo--6E,828
461
+ django_cfg/templatetags/django_cfg.py,sha256=11_3YX0jAVuOd_fVcDixAxyDMG4V7A98SPqv8Lbfpxc,1308
460
462
  django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
461
463
  django_cfg/utils/path_resolution.py,sha256=C9As6p4Q9l3VeoVkFDRPQWGrzAWf8O8UxLVkaI3ToVM,13899
462
464
  django_cfg/utils/smart_defaults.py,sha256=b6A1z7VO1NJGq0oUQXN5P97c3k_Ssgw6qUi0mK-4TlM,19786
463
465
  django_cfg/utils/toolkit.py,sha256=Td8_iXNaftonF_xdZP4Y3uO65nuA_4_zditn5Q_Pfcw,23310
464
466
  django_cfg/utils/version_check.py,sha256=jI4v3YMdQriUEeb_TvRl511sDghy6I75iKRDUaNpucs,4800
465
- django_cfg-1.2.13.dist-info/METADATA,sha256=hA_EqJ1AfYhL-cffQJuaZ0KmF-6IewvuELFL1ErW9Xo,36406
466
- django_cfg-1.2.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
467
- django_cfg-1.2.13.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
468
- django_cfg-1.2.13.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
469
- django_cfg-1.2.13.dist-info/RECORD,,
467
+ django_cfg-1.2.15.dist-info/METADATA,sha256=QcWw3jNJ4GU7JwFC9Ya3UdOT-9UFo2Au_7XeCUT3KLE,36406
468
+ django_cfg-1.2.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
469
+ django_cfg-1.2.15.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
470
+ django_cfg-1.2.15.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
471
+ django_cfg-1.2.15.dist-info/RECORD,,