dwyeapi 0.9.3__tar.gz → 0.9.5__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 (53) hide show
  1. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/PKG-INFO +1 -1
  2. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/pyproject.toml +1 -1
  3. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/__init__.py +1 -1
  4. dwyeapi-0.9.5/src/dwyeapi/dt.py +113 -0
  5. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/base.py +8 -2
  6. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/factory.py +3 -0
  7. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/security.py +2 -2
  8. dwyeapi-0.9.5/src/dwyeapi/tasks/schema.py +42 -0
  9. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/PKG-INFO +1 -1
  10. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/SOURCES.txt +1 -0
  11. dwyeapi-0.9.5/tests/test_dt.py +75 -0
  12. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_tasks.py +24 -0
  13. dwyeapi-0.9.3/src/dwyeapi/dt.py +0 -52
  14. dwyeapi-0.9.3/src/dwyeapi/tasks/schema.py +0 -31
  15. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/README.md +0 -0
  16. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/setup.cfg +0 -0
  17. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/cache.py +0 -0
  18. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/config.py +0 -0
  19. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/database.py +0 -0
  20. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/dependencies.py +0 -0
  21. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/exceptions.py +0 -0
  22. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/health.py +0 -0
  23. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/logger.py +0 -0
  24. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/masking.py +0 -0
  25. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/pagination.py +0 -0
  26. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/__init__.py +0 -0
  27. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/__init__.py +0 -0
  28. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/config.py +0 -0
  29. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/protocol.py +0 -0
  30. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/resend.py +0 -0
  31. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/response.py +0 -0
  32. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/__init__.py +0 -0
  33. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/context.py +0 -0
  34. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/model.py +0 -0
  35. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/pool.py +0 -0
  36. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/registry.py +0 -0
  37. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/router.py +0 -0
  38. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/service.py +0 -0
  39. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/worker.py +0 -0
  40. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/dependency_links.txt +0 -0
  41. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/requires.txt +0 -0
  42. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/top_level.txt +0 -0
  43. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_cache.py +0 -0
  44. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_config.py +0 -0
  45. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_database.py +0 -0
  46. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_dependencies.py +0 -0
  47. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_exceptions.py +0 -0
  48. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_health.py +0 -0
  49. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_logger.py +0 -0
  50. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_masking.py +0 -0
  51. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_pagination.py +0 -0
  52. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_response.py +0 -0
  53. {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dwyeapi
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: Lightweight FastAPI infrastructure — exceptions, database, config, security, cache, pagination, logger
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/danweiyuancircle/dwy-ai-experience
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dwyeapi"
3
- version = "0.9.3"
3
+ version = "0.9.5"
4
4
  description = "Lightweight FastAPI infrastructure — exceptions, database, config, security, cache, pagination, logger"
5
5
  requires-python = ">=3.11"
6
6
  license = {text = "MIT"}
@@ -32,4 +32,4 @@ __all__ = [
32
32
  "is_prod",
33
33
  "logger",
34
34
  ]
35
- __version__ = "0.9.1"
35
+ __version__ = "0.9.5"
@@ -0,0 +1,113 @@
1
+ """全局时间工具 — 所有日期 / 时长操作的唯一入口。
2
+
3
+ 业务时间统一 Asia/Shanghai, JWT 等协议场景提供 UTC 方法。
4
+ 禁止在业务代码中直接使用 ``datetime.now()`` / ``datetime.utcnow()`` /
5
+ ``from datetime import timedelta``,统一通过本模块访问。
6
+
7
+ Usage::
8
+
9
+ from dwyeapi import dt
10
+
11
+ dt.now() # naive datetime, 中国时间, 用于数据库
12
+ dt.now_str() # "2026-04-06 14:30:00"
13
+ dt.today() # date(2026, 4, 6)
14
+ dt.timestamp() # Unix 时间戳(秒)
15
+ dt.utc_now() # aware datetime, UTC, 用于 JWT
16
+
17
+ dt.timedelta(minutes=10) # 时长(透传 stdlib timedelta)
18
+ dt.after(days=1) # now + 偏移(naive 中国时间)
19
+ dt.before(hours=1) # now - 偏移
20
+ dt.utc_after(minutes=30) # utc_now + 偏移(aware UTC, JWT exp)
21
+ dt.utc_before(days=1) # utc_now - 偏移
22
+ """
23
+
24
+ from datetime import date, datetime
25
+ from datetime import timedelta as _Timedelta
26
+ from zoneinfo import ZoneInfo
27
+
28
+ TZ = ZoneInfo("Asia/Shanghai")
29
+ UTC = ZoneInfo("UTC")
30
+
31
+ # 公开 re-export:业务侧用 dt.timedelta(...),不再 from datetime import timedelta
32
+ timedelta = _Timedelta
33
+
34
+
35
+ # ── 业务时间(Asia/Shanghai) ──────────────────────────────────────────
36
+
37
+
38
+ def now() -> datetime:
39
+ """当前中国时间(naive, 用于数据库存储)。"""
40
+ return datetime.now(TZ).replace(tzinfo=None)
41
+
42
+
43
+ def now_str(fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
44
+ """格式化的当前中国时间字符串。"""
45
+ return datetime.now(TZ).strftime(fmt)
46
+
47
+
48
+ def today() -> date:
49
+ """当前中国日期。"""
50
+ return datetime.now(TZ).date()
51
+
52
+
53
+ def timestamp() -> float:
54
+ """当前 Unix 时间戳(秒)。"""
55
+ return datetime.now(TZ).timestamp()
56
+
57
+
58
+ def after(**kwargs: int | float) -> datetime:
59
+ """当前中国时间加上偏移,返回 naive datetime(写库 / 业务截止时间)。
60
+
61
+ Args:
62
+ **kwargs: 同 ``datetime.timedelta`` 关键字参数
63
+ (``days`` / ``seconds`` / ``microseconds`` / ``milliseconds`` /
64
+ ``minutes`` / ``hours`` / ``weeks``)。示例:``days=1, hours=2``。
65
+
66
+ Returns:
67
+ datetime: ``now() + timedelta(**kwargs)``,无时区信息。
68
+ """
69
+ return now() + _Timedelta(**kwargs)
70
+
71
+
72
+ def before(**kwargs: int | float) -> datetime:
73
+ """当前中国时间减去偏移,返回 naive datetime。
74
+
75
+ Args:
76
+ **kwargs: 同 ``after``。示例:``hours=1``。
77
+
78
+ Returns:
79
+ datetime: ``now() - timedelta(**kwargs)``,无时区信息。
80
+ """
81
+ return now() - _Timedelta(**kwargs)
82
+
83
+
84
+ # ── UTC(JWT 等协议场景) ──────────────────────────────────────────────
85
+
86
+
87
+ def utc_now() -> datetime:
88
+ """当前 UTC 时间(aware, 用于 JWT exp 等协议字段)。"""
89
+ return datetime.now(UTC)
90
+
91
+
92
+ def utc_after(**kwargs: int | float) -> datetime:
93
+ """当前 UTC 时间加上偏移,返回 aware datetime(JWT exp 等)。
94
+
95
+ Args:
96
+ **kwargs: 同 ``after``。示例:``minutes=30``。
97
+
98
+ Returns:
99
+ datetime: ``utc_now() + timedelta(**kwargs)``,tzinfo=UTC。
100
+ """
101
+ return utc_now() + _Timedelta(**kwargs)
102
+
103
+
104
+ def utc_before(**kwargs: int | float) -> datetime:
105
+ """当前 UTC 时间减去偏移,返回 aware datetime。
106
+
107
+ Args:
108
+ **kwargs: 同 ``after``。示例:``days=1``。
109
+
110
+ Returns:
111
+ datetime: ``utc_now() - timedelta(**kwargs)``,tzinfo=UTC。
112
+ """
113
+ return utc_now() - _Timedelta(**kwargs)
@@ -63,11 +63,17 @@ class EmailProviderBase(ABC):
63
63
  return "".join(secrets.choice("0123456789") for _ in range(self._length))
64
64
 
65
65
  async def send_code(self, target: str) -> bool:
66
- """生成验证码存 Redis 并调用 `_send()` 发送。"""
66
+ """生成验证码:先发送成功再写入 Redis。
67
+
68
+ 顺序约束:``_send`` 失败时不落库,避免用户未收到邮件却有可校验码。
69
+ 发送成功后写 Redis;若写库失败则返回 False(邮件已发出属极端边界,业务可重发覆盖)。
70
+ """
67
71
  code = self._generate_code()
72
+ if not await self._send(target, code):
73
+ return False
68
74
  redis = await self._get_redis()
69
75
  await redis.set(f"{CODE_KEY_PREFIX}{target}", code, ex=self._ttl)
70
- return await self._send(target, code)
76
+ return True
71
77
 
72
78
  async def verify_code(self, target: str, code: str) -> bool:
73
79
  """从 Redis 读取存储的验证码比对,成功则删除 key(一次性)。"""
@@ -64,6 +64,9 @@ def make_email_provider(settings: EmailSettings) -> EmailProvider:
64
64
 
65
65
  if not settings.resend.api_key:
66
66
  raise ValueError("EMAIL__RESEND__API_KEY 未配置")
67
+ # from_email 启用时必填:缺省会导致 resend 运行时发信失败,尽早在工厂拦截
68
+ if not settings.resend.from_email:
69
+ raise ValueError("EMAIL__RESEND__FROM_EMAIL 未配置")
67
70
  return ResendEmailProvider(
68
71
  api_key=settings.resend.api_key,
69
72
  from_email=settings.resend.from_email,
@@ -4,7 +4,6 @@
4
4
  所有函数保持无状态 — 密钥和算法由调用方以参数显式传入,方便在不同环境或密钥版本之间切换。
5
5
  """
6
6
 
7
- from datetime import timedelta
8
7
  from typing import Any
9
8
 
10
9
  import bcrypt
@@ -63,7 +62,8 @@ def create_token(
63
62
  编码后的 JWT 字符串。
64
63
  """
65
64
  to_encode = data.copy()
66
- expire = dt.utc_now() + timedelta(minutes=expires_minutes)
65
+ # JWT exp 必须用 UTC aware;时长与当前时刻统一走 dt
66
+ expire = dt.utc_after(minutes=expires_minutes)
67
67
  to_encode["exp"] = expire
68
68
  return jwt.encode(to_encode, secret, algorithm=algorithm)
69
69
 
@@ -0,0 +1,42 @@
1
+ """任务 API 请求/响应的 Pydantic schema。"""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+ from dwyeapi.tasks.model import TaskStatus
9
+
10
+ # 与 Task ORM 列 String(50) 对齐;params 键数/体积限制防超大 body
11
+ _TASK_TYPE_MAX_LEN = 50
12
+ _PARAMS_MAX_KEYS = 50
13
+
14
+
15
+ class TaskCreate(BaseModel):
16
+ """提交新任务的请求体。
17
+
18
+ 约束与 ORM / 输入限长规则对齐:``task_type`` 最长 50;``params`` 最多 50 个键。
19
+ """
20
+
21
+ task_type: str = Field(min_length=1, max_length=_TASK_TYPE_MAX_LEN, description="已注册的任务类型名")
22
+ params: dict[str, Any] = Field(
23
+ default_factory=dict,
24
+ description="任务参数;键数量上限 50,避免超大 JSON 入库",
25
+ max_length=_PARAMS_MAX_KEYS,
26
+ )
27
+
28
+
29
+ class TaskResponse(BaseModel):
30
+ """任务详情响应体,覆盖状态、进度、日志与结果等字段。"""
31
+
32
+ id: str
33
+ task_type: str
34
+ status: TaskStatus
35
+ params: dict[str, Any]
36
+ result: dict[str, Any] | None
37
+ progress: int
38
+ logs: str
39
+ created_at: datetime
40
+ updated_at: datetime
41
+
42
+ model_config = ConfigDict(from_attributes=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dwyeapi
3
- Version: 0.9.3
3
+ Version: 0.9.5
4
4
  Summary: Lightweight FastAPI infrastructure — exceptions, database, config, security, cache, pagination, logger
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/danweiyuancircle/dwy-ai-experience
@@ -38,6 +38,7 @@ tests/test_cache.py
38
38
  tests/test_config.py
39
39
  tests/test_database.py
40
40
  tests/test_dependencies.py
41
+ tests/test_dt.py
41
42
  tests/test_exceptions.py
42
43
  tests/test_health.py
43
44
  tests/test_logger.py
@@ -0,0 +1,75 @@
1
+ """Tests for dwyeapi.dt — 时钟与时长统一入口。"""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+
5
+ from dwyeapi import dt
6
+
7
+
8
+ class TestClock:
9
+ def test_now_is_naive(self):
10
+ assert dt.now().tzinfo is None
11
+
12
+ def test_utc_now_is_aware_utc(self):
13
+ now = dt.utc_now()
14
+ assert now.tzinfo is not None
15
+ # 与 UTC 偏移为 0
16
+ assert now.utcoffset() == timedelta(0)
17
+
18
+ def test_today_is_date(self):
19
+ assert dt.today() == dt.now().date()
20
+
21
+ def test_timestamp_is_float(self):
22
+ ts = dt.timestamp()
23
+ assert isinstance(ts, float)
24
+ assert ts > 0
25
+
26
+
27
+ class TestTimedelta:
28
+ def test_timedelta_reexport(self):
29
+ delta = dt.timedelta(minutes=10)
30
+ assert isinstance(delta, timedelta)
31
+ assert delta == timedelta(minutes=10)
32
+
33
+ def test_timedelta_days_and_hours(self):
34
+ assert dt.timedelta(days=1, hours=2) == timedelta(days=1, hours=2)
35
+
36
+
37
+ class TestAfterBefore:
38
+ def test_after_is_naive_and_future(self):
39
+ base = dt.now()
40
+ future = dt.after(minutes=5)
41
+ assert future.tzinfo is None
42
+ assert future > base
43
+ # 约 5 分钟,允许毫秒级误差
44
+ assert abs((future - base).total_seconds() - 300) < 1
45
+
46
+ def test_before_is_naive_and_past(self):
47
+ base = dt.now()
48
+ past = dt.before(hours=1)
49
+ assert past.tzinfo is None
50
+ assert past < base
51
+ assert abs((base - past).total_seconds() - 3600) < 1
52
+
53
+ def test_utc_after_is_aware_and_future(self):
54
+ base = dt.utc_now()
55
+ future = dt.utc_after(minutes=30)
56
+ assert future.tzinfo is not None
57
+ assert future.utcoffset() == timedelta(0)
58
+ assert future > base
59
+ assert abs((future - base).total_seconds() - 1800) < 1
60
+
61
+ def test_utc_before_is_aware_and_past(self):
62
+ base = dt.utc_now()
63
+ past = dt.utc_before(days=1)
64
+ assert past.tzinfo is not None
65
+ assert past < base
66
+ assert abs((base - past).total_seconds() - 86400) < 1
67
+
68
+ def test_utc_after_compatible_with_jwt_exp(self):
69
+ """utc_after 结果可直接写入 JWT exp(aware UTC)。"""
70
+ exp = dt.utc_after(minutes=10)
71
+ # python-jose 接受 datetime;这里只校验形态
72
+ assert isinstance(exp, datetime)
73
+ assert exp.tzinfo is not None
74
+ # 与 timezone.utc 兼容
75
+ assert exp.astimezone(timezone.utc).tzinfo is not None
@@ -127,6 +127,30 @@ class TestSchemas:
127
127
  assert tc.task_type == "export"
128
128
  assert tc.params == {"format": "csv"}
129
129
 
130
+ def test_task_create_rejects_empty_task_type(self):
131
+ """task_type 最短 1 字符。"""
132
+ import pytest
133
+ from pydantic import ValidationError
134
+
135
+ with pytest.raises(ValidationError):
136
+ TaskCreate(task_type="", params={})
137
+
138
+ def test_task_create_rejects_long_task_type(self):
139
+ """task_type 最长 50,对齐 ORM String(50)。"""
140
+ import pytest
141
+ from pydantic import ValidationError
142
+
143
+ with pytest.raises(ValidationError):
144
+ TaskCreate(task_type="x" * 51, params={})
145
+
146
+ def test_task_create_rejects_too_many_params_keys(self):
147
+ """params 键数上限 50。"""
148
+ import pytest
149
+ from pydantic import ValidationError
150
+
151
+ with pytest.raises(ValidationError):
152
+ TaskCreate(task_type="export", params={f"k{i}": i for i in range(51)})
153
+
130
154
  async def test_task_response_from_attributes(self, session):
131
155
  """TaskResponse should be constructible from a persisted Task ORM object."""
132
156
  task = Task(
@@ -1,52 +0,0 @@
1
- """全局时间工具 — 所有日期操作的唯一入口。
2
-
3
- 业务时间统一 Asia/Shanghai, JWT 等协议场景提供 UTC 方法。
4
- 禁止在业务代码中直接使用 datetime.now(), 统一通过本模块访问。
5
-
6
- Usage::
7
-
8
- from dwyeapi import dt
9
-
10
- dt.now() # naive datetime, 中国时间, 用于数据库
11
- dt.now_str() # "2026-04-06 14:30:00"
12
- dt.today() # date(2026, 4, 6)
13
- dt.timestamp() # Unix 时间戳(秒)
14
- dt.utc_now() # aware datetime, UTC, 用于 JWT
15
- """
16
-
17
- from datetime import date, datetime
18
- from zoneinfo import ZoneInfo
19
-
20
- TZ = ZoneInfo("Asia/Shanghai")
21
- UTC = ZoneInfo("UTC")
22
-
23
-
24
- # ── 业务时间(Asia/Shanghai) ──────────────────────────────────────────
25
-
26
-
27
- def now() -> datetime:
28
- """当前中国时间(naive, 用于数据库存储)。"""
29
- return datetime.now(TZ).replace(tzinfo=None)
30
-
31
-
32
- def now_str(fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
33
- """格式化的当前中国时间字符串。"""
34
- return datetime.now(TZ).strftime(fmt)
35
-
36
-
37
- def today() -> date:
38
- """当前中国日期。"""
39
- return datetime.now(TZ).date()
40
-
41
-
42
- def timestamp() -> float:
43
- """当前 Unix 时间戳(秒)。"""
44
- return datetime.now(TZ).timestamp()
45
-
46
-
47
- # ── UTC(JWT 等协议场景) ──────────────────────────────────────────────
48
-
49
-
50
- def utc_now() -> datetime:
51
- """当前 UTC 时间(aware, 用于 JWT exp 等协议字段)。"""
52
- return datetime.now(UTC)
@@ -1,31 +0,0 @@
1
- """任务 API 请求/响应的 Pydantic schema。"""
2
-
3
- from datetime import datetime
4
- from typing import Any
5
-
6
- from pydantic import BaseModel, ConfigDict
7
-
8
- from dwyeapi.tasks.model import TaskStatus
9
-
10
-
11
- class TaskCreate(BaseModel):
12
- """提交新任务的请求体。"""
13
-
14
- task_type: str
15
- params: dict[str, Any]
16
-
17
-
18
- class TaskResponse(BaseModel):
19
- """任务详情响应体,覆盖状态、进度、日志与结果等字段。"""
20
-
21
- id: str
22
- task_type: str
23
- status: TaskStatus
24
- params: dict[str, Any]
25
- result: dict[str, Any] | None
26
- progress: int
27
- logs: str
28
- created_at: datetime
29
- updated_at: datetime
30
-
31
- model_config = ConfigDict(from_attributes=True)
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