django-cfg 1.4.106__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.

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.106"
35
+ __version__ = "1.4.107"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -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",
@@ -154,37 +154,92 @@ class PydanticAdminMixin:
154
154
 
155
155
  @classmethod
156
156
  def _generate_resource_class(cls, config: AdminConfig):
157
- """Auto-generate a ModelResource class for import/export."""
157
+ """
158
+ Auto-generate a ModelResource class for import/export.
159
+
160
+ Uses ResourceConfig if provided, otherwise generates basic Resource.
161
+ """
158
162
  from import_export import resources
159
163
 
160
164
  target_model = config.model
165
+ resource_config = config.resource_config
161
166
 
162
- # Get all model fields
163
- model_fields = []
164
- for field in target_model._meta.get_fields():
165
- # Skip relations and auto fields that shouldn't be imported
166
- if field.concrete and not field.many_to_many:
167
- # Skip password fields for security
168
- if 'password' not in field.name.lower():
169
- model_fields.append(field.name)
170
-
171
- # Create dynamic resource class with explicit Meta attributes
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
172
186
  meta_attrs = {
173
187
  'model': target_model,
174
188
  'fields': tuple(model_fields),
175
- 'import_id_fields': ['id'] if 'id' in model_fields else [],
176
- 'skip_unchanged': True,
177
- 'report_skipped': True,
178
189
  }
179
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
+
180
206
  # Create Meta class
181
207
  ResourceMeta = type('Meta', (), meta_attrs)
182
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
+
183
238
  # Create Resource class
184
239
  AutoGeneratedResource = type(
185
240
  f'{target_model.__name__}Resource',
186
241
  (resources.ModelResource,),
187
- {'Meta': ResourceMeta}
242
+ resource_attrs
188
243
  )
189
244
 
190
245
  return AutoGeneratedResource
@@ -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
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.106"
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.106
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=a9PJ3jnY_fzqDDHW2WT6IkYi7nMmZ7BbcBqBL7AtKfY,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
@@ -696,15 +696,18 @@ django_cfg/modules/__init__.py,sha256=Ip9WMpzImEwIAywpFwU056_v0O9oIGG7nCT1YSArxk
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
698
  django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md,sha256=F8z4oHZk7jkRChxW8tKsVf0Q_OujjlBUs8UJNirn55Y,2055
699
- django_cfg/modules/django_admin/__init__.py,sha256=rWUY6Le2gO-szuuQyrUUP8sLIaTwkNDBexdK8Vbwzv0,3094
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
700
701
  django_cfg/modules/django_admin/base/__init__.py,sha256=tzre09bnD_SlS-pA30WzYZRxyvch7eLq3q0wLEcZOmc,118
701
- django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=srGVXDdlb0YIcP_VIpVFjLegIkg7vEKp8GlAidtoBc0,18639
702
+ django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=v2U-Tm_zWkDsh1CGmYL-xJdsVvHKX7MjUMrFwpY6wpw,20879
702
703
  django_cfg/modules/django_admin/base/unfold_admin.py,sha256=iqpRWSkzW5HktXDuuG7G3J6RoIfW48dWPMJTa7Yk08g,729
703
- 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
704
705
  django_cfg/modules/django_admin/config/action_config.py,sha256=JjS01JxLT-FzUVq7RlKaB7L38wmVL8uibXO_iXZcljo,1668
705
- 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
706
708
  django_cfg/modules/django_admin/config/field_config.py,sha256=TXdmz1GHC0N7euzqgB9p5EhFCYzsEobb8VSgS4kG4ho,8928
707
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
708
711
  django_cfg/modules/django_admin/icons/README.md,sha256=j-NUixSC1QJh7PqYKxLZpPrTxKrAnx0urQraXgv4JHI,5612
709
712
  django_cfg/modules/django_admin/icons/__init__.py,sha256=XrdQ3vmADQ1y2yNaL2jvffHCXjy3GjpoBbTsDPLD8Ko,198
710
713
  django_cfg/modules/django_admin/icons/constants.py,sha256=g5rrZOMLP4bT824pwksRavNQsW1I2AdWKvoGPtk5qQA,124554
@@ -1094,9 +1097,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
1094
1097
  django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
1095
1098
  django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
1096
1099
  django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1097
- django_cfg/pyproject.toml,sha256=Azsm48lHmm_ksNBsj-MZGKqdiLerHiVMQ71jY8MldP8,8573
1098
- django_cfg-1.4.106.dist-info/METADATA,sha256=Dl1J5WrtjhAyxfHAWz64bAnAhjZ8JXz9djjDwg2-epg,23734
1099
- django_cfg-1.4.106.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1100
- django_cfg-1.4.106.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
1101
- django_cfg-1.4.106.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
1102
- django_cfg-1.4.106.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,,