logdetective 0.6.0__py3-none-any.whl → 0.9.1__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,515 @@
1
+ import enum
2
+ import datetime
3
+ from typing import Optional
4
+
5
+ import backoff
6
+
7
+ from sqlalchemy import (
8
+ Enum,
9
+ Column,
10
+ BigInteger,
11
+ DateTime,
12
+ String,
13
+ ForeignKey,
14
+ UniqueConstraint,
15
+ desc,
16
+ )
17
+ from sqlalchemy.orm import relationship
18
+ from sqlalchemy.exc import OperationalError
19
+ from logdetective.server.database.base import Base, transaction, DB_MAX_RETRIES
20
+
21
+
22
+ class Forge(str, enum.Enum):
23
+ """List of forges managed by logdetective"""
24
+
25
+ gitlab_com = "https://gitlab.com" # pylint: disable=(invalid-name)
26
+ gitlab_cee_redhat_com = "http://gitlab.cee.redhat.com/" # pylint: disable=(invalid-name)
27
+
28
+
29
+ class GitlabMergeRequestJobs(Base):
30
+ """Store details for the merge request jobs
31
+ which triggered logdetective.
32
+ """
33
+
34
+ __tablename__ = "gitlab_merge_request_jobs"
35
+
36
+ id = Column(BigInteger, primary_key=True)
37
+ forge = Column(Enum(Forge), nullable=False, index=True, comment="The forge name")
38
+ project_id = Column(
39
+ BigInteger,
40
+ nullable=False,
41
+ index=True,
42
+ comment="The project gitlab id",
43
+ )
44
+ mr_iid = Column(
45
+ BigInteger,
46
+ nullable=False,
47
+ index=False,
48
+ comment="The merge request gitlab iid",
49
+ )
50
+ job_id = Column(
51
+ BigInteger,
52
+ nullable=False,
53
+ index=True,
54
+ comment="The job gitlab id",
55
+ )
56
+
57
+ __table_args__ = (
58
+ UniqueConstraint("forge", "job_id", name="uix_forge_job"),
59
+ UniqueConstraint(
60
+ "forge", "project_id", "mr_iid", "job_id", name="uix_mr_project_job"
61
+ ),
62
+ )
63
+
64
+ comment = relationship(
65
+ "Comments", back_populates="merge_request_job", uselist=False
66
+ ) # 1 comment for 1 job
67
+
68
+ request_metrics = relationship("AnalyzeRequestMetrics", back_populates="mr_job")
69
+
70
+ @classmethod
71
+ @backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
72
+ def create(
73
+ cls,
74
+ forge: Forge,
75
+ project_id: int,
76
+ mr_iid: int,
77
+ job_id: int,
78
+ ) -> int:
79
+ """Create a new merge request job entry,
80
+ returns its PostgreSQL id
81
+
82
+ Args:
83
+ forge: forge name
84
+ project_id: forge project id
85
+ mr_iid: merge request forge iid
86
+ job_id: forge job id
87
+ """
88
+ with transaction(commit=True) as session:
89
+ mr = cls()
90
+ mr.forge = forge
91
+ mr.project_id = project_id
92
+ mr.mr_iid = mr_iid
93
+ mr.job_id = job_id
94
+ session.add(mr)
95
+ session.flush()
96
+ return mr.id
97
+
98
+ @classmethod
99
+ def get_by_id(
100
+ cls,
101
+ id_: int,
102
+ ) -> Optional["GitlabMergeRequestJobs"]:
103
+ """Search for a given PostgreSQL id"""
104
+ with transaction(commit=False) as session:
105
+ mr = session.query(cls).filter_by(id=id_).first()
106
+ return mr
107
+
108
+ @classmethod
109
+ def get_by_details(
110
+ cls,
111
+ forge: Forge,
112
+ project_id: int,
113
+ mr_iid: int,
114
+ job_id: int,
115
+ ) -> Optional["GitlabMergeRequestJobs"]:
116
+ """Search for a detailed merge request.
117
+
118
+ Args:
119
+ forge: forge name
120
+ project_id: forge project id
121
+ mr_iid: merge request forge iid
122
+ job_id: forge job id
123
+ """
124
+ with transaction(commit=False) as session:
125
+ mr = (
126
+ session.query(cls)
127
+ .filter_by(
128
+ forge=forge, project_id=project_id, mr_iid=mr_iid, job_id=job_id
129
+ )
130
+ .first()
131
+ )
132
+ return mr
133
+
134
+ @classmethod
135
+ def get_or_create(
136
+ cls,
137
+ forge: Forge,
138
+ project_id: int,
139
+ mr_iid: int,
140
+ job_id: int,
141
+ ) -> Optional["GitlabMergeRequestJobs"]:
142
+ """Search for a detailed merge request
143
+ or create a new one if not found.
144
+
145
+ Args:
146
+ forge: forge name
147
+ project_id: forge project id
148
+ mr_iid: merge request forge iid
149
+ job_id: forge job id
150
+ """
151
+ mr = GitlabMergeRequestJobs.get_by_details(forge, project_id, mr_iid, job_id)
152
+ if mr is None:
153
+ id_ = GitlabMergeRequestJobs.create(forge, project_id, mr_iid, job_id)
154
+ mr = GitlabMergeRequestJobs.get_by_id(id_)
155
+ return mr
156
+
157
+
158
+ class Comments(Base):
159
+ """Store details for comments
160
+ created by logdetective for a merge request job."""
161
+
162
+ __tablename__ = "comments"
163
+
164
+ id = Column(BigInteger, primary_key=True)
165
+ merge_request_job_id = Column(
166
+ BigInteger,
167
+ ForeignKey("gitlab_merge_request_jobs.id"),
168
+ nullable=False,
169
+ unique=True, # 1 comment for 1 job
170
+ index=True,
171
+ comment="The associated merge request job (db) id",
172
+ )
173
+ forge = Column(
174
+ Enum(Forge),
175
+ nullable=False,
176
+ index=True,
177
+ comment="The forge name"
178
+ )
179
+ comment_id = Column(
180
+ String(50), # e.g. 'd5a3ff139356ce33e37e73add446f16869741b50'
181
+ nullable=False,
182
+ index=True,
183
+ comment="The comment gitlab id",
184
+ )
185
+ created_at = Column(
186
+ DateTime, nullable=False, comment="Timestamp when the comment was created"
187
+ )
188
+
189
+ __table_args__ = (
190
+ UniqueConstraint("forge", "comment_id", name="uix_forge_comment_id"),
191
+ )
192
+
193
+ merge_request_job = relationship("GitlabMergeRequestJobs", back_populates="comment")
194
+ reactions = relationship("Reactions", back_populates="comment")
195
+
196
+ @classmethod
197
+ @backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
198
+ def create( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
199
+ cls,
200
+ forge: Forge,
201
+ project_id: int,
202
+ mr_iid: int,
203
+ job_id: int,
204
+ comment_id: str,
205
+ metrics: Optional["AnalyzeRequestMetrics"] = None, # noqa: F821
206
+ ) -> int:
207
+ """Create a new comment id entry,
208
+ returns its PostgreSQL id.
209
+ A Gitlab comment id is unique within a
210
+ gitlab instance.
211
+
212
+ Args:
213
+ forge: forge name
214
+ project_id: forge project id
215
+ mr_iid: merge request forge iid
216
+ job_id: forge job id
217
+ comment_id: forge comment id
218
+ """
219
+ with transaction(commit=True) as session:
220
+ mr_job = GitlabMergeRequestJobs.get_or_create(
221
+ forge, project_id, mr_iid, job_id
222
+ )
223
+
224
+ if metrics:
225
+ metrics.mr_job = mr_job
226
+ session.add(metrics)
227
+
228
+ comment = cls()
229
+ comment.forge = forge
230
+ comment.comment_id = comment_id
231
+ comment.created_at = datetime.datetime.now(datetime.timezone.utc)
232
+ comment.merge_request_job_id = mr_job.id
233
+ session.add(comment)
234
+ session.flush()
235
+ return comment.id
236
+
237
+ @classmethod
238
+ def get_by_id(
239
+ cls,
240
+ id_: int,
241
+ ) -> Optional["Comments"]:
242
+ """Search for a given PostgreSQL id"""
243
+ with transaction(commit=False) as session:
244
+ comment = session.query(cls).filter_by(id=id_).first()
245
+ return comment
246
+
247
+ @classmethod
248
+ def get_by_gitlab_id(
249
+ cls,
250
+ forge: Forge,
251
+ comment_id: str,
252
+ ) -> Optional["Comments"]:
253
+ """Search for a detailed comment
254
+ by its unique forge comment id.
255
+
256
+ Args:
257
+ forge: forge name
258
+ comment_id: forge comment id
259
+ """
260
+ with transaction(commit=False) as session:
261
+ comment = (
262
+ session.query(cls)
263
+ .join(
264
+ GitlabMergeRequestJobs,
265
+ cls.merge_request_job_id == GitlabMergeRequestJobs.id,
266
+ )
267
+ .filter(
268
+ GitlabMergeRequestJobs.forge == forge,
269
+ cls.comment_id == comment_id,
270
+ )
271
+ .first()
272
+ )
273
+
274
+ return comment
275
+
276
+ @classmethod
277
+ def get_latest_comment(
278
+ cls,
279
+ forge: Forge,
280
+ project_id: int,
281
+ mr_iid: int,
282
+ ) -> Optional["Comments"]:
283
+ """Search for the latest comment in the merge request.
284
+
285
+ Args:
286
+ forge: forge name
287
+ project_id: forge project id
288
+ mr_iid: merge request forge iid
289
+ """
290
+ with transaction(commit=False) as session:
291
+ comment = (
292
+ session.query(cls)
293
+ .join(
294
+ GitlabMergeRequestJobs,
295
+ cls.merge_request_job_id == GitlabMergeRequestJobs.id,
296
+ )
297
+ .filter(
298
+ GitlabMergeRequestJobs.forge == forge,
299
+ GitlabMergeRequestJobs.project_id == project_id,
300
+ GitlabMergeRequestJobs.mr_iid == mr_iid,
301
+ )
302
+ .order_by(desc(cls.created_at))
303
+ .first()
304
+ )
305
+
306
+ return comment
307
+
308
+ @classmethod
309
+ def get_mr_comments(
310
+ cls,
311
+ forge: Forge,
312
+ project_id: int,
313
+ mr_iid: int,
314
+ ) -> Optional["Comments"]:
315
+ """Search for all merge request comments.
316
+
317
+ Args:
318
+ forge: forge name
319
+ project_id: forge project id
320
+ mr_iid: merge request forge iid
321
+ """
322
+ with transaction(commit=False) as session:
323
+ comments = (
324
+ session.query(cls)
325
+ .join(
326
+ GitlabMergeRequestJobs,
327
+ cls.merge_request_job_id == GitlabMergeRequestJobs.id,
328
+ )
329
+ .filter(
330
+ GitlabMergeRequestJobs.forge == forge,
331
+ GitlabMergeRequestJobs.project_id == project_id,
332
+ GitlabMergeRequestJobs.mr_iid == mr_iid,
333
+ )
334
+ .order_by(desc(cls.created_at))
335
+ .all()
336
+ )
337
+
338
+ return comments
339
+
340
+ @classmethod
341
+ def get_or_create( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
342
+ cls,
343
+ forge: Forge,
344
+ project_id: int,
345
+ mr_iid: int,
346
+ job_id: int,
347
+ comment_id: str,
348
+ ) -> Optional["Comments"]:
349
+ """Search for a detailed comment
350
+ or create a new one if not found.
351
+
352
+ Args:
353
+ forge: forge name
354
+ project_id: forge project id
355
+ mr_iid: merge request forge iid
356
+ job_id: forge job id
357
+ comment_id: forge comment id
358
+ """
359
+ comment = Comments.get_by_gitlab_id(forge, comment_id)
360
+ if comment is None:
361
+ id_ = Comments.create(forge, project_id, mr_iid, job_id, comment_id)
362
+ comment = GitlabMergeRequestJobs.get_by_id(id_)
363
+ return comment
364
+
365
+
366
+ class Reactions(Base):
367
+ """Store and count reactions received for
368
+ logdetective comments"""
369
+
370
+ __tablename__ = "reactions"
371
+
372
+ id = Column(BigInteger, primary_key=True)
373
+ comment_id = Column(
374
+ BigInteger,
375
+ ForeignKey("comments.id"),
376
+ nullable=False,
377
+ index=True,
378
+ comment="The associated comment (db) id",
379
+ )
380
+ reaction_type = Column(
381
+ String(127), # e.g. 'thumbs-up'
382
+ nullable=False,
383
+ comment="The type of reaction",
384
+ )
385
+ count = Column(
386
+ BigInteger,
387
+ nullable=False,
388
+ comment="The number of reactions, of this type, given in the comment",
389
+ )
390
+
391
+ __table_args__ = (
392
+ UniqueConstraint("comment_id", "reaction_type", name="uix_comment_reaction"),
393
+ )
394
+
395
+ comment = relationship("Comments", back_populates="reactions")
396
+
397
+ @classmethod
398
+ @backoff.on_exception(backoff.expo, OperationalError, max_tries=DB_MAX_RETRIES)
399
+ def create_or_update( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
400
+ cls,
401
+ forge: Forge,
402
+ project_id: int,
403
+ mr_iid: int,
404
+ job_id: int,
405
+ comment_id: str,
406
+ reaction_type: str,
407
+ count: int,
408
+ ) -> int:
409
+ """Create or update the given reaction, and its associated count,
410
+ for a specified comment.
411
+
412
+ Args:
413
+ forge: forge name
414
+ project_id: forge project id
415
+ mr_iid: merge request forge iid
416
+ job_id: forge job id
417
+ comment_id: forge comment id
418
+ reaction_type: a str, ex. thumb_up
419
+ count: number of reactions, of this type, given in the comment
420
+ """
421
+ comment = Comments.get_or_create(forge, project_id, mr_iid, job_id, comment_id)
422
+ reaction = cls.get_reaction_by_type(
423
+ forge, project_id, mr_iid, job_id, comment_id, reaction_type
424
+ )
425
+ with transaction(commit=True) as session:
426
+ if reaction:
427
+ reaction.count = count # just update
428
+ else:
429
+ reaction = cls()
430
+ reaction.comment_id = comment.id
431
+ reaction.reaction_type = reaction_type
432
+ reaction.count = count
433
+ session.add(reaction)
434
+ session.flush()
435
+ return reaction.id
436
+
437
+ @classmethod
438
+ def get_all_reactions( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
439
+ cls,
440
+ forge: Forge,
441
+ project_id: int,
442
+ mr_iid: int,
443
+ job_id: int,
444
+ comment_id: str,
445
+ ) -> int:
446
+ """Get all reactions for a comment
447
+
448
+ Args:
449
+ forge: forge name
450
+ project_id: forge project id
451
+ mr_iid: merge request forge iid
452
+ job_id: forge job id
453
+ comment_id: forge comment id
454
+ """
455
+ with transaction(commit=False) as session:
456
+ reactions = (
457
+ session.query(cls)
458
+ .join(Comments, cls.comment_id == Comments.id)
459
+ .join(
460
+ GitlabMergeRequestJobs,
461
+ Comments.merge_request_job_id == GitlabMergeRequestJobs.id,
462
+ )
463
+ .filter(
464
+ Comments.comment_id == comment_id,
465
+ GitlabMergeRequestJobs.forge == forge,
466
+ GitlabMergeRequestJobs.project_id == project_id,
467
+ GitlabMergeRequestJobs.mr_iid == mr_iid,
468
+ GitlabMergeRequestJobs.job_id == job_id,
469
+ )
470
+ .all()
471
+ )
472
+
473
+ return reactions
474
+
475
+ @classmethod
476
+ def get_reaction_by_type( # pylint: disable=too-many-arguments disable=too-many-positional-arguments
477
+ cls,
478
+ forge: Forge,
479
+ project_id: int,
480
+ mr_iid: int,
481
+ job_id: int,
482
+ comment_id: str,
483
+ reaction_type: str,
484
+ ) -> int:
485
+ """Get reaction, of a given type,
486
+ for a comment
487
+
488
+ Args:
489
+ forge: forge name
490
+ project_id: forge project id
491
+ mr_iid: merge request forge iid
492
+ job_id: forge job id
493
+ comment_id: forge comment id
494
+ reaction_type: str like "thumb-up"
495
+ """
496
+ with transaction(commit=False) as session:
497
+ reaction = (
498
+ session.query(cls)
499
+ .join(Comments, cls.comment_id == Comments.id)
500
+ .join(
501
+ GitlabMergeRequestJobs,
502
+ Comments.merge_request_job_id == GitlabMergeRequestJobs.id,
503
+ )
504
+ .filter(
505
+ Comments.comment_id == comment_id,
506
+ GitlabMergeRequestJobs.forge == forge,
507
+ GitlabMergeRequestJobs.project_id == project_id,
508
+ GitlabMergeRequestJobs.mr_iid == mr_iid,
509
+ GitlabMergeRequestJobs.job_id == job_id,
510
+ Reactions.reaction_type == reaction_type,
511
+ )
512
+ .first()
513
+ )
514
+
515
+ return reaction