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.
Files changed (251) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/accounts/admin/__init__.py +24 -8
  3. django_cfg/apps/accounts/admin/activity_admin.py +146 -0
  4. django_cfg/apps/accounts/admin/filters.py +98 -22
  5. django_cfg/apps/accounts/admin/group_admin.py +86 -0
  6. django_cfg/apps/accounts/admin/inlines.py +42 -13
  7. django_cfg/apps/accounts/admin/otp_admin.py +115 -0
  8. django_cfg/apps/accounts/admin/registration_admin.py +173 -0
  9. django_cfg/apps/accounts/admin/resources.py +123 -19
  10. django_cfg/apps/accounts/admin/twilio_admin.py +327 -0
  11. django_cfg/apps/accounts/admin/user_admin.py +362 -0
  12. django_cfg/apps/agents/admin/__init__.py +17 -4
  13. django_cfg/apps/agents/admin/execution_admin.py +204 -183
  14. django_cfg/apps/agents/admin/registry_admin.py +230 -255
  15. django_cfg/apps/agents/admin/toolsets_admin.py +274 -321
  16. django_cfg/apps/agents/core/__init__.py +1 -1
  17. django_cfg/apps/agents/core/django_agent.py +221 -0
  18. django_cfg/apps/agents/core/exceptions.py +14 -0
  19. django_cfg/apps/agents/core/orchestrator.py +18 -3
  20. django_cfg/apps/knowbase/admin/__init__.py +1 -1
  21. django_cfg/apps/knowbase/admin/archive_admin.py +352 -640
  22. django_cfg/apps/knowbase/admin/chat_admin.py +258 -192
  23. django_cfg/apps/knowbase/admin/document_admin.py +269 -262
  24. django_cfg/apps/knowbase/admin/external_data_admin.py +271 -489
  25. django_cfg/apps/knowbase/config/settings.py +21 -4
  26. django_cfg/apps/knowbase/views/chat_views.py +3 -0
  27. django_cfg/apps/leads/admin/__init__.py +3 -1
  28. django_cfg/apps/leads/admin/leads_admin.py +235 -35
  29. django_cfg/apps/maintenance/admin/__init__.py +2 -2
  30. django_cfg/apps/maintenance/admin/api_key_admin.py +125 -63
  31. django_cfg/apps/maintenance/admin/log_admin.py +143 -61
  32. django_cfg/apps/maintenance/admin/scheduled_admin.py +212 -301
  33. django_cfg/apps/maintenance/admin/site_admin.py +213 -352
  34. django_cfg/apps/newsletter/admin/__init__.py +29 -2
  35. django_cfg/apps/newsletter/admin/newsletter_admin.py +531 -193
  36. django_cfg/apps/payments/admin/__init__.py +18 -27
  37. django_cfg/apps/payments/admin/api_keys_admin.py +179 -546
  38. django_cfg/apps/payments/admin/balance_admin.py +166 -632
  39. django_cfg/apps/payments/admin/currencies_admin.py +235 -607
  40. django_cfg/apps/payments/admin/endpoint_groups_admin.py +127 -0
  41. django_cfg/apps/payments/admin/filters.py +83 -3
  42. django_cfg/apps/payments/admin/networks_admin.py +258 -0
  43. django_cfg/apps/payments/admin/payments_admin.py +171 -461
  44. django_cfg/apps/payments/admin/subscriptions_admin.py +119 -636
  45. django_cfg/apps/payments/admin/tariffs_admin.py +248 -0
  46. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +105 -34
  47. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +12 -16
  48. django_cfg/apps/payments/admin_interface/views/__init__.py +2 -0
  49. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +13 -18
  50. django_cfg/apps/payments/management/commands/manage_currencies.py +236 -274
  51. django_cfg/apps/payments/management/commands/manage_providers.py +4 -1
  52. django_cfg/apps/payments/middleware/api_access.py +32 -6
  53. django_cfg/apps/payments/migrations/0002_currency_usd_rate_currency_usd_rate_updated_at.py +26 -0
  54. django_cfg/apps/payments/migrations/0003_remove_provider_currency_fields.py +28 -0
  55. django_cfg/apps/payments/migrations/0004_add_reserved_usd_field.py +30 -0
  56. django_cfg/apps/payments/models/balance.py +12 -0
  57. django_cfg/apps/payments/models/currencies.py +106 -32
  58. django_cfg/apps/payments/models/managers/currency_managers.py +65 -0
  59. django_cfg/apps/payments/services/core/currency_service.py +35 -28
  60. django_cfg/apps/payments/services/core/payment_service.py +1 -1
  61. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  62. django_cfg/apps/payments/services/providers/base.py +95 -39
  63. django_cfg/apps/payments/services/providers/models/__init__.py +40 -0
  64. django_cfg/apps/payments/services/providers/models/base.py +122 -0
  65. django_cfg/apps/payments/services/providers/models/providers.py +87 -0
  66. django_cfg/apps/payments/services/providers/models/universal.py +48 -0
  67. django_cfg/apps/payments/services/providers/nowpayments/__init__.py +31 -0
  68. django_cfg/apps/payments/services/providers/nowpayments/config.py +70 -0
  69. django_cfg/apps/payments/services/providers/nowpayments/models.py +150 -0
  70. django_cfg/apps/payments/services/providers/nowpayments/parsers.py +879 -0
  71. django_cfg/apps/payments/services/providers/{nowpayments.py → nowpayments/provider.py} +240 -209
  72. django_cfg/apps/payments/services/providers/nowpayments/sync.py +196 -0
  73. django_cfg/apps/payments/services/providers/registry.py +4 -32
  74. django_cfg/apps/payments/services/providers/sync_service.py +277 -0
  75. django_cfg/apps/payments/static/payments/js/api-client.js +23 -5
  76. django_cfg/apps/payments/static/payments/js/payment-form.js +65 -8
  77. django_cfg/apps/payments/tasks/__init__.py +39 -0
  78. django_cfg/apps/payments/tasks/types.py +73 -0
  79. django_cfg/apps/payments/tasks/usage_tracking.py +308 -0
  80. django_cfg/apps/payments/templates/admin/payments/_components/dashboard_header.html +23 -0
  81. django_cfg/apps/payments/templates/admin/payments/_components/stats_card.html +25 -0
  82. django_cfg/apps/payments/templates/admin/payments/_components/stats_grid.html +16 -0
  83. django_cfg/apps/payments/templates/admin/payments/apikey/change_list.html +39 -0
  84. django_cfg/apps/payments/templates/admin/payments/balance/change_list.html +50 -0
  85. django_cfg/apps/payments/templates/admin/payments/currency/change_list.html +40 -0
  86. django_cfg/apps/payments/templates/admin/payments/payment/change_list.html +48 -0
  87. django_cfg/apps/payments/templates/admin/payments/subscription/change_list.html +48 -0
  88. django_cfg/apps/payments/urls_admin.py +1 -1
  89. django_cfg/apps/payments/views/api/currencies.py +5 -5
  90. django_cfg/apps/payments/views/overview/services.py +2 -2
  91. django_cfg/apps/payments/views/serializers/currencies.py +4 -3
  92. django_cfg/apps/support/admin/__init__.py +10 -1
  93. django_cfg/apps/support/admin/support_admin.py +338 -141
  94. django_cfg/apps/tasks/admin/__init__.py +11 -0
  95. django_cfg/apps/tasks/admin/tasks_admin.py +430 -0
  96. django_cfg/config.py +1 -1
  97. django_cfg/core/config.py +10 -5
  98. django_cfg/core/generation.py +1 -1
  99. django_cfg/management/commands/__init__.py +13 -1
  100. django_cfg/management/commands/app_agent_diagnose.py +470 -0
  101. django_cfg/management/commands/app_agent_generate.py +342 -0
  102. django_cfg/management/commands/app_agent_info.py +308 -0
  103. django_cfg/management/commands/migrate_all.py +9 -3
  104. django_cfg/management/commands/migrator.py +11 -6
  105. django_cfg/management/commands/rundramatiq.py +3 -2
  106. django_cfg/middleware/__init__.py +0 -2
  107. django_cfg/models/api_keys.py +115 -0
  108. django_cfg/modules/django_admin/__init__.py +64 -0
  109. django_cfg/modules/django_admin/decorators/__init__.py +13 -0
  110. django_cfg/modules/django_admin/decorators/actions.py +106 -0
  111. django_cfg/modules/django_admin/decorators/display.py +106 -0
  112. django_cfg/modules/django_admin/mixins/__init__.py +14 -0
  113. django_cfg/modules/django_admin/mixins/display_mixin.py +81 -0
  114. django_cfg/modules/django_admin/mixins/optimization_mixin.py +41 -0
  115. django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +202 -0
  116. django_cfg/modules/django_admin/models/__init__.py +20 -0
  117. django_cfg/modules/django_admin/models/action_models.py +33 -0
  118. django_cfg/modules/django_admin/models/badge_models.py +20 -0
  119. django_cfg/modules/django_admin/models/base.py +26 -0
  120. django_cfg/modules/django_admin/models/display_models.py +31 -0
  121. django_cfg/modules/django_admin/utils/badges.py +159 -0
  122. django_cfg/modules/django_admin/utils/displays.py +247 -0
  123. django_cfg/modules/django_app_agent/__init__.py +87 -0
  124. django_cfg/modules/django_app_agent/agents/__init__.py +40 -0
  125. django_cfg/modules/django_app_agent/agents/base/__init__.py +24 -0
  126. django_cfg/modules/django_app_agent/agents/base/agent.py +354 -0
  127. django_cfg/modules/django_app_agent/agents/base/context.py +236 -0
  128. django_cfg/modules/django_app_agent/agents/base/executor.py +430 -0
  129. django_cfg/modules/django_app_agent/agents/generation/__init__.py +12 -0
  130. django_cfg/modules/django_app_agent/agents/generation/app_generator/__init__.py +15 -0
  131. django_cfg/modules/django_app_agent/agents/generation/app_generator/config_validator.py +147 -0
  132. django_cfg/modules/django_app_agent/agents/generation/app_generator/main.py +99 -0
  133. django_cfg/modules/django_app_agent/agents/generation/app_generator/models.py +32 -0
  134. django_cfg/modules/django_app_agent/agents/generation/app_generator/prompt_manager.py +290 -0
  135. django_cfg/modules/django_app_agent/agents/interfaces.py +376 -0
  136. django_cfg/modules/django_app_agent/core/__init__.py +33 -0
  137. django_cfg/modules/django_app_agent/core/config.py +300 -0
  138. django_cfg/modules/django_app_agent/core/exceptions.py +359 -0
  139. django_cfg/modules/django_app_agent/models/__init__.py +71 -0
  140. django_cfg/modules/django_app_agent/models/base.py +283 -0
  141. django_cfg/modules/django_app_agent/models/context.py +496 -0
  142. django_cfg/modules/django_app_agent/models/enums.py +481 -0
  143. django_cfg/modules/django_app_agent/models/requests.py +500 -0
  144. django_cfg/modules/django_app_agent/models/responses.py +585 -0
  145. django_cfg/modules/django_app_agent/pytest.ini +6 -0
  146. django_cfg/modules/django_app_agent/services/__init__.py +42 -0
  147. django_cfg/modules/django_app_agent/services/app_generator/__init__.py +30 -0
  148. django_cfg/modules/django_app_agent/services/app_generator/ai_integration.py +133 -0
  149. django_cfg/modules/django_app_agent/services/app_generator/context.py +40 -0
  150. django_cfg/modules/django_app_agent/services/app_generator/main.py +202 -0
  151. django_cfg/modules/django_app_agent/services/app_generator/structure.py +316 -0
  152. django_cfg/modules/django_app_agent/services/app_generator/validation.py +125 -0
  153. django_cfg/modules/django_app_agent/services/base.py +437 -0
  154. django_cfg/modules/django_app_agent/services/context_builder/__init__.py +34 -0
  155. django_cfg/modules/django_app_agent/services/context_builder/code_extractor.py +141 -0
  156. django_cfg/modules/django_app_agent/services/context_builder/context_generator.py +276 -0
  157. django_cfg/modules/django_app_agent/services/context_builder/main.py +272 -0
  158. django_cfg/modules/django_app_agent/services/context_builder/models.py +40 -0
  159. django_cfg/modules/django_app_agent/services/context_builder/pattern_analyzer.py +85 -0
  160. django_cfg/modules/django_app_agent/services/project_scanner/__init__.py +31 -0
  161. django_cfg/modules/django_app_agent/services/project_scanner/app_discovery.py +311 -0
  162. django_cfg/modules/django_app_agent/services/project_scanner/main.py +221 -0
  163. django_cfg/modules/django_app_agent/services/project_scanner/models.py +59 -0
  164. django_cfg/modules/django_app_agent/services/project_scanner/pattern_detection.py +94 -0
  165. django_cfg/modules/django_app_agent/services/questioning_service/__init__.py +28 -0
  166. django_cfg/modules/django_app_agent/services/questioning_service/main.py +273 -0
  167. django_cfg/modules/django_app_agent/services/questioning_service/models.py +111 -0
  168. django_cfg/modules/django_app_agent/services/questioning_service/question_generator.py +251 -0
  169. django_cfg/modules/django_app_agent/services/questioning_service/response_processor.py +347 -0
  170. django_cfg/modules/django_app_agent/services/questioning_service/session_manager.py +356 -0
  171. django_cfg/modules/django_app_agent/services/report_service.py +332 -0
  172. django_cfg/modules/django_app_agent/services/template_manager/__init__.py +18 -0
  173. django_cfg/modules/django_app_agent/services/template_manager/jinja_engine.py +236 -0
  174. django_cfg/modules/django_app_agent/services/template_manager/main.py +159 -0
  175. django_cfg/modules/django_app_agent/services/template_manager/models.py +36 -0
  176. django_cfg/modules/django_app_agent/services/template_manager/template_loader.py +100 -0
  177. django_cfg/modules/django_app_agent/services/template_manager/templates/admin.py.j2 +105 -0
  178. django_cfg/modules/django_app_agent/services/template_manager/templates/apps.py.j2 +31 -0
  179. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_config.py.j2 +44 -0
  180. django_cfg/modules/django_app_agent/services/template_manager/templates/cfg_module.py.j2 +81 -0
  181. django_cfg/modules/django_app_agent/services/template_manager/templates/forms.py.j2 +107 -0
  182. django_cfg/modules/django_app_agent/services/template_manager/templates/models.py.j2 +139 -0
  183. django_cfg/modules/django_app_agent/services/template_manager/templates/serializers.py.j2 +91 -0
  184. django_cfg/modules/django_app_agent/services/template_manager/templates/tests.py.j2 +195 -0
  185. django_cfg/modules/django_app_agent/services/template_manager/templates/urls.py.j2 +35 -0
  186. django_cfg/modules/django_app_agent/services/template_manager/templates/views.py.j2 +211 -0
  187. django_cfg/modules/django_app_agent/services/template_manager/variable_processor.py +200 -0
  188. django_cfg/modules/django_app_agent/services/validation_service/__init__.py +25 -0
  189. django_cfg/modules/django_app_agent/services/validation_service/django_validator.py +333 -0
  190. django_cfg/modules/django_app_agent/services/validation_service/main.py +242 -0
  191. django_cfg/modules/django_app_agent/services/validation_service/models.py +66 -0
  192. django_cfg/modules/django_app_agent/services/validation_service/quality_validator.py +352 -0
  193. django_cfg/modules/django_app_agent/services/validation_service/security_validator.py +272 -0
  194. django_cfg/modules/django_app_agent/services/validation_service/syntax_validator.py +203 -0
  195. django_cfg/modules/django_app_agent/ui/__init__.py +25 -0
  196. django_cfg/modules/django_app_agent/ui/cli.py +419 -0
  197. django_cfg/modules/django_app_agent/ui/rich_components.py +622 -0
  198. django_cfg/modules/django_app_agent/utils/__init__.py +38 -0
  199. django_cfg/modules/django_app_agent/utils/logging.py +360 -0
  200. django_cfg/modules/django_app_agent/utils/validation.py +417 -0
  201. django_cfg/modules/django_currency/__init__.py +2 -2
  202. django_cfg/modules/django_currency/clients/__init__.py +2 -2
  203. django_cfg/modules/django_currency/clients/hybrid_client.py +587 -0
  204. django_cfg/modules/django_currency/core/converter.py +12 -12
  205. django_cfg/modules/django_currency/database/__init__.py +2 -2
  206. django_cfg/modules/django_currency/database/database_loader.py +93 -42
  207. django_cfg/modules/django_llm/llm/client.py +10 -2
  208. django_cfg/modules/django_unfold/callbacks/actions.py +1 -1
  209. django_cfg/modules/django_unfold/callbacks/statistics.py +1 -1
  210. django_cfg/modules/django_unfold/dashboard.py +14 -13
  211. django_cfg/modules/django_unfold/models/config.py +1 -1
  212. django_cfg/registry/core.py +3 -0
  213. django_cfg/registry/third_party.py +2 -2
  214. django_cfg/template_archive/django_sample.zip +0 -0
  215. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/METADATA +2 -1
  216. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/RECORD +223 -117
  217. django_cfg/apps/accounts/admin/activity.py +0 -96
  218. django_cfg/apps/accounts/admin/group.py +0 -17
  219. django_cfg/apps/accounts/admin/otp.py +0 -59
  220. django_cfg/apps/accounts/admin/registration_source.py +0 -97
  221. django_cfg/apps/accounts/admin/twilio_response.py +0 -227
  222. django_cfg/apps/accounts/admin/user.py +0 -300
  223. django_cfg/apps/agents/core/agent.py +0 -281
  224. django_cfg/apps/payments/admin_interface/old/payments/base.html +0 -175
  225. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +0 -125
  226. django_cfg/apps/payments/admin_interface/old/payments/components/loading_spinner.html +0 -16
  227. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +0 -113
  228. django_cfg/apps/payments/admin_interface/old/payments/components/notification.html +0 -27
  229. django_cfg/apps/payments/admin_interface/old/payments/components/provider_card.html +0 -86
  230. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +0 -35
  231. django_cfg/apps/payments/admin_interface/old/payments/currency_converter.html +0 -382
  232. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +0 -309
  233. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +0 -303
  234. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +0 -382
  235. django_cfg/apps/payments/admin_interface/old/payments/payment_status.html +0 -500
  236. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +0 -518
  237. django_cfg/apps/payments/admin_interface/old/static/payments/css/components.css +0 -619
  238. django_cfg/apps/payments/admin_interface/old/static/payments/css/dashboard.css +0 -188
  239. django_cfg/apps/payments/admin_interface/old/static/payments/js/components.js +0 -545
  240. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +0 -163
  241. django_cfg/apps/payments/admin_interface/old/static/payments/js/utils.js +0 -412
  242. django_cfg/apps/tasks/admin.py +0 -320
  243. django_cfg/middleware/static_nocache.py +0 -55
  244. django_cfg/modules/django_currency/clients/yahoo_client.py +0 -157
  245. /django_cfg/modules/{django_unfold → django_admin}/icons/README.md +0 -0
  246. /django_cfg/modules/{django_unfold → django_admin}/icons/__init__.py +0 -0
  247. /django_cfg/modules/{django_unfold → django_admin}/icons/constants.py +0 -0
  248. /django_cfg/modules/{django_unfold → django_admin}/icons/generate_icons.py +0 -0
  249. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/WHEEL +0 -0
  250. {django_cfg-1.3.7.dist-info → django_cfg-1.3.9.dist-info}/entry_points.txt +0 -0
  251. {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 YahooFinanceClient, CoinPaprikaClient
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
- "YahooFinanceClient",
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 .yahoo_client import YahooFinanceClient
5
+ from .hybrid_client import HybridCurrencyClient
6
6
  from .coinpaprika_client import CoinPaprikaClient
7
7
 
8
8
  __all__ = [
9
- 'YahooFinanceClient',
9
+ 'HybridCurrencyClient',
10
10
  'CoinPaprikaClient'
11
11
  ]