dwyeapi 0.8.2__tar.gz → 0.9.0__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.
Files changed (61) hide show
  1. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/PKG-INFO +1 -8
  2. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/README.md +1 -4
  3. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/pyproject.toml +2 -7
  4. dwyeapi-0.9.0/src/dwyeapi/providers/email/__init__.py +62 -0
  5. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/providers/email/config.py +6 -38
  6. dwyeapi-0.9.0/src/dwyeapi/providers/email/factory.py +80 -0
  7. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/providers/email/protocol.py +1 -1
  8. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi.egg-info/PKG-INFO +1 -8
  9. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi.egg-info/SOURCES.txt +0 -9
  10. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi.egg-info/requires.txt +0 -10
  11. dwyeapi-0.8.2/src/dwyeapi/providers/email/__init__.py +0 -29
  12. dwyeapi-0.8.2/src/dwyeapi/providers/email/aliyun.py +0 -61
  13. dwyeapi-0.8.2/src/dwyeapi/providers/email/factory.py +0 -69
  14. dwyeapi-0.8.2/src/dwyeapi/providers/email/mock.py +0 -15
  15. dwyeapi-0.8.2/src/dwyeapi/providers/sms/__init__.py +0 -28
  16. dwyeapi-0.8.2/src/dwyeapi/providers/sms/aliyun.py +0 -46
  17. dwyeapi-0.8.2/src/dwyeapi/providers/sms/base.py +0 -71
  18. dwyeapi-0.8.2/src/dwyeapi/providers/sms/config.py +0 -64
  19. dwyeapi-0.8.2/src/dwyeapi/providers/sms/factory.py +0 -48
  20. dwyeapi-0.8.2/src/dwyeapi/providers/sms/mock.py +0 -23
  21. dwyeapi-0.8.2/src/dwyeapi/providers/sms/protocol.py +0 -34
  22. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/setup.cfg +0 -0
  23. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/__init__.py +0 -0
  24. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/cache.py +0 -0
  25. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/config.py +0 -0
  26. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/database.py +0 -0
  27. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/dependencies.py +0 -0
  28. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/dt.py +0 -0
  29. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/exceptions.py +0 -0
  30. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/health.py +0 -0
  31. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/logger.py +0 -0
  32. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/masking.py +0 -0
  33. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/pagination.py +0 -0
  34. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/providers/__init__.py +0 -0
  35. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/providers/email/base.py +0 -0
  36. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/providers/email/resend.py +0 -0
  37. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/response.py +0 -0
  38. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/security.py +0 -0
  39. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/__init__.py +0 -0
  40. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/context.py +0 -0
  41. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/model.py +0 -0
  42. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/pool.py +0 -0
  43. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/registry.py +0 -0
  44. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/router.py +0 -0
  45. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/schema.py +0 -0
  46. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/service.py +0 -0
  47. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi/tasks/worker.py +0 -0
  48. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi.egg-info/dependency_links.txt +0 -0
  49. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/src/dwyeapi.egg-info/top_level.txt +0 -0
  50. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_cache.py +0 -0
  51. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_config.py +0 -0
  52. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_database.py +0 -0
  53. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_dependencies.py +0 -0
  54. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_exceptions.py +0 -0
  55. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_health.py +0 -0
  56. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_logger.py +0 -0
  57. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_masking.py +0 -0
  58. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_pagination.py +0 -0
  59. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_response.py +0 -0
  60. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_security.py +0 -0
  61. {dwyeapi-0.8.2 → dwyeapi-0.9.0}/tests/test_tasks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dwyeapi
3
- Version: 0.8.2
3
+ Version: 0.9.0
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` / `providers.sms` pluggable 验证码 Provider 工厂 (mock / resend / aliyun)
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.8.2"
3
+ version = "0.9.0"
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", "alibabacloud_dm20151123>=1.0.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: Literal["mock", "resend", "aliyun"] = Field(
74
- default="mock",
75
- description='Email Provider 选择:"mock"(dev 限定,打印到日志)/ "resend" / "aliyun"',
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
- 实现类:MockEmailProvider(开发)、ResendEmailProvider(生产)、AliyunEmailProvider(生产备选)。
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.8.2
3
+ Version: 0.9.0
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