django-cfg 1.4.105__py3-none-any.whl → 1.4.107__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.

Potentially problematic release.


This version of django-cfg might be problematic. Click here for more details.

Files changed (24) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/core/generation/core_generators/templates.py +38 -23
  3. django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md +72 -0
  4. django_cfg/modules/django_admin/RESOURCE_CONFIG_ENHANCEMENT.md +350 -0
  5. django_cfg/modules/django_admin/__init__.py +4 -0
  6. django_cfg/modules/django_admin/base/pydantic_admin.py +126 -7
  7. django_cfg/modules/django_admin/config/__init__.py +4 -0
  8. django_cfg/modules/django_admin/config/admin_config.py +13 -1
  9. django_cfg/modules/django_admin/config/background_task_config.py +76 -0
  10. django_cfg/modules/django_admin/config/resource_config.py +129 -0
  11. django_cfg/modules/django_import_export/README.md +19 -1
  12. django_cfg/modules/django_import_export/__init__.py +8 -0
  13. django_cfg/modules/django_import_export/apps.py +16 -0
  14. django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export.html +6 -0
  15. django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export_item.html +7 -0
  16. django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import.html +6 -0
  17. django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_export.html +39 -0
  18. django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_item.html +7 -0
  19. django_cfg/pyproject.toml +2 -2
  20. {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/METADATA +3 -1
  21. {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/RECORD +24 -14
  22. {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/WHEEL +0 -0
  23. {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/entry_points.txt +0 -0
  24. {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/licenses/LICENSE +0 -0
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.4.105"
35
+ __version__ = "1.4.107"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -96,42 +96,57 @@ class TemplateSettingsGenerator:
96
96
 
97
97
  def _discover_app_templates(self) -> List[Path]:
98
98
  """
99
- Auto-discover template directories from django-cfg apps.
99
+ Auto-discover template directories from django-cfg apps and modules.
100
100
 
101
101
  Looks for:
102
102
  - app/templates/
103
103
  - app/admin_interface/templates/
104
104
  - app/frontend/templates/
105
+ - modules/*/templates/
105
106
 
106
107
  Returns:
107
108
  List of discovered template directory paths
108
109
  """
109
110
  app_templates = []
110
111
 
111
- # Find django-cfg apps directory
112
+ # Find django-cfg directory
112
113
  django_cfg_dir = Path(__file__).parent.parent.parent.parent
113
- apps_dir = django_cfg_dir / 'apps'
114
-
115
- if not apps_dir.exists():
116
- return app_templates
117
-
118
- # Scan each app directory
119
- for app_dir in apps_dir.iterdir():
120
- if not app_dir.is_dir():
121
- continue
122
-
123
- # Skip special directories
124
- if app_dir.name.startswith(('@', '_', '.')):
125
- continue
126
114
 
127
- # Look for common template directory patterns
128
- possible_template_dirs = [
129
- app_dir / 'templates',
130
- app_dir / 'admin_interface' / 'templates',
131
- app_dir / 'frontend' / 'templates',
132
- ]
133
-
134
- for template_dir in possible_template_dirs:
115
+ # Scan apps directory
116
+ apps_dir = django_cfg_dir / 'apps'
117
+ if apps_dir.exists():
118
+ for app_dir in apps_dir.iterdir():
119
+ if not app_dir.is_dir():
120
+ continue
121
+
122
+ # Skip special directories
123
+ if app_dir.name.startswith(('@', '_', '.')):
124
+ continue
125
+
126
+ # Look for common template directory patterns
127
+ possible_template_dirs = [
128
+ app_dir / 'templates',
129
+ app_dir / 'admin_interface' / 'templates',
130
+ app_dir / 'frontend' / 'templates',
131
+ ]
132
+
133
+ for template_dir in possible_template_dirs:
134
+ if template_dir.exists() and template_dir.is_dir():
135
+ app_templates.append(template_dir)
136
+
137
+ # Scan modules directory for templates
138
+ modules_dir = django_cfg_dir / 'modules'
139
+ if modules_dir.exists():
140
+ for module_dir in modules_dir.iterdir():
141
+ if not module_dir.is_dir():
142
+ continue
143
+
144
+ # Skip special directories
145
+ if module_dir.name.startswith(('@', '_', '.')):
146
+ continue
147
+
148
+ # Check if module has templates directory
149
+ template_dir = module_dir / 'templates'
135
150
  if template_dir.exists() and template_dir.is_dir():
136
151
  app_templates.append(template_dir)
137
152
 
@@ -0,0 +1,72 @@
1
+ # Import/Export Button Fix
2
+
3
+ ## Problem
4
+
5
+ Import/Export buttons in `PydanticAdmin` were displaying incorrectly because:
6
+
7
+ 1. `PydanticAdmin` was using the original `ImportExportModelAdmin` from django-import-export
8
+ 2. Original django-import-export templates don't integrate properly with Unfold UI
9
+ 3. Buttons were styled as tabs instead of toolbar buttons
10
+
11
+ ## Solution
12
+
13
+ Updated `PydanticAdmin` base class to use django_cfg's custom `ImportExportMixin`:
14
+
15
+ ### Before
16
+ ```python
17
+ class UnfoldImportExportModelAdmin(ImportExportModelAdmin, UnfoldModelAdmin):
18
+ pass
19
+ ```
20
+
21
+ ### After
22
+ ```python
23
+ class UnfoldImportExportModelAdmin(UnfoldModelAdmin, ImportExportMixin):
24
+ pass
25
+ ```
26
+
27
+ ## Changes Made
28
+
29
+ 1. **MRO (Method Resolution Order)**:
30
+ - `UnfoldModelAdmin` comes first (for template priority)
31
+ - `ImportExportMixin` comes second (adds import/export functionality)
32
+
33
+ 2. **Custom Templates** (`django_cfg.modules.django_import_export`):
34
+ - Custom `change_list_*.html` templates that extend Unfold's base
35
+ - Properly styled import/export buttons as round icons
36
+ - Import button: green with upload icon
37
+ - Export button: blue with download icon
38
+
39
+ 3. **Integration**:
40
+ - Uses Unfold's forms (`ImportForm`, `ExportForm`)
41
+ - Buttons appear in `object-tools-items` block (next to "Add" button)
42
+ - Full dark mode support
43
+
44
+ ## Result
45
+
46
+ Now `PydanticAdmin` admins with `import_export_enabled=True` will have:
47
+ - ✅ Properly styled import/export buttons
48
+ - ✅ Consistent Unfold UI
49
+ - ✅ Round icon buttons matching the design system
50
+ - ✅ Correct positioning in the toolbar
51
+
52
+ ## Usage
53
+
54
+ No changes required in user code - this fix is automatic:
55
+
56
+ ```python
57
+ from django_cfg.modules.django_admin import AdminConfig
58
+ from django_cfg.modules.django_admin.base import PydanticAdmin
59
+
60
+ config = AdminConfig(
61
+ model=MyModel,
62
+ import_export_enabled=True,
63
+ resource_class=MyModelResource,
64
+ list_display=["name", "status"],
65
+ )
66
+
67
+ @admin.register(MyModel)
68
+ class MyModelAdmin(PydanticAdmin):
69
+ config = config
70
+ ```
71
+
72
+ The import/export buttons will now display correctly!
@@ -0,0 +1,350 @@
1
+ # ResourceConfig & BackgroundTaskConfig Enhancement
2
+
3
+ ## Summary
4
+
5
+ Enhanced django-admin module with declarative import/export configuration and background task support.
6
+
7
+ ## New Features
8
+
9
+ ### 1. ResourceConfig
10
+
11
+ Declarative configuration for django-import-export Resource classes.
12
+
13
+ **Location:** `django_cfg/modules/django_admin/config/resource_config.py`
14
+
15
+ **Features:**
16
+ - Field selection and exclusion
17
+ - Import ID fields configuration
18
+ - Import/export behavior control
19
+ - Hook support (before/after import, per-row hooks)
20
+ - Export field ordering
21
+ - Batch processing options
22
+
23
+ **Example:**
24
+ ```python
25
+ from django_cfg.modules.django_admin import ResourceConfig
26
+
27
+ resource_config = ResourceConfig(
28
+ fields=['host', 'port', 'username', 'password'],
29
+ exclude=['metadata', 'config'],
30
+ import_id_fields=['host', 'port'],
31
+ skip_unchanged=True,
32
+ after_import_row='apps.myapp.tasks.test_after_import',
33
+ batch_size=100,
34
+ )
35
+ ```
36
+
37
+ ### 2. BackgroundTaskConfig
38
+
39
+ Configuration for background task processing.
40
+
41
+ **Location:** `django_cfg/modules/django_admin/config/background_task_config.py`
42
+
43
+ **Features:**
44
+ - Task runner selection (rearq, celery, django_q, sync)
45
+ - Batch size configuration
46
+ - Timeout settings
47
+ - Retry policy
48
+ - Priority levels
49
+
50
+ **Example:**
51
+ ```python
52
+ from django_cfg.modules.django_admin import BackgroundTaskConfig
53
+
54
+ background_config = BackgroundTaskConfig(
55
+ enabled=True,
56
+ task_runner='rearq',
57
+ batch_size=50,
58
+ timeout=300,
59
+ retry_on_failure=True,
60
+ max_retries=3,
61
+ )
62
+ ```
63
+
64
+ ### 3. Enhanced AdminConfig
65
+
66
+ **Updated:** `django_cfg/modules/django_admin/config/admin_config.py`
67
+
68
+ **New Fields:**
69
+ ```python
70
+ class AdminConfig(BaseModel):
71
+ # ... existing fields ...
72
+
73
+ # Import/Export enhancement
74
+ resource_config: Optional[ResourceConfig] = None
75
+
76
+ # Background task processing
77
+ background_task_config: Optional[BackgroundTaskConfig] = None
78
+ ```
79
+
80
+ ### 4. Enhanced PydanticAdmin
81
+
82
+ **Updated:** `django_cfg/modules/django_admin/base/pydantic_admin.py`
83
+
84
+ **Key Changes:**
85
+ - `_generate_resource_class()` now uses ResourceConfig if provided
86
+ - Auto-generates Resource with hooks support
87
+ - Dynamically attaches hook methods to Resource class
88
+
89
+ **Hook Support:**
90
+ - `before_import` - Called before import starts
91
+ - `after_import` - Called after import completes
92
+ - `before_import_row` - Called before each row import
93
+ - `after_import_row` - Called after each row import (★ most useful)
94
+
95
+ ## Usage Example
96
+
97
+ ### Complete Proxy Admin with Import/Export
98
+
99
+ ```python
100
+ from django_cfg.modules.django_admin import (
101
+ ActionConfig,
102
+ AdminConfig,
103
+ BackgroundTaskConfig,
104
+ ResourceConfig,
105
+ )
106
+
107
+ proxy_config = AdminConfig(
108
+ model=Proxy,
109
+
110
+ # Enable import/export with ResourceConfig
111
+ import_export_enabled=True,
112
+ resource_config=ResourceConfig(
113
+ fields=[
114
+ 'host', 'port', 'proxy_type', 'proxy_mode',
115
+ 'username', 'password',
116
+ 'provider', 'country',
117
+ ],
118
+ exclude=['metadata', 'config', 'last_error'],
119
+ import_id_fields=['host', 'port', 'provider'],
120
+ skip_unchanged=True,
121
+ # Auto-test after import
122
+ after_import_row='apps.proxies.tasks.after_import_row_test_proxy',
123
+ ),
124
+
125
+ # Background task configuration
126
+ background_task_config=BackgroundTaskConfig(
127
+ enabled=True,
128
+ task_runner='rearq',
129
+ batch_size=50,
130
+ timeout=300,
131
+ ),
132
+
133
+ # Admin actions
134
+ actions=[
135
+ ActionConfig(
136
+ name='test_selected_proxies',
137
+ description='Test selected proxies',
138
+ variant='warning',
139
+ icon='speed',
140
+ handler='apps.proxies.admin.actions.test_selected_proxies',
141
+ ),
142
+ ],
143
+
144
+ list_display=['host', 'port', 'status', 'success_rate'],
145
+ )
146
+
147
+ @admin.register(Proxy)
148
+ class ProxyAdmin(PydanticAdmin):
149
+ config = proxy_config
150
+ ```
151
+
152
+ ### Hook Implementation
153
+
154
+ ```python
155
+ # apps/proxies/tasks.py
156
+
157
+ def after_import_row_test_proxy(row, row_result, **kwargs):
158
+ """Hook called after each proxy import."""
159
+
160
+ # Skip if dry run
161
+ if kwargs.get('dry_run'):
162
+ return
163
+
164
+ # Only test new proxies
165
+ if row_result.import_type == 'new':
166
+ proxy = row_result.instance
167
+
168
+ # Queue async test
169
+ from api.workers import get_worker
170
+ worker = get_worker()
171
+ worker.enqueue_task(
172
+ 'apps.proxies.tasks.test_proxy_async',
173
+ proxy_id=str(proxy.id)
174
+ )
175
+ ```
176
+
177
+ ## Benefits
178
+
179
+ ### Before (Manual Resource Class)
180
+ ```python
181
+ from import_export import resources
182
+
183
+ class ProxyResource(resources.ModelResource):
184
+ class Meta:
185
+ model = Proxy
186
+ fields = ('host', 'port', 'username', 'password')
187
+ import_id_fields = ['host', 'port']
188
+ skip_unchanged = True
189
+
190
+ def after_import_row(self, row, row_result, **kwargs):
191
+ # Custom logic here
192
+ pass
193
+
194
+ proxy_config = AdminConfig(
195
+ model=Proxy,
196
+ import_export_enabled=True,
197
+ resource_class=ProxyResource, # Manual class
198
+ )
199
+ ```
200
+
201
+ ### After (Declarative ResourceConfig)
202
+ ```python
203
+ proxy_config = AdminConfig(
204
+ model=Proxy,
205
+ import_export_enabled=True,
206
+ resource_config=ResourceConfig(
207
+ fields=['host', 'port', 'username', 'password'],
208
+ import_id_fields=['host', 'port'],
209
+ skip_unchanged=True,
210
+ after_import_row='apps.proxies.tasks.after_import_row_test_proxy',
211
+ ),
212
+ )
213
+ ```
214
+
215
+ **Advantages:**
216
+ - ✅ No separate Resource class needed
217
+ - ✅ All configuration in one place
218
+ - ✅ Type-safe with Pydantic validation
219
+ - ✅ Reusable across multiple admins
220
+ - ✅ Hook as string path (lazy import)
221
+ - ✅ Auto-generated Resource with full control
222
+
223
+ ## Dependencies Added
224
+
225
+ **pyproject.toml updates:**
226
+ ```toml
227
+ dependencies = [
228
+ # ... existing ...
229
+ "pytz>=2025.1",
230
+ "httpx>=0.28.1,<1.0",
231
+ ]
232
+ ```
233
+
234
+ - `pytz` - Timezone support for datetime operations
235
+ - `httpx` - Modern HTTP client for proxy testing
236
+
237
+ ## Files Changed
238
+
239
+ ### django-cfg Core
240
+ 1. `config/resource_config.py` - NEW
241
+ 2. `config/background_task_config.py` - NEW
242
+ 3. `config/admin_config.py` - MODIFIED (added new configs)
243
+ 4. `config/__init__.py` - MODIFIED (exports)
244
+ 5. `base/pydantic_admin.py` - MODIFIED (Resource generation)
245
+ 6. `__init__.py` - MODIFIED (exports)
246
+ 7. `pyproject.toml` - MODIFIED (dependencies)
247
+
248
+ ### stockapis Implementation
249
+ 1. `apps/proxies/admin/proxy_admin.py` - Uses ResourceConfig
250
+ 2. `apps/proxies/admin/actions.py` - NEW (admin actions)
251
+ 3. `apps/proxies/services/proxy_tester.py` - NEW (testing logic)
252
+ 4. `apps/proxies/tasks.py` - NEW (background tasks)
253
+ 5. `apps/proxies/@docs/IMPORT_EXPORT_SETUP.md` - NEW (docs)
254
+
255
+ ## Backward Compatibility
256
+
257
+ ✅ **100% Backward Compatible**
258
+
259
+ Old code still works:
260
+ ```python
261
+ # Old way - still works
262
+ proxy_config = AdminConfig(
263
+ model=Proxy,
264
+ import_export_enabled=True,
265
+ resource_class=MyCustomResource, # Manual class
266
+ )
267
+
268
+ # New way - alternative
269
+ proxy_config = AdminConfig(
270
+ model=Proxy,
271
+ import_export_enabled=True,
272
+ resource_config=ResourceConfig(...), # Declarative
273
+ )
274
+ ```
275
+
276
+ Priority:
277
+ 1. If `resource_class` provided → use it (legacy)
278
+ 2. If `resource_config` provided → auto-generate Resource
279
+ 3. If neither → auto-generate basic Resource
280
+
281
+ ## Testing
282
+
283
+ ```bash
284
+ # Test django-cfg imports
285
+ cd django-cfg-dev
286
+ poetry run python -c "from django_cfg.modules.django_admin import ResourceConfig, BackgroundTaskConfig; print('✅ OK')"
287
+
288
+ # Test stockapis admin
289
+ cd stockapis
290
+ poetry run python manage.py check --tag admin
291
+
292
+ # Test in browser
293
+ poetry run python manage.py runserver
294
+ # Visit: http://localhost:8000/admin/proxies/proxy/
295
+ ```
296
+
297
+ ## Future Enhancements
298
+
299
+ - [ ] `ResourceConfig.field_widgets` - Custom widgets for fields
300
+ - [ ] `ResourceConfig.validators` - Custom validation rules
301
+ - [ ] `ResourceConfig.transformers` - Data transformation before import
302
+ - [ ] Progress tracking for large imports
303
+ - [ ] Import history and rollback
304
+ - [ ] Scheduled imports via cron
305
+ - [ ] API endpoint for programmatic imports
306
+
307
+ ## Migration Guide
308
+
309
+ ### For Existing Projects
310
+
311
+ 1. Update django-cfg to latest version
312
+ 2. Optional: Replace manual Resource classes with ResourceConfig
313
+ 3. Optional: Add BackgroundTaskConfig for async operations
314
+ 4. Optional: Add after_import_row hooks for post-import processing
315
+
316
+ ### Example Migration
317
+
318
+ **Before:**
319
+ ```python
320
+ # resources.py
321
+ class ProxyResource(resources.ModelResource):
322
+ class Meta:
323
+ model = Proxy
324
+ fields = ('host', 'port')
325
+
326
+ # admin.py
327
+ config = AdminConfig(
328
+ model=Proxy,
329
+ import_export_enabled=True,
330
+ resource_class=ProxyResource,
331
+ )
332
+ ```
333
+
334
+ **After:**
335
+ ```python
336
+ # admin.py only - no resources.py needed
337
+ config = AdminConfig(
338
+ model=Proxy,
339
+ import_export_enabled=True,
340
+ resource_config=ResourceConfig(
341
+ fields=['host', 'port'],
342
+ ),
343
+ )
344
+ ```
345
+
346
+ ## Version
347
+
348
+ - **django-cfg**: 1.4.106+
349
+ - **Python**: 3.12+
350
+ - **Django**: 5.2+
@@ -46,6 +46,7 @@ Example:
46
46
  from .config import (
47
47
  ActionConfig,
48
48
  AdminConfig,
49
+ BackgroundTaskConfig,
49
50
  BadgeField,
50
51
  BooleanField,
51
52
  CurrencyField,
@@ -53,6 +54,7 @@ from .config import (
53
54
  FieldConfig,
54
55
  FieldsetConfig,
55
56
  ImageField,
57
+ ResourceConfig,
56
58
  TextField,
57
59
  UserField,
58
60
  )
@@ -101,6 +103,8 @@ __all__ = [
101
103
  "FieldConfig",
102
104
  "FieldsetConfig",
103
105
  "ActionConfig",
106
+ "ResourceConfig",
107
+ "BackgroundTaskConfig",
104
108
  # Specialized Field Types
105
109
  "BadgeField",
106
110
  "BooleanField",
@@ -24,17 +24,24 @@ def _get_base_admin_class():
24
24
 
25
25
  MRO (Method Resolution Order):
26
26
  UnfoldImportExportModelAdmin
27
- └─ ImportExportModelAdmin # Import/Export functionality
28
- └─ UnfoldModelAdmin # Unfold UI
27
+ └─ UnfoldModelAdmin # Unfold UI (first for template priority)
28
+ └─ ImportExportMixin # Import/Export functionality (django_cfg custom)
29
29
  └─ Django ModelAdmin
30
30
 
31
31
  This ensures both Unfold UI and Import/Export work together seamlessly.
32
+
33
+ Uses django_cfg's custom ImportExportMixin which includes:
34
+ - Custom templates for proper Unfold styling
35
+ - Unfold forms (ImportForm, ExportForm)
36
+ - Properly styled import/export buttons
32
37
  """
33
- from import_export.admin import ImportExportModelAdmin
38
+ # Use original ImportExportModelAdmin with Unfold
39
+ from import_export.admin import ImportExportModelAdmin as BaseImportExportModelAdmin
34
40
  from unfold.admin import ModelAdmin as UnfoldModelAdmin
35
41
 
36
- class UnfoldImportExportModelAdmin(ImportExportModelAdmin, UnfoldModelAdmin):
37
- """Combined Unfold + Import/Export admin base class."""
42
+ class UnfoldImportExportModelAdmin(BaseImportExportModelAdmin, UnfoldModelAdmin):
43
+ """Combined Import/Export + Unfold admin base class."""
44
+ # Import/Export should come FIRST in MRO to get its get_urls() method
38
45
  pass
39
46
 
40
47
  return UnfoldImportExportModelAdmin
@@ -122,8 +129,120 @@ class PydanticAdminMixin:
122
129
  cls.preserve_filters = config.preserve_filters
123
130
 
124
131
  # Import/Export configuration
125
- if config.import_export_enabled and config.resource_class:
126
- cls.resource_class = config.resource_class
132
+ if config.import_export_enabled:
133
+ # Set import/export template
134
+ cls.change_list_template = 'admin/import_export/change_list_import_export.html'
135
+
136
+ if config.resource_class:
137
+ # Use provided resource class
138
+ cls.resource_class = config.resource_class
139
+ else:
140
+ # Auto-generate resource class
141
+ cls.resource_class = cls._generate_resource_class(config)
142
+
143
+ # Override changelist_view to add import/export context
144
+ original_changelist_view = cls.changelist_view
145
+
146
+ def changelist_view_with_import_export(self, request, extra_context=None):
147
+ if extra_context is None:
148
+ extra_context = {}
149
+ extra_context['has_import_permission'] = self.has_import_permission(request)
150
+ extra_context['has_export_permission'] = self.has_export_permission(request)
151
+ return original_changelist_view(self, request, extra_context)
152
+
153
+ cls.changelist_view = changelist_view_with_import_export
154
+
155
+ @classmethod
156
+ def _generate_resource_class(cls, config: AdminConfig):
157
+ """
158
+ Auto-generate a ModelResource class for import/export.
159
+
160
+ Uses ResourceConfig if provided, otherwise generates basic Resource.
161
+ """
162
+ from import_export import resources
163
+
164
+ target_model = config.model
165
+ resource_config = config.resource_config
166
+
167
+ # Determine fields to include
168
+ if resource_config and resource_config.fields:
169
+ # Use explicitly specified fields
170
+ model_fields = resource_config.fields
171
+ else:
172
+ # Auto-detect fields from model
173
+ model_fields = []
174
+ for field in target_model._meta.get_fields():
175
+ # Skip relations and auto fields that shouldn't be imported
176
+ if field.concrete and not field.many_to_many:
177
+ # Skip password fields for security
178
+ if 'password' not in field.name.lower():
179
+ model_fields.append(field.name)
180
+
181
+ # Apply exclusions if specified
182
+ if resource_config and resource_config.exclude:
183
+ model_fields = [f for f in model_fields if f not in resource_config.exclude]
184
+
185
+ # Build Meta attributes
186
+ meta_attrs = {
187
+ 'model': target_model,
188
+ 'fields': tuple(model_fields),
189
+ }
190
+
191
+ # Add ResourceConfig settings
192
+ if resource_config:
193
+ meta_attrs['import_id_fields'] = resource_config.import_id_fields
194
+ meta_attrs['skip_unchanged'] = resource_config.skip_unchanged
195
+ meta_attrs['report_skipped'] = resource_config.report_skipped
196
+ meta_attrs['use_transactions'] = resource_config.use_transactions
197
+
198
+ if resource_config.export_order:
199
+ meta_attrs['export_order'] = tuple(resource_config.export_order)
200
+ else:
201
+ # Default settings
202
+ meta_attrs['import_id_fields'] = ['id'] if 'id' in model_fields else []
203
+ meta_attrs['skip_unchanged'] = True
204
+ meta_attrs['report_skipped'] = True
205
+
206
+ # Create Meta class
207
+ ResourceMeta = type('Meta', (), meta_attrs)
208
+
209
+ # Build Resource class attributes (methods + Meta)
210
+ resource_attrs = {'Meta': ResourceMeta}
211
+
212
+ # Add hooks from ResourceConfig
213
+ if resource_config:
214
+ # before_import hook
215
+ if resource_config.before_import:
216
+ hook = resource_config.get_callable('before_import')
217
+ if hook:
218
+ resource_attrs['before_import'] = hook
219
+
220
+ # after_import hook
221
+ if resource_config.after_import:
222
+ hook = resource_config.get_callable('after_import')
223
+ if hook:
224
+ resource_attrs['after_import'] = hook
225
+
226
+ # before_import_row hook
227
+ if resource_config.before_import_row:
228
+ hook = resource_config.get_callable('before_import_row')
229
+ if hook:
230
+ resource_attrs['before_import_row'] = hook
231
+
232
+ # after_import_row hook
233
+ if resource_config.after_import_row:
234
+ hook = resource_config.get_callable('after_import_row')
235
+ if hook:
236
+ resource_attrs['after_import_row'] = hook
237
+
238
+ # Create Resource class
239
+ AutoGeneratedResource = type(
240
+ f'{target_model.__name__}Resource',
241
+ (resources.ModelResource,),
242
+ resource_attrs
243
+ )
244
+
245
+ return AutoGeneratedResource
127
246
 
128
247
  @classmethod
129
248
  def _build_list_display(cls, config: AdminConfig) -> List[str]:
@@ -4,6 +4,7 @@ Configuration models for declarative Django Admin.
4
4
 
5
5
  from .action_config import ActionConfig
6
6
  from .admin_config import AdminConfig
7
+ from .background_task_config import BackgroundTaskConfig
7
8
  from .field_config import (
8
9
  FieldConfig,
9
10
  BadgeField,
@@ -15,12 +16,15 @@ from .field_config import (
15
16
  UserField,
16
17
  )
17
18
  from .fieldset_config import FieldsetConfig
19
+ from .resource_config import ResourceConfig
18
20
 
19
21
  __all__ = [
20
22
  "AdminConfig",
21
23
  "FieldConfig",
22
24
  "FieldsetConfig",
23
25
  "ActionConfig",
26
+ "ResourceConfig",
27
+ "BackgroundTaskConfig",
24
28
  # Specialized Field Types
25
29
  "BadgeField",
26
30
  "BooleanField",
@@ -8,8 +8,10 @@ from django.db import models
8
8
  from pydantic import BaseModel, ConfigDict, Field
9
9
 
10
10
  from .action_config import ActionConfig
11
+ from .background_task_config import BackgroundTaskConfig
11
12
  from .field_config import FieldConfig
12
13
  from .fieldset_config import FieldsetConfig
14
+ from .resource_config import ResourceConfig
13
15
 
14
16
 
15
17
  class AdminConfig(BaseModel):
@@ -127,7 +129,17 @@ class AdminConfig(BaseModel):
127
129
 
128
130
  # Import/Export options
129
131
  import_export_enabled: bool = Field(False, description="Enable import/export functionality")
130
- resource_class: Optional[Type] = Field(None, description="Resource class for import/export")
132
+ resource_class: Optional[Type] = Field(None, description="Custom Resource class for import/export")
133
+ resource_config: Optional[ResourceConfig] = Field(
134
+ None,
135
+ description="Declarative resource configuration (alternative to resource_class)"
136
+ )
137
+
138
+ # Background task processing
139
+ background_task_config: Optional[BackgroundTaskConfig] = Field(
140
+ None,
141
+ description="Configuration for background task processing"
142
+ )
131
143
 
132
144
  def get_display_field_config(self, field_name: str) -> Optional[FieldConfig]:
133
145
  """Get FieldConfig for a specific field."""
@@ -0,0 +1,76 @@
1
+ """
2
+ Background task configuration for async operations.
3
+ """
4
+
5
+ from typing import Literal, Optional
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class BackgroundTaskConfig(BaseModel):
11
+ """
12
+ Configuration for background task processing.
13
+
14
+ Used for async operations like testing imported items,
15
+ bulk processing, or long-running operations.
16
+
17
+ Example:
18
+ ```python
19
+ background_config = BackgroundTaskConfig(
20
+ enabled=True,
21
+ task_runner='rearq',
22
+ batch_size=100,
23
+ timeout=300,
24
+ )
25
+ ```
26
+ """
27
+
28
+ model_config = ConfigDict(
29
+ validate_assignment=True,
30
+ extra="forbid"
31
+ )
32
+
33
+ enabled: bool = Field(
34
+ True,
35
+ description="Enable background task processing"
36
+ )
37
+
38
+ task_runner: Literal['rearq', 'celery', 'django_q', 'sync'] = Field(
39
+ 'rearq',
40
+ description="Task runner to use for background operations"
41
+ )
42
+
43
+ batch_size: int = Field(
44
+ 100,
45
+ ge=1,
46
+ le=10000,
47
+ description="Number of items to process in each batch"
48
+ )
49
+
50
+ timeout: int = Field(
51
+ 300,
52
+ ge=1,
53
+ le=3600,
54
+ description="Task timeout in seconds (max 1 hour)"
55
+ )
56
+
57
+ retry_on_failure: bool = Field(
58
+ True,
59
+ description="Automatically retry failed tasks"
60
+ )
61
+
62
+ max_retries: int = Field(
63
+ 3,
64
+ ge=0,
65
+ le=10,
66
+ description="Maximum number of retry attempts"
67
+ )
68
+
69
+ priority: Literal['high', 'normal', 'low'] = Field(
70
+ 'normal',
71
+ description="Task priority in queue"
72
+ )
73
+
74
+ def should_use_background(self) -> bool:
75
+ """Check if background processing should be used."""
76
+ return self.enabled and self.task_runner != 'sync'
@@ -0,0 +1,129 @@
1
+ """
2
+ Resource configuration for import/export functionality.
3
+ """
4
+
5
+ from typing import Any, Callable, Dict, List, Optional, Union
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class ResourceConfig(BaseModel):
11
+ """
12
+ Configuration for django-import-export Resource class.
13
+
14
+ Provides declarative configuration for import/export behavior without
15
+ needing to manually create Resource classes.
16
+
17
+ Example:
18
+ ```python
19
+ resource_config = ResourceConfig(
20
+ fields=['host', 'port', 'username', 'password', 'provider'],
21
+ exclude=['metadata', 'config'],
22
+ import_id_fields=['host', 'port'],
23
+ after_import_row='apps.proxies.services.test_proxy_async',
24
+ skip_unchanged=True,
25
+ )
26
+ ```
27
+ """
28
+
29
+ model_config = ConfigDict(
30
+ validate_assignment=True,
31
+ extra="forbid",
32
+ arbitrary_types_allowed=True
33
+ )
34
+
35
+ # Field configuration
36
+ fields: List[str] = Field(
37
+ default_factory=list,
38
+ description="Fields to include in import/export (empty = all fields)"
39
+ )
40
+ exclude: List[str] = Field(
41
+ default_factory=list,
42
+ description="Fields to exclude from import/export"
43
+ )
44
+ import_id_fields: List[str] = Field(
45
+ default_factory=lambda: ['id'],
46
+ description="Fields used to identify existing rows during import"
47
+ )
48
+
49
+ # Import behavior
50
+ skip_unchanged: bool = Field(
51
+ True,
52
+ description="Skip rows that haven't changed during import"
53
+ )
54
+ report_skipped: bool = Field(
55
+ True,
56
+ description="Include skipped rows in import report"
57
+ )
58
+ skip_diff: bool = Field(
59
+ False,
60
+ description="Skip diff generation for faster imports (large datasets)"
61
+ )
62
+ use_transactions: bool = Field(
63
+ True,
64
+ description="Use database transactions for imports"
65
+ )
66
+
67
+ # Validation and hooks (can be string paths or callables)
68
+ before_import: Optional[Union[str, Callable]] = Field(
69
+ None,
70
+ description="Hook called before import starts (receives dataset, dry_run)"
71
+ )
72
+ after_import: Optional[Union[str, Callable]] = Field(
73
+ None,
74
+ description="Hook called after import completes (receives dataset, result, dry_run)"
75
+ )
76
+ before_import_row: Optional[Union[str, Callable]] = Field(
77
+ None,
78
+ description="Hook called before each row import (receives row, row_number, dry_run)"
79
+ )
80
+ after_import_row: Optional[Union[str, Callable]] = Field(
81
+ None,
82
+ description="Hook called after each row import (receives row, row_result, row_number)"
83
+ )
84
+
85
+ # Export options
86
+ export_order: List[str] = Field(
87
+ default_factory=list,
88
+ description="Field order in exported files (empty = model field order)"
89
+ )
90
+
91
+ # Field widgets/customization
92
+ field_widgets: Dict[str, Any] = Field(
93
+ default_factory=dict,
94
+ description="Custom widgets for fields (e.g., {'date': {'format': '%Y-%m-%d'}})"
95
+ )
96
+
97
+ # Batch processing
98
+ batch_size: Optional[int] = Field(
99
+ None,
100
+ description="Process imports in batches (for large datasets)"
101
+ )
102
+
103
+ def get_callable(self, hook_name: str) -> Optional[Callable]:
104
+ """
105
+ Get callable for a hook by name.
106
+
107
+ Args:
108
+ hook_name: Name of the hook ('before_import', 'after_import_row', etc.)
109
+
110
+ Returns:
111
+ Callable function or None if not set
112
+ """
113
+ hook_value = getattr(self, hook_name, None)
114
+
115
+ if hook_value is None:
116
+ return None
117
+
118
+ # If already a callable, return it
119
+ if callable(hook_value):
120
+ return hook_value
121
+
122
+ # If string, import it
123
+ if isinstance(hook_value, str):
124
+ import importlib
125
+ module_path, function_name = hook_value.rsplit('.', 1)
126
+ module = importlib.import_module(module_path)
127
+ return getattr(module, function_name)
128
+
129
+ return None
@@ -74,8 +74,26 @@ The module automatically works with:
74
74
  - Django-cfg configuration system
75
75
  - All original django-import-export features
76
76
 
77
+ ## Custom Templates
78
+
79
+ This module includes custom templates for proper Unfold integration:
80
+
81
+ ### Button Styling
82
+ - Import/Export buttons are styled as round icon buttons matching Unfold's design
83
+ - Import button: Green with upload icon
84
+ - Export button: Blue with download icon
85
+ - Positioned next to the "Add" button in the admin interface
86
+
87
+ ### Template Locations
88
+ Templates are located in `templates/admin/import_export/`:
89
+ - `change_list_import_export.html` - For both import and export
90
+ - `change_list_import.html` - Import only
91
+ - `change_list_export.html` - Export only
92
+ - `change_list_import_item.html` - Import button template
93
+ - `change_list_export_item.html` - Export button template
94
+
77
95
  ## Full Documentation
78
96
 
79
97
  For complete documentation, see the official [django-import-export docs](https://django-import-export.readthedocs.io/).
80
98
 
81
- This module adds no additional functionality - it's purely for convenience and consistency within the django-cfg ecosystem.
99
+ This module adds custom Unfold-styled templates for better visual integration with the admin interface.
@@ -22,6 +22,14 @@ class ImportExportMixin(BaseImportExportMixin):
22
22
  import_form_class = ImportForm
23
23
  export_form_class = ExportForm
24
24
 
25
+ def changelist_view(self, request, extra_context=None):
26
+ """Add import/export permissions to context."""
27
+ if extra_context is None:
28
+ extra_context = {}
29
+ extra_context['has_import_permission'] = self.has_import_permission(request)
30
+ extra_context['has_export_permission'] = self.has_export_permission(request)
31
+ return super().changelist_view(request, extra_context)
32
+
25
33
 
26
34
  class ImportExportModelAdmin(BaseImportExportModelAdmin):
27
35
  """Django-CFG enhanced ImportExportModelAdmin with custom templates and Unfold forms."""
@@ -0,0 +1,16 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoImportExportConfig(AppConfig):
5
+ """Django Import/Export integration app configuration."""
6
+
7
+ default_auto_field = 'django.db.models.BigAutoField'
8
+ name = 'django_cfg.modules.django_import_export'
9
+ label = 'django_cfg_import_export'
10
+ verbose_name = 'Django Import/Export Integration'
11
+
12
+ def ready(self):
13
+ """Initialize module classes when Django is ready."""
14
+ # Import and setup classes here to avoid AppRegistryNotReady
15
+ from . import _setup_classes
16
+ _setup_classes()
@@ -0,0 +1,6 @@
1
+ {% extends "admin/change_list.html" %}
2
+
3
+ {% block object-tools-items %}
4
+ {{ block.super }}
5
+ {% include "admin/import_export/change_list_export_item.html" %}
6
+ {% endblock %}
@@ -0,0 +1,7 @@
1
+ {% load admin_urls i18n %}
2
+
3
+ {% if has_export_permission %}
4
+ <a href="export/{{ cl.get_query_string }}" class="bg-blue-600 flex items-center h-9 justify-center -my-1 rounded-full w-9 hover:bg-blue-700" title="{% trans 'Export' %}">
5
+ <span class="material-symbols-outlined text-white">download</span>
6
+ </a>
7
+ {% endif %}
@@ -0,0 +1,6 @@
1
+ {% extends "admin/change_list.html" %}
2
+
3
+ {% block object-tools-items %}
4
+ {{ block.super }}
5
+ {% include "admin/import_export/change_list_import_item.html" %}
6
+ {% endblock %}
@@ -0,0 +1,39 @@
1
+ {% extends "admin/change_list.html" %}
2
+
3
+ {% block extrahead %}
4
+ {{ block.super }}
5
+ <style>
6
+ /* Object Tools - Admin toolbar buttons */
7
+ .object-tools {
8
+ display: flex !important;
9
+ gap: 1rem !important; /* 16px gap between groups */
10
+ align-items: center !important;
11
+ }
12
+
13
+ /* Remove extra spacing from individual object-tools items */
14
+ .object-tools > * {
15
+ margin: 0 !important;
16
+ }
17
+
18
+ /* Import/Export button group */
19
+ .import-export-group {
20
+ display: flex !important;
21
+ gap: 0.5rem !important; /* 8px gap between import/export buttons */
22
+ align-items: center !important;
23
+ }
24
+
25
+ /* Separator before import/export group */
26
+ .import-export-separator {
27
+ margin: 0 0.5rem !important; /* 8px margin on each side */
28
+ }
29
+ </style>
30
+ {% endblock %}
31
+
32
+ {% block object-tools-items %}
33
+ {{ block.super }}
34
+ <span class="block bg-base-200 h-5 w-px dark:bg-base-700 import-export-separator"></span>
35
+ <div class="import-export-group">
36
+ {% include "admin/import_export/change_list_import_item.html" %}
37
+ {% include "admin/import_export/change_list_export_item.html" %}
38
+ </div>
39
+ {% endblock %}
@@ -0,0 +1,7 @@
1
+ {% load admin_urls i18n %}
2
+
3
+ {% if has_import_permission %}
4
+ <a href="import/" class="bg-green-600 flex items-center h-9 justify-center -my-1 rounded-full w-9 hover:bg-green-700" title="{% trans 'Import' %}">
5
+ <span class="material-symbols-outlined text-white">upload</span>
6
+ </a>
7
+ {% endif %}
django_cfg/pyproject.toml CHANGED
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "django-cfg"
7
- version = "1.4.105"
7
+ version = "1.4.107"
8
8
  description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
9
9
  readme = "README.md"
10
10
  keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
11
11
  classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed",]
12
12
  requires-python = ">=3.12,<3.14"
13
- dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "dramatiq[redis]>=1.18.0,<2.0", "django-dramatiq>=0.14.0,<1.0", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)",]
13
+ dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "dramatiq[redis]>=1.18.0,<2.0", "django-dramatiq>=0.14.0,<1.0", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0",]
14
14
  [[project.authors]]
15
15
  name = "Django-CFG Team"
16
16
  email = "info@djangocfg.com"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.4.105
3
+ Version: 1.4.107
4
4
  Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
5
5
  Project-URL: Homepage, https://djangocfg.com
6
6
  Project-URL: Documentation, https://djangocfg.com
@@ -56,6 +56,7 @@ Requires-Dist: drf-nested-routers<1.0,>=0.94.0
56
56
  Requires-Dist: drf-spectacular-sidecar<2026.0,>=2025.8.0
57
57
  Requires-Dist: drf-spectacular<1.0,>=0.28.0
58
58
  Requires-Dist: hiredis<4.0,>=2.0.0
59
+ Requires-Dist: httpx<1.0,>=0.28.1
59
60
  Requires-Dist: jinja2<4.0.0,>=3.1.6
60
61
  Requires-Dist: loguru<1.0,>=0.7.0
61
62
  Requires-Dist: lxml<7.0,>=6.0.0
@@ -69,6 +70,7 @@ Requires-Dist: pydantic<3.0,>=2.11.0
69
70
  Requires-Dist: pydantic[email]<3.0,>=2.11.0
70
71
  Requires-Dist: pytelegrambotapi<5.0,>=4.28.0
71
72
  Requires-Dist: python-json-logger<4.0,>=3.3.0
73
+ Requires-Dist: pytz>=2025.1
72
74
  Requires-Dist: pyyaml<7.0,>=6.0
73
75
  Requires-Dist: questionary<3.0,>=2.1.0
74
76
  Requires-Dist: redis<7.0,>=6.4.0
@@ -1,5 +1,5 @@
1
1
  django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_cfg/__init__.py,sha256=MXUS0jQw0iZtqEX_dQus7hIBxuCa6q1o31FhWBRFJOo,1621
2
+ django_cfg/__init__.py,sha256=KYlYkP_W1VDKzLgBVLlALZylOST7Myun4rcy-Sa-_80,1621
3
3
  django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
4
4
  django_cfg/config.py,sha256=y4Z3rnYsHBE0TehpwAIPaxr---mkvyKrZGGsNwYso74,1398
5
5
  django_cfg/apps/__init__.py,sha256=JtDmEYt1OcleWM2ZaeX0LKDnRQzPOavfaXBWG4ECB5Q,26
@@ -577,7 +577,7 @@ django_cfg/core/generation/protocols.py,sha256=9pqGsZD_Y5v3jdYp4YdkL-i--8feuvX_i
577
577
  django_cfg/core/generation/core_generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
578
578
  django_cfg/core/generation/core_generators/settings.py,sha256=Rto9qjqQC-MeQJMNyoflS91RrNDS_lBNH4d4ZtgER-8,2632
579
579
  django_cfg/core/generation/core_generators/static.py,sha256=7xo7F9DSbz5Gk2Di-SbBo0petNMX4ZKfS8wuQCe9lz8,2747
580
- django_cfg/core/generation/core_generators/templates.py,sha256=0H2uL0YElsH7U2G1kZzOF-GgeimkzV-8UltLnkhfVfs,4198
580
+ django_cfg/core/generation/core_generators/templates.py,sha256=XeGKzF790_zKwFdRw3FN-VHCEgw6Q1dWPX7C1tNas1s,4885
581
581
  django_cfg/core/generation/data_generators/__init__.py,sha256=O7cmt_k34hWRgrdaAzCRPudcaowaOwMgaoXmZY8HG54,298
582
582
  django_cfg/core/generation/data_generators/cache.py,sha256=bdcbnaDarl_8UQ18gk7bIKrQ9PDZOhpDAVjCEgYVeUU,3840
583
583
  django_cfg/core/generation/data_generators/database.py,sha256=mYR2mBvqWU93YP9XUUVlcZaQJtSNeAQ7Dk4Vj6Tu95k,3481
@@ -695,15 +695,19 @@ django_cfg/models/tasks/utils.py,sha256=5CR_d6dD6vYZ9UpI3kKD9EJwuPdWWuqiA_hhxXzm
695
695
  django_cfg/modules/__init__.py,sha256=Ip9WMpzImEwIAywpFwU056_v0O9oIGG7nCT1YSArxkw,316
696
696
  django_cfg/modules/base.py,sha256=Grmgxc5dvnAEM1sudWEWO4kv8L0Ks-y32nxTk2vwdjQ,6272
697
697
  django_cfg/modules/django_admin/CHANGELOG_CODE_METHODS.md,sha256=HfE_rUlovx1zX_1hkzQsjwghaFvIvUWjZ_Aume8lhIs,3823
698
- django_cfg/modules/django_admin/__init__.py,sha256=rWUY6Le2gO-szuuQyrUUP8sLIaTwkNDBexdK8Vbwzv0,3094
698
+ django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md,sha256=F8z4oHZk7jkRChxW8tKsVf0Q_OujjlBUs8UJNirn55Y,2055
699
+ django_cfg/modules/django_admin/RESOURCE_CONFIG_ENHANCEMENT.md,sha256=MKUpRHMqM3KDEKZthWhul-4EoSnE75kkWd7IcEzwYDQ,8729
700
+ django_cfg/modules/django_admin/__init__.py,sha256=LYVFCVgN6Irr80dK8SkXgKQ-Xgg2MyRL5CgfOgmSt8A,3190
699
701
  django_cfg/modules/django_admin/base/__init__.py,sha256=tzre09bnD_SlS-pA30WzYZRxyvch7eLq3q0wLEcZOmc,118
700
- django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=jCsueX1r_nD73FNeiFwhI149JKdpwIyQm0NCHmtNzXU,15976
702
+ django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=v2U-Tm_zWkDsh1CGmYL-xJdsVvHKX7MjUMrFwpY6wpw,20879
701
703
  django_cfg/modules/django_admin/base/unfold_admin.py,sha256=iqpRWSkzW5HktXDuuG7G3J6RoIfW48dWPMJTa7Yk08g,729
702
- django_cfg/modules/django_admin/config/__init__.py,sha256=UJGJMP1iAguzd33E1BgeIjWaooFYylku3aR_Arib-cg,604
704
+ django_cfg/modules/django_admin/config/__init__.py,sha256=IjD2pJBb_rboj9V4_4uzBmzIIzm4a5Xy8XIieL5myww,755
703
705
  django_cfg/modules/django_admin/config/action_config.py,sha256=JjS01JxLT-FzUVq7RlKaB7L38wmVL8uibXO_iXZcljo,1668
704
- django_cfg/modules/django_admin/config/admin_config.py,sha256=vX9CVjC8FgDLdkhUMHNqkpaoO9c1dhzR4mNltET12kM,4569
706
+ django_cfg/modules/django_admin/config/admin_config.py,sha256=7ozVnLqJcUrHu-UK-PdOfuX2UOvR1gkigThgQsfO0wY,5030
707
+ django_cfg/modules/django_admin/config/background_task_config.py,sha256=7-8B1rhpeafanVxtjFQUx0mVjcA5xmxZIxqKzaBwMX0,1760
705
708
  django_cfg/modules/django_admin/config/field_config.py,sha256=TXdmz1GHC0N7euzqgB9p5EhFCYzsEobb8VSgS4kG4ho,8928
706
709
  django_cfg/modules/django_admin/config/fieldset_config.py,sha256=5BPUWO_HvS6YhPU_vqQPzRT2y3OIPrBCioFuer5-mrA,1249
710
+ django_cfg/modules/django_admin/config/resource_config.py,sha256=6ELWSuHlrpNqV2wO2sL_o2mE2DXLd_solcsdN5O6eQg,3962
707
711
  django_cfg/modules/django_admin/icons/README.md,sha256=j-NUixSC1QJh7PqYKxLZpPrTxKrAnx0urQraXgv4JHI,5612
708
712
  django_cfg/modules/django_admin/icons/__init__.py,sha256=XrdQ3vmADQ1y2yNaL2jvffHCXjy3GjpoBbTsDPLD8Ko,198
709
713
  django_cfg/modules/django_admin/icons/constants.py,sha256=g5rrZOMLP4bT824pwksRavNQsW1I2AdWKvoGPtk5qQA,124554
@@ -909,8 +913,14 @@ django_cfg/modules/django_email/management/commands/__init__.py,sha256=47DEQpj8H
909
913
  django_cfg/modules/django_email/management/commands/test_email.py,sha256=PecBUXIkT0tlH6_ZbhLOrju_lKCnsfICuFlWBpXIyGk,3316
910
914
  django_cfg/modules/django_health/__init__.py,sha256=lX8uJlAmXZeAVfQy12H67uTTy5SOjYMu3N8d8WF8ZNM,156
911
915
  django_cfg/modules/django_health/service.py,sha256=K7Fm3fmRjrjRiPpaQIfyaQ4cI3coSIX2HrsRXjUzxOk,8837
912
- django_cfg/modules/django_import_export/README.md,sha256=OuGT5gv0hwt3dcbMLfZUfostZnXALeN4OifgRvv--4k,2453
913
- django_cfg/modules/django_import_export/__init__.py,sha256=scmdtK0GoxGBLk7W0w96n9p1tLEJSyJj-clFUMv4UWY,2003
916
+ django_cfg/modules/django_import_export/README.md,sha256=Oha18L7skITg-5oOfcKEvwealrD7FIziXt2y66WoaYM,3115
917
+ django_cfg/modules/django_import_export/__init__.py,sha256=vnMEpayU1RCmsC3yC38oIRvZzlmcLg0d0Y24fzQbl0Y,2418
918
+ django_cfg/modules/django_import_export/apps.py,sha256=W_yhcefsNxz8aYFCwacFmeFeTZ0clUTGU0gL-GfGOQ4,560
919
+ django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export.html,sha256=RcbmcksrwUtZVA_hKszu2pMOPw3I758qsNWMNJSrDJk,177
920
+ django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export_item.html,sha256=Lq_c0KS-FEbG7eCo6XjUSgB-Mp9XOr_m7Mn77Z2f_CA,330
921
+ django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import.html,sha256=XW89hAnilxQT2TKuIpQ4SPtxcdnRMl_1X_h_Ie5V2Xc,177
922
+ django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_export.html,sha256=BHr4h4aX7_ZzodfwisVbAewf8qKOuhGNeseURcMmK7o,1269
923
+ django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_item.html,sha256=XYsTEbQc3CCQR06-4qazXx-svQ0RoEDE6YPKoZw1oKo,305
914
924
  django_cfg/modules/django_llm/README.md,sha256=31uQG9_z-HroEUevtOPJbZ86H9yiwzZJs0omf-Kwh-o,9374
915
925
  django_cfg/modules/django_llm/__init__.py,sha256=tksyeH6e5ajrSNa6MVkeu_IZQE_I-zka1iTLAIU-WtE,1913
916
926
  django_cfg/modules/django_llm/example.py,sha256=PqOZXV1qY8GjFfRdOM3a8Rjj59CzDbETokkvPqAkkm8,12797
@@ -1087,9 +1097,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1087
1097
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1088
1098
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1089
1099
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1090
- django_cfg/pyproject.toml,sha256=mSzm-YWCFBLu53-OiCS9-wjIBxA9vm-yMg6Dii7FzYc,8573
1091
- django_cfg-1.4.105.dist-info/METADATA,sha256=mQjCmuDpR7D-vMY12IAIzJskSnrlBEBNWQ2Fwn-vAEc,23734
1092
- django_cfg-1.4.105.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1093
- django_cfg-1.4.105.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1094
- django_cfg-1.4.105.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1095
- django_cfg-1.4.105.dist-info/RECORD,,
1100
+ django_cfg/pyproject.toml,sha256=ejHs8Z6myjIm2T37CZ34T6AZZsJQokkCICbzWwZTS4s,8611
1101
+ django_cfg-1.4.107.dist-info/METADATA,sha256=1wc8DsLeaO5VvXTAWpExOOqhKuT7Q_LXw4NNAp4znOo,23796
1102
+ django_cfg-1.4.107.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1103
+ django_cfg-1.4.107.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1104
+ django_cfg-1.4.107.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1105
+ django_cfg-1.4.107.dist-info/RECORD,,