django-cfg 1.1.82__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +20 -448
- django_cfg/apps/accounts/README.md +3 -3
- django_cfg/apps/accounts/admin/__init__.py +0 -2
- django_cfg/apps/accounts/admin/activity.py +2 -9
- django_cfg/apps/accounts/admin/filters.py +0 -42
- django_cfg/apps/accounts/admin/inlines.py +8 -8
- django_cfg/apps/accounts/admin/otp.py +5 -5
- django_cfg/apps/accounts/admin/registration_source.py +1 -8
- django_cfg/apps/accounts/admin/user.py +12 -20
- django_cfg/apps/accounts/managers/user_manager.py +2 -129
- django_cfg/apps/accounts/migrations/0006_remove_twilioresponse_otp_secret_and_more.py +46 -0
- django_cfg/apps/accounts/models.py +3 -123
- django_cfg/apps/accounts/serializers/otp.py +40 -44
- django_cfg/apps/accounts/serializers/profile.py +0 -2
- django_cfg/apps/accounts/services/otp_service.py +98 -186
- django_cfg/apps/accounts/signals.py +25 -15
- django_cfg/apps/accounts/utils/auth_email_service.py +84 -0
- django_cfg/apps/accounts/views/otp.py +35 -36
- django_cfg/apps/agents/README.md +129 -0
- django_cfg/apps/agents/__init__.py +68 -0
- django_cfg/apps/agents/admin/__init__.py +17 -0
- django_cfg/apps/agents/admin/execution_admin.py +460 -0
- django_cfg/apps/agents/admin/registry_admin.py +360 -0
- django_cfg/apps/agents/admin/toolsets_admin.py +482 -0
- django_cfg/apps/agents/apps.py +29 -0
- django_cfg/apps/agents/core/__init__.py +20 -0
- django_cfg/apps/agents/core/agent.py +281 -0
- django_cfg/apps/agents/core/dependencies.py +154 -0
- django_cfg/apps/agents/core/exceptions.py +66 -0
- django_cfg/apps/agents/core/models.py +106 -0
- django_cfg/apps/agents/core/orchestrator.py +391 -0
- django_cfg/apps/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- django_cfg/apps/agents/integration/__init__.py +14 -0
- django_cfg/apps/agents/integration/middleware.py +80 -0
- django_cfg/apps/agents/integration/registry.py +345 -0
- django_cfg/apps/agents/integration/signals.py +50 -0
- django_cfg/apps/agents/management/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/__init__.py +3 -0
- django_cfg/apps/agents/management/commands/create_agent.py +365 -0
- django_cfg/apps/agents/management/commands/orchestrator_status.py +191 -0
- django_cfg/apps/agents/managers/__init__.py +23 -0
- django_cfg/apps/agents/managers/execution.py +236 -0
- django_cfg/apps/agents/managers/registry.py +254 -0
- django_cfg/apps/agents/managers/toolsets.py +496 -0
- django_cfg/apps/agents/migrations/0001_initial.py +286 -0
- django_cfg/apps/agents/migrations/__init__.py +5 -0
- django_cfg/apps/agents/models/__init__.py +15 -0
- django_cfg/apps/agents/models/execution.py +215 -0
- django_cfg/apps/agents/models/registry.py +220 -0
- django_cfg/apps/agents/models/toolsets.py +305 -0
- django_cfg/apps/agents/patterns/__init__.py +24 -0
- django_cfg/apps/agents/patterns/content_agents.py +234 -0
- django_cfg/apps/agents/toolsets/__init__.py +15 -0
- django_cfg/apps/agents/toolsets/cache_toolset.py +285 -0
- django_cfg/apps/agents/toolsets/django_toolset.py +220 -0
- django_cfg/apps/agents/toolsets/file_toolset.py +324 -0
- django_cfg/apps/agents/toolsets/orm_toolset.py +319 -0
- django_cfg/apps/agents/urls.py +46 -0
- django_cfg/apps/knowbase/README.md +150 -0
- django_cfg/apps/knowbase/__init__.py +27 -0
- django_cfg/apps/knowbase/admin/__init__.py +23 -0
- django_cfg/apps/knowbase/admin/archive_admin.py +857 -0
- django_cfg/apps/knowbase/admin/chat_admin.py +386 -0
- django_cfg/apps/knowbase/admin/document_admin.py +650 -0
- django_cfg/apps/knowbase/admin/external_data_admin.py +685 -0
- django_cfg/apps/knowbase/apps.py +81 -0
- django_cfg/apps/knowbase/config/README.md +176 -0
- django_cfg/apps/knowbase/config/__init__.py +51 -0
- django_cfg/apps/knowbase/config/constance_fields.py +186 -0
- django_cfg/apps/knowbase/config/constance_settings.py +200 -0
- django_cfg/apps/knowbase/config/settings.py +450 -0
- django_cfg/apps/knowbase/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- django_cfg/apps/knowbase/management/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/__init__.py +0 -0
- django_cfg/apps/knowbase/management/commands/knowbase_stats.py +158 -0
- django_cfg/apps/knowbase/management/commands/setup_knowbase.py +59 -0
- django_cfg/apps/knowbase/managers/__init__.py +22 -0
- django_cfg/apps/knowbase/managers/archive.py +426 -0
- django_cfg/apps/knowbase/managers/base.py +32 -0
- django_cfg/apps/knowbase/managers/chat.py +141 -0
- django_cfg/apps/knowbase/managers/document.py +203 -0
- django_cfg/apps/knowbase/managers/external_data.py +471 -0
- django_cfg/apps/knowbase/migrations/0001_initial.py +427 -0
- django_cfg/apps/knowbase/migrations/0002_archiveitem_archiveitemchunk_documentarchive_and_more.py +434 -0
- django_cfg/apps/knowbase/migrations/__init__.py +5 -0
- django_cfg/apps/knowbase/mixins/__init__.py +15 -0
- django_cfg/apps/knowbase/mixins/config.py +108 -0
- django_cfg/apps/knowbase/mixins/creator.py +81 -0
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +199 -0
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +813 -0
- django_cfg/apps/knowbase/mixins/service.py +362 -0
- django_cfg/apps/knowbase/models/__init__.py +41 -0
- django_cfg/apps/knowbase/models/archive.py +599 -0
- django_cfg/apps/knowbase/models/base.py +58 -0
- django_cfg/apps/knowbase/models/chat.py +157 -0
- django_cfg/apps/knowbase/models/document.py +267 -0
- django_cfg/apps/knowbase/models/external_data.py +376 -0
- django_cfg/apps/knowbase/serializers/__init__.py +68 -0
- django_cfg/apps/knowbase/serializers/archive_serializers.py +386 -0
- django_cfg/apps/knowbase/serializers/chat_serializers.py +137 -0
- django_cfg/apps/knowbase/serializers/document_serializers.py +94 -0
- django_cfg/apps/knowbase/serializers/external_data_serializers.py +256 -0
- django_cfg/apps/knowbase/serializers/public_serializers.py +74 -0
- django_cfg/apps/knowbase/services/__init__.py +40 -0
- django_cfg/apps/knowbase/services/archive/__init__.py +42 -0
- django_cfg/apps/knowbase/services/archive/archive_service.py +541 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +791 -0
- django_cfg/apps/knowbase/services/archive/exceptions.py +52 -0
- django_cfg/apps/knowbase/services/archive/extraction_service.py +508 -0
- django_cfg/apps/knowbase/services/archive/vectorization_service.py +362 -0
- django_cfg/apps/knowbase/services/base.py +53 -0
- django_cfg/apps/knowbase/services/chat_service.py +239 -0
- django_cfg/apps/knowbase/services/document_service.py +144 -0
- django_cfg/apps/knowbase/services/embedding/__init__.py +43 -0
- django_cfg/apps/knowbase/services/embedding/async_processor.py +244 -0
- django_cfg/apps/knowbase/services/embedding/batch_processor.py +250 -0
- django_cfg/apps/knowbase/services/embedding/batch_result.py +61 -0
- django_cfg/apps/knowbase/services/embedding/models.py +229 -0
- django_cfg/apps/knowbase/services/embedding/processors.py +148 -0
- django_cfg/apps/knowbase/services/embedding/utils.py +176 -0
- django_cfg/apps/knowbase/services/prompt_builder.py +191 -0
- django_cfg/apps/knowbase/services/search_service.py +293 -0
- django_cfg/apps/knowbase/signals/__init__.py +21 -0
- django_cfg/apps/knowbase/signals/archive_signals.py +211 -0
- django_cfg/apps/knowbase/signals/chat_signals.py +37 -0
- django_cfg/apps/knowbase/signals/document_signals.py +143 -0
- django_cfg/apps/knowbase/signals/external_data_signals.py +157 -0
- django_cfg/apps/knowbase/tasks/__init__.py +39 -0
- django_cfg/apps/knowbase/tasks/archive_tasks.py +316 -0
- django_cfg/apps/knowbase/tasks/document_processing.py +341 -0
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +341 -0
- django_cfg/apps/knowbase/tasks/maintenance.py +195 -0
- django_cfg/apps/knowbase/urls.py +43 -0
- django_cfg/apps/knowbase/utils/__init__.py +12 -0
- django_cfg/apps/knowbase/utils/chunk_settings.py +261 -0
- django_cfg/apps/knowbase/utils/text_processing.py +375 -0
- django_cfg/apps/knowbase/utils/validation.py +99 -0
- django_cfg/apps/knowbase/views/__init__.py +28 -0
- django_cfg/apps/knowbase/views/archive_views.py +469 -0
- django_cfg/apps/knowbase/views/base.py +49 -0
- django_cfg/apps/knowbase/views/chat_views.py +181 -0
- django_cfg/apps/knowbase/views/document_views.py +183 -0
- django_cfg/apps/knowbase/views/public_views.py +129 -0
- django_cfg/apps/leads/admin.py +70 -0
- django_cfg/apps/newsletter/admin.py +234 -0
- django_cfg/apps/newsletter/admin_filters.py +124 -0
- django_cfg/apps/support/admin.py +196 -0
- django_cfg/apps/support/admin_filters.py +71 -0
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/apps/urls.py +5 -4
- django_cfg/cli/README.md +1 -1
- django_cfg/cli/commands/create_project.py +2 -2
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/config.py +44 -0
- django_cfg/core/config.py +29 -82
- django_cfg/core/environment.py +1 -1
- django_cfg/core/generation.py +19 -107
- django_cfg/{integration.py → core/integration.py} +18 -16
- django_cfg/core/validation.py +1 -1
- django_cfg/management/__init__.py +1 -1
- django_cfg/management/commands/__init__.py +1 -1
- django_cfg/management/commands/auto_generate.py +482 -0
- django_cfg/management/commands/migrator.py +19 -101
- django_cfg/management/commands/test_email.py +1 -1
- django_cfg/middleware/README.md +0 -158
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/api.py +145 -0
- django_cfg/models/base.py +287 -0
- django_cfg/models/cache.py +4 -4
- django_cfg/models/constance.py +25 -88
- django_cfg/models/database.py +9 -9
- django_cfg/models/drf.py +3 -36
- django_cfg/models/email.py +163 -0
- django_cfg/models/environment.py +276 -0
- django_cfg/models/limits.py +1 -1
- django_cfg/models/logging.py +366 -0
- django_cfg/models/revolution.py +41 -2
- django_cfg/models/security.py +125 -0
- django_cfg/models/services.py +1 -1
- django_cfg/modules/__init__.py +2 -56
- django_cfg/modules/base.py +78 -52
- django_cfg/modules/django_currency/service.py +2 -2
- django_cfg/modules/django_email.py +2 -2
- django_cfg/modules/django_health.py +267 -0
- django_cfg/modules/django_llm/llm/client.py +91 -19
- django_cfg/modules/django_llm/translator/translator.py +2 -2
- django_cfg/modules/django_logger.py +2 -2
- django_cfg/modules/django_ngrok.py +2 -2
- django_cfg/modules/django_tasks.py +68 -3
- django_cfg/modules/django_telegram.py +3 -3
- django_cfg/modules/django_twilio/sendgrid_service.py +2 -2
- django_cfg/modules/django_twilio/service.py +2 -2
- django_cfg/modules/django_twilio/simple_service.py +2 -2
- django_cfg/modules/django_twilio/twilio_service.py +2 -2
- django_cfg/modules/django_unfold/__init__.py +69 -0
- django_cfg/modules/{unfold → django_unfold}/callbacks.py +23 -22
- django_cfg/modules/django_unfold/dashboard.py +278 -0
- django_cfg/modules/django_unfold/icons/README.md +145 -0
- django_cfg/modules/django_unfold/icons/__init__.py +12 -0
- django_cfg/modules/django_unfold/icons/constants.py +2851 -0
- django_cfg/modules/django_unfold/icons/generate_icons.py +486 -0
- django_cfg/modules/django_unfold/models/__init__.py +42 -0
- django_cfg/modules/django_unfold/models/config.py +601 -0
- django_cfg/modules/django_unfold/models/dashboard.py +206 -0
- django_cfg/modules/django_unfold/models/dropdown.py +40 -0
- django_cfg/modules/django_unfold/models/navigation.py +73 -0
- django_cfg/modules/django_unfold/models/tabs.py +25 -0
- django_cfg/modules/{unfold → django_unfold}/system_monitor.py +2 -2
- django_cfg/modules/django_unfold/utils.py +140 -0
- django_cfg/registry/__init__.py +23 -0
- django_cfg/registry/core.py +61 -0
- django_cfg/registry/exceptions.py +11 -0
- django_cfg/registry/modules.py +12 -0
- django_cfg/registry/services.py +26 -0
- django_cfg/registry/third_party.py +52 -0
- django_cfg/routing/__init__.py +19 -0
- django_cfg/routing/callbacks.py +198 -0
- django_cfg/routing/routers.py +48 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +8 -9
- django_cfg/templatetags/__init__.py +0 -0
- django_cfg/templatetags/django_cfg.py +33 -0
- django_cfg/urls.py +33 -0
- django_cfg/utils/path_resolution.py +1 -1
- django_cfg/utils/smart_defaults.py +7 -61
- django_cfg/utils/toolkit.py +663 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/METADATA +83 -86
- django_cfg-1.2.1.dist-info/RECORD +441 -0
- django_cfg/archive/django_sample.zip +0 -0
- django_cfg/models/unfold.py +0 -271
- django_cfg/modules/unfold/__init__.py +0 -29
- django_cfg/modules/unfold/dashboard.py +0 -318
- django_cfg/pyproject.toml +0 -370
- django_cfg/routers.py +0 -83
- django_cfg-1.1.82.dist-info/RECORD +0 -278
- /django_cfg/{exceptions.py → core/exceptions.py} +0 -0
- /django_cfg/modules/{unfold → django_unfold}/models.py +0 -0
- /django_cfg/modules/{unfold → django_unfold}/tailwind.py +0 -0
- /django_cfg/{version_check.py → utils/version_check.py} +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.82.dist-info → django_cfg-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
"""
|
2
|
+
Core Django toolset with common Django operations.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import Dict, Any, List, Optional
|
7
|
+
from pydantic_ai.toolsets import AbstractToolset
|
8
|
+
from pydantic_ai import RunContext
|
9
|
+
|
10
|
+
from ..core.dependencies import DjangoDeps
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class DjangoToolset(AbstractToolset[DjangoDeps]):
|
16
|
+
"""
|
17
|
+
Core Django toolset providing common Django operations.
|
18
|
+
|
19
|
+
Includes tools for:
|
20
|
+
- User management
|
21
|
+
- Session handling
|
22
|
+
- Settings access
|
23
|
+
- Logging
|
24
|
+
"""
|
25
|
+
|
26
|
+
@property
|
27
|
+
def id(self) -> str:
|
28
|
+
return "django_core"
|
29
|
+
|
30
|
+
async def get_user_info(self, ctx: RunContext[DjangoDeps]) -> Dict[str, Any]:
|
31
|
+
"""Get current user information."""
|
32
|
+
user = ctx.deps.user
|
33
|
+
|
34
|
+
return {
|
35
|
+
'id': user.id,
|
36
|
+
'username': user.username,
|
37
|
+
'email': user.email,
|
38
|
+
'first_name': user.first_name,
|
39
|
+
'last_name': user.last_name,
|
40
|
+
'is_active': user.is_active,
|
41
|
+
'is_staff': user.is_staff,
|
42
|
+
'is_superuser': user.is_superuser,
|
43
|
+
'date_joined': user.date_joined.isoformat() if user.date_joined else None,
|
44
|
+
'last_login': user.last_login.isoformat() if user.last_login else None,
|
45
|
+
}
|
46
|
+
|
47
|
+
async def get_user_permissions(self, ctx: RunContext[DjangoDeps]) -> List[str]:
|
48
|
+
"""Get user permissions."""
|
49
|
+
user = ctx.deps.user
|
50
|
+
|
51
|
+
# Get all permissions
|
52
|
+
permissions = []
|
53
|
+
|
54
|
+
# Direct user permissions
|
55
|
+
user_perms = await user.user_permissions.aall()
|
56
|
+
async for perm in user_perms:
|
57
|
+
permissions.append(f"{perm.content_type.app_label}.{perm.codename}")
|
58
|
+
|
59
|
+
# Group permissions
|
60
|
+
groups = await user.groups.aall()
|
61
|
+
async for group in groups:
|
62
|
+
group_perms = await group.permissions.aall()
|
63
|
+
async for perm in group_perms:
|
64
|
+
perm_str = f"{perm.content_type.app_label}.{perm.codename}"
|
65
|
+
if perm_str not in permissions:
|
66
|
+
permissions.append(perm_str)
|
67
|
+
|
68
|
+
return sorted(permissions)
|
69
|
+
|
70
|
+
async def check_permission(self, ctx: RunContext[DjangoDeps], permission: str) -> bool:
|
71
|
+
"""Check if user has specific permission."""
|
72
|
+
user = ctx.deps.user
|
73
|
+
|
74
|
+
# Handle superuser
|
75
|
+
if user.is_superuser:
|
76
|
+
return True
|
77
|
+
|
78
|
+
# Check permission
|
79
|
+
return user.has_perm(permission)
|
80
|
+
|
81
|
+
async def get_session_data(self, ctx: RunContext[DjangoDeps], key: Optional[str] = None) -> Any:
|
82
|
+
"""Get session data."""
|
83
|
+
session_data = ctx.deps.session_data
|
84
|
+
|
85
|
+
if key:
|
86
|
+
return session_data.get(key)
|
87
|
+
|
88
|
+
return session_data
|
89
|
+
|
90
|
+
async def log_message(
|
91
|
+
self,
|
92
|
+
ctx: RunContext[DjangoDeps],
|
93
|
+
message: str,
|
94
|
+
level: str = "info",
|
95
|
+
extra_data: Optional[Dict[str, Any]] = None
|
96
|
+
) -> bool:
|
97
|
+
"""Log message with user context."""
|
98
|
+
user = ctx.deps.user
|
99
|
+
|
100
|
+
# Prepare log data
|
101
|
+
log_data = {
|
102
|
+
'user_id': user.id,
|
103
|
+
'username': user.username,
|
104
|
+
'message': message,
|
105
|
+
}
|
106
|
+
|
107
|
+
if extra_data:
|
108
|
+
log_data.update(extra_data)
|
109
|
+
|
110
|
+
# Log based on level
|
111
|
+
if level.lower() == 'debug':
|
112
|
+
logger.debug(message, extra=log_data)
|
113
|
+
elif level.lower() == 'info':
|
114
|
+
logger.info(message, extra=log_data)
|
115
|
+
elif level.lower() == 'warning':
|
116
|
+
logger.warning(message, extra=log_data)
|
117
|
+
elif level.lower() == 'error':
|
118
|
+
logger.error(message, extra=log_data)
|
119
|
+
elif level.lower() == 'critical':
|
120
|
+
logger.critical(message, extra=log_data)
|
121
|
+
else:
|
122
|
+
logger.info(message, extra=log_data)
|
123
|
+
|
124
|
+
return True
|
125
|
+
|
126
|
+
async def get_django_setting(self, ctx: RunContext[DjangoDeps], setting_name: str) -> Any:
|
127
|
+
"""Get Django setting value (safe settings only)."""
|
128
|
+
from django.conf import settings
|
129
|
+
|
130
|
+
# Whitelist of safe settings to expose
|
131
|
+
safe_settings = {
|
132
|
+
'DEBUG',
|
133
|
+
'TIME_ZONE',
|
134
|
+
'LANGUAGE_CODE',
|
135
|
+
'USE_TZ',
|
136
|
+
'USE_I18N',
|
137
|
+
'MEDIA_URL',
|
138
|
+
'STATIC_URL',
|
139
|
+
'DEFAULT_AUTO_FIELD',
|
140
|
+
}
|
141
|
+
|
142
|
+
if setting_name not in safe_settings:
|
143
|
+
raise ValueError(f"Setting '{setting_name}' is not in the safe settings list")
|
144
|
+
|
145
|
+
return getattr(settings, setting_name, None)
|
146
|
+
|
147
|
+
async def get_app_config(self, ctx: RunContext[DjangoDeps], app_label: str) -> Dict[str, Any]:
|
148
|
+
"""Get Django app configuration."""
|
149
|
+
from django.apps import apps
|
150
|
+
|
151
|
+
try:
|
152
|
+
app_config = apps.get_app_config(app_label)
|
153
|
+
|
154
|
+
return {
|
155
|
+
'name': app_config.name,
|
156
|
+
'label': app_config.label,
|
157
|
+
'verbose_name': app_config.verbose_name,
|
158
|
+
'path': str(app_config.path),
|
159
|
+
'models_module': app_config.models_module.__name__ if app_config.models_module else None,
|
160
|
+
}
|
161
|
+
except Exception as e:
|
162
|
+
logger.error(f"Failed to get app config for '{app_label}': {e}")
|
163
|
+
return {}
|
164
|
+
|
165
|
+
async def format_datetime(
|
166
|
+
self,
|
167
|
+
ctx: RunContext[DjangoDeps],
|
168
|
+
datetime_str: str,
|
169
|
+
format_str: str = "%Y-%m-%d %H:%M:%S"
|
170
|
+
) -> str:
|
171
|
+
"""Format datetime string using Django's timezone handling."""
|
172
|
+
from django.utils import timezone
|
173
|
+
from django.utils.dateparse import parse_datetime
|
174
|
+
|
175
|
+
try:
|
176
|
+
# Parse datetime
|
177
|
+
dt = parse_datetime(datetime_str)
|
178
|
+
if not dt:
|
179
|
+
return datetime_str
|
180
|
+
|
181
|
+
# Convert to user's timezone if available
|
182
|
+
if hasattr(ctx.deps, 'request') and ctx.deps.request:
|
183
|
+
# Use request timezone if available
|
184
|
+
dt = timezone.localtime(dt)
|
185
|
+
|
186
|
+
return dt.strftime(format_str)
|
187
|
+
except Exception as e:
|
188
|
+
logger.error(f"Failed to format datetime '{datetime_str}': {e}")
|
189
|
+
return datetime_str
|
190
|
+
|
191
|
+
async def get_model_info(self, ctx: RunContext[DjangoDeps], app_label: str, model_name: str) -> Dict[str, Any]:
|
192
|
+
"""Get Django model information."""
|
193
|
+
from django.apps import apps
|
194
|
+
|
195
|
+
try:
|
196
|
+
model = apps.get_model(app_label, model_name)
|
197
|
+
|
198
|
+
# Get field information
|
199
|
+
fields = []
|
200
|
+
for field in model._meta.fields:
|
201
|
+
fields.append({
|
202
|
+
'name': field.name,
|
203
|
+
'type': field.__class__.__name__,
|
204
|
+
'null': field.null,
|
205
|
+
'blank': field.blank,
|
206
|
+
'help_text': field.help_text,
|
207
|
+
})
|
208
|
+
|
209
|
+
return {
|
210
|
+
'app_label': model._meta.app_label,
|
211
|
+
'model_name': model._meta.model_name,
|
212
|
+
'verbose_name': str(model._meta.verbose_name),
|
213
|
+
'verbose_name_plural': str(model._meta.verbose_name_plural),
|
214
|
+
'db_table': model._meta.db_table,
|
215
|
+
'fields': fields,
|
216
|
+
'field_count': len(fields),
|
217
|
+
}
|
218
|
+
except Exception as e:
|
219
|
+
logger.error(f"Failed to get model info for '{app_label}.{model_name}': {e}")
|
220
|
+
return {}
|
@@ -0,0 +1,324 @@
|
|
1
|
+
"""
|
2
|
+
Django file toolset for file operations.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import logging
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Dict, Any, List, Optional, Union
|
9
|
+
from pydantic_ai.toolsets import AbstractToolset
|
10
|
+
from pydantic_ai import RunContext
|
11
|
+
from django.conf import settings
|
12
|
+
from django.core.files.storage import default_storage
|
13
|
+
from django.core.files.base import ContentFile
|
14
|
+
|
15
|
+
from ..core.dependencies import DjangoDeps
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class FileToolset(AbstractToolset[DjangoDeps]):
|
21
|
+
"""
|
22
|
+
Django file toolset for safe file operations.
|
23
|
+
|
24
|
+
Provides tools for:
|
25
|
+
- File reading/writing (within allowed directories)
|
26
|
+
- Media file handling
|
27
|
+
- Static file operations
|
28
|
+
- File metadata
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, allowed_paths: Optional[List[str]] = None, max_file_size: int = 10 * 1024 * 1024):
|
32
|
+
"""
|
33
|
+
Initialize file toolset.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
allowed_paths: List of allowed directory paths (relative to MEDIA_ROOT)
|
37
|
+
max_file_size: Maximum file size in bytes (default: 10MB)
|
38
|
+
"""
|
39
|
+
self.allowed_paths = allowed_paths or ['orchestrator', 'temp']
|
40
|
+
self.max_file_size = max_file_size
|
41
|
+
|
42
|
+
@property
|
43
|
+
def id(self) -> str:
|
44
|
+
return "django_files"
|
45
|
+
|
46
|
+
def _check_path_access(self, file_path: str) -> bool:
|
47
|
+
"""Check if file path is within allowed directories."""
|
48
|
+
path = Path(file_path)
|
49
|
+
|
50
|
+
# Normalize path and check if it's within allowed paths
|
51
|
+
try:
|
52
|
+
# Remove any parent directory traversal
|
53
|
+
normalized_path = path.resolve()
|
54
|
+
|
55
|
+
# Check against allowed paths
|
56
|
+
for allowed_path in self.allowed_paths:
|
57
|
+
allowed_full_path = Path(settings.MEDIA_ROOT) / allowed_path
|
58
|
+
try:
|
59
|
+
normalized_path.relative_to(allowed_full_path.resolve())
|
60
|
+
return True
|
61
|
+
except ValueError:
|
62
|
+
continue
|
63
|
+
|
64
|
+
return False
|
65
|
+
except Exception:
|
66
|
+
return False
|
67
|
+
|
68
|
+
def _get_safe_path(self, file_path: str, user_id: int) -> str:
|
69
|
+
"""Get safe file path with user scoping."""
|
70
|
+
# Add user scoping to prevent access to other users' files
|
71
|
+
safe_path = f"orchestrator/user_{user_id}/{file_path}"
|
72
|
+
|
73
|
+
# Normalize and validate
|
74
|
+
normalized = os.path.normpath(safe_path)
|
75
|
+
|
76
|
+
# Ensure no directory traversal
|
77
|
+
if '..' in normalized or normalized.startswith('/'):
|
78
|
+
raise ValueError("Invalid file path")
|
79
|
+
|
80
|
+
return normalized
|
81
|
+
|
82
|
+
async def read_file(
|
83
|
+
self,
|
84
|
+
ctx: RunContext[DjangoDeps],
|
85
|
+
file_path: str,
|
86
|
+
encoding: str = 'utf-8'
|
87
|
+
) -> Optional[str]:
|
88
|
+
"""Read text file content."""
|
89
|
+
user_id = ctx.deps.user.id
|
90
|
+
safe_path = self._get_safe_path(file_path, user_id)
|
91
|
+
|
92
|
+
try:
|
93
|
+
if default_storage.exists(safe_path):
|
94
|
+
with default_storage.open(safe_path, 'r') as f:
|
95
|
+
content = f.read()
|
96
|
+
|
97
|
+
logger.debug(f"Read file: {safe_path} ({len(content)} chars)")
|
98
|
+
return content
|
99
|
+
else:
|
100
|
+
logger.warning(f"File not found: {safe_path}")
|
101
|
+
return None
|
102
|
+
except Exception as e:
|
103
|
+
logger.error(f"Failed to read file '{safe_path}': {e}")
|
104
|
+
return None
|
105
|
+
|
106
|
+
async def write_file(
|
107
|
+
self,
|
108
|
+
ctx: RunContext[DjangoDeps],
|
109
|
+
file_path: str,
|
110
|
+
content: str,
|
111
|
+
encoding: str = 'utf-8'
|
112
|
+
) -> bool:
|
113
|
+
"""Write text content to file."""
|
114
|
+
user_id = ctx.deps.user.id
|
115
|
+
safe_path = self._get_safe_path(file_path, user_id)
|
116
|
+
|
117
|
+
# Check file size
|
118
|
+
content_bytes = content.encode(encoding)
|
119
|
+
if len(content_bytes) > self.max_file_size:
|
120
|
+
logger.error(f"File too large: {len(content_bytes)} bytes > {self.max_file_size}")
|
121
|
+
return False
|
122
|
+
|
123
|
+
try:
|
124
|
+
# Create directory if needed
|
125
|
+
dir_path = os.path.dirname(safe_path)
|
126
|
+
if dir_path and not default_storage.exists(dir_path):
|
127
|
+
# Create directory structure
|
128
|
+
parts = dir_path.split('/')
|
129
|
+
current_path = ''
|
130
|
+
for part in parts:
|
131
|
+
current_path = os.path.join(current_path, part) if current_path else part
|
132
|
+
if not default_storage.exists(current_path):
|
133
|
+
default_storage.save(f"{current_path}/.keep", ContentFile(b''))
|
134
|
+
|
135
|
+
# Write file
|
136
|
+
default_storage.save(safe_path, ContentFile(content_bytes))
|
137
|
+
|
138
|
+
logger.debug(f"Wrote file: {safe_path} ({len(content_bytes)} bytes)")
|
139
|
+
return True
|
140
|
+
except Exception as e:
|
141
|
+
logger.error(f"Failed to write file '{safe_path}': {e}")
|
142
|
+
return False
|
143
|
+
|
144
|
+
async def delete_file(self, ctx: RunContext[DjangoDeps], file_path: str) -> bool:
|
145
|
+
"""Delete file."""
|
146
|
+
user_id = ctx.deps.user.id
|
147
|
+
safe_path = self._get_safe_path(file_path, user_id)
|
148
|
+
|
149
|
+
try:
|
150
|
+
if default_storage.exists(safe_path):
|
151
|
+
default_storage.delete(safe_path)
|
152
|
+
logger.debug(f"Deleted file: {safe_path}")
|
153
|
+
return True
|
154
|
+
else:
|
155
|
+
logger.warning(f"File not found for deletion: {safe_path}")
|
156
|
+
return False
|
157
|
+
except Exception as e:
|
158
|
+
logger.error(f"Failed to delete file '{safe_path}': {e}")
|
159
|
+
return False
|
160
|
+
|
161
|
+
async def list_files(
|
162
|
+
self,
|
163
|
+
ctx: RunContext[DjangoDeps],
|
164
|
+
directory_path: str = "",
|
165
|
+
pattern: Optional[str] = None
|
166
|
+
) -> List[Dict[str, Any]]:
|
167
|
+
"""List files in directory."""
|
168
|
+
user_id = ctx.deps.user.id
|
169
|
+
safe_path = self._get_safe_path(directory_path, user_id)
|
170
|
+
|
171
|
+
try:
|
172
|
+
if not default_storage.exists(safe_path):
|
173
|
+
return []
|
174
|
+
|
175
|
+
# List directory contents
|
176
|
+
directories, files = default_storage.listdir(safe_path)
|
177
|
+
|
178
|
+
results = []
|
179
|
+
|
180
|
+
# Add directories
|
181
|
+
for directory in directories:
|
182
|
+
dir_path = os.path.join(safe_path, directory)
|
183
|
+
results.append({
|
184
|
+
'name': directory,
|
185
|
+
'type': 'directory',
|
186
|
+
'path': dir_path,
|
187
|
+
'size': None,
|
188
|
+
'modified': None,
|
189
|
+
})
|
190
|
+
|
191
|
+
# Add files
|
192
|
+
for file in files:
|
193
|
+
file_path = os.path.join(safe_path, file)
|
194
|
+
|
195
|
+
# Apply pattern filter if specified
|
196
|
+
if pattern and pattern not in file:
|
197
|
+
continue
|
198
|
+
|
199
|
+
try:
|
200
|
+
size = default_storage.size(file_path)
|
201
|
+
modified = default_storage.get_modified_time(file_path)
|
202
|
+
|
203
|
+
results.append({
|
204
|
+
'name': file,
|
205
|
+
'type': 'file',
|
206
|
+
'path': file_path,
|
207
|
+
'size': size,
|
208
|
+
'modified': modified.isoformat() if modified else None,
|
209
|
+
})
|
210
|
+
except Exception as e:
|
211
|
+
logger.warning(f"Could not get file info for '{file_path}': {e}")
|
212
|
+
results.append({
|
213
|
+
'name': file,
|
214
|
+
'type': 'file',
|
215
|
+
'path': file_path,
|
216
|
+
'size': None,
|
217
|
+
'modified': None,
|
218
|
+
})
|
219
|
+
|
220
|
+
return results
|
221
|
+
except Exception as e:
|
222
|
+
logger.error(f"Failed to list files in '{safe_path}': {e}")
|
223
|
+
return []
|
224
|
+
|
225
|
+
async def get_file_info(
|
226
|
+
self,
|
227
|
+
ctx: RunContext[DjangoDeps],
|
228
|
+
file_path: str
|
229
|
+
) -> Optional[Dict[str, Any]]:
|
230
|
+
"""Get file metadata."""
|
231
|
+
user_id = ctx.deps.user.id
|
232
|
+
safe_path = self._get_safe_path(file_path, user_id)
|
233
|
+
|
234
|
+
try:
|
235
|
+
if not default_storage.exists(safe_path):
|
236
|
+
return None
|
237
|
+
|
238
|
+
size = default_storage.size(safe_path)
|
239
|
+
modified = default_storage.get_modified_time(safe_path)
|
240
|
+
url = default_storage.url(safe_path) if hasattr(default_storage, 'url') else None
|
241
|
+
|
242
|
+
return {
|
243
|
+
'path': safe_path,
|
244
|
+
'size': size,
|
245
|
+
'modified': modified.isoformat() if modified else None,
|
246
|
+
'url': url,
|
247
|
+
'exists': True,
|
248
|
+
}
|
249
|
+
except Exception as e:
|
250
|
+
logger.error(f"Failed to get file info for '{safe_path}': {e}")
|
251
|
+
return None
|
252
|
+
|
253
|
+
async def copy_file(
|
254
|
+
self,
|
255
|
+
ctx: RunContext[DjangoDeps],
|
256
|
+
source_path: str,
|
257
|
+
destination_path: str
|
258
|
+
) -> bool:
|
259
|
+
"""Copy file to new location."""
|
260
|
+
user_id = ctx.deps.user.id
|
261
|
+
safe_source = self._get_safe_path(source_path, user_id)
|
262
|
+
safe_dest = self._get_safe_path(destination_path, user_id)
|
263
|
+
|
264
|
+
try:
|
265
|
+
if not default_storage.exists(safe_source):
|
266
|
+
logger.error(f"Source file not found: {safe_source}")
|
267
|
+
return False
|
268
|
+
|
269
|
+
# Read source file
|
270
|
+
with default_storage.open(safe_source, 'rb') as source_file:
|
271
|
+
content = source_file.read()
|
272
|
+
|
273
|
+
# Check size limit
|
274
|
+
if len(content) > self.max_file_size:
|
275
|
+
logger.error(f"File too large to copy: {len(content)} bytes")
|
276
|
+
return False
|
277
|
+
|
278
|
+
# Write to destination
|
279
|
+
default_storage.save(safe_dest, ContentFile(content))
|
280
|
+
|
281
|
+
logger.debug(f"Copied file: {safe_source} -> {safe_dest}")
|
282
|
+
return True
|
283
|
+
except Exception as e:
|
284
|
+
logger.error(f"Failed to copy file '{safe_source}' to '{safe_dest}': {e}")
|
285
|
+
return False
|
286
|
+
|
287
|
+
async def move_file(
|
288
|
+
self,
|
289
|
+
ctx: RunContext[DjangoDeps],
|
290
|
+
source_path: str,
|
291
|
+
destination_path: str
|
292
|
+
) -> bool:
|
293
|
+
"""Move file to new location."""
|
294
|
+
# Copy then delete
|
295
|
+
if await self.copy_file(ctx, source_path, destination_path):
|
296
|
+
return await self.delete_file(ctx, source_path)
|
297
|
+
return False
|
298
|
+
|
299
|
+
async def get_storage_info(self, ctx: RunContext[DjangoDeps]) -> Dict[str, Any]:
|
300
|
+
"""Get storage backend information."""
|
301
|
+
user_id = ctx.deps.user.id
|
302
|
+
user_dir = self._get_safe_path("", user_id)
|
303
|
+
|
304
|
+
info = {
|
305
|
+
'storage_backend': default_storage.__class__.__name__,
|
306
|
+
'user_directory': user_dir,
|
307
|
+
'max_file_size': self.max_file_size,
|
308
|
+
'allowed_paths': self.allowed_paths,
|
309
|
+
}
|
310
|
+
|
311
|
+
try:
|
312
|
+
# Try to get user directory size
|
313
|
+
files = await self.list_files(ctx, "")
|
314
|
+
total_size = sum(f.get('size', 0) or 0 for f in files if f['type'] == 'file')
|
315
|
+
file_count = sum(1 for f in files if f['type'] == 'file')
|
316
|
+
|
317
|
+
info.update({
|
318
|
+
'user_files_count': file_count,
|
319
|
+
'user_total_size': total_size,
|
320
|
+
})
|
321
|
+
except Exception as e:
|
322
|
+
logger.warning(f"Could not get storage stats: {e}")
|
323
|
+
|
324
|
+
return info
|