django-cfg 1.3.13__py3-none-any.whl → 1.4.3__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/agents/examples/__init__.py +3 -0
- django_cfg/apps/agents/examples/simple_example.py +161 -0
- 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/examples/__init__.py +3 -0
- django_cfg/apps/knowbase/examples/external_data_usage.py +191 -0
- 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/examples/vehicle_model_example.py +199 -0
- 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_currency/examples/__init__.py +3 -0
- django_cfg/modules/django_currency/examples/example_database_usage.py +144 -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_ipc_client/README.md +346 -0
- django_cfg/modules/django_ipc_client/__init__.py +51 -0
- django_cfg/modules/django_ipc_client/client.py +540 -0
- django_cfg/modules/django_ipc_client/config.py +207 -0
- django_cfg/modules/django_ipc_client/dashboard/README.md +517 -0
- django_cfg/modules/django_ipc_client/dashboard/UNFOLD_INTEGRATION.md +439 -0
- django_cfg/modules/django_ipc_client/dashboard/__init__.py +11 -0
- django_cfg/modules/django_ipc_client/dashboard/apps.py +22 -0
- django_cfg/modules/django_ipc_client/dashboard/monitor.py +435 -0
- django_cfg/modules/django_ipc_client/dashboard/static/django_ipc_dashboard/js/dashboard.js +373 -0
- django_cfg/modules/django_ipc_client/dashboard/templates/django_ipc_dashboard/base.html +76 -0
- django_cfg/modules/django_ipc_client/dashboard/templates/django_ipc_dashboard/dashboard.html +200 -0
- django_cfg/modules/django_ipc_client/dashboard/urls.py +22 -0
- django_cfg/modules/django_ipc_client/dashboard/urls_admin.py +9 -0
- django_cfg/modules/django_ipc_client/dashboard/views.py +251 -0
- django_cfg/modules/django_ipc_client/exceptions.py +201 -0
- 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/examples/component_class_example.html +156 -0
- 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.3.dist-info/METADATA +533 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.3.dist-info}/RECORD +432 -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.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.13.dist-info → django_cfg-1.4.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,942 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Auto-configuring Twilio services for django_cfg.
|
3
|
-
|
4
|
-
This module provides comprehensive OTP and messaging services via WhatsApp, Email, and SMS.
|
5
|
-
Supports both synchronous and asynchronous operations following Django 5.2+ patterns.
|
6
|
-
|
7
|
-
Following CRITICAL_REQUIREMENTS.md:
|
8
|
-
- No raw Dict/Any usage - everything through Pydantic models
|
9
|
-
- Proper type annotations for all fields
|
10
|
-
- Comprehensive error handling with specific exceptions
|
11
|
-
- Full async/await support with context detection
|
12
|
-
"""
|
13
|
-
|
14
|
-
import asyncio
|
15
|
-
import logging
|
16
|
-
import random
|
17
|
-
import string
|
18
|
-
from typing import Optional, Tuple, Dict, Any, List, Union
|
19
|
-
from datetime import datetime, timedelta
|
20
|
-
from contextlib import asynccontextmanager
|
21
|
-
|
22
|
-
# Third-party imports
|
23
|
-
from twilio.rest import Client
|
24
|
-
from twilio.base.exceptions import TwilioException
|
25
|
-
from sendgrid import SendGridAPIClient
|
26
|
-
from sendgrid.helpers.mail import Mail
|
27
|
-
from asgiref.sync import sync_to_async, async_to_sync
|
28
|
-
|
29
|
-
# Django CFG imports
|
30
|
-
from django_cfg.modules.base import BaseCfgModule
|
31
|
-
from django_cfg.modules.django_twilio.models import (
|
32
|
-
TwilioConfig,
|
33
|
-
TwilioChannelType,
|
34
|
-
TwilioVerifyConfig,
|
35
|
-
SendGridConfig,
|
36
|
-
)
|
37
|
-
from django_cfg.modules.django_twilio.exceptions import (
|
38
|
-
TwilioError,
|
39
|
-
TwilioConfigurationError,
|
40
|
-
TwilioVerificationError,
|
41
|
-
TwilioSendError,
|
42
|
-
TwilioRateLimitError,
|
43
|
-
TwilioNetworkError,
|
44
|
-
)
|
45
|
-
|
46
|
-
logger = logging.getLogger(__name__)
|
47
|
-
|
48
|
-
|
49
|
-
def is_async_context() -> bool:
|
50
|
-
"""Detect if running in async context."""
|
51
|
-
try:
|
52
|
-
asyncio.get_running_loop()
|
53
|
-
return True
|
54
|
-
except RuntimeError:
|
55
|
-
return False
|
56
|
-
|
57
|
-
|
58
|
-
class BaseTwilioService(BaseCfgModule):
|
59
|
-
"""
|
60
|
-
Base service class for all Twilio operations.
|
61
|
-
|
62
|
-
Provides auto-configuration from DjangoConfig and common utilities
|
63
|
-
for all Twilio services including error handling and logging.
|
64
|
-
"""
|
65
|
-
|
66
|
-
def __init__(self):
|
67
|
-
"""Initialize with auto-discovered configuration."""
|
68
|
-
super().__init__()
|
69
|
-
self._config: Optional[TwilioConfig] = None
|
70
|
-
self._twilio_client: Optional[Client] = None
|
71
|
-
self._sendgrid_client: Optional[SendGridAPIClient] = None
|
72
|
-
self._otp_storage: Dict[str, Dict[str, Any]] = {} # In-memory storage for development
|
73
|
-
|
74
|
-
def get_twilio_config(self) -> TwilioConfig:
|
75
|
-
"""
|
76
|
-
Get Twilio configuration from DjangoConfig.
|
77
|
-
|
78
|
-
Returns:
|
79
|
-
TwilioConfig instance
|
80
|
-
|
81
|
-
Raises:
|
82
|
-
TwilioConfigurationError: If configuration is missing or invalid
|
83
|
-
"""
|
84
|
-
if self._config is None:
|
85
|
-
django_config = self.get_config()
|
86
|
-
if not django_config:
|
87
|
-
raise TwilioConfigurationError(
|
88
|
-
"DjangoConfig instance not found",
|
89
|
-
suggestions=["Ensure DjangoConfig is properly initialized"]
|
90
|
-
)
|
91
|
-
|
92
|
-
twilio_config = getattr(django_config, 'twilio', None)
|
93
|
-
if not twilio_config:
|
94
|
-
raise TwilioConfigurationError(
|
95
|
-
"Twilio configuration not found in DjangoConfig",
|
96
|
-
missing_fields=["twilio"],
|
97
|
-
suggestions=["Add TwilioConfig to your DjangoConfig class"]
|
98
|
-
)
|
99
|
-
|
100
|
-
self._config = twilio_config
|
101
|
-
|
102
|
-
return self._config
|
103
|
-
|
104
|
-
def get_twilio_client(self) -> Client:
|
105
|
-
"""
|
106
|
-
Get initialized Twilio client.
|
107
|
-
|
108
|
-
Returns:
|
109
|
-
Twilio Client instance
|
110
|
-
|
111
|
-
Raises:
|
112
|
-
TwilioConfigurationError: If client cannot be initialized
|
113
|
-
"""
|
114
|
-
if self._twilio_client is None:
|
115
|
-
config = self.get_twilio_config()
|
116
|
-
|
117
|
-
try:
|
118
|
-
client_config = config.get_client_config()
|
119
|
-
self._twilio_client = Client(
|
120
|
-
client_config["username"],
|
121
|
-
client_config["password"],
|
122
|
-
region=client_config.get("region")
|
123
|
-
)
|
124
|
-
|
125
|
-
# Test connection with a simple API call
|
126
|
-
try:
|
127
|
-
self._twilio_client.api.v2010.accounts(config.account_sid).fetch()
|
128
|
-
except TwilioException as e:
|
129
|
-
raise TwilioConfigurationError(
|
130
|
-
f"Failed to authenticate with Twilio: {e}",
|
131
|
-
error_code=getattr(e, 'code', None),
|
132
|
-
suggestions=[
|
133
|
-
"Verify TWILIO_ACCOUNT_SID is correct",
|
134
|
-
"Verify TWILIO_AUTH_TOKEN is correct",
|
135
|
-
"Check Twilio account status"
|
136
|
-
]
|
137
|
-
) from e
|
138
|
-
|
139
|
-
except Exception as e:
|
140
|
-
raise TwilioConfigurationError(
|
141
|
-
f"Failed to initialize Twilio client: {e}",
|
142
|
-
suggestions=["Check Twilio configuration parameters"]
|
143
|
-
) from e
|
144
|
-
|
145
|
-
return self._twilio_client
|
146
|
-
|
147
|
-
def get_sendgrid_client(self) -> Optional[SendGridAPIClient]:
|
148
|
-
"""
|
149
|
-
Get initialized SendGrid client.
|
150
|
-
|
151
|
-
Returns:
|
152
|
-
SendGrid client instance or None if not configured
|
153
|
-
|
154
|
-
Raises:
|
155
|
-
TwilioConfigurationError: If client cannot be initialized
|
156
|
-
"""
|
157
|
-
config = self.get_twilio_config()
|
158
|
-
|
159
|
-
if not config.sendgrid:
|
160
|
-
return None
|
161
|
-
|
162
|
-
if self._sendgrid_client is None:
|
163
|
-
try:
|
164
|
-
sendgrid_config = config.get_sendgrid_config()
|
165
|
-
if sendgrid_config:
|
166
|
-
self._sendgrid_client = SendGridAPIClient(
|
167
|
-
api_key=sendgrid_config["api_key"]
|
168
|
-
)
|
169
|
-
|
170
|
-
except Exception as e:
|
171
|
-
raise TwilioConfigurationError(
|
172
|
-
f"Failed to initialize SendGrid client: {e}",
|
173
|
-
suggestions=["Check SendGrid API key configuration"]
|
174
|
-
) from e
|
175
|
-
|
176
|
-
return self._sendgrid_client
|
177
|
-
|
178
|
-
def _generate_otp(self, length: int = 6) -> str:
|
179
|
-
"""Generate numeric OTP code."""
|
180
|
-
return ''.join(random.choices(string.digits, k=length))
|
181
|
-
|
182
|
-
def _store_otp(self, identifier: str, code: str, ttl_seconds: int = 600) -> None:
|
183
|
-
"""Store OTP code with expiration (in-memory for development)."""
|
184
|
-
self._otp_storage[identifier] = {
|
185
|
-
'code': code,
|
186
|
-
'created_at': datetime.now(),
|
187
|
-
'expires_at': datetime.now() + timedelta(seconds=ttl_seconds),
|
188
|
-
'attempts': 0,
|
189
|
-
}
|
190
|
-
|
191
|
-
def _get_stored_otp(self, identifier: str) -> Optional[Dict[str, Any]]:
|
192
|
-
"""Get stored OTP data."""
|
193
|
-
return self._otp_storage.get(identifier)
|
194
|
-
|
195
|
-
def _remove_otp(self, identifier: str) -> None:
|
196
|
-
"""Remove OTP from storage."""
|
197
|
-
self._otp_storage.pop(identifier, None)
|
198
|
-
|
199
|
-
def _mask_identifier(self, identifier: str) -> str:
|
200
|
-
"""Mask identifier for security in logs."""
|
201
|
-
if "@" in identifier: # Email
|
202
|
-
parts = identifier.split("@")
|
203
|
-
if len(parts) == 2:
|
204
|
-
return f"{parts[0][:2]}***@{parts[1]}"
|
205
|
-
else: # Phone number
|
206
|
-
return f"***{identifier[-4:]}" if len(identifier) > 4 else "***"
|
207
|
-
return "***"
|
208
|
-
|
209
|
-
|
210
|
-
class WhatsAppOTPService(BaseTwilioService):
|
211
|
-
"""
|
212
|
-
WhatsApp OTP service using Twilio Verify API.
|
213
|
-
|
214
|
-
Provides OTP delivery via WhatsApp with automatic SMS fallback.
|
215
|
-
Supports both sync and async operations.
|
216
|
-
"""
|
217
|
-
|
218
|
-
def send_otp(self, phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
219
|
-
"""
|
220
|
-
Send OTP via WhatsApp with optional SMS fallback.
|
221
|
-
|
222
|
-
Args:
|
223
|
-
phone_number: Phone number in E.164 format (e.g., +1234567890)
|
224
|
-
fallback_to_sms: Whether to fallback to SMS if WhatsApp fails
|
225
|
-
|
226
|
-
Returns:
|
227
|
-
Tuple[bool, str]: (success, message)
|
228
|
-
|
229
|
-
Raises:
|
230
|
-
TwilioConfigurationError: If service is not configured
|
231
|
-
TwilioSendError: If sending fails
|
232
|
-
"""
|
233
|
-
config = self.get_twilio_config()
|
234
|
-
|
235
|
-
if not config.verify:
|
236
|
-
raise TwilioConfigurationError(
|
237
|
-
"Twilio Verify service not configured",
|
238
|
-
missing_fields=["verify"],
|
239
|
-
suggestions=["Configure TwilioVerifyConfig in your Twilio settings"]
|
240
|
-
)
|
241
|
-
|
242
|
-
client = self.get_twilio_client()
|
243
|
-
|
244
|
-
try:
|
245
|
-
# Try WhatsApp first
|
246
|
-
verification = client.verify.v2.services(
|
247
|
-
config.verify.service_sid
|
248
|
-
).verifications.create(
|
249
|
-
to=phone_number,
|
250
|
-
channel='whatsapp'
|
251
|
-
)
|
252
|
-
|
253
|
-
if verification.status == 'pending':
|
254
|
-
logger.info(f"WhatsApp OTP sent successfully to {self._mask_identifier(phone_number)}")
|
255
|
-
return True, f"OTP sent via WhatsApp to {self._mask_identifier(phone_number)}"
|
256
|
-
|
257
|
-
# If WhatsApp failed and fallback is enabled, try SMS
|
258
|
-
if fallback_to_sms and verification.status != 'pending':
|
259
|
-
logger.warning(f"WhatsApp failed for {self._mask_identifier(phone_number)}, trying SMS fallback")
|
260
|
-
return self._send_sms_otp(phone_number, client, config.verify)
|
261
|
-
|
262
|
-
raise TwilioSendError(
|
263
|
-
f"WhatsApp OTP failed with status: {verification.status}",
|
264
|
-
channel="whatsapp",
|
265
|
-
recipient=phone_number,
|
266
|
-
suggestions=["Check if recipient has WhatsApp Business account", "Try SMS fallback"]
|
267
|
-
)
|
268
|
-
|
269
|
-
except TwilioException as e:
|
270
|
-
if fallback_to_sms:
|
271
|
-
logger.warning(f"WhatsApp error for {self._mask_identifier(phone_number)}: {e}, trying SMS")
|
272
|
-
return self._send_sms_otp(phone_number, client, config.verify)
|
273
|
-
|
274
|
-
raise TwilioSendError(
|
275
|
-
f"WhatsApp OTP failed: {e}",
|
276
|
-
channel="whatsapp",
|
277
|
-
recipient=phone_number,
|
278
|
-
twilio_error_code=getattr(e, 'code', None),
|
279
|
-
twilio_error_message=str(e)
|
280
|
-
) from e
|
281
|
-
except Exception as e:
|
282
|
-
raise TwilioSendError(
|
283
|
-
f"Unexpected error sending WhatsApp OTP: {e}",
|
284
|
-
channel="whatsapp",
|
285
|
-
recipient=phone_number
|
286
|
-
) from e
|
287
|
-
|
288
|
-
async def asend_otp(self, phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
289
|
-
"""Async version of send_otp."""
|
290
|
-
return await sync_to_async(self.send_otp)(phone_number, fallback_to_sms)
|
291
|
-
|
292
|
-
def _send_sms_otp(self, phone_number: str, client: Client, verify_config: TwilioVerifyConfig) -> Tuple[bool, str]:
|
293
|
-
"""Internal SMS fallback method."""
|
294
|
-
try:
|
295
|
-
verification = client.verify.v2.services(
|
296
|
-
verify_config.service_sid
|
297
|
-
).verifications.create(
|
298
|
-
to=phone_number,
|
299
|
-
channel='sms'
|
300
|
-
)
|
301
|
-
|
302
|
-
if verification.status == 'pending':
|
303
|
-
logger.info(f"SMS fallback OTP sent to {self._mask_identifier(phone_number)}")
|
304
|
-
return True, f"OTP sent via SMS to {self._mask_identifier(phone_number)} (WhatsApp fallback)"
|
305
|
-
|
306
|
-
raise TwilioSendError(
|
307
|
-
f"SMS fallback failed with status: {verification.status}",
|
308
|
-
channel="sms",
|
309
|
-
recipient=phone_number
|
310
|
-
)
|
311
|
-
|
312
|
-
except TwilioException as e:
|
313
|
-
raise TwilioSendError(
|
314
|
-
f"SMS fallback failed: {e}",
|
315
|
-
channel="sms",
|
316
|
-
recipient=phone_number,
|
317
|
-
twilio_error_code=getattr(e, 'code', None),
|
318
|
-
twilio_error_message=str(e)
|
319
|
-
) from e
|
320
|
-
|
321
|
-
|
322
|
-
class EmailOTPService(BaseTwilioService):
|
323
|
-
"""
|
324
|
-
Email OTP service using SendGrid.
|
325
|
-
|
326
|
-
Provides OTP delivery via email with template support and
|
327
|
-
comprehensive deliverability optimization.
|
328
|
-
"""
|
329
|
-
|
330
|
-
def send_otp(
|
331
|
-
self,
|
332
|
-
email: str,
|
333
|
-
subject: Optional[str] = None,
|
334
|
-
template_data: Optional[Dict[str, Any]] = None
|
335
|
-
) -> Tuple[bool, str, str]:
|
336
|
-
"""
|
337
|
-
Send OTP via email.
|
338
|
-
|
339
|
-
Args:
|
340
|
-
email: Recipient email address
|
341
|
-
subject: Custom email subject (uses default if not provided)
|
342
|
-
template_data: Additional data for email template
|
343
|
-
|
344
|
-
Returns:
|
345
|
-
Tuple[bool, str, str]: (success, message, otp_code)
|
346
|
-
|
347
|
-
Raises:
|
348
|
-
TwilioConfigurationError: If SendGrid is not configured
|
349
|
-
TwilioSendError: If email sending fails
|
350
|
-
"""
|
351
|
-
config = self.get_twilio_config()
|
352
|
-
|
353
|
-
if not config.sendgrid:
|
354
|
-
raise TwilioConfigurationError(
|
355
|
-
"SendGrid configuration not found",
|
356
|
-
missing_fields=["sendgrid"],
|
357
|
-
suggestions=["Configure SendGridConfig in your Twilio settings"]
|
358
|
-
)
|
359
|
-
|
360
|
-
sendgrid_client = self.get_sendgrid_client()
|
361
|
-
if not sendgrid_client:
|
362
|
-
raise TwilioConfigurationError("SendGrid client not initialized")
|
363
|
-
|
364
|
-
try:
|
365
|
-
# Generate OTP code
|
366
|
-
otp_code = self._generate_otp(6)
|
367
|
-
|
368
|
-
# Store OTP for verification
|
369
|
-
self._store_otp(email, otp_code, config.verify.ttl_seconds if config.verify else 600)
|
370
|
-
|
371
|
-
# Prepare email content
|
372
|
-
if config.sendgrid.otp_template_id:
|
373
|
-
# Use dynamic template
|
374
|
-
success, message = self._send_template_email(
|
375
|
-
sendgrid_client, config.sendgrid, email, otp_code, template_data
|
376
|
-
)
|
377
|
-
else:
|
378
|
-
# Use simple HTML email
|
379
|
-
success, message = self._send_simple_email(
|
380
|
-
sendgrid_client, config.sendgrid, email, otp_code, subject
|
381
|
-
)
|
382
|
-
|
383
|
-
if success:
|
384
|
-
logger.info(f"Email OTP sent successfully to {self._mask_identifier(email)}")
|
385
|
-
return True, message, otp_code
|
386
|
-
else:
|
387
|
-
raise TwilioSendError(message, channel="email", recipient=email)
|
388
|
-
|
389
|
-
except Exception as e:
|
390
|
-
if isinstance(e, TwilioSendError):
|
391
|
-
raise
|
392
|
-
raise TwilioSendError(
|
393
|
-
f"Failed to send email OTP: {e}",
|
394
|
-
channel="email",
|
395
|
-
recipient=email
|
396
|
-
) from e
|
397
|
-
|
398
|
-
async def asend_otp(
|
399
|
-
self,
|
400
|
-
email: str,
|
401
|
-
subject: Optional[str] = None,
|
402
|
-
template_data: Optional[Dict[str, Any]] = None
|
403
|
-
) -> Tuple[bool, str, str]:
|
404
|
-
"""Async version of send_otp."""
|
405
|
-
return await sync_to_async(self.send_otp)(email, subject, template_data)
|
406
|
-
|
407
|
-
def _send_template_email(
|
408
|
-
self,
|
409
|
-
client: SendGridAPIClient,
|
410
|
-
config: SendGridConfig,
|
411
|
-
email: str,
|
412
|
-
otp_code: str,
|
413
|
-
template_data: Optional[Dict[str, Any]] = None
|
414
|
-
) -> Tuple[bool, str]:
|
415
|
-
"""Send email using SendGrid dynamic template."""
|
416
|
-
try:
|
417
|
-
# Prepare template data
|
418
|
-
dynamic_data = {
|
419
|
-
'verification_code': otp_code,
|
420
|
-
'user_email': email,
|
421
|
-
'expiry_minutes': 10,
|
422
|
-
'company_name': config.from_name,
|
423
|
-
**config.custom_template_data,
|
424
|
-
**(template_data or {})
|
425
|
-
}
|
426
|
-
|
427
|
-
message = Mail(
|
428
|
-
from_email=(config.from_email, config.from_name),
|
429
|
-
to_emails=email
|
430
|
-
)
|
431
|
-
|
432
|
-
message.template_id = config.otp_template_id
|
433
|
-
message.dynamic_template_data = dynamic_data
|
434
|
-
|
435
|
-
if config.reply_to_email:
|
436
|
-
message.reply_to = config.reply_to_email
|
437
|
-
|
438
|
-
response = client.send(message)
|
439
|
-
|
440
|
-
if response.status_code in [200, 201, 202]:
|
441
|
-
return True, f"OTP sent via email template to {self._mask_identifier(email)}"
|
442
|
-
else:
|
443
|
-
return False, f"SendGrid API error: {response.status_code}"
|
444
|
-
|
445
|
-
except Exception as e:
|
446
|
-
return False, f"Template email error: {e}"
|
447
|
-
|
448
|
-
def _send_simple_email(
|
449
|
-
self,
|
450
|
-
client: SendGridAPIClient,
|
451
|
-
config: SendGridConfig,
|
452
|
-
email: str,
|
453
|
-
otp_code: str,
|
454
|
-
subject: Optional[str] = None
|
455
|
-
) -> Tuple[bool, str]:
|
456
|
-
"""Send simple HTML email without template."""
|
457
|
-
try:
|
458
|
-
html_content = self._generate_html_content(otp_code, config.from_name)
|
459
|
-
plain_content = self._generate_plain_content(otp_code)
|
460
|
-
|
461
|
-
message = Mail(
|
462
|
-
from_email=(config.from_email, config.from_name),
|
463
|
-
to_emails=email,
|
464
|
-
subject=subject or config.default_subject,
|
465
|
-
html_content=html_content,
|
466
|
-
plain_text_content=plain_content
|
467
|
-
)
|
468
|
-
|
469
|
-
if config.reply_to_email:
|
470
|
-
message.reply_to = config.reply_to_email
|
471
|
-
|
472
|
-
response = client.send(message)
|
473
|
-
|
474
|
-
if response.status_code in [200, 201, 202]:
|
475
|
-
return True, f"OTP sent via email to {self._mask_identifier(email)}"
|
476
|
-
else:
|
477
|
-
return False, f"SendGrid API error: {response.status_code}"
|
478
|
-
|
479
|
-
except Exception as e:
|
480
|
-
return False, f"Simple email error: {e}"
|
481
|
-
|
482
|
-
def _generate_html_content(self, otp_code: str, company_name: str) -> str:
|
483
|
-
"""Generate HTML email content."""
|
484
|
-
return f"""
|
485
|
-
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
486
|
-
<div style="background-color: #f8f9fa; padding: 30px; border-radius: 10px; text-align: center;">
|
487
|
-
<h1 style="color: #333; margin-bottom: 20px;">Verification Code</h1>
|
488
|
-
<p style="color: #666; font-size: 16px; margin-bottom: 30px;">
|
489
|
-
Your verification code is:
|
490
|
-
</p>
|
491
|
-
<div style="background-color: #007bff; color: white; font-size: 32px; font-weight: bold;
|
492
|
-
padding: 20px; border-radius: 8px; letter-spacing: 5px; margin: 30px 0;">
|
493
|
-
{otp_code}
|
494
|
-
</div>
|
495
|
-
<p style="color: #999; font-size: 14px;">
|
496
|
-
This code expires in 10 minutes<br>
|
497
|
-
If you didn't request this code, please ignore this email
|
498
|
-
</p>
|
499
|
-
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
|
500
|
-
<p style="color: #999; font-size: 12px;">
|
501
|
-
Sent by {company_name}
|
502
|
-
</p>
|
503
|
-
</div>
|
504
|
-
</div>
|
505
|
-
"""
|
506
|
-
|
507
|
-
def _generate_plain_content(self, otp_code: str) -> str:
|
508
|
-
"""Generate plain text email content."""
|
509
|
-
return f"""
|
510
|
-
Your verification code: {otp_code}
|
511
|
-
|
512
|
-
This code expires in 10 minutes.
|
513
|
-
If you didn't request this code, please ignore this email.
|
514
|
-
""".strip()
|
515
|
-
|
516
|
-
|
517
|
-
class SMSOTPService(BaseTwilioService):
|
518
|
-
"""
|
519
|
-
SMS OTP service using Twilio Verify API.
|
520
|
-
|
521
|
-
Provides reliable SMS OTP delivery with comprehensive
|
522
|
-
error handling and international support.
|
523
|
-
"""
|
524
|
-
|
525
|
-
def send_otp(self, phone_number: str) -> Tuple[bool, str]:
|
526
|
-
"""
|
527
|
-
Send OTP via SMS.
|
528
|
-
|
529
|
-
Args:
|
530
|
-
phone_number: Phone number in E.164 format
|
531
|
-
|
532
|
-
Returns:
|
533
|
-
Tuple[bool, str]: (success, message)
|
534
|
-
|
535
|
-
Raises:
|
536
|
-
TwilioConfigurationError: If Verify service not configured
|
537
|
-
TwilioSendError: If SMS sending fails
|
538
|
-
"""
|
539
|
-
config = self.get_twilio_config()
|
540
|
-
|
541
|
-
if not config.verify:
|
542
|
-
raise TwilioConfigurationError(
|
543
|
-
"Twilio Verify service not configured",
|
544
|
-
missing_fields=["verify"],
|
545
|
-
suggestions=["Configure TwilioVerifyConfig in your Twilio settings"]
|
546
|
-
)
|
547
|
-
|
548
|
-
client = self.get_twilio_client()
|
549
|
-
|
550
|
-
try:
|
551
|
-
verification = client.verify.v2.services(
|
552
|
-
config.verify.service_sid
|
553
|
-
).verifications.create(
|
554
|
-
to=phone_number,
|
555
|
-
channel='sms'
|
556
|
-
)
|
557
|
-
|
558
|
-
if verification.status == 'pending':
|
559
|
-
logger.info(f"SMS OTP sent successfully to {self._mask_identifier(phone_number)}")
|
560
|
-
return True, f"OTP sent via SMS to {self._mask_identifier(phone_number)}"
|
561
|
-
else:
|
562
|
-
raise TwilioSendError(
|
563
|
-
f"SMS OTP failed with status: {verification.status}",
|
564
|
-
channel="sms",
|
565
|
-
recipient=phone_number
|
566
|
-
)
|
567
|
-
|
568
|
-
except TwilioException as e:
|
569
|
-
raise TwilioSendError(
|
570
|
-
f"SMS OTP failed: {e}",
|
571
|
-
channel="sms",
|
572
|
-
recipient=phone_number,
|
573
|
-
twilio_error_code=getattr(e, 'code', None),
|
574
|
-
twilio_error_message=str(e)
|
575
|
-
) from e
|
576
|
-
except Exception as e:
|
577
|
-
raise TwilioSendError(
|
578
|
-
f"Unexpected error sending SMS OTP: {e}",
|
579
|
-
channel="sms",
|
580
|
-
recipient=phone_number
|
581
|
-
) from e
|
582
|
-
|
583
|
-
async def asend_otp(self, phone_number: str) -> Tuple[bool, str]:
|
584
|
-
"""Async version of send_otp."""
|
585
|
-
return await sync_to_async(self.send_otp)(phone_number)
|
586
|
-
|
587
|
-
|
588
|
-
class UnifiedOTPService(BaseTwilioService):
|
589
|
-
"""
|
590
|
-
Unified OTP service that handles all channels with smart fallbacks.
|
591
|
-
|
592
|
-
Provides intelligent channel selection and automatic fallback
|
593
|
-
based on configuration and delivery success rates.
|
594
|
-
"""
|
595
|
-
|
596
|
-
def __init__(self):
|
597
|
-
"""Initialize with specialized service instances."""
|
598
|
-
super().__init__()
|
599
|
-
self._whatsapp_service = WhatsAppOTPService()
|
600
|
-
self._email_service = EmailOTPService()
|
601
|
-
self._sms_service = SMSOTPService()
|
602
|
-
|
603
|
-
def send_otp(
|
604
|
-
self,
|
605
|
-
identifier: str,
|
606
|
-
preferred_channel: Optional[TwilioChannelType] = None,
|
607
|
-
enable_fallback: bool = True
|
608
|
-
) -> Tuple[bool, str, TwilioChannelType]:
|
609
|
-
"""
|
610
|
-
Send OTP using the best available channel.
|
611
|
-
|
612
|
-
Args:
|
613
|
-
identifier: Phone number (E.164) or email address
|
614
|
-
preferred_channel: Preferred delivery channel
|
615
|
-
enable_fallback: Whether to try fallback channels
|
616
|
-
|
617
|
-
Returns:
|
618
|
-
Tuple[bool, str, TwilioChannelType]: (success, message, used_channel)
|
619
|
-
"""
|
620
|
-
config = self.get_twilio_config()
|
621
|
-
|
622
|
-
# Determine identifier type
|
623
|
-
is_email = "@" in identifier
|
624
|
-
|
625
|
-
# Get available channels
|
626
|
-
available_channels = self._get_available_channels(is_email, config)
|
627
|
-
|
628
|
-
if not available_channels:
|
629
|
-
raise TwilioConfigurationError(
|
630
|
-
"No channels configured for OTP delivery",
|
631
|
-
suggestions=["Configure at least one channel (WhatsApp, SMS, or Email)"]
|
632
|
-
)
|
633
|
-
|
634
|
-
# Determine channel order
|
635
|
-
channel_order = self._get_channel_order(
|
636
|
-
available_channels, preferred_channel, is_email, config
|
637
|
-
)
|
638
|
-
|
639
|
-
last_error = None
|
640
|
-
|
641
|
-
for channel in channel_order:
|
642
|
-
try:
|
643
|
-
success, message = self._send_via_channel(identifier, channel)
|
644
|
-
if success:
|
645
|
-
return True, message, channel
|
646
|
-
|
647
|
-
except Exception as e:
|
648
|
-
last_error = e
|
649
|
-
logger.warning(f"Channel {channel.value} failed for {self._mask_identifier(identifier)}: {e}")
|
650
|
-
|
651
|
-
if not enable_fallback:
|
652
|
-
raise
|
653
|
-
|
654
|
-
# All channels failed
|
655
|
-
raise TwilioSendError(
|
656
|
-
f"All configured channels failed for {self._mask_identifier(identifier)}",
|
657
|
-
context={"tried_channels": [ch.value for ch in channel_order]},
|
658
|
-
suggestions=["Check service configurations", "Verify recipient details"]
|
659
|
-
) from last_error
|
660
|
-
|
661
|
-
async def asend_otp(
|
662
|
-
self,
|
663
|
-
identifier: str,
|
664
|
-
preferred_channel: Optional[TwilioChannelType] = None,
|
665
|
-
enable_fallback: bool = True
|
666
|
-
) -> Tuple[bool, str, TwilioChannelType]:
|
667
|
-
"""Async version of send_otp."""
|
668
|
-
return await sync_to_async(self.send_otp)(identifier, preferred_channel, enable_fallback)
|
669
|
-
|
670
|
-
def verify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
671
|
-
"""
|
672
|
-
Verify OTP code for any channel.
|
673
|
-
|
674
|
-
Args:
|
675
|
-
identifier: Phone number or email used for OTP
|
676
|
-
code: OTP code to verify
|
677
|
-
|
678
|
-
Returns:
|
679
|
-
Tuple[bool, str]: (is_valid, message)
|
680
|
-
"""
|
681
|
-
config = self.get_twilio_config()
|
682
|
-
|
683
|
-
# For Twilio Verify channels (WhatsApp, SMS), use Twilio verification
|
684
|
-
if not "@" in identifier and config.verify:
|
685
|
-
return self._verify_twilio_otp(identifier, code, config)
|
686
|
-
|
687
|
-
# For email or custom verification, use stored OTP
|
688
|
-
return self._verify_stored_otp(identifier, code)
|
689
|
-
|
690
|
-
async def averify_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
691
|
-
"""Async version of verify_otp."""
|
692
|
-
return await sync_to_async(self.verify_otp)(identifier, code)
|
693
|
-
|
694
|
-
def _get_available_channels(self, is_email: bool, config: TwilioConfig) -> List[TwilioChannelType]:
|
695
|
-
"""Get list of available channels based on configuration."""
|
696
|
-
channels = []
|
697
|
-
|
698
|
-
if config.verify:
|
699
|
-
if not is_email: # Phone number - can use WhatsApp/SMS
|
700
|
-
channels.extend([TwilioChannelType.WHATSAPP, TwilioChannelType.SMS])
|
701
|
-
|
702
|
-
if config.sendgrid: # Email available
|
703
|
-
channels.append(TwilioChannelType.EMAIL)
|
704
|
-
|
705
|
-
return channels
|
706
|
-
|
707
|
-
def _get_channel_order(
|
708
|
-
self,
|
709
|
-
available_channels: List[TwilioChannelType],
|
710
|
-
preferred_channel: Optional[TwilioChannelType],
|
711
|
-
is_email: bool,
|
712
|
-
config: TwilioConfig
|
713
|
-
) -> List[TwilioChannelType]:
|
714
|
-
"""Determine optimal channel order for delivery attempts."""
|
715
|
-
|
716
|
-
# If preferred channel is specified and available, try it first
|
717
|
-
if preferred_channel and preferred_channel in available_channels:
|
718
|
-
ordered_channels = [preferred_channel]
|
719
|
-
remaining = [ch for ch in available_channels if ch != preferred_channel]
|
720
|
-
ordered_channels.extend(remaining)
|
721
|
-
return ordered_channels
|
722
|
-
|
723
|
-
# Default ordering based on identifier type and configuration
|
724
|
-
if is_email:
|
725
|
-
return [TwilioChannelType.EMAIL]
|
726
|
-
|
727
|
-
# For phone numbers, prefer WhatsApp -> SMS
|
728
|
-
phone_channels = []
|
729
|
-
if TwilioChannelType.WHATSAPP in available_channels:
|
730
|
-
phone_channels.append(TwilioChannelType.WHATSAPP)
|
731
|
-
if TwilioChannelType.SMS in available_channels:
|
732
|
-
phone_channels.append(TwilioChannelType.SMS)
|
733
|
-
|
734
|
-
return phone_channels
|
735
|
-
|
736
|
-
def _send_via_channel(self, identifier: str, channel: TwilioChannelType) -> Tuple[bool, str]:
|
737
|
-
"""Send OTP via specific channel."""
|
738
|
-
if channel == TwilioChannelType.WHATSAPP:
|
739
|
-
return self._whatsapp_service.send_otp(identifier, fallback_to_sms=False)
|
740
|
-
elif channel == TwilioChannelType.SMS:
|
741
|
-
return self._sms_service.send_otp(identifier)
|
742
|
-
elif channel == TwilioChannelType.EMAIL:
|
743
|
-
success, message, _ = self._email_service.send_otp(identifier)
|
744
|
-
return success, message
|
745
|
-
else:
|
746
|
-
raise TwilioSendError(f"Unsupported channel: {channel.value}")
|
747
|
-
|
748
|
-
def _verify_twilio_otp(self, phone_number: str, code: str, config: TwilioConfig) -> Tuple[bool, str]:
|
749
|
-
"""Verify OTP using Twilio Verify API."""
|
750
|
-
try:
|
751
|
-
client = self.get_twilio_client()
|
752
|
-
|
753
|
-
verification_check = client.verify.v2.services(
|
754
|
-
config.verify.service_sid
|
755
|
-
).verification_checks.create(
|
756
|
-
to=phone_number,
|
757
|
-
code=code
|
758
|
-
)
|
759
|
-
|
760
|
-
if verification_check.status == 'approved':
|
761
|
-
logger.info(f"OTP verified successfully for {self._mask_identifier(phone_number)}")
|
762
|
-
return True, "OTP verified successfully"
|
763
|
-
else:
|
764
|
-
return False, f"Invalid OTP code: {verification_check.status}"
|
765
|
-
|
766
|
-
except TwilioException as e:
|
767
|
-
raise TwilioVerificationError(
|
768
|
-
f"OTP verification failed: {e}",
|
769
|
-
phone_number=phone_number,
|
770
|
-
twilio_error_code=getattr(e, 'code', None),
|
771
|
-
twilio_error_message=str(e)
|
772
|
-
) from e
|
773
|
-
|
774
|
-
def _verify_stored_otp(self, identifier: str, code: str) -> Tuple[bool, str]:
|
775
|
-
"""Verify OTP using stored codes (for email)."""
|
776
|
-
stored_data = self._get_stored_otp(identifier)
|
777
|
-
|
778
|
-
if not stored_data:
|
779
|
-
return False, "OTP not found. Please request a new code."
|
780
|
-
|
781
|
-
if datetime.now() > stored_data['expires_at']:
|
782
|
-
self._remove_otp(identifier)
|
783
|
-
return False, "OTP expired. Please request a new code."
|
784
|
-
|
785
|
-
# Increment attempt counter
|
786
|
-
stored_data['attempts'] += 1
|
787
|
-
|
788
|
-
if stored_data['attempts'] > 5: # Max attempts
|
789
|
-
self._remove_otp(identifier)
|
790
|
-
return False, "Too many attempts. Please request a new code."
|
791
|
-
|
792
|
-
if stored_data['code'] == code:
|
793
|
-
self._remove_otp(identifier)
|
794
|
-
logger.info(f"Stored OTP verified successfully for {self._mask_identifier(identifier)}")
|
795
|
-
return True, "OTP verified successfully"
|
796
|
-
else:
|
797
|
-
return False, f"Invalid OTP code. {5 - stored_data['attempts']} attempts remaining."
|
798
|
-
|
799
|
-
|
800
|
-
class DjangoTwilioService(UnifiedOTPService):
|
801
|
-
"""
|
802
|
-
Main Twilio service for django_cfg integration.
|
803
|
-
|
804
|
-
Provides unified access to all Twilio services with auto-configuration
|
805
|
-
and comprehensive error handling. This is the primary service class
|
806
|
-
that should be used in most applications.
|
807
|
-
"""
|
808
|
-
|
809
|
-
def __init__(self):
|
810
|
-
"""Initialize with all service capabilities."""
|
811
|
-
super().__init__()
|
812
|
-
logger.info("DjangoTwilioService initialized with auto-configuration")
|
813
|
-
|
814
|
-
def get_service_status(self) -> Dict[str, Any]:
|
815
|
-
"""
|
816
|
-
Get comprehensive status of all Twilio services.
|
817
|
-
|
818
|
-
Returns:
|
819
|
-
Dictionary with service status information
|
820
|
-
"""
|
821
|
-
try:
|
822
|
-
config = self.get_twilio_config()
|
823
|
-
|
824
|
-
status = {
|
825
|
-
"twilio_configured": True,
|
826
|
-
"account_sid": config.account_sid,
|
827
|
-
"region": config.region.value,
|
828
|
-
"services": {},
|
829
|
-
"enabled_channels": [ch.value for ch in config.get_enabled_channels()],
|
830
|
-
"test_mode": config.test_mode,
|
831
|
-
}
|
832
|
-
|
833
|
-
# Check Verify service
|
834
|
-
if config.verify:
|
835
|
-
status["services"]["verify"] = {
|
836
|
-
"enabled": True,
|
837
|
-
"service_sid": config.verify.service_sid,
|
838
|
-
"default_channel": config.verify.default_channel.value,
|
839
|
-
"fallback_channels": [ch.value for ch in config.verify.fallback_channels],
|
840
|
-
"code_length": config.verify.code_length,
|
841
|
-
"ttl_seconds": config.verify.ttl_seconds,
|
842
|
-
}
|
843
|
-
else:
|
844
|
-
status["services"]["verify"] = {"enabled": False}
|
845
|
-
|
846
|
-
# Check SendGrid service
|
847
|
-
if config.sendgrid:
|
848
|
-
status["services"]["sendgrid"] = {
|
849
|
-
"enabled": True,
|
850
|
-
"from_email": config.sendgrid.from_email,
|
851
|
-
"from_name": config.sendgrid.from_name,
|
852
|
-
"template_configured": config.sendgrid.otp_template_id is not None,
|
853
|
-
"tracking_enabled": config.sendgrid.tracking_enabled,
|
854
|
-
}
|
855
|
-
else:
|
856
|
-
status["services"]["sendgrid"] = {"enabled": False}
|
857
|
-
|
858
|
-
return status
|
859
|
-
|
860
|
-
except Exception as e:
|
861
|
-
return {
|
862
|
-
"twilio_configured": False,
|
863
|
-
"error": str(e),
|
864
|
-
"services": {},
|
865
|
-
}
|
866
|
-
|
867
|
-
|
868
|
-
# Convenience functions for direct usage
|
869
|
-
def send_whatsapp_otp(phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
870
|
-
"""Send WhatsApp OTP with optional SMS fallback."""
|
871
|
-
service = WhatsAppOTPService()
|
872
|
-
return service.send_otp(phone_number, fallback_to_sms)
|
873
|
-
|
874
|
-
|
875
|
-
def send_email_otp(email: str, subject: Optional[str] = None) -> Tuple[bool, str, str]:
|
876
|
-
"""Send email OTP."""
|
877
|
-
service = EmailOTPService()
|
878
|
-
return service.send_otp(email, subject)
|
879
|
-
|
880
|
-
|
881
|
-
def send_sms_otp(phone_number: str) -> Tuple[bool, str]:
|
882
|
-
"""Send SMS OTP."""
|
883
|
-
service = SMSOTPService()
|
884
|
-
return service.send_otp(phone_number)
|
885
|
-
|
886
|
-
|
887
|
-
def verify_otp(identifier: str, code: str) -> Tuple[bool, str]:
|
888
|
-
"""Verify OTP code for any channel."""
|
889
|
-
service = UnifiedOTPService()
|
890
|
-
return service.verify_otp(identifier, code)
|
891
|
-
|
892
|
-
|
893
|
-
# Async convenience functions
|
894
|
-
async def asend_whatsapp_otp(phone_number: str, fallback_to_sms: bool = True) -> Tuple[bool, str]:
|
895
|
-
"""Async send WhatsApp OTP."""
|
896
|
-
service = WhatsAppOTPService()
|
897
|
-
return await service.asend_otp(phone_number, fallback_to_sms)
|
898
|
-
|
899
|
-
|
900
|
-
async def asend_email_otp(email: str, subject: Optional[str] = None) -> Tuple[bool, str, str]:
|
901
|
-
"""Async send email OTP."""
|
902
|
-
service = EmailOTPService()
|
903
|
-
return await service.asend_otp(email, subject)
|
904
|
-
|
905
|
-
|
906
|
-
async def asend_sms_otp(phone_number: str) -> Tuple[bool, str]:
|
907
|
-
"""Async send SMS OTP."""
|
908
|
-
service = SMSOTPService()
|
909
|
-
return await service.asend_otp(phone_number)
|
910
|
-
|
911
|
-
|
912
|
-
async def averify_otp(identifier: str, code: str) -> Tuple[bool, str]:
|
913
|
-
"""Async verify OTP code."""
|
914
|
-
service = UnifiedOTPService()
|
915
|
-
return await service.averify_otp(identifier, code)
|
916
|
-
|
917
|
-
|
918
|
-
# Export all service classes and functions
|
919
|
-
__all__ = [
|
920
|
-
# Service classes
|
921
|
-
"DjangoTwilioService",
|
922
|
-
"WhatsAppOTPService",
|
923
|
-
"EmailOTPService",
|
924
|
-
"SMSOTPService",
|
925
|
-
"UnifiedOTPService",
|
926
|
-
"BaseTwilioService",
|
927
|
-
|
928
|
-
# Sync convenience functions
|
929
|
-
"send_whatsapp_otp",
|
930
|
-
"send_email_otp",
|
931
|
-
"send_sms_otp",
|
932
|
-
"verify_otp",
|
933
|
-
|
934
|
-
# Async convenience functions
|
935
|
-
"asend_whatsapp_otp",
|
936
|
-
"asend_email_otp",
|
937
|
-
"asend_sms_otp",
|
938
|
-
"averify_otp",
|
939
|
-
|
940
|
-
# Utility functions
|
941
|
-
"is_async_context",
|
942
|
-
]
|