logdetective 0.4.0__py3-none-any.whl → 2.11.0__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.
- logdetective/constants.py +33 -12
- logdetective/extractors.py +137 -68
- logdetective/logdetective.py +102 -33
- logdetective/models.py +99 -0
- logdetective/prompts-summary-first.yml +20 -0
- logdetective/prompts-summary-only.yml +13 -0
- logdetective/prompts.yml +90 -0
- logdetective/remote_log.py +67 -0
- logdetective/server/compressors.py +186 -0
- logdetective/server/config.py +78 -0
- logdetective/server/database/base.py +34 -26
- logdetective/server/database/models/__init__.py +33 -0
- logdetective/server/database/models/exceptions.py +17 -0
- logdetective/server/database/models/koji.py +143 -0
- logdetective/server/database/models/merge_request_jobs.py +623 -0
- logdetective/server/database/models/metrics.py +427 -0
- logdetective/server/emoji.py +148 -0
- logdetective/server/exceptions.py +37 -0
- logdetective/server/gitlab.py +451 -0
- logdetective/server/koji.py +159 -0
- logdetective/server/llm.py +309 -0
- logdetective/server/metric.py +75 -30
- logdetective/server/models.py +426 -23
- logdetective/server/plot.py +432 -0
- logdetective/server/server.py +580 -468
- logdetective/server/templates/base_response.html.j2 +59 -0
- logdetective/server/templates/gitlab_full_comment.md.j2 +73 -0
- logdetective/server/templates/gitlab_short_comment.md.j2 +62 -0
- logdetective/server/utils.py +98 -32
- logdetective/skip_snippets.yml +12 -0
- logdetective/utils.py +187 -73
- logdetective-2.11.0.dist-info/METADATA +568 -0
- logdetective-2.11.0.dist-info/RECORD +40 -0
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info}/WHEEL +1 -1
- logdetective/server/database/models.py +0 -88
- logdetective-0.4.0.dist-info/METADATA +0 -333
- logdetective-0.4.0.dist-info/RECORD +0 -19
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info}/entry_points.txt +0 -0
- {logdetective-0.4.0.dist-info → logdetective-2.11.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import enum
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import Optional, List, Tuple, Self, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import backoff
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import (
|
|
9
|
+
Enum,
|
|
10
|
+
BigInteger,
|
|
11
|
+
DateTime,
|
|
12
|
+
String,
|
|
13
|
+
ForeignKey,
|
|
14
|
+
UniqueConstraint,
|
|
15
|
+
desc,
|
|
16
|
+
select,
|
|
17
|
+
)
|
|
18
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
19
|
+
from sqlalchemy.engine import Row
|
|
20
|
+
from sqlalchemy.exc import OperationalError
|
|
21
|
+
from logdetective.server.database.base import Base, transaction, DB_MAX_RETRIES
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from .metrics import AnalyzeRequestMetrics
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Forge(str, enum.Enum):
|
|
29
|
+
"""List of forges managed by logdetective"""
|
|
30
|
+
|
|
31
|
+
gitlab_com = "https://gitlab.com" # pylint: disable=(invalid-name)
|
|
32
|
+
gitlab_cee_redhat_com = "http://gitlab.cee.redhat.com/" # pylint: disable=(invalid-name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GitlabMergeRequestJobs(Base):
|
|
36
|
+
"""Store details for the merge request jobs
|
|
37
|
+
which triggered logdetective.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
__tablename__ = "gitlab_merge_request_jobs"
|
|
41
|
+
|
|
42
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
43
|
+
forge: Mapped[Forge] = mapped_column(
|
|
44
|
+
Enum(Forge),
|
|
45
|
+
nullable=False,
|
|
46
|
+
index=True,
|
|
47
|
+
comment="The forge name"
|
|
48
|
+
)
|
|
49
|
+
project_id: Mapped[int] = mapped_column(
|
|
50
|
+
BigInteger,
|
|
51
|
+
nullable=False,
|
|
52
|
+
index=True,
|
|
53
|
+
comment="The project gitlab id",
|
|
54
|
+
)
|
|
55
|
+
mr_iid: Mapped[int] = mapped_column(
|
|
56
|
+
BigInteger,
|
|
57
|
+
nullable=False,
|
|
58
|
+
index=False,
|
|
59
|
+
comment="The merge request gitlab iid",
|
|
60
|
+
)
|
|
61
|
+
job_id: Mapped[int] = mapped_column(
|
|
62
|
+
BigInteger,
|
|
63
|
+
nullable=False,
|
|
64
|
+
index=True,
|
|
65
|
+
comment="The job gitlab id",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
__table_args__ = (
|
|
69
|
+
UniqueConstraint("forge", "job_id", name="uix_forge_job"),
|
|
70
|
+
UniqueConstraint(
|
|
71
|
+
"forge", "project_id", "mr_iid", "job_id", name="uix_mr_project_job"
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
comment: Mapped[List["Comments"]] = relationship(
|
|
76
|
+
"Comments", back_populates="merge_request_job", uselist=False
|
|
77
|
+
) # 1 comment for 1 job
|
|
78
|
+
|
|
79
|
+
request_metrics: Mapped[List["AnalyzeRequestMetrics"]] = relationship(
|
|
80
|
+
"AnalyzeRequestMetrics",
|
|
81
|
+
back_populates="mr_job"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
@backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
|
|
86
|
+
async def create(
|
|
87
|
+
cls,
|
|
88
|
+
forge: Forge,
|
|
89
|
+
project_id: int,
|
|
90
|
+
mr_iid: int,
|
|
91
|
+
job_id: int,
|
|
92
|
+
) -> int:
|
|
93
|
+
"""Create a new merge request job entry,
|
|
94
|
+
returns its PostgreSQL id
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
forge: forge name
|
|
98
|
+
project_id: forge project id
|
|
99
|
+
mr_iid: merge request forge iid
|
|
100
|
+
job_id: forge job id
|
|
101
|
+
"""
|
|
102
|
+
async with transaction(commit=True) as session:
|
|
103
|
+
mr = cls()
|
|
104
|
+
mr.forge = forge
|
|
105
|
+
mr.project_id = project_id
|
|
106
|
+
mr.mr_iid = mr_iid
|
|
107
|
+
mr.job_id = job_id
|
|
108
|
+
session.add(mr)
|
|
109
|
+
await session.flush()
|
|
110
|
+
return mr.id
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
async def get_by_id(
|
|
114
|
+
cls,
|
|
115
|
+
id_: int,
|
|
116
|
+
) -> Optional["GitlabMergeRequestJobs"]:
|
|
117
|
+
"""Search for a given PostgreSQL id"""
|
|
118
|
+
query = select(cls).where(cls.id == id_)
|
|
119
|
+
async with transaction(commit=False) as session:
|
|
120
|
+
query_result = await session.execute(query)
|
|
121
|
+
mr = query_result.scalars().first()
|
|
122
|
+
return mr
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
async def get_by_details(
|
|
126
|
+
cls,
|
|
127
|
+
forge: Forge,
|
|
128
|
+
project_id: int,
|
|
129
|
+
mr_iid: int,
|
|
130
|
+
job_id: int,
|
|
131
|
+
) -> Optional["GitlabMergeRequestJobs"]:
|
|
132
|
+
"""Search for a detailed merge request.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
forge: forge name
|
|
136
|
+
project_id: forge project id
|
|
137
|
+
mr_iid: merge request forge iid
|
|
138
|
+
job_id: forge job id
|
|
139
|
+
"""
|
|
140
|
+
query = select(cls).where(
|
|
141
|
+
cls.forge == forge,
|
|
142
|
+
cls.project_id == project_id,
|
|
143
|
+
cls.mr_iid == mr_iid,
|
|
144
|
+
cls.job_id == job_id,
|
|
145
|
+
)
|
|
146
|
+
async with transaction(commit=False) as session:
|
|
147
|
+
query_result = await session.execute(query)
|
|
148
|
+
mr = query_result.scalars().first()
|
|
149
|
+
return mr
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
async def get_by_mr_iid(cls, forge: Forge, project_id: int, mr_iid) -> List[Self]:
|
|
153
|
+
"""Get all the mr jobs saved for the specified mr iid and project id."""
|
|
154
|
+
query = select(cls).where(
|
|
155
|
+
GitlabMergeRequestJobs.forge == forge,
|
|
156
|
+
GitlabMergeRequestJobs.project_id == project_id,
|
|
157
|
+
GitlabMergeRequestJobs.mr_iid == mr_iid,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
async with transaction(commit=False) as session:
|
|
161
|
+
query_result = await session.execute(query)
|
|
162
|
+
comments = query_result.scalars().all()
|
|
163
|
+
|
|
164
|
+
return comments
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
async def get_or_create(
|
|
168
|
+
cls,
|
|
169
|
+
forge: Forge,
|
|
170
|
+
project_id: int,
|
|
171
|
+
mr_iid: int,
|
|
172
|
+
job_id: int,
|
|
173
|
+
) -> Self:
|
|
174
|
+
"""Search for a detailed merge request
|
|
175
|
+
or create a new one if not found.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
forge: forge name
|
|
179
|
+
project_id: forge project id
|
|
180
|
+
mr_iid: merge request forge iid
|
|
181
|
+
job_id: forge job id
|
|
182
|
+
"""
|
|
183
|
+
mr = await GitlabMergeRequestJobs.get_by_details(
|
|
184
|
+
forge, project_id, mr_iid, job_id
|
|
185
|
+
)
|
|
186
|
+
if mr is None:
|
|
187
|
+
id_ = await GitlabMergeRequestJobs.create(forge, project_id, mr_iid, job_id)
|
|
188
|
+
mr = await GitlabMergeRequestJobs.get_by_id(id_)
|
|
189
|
+
return mr
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Comments(Base):
|
|
193
|
+
"""Store details for comments
|
|
194
|
+
created by logdetective for a merge request job."""
|
|
195
|
+
|
|
196
|
+
__tablename__ = "comments"
|
|
197
|
+
|
|
198
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
199
|
+
merge_request_job_id: Mapped[int] = mapped_column(
|
|
200
|
+
BigInteger,
|
|
201
|
+
ForeignKey("gitlab_merge_request_jobs.id"),
|
|
202
|
+
nullable=False,
|
|
203
|
+
unique=True, # 1 comment for 1 job
|
|
204
|
+
index=True,
|
|
205
|
+
comment="The associated merge request job (db) id",
|
|
206
|
+
)
|
|
207
|
+
forge: Mapped[Forge] = mapped_column(
|
|
208
|
+
Enum(Forge),
|
|
209
|
+
nullable=False,
|
|
210
|
+
index=True,
|
|
211
|
+
comment="The forge name"
|
|
212
|
+
)
|
|
213
|
+
comment_id: Mapped[str] = mapped_column(
|
|
214
|
+
String(50), # e.g. 'd5a3ff139356ce33e37e73add446f16869741b50'
|
|
215
|
+
nullable=False,
|
|
216
|
+
index=True,
|
|
217
|
+
comment="The comment gitlab id",
|
|
218
|
+
)
|
|
219
|
+
created_at: Mapped[datetime.datetime] = mapped_column(
|
|
220
|
+
DateTime(timezone=True),
|
|
221
|
+
nullable=False,
|
|
222
|
+
comment="Timestamp when the comment was created",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
__table_args__ = (
|
|
226
|
+
UniqueConstraint("forge", "comment_id", name="uix_forge_comment_id"),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
merge_request_job: Mapped["GitlabMergeRequestJobs"] = relationship(
|
|
230
|
+
"GitlabMergeRequestJobs",
|
|
231
|
+
back_populates="comment"
|
|
232
|
+
)
|
|
233
|
+
reactions: Mapped[list["Reactions"]] = relationship("Reactions", back_populates="comment")
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
@backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
|
|
237
|
+
async def create( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
238
|
+
cls,
|
|
239
|
+
forge: Forge,
|
|
240
|
+
project_id: int,
|
|
241
|
+
mr_iid: int,
|
|
242
|
+
job_id: int,
|
|
243
|
+
comment_id: str,
|
|
244
|
+
metrics: Optional["AnalyzeRequestMetrics"] = None, # noqa: F821
|
|
245
|
+
) -> int:
|
|
246
|
+
"""Create a new comment id entry,
|
|
247
|
+
returns its PostgreSQL id.
|
|
248
|
+
A Gitlab comment id is unique within a
|
|
249
|
+
gitlab instance.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
forge: forge name
|
|
253
|
+
project_id: forge project id
|
|
254
|
+
mr_iid: merge request forge iid
|
|
255
|
+
job_id: forge job id
|
|
256
|
+
comment_id: forge comment id
|
|
257
|
+
"""
|
|
258
|
+
async with transaction(commit=True) as session:
|
|
259
|
+
mr_job = await GitlabMergeRequestJobs.get_or_create(
|
|
260
|
+
forge, project_id, mr_iid, job_id
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if metrics:
|
|
264
|
+
metrics.mr_job = mr_job
|
|
265
|
+
session.add(metrics)
|
|
266
|
+
|
|
267
|
+
comment = cls()
|
|
268
|
+
comment.forge = forge
|
|
269
|
+
comment.comment_id = comment_id
|
|
270
|
+
comment.created_at = datetime.datetime.now(datetime.timezone.utc)
|
|
271
|
+
comment.merge_request_job_id = mr_job.id
|
|
272
|
+
session.add(comment)
|
|
273
|
+
await session.flush()
|
|
274
|
+
return comment.id
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
async def get_by_id(
|
|
278
|
+
cls,
|
|
279
|
+
id_: int,
|
|
280
|
+
) -> Optional["Comments"]:
|
|
281
|
+
"""Search for a given PostgreSQL id"""
|
|
282
|
+
query = select(cls).where(cls.id == id_)
|
|
283
|
+
async with transaction(commit=False) as session:
|
|
284
|
+
query_result = await session.execute(query)
|
|
285
|
+
comment = query_result.scalars().first()
|
|
286
|
+
return comment
|
|
287
|
+
|
|
288
|
+
@classmethod
|
|
289
|
+
async def get_by_gitlab_id(
|
|
290
|
+
cls,
|
|
291
|
+
forge: Forge,
|
|
292
|
+
comment_id: str,
|
|
293
|
+
) -> Optional[Self]:
|
|
294
|
+
"""Search for a detailed comment
|
|
295
|
+
by its unique forge comment id.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
forge: forge name
|
|
299
|
+
comment_id: forge comment id
|
|
300
|
+
"""
|
|
301
|
+
query = (
|
|
302
|
+
select(cls)
|
|
303
|
+
.join(
|
|
304
|
+
GitlabMergeRequestJobs,
|
|
305
|
+
cls.merge_request_job_id == GitlabMergeRequestJobs.id,
|
|
306
|
+
)
|
|
307
|
+
.filter(GitlabMergeRequestJobs.forge == forge, cls.comment_id == comment_id)
|
|
308
|
+
)
|
|
309
|
+
async with transaction(commit=False) as session:
|
|
310
|
+
query_result = await session.execute(query)
|
|
311
|
+
comment = query_result.scalars().first()
|
|
312
|
+
return comment
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
async def get_latest_comment(
|
|
316
|
+
cls,
|
|
317
|
+
forge: Forge,
|
|
318
|
+
project_id: int,
|
|
319
|
+
mr_iid: int,
|
|
320
|
+
) -> Optional["Comments"]:
|
|
321
|
+
"""Search for the latest comment in the merge request.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
forge: forge name
|
|
325
|
+
project_id: forge project id
|
|
326
|
+
mr_iid: merge request forge iid
|
|
327
|
+
"""
|
|
328
|
+
query = (
|
|
329
|
+
select(cls)
|
|
330
|
+
.join(
|
|
331
|
+
GitlabMergeRequestJobs,
|
|
332
|
+
cls.merge_request_job_id == GitlabMergeRequestJobs.id,
|
|
333
|
+
)
|
|
334
|
+
.filter(
|
|
335
|
+
GitlabMergeRequestJobs.forge == forge,
|
|
336
|
+
GitlabMergeRequestJobs.project_id == project_id,
|
|
337
|
+
GitlabMergeRequestJobs.mr_iid == mr_iid,
|
|
338
|
+
)
|
|
339
|
+
.order_by(desc(cls.created_at))
|
|
340
|
+
)
|
|
341
|
+
async with transaction(commit=False) as session:
|
|
342
|
+
query_result = await session.execute(query)
|
|
343
|
+
comment = query_result.scalars().first()
|
|
344
|
+
return comment
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
async def get_mr_comments(
|
|
348
|
+
cls,
|
|
349
|
+
forge: Forge,
|
|
350
|
+
project_id: int,
|
|
351
|
+
mr_iid: int,
|
|
352
|
+
) -> List[Self]:
|
|
353
|
+
"""Search for all merge request comments.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
forge: forge name
|
|
357
|
+
project_id: forge project id
|
|
358
|
+
mr_iid: merge request forge iid
|
|
359
|
+
"""
|
|
360
|
+
query = (
|
|
361
|
+
select(cls)
|
|
362
|
+
.join(
|
|
363
|
+
GitlabMergeRequestJobs,
|
|
364
|
+
cls.merge_request_job_id == GitlabMergeRequestJobs.id,
|
|
365
|
+
)
|
|
366
|
+
.filter(
|
|
367
|
+
GitlabMergeRequestJobs.forge == forge,
|
|
368
|
+
GitlabMergeRequestJobs.project_id == project_id,
|
|
369
|
+
GitlabMergeRequestJobs.mr_iid == mr_iid,
|
|
370
|
+
)
|
|
371
|
+
.order_by(desc(cls.created_at))
|
|
372
|
+
)
|
|
373
|
+
async with transaction(commit=False) as session:
|
|
374
|
+
query_result = await session.execute(query)
|
|
375
|
+
comments = query_result.scalars().all()
|
|
376
|
+
return comments
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
async def get_or_create( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
380
|
+
cls,
|
|
381
|
+
forge: Forge,
|
|
382
|
+
project_id: int,
|
|
383
|
+
mr_iid: int,
|
|
384
|
+
job_id: int,
|
|
385
|
+
comment_id: str,
|
|
386
|
+
) -> Self:
|
|
387
|
+
"""Search for a detailed comment
|
|
388
|
+
or create a new one if not found.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
forge: forge name
|
|
392
|
+
project_id: forge project id
|
|
393
|
+
mr_iid: merge request forge iid
|
|
394
|
+
job_id: forge job id
|
|
395
|
+
comment_id: forge comment id
|
|
396
|
+
"""
|
|
397
|
+
comment = await Comments.get_by_gitlab_id(forge, comment_id)
|
|
398
|
+
if comment is None:
|
|
399
|
+
id_ = await Comments.create(forge, project_id, mr_iid, job_id, comment_id)
|
|
400
|
+
comment = await Comments.get_by_id(id_)
|
|
401
|
+
return comment
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
async def get_since(cls, time: datetime.datetime) -> List[Self]:
|
|
405
|
+
"""Get all the comments created after the given time."""
|
|
406
|
+
query = select(cls).filter(Comments.created_at > time)
|
|
407
|
+
async with transaction(commit=False) as session:
|
|
408
|
+
query_result = await session.execute(query)
|
|
409
|
+
comments = query_result.scalars().all()
|
|
410
|
+
|
|
411
|
+
return comments
|
|
412
|
+
|
|
413
|
+
@classmethod
|
|
414
|
+
async def get_by_mr_job(
|
|
415
|
+
cls, merge_request_job: GitlabMergeRequestJobs
|
|
416
|
+
) -> Optional["Comments"]:
|
|
417
|
+
"""Get the comment added for the specified merge request's job."""
|
|
418
|
+
query = select(cls).filter(Comments.merge_request_job == merge_request_job)
|
|
419
|
+
async with transaction(commit=False) as session:
|
|
420
|
+
query_result = await session.execute(query)
|
|
421
|
+
comments = query_result.scalars().first()
|
|
422
|
+
return comments
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class Reactions(Base):
|
|
426
|
+
"""Store and count reactions received for
|
|
427
|
+
logdetective comments"""
|
|
428
|
+
|
|
429
|
+
__tablename__ = "reactions"
|
|
430
|
+
|
|
431
|
+
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
432
|
+
comment_id: Mapped[int] = mapped_column(
|
|
433
|
+
BigInteger,
|
|
434
|
+
ForeignKey("comments.id"),
|
|
435
|
+
nullable=False,
|
|
436
|
+
index=True,
|
|
437
|
+
comment="The associated comment (db) id",
|
|
438
|
+
)
|
|
439
|
+
reaction_type: Mapped[str] = mapped_column(
|
|
440
|
+
String(127), # e.g. 'thumbs-up'
|
|
441
|
+
nullable=False,
|
|
442
|
+
comment="The type of reaction",
|
|
443
|
+
)
|
|
444
|
+
count: Mapped[int] = mapped_column(
|
|
445
|
+
BigInteger,
|
|
446
|
+
nullable=False,
|
|
447
|
+
comment="The number of reactions, of this type, given in the comment",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
__table_args__ = (
|
|
451
|
+
UniqueConstraint("comment_id", "reaction_type", name="uix_comment_reaction"),
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
comment: Mapped["Comments"] = relationship("Comments", back_populates="reactions")
|
|
455
|
+
|
|
456
|
+
@classmethod
|
|
457
|
+
@backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
|
|
458
|
+
async def create_or_update( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
459
|
+
cls,
|
|
460
|
+
forge: Forge,
|
|
461
|
+
project_id: int,
|
|
462
|
+
mr_iid: int,
|
|
463
|
+
job_id: int,
|
|
464
|
+
comment_id: str,
|
|
465
|
+
reaction_type: str,
|
|
466
|
+
count: int,
|
|
467
|
+
) -> int:
|
|
468
|
+
"""Create or update the given reaction, and its associated count,
|
|
469
|
+
for a specified comment.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
forge: forge name
|
|
473
|
+
project_id: forge project id
|
|
474
|
+
mr_iid: merge request forge iid
|
|
475
|
+
job_id: forge job id
|
|
476
|
+
comment_id: forge comment id
|
|
477
|
+
reaction_type: a str, ex. thumb_up
|
|
478
|
+
count: number of reactions, of this type, given in the comment
|
|
479
|
+
"""
|
|
480
|
+
comment = await Comments.get_or_create(
|
|
481
|
+
forge, project_id, mr_iid, job_id, comment_id
|
|
482
|
+
)
|
|
483
|
+
reaction = await cls.get_reaction_by_type(
|
|
484
|
+
forge, project_id, mr_iid, job_id, comment_id, reaction_type
|
|
485
|
+
)
|
|
486
|
+
async with transaction(commit=True) as session:
|
|
487
|
+
if reaction:
|
|
488
|
+
reaction.count = count # just update
|
|
489
|
+
else:
|
|
490
|
+
reaction = cls()
|
|
491
|
+
reaction.comment_id = comment.id
|
|
492
|
+
reaction.reaction_type = reaction_type
|
|
493
|
+
reaction.count = count
|
|
494
|
+
session.add(reaction)
|
|
495
|
+
await session.flush()
|
|
496
|
+
return reaction.id
|
|
497
|
+
|
|
498
|
+
@classmethod
|
|
499
|
+
async def get_all_reactions( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
500
|
+
cls,
|
|
501
|
+
forge: Forge,
|
|
502
|
+
project_id: int,
|
|
503
|
+
mr_iid: int,
|
|
504
|
+
job_id: int,
|
|
505
|
+
comment_id: str,
|
|
506
|
+
) -> List[Self]:
|
|
507
|
+
"""Get all reactions for a comment
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
forge: forge name
|
|
511
|
+
project_id: forge project id
|
|
512
|
+
mr_iid: merge request forge iid
|
|
513
|
+
job_id: forge job id
|
|
514
|
+
comment_id: forge comment id
|
|
515
|
+
"""
|
|
516
|
+
query = (
|
|
517
|
+
select(cls)
|
|
518
|
+
.join(Comments, cls.comment_id == Comments.id)
|
|
519
|
+
.join(
|
|
520
|
+
GitlabMergeRequestJobs,
|
|
521
|
+
Comments.merge_request_job_id == GitlabMergeRequestJobs.id,
|
|
522
|
+
)
|
|
523
|
+
.filter(
|
|
524
|
+
Comments.comment_id == comment_id,
|
|
525
|
+
GitlabMergeRequestJobs.forge == forge,
|
|
526
|
+
GitlabMergeRequestJobs.project_id == project_id,
|
|
527
|
+
GitlabMergeRequestJobs.mr_iid == mr_iid,
|
|
528
|
+
GitlabMergeRequestJobs.job_id == job_id,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
async with transaction(commit=False) as session:
|
|
532
|
+
query_result = await session.execute(query)
|
|
533
|
+
reactions = query_result.scalars().all()
|
|
534
|
+
return reactions
|
|
535
|
+
|
|
536
|
+
@classmethod
|
|
537
|
+
async def get_reaction_by_type( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
538
|
+
cls,
|
|
539
|
+
forge: Forge,
|
|
540
|
+
project_id: int,
|
|
541
|
+
mr_iid: int,
|
|
542
|
+
job_id: int,
|
|
543
|
+
comment_id: str,
|
|
544
|
+
reaction_type: str,
|
|
545
|
+
) -> Self | None:
|
|
546
|
+
"""Get reaction, of a given type,
|
|
547
|
+
for a comment
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
forge: forge name
|
|
551
|
+
project_id: forge project id
|
|
552
|
+
mr_iid: merge request forge iid
|
|
553
|
+
job_id: forge job id
|
|
554
|
+
comment_id: forge comment id
|
|
555
|
+
reaction_type: str like "thumb-up"
|
|
556
|
+
"""
|
|
557
|
+
query = (
|
|
558
|
+
select(cls)
|
|
559
|
+
.join(Comments, cls.comment_id == Comments.id)
|
|
560
|
+
.join(
|
|
561
|
+
GitlabMergeRequestJobs,
|
|
562
|
+
Comments.merge_request_job_id == GitlabMergeRequestJobs.id,
|
|
563
|
+
)
|
|
564
|
+
.filter(
|
|
565
|
+
Comments.comment_id == comment_id,
|
|
566
|
+
GitlabMergeRequestJobs.forge == forge,
|
|
567
|
+
GitlabMergeRequestJobs.project_id == project_id,
|
|
568
|
+
GitlabMergeRequestJobs.mr_iid == mr_iid,
|
|
569
|
+
GitlabMergeRequestJobs.job_id == job_id,
|
|
570
|
+
Reactions.reaction_type == reaction_type,
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
async with transaction(commit=False) as session:
|
|
574
|
+
query_result = await session.execute(query)
|
|
575
|
+
reaction = query_result.scalars().first()
|
|
576
|
+
return reaction
|
|
577
|
+
|
|
578
|
+
@classmethod
|
|
579
|
+
@backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
|
|
580
|
+
async def delete( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
|
|
581
|
+
cls,
|
|
582
|
+
forge: Forge,
|
|
583
|
+
project_id: int,
|
|
584
|
+
mr_iid: int,
|
|
585
|
+
job_id: int,
|
|
586
|
+
comment_id: str,
|
|
587
|
+
reaction_types: List[str],
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Remove rows with given reaction types
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
forge: forge name
|
|
593
|
+
project_id: forge project id
|
|
594
|
+
mr_iid: merge request forge iid
|
|
595
|
+
job_id: forge job id
|
|
596
|
+
comment_id: forge comment id
|
|
597
|
+
reaction_type: a str iterable, ex. ['thumbsup', 'thumbsdown']
|
|
598
|
+
"""
|
|
599
|
+
for reaction_type in reaction_types:
|
|
600
|
+
reaction = await cls.get_reaction_by_type(
|
|
601
|
+
forge, project_id, mr_iid, job_id, comment_id, reaction_type
|
|
602
|
+
)
|
|
603
|
+
async with transaction(commit=True) as session:
|
|
604
|
+
await session.delete(reaction)
|
|
605
|
+
await session.flush()
|
|
606
|
+
|
|
607
|
+
@classmethod
|
|
608
|
+
async def get_since(
|
|
609
|
+
cls, time: datetime.datetime
|
|
610
|
+
) -> List[Row[Tuple[datetime.datetime, Self]]]:
|
|
611
|
+
"""Get all the reactions on comments created after the given time
|
|
612
|
+
and the comment creation time."""
|
|
613
|
+
query = (
|
|
614
|
+
select(Comments.created_at, cls)
|
|
615
|
+
.join(Comments, cls.comment_id == Comments.id)
|
|
616
|
+
.filter(Comments.created_at > time)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
async with transaction(commit=False) as session:
|
|
620
|
+
query_results = await session.execute(query)
|
|
621
|
+
reactions = query_results.all()
|
|
622
|
+
|
|
623
|
+
return reactions
|