dwyeapi 0.8.2__tar.gz → 0.9.1__tar.gz
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.
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/PKG-INFO +1 -8
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/README.md +1 -4
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/pyproject.toml +2 -7
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/__init__.py +1 -1
- dwyeapi-0.9.1/src/dwyeapi/providers/email/__init__.py +62 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/providers/email/config.py +6 -38
- dwyeapi-0.9.1/src/dwyeapi/providers/email/factory.py +80 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/providers/email/protocol.py +1 -1
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi.egg-info/PKG-INFO +1 -8
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi.egg-info/SOURCES.txt +0 -9
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi.egg-info/requires.txt +0 -10
- dwyeapi-0.8.2/src/dwyeapi/providers/email/__init__.py +0 -29
- dwyeapi-0.8.2/src/dwyeapi/providers/email/aliyun.py +0 -61
- dwyeapi-0.8.2/src/dwyeapi/providers/email/factory.py +0 -69
- dwyeapi-0.8.2/src/dwyeapi/providers/email/mock.py +0 -15
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/__init__.py +0 -28
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/aliyun.py +0 -46
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/base.py +0 -71
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/config.py +0 -64
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/factory.py +0 -48
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/mock.py +0 -23
- dwyeapi-0.8.2/src/dwyeapi/providers/sms/protocol.py +0 -34
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/setup.cfg +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/cache.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/config.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/database.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/dependencies.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/dt.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/exceptions.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/health.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/logger.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/masking.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/pagination.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/providers/__init__.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/providers/email/base.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/providers/email/resend.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/response.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/security.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/__init__.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/context.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/model.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/pool.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/registry.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/router.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/schema.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/service.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi/tasks/worker.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi.egg-info/dependency_links.txt +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/src/dwyeapi.egg-info/top_level.txt +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_cache.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_config.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_database.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_dependencies.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_exceptions.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_health.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_logger.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_masking.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_pagination.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_response.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_security.py +0 -0
- {dwyeapi-0.8.2 → dwyeapi-0.9.1}/tests/test_tasks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dwyeapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Lightweight FastAPI infrastructure — exceptions, database, config, security, cache, pagination, logger
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -16,15 +16,8 @@ Provides-Extra: tasks
|
|
|
16
16
|
Requires-Dist: arq>=0.26.0; extra == "tasks"
|
|
17
17
|
Provides-Extra: email-resend
|
|
18
18
|
Requires-Dist: resend[async]>=2.29.0; extra == "email-resend"
|
|
19
|
-
Provides-Extra: email-aliyun
|
|
20
|
-
Requires-Dist: alibabacloud_dm20151123>=1.0.0; extra == "email-aliyun"
|
|
21
|
-
Provides-Extra: sms-aliyun
|
|
22
|
-
Requires-Dist: alibabacloud_dysmsapi20170525>=2.0.0; extra == "sms-aliyun"
|
|
23
19
|
Provides-Extra: email
|
|
24
20
|
Requires-Dist: resend[async]>=2.29.0; extra == "email"
|
|
25
|
-
Requires-Dist: alibabacloud_dm20151123>=1.0.0; extra == "email"
|
|
26
|
-
Provides-Extra: sms
|
|
27
|
-
Requires-Dist: alibabacloud_dysmsapi20170525>=2.0.0; extra == "sms"
|
|
28
21
|
Provides-Extra: dev
|
|
29
22
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
30
23
|
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
@@ -19,7 +19,7 @@ pip install dwyeapi
|
|
|
19
19
|
- `response` — Unified API response helpers
|
|
20
20
|
- `pagination` — PaginationParams + paginate helper
|
|
21
21
|
- `logger` — Facade logger (loguru-backed) with daily + size rotation and stdlib interception; downstream code depends on `dwyeapi.logger.Logger`, not loguru directly
|
|
22
|
-
- `providers.email`
|
|
22
|
+
- `providers.email` — 邮件验证码 Provider 工厂(内置 resend;支持业务通过 `register_email_provider` 注入自定义实现)
|
|
23
23
|
|
|
24
24
|
## Environment (dev / prod)
|
|
25
25
|
|
|
@@ -54,6 +54,3 @@ app = FastAPI(
|
|
|
54
54
|
engine = create_async_engine_factory(settings.database_url, echo=is_dev())
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
### Mock provider 仅 dev 可用
|
|
58
|
-
|
|
59
|
-
`providers.email` / `providers.sms` 的 `provider="mock"` 只在 `ENVIRONMENT=dev` 下可用。prod 环境下调用 `make_email_provider()` / `make_sms_provider()` 会抛 `ValueError`,保证正式环境一旦误配置 `EMAIL__PROVIDER=mock` 会启动失败(fail fast),而不会静默吞掉真实验证码。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dwyeapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Lightweight FastAPI infrastructure — exceptions, database, config, security, cache, pagination, logger"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -23,14 +23,9 @@ tasks = [
|
|
|
23
23
|
# Email Provider 实现 — 按需安装
|
|
24
24
|
# resend[async] 会拉入 httpx 以启用 Emails.send_async 原生异步 API
|
|
25
25
|
email-resend = ["resend[async]>=2.29.0"]
|
|
26
|
-
email-aliyun = ["alibabacloud_dm20151123>=1.0.0"]
|
|
27
|
-
|
|
28
|
-
# SMS Provider 实现 — 按需安装
|
|
29
|
-
sms-aliyun = ["alibabacloud_dysmsapi20170525>=2.0.0"]
|
|
30
26
|
|
|
31
27
|
# 聚合 extras(按模块)
|
|
32
|
-
email = ["resend[async]>=2.29.0"
|
|
33
|
-
sms = ["alibabacloud_dysmsapi20170525>=2.0.0"]
|
|
28
|
+
email = ["resend[async]>=2.29.0"]
|
|
34
29
|
|
|
35
30
|
dev = [
|
|
36
31
|
"pytest>=8.0.0",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Email Provider 模块 -- 提供可插拔的邮件验证码发送能力。
|
|
2
|
+
|
|
3
|
+
模块由 Protocol 抽象 + 抽象基类 + 配置类 + 工厂函数 + 内置 resend 实现组成。
|
|
4
|
+
内置仅 resend 一种 provider;业务项目可通过 `register_email_provider` 注入自定义 provider
|
|
5
|
+
(继承 `EmailProviderBase` 实现 `_send`,即可复用 Redis 验证码 + 品牌化模板)。
|
|
6
|
+
|
|
7
|
+
典型用法 -- 内置 resend:
|
|
8
|
+
from dwyeapi.providers.email import EmailSettings, make_email_provider
|
|
9
|
+
|
|
10
|
+
class Settings(DwyBaseSettings):
|
|
11
|
+
email: EmailSettings = EmailSettings()
|
|
12
|
+
|
|
13
|
+
settings = Settings()
|
|
14
|
+
provider = make_email_provider(settings.email)
|
|
15
|
+
await provider.send_code("user@example.com")
|
|
16
|
+
|
|
17
|
+
典型用法 -- 注册自定义 provider:
|
|
18
|
+
from dwyeapi.providers.email import (
|
|
19
|
+
EmailProvider,
|
|
20
|
+
EmailProviderBase,
|
|
21
|
+
EmailSettings,
|
|
22
|
+
register_email_provider,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
class TencentSesProvider(EmailProviderBase):
|
|
26
|
+
async def _send(self, target: str, code: str) -> bool:
|
|
27
|
+
html = self._render_code_html(code)
|
|
28
|
+
text = self._render_code_text(code)
|
|
29
|
+
# 调腾讯云 SES API 发送 ...
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
def _build(s: EmailSettings) -> EmailProvider:
|
|
33
|
+
from app.config import settings as app_settings
|
|
34
|
+
return TencentSesProvider(
|
|
35
|
+
secret_id=app_settings.tencent.secret_id,
|
|
36
|
+
code_ttl=s.code_ttl,
|
|
37
|
+
brand_name=s.brand_name,
|
|
38
|
+
...,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
register_email_provider("tencent_ses", _build)
|
|
42
|
+
# .env: EMAIL__PROVIDER=tencent_ses
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from dwyeapi.providers.email.base import EmailProviderBase
|
|
46
|
+
from dwyeapi.providers.email.config import EmailSettings, ResendConfig
|
|
47
|
+
from dwyeapi.providers.email.factory import (
|
|
48
|
+
EmailProviderFactory,
|
|
49
|
+
make_email_provider,
|
|
50
|
+
register_email_provider,
|
|
51
|
+
)
|
|
52
|
+
from dwyeapi.providers.email.protocol import EmailProvider
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"EmailProvider",
|
|
56
|
+
"EmailProviderBase",
|
|
57
|
+
"EmailProviderFactory",
|
|
58
|
+
"EmailSettings",
|
|
59
|
+
"ResendConfig",
|
|
60
|
+
"make_email_provider",
|
|
61
|
+
"register_email_provider",
|
|
62
|
+
]
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
"""Email Provider 配置模型。"""
|
|
2
2
|
|
|
3
|
-
from typing import Literal
|
|
4
|
-
|
|
5
3
|
from pydantic import BaseModel, Field
|
|
6
4
|
|
|
7
5
|
|
|
@@ -26,35 +24,6 @@ class ResendConfig(BaseModel):
|
|
|
26
24
|
)
|
|
27
25
|
|
|
28
26
|
|
|
29
|
-
class AliyunEmailConfig(BaseModel):
|
|
30
|
-
"""阿里云邮件推送 Provider 配置。
|
|
31
|
-
|
|
32
|
-
EMAIL__PROVIDER=aliyun 时生效;空串默认仅为 pydantic 嵌套实例化需要,
|
|
33
|
-
真正启用时凭证 + account_name 必须通过 .env 显式提供。
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
access_key_id: str = Field(
|
|
37
|
-
default="",
|
|
38
|
-
description="[必填: aliyun] 阿里云 AccessKey ID",
|
|
39
|
-
)
|
|
40
|
-
access_key_secret: str = Field(
|
|
41
|
-
default="",
|
|
42
|
-
description="[必填: aliyun] 阿里云 AccessKey Secret",
|
|
43
|
-
)
|
|
44
|
-
account_name: str = Field(
|
|
45
|
-
default="",
|
|
46
|
-
description="[必填: aliyun] 发信地址,需在阿里云邮件推送控制台预先配置",
|
|
47
|
-
)
|
|
48
|
-
from_alias: str = Field(
|
|
49
|
-
default="",
|
|
50
|
-
description="发件人显示名,留空则展示 account_name",
|
|
51
|
-
)
|
|
52
|
-
subject: str = Field(
|
|
53
|
-
default="验证码",
|
|
54
|
-
description="验证码邮件主题",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
27
|
class EmailSettings(BaseModel):
|
|
59
28
|
"""Email 模块配置 -- 业务项目嵌入 Settings 的入口。
|
|
60
29
|
|
|
@@ -70,9 +39,12 @@ class EmailSettings(BaseModel):
|
|
|
70
39
|
EMAIL__RESEND__API_KEY=re_xxx
|
|
71
40
|
"""
|
|
72
41
|
|
|
73
|
-
provider:
|
|
74
|
-
default="
|
|
75
|
-
description=
|
|
42
|
+
provider: str = Field(
|
|
43
|
+
default="resend",
|
|
44
|
+
description=(
|
|
45
|
+
"Email Provider 名称。内置仅支持 'resend';"
|
|
46
|
+
"业务可通过 register_email_provider(name, factory) 注册自定义 provider 后填入对应名称"
|
|
47
|
+
),
|
|
76
48
|
)
|
|
77
49
|
code_ttl: int = Field(
|
|
78
50
|
default=300,
|
|
@@ -106,7 +78,3 @@ class EmailSettings(BaseModel):
|
|
|
106
78
|
default_factory=ResendConfig,
|
|
107
79
|
description="Resend 配置,provider=resend 时必填",
|
|
108
80
|
)
|
|
109
|
-
aliyun: AliyunEmailConfig = Field(
|
|
110
|
-
default_factory=AliyunEmailConfig,
|
|
111
|
-
description="阿里云邮件推送配置,provider=aliyun 时必填",
|
|
112
|
-
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Email Provider 工厂 -- 内置 resend,支持外部注册自定义 provider。"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from dwyeapi.logger import get_logger
|
|
6
|
+
from dwyeapi.providers.email.config import EmailSettings
|
|
7
|
+
from dwyeapi.providers.email.protocol import EmailProvider
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
EmailProviderFactory = Callable[[EmailSettings], EmailProvider]
|
|
12
|
+
|
|
13
|
+
_BUILTIN_RESEND = "resend"
|
|
14
|
+
_PROVIDER_REGISTRY: dict[str, EmailProviderFactory] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_email_provider(name: str, factory: EmailProviderFactory) -> None:
|
|
18
|
+
"""注册自定义 Email Provider。
|
|
19
|
+
|
|
20
|
+
业务项目继承 `EmailProviderBase` 实现 `_send` 后,通过此函数注册到工厂,
|
|
21
|
+
`.env` 设 `EMAIL__PROVIDER=<name>` 即可启用。
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: provider 名称,与 EMAIL__PROVIDER 环境变量值对应。
|
|
25
|
+
factory: 工厂函数,接收 EmailSettings 返回 EmailProvider 实例。
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: 名称为空或与内置名称冲突。
|
|
29
|
+
"""
|
|
30
|
+
if not name:
|
|
31
|
+
raise ValueError("provider 名称不能为空")
|
|
32
|
+
if name == _BUILTIN_RESEND:
|
|
33
|
+
raise ValueError(f"'{name}' 为内置 provider 名称,不可覆盖")
|
|
34
|
+
if name in _PROVIDER_REGISTRY:
|
|
35
|
+
logger.warning("Email provider %s 被覆盖注册", name)
|
|
36
|
+
_PROVIDER_REGISTRY[name] = factory
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_email_provider(settings: EmailSettings) -> EmailProvider:
|
|
40
|
+
"""根据 `settings.provider` 构造对应的 Email Provider 实例。
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
settings: Email 模块配置(项目 Settings 的嵌套字段)。
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
实现了 `EmailProvider` Protocol 的实例。
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: 未知 provider 或必填配置缺失。
|
|
50
|
+
ImportError: resend extra 未安装。
|
|
51
|
+
"""
|
|
52
|
+
common = {
|
|
53
|
+
"code_ttl": settings.code_ttl,
|
|
54
|
+
"code_length": settings.code_length,
|
|
55
|
+
"brand_name": settings.brand_name,
|
|
56
|
+
"brand_tagline": settings.brand_tagline,
|
|
57
|
+
"brand_url": settings.brand_url,
|
|
58
|
+
"brand_slogan": settings.brand_slogan,
|
|
59
|
+
"support_email": settings.support_email,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if settings.provider == _BUILTIN_RESEND:
|
|
63
|
+
from dwyeapi.providers.email.resend import ResendEmailProvider
|
|
64
|
+
|
|
65
|
+
if not settings.resend.api_key:
|
|
66
|
+
raise ValueError("EMAIL__RESEND__API_KEY 未配置")
|
|
67
|
+
return ResendEmailProvider(
|
|
68
|
+
api_key=settings.resend.api_key,
|
|
69
|
+
from_email=settings.resend.from_email,
|
|
70
|
+
subject=settings.resend.subject,
|
|
71
|
+
**common,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if settings.provider in _PROVIDER_REGISTRY:
|
|
75
|
+
return _PROVIDER_REGISTRY[settings.provider](settings)
|
|
76
|
+
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"未知 email provider: {settings.provider};"
|
|
79
|
+
f"内置仅支持 'resend',自定义 provider 需先调用 register_email_provider 注册"
|
|
80
|
+
)
|
|
@@ -6,7 +6,7 @@ from typing import Protocol
|
|
|
6
6
|
class EmailProvider(Protocol):
|
|
7
7
|
"""邮箱验证码服务抽象接口。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
内置实现:ResendEmailProvider;业务可通过 `register_email_provider` 注入自定义实现。
|
|
10
10
|
通过 `dwyeapi.providers.email.make_email_provider(settings.email)` 工厂根据配置选择实现。
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dwyeapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Lightweight FastAPI infrastructure — exceptions, database, config, security, cache, pagination, logger
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -16,15 +16,8 @@ Provides-Extra: tasks
|
|
|
16
16
|
Requires-Dist: arq>=0.26.0; extra == "tasks"
|
|
17
17
|
Provides-Extra: email-resend
|
|
18
18
|
Requires-Dist: resend[async]>=2.29.0; extra == "email-resend"
|
|
19
|
-
Provides-Extra: email-aliyun
|
|
20
|
-
Requires-Dist: alibabacloud_dm20151123>=1.0.0; extra == "email-aliyun"
|
|
21
|
-
Provides-Extra: sms-aliyun
|
|
22
|
-
Requires-Dist: alibabacloud_dysmsapi20170525>=2.0.0; extra == "sms-aliyun"
|
|
23
19
|
Provides-Extra: email
|
|
24
20
|
Requires-Dist: resend[async]>=2.29.0; extra == "email"
|
|
25
|
-
Requires-Dist: alibabacloud_dm20151123>=1.0.0; extra == "email"
|
|
26
|
-
Provides-Extra: sms
|
|
27
|
-
Requires-Dist: alibabacloud_dysmsapi20170525>=2.0.0; extra == "sms"
|
|
28
21
|
Provides-Extra: dev
|
|
29
22
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
30
23
|
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
@@ -20,20 +20,11 @@ src/dwyeapi.egg-info/requires.txt
|
|
|
20
20
|
src/dwyeapi.egg-info/top_level.txt
|
|
21
21
|
src/dwyeapi/providers/__init__.py
|
|
22
22
|
src/dwyeapi/providers/email/__init__.py
|
|
23
|
-
src/dwyeapi/providers/email/aliyun.py
|
|
24
23
|
src/dwyeapi/providers/email/base.py
|
|
25
24
|
src/dwyeapi/providers/email/config.py
|
|
26
25
|
src/dwyeapi/providers/email/factory.py
|
|
27
|
-
src/dwyeapi/providers/email/mock.py
|
|
28
26
|
src/dwyeapi/providers/email/protocol.py
|
|
29
27
|
src/dwyeapi/providers/email/resend.py
|
|
30
|
-
src/dwyeapi/providers/sms/__init__.py
|
|
31
|
-
src/dwyeapi/providers/sms/aliyun.py
|
|
32
|
-
src/dwyeapi/providers/sms/base.py
|
|
33
|
-
src/dwyeapi/providers/sms/config.py
|
|
34
|
-
src/dwyeapi/providers/sms/factory.py
|
|
35
|
-
src/dwyeapi/providers/sms/mock.py
|
|
36
|
-
src/dwyeapi/providers/sms/protocol.py
|
|
37
28
|
src/dwyeapi/tasks/__init__.py
|
|
38
29
|
src/dwyeapi/tasks/context.py
|
|
39
30
|
src/dwyeapi/tasks/model.py
|
|
@@ -18,19 +18,9 @@ fakeredis>=2.0.0
|
|
|
18
18
|
|
|
19
19
|
[email]
|
|
20
20
|
resend[async]>=2.29.0
|
|
21
|
-
alibabacloud_dm20151123>=1.0.0
|
|
22
|
-
|
|
23
|
-
[email-aliyun]
|
|
24
|
-
alibabacloud_dm20151123>=1.0.0
|
|
25
21
|
|
|
26
22
|
[email-resend]
|
|
27
23
|
resend[async]>=2.29.0
|
|
28
24
|
|
|
29
|
-
[sms]
|
|
30
|
-
alibabacloud_dysmsapi20170525>=2.0.0
|
|
31
|
-
|
|
32
|
-
[sms-aliyun]
|
|
33
|
-
alibabacloud_dysmsapi20170525>=2.0.0
|
|
34
|
-
|
|
35
25
|
[tasks]
|
|
36
26
|
arq>=0.26.0
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""Email Provider 模块 -- 提供可插拔的邮件验证码发送能力。
|
|
2
|
-
|
|
3
|
-
提供 Protocol 抽象 + 配置类 + 工厂函数 + 多种实现(mock/resend/aliyun)。
|
|
4
|
-
业务项目通过 `pip install dwyeapi[email-resend]` 等 extras 按需安装 provider 依赖。
|
|
5
|
-
|
|
6
|
-
典型用法:
|
|
7
|
-
from dwyeapi.providers.email import EmailSettings, make_email_provider
|
|
8
|
-
|
|
9
|
-
class Settings(DwyBaseSettings):
|
|
10
|
-
email: EmailSettings = EmailSettings()
|
|
11
|
-
|
|
12
|
-
settings = Settings()
|
|
13
|
-
provider = make_email_provider(settings.email)
|
|
14
|
-
await provider.send_code("user@example.com")
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from dwyeapi.providers.email.base import EmailProviderBase
|
|
18
|
-
from dwyeapi.providers.email.config import AliyunEmailConfig, EmailSettings, ResendConfig
|
|
19
|
-
from dwyeapi.providers.email.factory import make_email_provider
|
|
20
|
-
from dwyeapi.providers.email.protocol import EmailProvider
|
|
21
|
-
|
|
22
|
-
__all__ = [
|
|
23
|
-
"AliyunEmailConfig",
|
|
24
|
-
"EmailProvider",
|
|
25
|
-
"EmailProviderBase",
|
|
26
|
-
"EmailSettings",
|
|
27
|
-
"ResendConfig",
|
|
28
|
-
"make_email_provider",
|
|
29
|
-
]
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""阿里云邮件推送 Email Provider -- 占位实现。
|
|
2
|
-
|
|
3
|
-
需要 `pip install dwyeapi[email-aliyun]` 才能使用。实际调用待首次接入项目时补全。
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import redis.asyncio as aioredis
|
|
7
|
-
|
|
8
|
-
from dwyeapi.providers.email.base import DEFAULT_CODE_LENGTH, DEFAULT_CODE_TTL, EmailProviderBase
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class AliyunEmailProvider(EmailProviderBase):
|
|
12
|
-
"""基于阿里云邮件推送的 Email Provider(占位)。
|
|
13
|
-
|
|
14
|
-
首次接入项目时基于 `alibabacloud_dm20151123` SDK 实现 `_send()`,
|
|
15
|
-
调用时使用 `self._render_code_html(code)` / `self._render_code_text(code)` 复用品牌模板。
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
def __init__(
|
|
19
|
-
self,
|
|
20
|
-
access_key_id: str,
|
|
21
|
-
access_key_secret: str,
|
|
22
|
-
account_name: str,
|
|
23
|
-
from_alias: str = "",
|
|
24
|
-
subject: str = "验证码",
|
|
25
|
-
code_ttl: int = DEFAULT_CODE_TTL,
|
|
26
|
-
code_length: int = DEFAULT_CODE_LENGTH,
|
|
27
|
-
brand_name: str = "",
|
|
28
|
-
brand_tagline: str = "",
|
|
29
|
-
brand_url: str = "",
|
|
30
|
-
brand_slogan: str = "",
|
|
31
|
-
support_email: str = "",
|
|
32
|
-
redis: aioredis.Redis | None = None,
|
|
33
|
-
) -> None:
|
|
34
|
-
"""初始化。
|
|
35
|
-
|
|
36
|
-
Raises:
|
|
37
|
-
ImportError: 未安装 alibabacloud_dm20151123 时抛出。
|
|
38
|
-
"""
|
|
39
|
-
super().__init__(
|
|
40
|
-
code_ttl=code_ttl,
|
|
41
|
-
code_length=code_length,
|
|
42
|
-
brand_name=brand_name,
|
|
43
|
-
brand_tagline=brand_tagline,
|
|
44
|
-
brand_url=brand_url,
|
|
45
|
-
brand_slogan=brand_slogan,
|
|
46
|
-
support_email=support_email,
|
|
47
|
-
redis=redis,
|
|
48
|
-
)
|
|
49
|
-
try:
|
|
50
|
-
import alibabacloud_dm20151123 # noqa: F401
|
|
51
|
-
except ImportError as e:
|
|
52
|
-
raise ImportError("使用 AliyunEmailProvider 需要安装: pip install dwyeapi[email-aliyun]") from e
|
|
53
|
-
self._access_key_id = access_key_id
|
|
54
|
-
self._access_key_secret = access_key_secret
|
|
55
|
-
self._account_name = account_name
|
|
56
|
-
self._from_alias = from_alias or brand_name
|
|
57
|
-
self._subject = subject
|
|
58
|
-
|
|
59
|
-
async def _send(self, target: str, code: str) -> bool:
|
|
60
|
-
"""调用阿里云 DirectMail API 发送邮件。"""
|
|
61
|
-
raise NotImplementedError("AliyunEmailProvider 暂未实现,首次接入项目时补全 _send")
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""Email Provider 工厂 -- 根据配置选择实现。"""
|
|
2
|
-
|
|
3
|
-
from dwyeapi.config import is_prod
|
|
4
|
-
from dwyeapi.providers.email.config import EmailSettings
|
|
5
|
-
from dwyeapi.providers.email.protocol import EmailProvider
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def make_email_provider(settings: EmailSettings) -> EmailProvider:
|
|
9
|
-
"""根据 `settings.provider` 构造对应的 Email Provider 实例。
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
settings: Email 模块配置(项目 Settings 的嵌套字段)。
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
实现了 `EmailProvider` Protocol 的实例。
|
|
16
|
-
|
|
17
|
-
Raises:
|
|
18
|
-
ValueError: 未知 provider、必填配置缺失,或 prod 环境下选择 mock provider。
|
|
19
|
-
ImportError: provider 对应的 extra 未安装。
|
|
20
|
-
"""
|
|
21
|
-
common = {
|
|
22
|
-
"code_ttl": settings.code_ttl,
|
|
23
|
-
"code_length": settings.code_length,
|
|
24
|
-
"brand_name": settings.brand_name,
|
|
25
|
-
"brand_tagline": settings.brand_tagline,
|
|
26
|
-
"brand_url": settings.brand_url,
|
|
27
|
-
"brand_slogan": settings.brand_slogan,
|
|
28
|
-
"support_email": settings.support_email,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
match settings.provider:
|
|
32
|
-
case "mock":
|
|
33
|
-
if is_prod():
|
|
34
|
-
raise ValueError(
|
|
35
|
-
"EMAIL__PROVIDER=mock 在 prod 环境下被禁止;"
|
|
36
|
-
"请设置 ENVIRONMENT=dev 或改用真实 provider(resend/aliyun)"
|
|
37
|
-
)
|
|
38
|
-
from dwyeapi.providers.email.mock import MockEmailProvider
|
|
39
|
-
|
|
40
|
-
return MockEmailProvider(**common)
|
|
41
|
-
|
|
42
|
-
case "resend":
|
|
43
|
-
from dwyeapi.providers.email.resend import ResendEmailProvider
|
|
44
|
-
|
|
45
|
-
if not settings.resend.api_key:
|
|
46
|
-
raise ValueError("EMAIL__RESEND__API_KEY 未配置")
|
|
47
|
-
return ResendEmailProvider(
|
|
48
|
-
api_key=settings.resend.api_key,
|
|
49
|
-
from_email=settings.resend.from_email,
|
|
50
|
-
subject=settings.resend.subject,
|
|
51
|
-
**common,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
case "aliyun":
|
|
55
|
-
from dwyeapi.providers.email.aliyun import AliyunEmailProvider
|
|
56
|
-
|
|
57
|
-
if not settings.aliyun.access_key_id or not settings.aliyun.access_key_secret:
|
|
58
|
-
raise ValueError("EMAIL__ALIYUN__ACCESS_KEY_ID/SECRET 未配置")
|
|
59
|
-
return AliyunEmailProvider(
|
|
60
|
-
access_key_id=settings.aliyun.access_key_id,
|
|
61
|
-
access_key_secret=settings.aliyun.access_key_secret,
|
|
62
|
-
account_name=settings.aliyun.account_name,
|
|
63
|
-
from_alias=settings.aliyun.from_alias,
|
|
64
|
-
subject=settings.aliyun.subject,
|
|
65
|
-
**common,
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
case _:
|
|
69
|
-
raise ValueError(f"未知 email provider: {settings.provider}")
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"""Mock Email Provider -- 开发/测试环境使用,日志打印验证码不真实发送。"""
|
|
2
|
-
|
|
3
|
-
from dwyeapi.logger import get_logger
|
|
4
|
-
from dwyeapi.providers.email.base import EmailProviderBase
|
|
5
|
-
|
|
6
|
-
logger = get_logger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class MockEmailProvider(EmailProviderBase):
|
|
10
|
-
"""Mock 邮箱验证码 Provider。"""
|
|
11
|
-
|
|
12
|
-
async def _send(self, target: str, code: str) -> bool:
|
|
13
|
-
"""打印验证码到日志,始终返回 True。"""
|
|
14
|
-
logger.info("Mock Email sent: target=%s code=%s", target, code)
|
|
15
|
-
return True
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""SMS Provider 模块 -- 提供可插拔的短信验证码发送能力。
|
|
2
|
-
|
|
3
|
-
提供 Protocol 抽象 + 配置类 + 工厂函数 + 多种实现(mock/aliyun)。
|
|
4
|
-
业务项目通过 `pip install dwyeapi[sms-aliyun]` 等 extras 按需安装 provider 依赖。
|
|
5
|
-
|
|
6
|
-
典型用法:
|
|
7
|
-
from dwyeapi.providers.sms import SmsSettings, make_sms_provider
|
|
8
|
-
|
|
9
|
-
class Settings(DwyBaseSettings):
|
|
10
|
-
sms: SmsSettings = SmsSettings()
|
|
11
|
-
|
|
12
|
-
settings = Settings()
|
|
13
|
-
provider = make_sms_provider(settings.sms)
|
|
14
|
-
await provider.send_code("13800138000")
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from dwyeapi.providers.sms.base import SmsProviderBase
|
|
18
|
-
from dwyeapi.providers.sms.config import AliyunSmsConfig, SmsSettings
|
|
19
|
-
from dwyeapi.providers.sms.factory import make_sms_provider
|
|
20
|
-
from dwyeapi.providers.sms.protocol import SmsProvider
|
|
21
|
-
|
|
22
|
-
__all__ = [
|
|
23
|
-
"AliyunSmsConfig",
|
|
24
|
-
"SmsProvider",
|
|
25
|
-
"SmsProviderBase",
|
|
26
|
-
"SmsSettings",
|
|
27
|
-
"make_sms_provider",
|
|
28
|
-
]
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
"""阿里云短信 Provider -- 占位实现。
|
|
2
|
-
|
|
3
|
-
需要 `pip install dwyeapi[sms-aliyun]` 才能使用。实际调用待首次接入项目时补全。
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import redis.asyncio as aioredis
|
|
7
|
-
|
|
8
|
-
from dwyeapi.providers.sms.base import DEFAULT_CODE_LENGTH, DEFAULT_CODE_TTL, SmsProviderBase
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class AliyunSmsProvider(SmsProviderBase):
|
|
12
|
-
"""基于阿里云 dysmsapi 的 SMS Provider(占位)。
|
|
13
|
-
|
|
14
|
-
首次接入项目时基于 `alibabacloud_dysmsapi20170525` SDK 实现 `_send()`。
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
def __init__(
|
|
18
|
-
self,
|
|
19
|
-
access_key_id: str,
|
|
20
|
-
access_key_secret: str,
|
|
21
|
-
sign_name: str,
|
|
22
|
-
template_code: str,
|
|
23
|
-
region_id: str = "cn-hangzhou",
|
|
24
|
-
code_ttl: int = DEFAULT_CODE_TTL,
|
|
25
|
-
code_length: int = DEFAULT_CODE_LENGTH,
|
|
26
|
-
redis: aioredis.Redis | None = None,
|
|
27
|
-
) -> None:
|
|
28
|
-
"""初始化。
|
|
29
|
-
|
|
30
|
-
Raises:
|
|
31
|
-
ImportError: 未安装 alibabacloud_dysmsapi20170525 时抛出。
|
|
32
|
-
"""
|
|
33
|
-
super().__init__(code_ttl=code_ttl, code_length=code_length, redis=redis)
|
|
34
|
-
try:
|
|
35
|
-
import alibabacloud_dysmsapi20170525 # noqa: F401
|
|
36
|
-
except ImportError as e:
|
|
37
|
-
raise ImportError("使用 AliyunSmsProvider 需要安装: pip install dwyeapi[sms-aliyun]") from e
|
|
38
|
-
self._access_key_id = access_key_id
|
|
39
|
-
self._access_key_secret = access_key_secret
|
|
40
|
-
self._sign_name = sign_name
|
|
41
|
-
self._template_code = template_code
|
|
42
|
-
self._region_id = region_id
|
|
43
|
-
|
|
44
|
-
async def _send(self, target: str, code: str) -> bool:
|
|
45
|
-
"""调用阿里云短信 API 发送验证码。"""
|
|
46
|
-
raise NotImplementedError("AliyunSmsProvider 暂未实现,首次接入项目时补全 _send")
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""SMS Provider 基类 -- 封装 Redis 验证码存取逻辑。"""
|
|
2
|
-
|
|
3
|
-
import secrets
|
|
4
|
-
from abc import ABC, abstractmethod
|
|
5
|
-
|
|
6
|
-
import redis.asyncio as aioredis
|
|
7
|
-
|
|
8
|
-
from dwyeapi.cache import get_redis
|
|
9
|
-
|
|
10
|
-
CODE_KEY_PREFIX = "dwyeapi:sms:code:"
|
|
11
|
-
DEFAULT_CODE_TTL = 300
|
|
12
|
-
DEFAULT_CODE_LENGTH = 6
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class SmsProviderBase(ABC):
|
|
16
|
-
"""SMS Provider 抽象基类。
|
|
17
|
-
|
|
18
|
-
共享验证码生成 + Redis 存取 + 一次性校验逻辑;子类只实现 `_send()` 发送动作。
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(
|
|
22
|
-
self,
|
|
23
|
-
code_ttl: int = DEFAULT_CODE_TTL,
|
|
24
|
-
code_length: int = DEFAULT_CODE_LENGTH,
|
|
25
|
-
redis: aioredis.Redis | None = None,
|
|
26
|
-
) -> None:
|
|
27
|
-
"""初始化。
|
|
28
|
-
|
|
29
|
-
Args:
|
|
30
|
-
code_ttl: 验证码有效期(秒),默认 300。
|
|
31
|
-
code_length: 验证码位数,默认 6。
|
|
32
|
-
redis: 可选显式注入的 Redis 连接;为 None 时 fallback 到 dwyeapi.cache.get_redis()。
|
|
33
|
-
"""
|
|
34
|
-
self._ttl = code_ttl
|
|
35
|
-
self._length = code_length
|
|
36
|
-
self._redis = redis
|
|
37
|
-
|
|
38
|
-
async def _get_redis(self) -> aioredis.Redis:
|
|
39
|
-
"""获取 Redis 连接,优先使用注入的,否则取全局单例。"""
|
|
40
|
-
if self._redis is not None:
|
|
41
|
-
return self._redis
|
|
42
|
-
return await get_redis()
|
|
43
|
-
|
|
44
|
-
def _generate_code(self) -> str:
|
|
45
|
-
"""生成指定位数的数字验证码。"""
|
|
46
|
-
return "".join(secrets.choice("0123456789") for _ in range(self._length))
|
|
47
|
-
|
|
48
|
-
async def send_code(self, target: str) -> bool:
|
|
49
|
-
"""生成验证码存 Redis 并调用 `_send()` 发送。"""
|
|
50
|
-
code = self._generate_code()
|
|
51
|
-
redis = await self._get_redis()
|
|
52
|
-
await redis.set(f"{CODE_KEY_PREFIX}{target}", code, ex=self._ttl)
|
|
53
|
-
return await self._send(target, code)
|
|
54
|
-
|
|
55
|
-
async def verify_code(self, target: str, code: str) -> bool:
|
|
56
|
-
"""从 Redis 读取存储的验证码比对,成功则删除 key(一次性)。"""
|
|
57
|
-
redis = await self._get_redis()
|
|
58
|
-
key = f"{CODE_KEY_PREFIX}{target}"
|
|
59
|
-
stored = await redis.get(key)
|
|
60
|
-
if stored is None:
|
|
61
|
-
return False
|
|
62
|
-
stored_str = stored if isinstance(stored, str) else stored.decode()
|
|
63
|
-
if stored_str != code:
|
|
64
|
-
return False
|
|
65
|
-
await redis.delete(key)
|
|
66
|
-
return True
|
|
67
|
-
|
|
68
|
-
@abstractmethod
|
|
69
|
-
async def _send(self, target: str, code: str) -> bool:
|
|
70
|
-
"""子类实现发送动作。"""
|
|
71
|
-
...
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"""SMS Provider 配置模型。"""
|
|
2
|
-
|
|
3
|
-
from typing import Literal
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AliyunSmsConfig(BaseModel):
|
|
9
|
-
"""阿里云短信 Provider 配置。
|
|
10
|
-
|
|
11
|
-
SMS__PROVIDER=aliyun 时生效;空串默认仅为 pydantic 嵌套实例化需要,
|
|
12
|
-
provider=mock 场景下不会读取,真正启用时四项凭证必须通过 .env 显式提供。
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
access_key_id: str = Field(
|
|
16
|
-
default="",
|
|
17
|
-
description="[必填: aliyun] 阿里云 AccessKey ID",
|
|
18
|
-
)
|
|
19
|
-
access_key_secret: str = Field(
|
|
20
|
-
default="",
|
|
21
|
-
description="[必填: aliyun] 阿里云 AccessKey Secret",
|
|
22
|
-
)
|
|
23
|
-
sign_name: str = Field(
|
|
24
|
-
default="",
|
|
25
|
-
description="[必填: aliyun] 短信签名(需在阿里云控制台预先申请)",
|
|
26
|
-
)
|
|
27
|
-
template_code: str = Field(
|
|
28
|
-
default="",
|
|
29
|
-
description="[必填: aliyun] 短信模板 code,例 SMS_123456789",
|
|
30
|
-
)
|
|
31
|
-
region_id: str = Field(
|
|
32
|
-
default="cn-hangzhou",
|
|
33
|
-
description="阿里云地域 ID,默认 cn-hangzhou",
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class SmsSettings(BaseModel):
|
|
38
|
-
"""SMS 模块配置 -- 业务项目嵌入 Settings 的入口。
|
|
39
|
-
|
|
40
|
-
用法:
|
|
41
|
-
class Settings(DwyBaseSettings):
|
|
42
|
-
sms: SmsSettings = SmsSettings()
|
|
43
|
-
|
|
44
|
-
`.env` 自动识别(双下划线嵌套):
|
|
45
|
-
SMS__PROVIDER=aliyun
|
|
46
|
-
SMS__ALIYUN__ACCESS_KEY_ID=LTAI...
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
provider: Literal["mock", "aliyun"] = Field(
|
|
50
|
-
default="mock",
|
|
51
|
-
description='SMS Provider 选择:"mock"(dev 限定,打印到日志)/ "aliyun"(生产)',
|
|
52
|
-
)
|
|
53
|
-
code_ttl: int = Field(
|
|
54
|
-
default=300,
|
|
55
|
-
description="验证码有效期(秒),默认 5 分钟",
|
|
56
|
-
)
|
|
57
|
-
code_length: int = Field(
|
|
58
|
-
default=6,
|
|
59
|
-
description="验证码位数,默认 6 位数字",
|
|
60
|
-
)
|
|
61
|
-
aliyun: AliyunSmsConfig = Field(
|
|
62
|
-
default_factory=AliyunSmsConfig,
|
|
63
|
-
description="阿里云短信凭证,provider=aliyun 时必填",
|
|
64
|
-
)
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
"""SMS Provider 工厂 -- 根据配置选择实现。"""
|
|
2
|
-
|
|
3
|
-
from dwyeapi.config import is_prod
|
|
4
|
-
from dwyeapi.providers.sms.config import SmsSettings
|
|
5
|
-
from dwyeapi.providers.sms.protocol import SmsProvider
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def make_sms_provider(settings: SmsSettings) -> SmsProvider:
|
|
9
|
-
"""根据 `settings.provider` 构造对应的 SMS Provider 实例。
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
settings: SMS 模块配置(项目 Settings 的嵌套字段)。
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
实现了 `SmsProvider` Protocol 的实例。
|
|
16
|
-
|
|
17
|
-
Raises:
|
|
18
|
-
ValueError: 未知 provider、必填配置缺失,或 prod 环境下选择 mock provider。
|
|
19
|
-
ImportError: provider 对应的 extra 未安装。
|
|
20
|
-
"""
|
|
21
|
-
common = {"code_ttl": settings.code_ttl, "code_length": settings.code_length}
|
|
22
|
-
|
|
23
|
-
match settings.provider:
|
|
24
|
-
case "mock":
|
|
25
|
-
if is_prod():
|
|
26
|
-
raise ValueError(
|
|
27
|
-
"SMS__PROVIDER=mock 在 prod 环境下被禁止;请设置 ENVIRONMENT=dev 或改用真实 provider(aliyun)"
|
|
28
|
-
)
|
|
29
|
-
from dwyeapi.providers.sms.mock import MockSmsProvider
|
|
30
|
-
|
|
31
|
-
return MockSmsProvider(**common)
|
|
32
|
-
|
|
33
|
-
case "aliyun":
|
|
34
|
-
from dwyeapi.providers.sms.aliyun import AliyunSmsProvider
|
|
35
|
-
|
|
36
|
-
if not settings.aliyun.access_key_id or not settings.aliyun.access_key_secret:
|
|
37
|
-
raise ValueError("SMS__ALIYUN__ACCESS_KEY_ID/SECRET 未配置")
|
|
38
|
-
return AliyunSmsProvider(
|
|
39
|
-
access_key_id=settings.aliyun.access_key_id,
|
|
40
|
-
access_key_secret=settings.aliyun.access_key_secret,
|
|
41
|
-
sign_name=settings.aliyun.sign_name,
|
|
42
|
-
template_code=settings.aliyun.template_code,
|
|
43
|
-
region_id=settings.aliyun.region_id,
|
|
44
|
-
**common,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
case _:
|
|
48
|
-
raise ValueError(f"未知 sms provider: {settings.provider}")
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Mock SMS Provider -- 开发/测试环境使用,日志打印验证码不真实发送。"""
|
|
2
|
-
|
|
3
|
-
from dwyeapi.logger import get_logger
|
|
4
|
-
from dwyeapi.providers.sms.base import SmsProviderBase
|
|
5
|
-
|
|
6
|
-
logger = get_logger(__name__)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class MockSmsProvider(SmsProviderBase):
|
|
10
|
-
"""Mock 短信验证码 Provider。
|
|
11
|
-
|
|
12
|
-
verify_code 故意放宽:任意 4-8 位数字都通过,方便开发者 curl 调试不需要先 send。
|
|
13
|
-
Email Mock 保持严格校验,因为邮箱注册是主流路径,测试里先 send_code 再 register 更接近生产。
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
async def _send(self, target: str, code: str) -> bool:
|
|
17
|
-
"""打印验证码到日志,始终返回 True。"""
|
|
18
|
-
logger.info("Mock SMS sent: target=%s code=%s", target, code)
|
|
19
|
-
return True
|
|
20
|
-
|
|
21
|
-
async def verify_code(self, target: str, code: str) -> bool:
|
|
22
|
-
"""Mock 模式宽松校验:任意 4-8 位数字即通过。"""
|
|
23
|
-
return 4 <= len(code) <= 8 and code.isdigit()
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""SMS Provider Protocol。"""
|
|
2
|
-
|
|
3
|
-
from typing import Protocol
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class SmsProvider(Protocol):
|
|
7
|
-
"""短信验证码服务抽象接口。
|
|
8
|
-
|
|
9
|
-
实现类:MockSmsProvider(开发)、AliyunSmsProvider(生产)。
|
|
10
|
-
通过 `dwyeapi.providers.sms.make_sms_provider(settings.sms)` 工厂根据配置选择实现。
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
async def send_code(self, target: str) -> bool:
|
|
14
|
-
"""生成并发送短信验证码。
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
target: 收件手机号。
|
|
18
|
-
|
|
19
|
-
Returns:
|
|
20
|
-
发送成功 True,失败 False(异常已记录日志)。
|
|
21
|
-
"""
|
|
22
|
-
...
|
|
23
|
-
|
|
24
|
-
async def verify_code(self, target: str, code: str) -> bool:
|
|
25
|
-
"""校验短信验证码,成功后一次性删除。
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
target: 收件手机号。
|
|
29
|
-
code: 用户提交的验证码。
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
匹配且未过期返回 True,否则 False。
|
|
33
|
-
"""
|
|
34
|
-
...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|