django-cfg 1.3.7__py3-none-any.whl → 1.3.9__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 +1 -1
- django_cfg/apps/accounts/admin/__init__.py +24 -8
- django_cfg/apps/accounts/admin/activity_admin.py +146 -0
- django_cfg/apps/accounts/admin/filters.py +98 -22
- django_cfg/apps/accounts/admin/group_admin.py +86 -0
- django_cfg/apps/accounts/admin/inlines.py +42 -13
- django_cfg/apps/accounts/admin/otp_admin.py +115 -0
- django_cfg/apps/accounts/admin/registration_admin.py +173 -0
- django_cfg/apps/accounts/admin/resources.py +123 -19
- django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
- django_cfg/apps/accounts/admin/user_admin.py +362 -0
- django_cfg/apps/agents/admin/__init__.py +17 -4
- django_cfg/apps/agents/admin/execution_admin.py +204 -183
- django_cfg/apps/agents/admin/registry_admin.py +230 -255
- django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
- django_cfg/apps/agents/core/__init__.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +221 -0
- django_cfg/apps/agents/core/exceptions.py +14 -0
- django_cfg/apps/agents/core/orchestrator.py +18 -3
- django_cfg/apps/knowbase/admin/__init__.py +1 -1
- django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
- django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
- django_cfg/apps/knowbase/admin/document_admin.py +269 -262
- django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
- django_cfg/apps/knowbase/config/settings.py +21 -4
- django_cfg/apps/knowbase/views/chat_views.py +3 -0
- django_cfg/apps/leads/admin/__init__.py +3 -1
- django_cfg/apps/leads/admin/leads_admin.py +235 -35
- django_cfg/apps/maintenance/admin/__init__.py +2 -2
- django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
- django_cfg/apps/maintenance/admin/log_admin.py +143 -61
- django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
- django_cfg/apps/maintenance/admin/site_admin.py +213 -352
- django_cfg/apps/newsletter/admin/__init__.py +29 -2
- django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
- django_cfg/apps/payments/admin/__init__.py +18 -27
- django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
- django_cfg/apps/payments/admin/balance_admin.py +166 -632
- django_cfg/apps/payments/admin/currencies_admin.py +235 -607
- django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
- django_cfg/apps/payments/admin/filters.py +83 -3
- django_cfg/apps/payments/admin/networks_admin.py +258 -0
- django_cfg/apps/payments/admin/payments_admin.py +171 -461
- django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
- django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
- django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
- django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
- django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
- django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
- django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
- django_cfg/apps/payments/middleware/api_access.py +32 -6
- django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
- django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
- django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
- django_cfg/apps/payments/models/balance.py +12 -0
- django_cfg/apps/payments/models/currencies.py +106 -32
- django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
- django_cfg/apps/payments/services/core/currency_service.py +35 -28
- django_cfg/apps/payments/services/core/payment_service.py +1 -1
- django_cfg/apps/payments/services/providers/__init__.py +3 -0
- django_cfg/apps/payments/services/providers/base.py +95 -39
- django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
- django_cfg/apps/payments/services/providers/models/base.py +122 -0
- django_cfg/apps/payments/services/providers/models/providers.py +87 -0
- django_cfg/apps/payments/services/providers/models/universal.py +48 -0
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
- django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
- django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
- django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
- django_cfg/apps/payments/services/providers/registry.py +4 -32
- django_cfg/apps/payments/services/providers/sync_service.py +277 -0
- django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
- django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
- django_cfg/apps/payments/tasks/__init__.py +39 -0
- django_cfg/apps/payments/tasks/types.py +73 -0
- django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
- django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
- django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
- django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
- django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
- django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
- django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
- django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
- django_cfg/apps/payments/urls_admin.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +5 -5
- django_cfg/apps/payments/views/overview/services.py +2 -2
- django_cfg/apps/payments/views/serializers/currencies.py +4 -3
- django_cfg/apps/support/admin/__init__.py +10 -1
- django_cfg/apps/support/admin/support_admin.py +338 -141
- django_cfg/apps/tasks/admin/__init__.py +11 -0
- django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
- django_cfg/config.py +1 -1
- django_cfg/core/config.py +10 -5
- django_cfg/core/generation.py +1 -1
- django_cfg/management/commands/__init__.py +13 -1
- django_cfg/management/commands/app_agent_diagnose.py +470 -0
- django_cfg/management/commands/app_agent_generate.py +342 -0
- django_cfg/management/commands/app_agent_info.py +308 -0
- django_cfg/management/commands/migrate_all.py +9 -3
- django_cfg/management/commands/migrator.py +11 -6
- django_cfg/management/commands/rundramatiq.py +3 -2
- django_cfg/middleware/__init__.py +0 -2
- django_cfg/models/api_keys.py +115 -0
- django_cfg/modules/django_admin/__init__.py +64 -0
- django_cfg/modules/django_admin/decorators/__init__.py +13 -0
- django_cfg/modules/django_admin/decorators/actions.py +106 -0
- django_cfg/modules/django_admin/decorators/display.py +106 -0
- django_cfg/modules/django_admin/mixins/__init__.py +14 -0
- django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
- django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
- django_cfg/modules/django_admin/models/__init__.py +20 -0
- django_cfg/modules/django_admin/models/action_models.py +33 -0
- django_cfg/modules/django_admin/models/badge_models.py +20 -0
- django_cfg/modules/django_admin/models/base.py +26 -0
- django_cfg/modules/django_admin/models/display_models.py +31 -0
- django_cfg/modules/django_admin/utils/badges.py +159 -0
- django_cfg/modules/django_admin/utils/displays.py +247 -0
- django_cfg/modules/django_app_agent/__init__.py +87 -0
- django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
- django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
- django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
- django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
- django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
- django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
- django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
- django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
- django_cfg/modules/django_app_agent/core/__init__.py +33 -0
- django_cfg/modules/django_app_agent/core/config.py +300 -0
- django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
- django_cfg/modules/django_app_agent/models/__init__.py +71 -0
- django_cfg/modules/django_app_agent/models/base.py +283 -0
- django_cfg/modules/django_app_agent/models/context.py +496 -0
- django_cfg/modules/django_app_agent/models/enums.py +481 -0
- django_cfg/modules/django_app_agent/models/requests.py +500 -0
- django_cfg/modules/django_app_agent/models/responses.py +585 -0
- django_cfg/modules/django_app_agent/pytest.ini +6 -0
- django_cfg/modules/django_app_agent/services/__init__.py +42 -0
- django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
- django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
- django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
- django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
- django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
- django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
- django_cfg/modules/django_app_agent/services/base.py +437 -0
- django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
- django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
- django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
- django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
- django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
- django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
- django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
- django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
- django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
- django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
- django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
- django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
- django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
- django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
- django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
- django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
- django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
- django_cfg/modules/django_app_agent/services/report_service.py +332 -0
- django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
- django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
- django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
- django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
- django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
- django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
- django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
- django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
- django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
- django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
- django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
- django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
- django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
- django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
- django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
- django_cfg/modules/django_app_agent/ui/cli.py +419 -0
- django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
- django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
- django_cfg/modules/django_app_agent/utils/logging.py +360 -0
- django_cfg/modules/django_app_agent/utils/validation.py +417 -0
- django_cfg/modules/django_currency/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/__init__.py +2 -2
- django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
- django_cfg/modules/django_currency/core/converter.py +12 -12
- django_cfg/modules/django_currency/database/__init__.py +2 -2
- django_cfg/modules/django_currency/database/database_loader.py +93 -42
- django_cfg/modules/django_llm/llm/client.py +10 -2
- django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
- django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
- django_cfg/modules/django_unfold/dashboard.py +14 -13
- django_cfg/modules/django_unfold/models/config.py +1 -1
- django_cfg/registry/core.py +3 -0
- django_cfg/registry/third_party.py +2 -2
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
- django_cfg/apps/accounts/admin/activity.py +0 -96
- django_cfg/apps/accounts/admin/group.py +0 -17
- django_cfg/apps/accounts/admin/otp.py +0 -59
- django_cfg/apps/accounts/admin/registration_source.py +0 -97
- django_cfg/apps/accounts/admin/twilio_response.py +0 -227
- django_cfg/apps/accounts/admin/user.py +0 -300
- django_cfg/apps/agents/core/agent.py +0 -281
- django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
- django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
- django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
- django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
- django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
- django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
- django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
- django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
- django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
- django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
- django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
- django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
- django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
- django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
- django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
- django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
- django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
- django_cfg/apps/tasks/admin.py +0 -320
- django_cfg/middleware/static_nocache.py +0 -55
- django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
- /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
- /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,417 @@
|
|
1
|
+
"""
|
2
|
+
Validation utilities for Django App Agent Module.
|
3
|
+
|
4
|
+
This module provides comprehensive validation functions for:
|
5
|
+
- Django app names and identifiers
|
6
|
+
- File paths and names
|
7
|
+
- Python code validation
|
8
|
+
- Input sanitization
|
9
|
+
"""
|
10
|
+
|
11
|
+
import re
|
12
|
+
import keyword
|
13
|
+
from typing import List, Optional, Tuple
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
from ..core.exceptions import ValidationError
|
17
|
+
|
18
|
+
|
19
|
+
# Regular expressions for validation
|
20
|
+
APP_NAME_PATTERN = re.compile(r'^[a-z][a-z0-9_]*$')
|
21
|
+
PYTHON_IDENTIFIER_PATTERN = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
|
22
|
+
SAFE_FILENAME_PATTERN = re.compile(r'^[a-zA-Z0-9._-]+$')
|
23
|
+
|
24
|
+
# Reserved Django app names
|
25
|
+
RESERVED_APP_NAMES = {
|
26
|
+
'admin', 'auth', 'contenttypes', 'sessions', 'messages', 'staticfiles',
|
27
|
+
'django', 'test', 'tests', 'migrations', 'management', 'locale',
|
28
|
+
'fixtures', 'templates', 'static', 'media'
|
29
|
+
}
|
30
|
+
|
31
|
+
# Reserved Python keywords and builtins
|
32
|
+
RESERVED_PYTHON_NAMES = set(keyword.kwlist) | {
|
33
|
+
'True', 'False', 'None', '__builtins__', '__name__', '__file__',
|
34
|
+
'int', 'str', 'list', 'dict', 'tuple', 'set', 'bool', 'float'
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
def validate_app_name(name: str, *, check_reserved: bool = True) -> str:
|
39
|
+
"""Validate Django application name.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
name: Application name to validate
|
43
|
+
check_reserved: Whether to check against reserved names
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Validated and normalized app name
|
47
|
+
|
48
|
+
Raises:
|
49
|
+
ValidationError: If name is invalid
|
50
|
+
"""
|
51
|
+
if not name:
|
52
|
+
raise ValidationError(
|
53
|
+
"App name cannot be empty",
|
54
|
+
validation_type="app_name",
|
55
|
+
field_name="name",
|
56
|
+
field_value=name
|
57
|
+
)
|
58
|
+
|
59
|
+
# Normalize name
|
60
|
+
normalized_name = name.strip().lower()
|
61
|
+
|
62
|
+
# Check length
|
63
|
+
if len(normalized_name) < 2:
|
64
|
+
raise ValidationError(
|
65
|
+
"App name must be at least 2 characters long",
|
66
|
+
validation_type="app_name",
|
67
|
+
field_name="name",
|
68
|
+
field_value=name
|
69
|
+
)
|
70
|
+
|
71
|
+
if len(normalized_name) > 50:
|
72
|
+
raise ValidationError(
|
73
|
+
"App name must be no more than 50 characters long",
|
74
|
+
validation_type="app_name",
|
75
|
+
field_name="name",
|
76
|
+
field_value=name
|
77
|
+
)
|
78
|
+
|
79
|
+
# Check pattern
|
80
|
+
if not APP_NAME_PATTERN.match(normalized_name):
|
81
|
+
raise ValidationError(
|
82
|
+
"App name must start with a letter and contain only lowercase letters, numbers, and underscores",
|
83
|
+
validation_type="app_name",
|
84
|
+
field_name="name",
|
85
|
+
field_value=name
|
86
|
+
)
|
87
|
+
|
88
|
+
# Check reserved names
|
89
|
+
if check_reserved and normalized_name in RESERVED_APP_NAMES:
|
90
|
+
raise ValidationError(
|
91
|
+
f"'{normalized_name}' is a reserved Django app name",
|
92
|
+
validation_type="app_name",
|
93
|
+
field_name="name",
|
94
|
+
field_value=name
|
95
|
+
)
|
96
|
+
|
97
|
+
# Check Python keywords
|
98
|
+
if normalized_name in RESERVED_PYTHON_NAMES:
|
99
|
+
raise ValidationError(
|
100
|
+
f"'{normalized_name}' is a reserved Python keyword",
|
101
|
+
validation_type="app_name",
|
102
|
+
field_name="name",
|
103
|
+
field_value=name
|
104
|
+
)
|
105
|
+
|
106
|
+
return normalized_name
|
107
|
+
|
108
|
+
|
109
|
+
def validate_python_identifier(identifier: str, *, context: str = "identifier") -> str:
|
110
|
+
"""Validate Python identifier (variable, class, function name).
|
111
|
+
|
112
|
+
Args:
|
113
|
+
identifier: Identifier to validate
|
114
|
+
context: Context for error messages
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Validated identifier
|
118
|
+
|
119
|
+
Raises:
|
120
|
+
ValidationError: If identifier is invalid
|
121
|
+
"""
|
122
|
+
if not identifier:
|
123
|
+
raise ValidationError(
|
124
|
+
f"Python {context} cannot be empty",
|
125
|
+
validation_type="python_identifier",
|
126
|
+
field_name=context,
|
127
|
+
field_value=identifier
|
128
|
+
)
|
129
|
+
|
130
|
+
# Check pattern
|
131
|
+
if not PYTHON_IDENTIFIER_PATTERN.match(identifier):
|
132
|
+
raise ValidationError(
|
133
|
+
f"Python {context} must start with a letter or underscore and contain only letters, numbers, and underscores",
|
134
|
+
validation_type="python_identifier",
|
135
|
+
field_name=context,
|
136
|
+
field_value=identifier
|
137
|
+
)
|
138
|
+
|
139
|
+
# Check keywords
|
140
|
+
if keyword.iskeyword(identifier):
|
141
|
+
raise ValidationError(
|
142
|
+
f"'{identifier}' is a reserved Python keyword",
|
143
|
+
validation_type="python_identifier",
|
144
|
+
field_name=context,
|
145
|
+
field_value=identifier
|
146
|
+
)
|
147
|
+
|
148
|
+
# Check builtins
|
149
|
+
if identifier in RESERVED_PYTHON_NAMES:
|
150
|
+
raise ValidationError(
|
151
|
+
f"'{identifier}' is a reserved Python name",
|
152
|
+
validation_type="python_identifier",
|
153
|
+
field_name=context,
|
154
|
+
field_value=identifier
|
155
|
+
)
|
156
|
+
|
157
|
+
return identifier
|
158
|
+
|
159
|
+
|
160
|
+
def validate_file_path(
|
161
|
+
path: Path,
|
162
|
+
*,
|
163
|
+
must_exist: bool = False,
|
164
|
+
must_be_dir: bool = False,
|
165
|
+
must_be_file: bool = False,
|
166
|
+
create_parents: bool = False
|
167
|
+
) -> Path:
|
168
|
+
"""Validate file or directory path.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
path: Path to validate
|
172
|
+
must_exist: Whether path must exist
|
173
|
+
must_be_dir: Whether path must be a directory
|
174
|
+
must_be_file: Whether path must be a file
|
175
|
+
create_parents: Whether to create parent directories
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
Validated path
|
179
|
+
|
180
|
+
Raises:
|
181
|
+
ValidationError: If path is invalid
|
182
|
+
"""
|
183
|
+
if not isinstance(path, Path):
|
184
|
+
try:
|
185
|
+
path = Path(path)
|
186
|
+
except Exception as e:
|
187
|
+
raise ValidationError(
|
188
|
+
f"Invalid path format: {e}",
|
189
|
+
validation_type="file_path",
|
190
|
+
field_name="path",
|
191
|
+
field_value=str(path),
|
192
|
+
cause=e
|
193
|
+
)
|
194
|
+
|
195
|
+
# Resolve path
|
196
|
+
try:
|
197
|
+
resolved_path = path.resolve()
|
198
|
+
except Exception as e:
|
199
|
+
raise ValidationError(
|
200
|
+
f"Cannot resolve path: {e}",
|
201
|
+
validation_type="file_path",
|
202
|
+
field_name="path",
|
203
|
+
field_value=str(path),
|
204
|
+
cause=e
|
205
|
+
)
|
206
|
+
|
207
|
+
# Check existence
|
208
|
+
if must_exist and not resolved_path.exists():
|
209
|
+
raise ValidationError(
|
210
|
+
f"Path does not exist: {resolved_path}",
|
211
|
+
validation_type="file_path",
|
212
|
+
field_name="path",
|
213
|
+
field_value=str(path)
|
214
|
+
)
|
215
|
+
|
216
|
+
# Check type constraints
|
217
|
+
if resolved_path.exists():
|
218
|
+
if must_be_dir and not resolved_path.is_dir():
|
219
|
+
raise ValidationError(
|
220
|
+
f"Path is not a directory: {resolved_path}",
|
221
|
+
validation_type="file_path",
|
222
|
+
field_name="path",
|
223
|
+
field_value=str(path)
|
224
|
+
)
|
225
|
+
|
226
|
+
if must_be_file and not resolved_path.is_file():
|
227
|
+
raise ValidationError(
|
228
|
+
f"Path is not a file: {resolved_path}",
|
229
|
+
validation_type="file_path",
|
230
|
+
field_name="path",
|
231
|
+
field_value=str(path)
|
232
|
+
)
|
233
|
+
|
234
|
+
# Create parent directories if requested
|
235
|
+
if create_parents and not resolved_path.parent.exists():
|
236
|
+
try:
|
237
|
+
resolved_path.parent.mkdir(parents=True, exist_ok=True)
|
238
|
+
except Exception as e:
|
239
|
+
raise ValidationError(
|
240
|
+
f"Cannot create parent directories: {e}",
|
241
|
+
validation_type="file_path",
|
242
|
+
field_name="path",
|
243
|
+
field_value=str(path),
|
244
|
+
cause=e
|
245
|
+
)
|
246
|
+
|
247
|
+
return resolved_path
|
248
|
+
|
249
|
+
|
250
|
+
def sanitize_filename(filename: str, *, max_length: int = 255) -> str:
|
251
|
+
"""Sanitize filename for safe file system usage.
|
252
|
+
|
253
|
+
Args:
|
254
|
+
filename: Filename to sanitize
|
255
|
+
max_length: Maximum filename length
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
Sanitized filename
|
259
|
+
|
260
|
+
Raises:
|
261
|
+
ValidationError: If filename cannot be sanitized
|
262
|
+
"""
|
263
|
+
if not filename:
|
264
|
+
raise ValidationError(
|
265
|
+
"Filename cannot be empty",
|
266
|
+
validation_type="filename",
|
267
|
+
field_name="filename",
|
268
|
+
field_value=filename
|
269
|
+
)
|
270
|
+
|
271
|
+
# Remove or replace unsafe characters
|
272
|
+
sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
273
|
+
|
274
|
+
# Remove control characters
|
275
|
+
sanitized = re.sub(r'[\x00-\x1f\x7f]', '', sanitized)
|
276
|
+
|
277
|
+
# Trim whitespace
|
278
|
+
sanitized = sanitized.strip()
|
279
|
+
|
280
|
+
# Handle reserved Windows names
|
281
|
+
reserved_windows = {
|
282
|
+
'CON', 'PRN', 'AUX', 'NUL',
|
283
|
+
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
284
|
+
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
|
285
|
+
}
|
286
|
+
|
287
|
+
name_part = sanitized.split('.')[0].upper()
|
288
|
+
if name_part in reserved_windows:
|
289
|
+
sanitized = f"_{sanitized}"
|
290
|
+
|
291
|
+
# Ensure not empty after sanitization
|
292
|
+
if not sanitized:
|
293
|
+
raise ValidationError(
|
294
|
+
"Filename becomes empty after sanitization",
|
295
|
+
validation_type="filename",
|
296
|
+
field_name="filename",
|
297
|
+
field_value=filename
|
298
|
+
)
|
299
|
+
|
300
|
+
# Check length
|
301
|
+
if len(sanitized) > max_length:
|
302
|
+
# Try to preserve extension
|
303
|
+
if '.' in sanitized:
|
304
|
+
name, ext = sanitized.rsplit('.', 1)
|
305
|
+
max_name_length = max_length - len(ext) - 1
|
306
|
+
if max_name_length > 0:
|
307
|
+
sanitized = f"{name[:max_name_length]}.{ext}"
|
308
|
+
else:
|
309
|
+
sanitized = sanitized[:max_length]
|
310
|
+
else:
|
311
|
+
sanitized = sanitized[:max_length]
|
312
|
+
|
313
|
+
return sanitized
|
314
|
+
|
315
|
+
|
316
|
+
def validate_description(description: str, *, min_length: int = 10, max_length: int = 500) -> str:
|
317
|
+
"""Validate application description.
|
318
|
+
|
319
|
+
Args:
|
320
|
+
description: Description to validate
|
321
|
+
min_length: Minimum description length
|
322
|
+
max_length: Maximum description length
|
323
|
+
|
324
|
+
Returns:
|
325
|
+
Validated description
|
326
|
+
|
327
|
+
Raises:
|
328
|
+
ValidationError: If description is invalid
|
329
|
+
"""
|
330
|
+
if not description:
|
331
|
+
raise ValidationError(
|
332
|
+
"Description cannot be empty",
|
333
|
+
validation_type="description",
|
334
|
+
field_name="description",
|
335
|
+
field_value=description
|
336
|
+
)
|
337
|
+
|
338
|
+
# Normalize whitespace
|
339
|
+
normalized = ' '.join(description.split())
|
340
|
+
|
341
|
+
# Check length
|
342
|
+
if len(normalized) < min_length:
|
343
|
+
raise ValidationError(
|
344
|
+
f"Description must be at least {min_length} characters long",
|
345
|
+
validation_type="description",
|
346
|
+
field_name="description",
|
347
|
+
field_value=description
|
348
|
+
)
|
349
|
+
|
350
|
+
if len(normalized) > max_length:
|
351
|
+
raise ValidationError(
|
352
|
+
f"Description must be no more than {max_length} characters long",
|
353
|
+
validation_type="description",
|
354
|
+
field_name="description",
|
355
|
+
field_value=description
|
356
|
+
)
|
357
|
+
|
358
|
+
return normalized
|
359
|
+
|
360
|
+
|
361
|
+
def validate_existing_apps(
|
362
|
+
app_name: str,
|
363
|
+
existing_apps: List[str],
|
364
|
+
*,
|
365
|
+
case_sensitive: bool = False
|
366
|
+
) -> None:
|
367
|
+
"""Validate that app name doesn't conflict with existing apps.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
app_name: New app name to check
|
371
|
+
existing_apps: List of existing app names
|
372
|
+
case_sensitive: Whether comparison should be case sensitive
|
373
|
+
|
374
|
+
Raises:
|
375
|
+
ValidationError: If app name conflicts with existing apps
|
376
|
+
"""
|
377
|
+
if not case_sensitive:
|
378
|
+
existing_lower = [app.lower() for app in existing_apps]
|
379
|
+
if app_name.lower() in existing_lower:
|
380
|
+
# Find the actual conflicting app name
|
381
|
+
for existing_app in existing_apps:
|
382
|
+
if existing_app.lower() == app_name.lower():
|
383
|
+
raise ValidationError(
|
384
|
+
f"App '{app_name}' conflicts with existing app '{existing_app}'",
|
385
|
+
validation_type="app_name_conflict",
|
386
|
+
field_name="app_name",
|
387
|
+
field_value=app_name
|
388
|
+
)
|
389
|
+
else:
|
390
|
+
if app_name in existing_apps:
|
391
|
+
raise ValidationError(
|
392
|
+
f"App '{app_name}' already exists",
|
393
|
+
validation_type="app_name_conflict",
|
394
|
+
field_name="app_name",
|
395
|
+
field_value=app_name
|
396
|
+
)
|
397
|
+
|
398
|
+
|
399
|
+
def validate_python_code(code: str, *, filename: str = "<generated>") -> Tuple[bool, Optional[str]]:
|
400
|
+
"""Validate Python code syntax.
|
401
|
+
|
402
|
+
Args:
|
403
|
+
code: Python code to validate
|
404
|
+
filename: Filename for error reporting
|
405
|
+
|
406
|
+
Returns:
|
407
|
+
Tuple of (is_valid, error_message)
|
408
|
+
"""
|
409
|
+
try:
|
410
|
+
compile(code, filename, 'exec')
|
411
|
+
return True, None
|
412
|
+
except SyntaxError as e:
|
413
|
+
error_msg = f"Syntax error in {filename} at line {e.lineno}: {e.msg}"
|
414
|
+
return False, error_msg
|
415
|
+
except Exception as e:
|
416
|
+
error_msg = f"Compilation error in {filename}: {e}"
|
417
|
+
return False, error_msg
|
@@ -22,7 +22,7 @@ from .core import (
|
|
22
22
|
from .utils import CacheManager
|
23
23
|
|
24
24
|
# Clients
|
25
|
-
from .clients import
|
25
|
+
from .clients import HybridCurrencyClient, CoinPaprikaClient
|
26
26
|
|
27
27
|
# Database tools
|
28
28
|
from .database import (
|
@@ -95,7 +95,7 @@ __all__ = [
|
|
95
95
|
"CacheManager",
|
96
96
|
|
97
97
|
# Clients
|
98
|
-
"
|
98
|
+
"HybridCurrencyClient",
|
99
99
|
"CoinPaprikaClient",
|
100
100
|
|
101
101
|
# Database tools
|
@@ -2,10 +2,10 @@
|
|
2
2
|
Currency data clients for fetching rates from external APIs.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from .
|
5
|
+
from .hybrid_client import HybridCurrencyClient
|
6
6
|
from .coinpaprika_client import CoinPaprikaClient
|
7
7
|
|
8
8
|
__all__ = [
|
9
|
-
'
|
9
|
+
'HybridCurrencyClient',
|
10
10
|
'CoinPaprikaClient'
|
11
11
|
]
|