tiebameow 0.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from functools import cached_property
5
+ from types import UnionType
6
+ from typing import Any, Literal, Self, get_args, get_origin
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ from ..schemas.fragments import FragAtModel, FragImageModel, Fragment, TypeFragText
11
+
12
+
13
+ class BaseDTO(BaseModel):
14
+ """
15
+ 基础 DTO 类。
16
+
17
+ 在保证类型严格的同时,允许从不完整的数据源构造 DTO 对象。
18
+ 缺失的字段将自动填充为该类型的零值。
19
+ """
20
+
21
+ model_config = ConfigDict(from_attributes=True)
22
+
23
+ @classmethod
24
+ def from_incomplete_data(cls, data: dict[str, Any] | BaseModel | None = None) -> Self:
25
+ """
26
+ 递归补全不完整的数据源缺失的字段并返回 DTO 实例。
27
+
28
+ Args:
29
+ data: 不完整的数据源,可以是字典或 Pydantic 模型实例。
30
+ Returns:
31
+ 补全后的 DTO 实例。
32
+ """
33
+ if data is None:
34
+ data = {}
35
+ if isinstance(data, BaseModel):
36
+ data = data.model_dump()
37
+
38
+ input_payload = data.copy()
39
+
40
+ for field_name, field_info in cls.model_fields.items():
41
+ field_type = field_info.annotation
42
+
43
+ if field_name in input_payload:
44
+ curr_value = input_payload[field_name]
45
+
46
+ if curr_value is None:
47
+ zero_val = cls._get_zero_value(field_type)
48
+ if zero_val is not None:
49
+ input_payload[field_name] = zero_val
50
+ curr_value = zero_val
51
+
52
+ # 特殊处理:如果字段是 Pydantic 模型,但传入的是 dict,需要递归补全
53
+ # 防止嵌套对象内部缺字段
54
+ if isinstance(curr_value, dict) and isinstance(field_type, type) and issubclass(field_type, BaseModel):
55
+ if issubclass(field_type, BaseDTO):
56
+ input_payload[field_name] = field_type.from_incomplete_data(curr_value)
57
+ # 处理没有继承 BaseDTO 的普通 Pydantic 模型
58
+ else:
59
+ zero_obj = cls._get_zero_value(field_type)
60
+ # 用传入的 value 覆盖 zero_obj
61
+ if isinstance(zero_obj, BaseModel):
62
+ merged_data = zero_obj.model_dump()
63
+ merged_data.update(curr_value)
64
+ input_payload[field_name] = field_type.model_validate(merged_data)
65
+
66
+ else:
67
+ input_payload[field_name] = cls._get_zero_value(field_type)
68
+
69
+ return cls.model_validate(input_payload)
70
+
71
+ @classmethod
72
+ def _get_zero_value(cls, field_type: Any) -> Any:
73
+ """根据类型注解生成对应的零值。"""
74
+ # 处理 ForwardRef 或字符串类型的注解
75
+ type_str = None
76
+ if hasattr(field_type, "__forward_arg__"):
77
+ type_str = field_type.__forward_arg__
78
+ elif isinstance(field_type, str):
79
+ type_str = field_type
80
+
81
+ if type_str:
82
+ # 简单的字符串匹配,处理常见的容器类型
83
+ clean_str = type_str.strip()
84
+ if clean_str.startswith("list[") or clean_str == "list":
85
+ return []
86
+ if clean_str.startswith("dict[") or clean_str == "dict":
87
+ return {}
88
+ if clean_str.startswith("set[") or clean_str == "set":
89
+ return set()
90
+
91
+ origin = get_origin(field_type)
92
+ args = get_args(field_type)
93
+
94
+ if origin is Literal:
95
+ return args[0]
96
+ if origin is list:
97
+ return []
98
+ if origin is dict:
99
+ return {}
100
+ if origin is set:
101
+ return set()
102
+ if field_type is int:
103
+ return 0
104
+ if field_type is float:
105
+ return 0.0
106
+ if field_type is str:
107
+ return ""
108
+ if field_type is bool:
109
+ return False
110
+ if field_type is datetime:
111
+ return datetime.fromtimestamp(0)
112
+
113
+ if isinstance(field_type, type) and issubclass(field_type, BaseModel):
114
+ if issubclass(field_type, BaseDTO):
115
+ return field_type.from_incomplete_data({})
116
+ else:
117
+ dummy_data = {name: cls._get_zero_value(f.annotation) for name, f in field_type.model_fields.items()}
118
+ return field_type.model_validate(dummy_data)
119
+
120
+ # 预留复杂类型的处理接口
121
+ if origin is UnionType and type(None) in args:
122
+ pass
123
+
124
+ return None
125
+
126
+
127
+ class BaseForumDTO(BaseDTO):
128
+ fid: int
129
+ fname: str
130
+
131
+
132
+ class BaseUserDTO(BaseDTO):
133
+ user_id: int
134
+ portrait: str
135
+ user_name: str
136
+ nick_name_new: str
137
+
138
+ @property
139
+ def nick_name(self) -> str:
140
+ return self.nick_name_new
141
+
142
+ @property
143
+ def show_name(self) -> str:
144
+ return self.nick_name_new or self.user_name
145
+
146
+
147
+ class ThreadUserDTO(BaseUserDTO):
148
+ level: int
149
+ glevel: int
150
+
151
+ gender: Literal["UNKNOWN", "MALE", "FEMALE"]
152
+ icons: list[str]
153
+
154
+ is_bawu: bool
155
+ is_vip: bool
156
+ is_god: bool
157
+
158
+ priv_like: Literal["PUBLIC", "FRIEND", "HIDE"]
159
+ priv_reply: Literal["ALL", "FANS", "FOLLOW"]
160
+
161
+
162
+ class PostUserDTO(BaseUserDTO):
163
+ level: int
164
+ glevel: int
165
+
166
+ gender: Literal["UNKNOWN", "MALE", "FEMALE"]
167
+ ip: str
168
+ icons: list[str]
169
+
170
+ is_bawu: bool
171
+ is_vip: bool
172
+ is_god: bool
173
+
174
+ priv_like: Literal["PUBLIC", "FRIEND", "HIDE"]
175
+ priv_reply: Literal["ALL", "FANS", "FOLLOW"]
176
+
177
+
178
+ class CommentUserDTO(BaseUserDTO):
179
+ level: int
180
+
181
+ gender: Literal["UNKNOWN", "MALE", "FEMALE"]
182
+ icons: list[str]
183
+
184
+ is_bawu: bool
185
+ is_vip: bool
186
+ is_god: bool
187
+
188
+ priv_like: Literal["PUBLIC", "FRIEND", "HIDE"]
189
+ priv_reply: Literal["ALL", "FANS", "FOLLOW"]
190
+
191
+
192
+ class UserInfoDTO(BaseUserDTO):
193
+ nick_name_old: str
194
+ tieba_uid: int
195
+
196
+ glevel: int
197
+ gender: Literal["UNKNOWN", "MALE", "FEMALE"]
198
+ age: float
199
+ post_num: int
200
+ agree_num: int
201
+ fan_num: int
202
+ follow_num: int
203
+ forum_num: int
204
+ sign: str
205
+ ip: str
206
+ icons: list[str]
207
+
208
+ is_vip: bool
209
+ is_god: bool
210
+ is_blocked: bool
211
+
212
+ priv_like: Literal["PUBLIC", "FRIEND", "HIDE"]
213
+ priv_reply: Literal["ALL", "FANS", "FOLLOW"]
214
+
215
+
216
+ class BaseThreadDTO(BaseDTO):
217
+ pid: int
218
+ tid: int
219
+ fid: int
220
+ fname: str
221
+
222
+ author_id: int
223
+
224
+ title: str
225
+ contents: list[Fragment] = Field(default_factory=list)
226
+
227
+ @cached_property
228
+ def text(self) -> str:
229
+ text = "".join(frag.text for frag in self.contents if isinstance(frag, TypeFragText))
230
+ return text
231
+
232
+ @cached_property
233
+ def full_text(self) -> str:
234
+ text = "".join(frag.text for frag in self.contents if isinstance(frag, TypeFragText))
235
+ return self.title + "\n" + text
236
+
237
+ @cached_property
238
+ def images(self) -> list[FragImageModel]:
239
+ images = [frag for frag in self.contents if isinstance(frag, FragImageModel)]
240
+ return images
241
+
242
+ @cached_property
243
+ def ats(self) -> list[int]:
244
+ ats = [frag.user_id for frag in self.contents if isinstance(frag, FragAtModel)]
245
+ return ats
246
+
247
+
248
+ class ThreadpDTO(BaseThreadDTO):
249
+ author: ThreadUserDTO
250
+
251
+ is_share: bool
252
+
253
+ agree_num: int
254
+ disagree_num: int
255
+ reply_num: int
256
+ view_num: int
257
+ share_num: int
258
+ create_time: datetime
259
+
260
+ thread_type: int
261
+ share_origin: BaseThreadDTO
262
+
263
+
264
+ class ThreadDTO(BaseThreadDTO):
265
+ author: ThreadUserDTO
266
+
267
+ is_good: bool
268
+ is_top: bool
269
+ is_share: bool
270
+ is_hide: bool
271
+ is_livepost: bool
272
+ is_help: bool
273
+
274
+ agree_num: int
275
+ disagree_num: int
276
+ reply_num: int
277
+ view_num: int
278
+ share_num: int
279
+ create_time: datetime
280
+ last_time: datetime
281
+
282
+ thread_type: int
283
+ tab_id: int
284
+ share_origin: BaseThreadDTO
285
+
286
+
287
+ class PostDTO(BaseDTO):
288
+ pid: int
289
+ tid: int
290
+ fid: int
291
+ fname: str
292
+
293
+ author_id: int
294
+ author: PostUserDTO
295
+
296
+ contents: list[Fragment] = Field(default_factory=list)
297
+ sign: str
298
+ comments: list[CommentDTO] = Field(default_factory=list)
299
+
300
+ is_aimeme: bool
301
+ is_thread_author: bool
302
+
303
+ agree_num: int
304
+ disagree_num: int
305
+ reply_num: int
306
+ create_time: datetime
307
+
308
+ floor: int
309
+
310
+ @cached_property
311
+ def text(self) -> str:
312
+ text = "".join(frag.text for frag in self.contents if isinstance(frag, TypeFragText))
313
+ return text
314
+
315
+ @cached_property
316
+ def full_text(self) -> str:
317
+ return self.text
318
+
319
+ @cached_property
320
+ def images(self) -> list[FragImageModel]:
321
+ images = [frag for frag in self.contents if isinstance(frag, FragImageModel)]
322
+ return images
323
+
324
+ @cached_property
325
+ def ats(self) -> list[int]:
326
+ ats = [frag.user_id for frag in self.contents if isinstance(frag, FragAtModel)]
327
+ return ats
328
+
329
+
330
+ class CommentDTO(BaseDTO):
331
+ cid: int
332
+ pid: int
333
+ tid: int
334
+ fid: int
335
+ fname: str
336
+
337
+ author_id: int
338
+ author: CommentUserDTO
339
+
340
+ contents: list[Fragment] = Field(default_factory=list)
341
+ reply_to_id: int
342
+
343
+ is_thread_author: bool
344
+
345
+ agree_num: int
346
+ disagree_num: int
347
+ create_time: datetime
348
+
349
+ floor: int
350
+
351
+ @cached_property
352
+ def text(self) -> str:
353
+ text = "".join(frag.text for frag in self.contents if isinstance(frag, TypeFragText))
354
+ return text
355
+
356
+ @cached_property
357
+ def full_text(self) -> str:
358
+ return self.text
359
+
360
+ @cached_property
361
+ def ats(self) -> list[int]:
362
+ ats = [frag.user_id for frag in self.contents if isinstance(frag, FragAtModel)]
363
+ return ats
364
+
365
+
366
+ class PageInfoDTO(BaseDTO):
367
+ page_size: int = 0
368
+ current_page: int = 0
369
+ total_page: int = 0
370
+ total_count: int = 0
371
+
372
+ has_more: bool = False
373
+ has_prev: bool = False
374
+
375
+
376
+ class ThreadsDTO(BaseDTO):
377
+ objs: list[ThreadDTO] = Field(default_factory=list)
378
+ page: PageInfoDTO
379
+ forum: BaseForumDTO
380
+
381
+
382
+ class PostsDTO(BaseDTO):
383
+ objs: list[PostDTO] = Field(default_factory=list)
384
+ page: PageInfoDTO
385
+ forum: BaseForumDTO
386
+
387
+
388
+ class CommentsDTO(BaseDTO):
389
+ objs: list[CommentDTO] = Field(default_factory=list)
390
+ page: PageInfoDTO
391
+ forum: BaseForumDTO