django-cfg 1.3.13__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/accounts/admin/user_admin.py +39 -16
- django_cfg/apps/accounts/serializers/profile.py +1 -1
- django_cfg/apps/accounts/services/otp_service.py +18 -11
- django_cfg/apps/accounts/signals.py +15 -24
- django_cfg/apps/accounts/utils/notifications.py +217 -358
- django_cfg/apps/accounts/views/otp.py +2 -2
- django_cfg/apps/accounts/views/webhook.py +1 -1
- django_cfg/apps/agents/core/django_agent.py +1 -1
- django_cfg/apps/api/commands/views.py +66 -83
- django_cfg/apps/api/health/drf_views.py +269 -0
- django_cfg/apps/api/health/serializers.py +45 -0
- django_cfg/apps/api/health/urls.py +6 -1
- django_cfg/apps/knowbase/admin/actions/__init__.py +13 -0
- django_cfg/apps/knowbase/admin/actions/visibility_actions.py +56 -0
- django_cfg/apps/knowbase/admin/document_admin.py +136 -270
- django_cfg/apps/knowbase/admin/helpers/__init__.py +17 -0
- django_cfg/apps/knowbase/admin/helpers/configs.py +72 -0
- django_cfg/apps/knowbase/admin/helpers/display_helpers.py +156 -0
- django_cfg/apps/knowbase/admin/helpers/statistics.py +108 -0
- django_cfg/apps/knowbase/config/constance_fields.py +1 -1
- django_cfg/apps/knowbase/config/settings.py +2 -2
- django_cfg/apps/knowbase/mixins/__init__.py +19 -2
- django_cfg/apps/knowbase/mixins/config/__init__.py +14 -0
- django_cfg/apps/knowbase/mixins/config/defaults.py +75 -0
- django_cfg/apps/knowbase/mixins/config/meta_config.py +120 -0
- django_cfg/apps/knowbase/mixins/creator.py +10 -10
- django_cfg/apps/knowbase/mixins/external_data_mixin.py +105 -403
- django_cfg/apps/knowbase/mixins/generators/__init__.py +16 -0
- django_cfg/apps/knowbase/mixins/generators/content_generator.py +218 -0
- django_cfg/apps/knowbase/mixins/generators/field_analyzer.py +76 -0
- django_cfg/apps/knowbase/mixins/generators/metadata_generator.py +124 -0
- django_cfg/apps/knowbase/mixins/service.py +2 -2
- django_cfg/apps/knowbase/services/archive/__init__.py +1 -0
- django_cfg/apps/knowbase/services/archive/analyzers/__init__.py +17 -0
- django_cfg/apps/knowbase/services/archive/analyzers/complexity_analyzer.py +33 -0
- django_cfg/apps/knowbase/services/archive/analyzers/purpose_detector.py +36 -0
- django_cfg/apps/knowbase/services/archive/analyzers/quality_analyzer.py +39 -0
- django_cfg/apps/knowbase/services/archive/analyzers/tag_generator.py +103 -0
- django_cfg/apps/knowbase/services/archive/chunking/__init__.py +19 -0
- django_cfg/apps/knowbase/services/archive/chunking/base.py +81 -0
- django_cfg/apps/knowbase/services/archive/chunking/json_chunker.py +62 -0
- django_cfg/apps/knowbase/services/archive/chunking/markdown_chunker.py +107 -0
- django_cfg/apps/knowbase/services/archive/chunking/python_chunker.py +248 -0
- django_cfg/apps/knowbase/services/archive/chunking/text_chunker.py +70 -0
- django_cfg/apps/knowbase/services/archive/chunking_service.py +110 -729
- django_cfg/apps/knowbase/services/archive/context/__init__.py +14 -0
- django_cfg/apps/knowbase/services/archive/context/builders.py +220 -0
- django_cfg/apps/knowbase/services/archive/context/models.py +38 -0
- django_cfg/apps/knowbase/services/embedding/models.py +18 -14
- django_cfg/apps/knowbase/services/embedding/processors.py +6 -3
- django_cfg/apps/knowbase/tasks/document_processing.py +11 -3
- django_cfg/apps/leads/tests.py +1 -1
- django_cfg/apps/payments/admin/api_keys_admin.py +1 -1
- django_cfg/apps/payments/admin/balance_admin.py +1 -1
- django_cfg/apps/payments/admin/currencies_admin.py +1 -1
- django_cfg/apps/payments/admin/payments_admin.py +1 -1
- django_cfg/apps/payments/admin/subscriptions_admin.py +1 -1
- django_cfg/apps/payments/admin_interface/templates/payments/base.html +59 -126
- django_cfg/apps/payments/admin_interface/views/api/payments.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/stats.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/users.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +1 -1
- django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +1 -1
- django_cfg/apps/payments/admin_interface/views/base.py +29 -2
- django_cfg/apps/payments/apps.py +1 -1
- django_cfg/apps/payments/config/django_cfg_integration.py +2 -2
- django_cfg/apps/payments/config/helpers.py +3 -2
- django_cfg/apps/payments/management/commands/cleanup_expired_data.py +1 -1
- django_cfg/apps/payments/management/commands/currency_stats.py +1 -1
- django_cfg/apps/payments/management/commands/manage_currencies.py +1 -1
- django_cfg/apps/payments/management/commands/manage_providers.py +1 -1
- django_cfg/apps/payments/management/commands/process_pending_payments.py +1 -1
- django_cfg/apps/payments/management/commands/test_providers.py +1 -1
- django_cfg/apps/payments/middleware/api_access.py +1 -1
- django_cfg/apps/payments/middleware/rate_limiting.py +1 -1
- django_cfg/apps/payments/middleware/usage_tracking.py +1 -1
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/managers/api_key_managers.py +1 -1
- django_cfg/apps/payments/models/managers/balance_managers.py +1 -1
- django_cfg/apps/payments/models/managers/currency_managers.py +1 -1
- django_cfg/apps/payments/models/managers/payment_managers.py +1 -1
- django_cfg/apps/payments/models/managers/subscription_managers.py +1 -1
- django_cfg/apps/payments/models/payments.py +2 -2
- django_cfg/apps/payments/services/cache_service/__init__.py +1 -1
- django_cfg/apps/payments/services/cache_service/simple_cache.py +10 -5
- django_cfg/apps/payments/services/core/base.py +1 -1
- django_cfg/apps/payments/services/core/currency/__init__.py +13 -0
- django_cfg/apps/payments/services/core/currency/currency_converter.py +57 -0
- django_cfg/apps/payments/services/core/currency/currency_validator.py +61 -0
- django_cfg/apps/payments/services/core/operations/__init__.py +15 -0
- django_cfg/apps/payments/services/core/operations/payment_canceller.py +100 -0
- django_cfg/apps/payments/services/core/operations/payment_creator.py +196 -0
- django_cfg/apps/payments/services/core/operations/status_checker.py +100 -0
- django_cfg/apps/payments/services/core/payment_service.py +124 -612
- django_cfg/apps/payments/services/core/providers/__init__.py +13 -0
- django_cfg/apps/payments/services/core/providers/provider_client.py +132 -0
- django_cfg/apps/payments/services/core/providers/status_mapper.py +89 -0
- django_cfg/apps/payments/services/core/utils/__init__.py +13 -0
- django_cfg/apps/payments/services/core/utils/data_converter.py +48 -0
- django_cfg/apps/payments/services/core/utils/statistics_calculator.py +69 -0
- django_cfg/apps/payments/services/providers/base.py +1 -1
- django_cfg/apps/payments/services/providers/nowpayments/__init__.py +3 -3
- django_cfg/apps/payments/services/providers/nowpayments/parsers/__init__.py +9 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/__init__.py +23 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/constants.py +23 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/currency_names.py +244 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/data/patterns.py +511 -0
- django_cfg/apps/payments/services/providers/nowpayments/parsers/parser.py +168 -0
- django_cfg/apps/payments/services/providers/nowpayments/provider.py +1 -1
- django_cfg/apps/payments/services/providers/nowpayments/sync.py +1 -1
- django_cfg/apps/payments/services/providers/registry.py +1 -1
- django_cfg/apps/payments/services/providers/sync_service.py +1 -1
- django_cfg/apps/payments/signals/__init__.py +1 -1
- django_cfg/apps/payments/signals/api_key_signals.py +1 -1
- django_cfg/apps/payments/signals/balance_signals.py +1 -1
- django_cfg/apps/payments/signals/payment_signals.py +1 -1
- django_cfg/apps/payments/signals/subscription_signals.py +1 -1
- django_cfg/apps/payments/views/api/api_keys.py +1 -1
- django_cfg/apps/payments/views/api/balances.py +1 -1
- django_cfg/apps/payments/views/api/base.py +1 -1
- django_cfg/apps/payments/views/api/currencies.py +1 -1
- django_cfg/apps/payments/views/api/payments.py +1 -1
- django_cfg/apps/payments/views/api/subscriptions.py +1 -1
- django_cfg/apps/payments/views/api/webhooks.py +1 -1
- django_cfg/apps/payments/views/serializers/api_keys.py +1 -1
- django_cfg/apps/payments/views/serializers/balances.py +1 -1
- django_cfg/apps/payments/views/serializers/currencies.py +1 -1
- django_cfg/apps/payments/views/serializers/payments.py +1 -1
- django_cfg/apps/payments/views/serializers/subscriptions.py +1 -1
- django_cfg/apps/payments/views/serializers/webhooks.py +1 -1
- django_cfg/apps/support/admin/support_admin.py +21 -13
- django_cfg/apps/support/templates/support/chat/access_denied.html +21 -27
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +183 -254
- django_cfg/apps/support/utils/support_email_service.py +1 -1
- django_cfg/apps/tasks/templates/tasks/layout/base.html +20 -115
- django_cfg/apps/tasks/utils/simulator.py +1 -1
- django_cfg/apps/tasks/views/dashboard.py +33 -3
- django_cfg/apps/urls.py +5 -1
- django_cfg/cli/README.md +57 -471
- django_cfg/cli/commands/create_project.py +140 -529
- django_cfg/cli/main.py +13 -10
- django_cfg/core/__init__.py +63 -6
- django_cfg/core/base/__init__.py +5 -0
- django_cfg/core/base/config_model.py +652 -0
- django_cfg/core/builders/__init__.py +11 -0
- django_cfg/core/builders/apps_builder.py +258 -0
- django_cfg/core/builders/middleware_builder.py +115 -0
- django_cfg/core/builders/security_builder.py +96 -0
- django_cfg/core/config.py +20 -892
- django_cfg/core/constants.py +69 -0
- django_cfg/core/environment/__init__.py +9 -0
- django_cfg/core/exceptions.py +45 -298
- django_cfg/core/generation/__init__.py +51 -0
- django_cfg/core/generation/core_generators/__init__.py +0 -0
- django_cfg/core/generation/core_generators/settings.py +90 -0
- django_cfg/core/generation/core_generators/static.py +82 -0
- django_cfg/core/generation/core_generators/templates.py +141 -0
- django_cfg/core/generation/data_generators/__init__.py +15 -0
- django_cfg/core/generation/data_generators/cache.py +132 -0
- django_cfg/core/generation/data_generators/database.py +117 -0
- django_cfg/core/generation/generation.py +92 -0
- django_cfg/core/generation/integration_generators/__init__.py +21 -0
- django_cfg/core/generation/integration_generators/api.py +237 -0
- django_cfg/core/generation/integration_generators/sessions.py +65 -0
- django_cfg/core/generation/integration_generators/tailwind.py +54 -0
- django_cfg/core/generation/integration_generators/tasks.py +92 -0
- django_cfg/core/generation/integration_generators/third_party.py +144 -0
- django_cfg/core/generation/orchestrator.py +285 -0
- django_cfg/core/generation/protocols.py +30 -0
- django_cfg/core/generation/security_generators/__init__.py +0 -0
- django_cfg/core/generation/utility_generators/__init__.py +24 -0
- django_cfg/core/generation/utility_generators/email.py +58 -0
- django_cfg/core/generation/utility_generators/i18n.py +66 -0
- django_cfg/core/generation/utility_generators/limits.py +58 -0
- django_cfg/core/generation/utility_generators/logging.py +66 -0
- django_cfg/core/generation/utility_generators/security.py +101 -0
- django_cfg/core/generation/utils/__init__.py +0 -0
- django_cfg/core/generation/utils/helpers.py +32 -0
- django_cfg/core/integration/__init__.py +18 -25
- django_cfg/core/integration/display/startup.py +146 -133
- django_cfg/core/integration/url_integration.py +13 -2
- django_cfg/core/services/__init__.py +5 -0
- django_cfg/core/services/config_service.py +121 -0
- django_cfg/core/state/__init__.py +9 -0
- django_cfg/core/state/registry.py +84 -0
- django_cfg/core/types/__init__.py +15 -0
- django_cfg/core/types/aliases.py +15 -0
- django_cfg/core/types/enums.py +49 -0
- django_cfg/dashboard/DEBUG_README.md +105 -0
- django_cfg/dashboard/REFACTORING_SUMMARY.md +237 -0
- django_cfg/dashboard/__init__.py +24 -0
- django_cfg/dashboard/components.py +308 -0
- django_cfg/dashboard/debug.py +176 -0
- django_cfg/dashboard/management/__init__.py +0 -0
- django_cfg/dashboard/management/commands/__init__.py +0 -0
- django_cfg/dashboard/management/commands/debug_dashboard.py +109 -0
- django_cfg/dashboard/sections/__init__.py +1 -0
- django_cfg/dashboard/sections/base.py +128 -0
- django_cfg/dashboard/sections/commands.py +32 -0
- django_cfg/dashboard/sections/overview.py +394 -0
- django_cfg/dashboard/sections/stats.py +48 -0
- django_cfg/dashboard/sections/system.py +73 -0
- django_cfg/management/commands/check_settings.py +6 -2
- django_cfg/management/commands/clear_constance.py +6 -1
- django_cfg/management/commands/create_token.py +5 -4
- django_cfg/management/commands/generate.py +5 -0
- django_cfg/management/commands/list_urls.py +7 -2
- django_cfg/management/commands/migrate_all.py +6 -2
- django_cfg/management/commands/migrator.py +6 -1
- django_cfg/management/commands/rundramatiq.py +6 -1
- django_cfg/management/commands/rundramatiq_simulator.py +11 -4
- django_cfg/management/commands/runserver_ngrok.py +9 -7
- django_cfg/management/commands/script.py +25 -21
- django_cfg/management/commands/show_config.py +6 -1
- django_cfg/management/commands/show_urls.py +8 -3
- django_cfg/management/commands/superuser.py +5 -4
- django_cfg/management/commands/task_clear.py +8 -3
- django_cfg/management/commands/task_status.py +8 -3
- django_cfg/management/commands/test_email.py +6 -1
- django_cfg/management/commands/test_telegram.py +6 -1
- django_cfg/management/commands/test_twilio.py +6 -1
- django_cfg/management/commands/tree.py +7 -4
- django_cfg/models/__init__.py +88 -3
- django_cfg/models/api/__init__.py +27 -0
- django_cfg/models/{api.py → api/config.py} +1 -1
- django_cfg/models/api/drf/__init__.py +21 -0
- django_cfg/models/api/drf/config.py +101 -0
- django_cfg/models/api/drf/redoc.py +31 -0
- django_cfg/models/api/drf/spectacular.py +129 -0
- django_cfg/models/api/drf/swagger.py +59 -0
- django_cfg/models/{api_keys.py → api/keys.py} +16 -6
- django_cfg/models/{limits.py → api/limits.py} +0 -1
- django_cfg/models/base/__init__.py +14 -0
- django_cfg/models/django/__init__.py +16 -0
- django_cfg/models/{constance.py → django/constance.py} +1 -1
- django_cfg/models/{environment.py → django/environment.py} +1 -1
- django_cfg/models/infrastructure/__init__.py +17 -0
- django_cfg/models/{cache.py → infrastructure/cache.py} +3 -2
- django_cfg/models/infrastructure/database/__init__.py +22 -0
- django_cfg/models/infrastructure/database/config.py +265 -0
- django_cfg/models/infrastructure/database/converters.py +91 -0
- django_cfg/models/infrastructure/database/parsers.py +96 -0
- django_cfg/models/infrastructure/database/routing.py +85 -0
- django_cfg/models/infrastructure/database/validators.py +170 -0
- django_cfg/models/{logging.py → infrastructure/logging.py} +1 -1
- django_cfg/models/{security.py → infrastructure/security.py} +2 -2
- django_cfg/models/ngrok/__init__.py +11 -0
- django_cfg/models/ngrok/auth.py +37 -0
- django_cfg/models/ngrok/config.py +77 -0
- django_cfg/models/ngrok/tunnel.py +35 -0
- django_cfg/models/payments/__init__.py +20 -0
- django_cfg/models/payments/api_keys.py +57 -0
- django_cfg/models/{payments.py → payments/config.py} +56 -154
- django_cfg/models/payments/providers/__init__.py +15 -0
- django_cfg/models/payments/providers/base.py +25 -0
- django_cfg/models/payments/providers/nowpayments.py +48 -0
- django_cfg/models/services/__init__.py +18 -0
- django_cfg/models/services/base.py +65 -0
- django_cfg/models/{email.py → services/email.py} +1 -1
- django_cfg/models/services/telegram.py +172 -0
- django_cfg/models/tasks/__init__.py +51 -0
- django_cfg/models/tasks/backends.py +250 -0
- django_cfg/models/tasks/config.py +314 -0
- django_cfg/models/tasks/utils.py +174 -0
- django_cfg/modules/base.py +18 -3
- django_cfg/modules/django_admin/decorators/actions.py +1 -1
- django_cfg/modules/django_admin/decorators/display.py +1 -1
- django_cfg/modules/django_admin/mixins/standalone_actions_mixin.py +1 -1
- django_cfg/modules/django_cfg_rpc_client/README.md +346 -0
- django_cfg/modules/django_cfg_rpc_client/__init__.py +51 -0
- django_cfg/modules/django_cfg_rpc_client/client.py +540 -0
- django_cfg/modules/django_cfg_rpc_client/config.py +207 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/README.md +517 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/UNFOLD_INTEGRATION.md +439 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/__init__.py +11 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/apps.py +22 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/monitor.py +435 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/static/django_cfg_rpc_dashboard/js/dashboard.js +373 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/templates/django_cfg_rpc_dashboard/base.html +76 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/templates/django_cfg_rpc_dashboard/dashboard.html +200 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/urls.py +22 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/urls_admin.py +9 -0
- django_cfg/modules/django_cfg_rpc_client/dashboard/views.py +251 -0
- django_cfg/modules/django_cfg_rpc_client/exceptions.py +201 -0
- django_cfg/modules/django_drf_theme/CHANGELOG.md +210 -0
- django_cfg/modules/django_drf_theme/EXAMPLE.md +465 -0
- django_cfg/modules/django_drf_theme/IMPLEMENTATION.md +232 -0
- django_cfg/modules/django_drf_theme/README.md +207 -0
- django_cfg/modules/django_drf_theme/TAILWIND_CDN_GUIDE.md +274 -0
- django_cfg/modules/django_drf_theme/__init__.py +23 -0
- django_cfg/modules/django_drf_theme/apps.py +15 -0
- django_cfg/modules/django_drf_theme/renderers.py +58 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/api.html +375 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/base.html +938 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/forms/filter_form.html +132 -0
- django_cfg/modules/django_drf_theme/templates/rest_framework/tailwind/forms/raw_data_form.html +123 -0
- django_cfg/modules/django_drf_theme/templatetags/__init__.py +1 -0
- django_cfg/modules/django_drf_theme/templatetags/tailwind_tags.py +57 -0
- django_cfg/modules/django_email/__init__.py +14 -0
- django_cfg/modules/{django_email.py → django_email/service.py} +78 -113
- django_cfg/modules/django_email/utils.py +40 -0
- django_cfg/modules/django_health/__init__.py +9 -0
- django_cfg/modules/{django_health.py → django_health/service.py} +23 -21
- django_cfg/modules/django_llm/llm/client.py +155 -550
- django_cfg/modules/django_llm/llm/embeddings/__init__.py +13 -0
- django_cfg/modules/django_llm/llm/embeddings/mock_embedder.py +106 -0
- django_cfg/modules/django_llm/llm/embeddings/openai_embedder.py +79 -0
- django_cfg/modules/django_llm/llm/models_api/__init__.py +9 -0
- django_cfg/modules/django_llm/llm/models_api/models_query.py +163 -0
- django_cfg/modules/django_llm/llm/providers/__init__.py +15 -0
- django_cfg/modules/django_llm/llm/providers/config_builder.py +103 -0
- django_cfg/modules/django_llm/llm/providers/provider_manager.py +148 -0
- django_cfg/modules/django_llm/llm/providers/provider_selector.py +60 -0
- django_cfg/modules/django_llm/llm/requests/__init__.py +15 -0
- django_cfg/modules/django_llm/llm/requests/cache_manager.py +170 -0
- django_cfg/modules/django_llm/llm/requests/chat_handler.py +199 -0
- django_cfg/modules/django_llm/llm/requests/embedding_handler.py +113 -0
- django_cfg/modules/django_llm/llm/responses/__init__.py +9 -0
- django_cfg/modules/django_llm/llm/responses/response_builder.py +131 -0
- django_cfg/modules/django_llm/llm/stats/__init__.py +9 -0
- django_cfg/modules/django_llm/llm/stats/stats_manager.py +107 -0
- django_cfg/modules/django_llm/translator/detectors/__init__.py +13 -0
- django_cfg/modules/django_llm/translator/detectors/language_detector.py +90 -0
- django_cfg/modules/django_llm/translator/detectors/script_detector.py +153 -0
- django_cfg/modules/django_llm/translator/stats/__init__.py +11 -0
- django_cfg/modules/django_llm/translator/stats/stats_tracker.py +85 -0
- django_cfg/modules/django_llm/translator/translator.py +150 -603
- django_cfg/modules/django_llm/translator/translators/__init__.py +15 -0
- django_cfg/modules/django_llm/translator/translators/json_translator.py +316 -0
- django_cfg/modules/django_llm/translator/translators/text_translator.py +139 -0
- django_cfg/modules/django_llm/translator/utils/__init__.py +13 -0
- django_cfg/modules/django_llm/translator/utils/prompt_builder.py +110 -0
- django_cfg/modules/django_llm/translator/utils/text_utils.py +114 -0
- django_cfg/modules/django_logging/FIXES_SUMMARY.md +276 -0
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +504 -0
- django_cfg/modules/django_logging/__init__.py +14 -0
- django_cfg/modules/{django_logger.py → django_logging/django_logger.py} +13 -13
- django_cfg/modules/{logger.py → django_logging/logger.py} +14 -4
- django_cfg/modules/django_ngrok/__init__.py +39 -0
- django_cfg/modules/{django_ngrok.py → django_ngrok/service.py} +14 -42
- django_cfg/modules/django_rpc_old/POETRY.md +344 -0
- django_cfg/modules/django_rpc_old/README.md +397 -0
- django_cfg/modules/django_rpc_old/TESTING.md +358 -0
- django_cfg/modules/django_rpc_old/__init__.py +39 -0
- django_cfg/modules/django_rpc_old/client.py +531 -0
- django_cfg/modules/django_rpc_old/config.py +279 -0
- django_cfg/modules/django_rpc_old/exceptions.py +172 -0
- django_cfg/modules/django_tailwind/README.md +478 -0
- django_cfg/modules/django_tailwind/__init__.py +7 -0
- django_cfg/modules/django_tailwind/apps.py +10 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/app.html +5 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/base.html +117 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/components/navbar.html +124 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/components/theme_toggle.html +54 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/components/user_menu.html +116 -0
- django_cfg/modules/django_tailwind/templates/django_tailwind/simple.html +46 -0
- django_cfg/modules/django_tailwind/templatetags/__init__.py +1 -0
- django_cfg/modules/django_tailwind/templatetags/tailwind_info.py +185 -0
- django_cfg/modules/django_tasks/__init__.py +29 -0
- django_cfg/modules/django_tasks/factory.py +127 -0
- django_cfg/modules/{django_tasks.py → django_tasks/service.py} +45 -274
- django_cfg/modules/django_tasks/settings.py +107 -0
- django_cfg/modules/django_telegram/__init__.py +29 -0
- django_cfg/modules/{django_telegram.py → django_telegram/service.py} +45 -113
- django_cfg/modules/django_telegram/utils.py +62 -0
- django_cfg/modules/django_twilio/__init__.py +54 -107
- django_cfg/modules/django_twilio/_imports.py +30 -0
- django_cfg/modules/django_twilio/base.py +192 -0
- django_cfg/modules/django_twilio/email_otp.py +227 -0
- django_cfg/modules/django_twilio/sendgrid_service.py +1 -1
- django_cfg/modules/django_twilio/simple_service.py +1 -2
- django_cfg/modules/django_twilio/sms.py +94 -0
- django_cfg/modules/django_twilio/twilio_service.py +2 -3
- django_cfg/modules/django_twilio/unified.py +310 -0
- django_cfg/modules/django_twilio/utils.py +190 -0
- django_cfg/modules/django_twilio/whatsapp.py +137 -0
- django_cfg/modules/django_unfold/callbacks/base.py +198 -7
- django_cfg/modules/django_unfold/callbacks/main.py +102 -10
- django_cfg/modules/django_unfold/dashboard.py +65 -43
- django_cfg/modules/django_unfold/models/config.py +13 -12
- django_cfg/modules/django_unfold/models/navigation.py +8 -3
- django_cfg/modules/django_unfold/models/tabs.py +2 -2
- django_cfg/modules/django_unfold/templates/unfold/helpers/app_list.html +102 -0
- django_cfg/registry/core.py +24 -26
- django_cfg/registry/modules.py +5 -2
- django_cfg/registry/services.py +20 -3
- django_cfg/registry/third_party.py +8 -8
- django_cfg/static/admin/css/dashboard.css +260 -0
- django_cfg/static/admin/js/commands.js +171 -0
- django_cfg/static/admin/js/dashboard.js +126 -0
- django_cfg/templates/admin/components/management_commands.js +375 -0
- django_cfg/templates/admin/components/progress_bar.html +18 -23
- django_cfg/templates/admin/index.html +48 -20
- django_cfg/templates/admin/index_new.html +106 -0
- django_cfg/templates/admin/layouts/base_dashboard.html +60 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +1 -20
- django_cfg/templates/admin/sections/commands_section.html +626 -0
- django_cfg/templates/admin/sections/overview_section.html +112 -0
- django_cfg/templates/admin/sections/stats_section.html +35 -0
- django_cfg/templates/admin/sections/system_section.html +99 -0
- django_cfg/templates/admin/snippets/components/CHARTS_GUIDE.md +322 -0
- django_cfg/templates/admin/snippets/components/activity_tracker.html +85 -47
- django_cfg/templates/admin/snippets/components/charts_section.html +154 -64
- django_cfg/templates/admin/snippets/components/django_commands.html +3 -3
- django_cfg/templates/admin/snippets/components/recent_activity_improved.html +25 -0
- django_cfg/templates/admin/snippets/components/recent_users_table.html +1 -1
- django_cfg/templates/admin/snippets/components/system_metrics.html +179 -93
- django_cfg/templates/admin/snippets/zones/zones_table.html +2 -2
- django_cfg/templatetags/django_cfg.py +7 -1
- django_cfg/utils/smart_defaults.py +4 -4
- django_cfg-1.4.0.dist-info/METADATA +920 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.0.dist-info}/RECORD +424 -195
- django_cfg/apps/accounts/utils/auth_email_service.py +0 -84
- django_cfg/apps/payments/services/providers/nowpayments/parsers.py +0 -879
- django_cfg/core/generation.py +0 -621
- django_cfg/management/commands/validate_config.py +0 -189
- django_cfg/models/database.py +0 -480
- django_cfg/models/drf.py +0 -272
- django_cfg/models/ngrok.py +0 -122
- django_cfg/models/services.py +0 -440
- django_cfg/models/tasks.py +0 -550
- django_cfg/modules/django_twilio/service.py +0 -942
- django_cfg/template_archive/django_sample.zip +0 -0
- django_cfg/templates/rest_framework/api.html +0 -12
- django_cfg/utils/toolkit.py +0 -703
- django_cfg-1.3.13.dist-info/METADATA +0 -1029
- /django_cfg/apps/accounts/management/commands/{test_otp.py → otp_test.py} +0 -0
- /django_cfg/core/{environment.py → environment/detector.py} +0 -0
- /django_cfg/models/{cors.py → api/cors.py} +0 -0
- /django_cfg/models/{jwt.py → api/jwt.py} +0 -0
- /django_cfg/models/{base.py → base/config.py} +0 -0
- /django_cfg/models/{cfg.py → base/module.py} +0 -0
- /django_cfg/models/{revolution.py → django/revolution.py} +0 -0
- /django_cfg/modules/{dramatiq_setup.py → django_tasks/dramatiq_setup.py} +0 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.0.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.0.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,310 @@
|
|
1
|
+
"""
|
2
|
+
Unified OTP service with multi-channel support and DjangoTwilioService.
|
3
|
+
|
4
|
+
Provides intelligent channel selection and automatic fallback based on
|
5
|
+
configuration and delivery success rates.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Tuple, Optional, List, Dict, Any
|
10
|
+
from datetime import datetime
|
11
|
+
from asgiref.sync import sync_to_async
|
12
|
+
|
13
|
+
from .base import BaseTwilioService
|
14
|
+
from .whatsapp import WhatsAppOTPService
|
15
|
+
from .email_otp import EmailOTPService
|
16
|
+
from .sms import SMSOTPService
|
17
|
+
from .models import TwilioConfig, TwilioChannelType
|
18
|
+
from .exceptions import (
|
19
|
+
TwilioConfigurationError,
|
20
|
+
TwilioSendError,
|
21
|
+
TwilioVerificationError,
|
22
|
+
)
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
class UnifiedOTPService(BaseTwilioService):
|
28
|
+
"""
|
29
|
+
Unified OTP service that handles all channels with smart fallbacks.
|
30
|
+
|
31
|
+
Provides intelligent channel selection and automatic fallback
|
32
|
+
based on configuration and delivery success rates.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self):
|
36
|
+
"""Initialize with specialized service instances."""
|
37
|
+
super().__init__()
|
38
|
+
self._whatsapp_service = WhatsAppOTPService()
|
39
|
+
self._email_service = EmailOTPService()
|
40
|
+
self._sms_service = SMSOTPService()
|
41
|
+
|
42
|
+
def send_otp(
|
43
|
+
self,
|
44
|
+
identifier: str,
|
45
|
+
preferred_channel: Optional[TwilioChannelType] = None,
|
46
|
+
enable_fallback: bool = True
|
47
|
+
) -> Tuple[bool, str, TwilioChannelType]:
|
48
|
+
"""
|
49
|
+
Send OTP using the best available channel.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
identifier: Phone number (E.164) or email address
|
53
|
+
preferred_channel: Preferred delivery channel
|
54
|
+
enable_fallback: Whether to try fallback channels
|
55
|
+
|
56
|
+
Returns:
|
57
|
+
Tuple[bool, str, TwilioChannelType]: (success, message, used_channel)
|
58
|
+
"""
|
59
|
+
config = self.get_twilio_config()
|
60
|
+
|
61
|
+
# Determine identifier type
|
62
|
+
is_email = "@" in identifier
|
63
|
+
|
64
|
+
# Get available channels
|
65
|
+
available_channels = self._get_available_channels(is_email, config)
|
66
|
+
|
67
|
+
if not available_channels:
|
68
|
+
raise TwilioConfigurationError(
|
69
|
+
"No channels configured for OTP delivery",
|
70
|
+
suggestions=["Configure at least one channel (WhatsApp, SMS, or Email)"]
|
71
|
+
)
|
72
|
+
|
73
|
+
# Determine channel order
|
74
|
+
channel_order = self._get_channel_order(
|
75
|
+
available_channels, preferred_channel, is_email, config
|
76
|
+
)
|
77
|
+
|
78
|
+
last_error = None
|
79
|
+
|
80
|
+
for channel in channel_order:
|
81
|
+
try:
|
82
|
+
success, message = self._send_via_channel(identifier, channel)
|
83
|
+
if success:
|
84
|
+
return True, message, channel
|
85
|
+
|
86
|
+
except Exception as e:
|
87
|
+
last_error = e
|
88
|
+
logger.warning(f"Channel {channel.value} failed for {self._mask_identifier(identifier)}: {e}")
|
89
|
+
|
90
|
+
if not enable_fallback:
|
91
|
+
raise
|
92
|
+
|
93
|
+
# All channels failed
|
94
|
+
raise TwilioSendError(
|
95
|
+
f"All configured channels failed for {self._mask_identifier(identifier)}",
|
96
|
+
context={"tried_channels": [ch.value for ch in channel_order]},
|
97
|
+
suggestions=["Check service configurations", "Verify recipient details"]
|
98
|
+
) from last_error
|
99
|
+
|
100
|
+
async def asend_otp(
|
101
|
+
self,
|
102
|
+
identifier: str,
|
103
|
+
preferred_channel: Optional[TwilioChannelType] = None,
|
104
|
+
enable_fallback: bool = True
|
105
|
+
) -> Tuple[bool, str, TwilioChannelType]:
|
106
|
+
"""Async version of send_otp."""
|
107
|
+
return await sync_to_async(self.send_otp)(identifier, preferred_channel, enable_fallback)
|
108
|
+
|
109
|
+
def verify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
110
|
+
"""
|
111
|
+
Verify OTP code for any channel.
|
112
|
+
|
113
|
+
Args:
|
114
|
+
identifier: Phone number or email used for OTP
|
115
|
+
code: OTP code to verify
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
Tuple[bool, str]: (is_valid, message)
|
119
|
+
"""
|
120
|
+
config = self.get_twilio_config()
|
121
|
+
|
122
|
+
# For Twilio Verify channels (WhatsApp, SMS), use Twilio verification
|
123
|
+
if not "@" in identifier and config.verify:
|
124
|
+
return self._verify_twilio_otp(identifier, code, config)
|
125
|
+
|
126
|
+
# For email or custom verification, use stored OTP
|
127
|
+
return self._verify_stored_otp(identifier, code)
|
128
|
+
|
129
|
+
async def averify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
130
|
+
"""Async version of verify_otp."""
|
131
|
+
return await sync_to_async(self.verify_otp)(identifier, code)
|
132
|
+
|
133
|
+
def _get_available_channels(self, is_email: bool, config: TwilioConfig) -> List[TwilioChannelType]:
|
134
|
+
"""Get list of available channels based on configuration."""
|
135
|
+
channels = []
|
136
|
+
|
137
|
+
if config.verify:
|
138
|
+
if not is_email: # Phone number - can use WhatsApp/SMS
|
139
|
+
channels.extend([TwilioChannelType.WHATSAPP, TwilioChannelType.SMS])
|
140
|
+
|
141
|
+
if config.sendgrid: # Email available
|
142
|
+
channels.append(TwilioChannelType.EMAIL)
|
143
|
+
|
144
|
+
return channels
|
145
|
+
|
146
|
+
def _get_channel_order(
|
147
|
+
self,
|
148
|
+
available_channels: List[TwilioChannelType],
|
149
|
+
preferred_channel: Optional[TwilioChannelType],
|
150
|
+
is_email: bool,
|
151
|
+
config: TwilioConfig
|
152
|
+
) -> List[TwilioChannelType]:
|
153
|
+
"""Determine optimal channel order for delivery attempts."""
|
154
|
+
|
155
|
+
# If preferred channel is specified and available, try it first
|
156
|
+
if preferred_channel and preferred_channel in available_channels:
|
157
|
+
ordered_channels = [preferred_channel]
|
158
|
+
remaining = [ch for ch in available_channels if ch != preferred_channel]
|
159
|
+
ordered_channels.extend(remaining)
|
160
|
+
return ordered_channels
|
161
|
+
|
162
|
+
# Default ordering based on identifier type and configuration
|
163
|
+
if is_email:
|
164
|
+
return [TwilioChannelType.EMAIL]
|
165
|
+
|
166
|
+
# For phone numbers, prefer WhatsApp -> SMS
|
167
|
+
phone_channels = []
|
168
|
+
if TwilioChannelType.WHATSAPP in available_channels:
|
169
|
+
phone_channels.append(TwilioChannelType.WHATSAPP)
|
170
|
+
if TwilioChannelType.SMS in available_channels:
|
171
|
+
phone_channels.append(TwilioChannelType.SMS)
|
172
|
+
|
173
|
+
return phone_channels
|
174
|
+
|
175
|
+
def _send_via_channel(self, identifier: str, channel: TwilioChannelType) -> Tuple[bool, str]:
|
176
|
+
"""Send OTP via specific channel."""
|
177
|
+
if channel == TwilioChannelType.WHATSAPP:
|
178
|
+
return self._whatsapp_service.send_otp(identifier, fallback_to_sms=False)
|
179
|
+
elif channel == TwilioChannelType.SMS:
|
180
|
+
return self._sms_service.send_otp(identifier)
|
181
|
+
elif channel == TwilioChannelType.EMAIL:
|
182
|
+
success, message, _ = self._email_service.send_otp(identifier)
|
183
|
+
return success, message
|
184
|
+
else:
|
185
|
+
raise TwilioSendError(f"Unsupported channel: {channel.value}")
|
186
|
+
|
187
|
+
def _verify_twilio_otp(self, phone_number: str, code: str, config: TwilioConfig) -> Tuple[bool, str]:
|
188
|
+
"""Verify OTP using Twilio Verify API."""
|
189
|
+
try:
|
190
|
+
client = self.get_twilio_client()
|
191
|
+
|
192
|
+
verification_check = client.verify.v2.services(
|
193
|
+
config.verify.service_sid
|
194
|
+
).verification_checks.create(
|
195
|
+
to=phone_number,
|
196
|
+
code=code
|
197
|
+
)
|
198
|
+
|
199
|
+
if verification_check.status == 'approved':
|
200
|
+
logger.info(f"OTP verified successfully for {self._mask_identifier(phone_number)}")
|
201
|
+
return True, "OTP verified successfully"
|
202
|
+
else:
|
203
|
+
return False, f"Invalid OTP code: {verification_check.status}"
|
204
|
+
|
205
|
+
except TwilioException as e:
|
206
|
+
raise TwilioVerificationError(
|
207
|
+
f"OTP verification failed: {e}",
|
208
|
+
phone_number=phone_number,
|
209
|
+
twilio_error_code=getattr(e, 'code', None),
|
210
|
+
twilio_error_message=str(e)
|
211
|
+
) from e
|
212
|
+
|
213
|
+
def _verify_stored_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
214
|
+
"""Verify OTP using stored codes (for email)."""
|
215
|
+
stored_data = self._get_stored_otp(identifier)
|
216
|
+
|
217
|
+
if not stored_data:
|
218
|
+
return False, "OTP not found. Please request a new code."
|
219
|
+
|
220
|
+
if datetime.now() > stored_data['expires_at']:
|
221
|
+
self._remove_otp(identifier)
|
222
|
+
return False, "OTP expired. Please request a new code."
|
223
|
+
|
224
|
+
# Increment attempt counter
|
225
|
+
stored_data['attempts'] += 1
|
226
|
+
|
227
|
+
if stored_data['attempts'] > 5: # Max attempts
|
228
|
+
self._remove_otp(identifier)
|
229
|
+
return False, "Too many attempts. Please request a new code."
|
230
|
+
|
231
|
+
if stored_data['code'] == code:
|
232
|
+
self._remove_otp(identifier)
|
233
|
+
logger.info(f"Stored OTP verified successfully for {self._mask_identifier(identifier)}")
|
234
|
+
return True, "OTP verified successfully"
|
235
|
+
else:
|
236
|
+
return False, f"Invalid OTP code. {5 - stored_data['attempts']} attempts remaining."
|
237
|
+
|
238
|
+
|
239
|
+
class DjangoTwilioService(UnifiedOTPService):
|
240
|
+
"""
|
241
|
+
Main Twilio service for django_cfg integration.
|
242
|
+
|
243
|
+
Provides unified access to all Twilio services with auto-configuration
|
244
|
+
and comprehensive error handling. This is the primary service class
|
245
|
+
that should be used in most applications.
|
246
|
+
"""
|
247
|
+
|
248
|
+
def __init__(self):
|
249
|
+
"""Initialize with all service capabilities."""
|
250
|
+
super().__init__()
|
251
|
+
logger.info("DjangoTwilioService initialized with auto-configuration")
|
252
|
+
|
253
|
+
def get_service_status(self) -> Dict[str, Any]:
|
254
|
+
"""
|
255
|
+
Get comprehensive status of all Twilio services.
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
Dictionary with service status information
|
259
|
+
"""
|
260
|
+
try:
|
261
|
+
config = self.get_twilio_config()
|
262
|
+
|
263
|
+
status = {
|
264
|
+
"twilio_configured": True,
|
265
|
+
"account_sid": config.account_sid,
|
266
|
+
"region": config.region.value,
|
267
|
+
"services": {},
|
268
|
+
"enabled_channels": [ch.value for ch in config.get_enabled_channels()],
|
269
|
+
"test_mode": config.test_mode,
|
270
|
+
}
|
271
|
+
|
272
|
+
# Check Verify service
|
273
|
+
if config.verify:
|
274
|
+
status["services"]["verify"] = {
|
275
|
+
"enabled": True,
|
276
|
+
"service_sid": config.verify.service_sid,
|
277
|
+
"default_channel": config.verify.default_channel.value,
|
278
|
+
"fallback_channels": [ch.value for ch in config.verify.fallback_channels],
|
279
|
+
"code_length": config.verify.code_length,
|
280
|
+
"ttl_seconds": config.verify.ttl_seconds,
|
281
|
+
}
|
282
|
+
else:
|
283
|
+
status["services"]["verify"] = {"enabled": False}
|
284
|
+
|
285
|
+
# Check SendGrid service
|
286
|
+
if config.sendgrid:
|
287
|
+
status["services"]["sendgrid"] = {
|
288
|
+
"enabled": True,
|
289
|
+
"from_email": config.sendgrid.from_email,
|
290
|
+
"from_name": config.sendgrid.from_name,
|
291
|
+
"template_configured": config.sendgrid.otp_template_id is not None,
|
292
|
+
"tracking_enabled": config.sendgrid.tracking_enabled,
|
293
|
+
}
|
294
|
+
else:
|
295
|
+
status["services"]["sendgrid"] = {"enabled": False}
|
296
|
+
|
297
|
+
return status
|
298
|
+
|
299
|
+
except Exception as e:
|
300
|
+
return {
|
301
|
+
"twilio_configured": False,
|
302
|
+
"error": str(e),
|
303
|
+
"services": {},
|
304
|
+
}
|
305
|
+
|
306
|
+
|
307
|
+
__all__ = [
|
308
|
+
"UnifiedOTPService",
|
309
|
+
"DjangoTwilioService",
|
310
|
+
]
|
@@ -0,0 +1,190 @@
|
|
1
|
+
"""
|
2
|
+
Convenience functions for direct Twilio service usage.
|
3
|
+
|
4
|
+
Provides simple function-based API for sending and verifying OTPs
|
5
|
+
without manually instantiating service classes.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Tuple, Optional
|
9
|
+
|
10
|
+
from .whatsapp import WhatsAppOTPService
|
11
|
+
from .email_otp import EmailOTPService
|
12
|
+
from .sms import SMSOTPService
|
13
|
+
from .unified import UnifiedOTPService
|
14
|
+
|
15
|
+
|
16
|
+
# Sync convenience functions
|
17
|
+
|
18
|
+
def send_whatsapp_otp(phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
19
|
+
"""
|
20
|
+
Send WhatsApp OTP with optional SMS fallback.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
phone_number: Phone number in E.164 format
|
24
|
+
fallback_to_sms: Whether to fallback to SMS if WhatsApp fails
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
Tuple[bool, str]: (success, message)
|
28
|
+
|
29
|
+
Example:
|
30
|
+
>>> success, message = send_whatsapp_otp("+1234567890")
|
31
|
+
>>> if success:
|
32
|
+
... print(f"OTP sent: {message}")
|
33
|
+
"""
|
34
|
+
service = WhatsAppOTPService()
|
35
|
+
return service.send_otp(phone_number, fallback_to_sms)
|
36
|
+
|
37
|
+
|
38
|
+
def send_email_otp(email: str, subject: Optional[str] = None) -> Tuple[bool, str, str]:
|
39
|
+
"""
|
40
|
+
Send email OTP.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
email: Recipient email address
|
44
|
+
subject: Optional custom email subject
|
45
|
+
|
46
|
+
Returns:
|
47
|
+
Tuple[bool, str, str]: (success, message, otp_code)
|
48
|
+
|
49
|
+
Example:
|
50
|
+
>>> success, message, otp_code = send_email_otp("user@example.com")
|
51
|
+
>>> if success:
|
52
|
+
... print(f"OTP sent: {otp_code}")
|
53
|
+
"""
|
54
|
+
service = EmailOTPService()
|
55
|
+
return service.send_otp(email, subject)
|
56
|
+
|
57
|
+
|
58
|
+
def send_sms_otp(phone_number: str) -> Tuple[bool, str]:
|
59
|
+
"""
|
60
|
+
Send SMS OTP.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
phone_number: Phone number in E.164 format
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
Tuple[bool, str]: (success, message)
|
67
|
+
|
68
|
+
Example:
|
69
|
+
>>> success, message = send_sms_otp("+1234567890")
|
70
|
+
>>> if success:
|
71
|
+
... print(f"OTP sent: {message}")
|
72
|
+
"""
|
73
|
+
service = SMSOTPService()
|
74
|
+
return service.send_otp(phone_number)
|
75
|
+
|
76
|
+
|
77
|
+
def verify_otp(identifier: str, code: str) -> Tuple[bool, str]:
|
78
|
+
"""
|
79
|
+
Verify OTP code for any channel.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
identifier: Phone number or email used for OTP
|
83
|
+
code: OTP code to verify
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
Tuple[bool, str]: (is_valid, message)
|
87
|
+
|
88
|
+
Example:
|
89
|
+
>>> is_valid, message = verify_otp("+1234567890", "123456")
|
90
|
+
>>> if is_valid:
|
91
|
+
... print("OTP verified successfully!")
|
92
|
+
"""
|
93
|
+
service = UnifiedOTPService()
|
94
|
+
return service.verify_otp(identifier, code)
|
95
|
+
|
96
|
+
|
97
|
+
# Async convenience functions
|
98
|
+
|
99
|
+
async def asend_whatsapp_otp(phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
100
|
+
"""
|
101
|
+
Async send WhatsApp OTP.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
phone_number: Phone number in E.164 format
|
105
|
+
fallback_to_sms: Whether to fallback to SMS if WhatsApp fails
|
106
|
+
|
107
|
+
Returns:
|
108
|
+
Tuple[bool, str]: (success, message)
|
109
|
+
|
110
|
+
Example:
|
111
|
+
>>> success, message = await asend_whatsapp_otp("+1234567890")
|
112
|
+
>>> if success:
|
113
|
+
... print(f"OTP sent: {message}")
|
114
|
+
"""
|
115
|
+
service = WhatsAppOTPService()
|
116
|
+
return await service.asend_otp(phone_number, fallback_to_sms)
|
117
|
+
|
118
|
+
|
119
|
+
async def asend_email_otp(email: str, subject: Optional[str] = None) -> Tuple[bool, str, str]:
|
120
|
+
"""
|
121
|
+
Async send email OTP.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
email: Recipient email address
|
125
|
+
subject: Optional custom email subject
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
Tuple[bool, str, str]: (success, message, otp_code)
|
129
|
+
|
130
|
+
Example:
|
131
|
+
>>> success, message, otp_code = await asend_email_otp("user@example.com")
|
132
|
+
>>> if success:
|
133
|
+
... print(f"OTP sent: {otp_code}")
|
134
|
+
"""
|
135
|
+
service = EmailOTPService()
|
136
|
+
return await service.asend_otp(email, subject)
|
137
|
+
|
138
|
+
|
139
|
+
async def asend_sms_otp(phone_number: str) -> Tuple[bool, str]:
|
140
|
+
"""
|
141
|
+
Async send SMS OTP.
|
142
|
+
|
143
|
+
Args:
|
144
|
+
phone_number: Phone number in E.164 format
|
145
|
+
|
146
|
+
Returns:
|
147
|
+
Tuple[bool, str]: (success, message)
|
148
|
+
|
149
|
+
Example:
|
150
|
+
>>> success, message = await asend_sms_otp("+1234567890")
|
151
|
+
>>> if success:
|
152
|
+
... print(f"OTP sent: {message}")
|
153
|
+
"""
|
154
|
+
service = SMSOTPService()
|
155
|
+
return await service.asend_otp(phone_number)
|
156
|
+
|
157
|
+
|
158
|
+
async def averify_otp(identifier: str, code: str) -> Tuple[bool, str]:
|
159
|
+
"""
|
160
|
+
Async verify OTP code.
|
161
|
+
|
162
|
+
Args:
|
163
|
+
identifier: Phone number or email used for OTP
|
164
|
+
code: OTP code to verify
|
165
|
+
|
166
|
+
Returns:
|
167
|
+
Tuple[bool, str]: (is_valid, message)
|
168
|
+
|
169
|
+
Example:
|
170
|
+
>>> is_valid, message = await averify_otp("+1234567890", "123456")
|
171
|
+
>>> if is_valid:
|
172
|
+
... print("OTP verified successfully!")
|
173
|
+
"""
|
174
|
+
service = UnifiedOTPService()
|
175
|
+
return await service.averify_otp(identifier, code)
|
176
|
+
|
177
|
+
|
178
|
+
__all__ = [
|
179
|
+
# Sync convenience functions
|
180
|
+
"send_whatsapp_otp",
|
181
|
+
"send_email_otp",
|
182
|
+
"send_sms_otp",
|
183
|
+
"verify_otp",
|
184
|
+
|
185
|
+
# Async convenience functions
|
186
|
+
"asend_whatsapp_otp",
|
187
|
+
"asend_email_otp",
|
188
|
+
"asend_sms_otp",
|
189
|
+
"averify_otp",
|
190
|
+
]
|
@@ -0,0 +1,137 @@
|
|
1
|
+
"""
|
2
|
+
WhatsApp OTP service using Twilio Verify API.
|
3
|
+
|
4
|
+
Provides OTP delivery via WhatsApp with automatic SMS fallback.
|
5
|
+
Supports both sync and async operations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from typing import Tuple
|
10
|
+
from ._imports import Client, TwilioException
|
11
|
+
from asgiref.sync import sync_to_async
|
12
|
+
|
13
|
+
from .base import BaseTwilioService
|
14
|
+
from .models import TwilioVerifyConfig
|
15
|
+
from .exceptions import (
|
16
|
+
TwilioConfigurationError,
|
17
|
+
TwilioSendError,
|
18
|
+
)
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class WhatsAppOTPService(BaseTwilioService):
|
24
|
+
"""
|
25
|
+
WhatsApp OTP service using Twilio Verify API.
|
26
|
+
|
27
|
+
Provides OTP delivery via WhatsApp with automatic SMS fallback.
|
28
|
+
Supports both sync and async operations.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def send_otp(self, phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
32
|
+
"""
|
33
|
+
Send OTP via WhatsApp with optional SMS fallback.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
phone_number: Phone number in E.164 format (e.g., +1234567890)
|
37
|
+
fallback_to_sms: Whether to fallback to SMS if WhatsApp fails
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Tuple[bool, str]: (success, message)
|
41
|
+
|
42
|
+
Raises:
|
43
|
+
TwilioConfigurationError: If service is not configured
|
44
|
+
TwilioSendError: If sending fails
|
45
|
+
"""
|
46
|
+
config = self.get_twilio_config()
|
47
|
+
|
48
|
+
if not config.verify:
|
49
|
+
raise TwilioConfigurationError(
|
50
|
+
"Twilio Verify service not configured",
|
51
|
+
missing_fields=["verify"],
|
52
|
+
suggestions=["Configure TwilioVerifyConfig in your Twilio settings"]
|
53
|
+
)
|
54
|
+
|
55
|
+
client = self.get_twilio_client()
|
56
|
+
|
57
|
+
try:
|
58
|
+
# Try WhatsApp first
|
59
|
+
verification = client.verify.v2.services(
|
60
|
+
config.verify.service_sid
|
61
|
+
).verifications.create(
|
62
|
+
to=phone_number,
|
63
|
+
channel='whatsapp'
|
64
|
+
)
|
65
|
+
|
66
|
+
if verification.status == 'pending':
|
67
|
+
logger.info(f"WhatsApp OTP sent successfully to {self._mask_identifier(phone_number)}")
|
68
|
+
return True, f"OTP sent via WhatsApp to {self._mask_identifier(phone_number)}"
|
69
|
+
|
70
|
+
# If WhatsApp failed and fallback is enabled, try SMS
|
71
|
+
if fallback_to_sms and verification.status != 'pending':
|
72
|
+
logger.warning(f"WhatsApp failed for {self._mask_identifier(phone_number)}, trying SMS fallback")
|
73
|
+
return self._send_sms_otp(phone_number, client, config.verify)
|
74
|
+
|
75
|
+
raise TwilioSendError(
|
76
|
+
f"WhatsApp OTP failed with status: {verification.status}",
|
77
|
+
channel="whatsapp",
|
78
|
+
recipient=phone_number,
|
79
|
+
suggestions=["Check if recipient has WhatsApp Business account", "Try SMS fallback"]
|
80
|
+
)
|
81
|
+
|
82
|
+
except TwilioException as e:
|
83
|
+
if fallback_to_sms:
|
84
|
+
logger.warning(f"WhatsApp error for {self._mask_identifier(phone_number)}: {e}, trying SMS")
|
85
|
+
return self._send_sms_otp(phone_number, client, config.verify)
|
86
|
+
|
87
|
+
raise TwilioSendError(
|
88
|
+
f"WhatsApp OTP failed: {e}",
|
89
|
+
channel="whatsapp",
|
90
|
+
recipient=phone_number,
|
91
|
+
twilio_error_code=getattr(e, 'code', None),
|
92
|
+
twilio_error_message=str(e)
|
93
|
+
) from e
|
94
|
+
except Exception as e:
|
95
|
+
raise TwilioSendError(
|
96
|
+
f"Unexpected error sending WhatsApp OTP: {e}",
|
97
|
+
channel="whatsapp",
|
98
|
+
recipient=phone_number
|
99
|
+
) from e
|
100
|
+
|
101
|
+
async def asend_otp(self, phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
102
|
+
"""Async version of send_otp."""
|
103
|
+
return await sync_to_async(self.send_otp)(phone_number, fallback_to_sms)
|
104
|
+
|
105
|
+
def _send_sms_otp(self, phone_number: str, client: Client, verify_config: TwilioVerifyConfig) -> Tuple[bool, str]:
|
106
|
+
"""Internal SMS fallback method."""
|
107
|
+
try:
|
108
|
+
verification = client.verify.v2.services(
|
109
|
+
verify_config.service_sid
|
110
|
+
).verifications.create(
|
111
|
+
to=phone_number,
|
112
|
+
channel='sms'
|
113
|
+
)
|
114
|
+
|
115
|
+
if verification.status == 'pending':
|
116
|
+
logger.info(f"SMS fallback OTP sent to {self._mask_identifier(phone_number)}")
|
117
|
+
return True, f"OTP sent via SMS to {self._mask_identifier(phone_number)} (WhatsApp fallback)"
|
118
|
+
|
119
|
+
raise TwilioSendError(
|
120
|
+
f"SMS fallback failed with status: {verification.status}",
|
121
|
+
channel="sms",
|
122
|
+
recipient=phone_number
|
123
|
+
)
|
124
|
+
|
125
|
+
except TwilioException as e:
|
126
|
+
raise TwilioSendError(
|
127
|
+
f"SMS fallback failed: {e}",
|
128
|
+
channel="sms",
|
129
|
+
recipient=phone_number,
|
130
|
+
twilio_error_code=getattr(e, 'code', None),
|
131
|
+
twilio_error_message=str(e)
|
132
|
+
) from e
|
133
|
+
|
134
|
+
|
135
|
+
__all__ = [
|
136
|
+
"WhatsAppOTPService",
|
137
|
+
]
|