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.
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/PKG-INFO +1 -1
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/pyproject.toml +1 -1
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/__init__.py +1 -1
- dwyeapi-0.9.5/src/dwyeapi/dt.py +113 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/base.py +8 -2
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/factory.py +3 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/security.py +2 -2
- dwyeapi-0.9.5/src/dwyeapi/tasks/schema.py +42 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/PKG-INFO +1 -1
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/SOURCES.txt +1 -0
- dwyeapi-0.9.5/tests/test_dt.py +75 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_tasks.py +24 -0
- dwyeapi-0.9.3/src/dwyeapi/dt.py +0 -52
- dwyeapi-0.9.3/src/dwyeapi/tasks/schema.py +0 -31
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/README.md +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/setup.cfg +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/cache.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/config.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/database.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/dependencies.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/exceptions.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/health.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/logger.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/masking.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/pagination.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/__init__.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/__init__.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/config.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/protocol.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/providers/email/resend.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/response.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/__init__.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/context.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/model.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/pool.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/registry.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/router.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/service.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi/tasks/worker.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/dependency_links.txt +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/requires.txt +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/src/dwyeapi.egg-info/top_level.txt +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_cache.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_config.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_database.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_dependencies.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_exceptions.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_health.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_logger.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_masking.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_pagination.py +0 -0
- {dwyeapi-0.9.3 → dwyeapi-0.9.5}/tests/test_response.py +0 -0
- {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
|
+
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
|
|
@@ -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
|
-
"""
|
|
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
|
|
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
|
-
|
|
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
|
+
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
|
|
@@ -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(
|
dwyeapi-0.9.3/src/dwyeapi/dt.py
DELETED
|
@@ -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
|
|
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
|