django-cfg 1.1.81__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. django_cfg/__init__.py +20 -448
  2. django_cfg/apps/accounts/README.md +3 -3
  3. django_cfg/apps/accounts/admin/__init__.py +0 -2
  4. django_cfg/apps/accounts/admin/activity.py +2 -9
  5. django_cfg/apps/accounts/admin/filters.py +0 -42
  6. django_cfg/apps/accounts/admin/inlines.py +8 -8
  7. django_cfg/apps/accounts/admin/otp.py +5 -5
  8. django_cfg/apps/accounts/admin/registration_source.py +1 -8
  9. django_cfg/apps/accounts/admin/user.py +12 -20
  10. django_cfg/apps/accounts/managers/user_manager.py +2 -129
  11. django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
  12. django_cfg/apps/accounts/models.py +3 -123
  13. django_cfg/apps/accounts/serializers/otp.py +40 -44
  14. django_cfg/apps/accounts/serializers/profile.py +0 -2
  15. django_cfg/apps/accounts/services/otp_service.py +98 -186
  16. django_cfg/apps/accounts/signals.py +25 -15
  17. django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
  18. django_cfg/apps/accounts/views/otp.py +35 -36
  19. django_cfg/apps/agents/README.md +129 -0
  20. django_cfg/apps/agents/__init__.py +68 -0
  21. django_cfg/apps/agents/admin/__init__.py +17 -0
  22. django_cfg/apps/agents/admin/execution_admin.py +460 -0
  23. django_cfg/apps/agents/admin/registry_admin.py +360 -0
  24. django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
  25. django_cfg/apps/agents/apps.py +29 -0
  26. django_cfg/apps/agents/core/__init__.py +20 -0
  27. django_cfg/apps/agents/core/agent.py +281 -0
  28. django_cfg/apps/agents/core/dependencies.py +154 -0
  29. django_cfg/apps/agents/core/exceptions.py +66 -0
  30. django_cfg/apps/agents/core/models.py +106 -0
  31. django_cfg/apps/agents/core/orchestrator.py +391 -0
  32. django_cfg/apps/agents/examples/__init__.py +3 -0
  33. django_cfg/apps/agents/examples/simple_example.py +161 -0
  34. django_cfg/apps/agents/integration/__init__.py +14 -0
  35. django_cfg/apps/agents/integration/middleware.py +80 -0
  36. django_cfg/apps/agents/integration/registry.py +345 -0
  37. django_cfg/apps/agents/integration/signals.py +50 -0
  38. django_cfg/apps/agents/management/__init__.py +3 -0
  39. django_cfg/apps/agents/management/commands/__init__.py +3 -0
  40. django_cfg/apps/agents/management/commands/create_agent.py +365 -0
  41. django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
  42. django_cfg/apps/agents/managers/__init__.py +23 -0
  43. django_cfg/apps/agents/managers/execution.py +236 -0
  44. django_cfg/apps/agents/managers/registry.py +254 -0
  45. django_cfg/apps/agents/managers/toolsets.py +496 -0
  46. django_cfg/apps/agents/migrations/0001_initial.py +286 -0
  47. django_cfg/apps/agents/migrations/__init__.py +5 -0
  48. django_cfg/apps/agents/models/__init__.py +15 -0
  49. django_cfg/apps/agents/models/execution.py +215 -0
  50. django_cfg/apps/agents/models/registry.py +220 -0
  51. django_cfg/apps/agents/models/toolsets.py +305 -0
  52. django_cfg/apps/agents/patterns/__init__.py +24 -0
  53. django_cfg/apps/agents/patterns/content_agents.py +234 -0
  54. django_cfg/apps/agents/toolsets/__init__.py +15 -0
  55. django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
  56. django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
  57. django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
  58. django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
  59. django_cfg/apps/agents/urls.py +46 -0
  60. django_cfg/apps/knowbase/README.md +150 -0
  61. django_cfg/apps/knowbase/__init__.py +27 -0
  62. django_cfg/apps/knowbase/admin/__init__.py +23 -0
  63. django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
  64. django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
  65. django_cfg/apps/knowbase/admin/document_admin.py +650 -0
  66. django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
  67. django_cfg/apps/knowbase/apps.py +81 -0
  68. django_cfg/apps/knowbase/config/README.md +176 -0
  69. django_cfg/apps/knowbase/config/__init__.py +51 -0
  70. django_cfg/apps/knowbase/config/constance_fields.py +186 -0
  71. django_cfg/apps/knowbase/config/constance_settings.py +200 -0
  72. django_cfg/apps/knowbase/config/settings.py +444 -0
  73. django_cfg/apps/knowbase/examples/__init__.py +3 -0
  74. django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
  75. django_cfg/apps/knowbase/management/__init__.py +0 -0
  76. django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
  77. django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
  78. django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
  79. django_cfg/apps/knowbase/managers/__init__.py +22 -0
  80. django_cfg/apps/knowbase/managers/archive.py +426 -0
  81. django_cfg/apps/knowbase/managers/base.py +32 -0
  82. django_cfg/apps/knowbase/managers/chat.py +141 -0
  83. django_cfg/apps/knowbase/managers/document.py +203 -0
  84. django_cfg/apps/knowbase/managers/external_data.py +471 -0
  85. django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
  86. django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
  87. django_cfg/apps/knowbase/migrations/__init__.py +5 -0
  88. django_cfg/apps/knowbase/mixins/__init__.py +15 -0
  89. django_cfg/apps/knowbase/mixins/config.py +108 -0
  90. django_cfg/apps/knowbase/mixins/creator.py +81 -0
  91. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
  92. django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
  93. django_cfg/apps/knowbase/mixins/service.py +362 -0
  94. django_cfg/apps/knowbase/models/__init__.py +41 -0
  95. django_cfg/apps/knowbase/models/archive.py +599 -0
  96. django_cfg/apps/knowbase/models/base.py +58 -0
  97. django_cfg/apps/knowbase/models/chat.py +157 -0
  98. django_cfg/apps/knowbase/models/document.py +267 -0
  99. django_cfg/apps/knowbase/models/external_data.py +376 -0
  100. django_cfg/apps/knowbase/serializers/__init__.py +68 -0
  101. django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
  102. django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
  103. django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
  104. django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
  105. django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
  106. django_cfg/apps/knowbase/services/__init__.py +40 -0
  107. django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
  108. django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
  109. django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
  110. django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
  111. django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
  112. django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
  113. django_cfg/apps/knowbase/services/base.py +53 -0
  114. django_cfg/apps/knowbase/services/chat_service.py +239 -0
  115. django_cfg/apps/knowbase/services/document_service.py +144 -0
  116. django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
  117. django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
  118. django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
  119. django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
  120. django_cfg/apps/knowbase/services/embedding/models.py +229 -0
  121. django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
  122. django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
  123. django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
  124. django_cfg/apps/knowbase/services/search_service.py +293 -0
  125. django_cfg/apps/knowbase/signals/__init__.py +21 -0
  126. django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
  127. django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
  128. django_cfg/apps/knowbase/signals/document_signals.py +143 -0
  129. django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
  130. django_cfg/apps/knowbase/tasks/__init__.py +39 -0
  131. django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
  132. django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
  133. django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
  134. django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
  135. django_cfg/apps/knowbase/urls.py +43 -0
  136. django_cfg/apps/knowbase/utils/__init__.py +12 -0
  137. django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
  138. django_cfg/apps/knowbase/utils/text_processing.py +375 -0
  139. django_cfg/apps/knowbase/utils/validation.py +99 -0
  140. django_cfg/apps/knowbase/views/__init__.py +28 -0
  141. django_cfg/apps/knowbase/views/archive_views.py +469 -0
  142. django_cfg/apps/knowbase/views/base.py +49 -0
  143. django_cfg/apps/knowbase/views/chat_views.py +181 -0
  144. django_cfg/apps/knowbase/views/document_views.py +183 -0
  145. django_cfg/apps/knowbase/views/public_views.py +129 -0
  146. django_cfg/apps/leads/admin.py +70 -0
  147. django_cfg/apps/newsletter/admin.py +234 -0
  148. django_cfg/apps/newsletter/admin_filters.py +124 -0
  149. django_cfg/apps/support/admin.py +196 -0
  150. django_cfg/apps/support/admin_filters.py +71 -0
  151. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  152. django_cfg/apps/urls.py +5 -4
  153. django_cfg/cli/README.md +1 -1
  154. django_cfg/cli/commands/create_project.py +2 -2
  155. django_cfg/cli/commands/info.py +1 -1
  156. django_cfg/config.py +44 -0
  157. django_cfg/core/config.py +29 -82
  158. django_cfg/core/environment.py +1 -1
  159. django_cfg/core/generation.py +19 -107
  160. django_cfg/{integration.py → core/integration.py} +18 -16
  161. django_cfg/core/validation.py +1 -1
  162. django_cfg/management/__init__.py +1 -1
  163. django_cfg/management/commands/__init__.py +1 -1
  164. django_cfg/management/commands/auto_generate.py +482 -0
  165. django_cfg/management/commands/migrator.py +19 -101
  166. django_cfg/management/commands/test_email.py +1 -1
  167. django_cfg/middleware/README.md +0 -158
  168. django_cfg/middleware/__init__.py +0 -2
  169. django_cfg/middleware/user_activity.py +3 -3
  170. django_cfg/models/api.py +145 -0
  171. django_cfg/models/base.py +287 -0
  172. django_cfg/models/cache.py +4 -4
  173. django_cfg/models/constance.py +25 -88
  174. django_cfg/models/database.py +9 -9
  175. django_cfg/models/drf.py +3 -36
  176. django_cfg/models/email.py +163 -0
  177. django_cfg/models/environment.py +276 -0
  178. django_cfg/models/limits.py +1 -1
  179. django_cfg/models/logging.py +366 -0
  180. django_cfg/models/revolution.py +41 -2
  181. django_cfg/models/security.py +125 -0
  182. django_cfg/models/services.py +1 -1
  183. django_cfg/modules/__init__.py +2 -56
  184. django_cfg/modules/base.py +78 -52
  185. django_cfg/modules/django_currency/service.py +2 -2
  186. django_cfg/modules/django_email.py +2 -2
  187. django_cfg/modules/django_health.py +267 -0
  188. django_cfg/modules/django_llm/llm/client.py +79 -17
  189. django_cfg/modules/django_llm/translator/translator.py +2 -2
  190. django_cfg/modules/django_logger.py +2 -2
  191. django_cfg/modules/django_ngrok.py +2 -2
  192. django_cfg/modules/django_tasks.py +68 -3
  193. django_cfg/modules/django_telegram.py +3 -3
  194. django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
  195. django_cfg/modules/django_twilio/service.py +2 -2
  196. django_cfg/modules/django_twilio/simple_service.py +2 -2
  197. django_cfg/modules/django_twilio/templates/guide.md +266 -0
  198. django_cfg/modules/django_twilio/twilio_service.py +2 -2
  199. django_cfg/modules/django_unfold/__init__.py +69 -0
  200. django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
  201. django_cfg/modules/django_unfold/dashboard.py +278 -0
  202. django_cfg/modules/django_unfold/icons/README.md +145 -0
  203. django_cfg/modules/django_unfold/icons/__init__.py +12 -0
  204. django_cfg/modules/django_unfold/icons/constants.py +2851 -0
  205. django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
  206. django_cfg/modules/django_unfold/models/__init__.py +42 -0
  207. django_cfg/modules/django_unfold/models/config.py +601 -0
  208. django_cfg/modules/django_unfold/models/dashboard.py +206 -0
  209. django_cfg/modules/django_unfold/models/dropdown.py +40 -0
  210. django_cfg/modules/django_unfold/models/navigation.py +73 -0
  211. django_cfg/modules/django_unfold/models/tabs.py +25 -0
  212. django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
  213. django_cfg/modules/django_unfold/utils.py +140 -0
  214. django_cfg/registry/__init__.py +23 -0
  215. django_cfg/registry/core.py +61 -0
  216. django_cfg/registry/exceptions.py +11 -0
  217. django_cfg/registry/modules.py +12 -0
  218. django_cfg/registry/services.py +26 -0
  219. django_cfg/registry/third_party.py +52 -0
  220. django_cfg/routing/__init__.py +19 -0
  221. django_cfg/routing/callbacks.py +198 -0
  222. django_cfg/routing/routers.py +48 -0
  223. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
  224. django_cfg/templatetags/__init__.py +0 -0
  225. django_cfg/templatetags/django_cfg.py +33 -0
  226. django_cfg/urls.py +33 -0
  227. django_cfg/utils/path_resolution.py +1 -1
  228. django_cfg/utils/smart_defaults.py +7 -61
  229. django_cfg/utils/toolkit.py +663 -0
  230. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/METADATA +83 -86
  231. django_cfg-1.2.0.dist-info/RECORD +441 -0
  232. django_cfg/apps/tasks/@docs/README.md +0 -195
  233. django_cfg/archive/django_sample.zip +0 -0
  234. django_cfg/models/unfold.py +0 -271
  235. django_cfg/modules/unfold/__init__.py +0 -29
  236. django_cfg/modules/unfold/dashboard.py +0 -318
  237. django_cfg/pyproject.toml +0 -370
  238. django_cfg/routers.py +0 -83
  239. django_cfg-1.1.81.dist-info/RECORD +0 -278
  240. /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
  241. /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
  242. /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
  243. /django_cfg/{version_check.py → utils/version_check.py} +0 -0
  244. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/WHEEL +0 -0
  245. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/entry_points.txt +0 -0
  246. {django_cfg-1.1.81.dist-info → django_cfg-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,813 @@
1
+ """
2
+ Mixin for automatic ExternalData integration.
3
+
4
+ This mixin provides automatic integration with knowbase ExternalData system:
5
+ - Adds external_source_id field automatically
6
+ - Tracks model changes and updates vectorization
7
+ - Provides simple configuration interface
8
+ - Handles creation, updates, and deletion automatically
9
+
10
+ Usage:
11
+ class MyModel(ExternalDataMixin, models.Model):
12
+ name = models.CharField(max_length=100)
13
+ description = models.TextField()
14
+
15
+ class Meta:
16
+ # Standard Django Meta options...
17
+
18
+ class ExternalDataMeta:
19
+ # Required: fields to watch for changes
20
+ watch_fields = ['name', 'description']
21
+
22
+ # Optional: similarity threshold (default: 0.5)
23
+ similarity_threshold = 0.4
24
+
25
+ # Optional: source type (default: ExternalDataType.MODEL)
26
+ source_type = ExternalDataType.CUSTOM
27
+
28
+ # Optional: enable/disable auto-sync (default: True)
29
+ auto_sync = True
30
+
31
+ # Optional: make public (default: False)
32
+ is_public = False
33
+
34
+ # Required: content generation method
35
+ def get_external_content(self):
36
+ return f"# {self.name}\n\n{self.description}"
37
+
38
+ # Optional: custom title (default: str(instance))
39
+ def get_external_title(self):
40
+ return f"My Model: {self.name}"
41
+
42
+ # Optional: custom description (default: auto-generated)
43
+ def get_external_description(self):
44
+ return f"Information about {self.name}"
45
+
46
+ # Optional: metadata (default: basic model info)
47
+ def get_external_metadata(self):
48
+ return {
49
+ 'model_type': 'my_model',
50
+ 'model_id': str(self.id),
51
+ 'name': self.name,
52
+ }
53
+
54
+ # Optional: tags (default: [model_name.lower()])
55
+ def get_external_tags(self):
56
+ return ['my_model', self.name.lower()]
57
+ """
58
+
59
+ import logging
60
+ import hashlib
61
+ from typing import Optional, List, Dict, Any, Type
62
+ from django.db import models
63
+ from django.db.models.signals import post_save, post_delete
64
+ from django.dispatch import receiver
65
+ from django.contrib.contenttypes.models import ContentType
66
+
67
+ from ..models.external_data import ExternalData, ExternalDataType, ExternalDataStatus
68
+ from .creator import ExternalDataCreator
69
+ from .config import ExternalDataConfig
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+
74
+ class ExternalDataMixin(models.Model):
75
+ """
76
+ Mixin that automatically integrates models with knowbase ExternalData system.
77
+
78
+ Provides:
79
+ - Automatic external_source_id field
80
+ - Change tracking and vectorization
81
+ - Simple configuration interface
82
+ - Automatic cleanup on deletion
83
+ """
84
+
85
+ # Automatically added field for linking to ExternalData
86
+ external_source_id = models.UUIDField(
87
+ null=True,
88
+ blank=True,
89
+ db_index=True,
90
+ help_text="UUID of the linked ExternalData object in knowbase",
91
+ verbose_name="External Source ID"
92
+ )
93
+
94
+ # Track content hash for change detection
95
+ _external_content_hash = models.CharField(
96
+ max_length=64,
97
+ blank=True,
98
+ help_text="SHA256 hash of content for change detection",
99
+ verbose_name="Content Hash"
100
+ )
101
+
102
+ class Meta:
103
+ abstract = True
104
+
105
+ def __init_subclass__(cls, **kwargs):
106
+ """Register signal handlers for each subclass."""
107
+ super().__init_subclass__(**kwargs)
108
+
109
+ # Register signals for this specific model class
110
+ post_save.connect(
111
+ cls._external_data_post_save_handler,
112
+ sender=cls,
113
+ dispatch_uid=f"external_data_mixin_{cls.__name__}"
114
+ )
115
+
116
+ post_delete.connect(
117
+ cls._external_data_post_delete_handler,
118
+ sender=cls,
119
+ dispatch_uid=f"external_data_mixin_delete_{cls.__name__}"
120
+ )
121
+
122
+ @classmethod
123
+ def _external_data_post_save_handler(cls, sender, instance, created, **kwargs):
124
+ """Handle post_save signal for ExternalData integration."""
125
+ try:
126
+ meta_config = cls._get_external_data_meta()
127
+ if not meta_config or not meta_config.get('auto_sync', True):
128
+ return
129
+
130
+ # Check if we should process this save (only if watched fields changed)
131
+ if not created and not cls._should_update_external_data(instance, kwargs):
132
+ logger.debug(f"📊 No relevant field changes for {cls.__name__}: {instance}")
133
+ return
134
+
135
+ # Check if content changed
136
+ current_content = cls._get_content_for_instance(instance)
137
+ current_hash = cls._calculate_content_hash(current_content)
138
+
139
+ if created:
140
+ # New instance - create ExternalData
141
+ logger.info(f"🔗 Creating ExternalData for new {cls.__name__}: {instance}")
142
+ instance._external_content_hash = current_hash
143
+ instance.save(update_fields=['_external_content_hash'])
144
+ cls._create_external_data(instance)
145
+
146
+ elif instance._external_content_hash != current_hash:
147
+ # Content changed - update ExternalData
148
+ logger.info(f"🔮 Content changed for {cls.__name__}: {instance}, updating ExternalData")
149
+ instance._external_content_hash = current_hash
150
+ instance.save(update_fields=['_external_content_hash'])
151
+
152
+ if instance.external_source_id:
153
+ cls._update_external_data(instance)
154
+ else:
155
+ cls._create_external_data(instance)
156
+ else:
157
+ logger.debug(f"📊 No content changes for {cls.__name__}: {instance}")
158
+
159
+ except Exception as e:
160
+ logger.error(f"❌ Error in ExternalData post_save handler for {cls.__name__}: {e}")
161
+
162
+ @classmethod
163
+ def _external_data_post_delete_handler(cls, sender, instance, **kwargs):
164
+ """Handle post_delete signal for ExternalData cleanup."""
165
+ try:
166
+ if instance.external_source_id:
167
+ logger.info(f"🗑️ Cleaning up ExternalData for deleted {cls.__name__}: {instance}")
168
+ ExternalData.objects.filter(id=instance.external_source_id).delete()
169
+ except Exception as e:
170
+ logger.error(f"❌ Error cleaning up ExternalData for {cls.__name__}: {e}")
171
+
172
+ @classmethod
173
+ def _get_external_data_meta(cls) -> Dict[str, Any]:
174
+ """Get ExternalDataMeta configuration from the model or auto-generate smart defaults."""
175
+ config = {}
176
+
177
+ # If ExternalDataMeta exists, use it
178
+ if hasattr(cls, 'ExternalDataMeta'):
179
+ meta_class = cls.ExternalDataMeta
180
+ # Extract configuration from ExternalDataMeta
181
+ for attr in dir(meta_class):
182
+ if not attr.startswith('_'):
183
+ value = getattr(meta_class, attr)
184
+ if not callable(value): # Only properties, not methods
185
+ config[attr] = value
186
+
187
+ # Smart defaults based on model analysis
188
+ if 'watch_fields' not in config:
189
+ config['watch_fields'] = cls._auto_detect_watch_fields()
190
+
191
+ if 'similarity_threshold' not in config:
192
+ config['similarity_threshold'] = 0.5 # Balanced default
193
+
194
+ if 'source_type' not in config:
195
+ from ..models.external_data import ExternalDataType
196
+ config['source_type'] = ExternalDataType.MODEL # Smart default
197
+
198
+ if 'auto_sync' not in config:
199
+ config['auto_sync'] = True # Enable by default
200
+
201
+ if 'is_public' not in config:
202
+ config['is_public'] = False # Private by default for security
203
+
204
+ return config
205
+
206
+ @classmethod
207
+ def _should_update_external_data(cls, instance, save_kwargs) -> bool:
208
+ """Check if we should update ExternalData based on changed fields."""
209
+ meta_config = cls._get_external_data_meta()
210
+ if not meta_config:
211
+ return True # No config = update always
212
+
213
+ watch_fields = meta_config.get('watch_fields', [])
214
+ if not watch_fields:
215
+ return True # No watch fields = update always
216
+
217
+ # Check if update_fields was used in save()
218
+ update_fields = save_kwargs.get('update_fields')
219
+ if update_fields is not None:
220
+ # Only update if any watched field was updated
221
+ return any(field in update_fields for field in watch_fields)
222
+
223
+ # If no update_fields specified, assume all fields might have changed
224
+ return True
225
+
226
+ @classmethod
227
+ def _get_content_for_instance(cls, instance) -> str:
228
+ """Get content string for the instance."""
229
+ if hasattr(instance, 'get_external_content'):
230
+ try:
231
+ return str(instance.get_external_content())
232
+ except Exception as e:
233
+ logger.warning(f"Error calling get_external_content on {cls.__name__}: {e}")
234
+
235
+ # Smart auto-generation based on model fields
236
+ return cls._auto_generate_content(instance)
237
+
238
+ @classmethod
239
+ def _get_title_for_instance(cls, instance) -> str:
240
+ """Get title for the instance."""
241
+ if hasattr(instance, 'get_external_title'):
242
+ try:
243
+ return str(instance.get_external_title())
244
+ except Exception as e:
245
+ logger.warning(f"Error calling get_external_title on {cls.__name__}: {e}")
246
+
247
+ # Smart auto-generation based on model fields
248
+ return cls._auto_generate_title(instance)
249
+
250
+ @classmethod
251
+ def _get_description_for_instance(cls, instance) -> str:
252
+ """Get description for the instance."""
253
+ if hasattr(instance, 'get_external_description'):
254
+ try:
255
+ return str(instance.get_external_description())
256
+ except Exception as e:
257
+ logger.warning(f"Error calling get_external_description on {cls.__name__}: {e}")
258
+
259
+ # Smart auto-generation based on model fields
260
+ return cls._auto_generate_description(instance)
261
+
262
+ @classmethod
263
+ def _get_tags_for_instance(cls, instance) -> List[str]:
264
+ """Get tags for the instance."""
265
+ if hasattr(instance, 'get_external_tags'):
266
+ try:
267
+ tags = instance.get_external_tags()
268
+ if isinstance(tags, (list, tuple)):
269
+ return list(tags)
270
+ return [str(tags)]
271
+ except Exception as e:
272
+ logger.warning(f"Error calling get_external_tags on {cls.__name__}: {e}")
273
+
274
+ # Smart auto-generation based on model fields
275
+ return cls._auto_generate_tags(instance)
276
+
277
+ @classmethod
278
+ def _calculate_content_hash(cls, content: str) -> str:
279
+ """Calculate SHA256 hash of content."""
280
+ return hashlib.sha256(content.encode('utf-8')).hexdigest()
281
+
282
+ @classmethod
283
+ def _create_external_data(cls, instance):
284
+ """Create ExternalData for the instance."""
285
+ try:
286
+ meta_config = cls._get_external_data_meta()
287
+ if not meta_config:
288
+ logger.warning(f"No ExternalDataMeta found for {cls.__name__}")
289
+ return
290
+
291
+ # Get user (try to find from instance or use superuser)
292
+ user = cls._get_user_for_instance(instance)
293
+
294
+ # Build ExternalDataConfig
295
+ external_config = ExternalDataConfig(
296
+ title=cls._get_title_for_instance(instance),
297
+ description=cls._get_description_for_instance(instance),
298
+ source_type=meta_config.get('source_type', ExternalDataType.MODEL),
299
+ source_identifier=f"{cls._meta.label_lower}_{instance.pk}",
300
+ content=cls._get_content_for_instance(instance),
301
+ similarity_threshold=meta_config.get('similarity_threshold', 0.5),
302
+ is_active=True,
303
+ is_public=meta_config.get('is_public', False),
304
+ metadata=cls._build_metadata(instance, meta_config),
305
+ tags=cls._get_tags_for_instance(instance),
306
+ source_config={
307
+ 'model': cls._meta.label_lower,
308
+ 'pk': str(instance.pk),
309
+ 'auto_sync': meta_config.get('auto_sync', True),
310
+ 'watch_fields': meta_config.get('watch_fields', []),
311
+ }
312
+ )
313
+
314
+ # Create ExternalData
315
+ creator = ExternalDataCreator(user)
316
+ result = creator.create_from_config(external_config)
317
+
318
+ if result['success']:
319
+ external_data = result['external_data']
320
+ instance.external_source_id = external_data.id
321
+ instance.save(update_fields=['external_source_id'])
322
+ logger.info(f"✅ Created ExternalData {external_data.id} for {cls.__name__}: {instance}")
323
+ else:
324
+ logger.error(f"❌ Failed to create ExternalData for {cls.__name__}: {result.get('error')}")
325
+
326
+ except Exception as e:
327
+ logger.error(f"❌ Error creating ExternalData for {cls.__name__}: {e}")
328
+
329
+ @classmethod
330
+ def _update_external_data(cls, instance):
331
+ """Update existing ExternalData for the instance."""
332
+ try:
333
+ if not instance.external_source_id:
334
+ return
335
+
336
+ external_data = ExternalData.objects.get(id=instance.external_source_id)
337
+ meta_config = cls._get_external_data_meta() or {}
338
+
339
+ # Update fields using the same methods as creation
340
+ external_data.title = cls._get_title_for_instance(instance)
341
+ external_data.description = cls._get_description_for_instance(instance)
342
+ external_data.content = cls._get_content_for_instance(instance)
343
+ external_data.metadata = cls._build_metadata(instance, meta_config)
344
+ external_data.tags = cls._get_tags_for_instance(instance)
345
+ external_data.similarity_threshold = meta_config.get('similarity_threshold', 0.5)
346
+ external_data.status = ExternalDataStatus.PENDING # Mark for reprocessing
347
+
348
+ external_data.save()
349
+ logger.info(f"✅ Updated ExternalData {external_data.id} for {cls.__name__}: {instance}")
350
+
351
+ except ExternalData.DoesNotExist:
352
+ logger.warning(f"ExternalData {instance.external_source_id} not found, creating new one")
353
+ cls._create_external_data(instance)
354
+ except Exception as e:
355
+ logger.error(f"❌ Error updating ExternalData for {cls.__name__}: {e}")
356
+
357
+ @classmethod
358
+ def _build_metadata(cls, instance, config: Dict[str, Any]) -> Dict[str, Any]:
359
+ """Build metadata dictionary for ExternalData."""
360
+ metadata = {
361
+ 'model': cls._meta.label_lower,
362
+ 'model_name': cls.__name__,
363
+ 'pk': str(instance.pk),
364
+ 'app_label': cls._meta.app_label,
365
+ 'created_at': getattr(instance, 'created_at', None),
366
+ 'updated_at': getattr(instance, 'updated_at', None),
367
+ }
368
+
369
+ # Add custom metadata if method exists
370
+ if hasattr(instance, 'get_external_metadata'):
371
+ try:
372
+ custom_metadata = instance.get_external_metadata()
373
+ if isinstance(custom_metadata, dict):
374
+ metadata.update(custom_metadata)
375
+ except Exception as e:
376
+ logger.warning(f"Error calling get_external_metadata on {cls.__name__}: {e}")
377
+
378
+ # Convert datetime objects to strings
379
+ for key, value in metadata.items():
380
+ if hasattr(value, 'isoformat'):
381
+ metadata[key] = value.isoformat()
382
+
383
+ return metadata
384
+
385
+ @classmethod
386
+ def _get_user_for_instance(cls, instance):
387
+ """Get user for ExternalData ownership."""
388
+ # Try to get user from instance
389
+ if hasattr(instance, 'user'):
390
+ return instance.user
391
+ if hasattr(instance, 'created_by'):
392
+ return instance.created_by
393
+ if hasattr(instance, 'owner'):
394
+ return instance.owner
395
+
396
+ # Fallback to superuser
397
+ from django.contrib.auth import get_user_model
398
+ User = get_user_model()
399
+ superuser = User.objects.filter(is_superuser=True).first()
400
+ if superuser:
401
+ return superuser
402
+
403
+ raise ValueError("No user found for ExternalData ownership")
404
+
405
+ def regenerate_external_data(self):
406
+ """Manually regenerate ExternalData for this instance."""
407
+ if self.external_source_id:
408
+ self._update_external_data(self)
409
+ else:
410
+ self._create_external_data(self)
411
+
412
+ def create_external_data(self, user=None):
413
+ """Create ExternalData for this instance if it doesn't exist."""
414
+ if self.external_source_id:
415
+ return {
416
+ 'success': False,
417
+ 'error': f'External data already exists: {self.external_source_id}',
418
+ 'external_data': None
419
+ }
420
+
421
+ try:
422
+ self._create_external_data(self)
423
+ if self.external_source_id:
424
+ return {
425
+ 'success': True,
426
+ 'message': f'External data created for {self}',
427
+ 'external_data': self.external_source_id
428
+ }
429
+ else:
430
+ return {
431
+ 'success': False,
432
+ 'error': f'Failed to create external data for {self}',
433
+ 'external_data': None
434
+ }
435
+ except Exception as e:
436
+ return {
437
+ 'success': False,
438
+ 'error': f'Error creating external data: {str(e)}',
439
+ 'external_data': None
440
+ }
441
+
442
+ def delete_external_data(self):
443
+ """Manually delete ExternalData for this instance."""
444
+ if self.external_source_id:
445
+ try:
446
+ ExternalData.objects.filter(id=self.external_source_id).delete()
447
+ self.external_source_id = None
448
+ self.save(update_fields=['external_source_id'])
449
+ logger.info(f"🗑️ Deleted ExternalData for {self.__class__.__name__}: {self}")
450
+ except Exception as e:
451
+ logger.error(f"❌ Error deleting ExternalData: {e}")
452
+
453
+ @property
454
+ def has_external_data(self) -> bool:
455
+ """Check if this instance has linked ExternalData."""
456
+ return bool(self.external_source_id)
457
+
458
+ @property
459
+ def external_data_status(self) -> Optional[str]:
460
+ """Get status of linked ExternalData."""
461
+ if not self.external_source_id:
462
+ return None
463
+
464
+ try:
465
+ external_data = ExternalData.objects.get(id=self.external_source_id)
466
+ return external_data.status
467
+ except ExternalData.DoesNotExist:
468
+ return None
469
+
470
+ # ==========================================
471
+ # SMART AUTO-GENERATION METHODS
472
+ # ==========================================
473
+
474
+ @classmethod
475
+ def _auto_generate_title(cls, instance) -> str:
476
+ """Auto-generate title based on model fields."""
477
+ # Try common title fields first
478
+ title_fields = ['title', 'name', 'full_name', 'display_name', 'label']
479
+
480
+ for field_name in title_fields:
481
+ if hasattr(instance, field_name):
482
+ value = getattr(instance, field_name, None)
483
+ if value and str(value).strip():
484
+ # Add model context for clarity
485
+ model_name = cls._meta.verbose_name or cls.__name__
486
+ return f"{model_name}: {value}"
487
+
488
+ # Fallback: use string representation with model name
489
+ model_name = cls._meta.verbose_name or cls.__name__
490
+ return f"{model_name}: {instance}"
491
+
492
+ @classmethod
493
+ def _auto_generate_description(cls, instance) -> str:
494
+ """Auto-generate description based on model fields."""
495
+ model_name = cls._meta.verbose_name or cls.__name__
496
+
497
+ # Try common description fields
498
+ desc_fields = ['description', 'summary', 'about', 'details', 'info']
499
+ for field_name in desc_fields:
500
+ if hasattr(instance, field_name):
501
+ value = getattr(instance, field_name, None)
502
+ if value and str(value).strip():
503
+ return f"{model_name} information: {value}"
504
+
505
+ # Build description from key fields
506
+ key_info = []
507
+
508
+ # Add primary identifier
509
+ if hasattr(instance, 'name') and instance.name:
510
+ key_info.append(f"Name: {instance.name}")
511
+ elif hasattr(instance, 'title') and instance.title:
512
+ key_info.append(f"Title: {instance.title}")
513
+
514
+ # Add status if available
515
+ if hasattr(instance, 'is_active'):
516
+ status = "Active" if instance.is_active else "Inactive"
517
+ key_info.append(f"Status: {status}")
518
+
519
+ # Add creation date if available
520
+ if hasattr(instance, 'created_at') and instance.created_at:
521
+ key_info.append(f"Created: {instance.created_at.strftime('%Y-%m-%d')}")
522
+
523
+ if key_info:
524
+ return f"Comprehensive information about this {model_name.lower()}. {', '.join(key_info)}."
525
+
526
+ return f"Auto-generated information from {model_name} model."
527
+
528
+ @classmethod
529
+ def _auto_generate_tags(cls, instance) -> List[str]:
530
+ """Auto-generate tags based on model fields and metadata."""
531
+ tags = []
532
+
533
+ # Add model-based tags
534
+ tags.append(cls.__name__.lower())
535
+ if cls._meta.verbose_name:
536
+ tags.append(cls._meta.verbose_name.lower().replace(' ', '_'))
537
+
538
+ # Add app label
539
+ tags.append(cls._meta.app_label)
540
+
541
+ # Add field-based tags
542
+ tag_fields = ['category', 'type', 'kind', 'status', 'brand', 'model']
543
+ for field_name in tag_fields:
544
+ if hasattr(instance, field_name):
545
+ value = getattr(instance, field_name, None)
546
+ if value:
547
+ # Handle foreign key relationships
548
+ if hasattr(value, 'name'):
549
+ tags.append(str(value.name).lower())
550
+ elif hasattr(value, 'code'):
551
+ tags.append(str(value.code).lower())
552
+ else:
553
+ tags.append(str(value).lower())
554
+
555
+ # Add boolean field tags
556
+ bool_fields = ['is_active', 'is_public', 'is_featured', 'is_published']
557
+ for field_name in bool_fields:
558
+ if hasattr(instance, field_name):
559
+ value = getattr(instance, field_name, None)
560
+ if value is True:
561
+ tags.append(field_name.replace('is_', ''))
562
+
563
+ # Clean and deduplicate tags
564
+ clean_tags = []
565
+ for tag in tags:
566
+ clean_tag = str(tag).lower().strip().replace(' ', '_')
567
+ if clean_tag and clean_tag not in clean_tags:
568
+ clean_tags.append(clean_tag)
569
+
570
+ return clean_tags[:10] # Limit to 10 tags
571
+
572
+ @classmethod
573
+ def _auto_generate_content(cls, instance) -> str:
574
+ """Auto-generate comprehensive content based on model fields."""
575
+ content_parts = []
576
+
577
+ # Header with title
578
+ title = cls._auto_generate_title(instance)
579
+ content_parts.append(f"# {title}")
580
+ content_parts.append("")
581
+
582
+ # Basic Information section
583
+ content_parts.append("## Basic Information")
584
+
585
+ # Add key fields
586
+ key_fields = cls._get_content_fields(instance)
587
+ for field_name, field_value, field_label in key_fields:
588
+ if field_value is not None and str(field_value).strip():
589
+ content_parts.append(f"- **{field_label}**: {field_value}")
590
+
591
+ content_parts.append("")
592
+
593
+ # Add relationships section if any
594
+ relationships = cls._get_relationship_info(instance)
595
+ if relationships:
596
+ content_parts.append("## Related Information")
597
+ for rel_name, rel_info in relationships.items():
598
+ content_parts.append(f"- **{rel_name}**: {rel_info}")
599
+ content_parts.append("")
600
+
601
+ # Add statistics if available
602
+ stats = cls._get_statistics_info(instance)
603
+ if stats:
604
+ content_parts.append("## Statistics")
605
+ for stat_name, stat_value in stats.items():
606
+ content_parts.append(f"- **{stat_name}**: {stat_value}")
607
+ content_parts.append("")
608
+
609
+ # Add metadata section
610
+ content_parts.append("## Technical Information")
611
+ content_parts.append(f"This data is automatically synchronized from the {cls.__name__} model using ExternalDataMixin.")
612
+ content_parts.append(f"")
613
+ content_parts.append(f"**Model**: {cls._meta.label}")
614
+ content_parts.append(f"**ID**: {instance.pk}")
615
+ if hasattr(instance, 'created_at') and instance.created_at:
616
+ content_parts.append(f"**Created**: {instance.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
617
+ if hasattr(instance, 'updated_at') and instance.updated_at:
618
+ content_parts.append(f"**Updated**: {instance.updated_at.strftime('%Y-%m-%d %H:%M:%S')}")
619
+
620
+ return "\n".join(content_parts)
621
+
622
+ @classmethod
623
+ def _get_content_fields(cls, instance) -> List[tuple]:
624
+ """Get fields to include in content generation."""
625
+ fields_info = []
626
+
627
+ # Define field priority and labels
628
+ priority_fields = {
629
+ 'name': 'Name',
630
+ 'title': 'Title',
631
+ 'code': 'Code',
632
+ 'description': 'Description',
633
+ 'summary': 'Summary',
634
+ 'body_type': 'Body Type',
635
+ 'segment': 'Segment',
636
+ 'category': 'Category',
637
+ 'type': 'Type',
638
+ 'status': 'Status',
639
+ 'is_active': 'Active',
640
+ 'is_public': 'Public',
641
+ 'price': 'Price',
642
+ 'year': 'Year',
643
+ 'fuel_type': 'Fuel Type',
644
+ }
645
+
646
+ # Add priority fields first
647
+ for field_name, field_label in priority_fields.items():
648
+ if hasattr(instance, field_name):
649
+ value = getattr(instance, field_name, None)
650
+ if value is not None:
651
+ # Format boolean fields
652
+ if isinstance(value, bool):
653
+ value = "Yes" if value else "No"
654
+ # Format choice fields
655
+ elif hasattr(instance, f'get_{field_name}_display'):
656
+ display_value = getattr(instance, f'get_{field_name}_display')()
657
+ if display_value:
658
+ value = display_value
659
+ # Format foreign key relationships
660
+ elif hasattr(value, '__str__'):
661
+ value = str(value)
662
+
663
+ fields_info.append((field_name, value, field_label))
664
+
665
+ return fields_info
666
+
667
+ @classmethod
668
+ def _get_relationship_info(cls, instance) -> Dict[str, str]:
669
+ """Get relationship information for content."""
670
+ relationships = {}
671
+
672
+ # Common relationship field names
673
+ rel_fields = ['brand', 'category', 'parent', 'owner', 'user', 'created_by']
674
+
675
+ for field_name in rel_fields:
676
+ if hasattr(instance, field_name):
677
+ value = getattr(instance, field_name, None)
678
+ if value:
679
+ relationships[field_name.replace('_', ' ').title()] = str(value)
680
+
681
+ return relationships
682
+
683
+ @classmethod
684
+ def _get_statistics_info(cls, instance) -> Dict[str, Any]:
685
+ """Get statistics information for content."""
686
+ stats = {}
687
+
688
+ # Common statistics field names
689
+ stat_fields = ['total_vehicles', 'total_models', 'total_items', 'count', 'views', 'likes']
690
+
691
+ for field_name in stat_fields:
692
+ if hasattr(instance, field_name):
693
+ value = getattr(instance, field_name, None)
694
+ if value is not None and (isinstance(value, (int, float)) and value > 0):
695
+ label = field_name.replace('_', ' ').title()
696
+ if isinstance(value, float):
697
+ stats[label] = f"{value:,.2f}"
698
+ else:
699
+ stats[label] = f"{value:,}"
700
+
701
+ return stats
702
+
703
+ @classmethod
704
+ def _auto_detect_watch_fields(cls) -> List[str]:
705
+ """Auto-detect important fields to watch for changes."""
706
+ watch_fields = []
707
+
708
+ # Get all model fields
709
+ for field in cls._meta.get_fields():
710
+ if hasattr(field, 'name') and not field.name.startswith('_'):
711
+ field_name = field.name
712
+
713
+ # Skip auto-generated and system fields
714
+ skip_fields = {
715
+ 'id', 'pk', 'created_at', 'updated_at', 'external_source_id',
716
+ '_external_content_hash', 'slug'
717
+ }
718
+ if field_name in skip_fields:
719
+ continue
720
+
721
+ # Skip reverse foreign keys and many-to-many
722
+ if hasattr(field, 'related_model') and field.many_to_many:
723
+ continue
724
+ if hasattr(field, 'remote_field') and field.remote_field and hasattr(field.remote_field, 'related_name'):
725
+ continue
726
+
727
+ # Include important field types
728
+ if hasattr(field, '__class__'):
729
+ field_type = field.__class__.__name__
730
+ important_types = {
731
+ 'CharField', 'TextField', 'BooleanField', 'IntegerField',
732
+ 'PositiveIntegerField', 'ForeignKey', 'DecimalField', 'FloatField'
733
+ }
734
+ if field_type in important_types:
735
+ watch_fields.append(field_name)
736
+
737
+ # If no fields detected, watch all non-system fields
738
+ if not watch_fields:
739
+ for field in cls._meta.get_fields():
740
+ if hasattr(field, 'name') and not field.name.startswith('_') and field.name not in {'id', 'pk'}:
741
+ watch_fields.append(field.name)
742
+
743
+ return watch_fields[:10] # Limit to prevent too many triggers
744
+
745
+ # ==========================================
746
+ # MANAGER-LEVEL METHODS (CLASS METHODS)
747
+ # ==========================================
748
+
749
+ @classmethod
750
+ def with_external_data(cls):
751
+ """Return queryset of instances that have external data."""
752
+ return cls.objects.filter(external_source_id__isnull=False)
753
+
754
+ @classmethod
755
+ def without_external_data(cls):
756
+ """Return queryset of instances that don't have external data."""
757
+ return cls.objects.filter(external_source_id__isnull=True)
758
+
759
+ @classmethod
760
+ def sync_all_external_data(cls, limit=None):
761
+ """Sync external data for all instances that have it."""
762
+ instances_with_data = cls.with_external_data()
763
+
764
+ if limit:
765
+ instances_with_data = instances_with_data[:limit]
766
+
767
+ results = {
768
+ 'total_processed': 0,
769
+ 'successful_updates': 0,
770
+ 'failed_updates': 0,
771
+ 'errors': []
772
+ }
773
+
774
+ for instance in instances_with_data:
775
+ try:
776
+ instance.regenerate_external_data()
777
+ results['successful_updates'] += 1
778
+ results['total_processed'] += 1
779
+ except Exception as e:
780
+ results['failed_updates'] += 1
781
+ results['errors'].append(f"{instance}: {str(e)}")
782
+
783
+ return results
784
+
785
+ @classmethod
786
+ def create_external_data_for_all(cls, limit=None):
787
+ """Create external data for all instances that don't have it."""
788
+ instances_without_data = cls.without_external_data()
789
+
790
+ if limit:
791
+ instances_without_data = instances_without_data[:limit]
792
+
793
+ results = {
794
+ 'total_processed': 0,
795
+ 'successful_creates': 0,
796
+ 'failed_creates': 0,
797
+ 'errors': []
798
+ }
799
+
800
+ for instance in instances_without_data:
801
+ try:
802
+ result = instance.create_external_data()
803
+ if result['success']:
804
+ results['successful_creates'] += 1
805
+ else:
806
+ results['failed_creates'] += 1
807
+ results['errors'].append(f"{instance}: {result['error']}")
808
+ results['total_processed'] += 1
809
+ except Exception as e:
810
+ results['failed_creates'] += 1
811
+ results['errors'].append(f"{instance}: {str(e)}")
812
+
813
+ return results