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.
- tiebameow/__init__.py +0 -0
- tiebameow/client/__init__.py +4 -0
- tiebameow/client/http_client.py +103 -0
- tiebameow/client/tieba_client.py +517 -0
- tiebameow/models/__init__.py +0 -0
- tiebameow/models/dto.py +391 -0
- tiebameow/models/orm.py +572 -0
- tiebameow/parser/__init__.py +45 -0
- tiebameow/parser/parser.py +362 -0
- tiebameow/parser/rule_parser.py +990 -0
- tiebameow/py.typed +0 -0
- tiebameow/renderer/__init__.py +5 -0
- tiebameow/renderer/config.py +18 -0
- tiebameow/renderer/playwright_core.py +148 -0
- tiebameow/renderer/renderer.py +508 -0
- tiebameow/renderer/static/fonts/NotoSansSC-Regular.woff2 +0 -0
- tiebameow/renderer/style.py +32 -0
- tiebameow/renderer/templates/base.html +270 -0
- tiebameow/renderer/templates/macros.html +100 -0
- tiebameow/renderer/templates/text.html +99 -0
- tiebameow/renderer/templates/text_simple.html +79 -0
- tiebameow/renderer/templates/thread.html +8 -0
- tiebameow/renderer/templates/thread_detail.html +18 -0
- tiebameow/renderer/templates/thread_info.html +35 -0
- tiebameow/schemas/__init__.py +0 -0
- tiebameow/schemas/fragments.py +188 -0
- tiebameow/schemas/rules.py +247 -0
- tiebameow/serializer/__init__.py +15 -0
- tiebameow/serializer/serializer.py +115 -0
- tiebameow/utils/__init__.py +0 -0
- tiebameow/utils/logger.py +129 -0
- tiebameow/utils/time_utils.py +15 -0
- tiebameow-0.2.8.dist-info/METADATA +142 -0
- tiebameow-0.2.8.dist-info/RECORD +36 -0
- tiebameow-0.2.8.dist-info/WHEEL +4 -0
- tiebameow-0.2.8.dist-info/licenses/LICENSE +21 -0
tiebameow/models/orm.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""数据模型定义模块。
|
|
2
|
+
|
|
3
|
+
该模块定义了所有与贴吧数据相关的SQLAlchemy ORM模型和Pydantic验证模型,
|
|
4
|
+
包括论坛、用户、主题贴、回复、楼中楼等实体,以及各种内容片段的数据模型。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import datetime # noqa: TC003
|
|
10
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
11
|
+
|
|
12
|
+
from pydantic import TypeAdapter, ValidationError
|
|
13
|
+
from sqlalchemy import BIGINT, JSON, Boolean, DateTime, Enum, Index, Integer, String, Text, UniqueConstraint
|
|
14
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
15
|
+
from sqlalchemy.ext.mutable import MutableList
|
|
16
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, foreign, mapped_column, relationship
|
|
17
|
+
from sqlalchemy.types import TypeDecorator, TypeEngine
|
|
18
|
+
|
|
19
|
+
from ..schemas.fragments import FRAG_MAP, Fragment, FragUnknownModel
|
|
20
|
+
from ..schemas.rules import Actions, ReviewRule, RuleNode, TargetType
|
|
21
|
+
from ..utils.time_utils import now_with_tz
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
import aiotieba.typing as aiotieba
|
|
27
|
+
from sqlalchemy.engine.interfaces import Dialect
|
|
28
|
+
|
|
29
|
+
from .dto import BaseUserDTO, CommentDTO, PostDTO, ThreadDTO
|
|
30
|
+
|
|
31
|
+
type AiotiebaType = aiotieba.Thread | aiotieba.Post | aiotieba.Comment
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Base",
|
|
36
|
+
"Forum",
|
|
37
|
+
"User",
|
|
38
|
+
"Thread",
|
|
39
|
+
"Post",
|
|
40
|
+
"Comment",
|
|
41
|
+
"Fragment",
|
|
42
|
+
"RuleBase",
|
|
43
|
+
"ReviewRules",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Base(DeclarativeBase):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FragmentListType(TypeDecorator[list[Fragment]]):
|
|
52
|
+
"""自动处理Fragment模型列表的JSON序列化与反序列化。
|
|
53
|
+
|
|
54
|
+
自动适配不同数据库的JSON类型。
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
impl = JSON
|
|
58
|
+
cache_ok = True
|
|
59
|
+
|
|
60
|
+
def __init__(self, fallback: Callable[[], Fragment] | None = None, *args: object, **kwargs: object):
|
|
61
|
+
super().__init__(*args, **kwargs)
|
|
62
|
+
self.adapter: TypeAdapter[Fragment] = TypeAdapter(Fragment)
|
|
63
|
+
self.fallback = fallback
|
|
64
|
+
|
|
65
|
+
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]:
|
|
66
|
+
if dialect.name == "postgresql":
|
|
67
|
+
return dialect.type_descriptor(JSONB())
|
|
68
|
+
return dialect.type_descriptor(JSON())
|
|
69
|
+
|
|
70
|
+
def process_bind_param(self, value: list[Fragment] | None, dialect: Dialect) -> list[dict[str, Any]] | None:
|
|
71
|
+
if value is None:
|
|
72
|
+
return None
|
|
73
|
+
return [self.adapter.dump_python(item, mode="json") for item in value]
|
|
74
|
+
|
|
75
|
+
def process_result_value(self, value: list[dict[str, Any]] | None, dialect: Dialect) -> list[Fragment] | None:
|
|
76
|
+
if value is None:
|
|
77
|
+
return None
|
|
78
|
+
return [self._validate(item) for item in value]
|
|
79
|
+
|
|
80
|
+
def _validate(self, item: dict[str, Any]) -> Fragment:
|
|
81
|
+
if "type" in item:
|
|
82
|
+
if model_cls := FRAG_MAP.get(item["type"]):
|
|
83
|
+
return model_cls.model_construct(**item)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return self.adapter.validate_python(item)
|
|
87
|
+
except ValidationError:
|
|
88
|
+
if self.fallback:
|
|
89
|
+
return self.fallback()
|
|
90
|
+
raise
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RuleNodeType(TypeDecorator[RuleNode]):
|
|
94
|
+
"""自动处理RuleNode模型的JSON序列化与反序列化。"""
|
|
95
|
+
|
|
96
|
+
impl = JSON
|
|
97
|
+
cache_ok = True
|
|
98
|
+
|
|
99
|
+
def __init__(self, *args: object, **kwargs: object):
|
|
100
|
+
super().__init__(*args, **kwargs)
|
|
101
|
+
self.adapter: TypeAdapter[RuleNode] = TypeAdapter(RuleNode)
|
|
102
|
+
|
|
103
|
+
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]:
|
|
104
|
+
if dialect.name == "postgresql":
|
|
105
|
+
return dialect.type_descriptor(JSONB())
|
|
106
|
+
return dialect.type_descriptor(JSON())
|
|
107
|
+
|
|
108
|
+
def process_bind_param(self, value: RuleNode | None, dialect: Dialect) -> dict[str, Any] | None:
|
|
109
|
+
if value is None:
|
|
110
|
+
return None
|
|
111
|
+
return cast("dict[str, Any]", self.adapter.dump_python(value, mode="json"))
|
|
112
|
+
|
|
113
|
+
def process_result_value(self, value: dict[str, Any] | None, dialect: Dialect) -> RuleNode | None:
|
|
114
|
+
if value is None:
|
|
115
|
+
return None
|
|
116
|
+
return self.adapter.validate_python(value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ActionsType(TypeDecorator[Actions]):
|
|
120
|
+
"""自动处理Actions模型的JSON序列化与反序列化。"""
|
|
121
|
+
|
|
122
|
+
impl = JSON
|
|
123
|
+
cache_ok = True
|
|
124
|
+
|
|
125
|
+
def __init__(self, *args: object, **kwargs: object):
|
|
126
|
+
super().__init__(*args, **kwargs)
|
|
127
|
+
self.adapter: TypeAdapter[Actions] = TypeAdapter(Actions)
|
|
128
|
+
|
|
129
|
+
def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]:
|
|
130
|
+
if dialect.name == "postgresql":
|
|
131
|
+
return dialect.type_descriptor(JSONB())
|
|
132
|
+
return dialect.type_descriptor(JSON())
|
|
133
|
+
|
|
134
|
+
def process_bind_param(self, value: Actions | None, dialect: Dialect) -> dict[str, Any] | None:
|
|
135
|
+
if value is None:
|
|
136
|
+
return None
|
|
137
|
+
return cast("dict[str, Any]", self.adapter.dump_python(value, mode="json"))
|
|
138
|
+
|
|
139
|
+
def process_result_value(self, value: dict[str, Any] | None, dialect: Dialect) -> Actions | None:
|
|
140
|
+
if value is None:
|
|
141
|
+
return None
|
|
142
|
+
return self.adapter.validate_python(value)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class MixinBase(Base):
|
|
146
|
+
"""为SQLAlchemy模型提供通用方法的混入类。"""
|
|
147
|
+
|
|
148
|
+
__abstract__ = True
|
|
149
|
+
|
|
150
|
+
def to_dict(self) -> dict[str, Any]:
|
|
151
|
+
"""将模型实例的列数据转换为字典。
|
|
152
|
+
|
|
153
|
+
此方法包含直接映射到数据库表的列,用于批量插入操作。
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
dict: 包含模型列名和对应值的字典。
|
|
157
|
+
"""
|
|
158
|
+
result = {}
|
|
159
|
+
for c in self.__table__.columns:
|
|
160
|
+
value = getattr(self, c.name)
|
|
161
|
+
result[c.name] = value
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Forum(MixinBase):
|
|
166
|
+
"""贴吧信息数据模型。
|
|
167
|
+
|
|
168
|
+
Attributes:
|
|
169
|
+
fid: 论坛ID,主键。
|
|
170
|
+
fname: 论坛名称,建立索引用于快速查询。
|
|
171
|
+
threads: 该论坛下的所有帖子,与Thread模型的反向关系。
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
__tablename__ = "forum"
|
|
175
|
+
|
|
176
|
+
fid: Mapped[int] = mapped_column(BIGINT, primary_key=True)
|
|
177
|
+
fname: Mapped[str] = mapped_column(String(255), index=True)
|
|
178
|
+
|
|
179
|
+
threads: Mapped[list[Thread]] = relationship(
|
|
180
|
+
"Thread",
|
|
181
|
+
back_populates="forum",
|
|
182
|
+
primaryjoin=lambda: Forum.fid == foreign(Thread.fid),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class User(MixinBase):
|
|
187
|
+
"""用户数据模型。
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
user_id: 用户user_id,主键。
|
|
191
|
+
portrait: 用户portrait。
|
|
192
|
+
user_name: 用户名。
|
|
193
|
+
nick_name: 用户昵称。
|
|
194
|
+
threads: 该用户发布的所有帖子,与Thread模型的反向关系。
|
|
195
|
+
posts: 该用户发布的所有回复,与Post模型的反向关系。
|
|
196
|
+
comments: 该用户发布的所有评论,与Comment模型的反向关系。
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
__tablename__ = "user"
|
|
200
|
+
|
|
201
|
+
user_id: Mapped[int] = mapped_column(BIGINT, primary_key=True)
|
|
202
|
+
portrait: Mapped[str] = mapped_column(String(255), nullable=True, index=True)
|
|
203
|
+
user_name: Mapped[str] = mapped_column(String(255), nullable=True, index=True)
|
|
204
|
+
nick_name: Mapped[str] = mapped_column(String(255), nullable=True, index=True)
|
|
205
|
+
|
|
206
|
+
threads: Mapped[list[Thread]] = relationship(
|
|
207
|
+
"Thread",
|
|
208
|
+
back_populates="author",
|
|
209
|
+
primaryjoin=lambda: User.user_id == foreign(Thread.author_id),
|
|
210
|
+
)
|
|
211
|
+
posts: Mapped[list[Post]] = relationship(
|
|
212
|
+
"Post",
|
|
213
|
+
back_populates="author",
|
|
214
|
+
primaryjoin=lambda: User.user_id == foreign(Post.author_id),
|
|
215
|
+
)
|
|
216
|
+
comments: Mapped[list[Comment]] = relationship(
|
|
217
|
+
"Comment",
|
|
218
|
+
back_populates="author",
|
|
219
|
+
primaryjoin=lambda: User.user_id == foreign(Comment.author_id),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def from_dto(cls, dto: BaseUserDTO) -> User:
|
|
224
|
+
"""从UserDTO对象创建User模型实例。
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
dto: UserDTO对象。
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
User: 转换后的User模型实例。
|
|
231
|
+
"""
|
|
232
|
+
return cls(
|
|
233
|
+
user_id=dto.user_id,
|
|
234
|
+
portrait=dto.portrait,
|
|
235
|
+
user_name=dto.user_name,
|
|
236
|
+
nick_name=dto.nick_name,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class Thread(MixinBase):
|
|
241
|
+
"""主题贴数据模型。
|
|
242
|
+
|
|
243
|
+
Attributes:
|
|
244
|
+
tid: 主题贴tid,与create_time组成复合主键。
|
|
245
|
+
create_time: 主题贴创建时间,带时区信息,与tid组成复合主键。
|
|
246
|
+
title: 主题贴标题内容。
|
|
247
|
+
text: 主题贴的纯文本内容。
|
|
248
|
+
contents: 正文内容碎片列表,以JSONB格式存储。
|
|
249
|
+
last_time: 最后回复时间,带时区信息。
|
|
250
|
+
reply_num: 回复数。
|
|
251
|
+
author_level: 作者在主题贴所在吧的等级。
|
|
252
|
+
scrape_time: 数据抓取时间。
|
|
253
|
+
fid: 所属贴吧fid,外键关联到Forum表。
|
|
254
|
+
author_id: 作者user_id,外键关联到User表。
|
|
255
|
+
forum: 所属贴吧对象,与Forum模型的关系。
|
|
256
|
+
author: 作者用户对象,与User模型的关系。
|
|
257
|
+
posts: 该贴子下的所有回复,与Post模型的反向关系。
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
__tablename__ = "thread"
|
|
261
|
+
__table_args__ = (
|
|
262
|
+
Index("idx_thread_forum_ctime", "fid", "create_time"),
|
|
263
|
+
Index("idx_thread_forum_ltime", "fid", "last_time"),
|
|
264
|
+
Index("idx_thread_author_time", "author_id", "create_time"),
|
|
265
|
+
Index("idx_thread_author_forum_time", "author_id", "fid", "create_time"),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
tid: Mapped[int] = mapped_column(BIGINT, primary_key=True)
|
|
269
|
+
create_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True)
|
|
270
|
+
title: Mapped[str] = mapped_column(String(255))
|
|
271
|
+
text: Mapped[str] = mapped_column(Text)
|
|
272
|
+
contents: Mapped[list[Fragment] | None] = mapped_column(
|
|
273
|
+
MutableList.as_mutable(FragmentListType(fallback=FragUnknownModel)), nullable=True
|
|
274
|
+
)
|
|
275
|
+
last_time: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
|
276
|
+
reply_num: Mapped[int] = mapped_column(Integer)
|
|
277
|
+
author_level: Mapped[int] = mapped_column(Integer)
|
|
278
|
+
scrape_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_with_tz)
|
|
279
|
+
|
|
280
|
+
fid: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
281
|
+
author_id: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
282
|
+
|
|
283
|
+
forum: Mapped[Forum] = relationship(
|
|
284
|
+
"Forum",
|
|
285
|
+
back_populates="threads",
|
|
286
|
+
primaryjoin=lambda: foreign(Thread.fid) == Forum.fid,
|
|
287
|
+
)
|
|
288
|
+
author: Mapped[User] = relationship(
|
|
289
|
+
"User",
|
|
290
|
+
back_populates="threads",
|
|
291
|
+
primaryjoin=lambda: foreign(Thread.author_id) == User.user_id,
|
|
292
|
+
)
|
|
293
|
+
posts: Mapped[list[Post]] = relationship(
|
|
294
|
+
"Post",
|
|
295
|
+
back_populates="thread",
|
|
296
|
+
primaryjoin=lambda: Thread.tid == foreign(Post.tid),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def from_dto(cls, dto: ThreadDTO) -> Thread:
|
|
301
|
+
"""从ThreadDTO对象创建Thread模型实例。
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
dto: ThreadDTO对象。
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Thread: 转换后的Thread模型实例。
|
|
308
|
+
"""
|
|
309
|
+
return cls(
|
|
310
|
+
tid=dto.tid,
|
|
311
|
+
create_time=dto.create_time,
|
|
312
|
+
title=dto.title,
|
|
313
|
+
text=dto.text,
|
|
314
|
+
contents=dto.contents,
|
|
315
|
+
last_time=dto.last_time,
|
|
316
|
+
reply_num=dto.reply_num,
|
|
317
|
+
author_level=dto.author.level,
|
|
318
|
+
scrape_time=now_with_tz(),
|
|
319
|
+
fid=dto.fid,
|
|
320
|
+
author_id=dto.author_id,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class Post(MixinBase):
|
|
325
|
+
"""回复数据模型。
|
|
326
|
+
|
|
327
|
+
Attributes:
|
|
328
|
+
pid: 回复pid,与create_time组成复合主键。
|
|
329
|
+
create_time: 回复创建时间,带时区信息,与pid组成复合主键。
|
|
330
|
+
text: 回复的纯文本内容。
|
|
331
|
+
contents: 回复的正文内容碎片列表,以JSONB格式存储。
|
|
332
|
+
floor: 楼层号。
|
|
333
|
+
reply_num: 该回复下的楼中楼数量。
|
|
334
|
+
author_level: 作者在主题贴所在吧的等级。
|
|
335
|
+
scrape_time: 数据抓取时间。
|
|
336
|
+
tid: 所属贴子tid,外键关联到Thread表。
|
|
337
|
+
author_id: 作者user_id,外键关联到User表。
|
|
338
|
+
thread: 所属主题贴对象,与Thread模型的关系。
|
|
339
|
+
author: 作者用户对象,与User模型的关系。
|
|
340
|
+
comments: 该回复下的所有楼中楼,与Comment模型的反向关系。
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
__tablename__ = "post"
|
|
344
|
+
__table_args__ = (
|
|
345
|
+
Index("idx_post_thread_time", "tid", "create_time"),
|
|
346
|
+
Index("idx_post_author_time", "author_id", "create_time"),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
pid: Mapped[int] = mapped_column(BIGINT, primary_key=True)
|
|
350
|
+
create_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True)
|
|
351
|
+
text: Mapped[str] = mapped_column(Text)
|
|
352
|
+
contents: Mapped[list[Fragment] | None] = mapped_column(
|
|
353
|
+
MutableList.as_mutable(FragmentListType(fallback=FragUnknownModel)), nullable=True
|
|
354
|
+
)
|
|
355
|
+
floor: Mapped[int] = mapped_column(Integer)
|
|
356
|
+
reply_num: Mapped[int] = mapped_column(Integer)
|
|
357
|
+
author_level: Mapped[int] = mapped_column(Integer)
|
|
358
|
+
scrape_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_with_tz)
|
|
359
|
+
|
|
360
|
+
tid: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
361
|
+
fid: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
362
|
+
author_id: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
363
|
+
|
|
364
|
+
thread: Mapped[Thread] = relationship(
|
|
365
|
+
"Thread",
|
|
366
|
+
back_populates="posts",
|
|
367
|
+
primaryjoin=lambda: foreign(Post.tid) == Thread.tid,
|
|
368
|
+
)
|
|
369
|
+
author: Mapped[User] = relationship(
|
|
370
|
+
"User",
|
|
371
|
+
back_populates="posts",
|
|
372
|
+
primaryjoin=lambda: foreign(Post.author_id) == User.user_id,
|
|
373
|
+
)
|
|
374
|
+
comments: Mapped[list[Comment]] = relationship(
|
|
375
|
+
"Comment",
|
|
376
|
+
back_populates="post",
|
|
377
|
+
primaryjoin=lambda: Post.pid == foreign(Comment.pid),
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def from_dto(cls, dto: PostDTO) -> Post:
|
|
382
|
+
"""从PostDTO对象创建Post模型实例。
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
dto: PostDTO对象。
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Post: 转换后的Post模型实例。
|
|
389
|
+
"""
|
|
390
|
+
return cls(
|
|
391
|
+
pid=dto.pid,
|
|
392
|
+
create_time=dto.create_time,
|
|
393
|
+
text=dto.text,
|
|
394
|
+
contents=dto.contents,
|
|
395
|
+
floor=dto.floor,
|
|
396
|
+
reply_num=dto.reply_num,
|
|
397
|
+
author_level=dto.author.level,
|
|
398
|
+
scrape_time=now_with_tz(),
|
|
399
|
+
tid=dto.tid,
|
|
400
|
+
fid=dto.fid,
|
|
401
|
+
author_id=dto.author_id,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class Comment(MixinBase):
|
|
406
|
+
"""楼中楼数据模型。
|
|
407
|
+
|
|
408
|
+
Attributes:
|
|
409
|
+
cid: 楼中楼pid,存储为cid以区分,与create_time组成复合主键。
|
|
410
|
+
create_time: 楼中楼创建时间,带时区信息,与cid组成复合主键。
|
|
411
|
+
text: 楼中楼的纯文本内容。
|
|
412
|
+
contents: 楼中楼的正文内容碎片列表,以JSONB格式存储。
|
|
413
|
+
author_level: 作者在主题贴所在吧的等级。
|
|
414
|
+
reply_to_id: 被回复者的user_id,可为空。
|
|
415
|
+
scrape_time: 数据抓取时间。
|
|
416
|
+
pid: 所属回复ID,外键关联到Post表。
|
|
417
|
+
author_id: 作者user_id,外键关联到User表。
|
|
418
|
+
post: 所属回复对象,与Post模型的关系。
|
|
419
|
+
author: 作者用户对象,与User模型的关系。
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
__tablename__ = "comment"
|
|
423
|
+
__table_args__ = (
|
|
424
|
+
Index("idx_comment_post_time", "pid", "create_time"),
|
|
425
|
+
Index("idx_comment_author_time", "author_id", "create_time"),
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
cid: Mapped[int] = mapped_column(BIGINT, primary_key=True)
|
|
429
|
+
create_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), primary_key=True)
|
|
430
|
+
text: Mapped[str] = mapped_column(Text)
|
|
431
|
+
contents: Mapped[list[Fragment] | None] = mapped_column(
|
|
432
|
+
MutableList.as_mutable(FragmentListType(fallback=FragUnknownModel)), nullable=True
|
|
433
|
+
)
|
|
434
|
+
author_level: Mapped[int] = mapped_column(Integer)
|
|
435
|
+
reply_to_id: Mapped[int | None] = mapped_column(BIGINT, nullable=True)
|
|
436
|
+
scrape_time: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_with_tz)
|
|
437
|
+
|
|
438
|
+
pid: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
439
|
+
tid: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
440
|
+
fid: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
441
|
+
author_id: Mapped[int] = mapped_column(BIGINT, index=True)
|
|
442
|
+
|
|
443
|
+
post: Mapped[Post] = relationship(
|
|
444
|
+
"Post",
|
|
445
|
+
back_populates="comments",
|
|
446
|
+
primaryjoin=lambda: foreign(Comment.pid) == Post.pid,
|
|
447
|
+
)
|
|
448
|
+
author: Mapped[User] = relationship(
|
|
449
|
+
"User",
|
|
450
|
+
back_populates="comments",
|
|
451
|
+
primaryjoin=lambda: foreign(Comment.author_id) == User.user_id,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
@classmethod
|
|
455
|
+
def from_dto(cls, dto: CommentDTO) -> Comment:
|
|
456
|
+
"""从CommentDTO对象创建Comment模型实例。
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
dto: CommentDTO对象。
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Comment: 转换后的Comment模型实例。
|
|
463
|
+
"""
|
|
464
|
+
return cls(
|
|
465
|
+
cid=dto.cid,
|
|
466
|
+
create_time=dto.create_time,
|
|
467
|
+
text=dto.text,
|
|
468
|
+
contents=dto.contents,
|
|
469
|
+
author_level=dto.author.level,
|
|
470
|
+
reply_to_id=dto.reply_to_id,
|
|
471
|
+
scrape_time=now_with_tz(),
|
|
472
|
+
pid=dto.pid,
|
|
473
|
+
tid=dto.tid,
|
|
474
|
+
fid=dto.fid,
|
|
475
|
+
author_id=dto.author_id,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class RuleBase(DeclarativeBase):
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class ReviewRules(RuleBase):
|
|
484
|
+
"""审查规则的数据库模型。
|
|
485
|
+
|
|
486
|
+
对应数据库中的 review_rules 表。
|
|
487
|
+
|
|
488
|
+
Attributes:
|
|
489
|
+
id: 主键 ID。
|
|
490
|
+
fid: 贴吧 fid。
|
|
491
|
+
forum_rule_id: 贴吧规则 ID。
|
|
492
|
+
target_type: 规则作用目标类型。
|
|
493
|
+
name: 规则名称。
|
|
494
|
+
enabled: 是否启用。
|
|
495
|
+
priority: 优先级。
|
|
496
|
+
trigger: 触发条件 JSON。
|
|
497
|
+
actions: 动作列表 JSON。
|
|
498
|
+
created_at: 创建时间。
|
|
499
|
+
updated_at: 更新时间。
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
__tablename__ = "review_rules"
|
|
503
|
+
__table_args__ = (
|
|
504
|
+
UniqueConstraint("fid", "forum_rule_id", name="uq_review_rules_fid_forum_rule_id"),
|
|
505
|
+
Index("idx_review_rules_fid_forum_rule_id", "fid", "forum_rule_id"),
|
|
506
|
+
Index("idx_review_rules_fid_enabled_priority", "fid", "enabled", "priority"),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
|
510
|
+
fid: Mapped[int] = mapped_column(BIGINT, nullable=False)
|
|
511
|
+
forum_rule_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
512
|
+
uploader_id: Mapped[int] = mapped_column(BIGINT, default=0, nullable=False)
|
|
513
|
+
target_type: Mapped[TargetType] = mapped_column(
|
|
514
|
+
Enum(TargetType, name="target_type_enum"),
|
|
515
|
+
index=True,
|
|
516
|
+
default=TargetType.ALL,
|
|
517
|
+
nullable=False,
|
|
518
|
+
)
|
|
519
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
520
|
+
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
|
521
|
+
priority: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
|
522
|
+
trigger: Mapped[RuleNode] = mapped_column(RuleNodeType, index=True, nullable=False)
|
|
523
|
+
actions: Mapped[Actions] = mapped_column(ActionsType, nullable=False)
|
|
524
|
+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_with_tz, nullable=False)
|
|
525
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
526
|
+
DateTime(timezone=True), index=True, default=now_with_tz, onupdate=now_with_tz, nullable=False
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
@classmethod
|
|
530
|
+
def from_rule_data(cls, review_rule: ReviewRule) -> ReviewRules:
|
|
531
|
+
"""
|
|
532
|
+
从ReviewRule对象创建ReviewRules模型实例。
|
|
533
|
+
|
|
534
|
+
id 字段将被忽略,由数据库自动生成。
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
review_rule: ReviewRule对象。
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
ReviewRules: 转换后的ReviewRules模型实例。
|
|
541
|
+
"""
|
|
542
|
+
return cls(
|
|
543
|
+
fid=review_rule.fid,
|
|
544
|
+
forum_rule_id=review_rule.forum_rule_id,
|
|
545
|
+
uploader_id=review_rule.uploader_id,
|
|
546
|
+
target_type=review_rule.target_type,
|
|
547
|
+
name=review_rule.name,
|
|
548
|
+
enabled=review_rule.enabled,
|
|
549
|
+
priority=review_rule.priority,
|
|
550
|
+
trigger=review_rule.trigger,
|
|
551
|
+
actions=review_rule.actions,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def to_rule_data(self) -> ReviewRule:
|
|
555
|
+
"""
|
|
556
|
+
将ReviewRules模型实例转换为ReviewRule对象。
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
ReviewRule: 转换后的ReviewRule对象。
|
|
560
|
+
"""
|
|
561
|
+
return ReviewRule(
|
|
562
|
+
id=self.id,
|
|
563
|
+
fid=self.fid,
|
|
564
|
+
forum_rule_id=self.forum_rule_id,
|
|
565
|
+
uploader_id=self.uploader_id,
|
|
566
|
+
target_type=self.target_type,
|
|
567
|
+
name=self.name,
|
|
568
|
+
enabled=self.enabled,
|
|
569
|
+
priority=self.priority,
|
|
570
|
+
trigger=self.trigger,
|
|
571
|
+
actions=self.actions,
|
|
572
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .parser import (
|
|
2
|
+
convert_aiotieba_comment,
|
|
3
|
+
convert_aiotieba_comments,
|
|
4
|
+
convert_aiotieba_commentsp,
|
|
5
|
+
convert_aiotieba_commentuser,
|
|
6
|
+
convert_aiotieba_content_list,
|
|
7
|
+
convert_aiotieba_forum,
|
|
8
|
+
convert_aiotieba_fragment,
|
|
9
|
+
convert_aiotieba_pageinfo,
|
|
10
|
+
convert_aiotieba_post,
|
|
11
|
+
convert_aiotieba_posts,
|
|
12
|
+
convert_aiotieba_postuser,
|
|
13
|
+
convert_aiotieba_share_thread,
|
|
14
|
+
convert_aiotieba_thread,
|
|
15
|
+
convert_aiotieba_threadp,
|
|
16
|
+
convert_aiotieba_threads,
|
|
17
|
+
convert_aiotieba_threaduser,
|
|
18
|
+
convert_aiotieba_tiebauiduser,
|
|
19
|
+
convert_aiotieba_user,
|
|
20
|
+
convert_aiotieba_userinfo,
|
|
21
|
+
)
|
|
22
|
+
from .rule_parser import RuleEngineParser
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"convert_aiotieba_comment",
|
|
26
|
+
"convert_aiotieba_comments",
|
|
27
|
+
"convert_aiotieba_commentsp",
|
|
28
|
+
"convert_aiotieba_commentuser",
|
|
29
|
+
"convert_aiotieba_content_list",
|
|
30
|
+
"convert_aiotieba_forum",
|
|
31
|
+
"convert_aiotieba_fragment",
|
|
32
|
+
"convert_aiotieba_pageinfo",
|
|
33
|
+
"convert_aiotieba_post",
|
|
34
|
+
"convert_aiotieba_posts",
|
|
35
|
+
"convert_aiotieba_postuser",
|
|
36
|
+
"convert_aiotieba_share_thread",
|
|
37
|
+
"convert_aiotieba_thread",
|
|
38
|
+
"convert_aiotieba_threadp",
|
|
39
|
+
"convert_aiotieba_threads",
|
|
40
|
+
"convert_aiotieba_threaduser",
|
|
41
|
+
"convert_aiotieba_tiebauiduser",
|
|
42
|
+
"convert_aiotieba_user",
|
|
43
|
+
"convert_aiotieba_userinfo",
|
|
44
|
+
"RuleEngineParser",
|
|
45
|
+
]
|